141 lines
3.8 KiB
TypeScript
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, 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}`,
|
|
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,
|
|
};
|