Files
frontend/src/pages/account-income/ui/AccountIncomePage/AccountIncomePage.vm.ts
T
codex-bot 43b0f5574a
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 10s
Frontend verify / verify (push) Successful in 7m18s
feat: unify income payment cards and remove comparisons (WealthLedger/main#33) (#74)
feat: unify income payment cards and remove comparisons (WealthLedger/main#33)
2026-10-13 18:17:35 +03:00

141 lines
3.8 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, useInitialAvailability } 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;
onReset: () => void;
onSubmit: (values: IncomeForm) => void;
partial?: boolean;
state: "loaded";
}
| {
period: IncomePeriod;
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);
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]);
const availability = useInitialAvailability({
hasContent: !!analytics.data,
identity: `${accountId}:${period.from}:${period.to}`,
isError: analytics.isError || analytics.meta?.lastAttempt.state === "failed",
isFetching: analytics.isFetching,
});
if (availability === "loading" && analytics.isLoading)
return { state: "loading" };
if (!analytics.data)
return availability === "unavailable"
? { period, state: "error" }
: { state: "loading" };
const display = toIncomeDisplayModel(analytics.data);
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}`,
onReset,
onSubmit,
partial: analytics.meta?.completeness === "incomplete",
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,
};