Frontend verify / commits (pull_request) Successful in 31s
Frontend verify / security (pull_request) Successful in 11s
Frontend verify / verify (pull_request) Successful in 9m1s
Frontend verify / commits (push) Skipped
Frontend verify / security (push) Successful in 10s
Frontend verify / verify (push) Successful in 9m59s
256 lines
7.2 KiB
TypeScript
256 lines
7.2 KiB
TypeScript
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { useState } from "react";
|
|
import { useForm, useWatch } from "react-hook-form";
|
|
import { useTranslation } from "react-i18next";
|
|
|
|
import {
|
|
bankDateInputValue,
|
|
bankDateSchema,
|
|
bankMoneyValue,
|
|
type BankOperation,
|
|
bankOperationKinds,
|
|
bankToday,
|
|
useBankCategories,
|
|
useBankExpenses,
|
|
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";
|
|
|
|
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<BankOperation>();
|
|
const [sourcePage, setSourcePage] = useState(1);
|
|
|
|
const [sourceId, setSourceId] = useState(
|
|
props.operation?.originalExpenseId ?? "",
|
|
);
|
|
|
|
const sourceQuery = useBankOperation(sourceId || undefined);
|
|
const currentQuery = useBankOperation(props.operation?.id);
|
|
|
|
const [refundLimit, setRefundLimit] = useState(
|
|
props.operation?.availableRefund,
|
|
);
|
|
|
|
const [intent] = useState(() => crypto.randomUUID());
|
|
const [submittedValues, setSubmittedValues] = useState<OperationValues>();
|
|
const [error, setError] = useState<unknown>();
|
|
const categories = useBankCategories();
|
|
const operationCreation = useCreateBankOperation();
|
|
const updateOperation = useUpdateBankOperation();
|
|
|
|
const methods = useForm<OperationValues>({
|
|
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: zodResolver(
|
|
createOperationSchema({
|
|
cards: props.cards,
|
|
categories: categories.data,
|
|
expense: sourceQuery.data,
|
|
limit: props.operation
|
|
? refundLimit
|
|
: sourceQuery.data?.availableRefund,
|
|
submittedValues,
|
|
}),
|
|
),
|
|
});
|
|
|
|
const kind = useWatch({ control: methods.control, name: "kind" });
|
|
|
|
const expenses = useBankExpenses(
|
|
kind === "refund" ? props.card.id : undefined,
|
|
{ page: sourcePage },
|
|
);
|
|
|
|
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 || categories.data?.find(c => c.isDefault)?.id,
|
|
}),
|
|
...(values.kind === "refund" && {
|
|
originalExpenseId: values.originalExpenseId,
|
|
}),
|
|
amount: {
|
|
currency: "RUB" as const,
|
|
value: bankMoneyValue(values.amount),
|
|
},
|
|
date: values.date,
|
|
description: values.description,
|
|
};
|
|
|
|
if (props.operation)
|
|
await updateOperation.mutateAsync({
|
|
body: { ...body, version },
|
|
id: props.operation.id,
|
|
});
|
|
else
|
|
await operationCreation.mutateAsync({
|
|
id: intent,
|
|
...body,
|
|
});
|
|
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);
|
|
};
|
|
|
|
const expenseChoice = (item: BankOperation) => ({
|
|
label: t("bank.expenseChoice", {
|
|
amount: formatMoney(item.amount),
|
|
available: formatMoney(item.availableRefund),
|
|
date: formatDate(item.date),
|
|
name: item.description || item.categoryName,
|
|
}),
|
|
value: item.id,
|
|
});
|
|
|
|
const expenseOptions
|
|
= expenses.data?.items.map(element => expenseChoice(element)) ?? [];
|
|
|
|
const selected = sourceQuery.data
|
|
? expenseChoice(sourceQuery.data)
|
|
: undefined;
|
|
|
|
return {
|
|
categoriesError: categories.isError
|
|
? categories.error
|
|
: null,
|
|
categoriesOpen,
|
|
closeCategories: () => setCategoriesOpen(false),
|
|
error,
|
|
expensesError: expenses.isError
|
|
? expenses.error
|
|
: null,
|
|
methods,
|
|
onCategoryCreated: (id: string) => {
|
|
methods.setValue("categoryId", id);
|
|
setCategoriesOpen(false);
|
|
},
|
|
retryCategories: () => void categories.refetch(),
|
|
retryExpenses: () => void expenses.refetch(),
|
|
retrySource: () => void sourceQuery.refetch(),
|
|
sourceError: sourceQuery.isError
|
|
? sourceQuery.error
|
|
: null,
|
|
view: {
|
|
categoriesOpen,
|
|
editing: Boolean(props.operation),
|
|
emptyExpenses: Boolean(
|
|
expenses.data && expenses.data.items.length === 0 && !props.operation,
|
|
),
|
|
fields: {
|
|
cards: options,
|
|
categories:
|
|
categories.data?.map(c => ({ label: c.name, value: c.id })) ?? [],
|
|
dateInputValue: bankDateInputValue,
|
|
defaultCategory: categories.data?.find(c => c.isDefault)?.id ?? "",
|
|
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),
|
|
validDate: (value: string) => bankDateSchema.safeParse(value).success,
|
|
},
|
|
latest: latest
|
|
? {
|
|
details: `${formatDate(latest.date)} · ${formatMoney(latest.amount)} · ${latest.description ?? ""}`,
|
|
onAdopt: () => {
|
|
setVersion(latest.version);
|
|
setLatest(undefined);
|
|
setError(undefined);
|
|
},
|
|
version: latest.version,
|
|
}
|
|
: undefined,
|
|
loadingExpenses: expenses.isLoading,
|
|
nextDisabled: !expenses.data?.hasNext || expenses.isFetching,
|
|
onAddCategory: () => setCategoriesOpen(true),
|
|
onCancel: () => setOpened(false),
|
|
onClose: () => !isPending && !categoriesOpen && setOpened(false),
|
|
onExitTransitionEnd: props.onClose,
|
|
onNext: () => setSourcePage(sourcePage + 1),
|
|
onPrevious: () => setSourcePage(sourcePage - 1),
|
|
onReload: error && (sourceId || props.operation) ? onReload : undefined,
|
|
onSubmit,
|
|
opened,
|
|
pending: isPending,
|
|
previousDisabled: sourcePage === 1 || expenses.isFetching,
|
|
refund: {
|
|
editing: Boolean(props.operation),
|
|
expenses:
|
|
selected
|
|
&& expenseOptions.every(item => item.value !== selected.value)
|
|
? [selected, ...expenseOptions]
|
|
: expenseOptions,
|
|
limit: props.operation
|
|
? refundLimit
|
|
: sourceQuery.data?.availableRefund,
|
|
onSelect: setSourceId,
|
|
},
|
|
saveDisabled:
|
|
(kind === "expense" && !categories.data)
|
|
|| (kind === "refund" && !sourceQuery.data),
|
|
sourcePage,
|
|
},
|
|
};
|
|
};
|
|
|
|
export { useOperationFormVm };
|