feat: render hourly investment states
This commit was merged in pull request #51.
This commit is contained in:
@@ -46,9 +46,10 @@ type PreviewResource<T> = {
|
||||
isError: boolean;
|
||||
meta?: Pick<InvestmentResourceMeta,
|
||||
"availability"
|
||||
| "completeness"
|
||||
| "freshness"
|
||||
| "lastAttempt"
|
||||
| "lastSuccessfulAt"
|
||||
| "sync"
|
||||
>;
|
||||
};
|
||||
|
||||
@@ -57,10 +58,10 @@ function toIncomeSummary(
|
||||
income: PreviewResource<IncomeSummary>,
|
||||
): AccountPageIncomeSummary {
|
||||
const to = `/accounts/${accountId}/income`;
|
||||
if (!income.data && !income.isError && income.meta?.sync.state !== "failed")
|
||||
if (!income.data && !income.isError && income.meta?.lastAttempt.state !== "failed")
|
||||
return { state: "loading", to };
|
||||
if (income.meta?.availability === "partial") return { state: "partial", to };
|
||||
if (!income.data && (income.isError || income.meta?.sync.state === "failed"))
|
||||
if (income.meta?.completeness === "incomplete") return { state: "partial", to };
|
||||
if (!income.data && (income.isError || income.meta?.lastAttempt.state === "failed"))
|
||||
return { state: "error", to };
|
||||
if (!income.data?.totalsByCurrency.length) return { state: "empty", to };
|
||||
|
||||
@@ -76,15 +77,15 @@ function toRecentEvents(
|
||||
events: PreviewResource<{ events: BrokerageEvent[] }>,
|
||||
): AccountPageRecentEvents {
|
||||
const to = `/accounts/${accountId}/events`;
|
||||
if (!events.data && !events.isError && events.meta?.sync.state !== "failed")
|
||||
if (!events.data && !events.isError && events.meta?.lastAttempt.state !== "failed")
|
||||
return { state: "loading", to };
|
||||
if (events.meta?.availability === "partial")
|
||||
if (events.meta?.completeness === "incomplete")
|
||||
return {
|
||||
items: events.data?.events.slice(0, RECENT_EVENTS_LIMIT) ?? [],
|
||||
state: "partial",
|
||||
to,
|
||||
};
|
||||
if (!events.data && (events.isError || events.meta?.sync.state === "failed"))
|
||||
if (!events.data && (events.isError || events.meta?.lastAttempt.state === "failed"))
|
||||
return { state: "error", to };
|
||||
const items = events.data?.events.slice(0, RECENT_EVENTS_LIMIT) ?? [];
|
||||
if (items.length === 0) return { state: "empty", to };
|
||||
@@ -104,14 +105,14 @@ function toRecentOperations(
|
||||
if (
|
||||
!operations.data
|
||||
&& !operations.isError
|
||||
&& operations.meta?.sync.state !== "failed"
|
||||
&& operations.meta?.lastAttempt.state !== "failed"
|
||||
)
|
||||
return { state: "loading", to };
|
||||
if (operations.meta?.availability === "partial")
|
||||
if (operations.meta?.completeness === "incomplete")
|
||||
return { items: operations.data?.items ?? [], state: "partial", to };
|
||||
if (
|
||||
!operations.data
|
||||
&& (operations.isError || operations.meta?.sync.state === "failed")
|
||||
&& (operations.isError || operations.meta?.lastAttempt.state === "failed")
|
||||
)
|
||||
return { state: "error", to };
|
||||
if (!operations.data?.items.length) return { state: "empty", to };
|
||||
@@ -120,7 +121,7 @@ function toRecentOperations(
|
||||
items: operations.data.items,
|
||||
lastSuccessfulAt: operations.meta?.lastSuccessfulAt,
|
||||
refreshError:
|
||||
operations.isError || operations.meta?.sync.state === "failed",
|
||||
operations.isError || operations.meta?.lastAttempt.state === "failed",
|
||||
stale: operations.meta?.freshness === "stale",
|
||||
state: "loaded",
|
||||
to,
|
||||
|
||||
@@ -173,14 +173,14 @@ it("keeps overview sections independent when their source data is incomplete", a
|
||||
mockLoadedAccountPage();
|
||||
server.use(
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/portfolio", () =>
|
||||
HttpResponse.json(resource(portfolio, { availability: "partial" })),
|
||||
HttpResponse.json(resource(portfolio, { completeness: "incomplete" })),
|
||||
),
|
||||
http.get("*/api/v1/brokerage-accounts/:accountId/operations", () =>
|
||||
HttpResponse.json(
|
||||
|
||||
resource(
|
||||
{ hasNext: false, items: [], nextCursor: null },
|
||||
{ availability: "partial" },
|
||||
{ completeness: "incomplete" },
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -188,7 +188,7 @@ it("keeps overview sections independent when their source data is incomplete", a
|
||||
HttpResponse.json(
|
||||
resource(
|
||||
{ events: [], incomeTotalsByCurrency: [] },
|
||||
{ availability: "partial" },
|
||||
{ completeness: "incomplete" },
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useParams } from "react-router";
|
||||
|
||||
import type { Portfolio } from "@/entities/portfolio";
|
||||
import type { InvestmentResourceMeta } from "@/shared/api";
|
||||
|
||||
import { useBrokerageOperations } from "@/entities/broker-operation";
|
||||
import {
|
||||
@@ -45,6 +46,7 @@ type AccountPageEmptyViewModel = AccountPageContent & {
|
||||
type AccountPageLoadedViewModel = AccountPageContent & {
|
||||
portfolio: Portfolio;
|
||||
positionSections: AccountPagePositionSection[];
|
||||
source?: InvestmentResourceMeta;
|
||||
state: "loaded";
|
||||
};
|
||||
|
||||
@@ -87,7 +89,7 @@ function useAccountPageViewModel(): AccountPageViewModel {
|
||||
const availability = useInitialAvailability({
|
||||
hasContent: !!portfolio.data,
|
||||
identity: accountId ?? "account",
|
||||
isError: portfolio.isError || portfolio.meta?.sync.state === "failed",
|
||||
isError: portfolio.isError || portfolio.meta?.lastAttempt.state === "failed",
|
||||
isFetching: portfolio.isFetching,
|
||||
});
|
||||
|
||||
@@ -105,7 +107,7 @@ function useAccountPageViewModel(): AccountPageViewModel {
|
||||
const incomeSummary = toIncomeSummary(accountId, income);
|
||||
|
||||
if (
|
||||
portfolio.meta?.availability === "complete"
|
||||
portfolio.meta?.availability === "available"
|
||||
&& portfolio.data.positions.length === 0
|
||||
&& portfolio.data.cashBalances.length === 0
|
||||
)
|
||||
@@ -118,7 +120,7 @@ function useAccountPageViewModel(): AccountPageViewModel {
|
||||
state: "empty" as const,
|
||||
};
|
||||
|
||||
const isPartial = portfolio.meta?.availability === "partial";
|
||||
const isPartial = portfolio.meta?.completeness === "incomplete";
|
||||
|
||||
return {
|
||||
account,
|
||||
@@ -129,6 +131,7 @@ function useAccountPageViewModel(): AccountPageViewModel {
|
||||
positionSections: toAccountPagePositionSections(portfolio.data),
|
||||
recentEvents,
|
||||
recentOperations,
|
||||
source: portfolio.meta,
|
||||
state: "loaded" as const,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -5,8 +5,10 @@ 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 { SourceSyncStatus } from "@/shared/ui";
|
||||
import { PortfolioStructure } from "@/widgets/portfolio-structure";
|
||||
|
||||
import type { AccountPagePositionSection } from "../../../model/positions";
|
||||
@@ -32,6 +34,7 @@ type AccountPageLoadedProps = {
|
||||
positionSections: AccountPagePositionSection[];
|
||||
recentEvents: AccountPageRecentEvents;
|
||||
recentOperations: AccountPageRecentOperations;
|
||||
source?: InvestmentResourceMeta;
|
||||
};
|
||||
|
||||
const AccountPageLoaded: FC<AccountPageLoadedProps> = (props) => {
|
||||
@@ -49,6 +52,12 @@ const AccountPageLoaded: FC<AccountPageLoadedProps> = (props) => {
|
||||
<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
|
||||
? <Alert role="status">{t("investment.partial")}</Alert>
|
||||
: <CurrencyCards amounts={props.portfolio.totalsByCurrency} />}
|
||||
|
||||
Reference in New Issue
Block a user