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>; 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({ 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, };