import { zodResolver } from "@hookform/resolvers/zod"; import { notifications } from "@mantine/notifications"; import { useState } from "react"; import { useForm, useWatch } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { bankDateField, bankMoneyValue, type BankOperation, bankOperationKinds, bankToday, useBankCategories, useBankFormFocus, useBankOperation, useCreateBankOperation, useUpdateBankOperation, } from "@/entities/bank-card"; import { formatDate, formatMoney } from "@/shared/lib"; import type { OperationFormProps, OperationValues, } from "./OperationForm.types"; import { createOperationSchema } from "./OperationForm.schema"; import { useRefundPurchase } from "./useRefundPurchase"; const useOperationFormVm = (props: OperationFormProps) => { useBankFormFocus(); const [opened, setOpened] = useState(true); const { t } = useTranslation(); const [categoriesOpen, setCategoriesOpen] = useState(false); const [version, setVersion] = useState(props.operation?.version ?? 1); const [latest, setLatest] = useState(); const currentQuery = useBankOperation(props.operation?.id); const [refundLimit, setRefundLimit] = useState( props.operation?.availableRefund, ); const [intent] = useState(() => crypto.randomUUID()); const [submittedValues, setSubmittedValues] = useState(); const [error, setError] = useState(); const categories = useBankCategories(); const defaultCategoryId = categories.data?.find(c => c.isDefault)?.id; const operationCreation = useCreateBankOperation(); const updateOperation = useUpdateBankOperation(); const methods = useForm({ defaultValues: { amount: props.operation?.amount.value ?? "", cardId: props.operation?.cardId ?? props.card.id, categoryId: props.operation?.categoryId ?? "", date: props.operation?.date ?? bankToday(), description: props.operation?.description ?? "", kind: props.operation?.kind ?? "expense", originalExpenseId: props.operation?.originalExpenseId ?? "", targetCardId: props.operation?.targetCardId ?? "", }, resolver: (values, context, options) => zodResolver( createOperationSchema({ cards: props.cards, categories: categories.data, expense: sourceQuery.data, limit: props.operation ? refundLimit : sourceQuery.data?.availableRefund, submittedValues, }), )(values, context, options), }); const kind = useWatch({ control: methods.control, name: "kind" }); const purchase = useRefundPurchase( methods.control, props.card.id, kind, Boolean(props.operation), ); const { sourceId, sourceQuery } = purchase; const source = useWatch({ control: methods.control, name: "cardId" }); const isPending = !opened || methods.formState.isSubmitting; const options = props.cards .filter(c => !c.archivedAt) .map(c => ({ label: c.name, value: c.id })); const onSubmit = methods.handleSubmit(async (values) => { setSubmittedValues(values); setError(undefined); try { const body = { cardId: values.cardId, kind: values.kind, ...(values.kind === "transfer" && { targetCardId: values.targetCardId, }), ...(values.kind === "expense" && { categoryId: values.categoryId || defaultCategoryId, }), ...(values.kind === "refund" && { originalExpenseId: values.originalExpenseId, }), amount: { currency: "RUB" as const, value: bankMoneyValue(values.amount), }, date: values.date, description: values.description, }; const actionId = crypto.randomUUID(); if (props.operation) await updateOperation.mutateAsync({ body: { ...body, version }, id: props.operation.id, }); else { const base = { amount: body.amount, cardId: values.cardId, date: values.date, description: values.description, id: intent, }; switch (values.kind) { case "expense": { await operationCreation.mutateAsync({ ...base, categoryId: values.categoryId || defaultCategoryId, kind: values.kind, }); break; } case "refund": { await operationCreation.mutateAsync({ ...base, kind: values.kind, originalExpenseId: values.originalExpenseId, }); break; } case "topup": { await operationCreation.mutateAsync({ ...base, kind: values.kind, }); break; } case "transfer": { await operationCreation.mutateAsync({ ...base, kind: values.kind, targetCardId: values.targetCardId, }); break; } } } notifications.show({ id: "bank-operation-saved-" + actionId, message: t("bank.operationSaved"), }); setOpened(false); } catch (error_) { setError(error_); } }); const onReload = async () => { const query = props.operation ? currentQuery : sourceQuery; const result = await query.refetch(); if (result.data && !result.error) { if (props.operation) { setLatest(result.data); setRefundLimit(result.data.availableRefund); } else setError(undefined); } else setError(result.error); }; return { categoriesError: categories.isError ? categories.error : null, categoriesOpen, closeCategories: () => setCategoriesOpen(false), error, expensesError: purchase.error, methods, onCategoryCreated: (id: string) => { methods.setValue("categoryId", id); setCategoriesOpen(false); }, retryCategories: () => void categories.refetch(), retryExpenses: purchase.retry, retrySource: () => void sourceQuery.refetch(), sourceError: sourceQuery.isError ? sourceQuery.error : null, view: { categoriesOpen, editing: Boolean(props.operation), emptyExpenses: purchase.empty, fields: { bindDate: bankDateField, cards: options, categories: categories.data?.map(c => ({ label: c.name, value: c.id })) ?? [], defaultCategory: defaultCategoryId ?? "", editing: Boolean(props.operation), kind, kinds: bankOperationKinds.map(value => ({ label: t(`bank.kinds.${value}`), value, })), maxDate: bankToday, refundCardId: props.card.id, targets: options.filter(c => c.value !== source), }, latest: latest ? { details: `${formatDate(latest.date)} · ${formatMoney(latest.amount)} · ${latest.description ?? ""}`, onAdopt: () => { setVersion(latest.version); setLatest(undefined); setError(undefined); }, version: latest.version, } : undefined, loadingExpenses: purchase.loading, nextDisabled: purchase.nextDisabled, onAddCategory: () => setCategoriesOpen(true), onCancel: () => setOpened(false), onClose: () => !isPending && !categoriesOpen && setOpened(false), onExitTransitionEnd: props.onClose, onNext: purchase.onNext, onPrevious: purchase.onPrevious, onReload: error && (sourceId || props.operation) ? onReload : undefined, onSubmit, opened, pending: isPending, previousDisabled: purchase.previousDisabled, refund: { editing: Boolean(props.operation), expenses: purchase.options, limit: props.operation ? refundLimit : sourceQuery.data?.availableRefund, }, saveDisabled: (kind === "expense" && !categories.data) || (kind === "refund" && !sourceQuery.data), sourcePage: purchase.page, }, }; }; export { useOperationFormVm };