chore: finish frontend ESLint migration #18

Enable every plugin recommended preset and replace Biome with ESLint formatting.

Preserve API contracts and JSX whitespace; verify source, tests, builds and tooling.
This commit is contained in:
2026-09-28 16:36:40 +03:00
committed by ksv741
parent 57630102e4
commit 72d7835227
253 changed files with 10059 additions and 8864 deletions
@@ -3,11 +3,12 @@ import { useState } from "react";
import { useForm, useWatch } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { z } from "zod";
import {
type BankOperation,
bankDateInputValue,
bankDateSchema,
bankMoneyValue,
type BankOperation,
bankOperationKinds,
bankToday,
moneyCents,
@@ -20,6 +21,7 @@ import {
} from "@/entities/bank-card";
import { validationMessage } from "@/shared/i18n";
import { formatDate, formatMoney } from "@/shared/lib";
import type {
OperationFormProps,
OperationValues,
@@ -28,12 +30,9 @@ import type {
const MAX_DESCRIPTION_LENGTH = 500;
const operationSchema = z.object({
kind: z.enum(bankOperationKinds),
cardId: z.string(),
targetCardId: z.string(),
categoryId: z.string(),
originalExpenseId: z.string(),
amount: positiveMoneySchema,
cardId: z.string(),
categoryId: z.string(),
date: bankDateSchema,
description: z
.string()
@@ -41,8 +40,38 @@ const operationSchema = z.object({
MAX_DESCRIPTION_LENGTH,
validationMessage("bank.validation.description")(undefined),
),
kind: z.enum(bankOperationKinds),
originalExpenseId: z.string(),
targetCardId: z.string(),
});
function validateRefund(values: OperationValues, context: z.RefinementCtx,
expense: BankOperation | undefined, limit: BankOperation["availableRefund"] | undefined) {
if (!expense || expense.cardId !== values.cardId)
context.addIssue({
code: "custom",
path: ["originalExpenseId"],
...validationMessage("bank.validation.required")(undefined),
});
if (expense && values.date < expense.date)
context.addIssue({
code: "custom",
path: ["date"],
...validationMessage("bank.validation.start")(undefined),
});
const amount = moneyCents(values.amount);
if (
limit
&& amount !== null
&& amount > (moneyCents(limit.value) ?? 0n)
)
context.addIssue({
code: "custom",
path: ["amount"],
...validationMessage("bank.validation.refund")(undefined),
});
}
const useOperationFormVm = (props: OperationFormProps) => {
useBankFormFocus();
const [opened, setOpened] = useState(true);
@@ -65,97 +94,70 @@ const useOperationFormVm = (props: OperationFormProps) => {
const categories = useBankCategories();
const mutation = useBankMutations();
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(
operationSchema.superRefine((values, ctx) => {
operationSchema.superRefine((values, context) => {
// Repeated intent reaches server idempotency even after its source changed.
if (
submittedValues &&
JSON.stringify(values) === JSON.stringify(submittedValues)
submittedValues
&& JSON.stringify(values) === JSON.stringify(submittedValues)
)
return;
const source = props.cards.find((c) => c.id === values.cardId);
const target = props.cards.find((c) => c.id === values.targetCardId);
const source = props.cards.find(c => c.id === values.cardId);
const target = props.cards.find(c => c.id === values.targetCardId);
if (!source || source.archivedAt)
ctx.addIssue({
context.addIssue({
code: "custom",
path: ["cardId"],
...validationMessage("bank.validation.required")(undefined),
});
if (
values.kind === "transfer" &&
(!target || target.archivedAt || target.id === values.cardId)
values.kind === "transfer"
&& (!target || target.archivedAt || target.id === values.cardId)
)
ctx.addIssue({
context.addIssue({
code: "custom",
path: ["targetCardId"],
...validationMessage("bank.validation.destination")(undefined),
});
if (
(source && values.date < source.openingDate) ||
(values.kind === "transfer" &&
target &&
values.date < target.openingDate)
(source && values.date < source.openingDate)
|| (target
&& values.kind === "transfer"
&& values.date < target.openingDate)
)
ctx.addIssue({
context.addIssue({
code: "custom",
path: ["date"],
...validationMessage("bank.validation.start")(undefined),
});
if (values.kind === "refund") {
const expense = sourceQuery.data;
if (!expense || expense.cardId !== values.cardId)
ctx.addIssue({
code: "custom",
path: ["originalExpenseId"],
...validationMessage("bank.validation.required")(undefined),
});
if (expense && values.date < expense.date)
ctx.addIssue({
code: "custom",
path: ["date"],
...validationMessage("bank.validation.start")(undefined),
});
const limit = props.operation
? refundLimit
: expense?.availableRefund;
const amount = moneyCents(values.amount);
if (
limit &&
amount !== null &&
amount > (moneyCents(limit.value) ?? 0n)
)
ctx.addIssue({
code: "custom",
path: ["amount"],
...validationMessage("bank.validation.refund")(undefined),
});
}
if (values.kind === "refund") validateRefund(values, context, sourceQuery.data,
props.operation ? refundLimit : sourceQuery.data?.availableRefund);
if (
values.kind === "expense" &&
!categories.data?.some(
(c) =>
c.id ===
(values.categoryId ||
categories.data?.find((c) => c.isDefault)?.id),
values.kind === "expense"
&& !categories.data?.some(
c =>
c.id
=== (values.categoryId
|| categories.data?.find(c => c.isDefault)?.id),
)
)
ctx.addIssue({
context.addIssue({
code: "custom",
path: ["categoryId"],
...validationMessage("bank.validation.required")(undefined),
});
}),
),
defaultValues: {
kind: props.operation?.kind ?? "expense",
cardId: props.operation?.cardId ?? props.card.id,
targetCardId: props.operation?.targetCardId ?? "",
categoryId: props.operation?.categoryId ?? "",
originalExpenseId: props.operation?.originalExpenseId ?? "",
amount: props.operation?.amount.value ?? "",
date: props.operation?.date ?? bankToday(),
description: props.operation?.description ?? "",
},
});
const kind = useWatch({ control: methods.control, name: "kind" });
const expenses = useBankExpenses(
@@ -163,30 +165,24 @@ const useOperationFormVm = (props: OperationFormProps) => {
{ page: sourcePage },
);
const source = useWatch({ control: methods.control, name: "cardId" });
const pending = !opened || methods.formState.isSubmitting;
const isPending = !opened || methods.formState.isSubmitting;
const options = props.cards
.filter((c) => !c.archivedAt)
.map((c) => ({ value: c.id, label: c.name }));
.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 = {
kind: values.kind,
cardId: values.cardId,
...(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 }
: {}),
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),
@@ -196,8 +192,8 @@ const useOperationFormVm = (props: OperationFormProps) => {
};
if (props.operation)
await mutation.updateOperation.mutateAsync({
id: props.operation.id,
body: { ...body, version },
id: props.operation.id,
});
else
await mutation.createOperation.mutateAsync({
@@ -205,8 +201,9 @@ const useOperationFormVm = (props: OperationFormProps) => {
...body,
});
setOpened(false);
} catch (failure) {
setError(failure);
}
catch (error_) {
setError(error_);
}
});
const onReload = async () => {
@@ -216,101 +213,103 @@ const useOperationFormVm = (props: OperationFormProps) => {
if (props.operation) {
setLatest(result.data);
setRefundLimit(result.data.availableRefund);
} else setError(undefined);
} else setError(result.error);
}
else setError(undefined);
}
else setError(result.error);
};
const expenseChoice = (item: BankOperation) => ({
value: item.id,
label: t("bank.expenseChoice", {
date: formatDate(item.date),
name: item.description || item.categoryName,
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(expenseChoice) ?? [];
const expenseOptions = expenses.data?.items.map(element => expenseChoice(element)) ?? [];
const selected = sourceQuery.data
? expenseChoice(sourceQuery.data)
: undefined;
return {
methods,
categoriesError: categories.isError ? categories.error : /* eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model. */ null,
categoriesOpen,
error,
categoriesError: categories.isError ? categories.error : null,
expensesError: expenses.isError ? expenses.error : null,
sourceError: sourceQuery.isError ? sourceQuery.error : null,
retryCategories: () => void categories.refetch(),
retryExpenses: () => void expenses.refetch(),
retrySource: () => void sourceQuery.refetch(),
closeCategories: () => setCategoriesOpen(false),
error,
expensesError: expenses.isError ? expenses.error : /* eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model. */ 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 : /* eslint-disable-line unicorn/no-null -- Preserve the nullable domain value consumed by this view model. */ null,
view: {
opened,
pending,
categoriesOpen,
editing: Boolean(props.operation),
onClose: () => !pending && !categoriesOpen && setOpened(false),
onExitTransitionEnd: props.onClose,
onCancel: () => setOpened(false),
onSubmit,
emptyExpenses: Boolean(
expenses.data && expenses.data.items.length === 0 && !props.operation,
),
fields: {
refundCardId: props.card.id,
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,
cards: options,
targets: options.filter((c) => c.value !== source),
categories:
categories.data?.map((c) => ({ value: c.id, label: c.name })) ?? [],
defaultCategory: categories.data?.find((c) => c.isDefault)?.id ?? "",
kinds: bankOperationKinds.map((value) => ({
value,
kinds: bankOperationKinds.map(value => ({
label: t(`bank.kinds.${value}`),
value,
})),
dateInputValue: bankDateInputValue,
validDate: (value: string) => bankDateSchema.safeParse(value).success,
maxDate: bankToday,
refundCardId: props.card.id,
targets: options.filter(c => c.value !== source),
validDate: (value: string) => bankDateSchema.safeParse(value).success,
},
refund: {
editing: Boolean(props.operation),
expenses:
selected &&
!expenseOptions.some((item) => item.value === selected.value)
? [selected, ...expenseOptions]
: expenseOptions,
onSelect: setSourceId,
limit: props.operation
? refundLimit
: sourceQuery.data?.availableRefund,
},
loadingExpenses: expenses.isLoading,
emptyExpenses: Boolean(
expenses.data && !expenses.data.items.length && !props.operation,
),
sourcePage,
previousDisabled: sourcePage === 1 || expenses.isFetching,
nextDisabled: !expenses.data?.hasNext || expenses.isFetching,
onPrevious: () => setSourcePage(sourcePage - 1),
onNext: () => setSourcePage(sourcePage + 1),
onAddCategory: () => setCategoriesOpen(true),
onReload: error && (props.operation || sourceId) ? onReload : undefined,
latest: latest
? {
version: latest.version,
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),
(kind === "expense" && !categories.data)
|| (kind === "refund" && !sourceQuery.data),
sourcePage,
},
};
};