217 lines
5.9 KiB
TypeScript
217 lines
5.9 KiB
TypeScript
import { useQueryClient } from "@tanstack/react-query";
|
|
import { useState } from "react";
|
|
import { useParams } from "react-router";
|
|
|
|
import type { InvestmentResourceMeta } from "@/shared/api";
|
|
|
|
import {
|
|
type BrokerageOperation,
|
|
brokerageOperationsQueryOptions,
|
|
operationCategories,
|
|
type OperationCategory,
|
|
useBrokerageOperationHistory,
|
|
} from "@/entities/broker-operation";
|
|
import { useScreenSync } from "@/features/tbank-connection";
|
|
import {
|
|
ApiErrorCode,
|
|
apiErrorCode,
|
|
isApiErrorCode,
|
|
readRetry,
|
|
} from "@/shared/api";
|
|
import {
|
|
defaultPeriod,
|
|
isValidPeriod,
|
|
moscowDate,
|
|
useInitialAvailability,
|
|
useUrlPeriod,
|
|
} from "@/shared/lib";
|
|
|
|
const operationCategorySet = new Set<string>(operationCategories);
|
|
|
|
type AccountOperationsPageViewModel
|
|
= | {
|
|
allAccounts: boolean;
|
|
canApply: boolean;
|
|
draftCategories: OperationCategory[];
|
|
draftPeriod: [null | string, null | string];
|
|
hasNext: boolean;
|
|
hasPrevious: boolean;
|
|
historyLoading?: boolean;
|
|
items: BrokerageOperation[];
|
|
maxDate: string;
|
|
onApply: () => void;
|
|
onCategoriesChange: (value: string[]) => void;
|
|
onNext: () => void;
|
|
onPeriodChange: (value: [null | string, null | string]) => void;
|
|
onPeriodPopoverOpen: () => void;
|
|
onPrevious: () => void;
|
|
onReset: () => void;
|
|
partial?: boolean;
|
|
readRetry: ReturnType<typeof readRetry>;
|
|
source?: InvestmentResourceMeta;
|
|
state: "loaded";
|
|
totalVisible: boolean;
|
|
}
|
|
| {
|
|
onReset: () => void;
|
|
readRetry: ReturnType<typeof readRetry>;
|
|
state: "error";
|
|
}
|
|
| { state: "loading" };
|
|
|
|
const useAccountOperationsPageVm = (): AccountOperationsPageViewModel => {
|
|
const { accountId } = useParams();
|
|
const queryClient = useQueryClient();
|
|
|
|
const {
|
|
from: appliedFrom,
|
|
parameters: searchParameters,
|
|
setParameters: setSearchParameters,
|
|
to: appliedTo,
|
|
} = useUrlPeriod();
|
|
|
|
const appliedCategories = operationCategories.filter(category =>
|
|
(searchParameters.get("categories") ?? "").split(",").includes(category),
|
|
);
|
|
|
|
const [draftPeriod, setDraftPeriod] = useState<[
|
|
null | string,
|
|
null | string,
|
|
]>([appliedFrom, appliedTo]);
|
|
|
|
const [draftFrom, draftTo] = draftPeriod;
|
|
|
|
const [draftCategories, setDraftCategories]
|
|
= useState<OperationCategory[]>(appliedCategories);
|
|
|
|
const [maxDate, setMaxDate] = useState(() => moscowDate(new Date()));
|
|
|
|
const firstPageQuery = {
|
|
from: dateStart(appliedFrom),
|
|
to: dateEnd(appliedTo),
|
|
...(appliedCategories.length > 0 && { categories: appliedCategories }),
|
|
limit: 100,
|
|
};
|
|
|
|
const history = useBrokerageOperationHistory(accountId, firstPageQuery);
|
|
const { operations } = history;
|
|
const scope = JSON.stringify([accountId, searchParameters.toString()]);
|
|
const [previousScope, setPreviousScope] = useState(scope);
|
|
|
|
if (previousScope !== scope) {
|
|
setPreviousScope(scope);
|
|
setDraftPeriod([appliedFrom, appliedTo]);
|
|
setDraftCategories(appliedCategories);
|
|
history.reset();
|
|
}
|
|
|
|
useScreenSync(
|
|
{ accountId, screen: "operations", ...firstPageQuery },
|
|
[operations],
|
|
);
|
|
|
|
const availability = useInitialAvailability({
|
|
hasContent: !!operations.data,
|
|
identity: scope,
|
|
isError: operations.isError || operations.meta?.lastAttempt.state === "failed",
|
|
isFetching: operations.isFetching,
|
|
});
|
|
|
|
const retry = readRetry([operations]);
|
|
|
|
if (availability === "loading" && operations.isLoading)
|
|
return { state: "loading" };
|
|
|
|
if (history.isRecoveringInvalidCursor)
|
|
return { state: "loading" };
|
|
|
|
const resetQueryKey = brokerageOperationsQueryOptions(accountId, {
|
|
limit: 100,
|
|
}).queryKey;
|
|
|
|
const onReset = () => {
|
|
setDraftPeriod([appliedFrom, appliedTo]);
|
|
setDraftCategories([]);
|
|
setSearchParameters({});
|
|
history.reset();
|
|
void queryClient.invalidateQueries({ queryKey: resetQueryKey });
|
|
};
|
|
|
|
const isInvalidCursor = isApiErrorCode(
|
|
apiErrorCode(operations.error),
|
|
ApiErrorCode.InvalidOperationsCursor,
|
|
);
|
|
|
|
if (
|
|
(isInvalidCursor || !operations.data)
|
|
&& (operations.isError || operations.meta?.lastAttempt.state === "failed")
|
|
)
|
|
return { onReset, readRetry: retry, state: "error" };
|
|
|
|
if (!operations.data)
|
|
return availability === "unavailable"
|
|
? { onReset, readRetry: retry, state: "error" }
|
|
: { state: "loading" };
|
|
|
|
const nextCursor = operations.data.nextCursor;
|
|
|
|
const isDirty
|
|
= draftFrom !== appliedFrom
|
|
|| draftTo !== appliedTo
|
|
|| !isSameCategories(draftCategories, appliedCategories);
|
|
|
|
return {
|
|
allAccounts: accountId === "all",
|
|
canApply: isDirty && isValidPeriod(draftFrom, draftTo),
|
|
draftCategories,
|
|
draftPeriod,
|
|
hasNext: Boolean(nextCursor && operations.data.hasNext),
|
|
hasPrevious: history.hasPrevious,
|
|
historyLoading: operations.meta?.coverage?.historyState === "loading",
|
|
items: operations.data.items,
|
|
maxDate,
|
|
onApply: () => {
|
|
if (!isValidPeriod(draftFrom, draftTo)) return;
|
|
setSearchParameters({
|
|
...(draftFrom && { from: draftFrom }),
|
|
...(draftTo && { to: draftTo }),
|
|
...(draftCategories.length > 0 && {
|
|
categories: draftCategories.join(","),
|
|
}),
|
|
});
|
|
history.reset();
|
|
},
|
|
onCategoriesChange: values => setDraftCategories(
|
|
values.filter((value): value is OperationCategory =>
|
|
operationCategorySet.has(value),
|
|
),
|
|
),
|
|
onNext: history.onNext,
|
|
onPeriodChange: value => setDraftPeriod(
|
|
value[0] === null && value[1] === null ? defaultPeriod() : value,
|
|
),
|
|
onPeriodPopoverOpen: () => setMaxDate(moscowDate(new Date())),
|
|
onPrevious: history.onPrevious,
|
|
onReset,
|
|
partial: operations.meta?.completeness === "incomplete",
|
|
readRetry: retry,
|
|
source: operations.meta,
|
|
state: "loaded",
|
|
totalVisible: operations.meta?.completeness === "complete",
|
|
};
|
|
};
|
|
|
|
function dateEnd(value: null | string) {
|
|
return value ? `${value}T23:59:59.999Z` : undefined;
|
|
}
|
|
|
|
function dateStart(value: null | string) {
|
|
return value ? `${value}T00:00:00.000Z` : undefined;
|
|
}
|
|
|
|
function isSameCategories(a: OperationCategory[], b: OperationCategory[]) {
|
|
return a.length === b.length && a.every(value => b.includes(value));
|
|
}
|
|
|
|
export { type AccountOperationsPageViewModel, useAccountOperationsPageVm };
|