Frontend security / security (push) Successful in 10s
Frontend verify / verify (push) Successful in 4m57s
Conventional commits / commits (pull_request) Successful in 28s
Frontend security / security (pull_request) Successful in 10s
Frontend verify / verify (pull_request) Successful in 5m10s
Repair investment recovery, draft preservation, date validation, currency presentation, accessibility, and route error recovery. Correct Docker and dependency checks and consume explicit income roles.
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.retry();
|
|
};
|
|
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.retry : () => bond.refetch()),
|
|
code: apiErrorCode(bond.error),
|
|
logoSrc: instrumentLogoUrl(bond.data?.logoName),
|
|
metrics: metrics.data,
|
|
metricsState: section(metrics, sync.retry),
|
|
quote: quote.data,
|
|
quoteState: section(quote, sync.retry),
|
|
candles: candles.data,
|
|
chartData,
|
|
priceRows: chartData.toReversed(),
|
|
candlesState: section(candles, sync.retry),
|
|
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.retry),
|
|
coupons: coupons.data,
|
|
couponsState: section(coupons, sync.retry),
|
|
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.retry),
|
|
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 };
|