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 } from "@/shared/api"; import { defaultPeriod, isValidPeriod, moscowDate, useInitialAvailability, useUrlPeriod, } from "@/shared/lib"; const operationCategorySet = new Set(operationCategories); type AccountOperationsPageViewModel = | { 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; source?: InvestmentResourceMeta; state: "loaded"; } | { onReset: () => void; 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(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, }); 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, state: "error" }; if (!operations.data) return availability === "unavailable" ? { onReset, state: "error" } : { state: "loading" }; const nextCursor = operations.data.nextCursor; const isDirty = draftFrom !== appliedFrom || draftTo !== appliedTo || !isSameCategories(draftCategories, appliedCategories); return { canApply: isDirty && isValidPeriod(draftFrom, draftTo), draftCategories, draftPeriod, hasNext: Boolean(nextCursor && operations.data.hasNext), hasPrevious: history.hasPrevious, historyLoading: operations.meta?.coverage?.historyState === "loading" || operations.meta?.coverage?.historyState === "partial", 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", source: operations.meta, state: "loaded", }; }; 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 };