Files
frontend/src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.vm.ts
T
ksv741 0865b35f93
Frontend verify / commits (pull_request) Successful in 27s
Frontend verify / security (pull_request) Successful in 9s
Frontend verify / verify (pull_request) Successful in 8m20s
refactor: clarify frontend model ownership
2026-09-28 21:44:53 +03:00

149 lines
4.0 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);
if (!display)
return {
period,
retry: () => analytics.observe(),
state: "error",
};
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,
};