Files
frontend/src/pages/bond/ui/BondPage/BondPage.vm.ts
T
ksv741 f6db28defa
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
fix: resolve frontend audit findings
Repair investment recovery, draft preservation, date validation,
currency presentation, accessibility, and route error recovery.
Correct Docker and dependency checks and consume explicit income roles.
2026-09-27 11:38:20 +03:00

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 };