Files
frontend/src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.vm.ts
T
ksv741 69997075e5
Frontend verify / commits (pull_request) Failing after 27s
Frontend verify / security (pull_request) Skipped
Frontend verify / verify (pull_request) Skipped
refactor!: simplify query and session ownership
Use Query cancellation and native mutation results instead of HTTP
registries, access epochs and copied scope state.
Keep local auth ordering and mutation identity fences.
Allow staged generated files to remain excluded from ESLint.

Backend contract: WealthLedger/backend@d698ed9

BREAKING CHANGE: drop investment stateVersion/progress/resources
from the client contract and use data/metadata readiness instead of
rewritten query status. Cross-tab auth may require re-login.
2026-10-03 11:45:04 +03:00

143 lines
3.9 KiB
TypeScript

import { zodResolver } from "@hookform/resolvers/zod";
import { useEffect, useMemo, useState } from "react";
import { useForm } from "react-hook-form";
import { useParams, useSearchParams } from "react-router";
import { type IncomeAnalytics, useIncomeAnalytics } from "@/entities/income";
import { useScreenSync } from "@/features/tbank-connection";
import { isValidPeriod, moscowDate } from "@/shared/lib";
import {
type IncomeDisplayModel,
toIncomeDisplayModel,
} from "../../model/display";
import { incomeFormSchema } from "../../model/income.schema";
import {
formatDraftDate,
type IncomeForm,
type IncomePeriod,
parseDraftDate,
} from "../../model/period";
const ISO_MONTH_LENGTH = 7;
type AccountIncomePageViewModel
= | {
analytics: IncomeAnalytics;
display: IncomeDisplayModel;
form: ReturnType<typeof useForm<IncomeForm>>;
formRevision: string;
lastSuccessfulAt?: null | string;
onReset: () => void;
onSubmit: (values: IncomeForm) => void;
partial?: boolean;
refreshError: boolean;
refreshing: boolean;
retry: () => Promise<unknown> | undefined;
sourceError?: boolean;
state: "loaded";
}
| {
period: IncomePeriod;
retry: () => Promise<unknown> | undefined;
state: "error";
}
| { state: "loading" };
const useAccountIncomePageViewModel = (): AccountIncomePageViewModel => {
const { accountId = "" } = useParams();
const [searchParameters, setSearchParameters] = useSearchParams();
const requested = useMemo(
() => ({
from: searchParameters.get("from") ?? "",
to: searchParameters.get("to") ?? "",
}),
[searchParameters],
);
const period = useMemo(
() => (isValidIncomePeriod(requested) ? requested : defaultPeriod()),
[requested],
);
const form = useForm<IncomeForm>({
defaultValues: draftPeriod(period),
resolver: zodResolver(incomeFormSchema),
});
const [resetRevision, setResetRevision] = useState(0);
const analytics = useIncomeAnalytics(accountId, period);
const sync = useScreenSync(
{ accountId, from: period.from, screen: "income", to: period.to },
[analytics],
);
useEffect(() => {
if (!isValidIncomePeriod(requested))
setSearchParameters(period, { replace: true });
}, [period, requested, setSearchParameters]);
useEffect(() => {
form.reset(draftPeriod(period));
}, [accountId, form, period]);
if (analytics.isLoading || !analytics.data) {
return analytics.isError || analytics.meta?.sync.state === "failed"
? { period, retry: sync.retry, state: "error" }
: { state: "loading" };
}
const display = toIncomeDisplayModel(analytics.data, analytics.meta);
const onSubmit = (values: IncomeForm) => {
const from = parseDraftDate(values.from);
const to = parseDraftDate(values.to);
if (from && to) setSearchParameters({ from, to });
};
const onReset = () => {
const next = defaultPeriod();
form.reset(draftPeriod(next));
// Reset the open calendar along with the form.
setResetRevision(revision => revision + 1);
setSearchParameters(next, { replace: true });
};
return {
analytics: analytics.data,
display,
form,
formRevision: `${accountId}:${period.from}:${period.to}:${resetRevision}`,
lastSuccessfulAt: analytics.meta?.lastSuccessfulAt,
onReset,
onSubmit,
partial: analytics.meta?.availability !== "complete",
refreshError: analytics.isError || sync.sourceError || sync.transportError,
refreshing: analytics.isFetching || sync.refreshing,
retry: sync.retry,
sourceError: sync.sourceError,
state: "loaded",
};
};
function defaultPeriod(): IncomePeriod {
const today = moscowDate(new Date());
return { from: `${today.slice(0, ISO_MONTH_LENGTH)}-01`, to: today };
}
function draftPeriod(period: IncomePeriod): IncomeForm {
return { from: formatDraftDate(period.from), to: formatDraftDate(period.to) };
}
function isValidIncomePeriod(period: IncomePeriod): boolean {
return isValidPeriod(period.from, period.to)
&& period.to <= moscowDate(new Date());
}
export {
type AccountIncomePageViewModel,
useAccountIncomePageViewModel,
};