Consume the provided resource-envelope API snapshot. Preserve financial snapshots during refresh, observe synchronization without importing, and fence late responses across account and connection changes.
165 lines
5.1 KiB
TypeScript
165 lines
5.1 KiB
TypeScript
import { useQueryClient } from "@tanstack/react-query";
|
|
import { useState } from "react";
|
|
import { useParams, useSearchParams } from "react-router";
|
|
import {
|
|
type BondPeriod,
|
|
useBond,
|
|
useBondCandles,
|
|
useBondCoupons,
|
|
useBondMetrics,
|
|
useBondPosition,
|
|
useBondQuote,
|
|
} from "@/entities/bond";
|
|
import {
|
|
brokerageOperationsQueryOptions,
|
|
type OperationCategory,
|
|
useBrokerageOperations,
|
|
} from "@/entities/broker-operation";
|
|
import { section } from "@/features/instrument-details";
|
|
import { useScreenSync } from "@/features/tbank-connection";
|
|
import { apiErrorCode } from "@/shared/api";
|
|
import { formatDate, instrumentLogoUrl } from "@/shared/lib";
|
|
|
|
const periods: BondPeriod[] = ["month", "halfYear", "year", "all"];
|
|
const filters = ["all", "trades", "coupons", "other"] as const;
|
|
type BondEventFilter = (typeof filters)[number];
|
|
const categories: Record<BondEventFilter, OperationCategory[]> = {
|
|
all: [],
|
|
trades: ["trade"],
|
|
coupons: ["income"],
|
|
other: ["tax", "fee", "transfer", "other"],
|
|
};
|
|
function useBondPageVm() {
|
|
const { bondId = "" } = useParams();
|
|
const bond = useBond(bondId);
|
|
const uid = bond.data?.instrumentUid ?? "";
|
|
const [params, setParams] = useSearchParams();
|
|
const period =
|
|
periods.find((value) => value === params.get("period")) ?? "month";
|
|
const filter =
|
|
filters.find((value) => value === params.get("events")) ?? "all";
|
|
const scope = `${uid}:${filter}`;
|
|
const [pagination, setPagination] = useState<{
|
|
scope: string;
|
|
cursors: string[];
|
|
}>({ scope, cursors: [] });
|
|
const cursors = pagination.scope === scope ? pagination.cursors : [];
|
|
const client = useQueryClient();
|
|
const quote = useBondQuote(uid);
|
|
const metrics = useBondMetrics(uid);
|
|
const candles = useBondCandles(uid, period);
|
|
const position = useBondPosition(uid);
|
|
const coupons = useBondCoupons(uid);
|
|
const firstPage = {
|
|
instrumentUid: uid,
|
|
categories: categories[filter],
|
|
limit: 20,
|
|
};
|
|
const operations = useBrokerageOperations(uid ? "all" : undefined, {
|
|
...firstPage,
|
|
cursor: cursors.at(-1),
|
|
});
|
|
const sync = useScreenSync(
|
|
{
|
|
screen: "bond",
|
|
instrumentUid: uid,
|
|
candlePeriod: period,
|
|
accountId: "all",
|
|
categories: categories[filter],
|
|
limit: 20,
|
|
},
|
|
[bond, quote, candles, position, metrics, coupons, operations],
|
|
);
|
|
const setParam = (key: string, value: string) => {
|
|
const next = new URLSearchParams(params);
|
|
next.set(key, value);
|
|
setParams(next, { replace: true });
|
|
};
|
|
const retryOperations = () => {
|
|
if (apiErrorCode(operations.error) === "INVALID_OPERATIONS_CURSOR") {
|
|
setPagination({ scope, cursors: [] });
|
|
void client.invalidateQueries({
|
|
queryKey: brokerageOperationsQueryOptions("all", firstPage).queryKey,
|
|
exact: true,
|
|
});
|
|
} else void sync.refresh();
|
|
};
|
|
const chartData =
|
|
candles.data?.candles.map((candle) => ({
|
|
...candle,
|
|
date: formatDate(candle.time),
|
|
price: Number(candle.close),
|
|
})) ?? [];
|
|
return {
|
|
refresh: sync.refresh,
|
|
refreshing: sync.refreshing,
|
|
refreshError: sync.transportError || sync.sourceError,
|
|
canonicalTo:
|
|
bond.data?.slug && bond.data.slug !== bondId
|
|
? `/bonds/${encodeURIComponent(bond.data.slug)}${params.size ? `?${params.toString()}` : ""}`
|
|
: null,
|
|
bond: bond.data,
|
|
bondState: section(bond, uid ? sync.refresh : () => bond.refetch()),
|
|
code: apiErrorCode(bond.error),
|
|
logoSrc: instrumentLogoUrl(bond.data?.logoName),
|
|
metrics: metrics.data,
|
|
metricsState: section(metrics, sync.refresh),
|
|
quote: quote.data,
|
|
quoteState: section(quote, sync.refresh),
|
|
candles: candles.data,
|
|
chartData,
|
|
priceRows: chartData.toReversed(),
|
|
candlesState: section(candles, sync.refresh),
|
|
period,
|
|
setPeriod: (value: string) => setParam("period", value),
|
|
averageLine: null,
|
|
hasIncompleteCandle:
|
|
candles.data?.candles.some((candle) => !candle.isComplete) ?? false,
|
|
position: position.data,
|
|
positionState: section(position, sync.refresh),
|
|
coupons: coupons.data,
|
|
couponsState: section(coupons, sync.refresh),
|
|
upcoming:
|
|
coupons.data?.coupons
|
|
.filter((event) => event.timing === "upcoming")
|
|
.sort((a, b) =>
|
|
(a.paymentDate ?? a.recordDate ?? "").localeCompare(
|
|
b.paymentDate ?? b.recordDate ?? "",
|
|
),
|
|
) ?? [],
|
|
pastCoupons:
|
|
coupons.data?.coupons
|
|
.filter((event) => event.timing !== "upcoming")
|
|
.sort((a, b) =>
|
|
(b.paymentDate ?? b.recordDate ?? "").localeCompare(
|
|
a.paymentDate ?? a.recordDate ?? "",
|
|
),
|
|
) ?? [],
|
|
operations: operations.data,
|
|
operationsState: {
|
|
...section(operations, sync.refresh),
|
|
retry: retryOperations,
|
|
},
|
|
eventRows:
|
|
operations.data?.items.map((item) => ({
|
|
...item,
|
|
ticker: item.ticker ?? bond.data?.ticker ?? null,
|
|
logoName: item.logoName ?? bond.data?.logoName ?? null,
|
|
})) ?? [],
|
|
filter,
|
|
setFilter: (value: string) => setParam("events", value),
|
|
hasPrevious: cursors.length > 0,
|
|
onPrevious: () => setPagination({ scope, cursors: cursors.slice(0, -1) }),
|
|
onNext: () => {
|
|
if (operations.data?.nextCursor)
|
|
setPagination({
|
|
scope,
|
|
cursors: [...cursors, operations.data.nextCursor],
|
|
});
|
|
},
|
|
};
|
|
}
|
|
type BondPageVm = ReturnType<typeof useBondPageVm>;
|
|
|
|
export { type BondPageVm, useBondPageVm };
|