diff --git a/frontend/src/entity/expenses/item/index.ts b/frontend/src/entity/expenses/item/index.ts index 900bdde..1dbf0bd 100644 --- a/frontend/src/entity/expenses/item/index.ts +++ b/frontend/src/entity/expenses/item/index.ts @@ -8,11 +8,6 @@ export { useEditExpenseItem } from './hooks/useEditExpenseItem'; export type { ExpenseListItem } from './types'; -export type { AddExpenseItemFormProps, AddExpenseItemFormValues } from './ui/AddExpenseItemForm/AddExpenseItemForm.ui'; -export { default as AddExpenseItemForm } from './ui/AddExpenseItemForm/AddExpenseItemForm.ui'; - -export type { DeleteExpenseItemFormProps } from './ui/DeleteExpenseItemForm/DeleteExpenseItemForm.ui'; -export { default as DeleteExpenseItemForm } from './ui/DeleteExpenseItemForm/DeleteExpenseItemForm.ui'; - -export type { EditExpenseItemFormProps, EditExpenseItemFormValues } from './ui/EditExpenseItemForm/EditExpenseItemForm.ui'; -export { default as EditExpenseItemForm } from './ui/EditExpenseItemForm/EditExpenseItemForm.ui'; +export * from './ui/AddExpenseItemForm'; +export * from './ui/DeleteExpenseItemForm'; +export * from './ui/EditExpenseItemForm'; diff --git a/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/AddExpenseItemForm.schema.ts b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/AddExpenseItemForm.schema.ts new file mode 100644 index 0000000..f481104 --- /dev/null +++ b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/AddExpenseItemForm.schema.ts @@ -0,0 +1,14 @@ +import * as z from 'zod'; + +export const AddExpenseItemFormSchema = z.object({ + amount: z.number(), + 'bank-card': z.string().optional(), + cash: z.string().optional(), + count: z.number(), + date: z.date(), + description: z.string().trim().optional(), + paymentType: z.enum(['BANK_CARD', 'CASH', 'none']), + title: z.string().trim().min(5), +}); + +export type AddExpenseItemFormValues = z.infer; diff --git a/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/AddExpenseItemForm.test.tsx b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/AddExpenseItemForm.test.tsx index c69597f..2682fa8 100644 --- a/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/AddExpenseItemForm.test.tsx +++ b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/AddExpenseItemForm.test.tsx @@ -55,10 +55,9 @@ describe('Test AddExpenseItemForm', () => { expect(onSubmit).toHaveBeenCalledWith({ amount: 12, count: 1, - currency: 'RUB', - date: '2025-08-29T00:00:00.000Z', + date: new Date('2025-08-29T00:00:00.000Z'), description: 'Описание элемента', - payment: null, + paymentType: 'none', title: 'Название элемента', }); }); @@ -80,14 +79,11 @@ describe('Test AddExpenseItemForm', () => { expect(onSubmit).toHaveBeenCalledTimes(1); expect(onSubmit).toHaveBeenCalledWith({ amount: 12, + 'bank-card': bankCardList[1].id, count: 1, - currency: 'RUB', - date: '2025-08-29T00:00:00.000Z', + date: new Date('2025-08-29T00:00:00.000Z'), description: 'Описание элемента', - payment: { - id: bankCardList[1].id, - type: 'BANK_CARD', - }, + paymentType: 'BANK_CARD', title: 'Название элемента', }); }); @@ -106,14 +102,11 @@ describe('Test AddExpenseItemForm', () => { expect(onSubmit).toHaveBeenCalledTimes(1); expect(onSubmit).toHaveBeenCalledWith({ amount: 12, + cash: cashList[1].id, count: 1, - currency: 'RUB', - date: '2025-08-29T00:00:00.000Z', + date: new Date('2025-08-29T00:00:00.000Z'), description: 'Описание элемента', - payment: { - id: cashList[1].id, - type: 'CASH', - }, + paymentType: 'CASH', title: 'Название элемента', }); }); diff --git a/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/AddExpenseItemForm.ui.tsx b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/AddExpenseItemForm.ui.tsx index e3e956a..f21c5bc 100644 --- a/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/AddExpenseItemForm.ui.tsx +++ b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/AddExpenseItemForm.ui.tsx @@ -1,54 +1,27 @@ -import type { SelectChangeEvent } from '@mui/material'; -import type { - FC, - FormEventHandler, - FormHTMLAttributes, - RefObject, -} from 'react'; +import type { FC, FormHTMLAttributes, RefObject } from 'react'; -import { - Button, - FormControl, - InputLabel, - MenuItem, - Select, - TextField, - Typography, -} from '@mui/material'; -import { DatePicker } from '@mui/x-date-pickers'; -import { - useMemo, - useState, -} from 'react'; +import { Button, Typography } from '@mui/material'; +import { FormProvider } from 'react-hook-form'; -import type { ExpenseListItem } from '@/entity/expenses/item'; - -import { useBankCards } from '@/entity/cards'; -import { useCash } from '@/entity/cash'; import { DataTestId } from '@/shared/constants'; -import { formatRub, getUTCDate } from '@/shared/utils'; + +import type { UseAddExpenseItemFormArgs } from './hooks/useAddExpenseItemForm'; import classes from './AddExpenseItemForm.module.css'; +import AmountField from './fields/AmountField'; +import BankCardField from './fields/BankCardField'; +import CashField from './fields/CashField'; +import CountField from './fields/CountField'; +import DateField from './fields/DateField'; +import DescriptionField from './fields/DescriptionField'; +import PaymentTypeField from './fields/PaymentTypeField'; +import TitleField from './fields/TitleField'; +import { useAddExpenseItemForm } from './hooks/useAddExpenseItemForm'; -export type AddExpenseItemFormProps = BaseFormProps & { - disabled?: boolean; - onSubmit?: (data: AddExpenseItemFormValues) => Promise | void; +export type AddExpenseItemFormProps = BaseFormProps & UseAddExpenseItemFormArgs & { ref?: RefObject; }; -export type AddExpenseItemFormValues = { - amount: number; - count: number; - currency: string; - date: string; - description: string; - payment: null | { - id: string; - type: 'BANK_CARD' | 'CASH'; - }; - title: string; -}; - type BaseFormProps = Omit, 'onSubmit'>; const AddExpenseItemForm: FC = (props) => { @@ -59,214 +32,55 @@ const AddExpenseItemForm: FC = (props) => { ...formProps } = props; - const [paymentType, setPaymentType] = useState['type']>(null); - - const onFormSubmitHandler: FormEventHandler = (event) => { - event.preventDefault(); - const formData = new FormData(event.currentTarget); - - const title = formData.get('title'); - const date = formData.get('date'); - const amount = formData.get('amount'); - const description = formData.get('description'); - const count = formData.get('count'); - const paymentMethod = formData.get('paymentMethod'); - const bankCardId = formData.get('bank-card'); - const cashId = formData.get('cash'); - - const getPayment = () => { - switch (paymentMethod) { - case 'BANK_CARD': - return { - id: bankCardId, - type: 'BANK_CARD', - }; - case 'CASH': - return { - id: cashId, - type: 'CASH', - }; - case 'none': - case null: - default: - return null; - } - }; - - if (typeof title === 'string' && typeof date === 'string' && typeof amount === 'string' && typeof description === 'string' && typeof count === 'string') { - onSubmit?.({ - amount: Number(amount), - count: Number(count), - currency: 'RUB', - date: getUTCDate(date), - description, - // @ts-ignore todo - payment: getPayment(), - title, - }); - } - }; - const { data: bankCards } = useBankCards(); - const { data: cash } = useCash(); - - const bankCardOptions = bankCards?.map((card) => ( - - {`${card.name} (${formatRub(card.balance)})`} - - )); - - const cashOptions = cash?.map((cashItem) => ( - - {`${cashItem.name} (${formatRub(cashItem.balance)})`} - - )); - - const paymentOptions = useMemo(() => { - const options = [Без оплаты]; - - if (bankCardOptions && bankCardOptions.length > 0) { - options.push(Банковская карта); - } - - if (cashOptions && cashOptions.length > 0) { - options.push(Наличные); - } - - return options; - }, [bankCardOptions, cashOptions]); - - const onChangePaymentHandler = (event: SelectChangeEvent<'BANK_CARD' | 'CASH' | 'none'>) => { - const { value } = event.target; - if (value === 'none') { - setPaymentType(null); - } else { - // @ts-ignore todo - setPaymentType(value); - } - }; - - const renderCardSelect = () => ( - - Карта - - - - ); - - const renderCashSelect = () => ( - - Наличные - - - - ); + const { + formState, + handleSubmit, + methods, + onFormSubmitHandler, + watch, + } = useAddExpenseItemForm({ disabled, onSubmit }); return ( -
- - Добавить список расходов - - - - - - - - - - - - - - Способ оплаты - - - - - {paymentType === 'BANK_CARD' && renderCardSelect()} - - {paymentType === 'CASH' && renderCashSelect()} - - - + + Добавить элемент расхода + + + + + + + + + + + + + + + {watch('paymentType') === 'BANK_CARD' && } + + {watch('paymentType') === 'CASH' && } + + + + ); }; diff --git a/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/AmountField.tsx b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/AmountField.tsx new file mode 100644 index 0000000..1886cc8 --- /dev/null +++ b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/AmountField.tsx @@ -0,0 +1,33 @@ +import { TextField } from '@mui/material'; +import { Controller, useFormContext } from 'react-hook-form'; + +import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema'; + +const AmountField = () => { + const { control, formState } = useFormContext>(); + + return ( + ( + { + field.onChange(Number(event.target.value)); + }} + required + size="small" + type="number" + /> + )} + /> + ); +}; + +export default AmountField; diff --git a/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/BankCardField.tsx b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/BankCardField.tsx new file mode 100644 index 0000000..576600b --- /dev/null +++ b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/BankCardField.tsx @@ -0,0 +1,58 @@ +import { + FormControl, + FormHelperText, + InputLabel, + MenuItem, + Select, +} from '@mui/material'; +import { Controller, useFormContext } from 'react-hook-form'; + +import { useBankCards } from '@/entity/cards'; +import { formatRub } from '@/shared/utils'; + +import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema'; + +const BankCardField = () => { + const { control, formState } = useFormContext>(); + + const { data: bankCards } = useBankCards(); + + const bankCardOptions = bankCards?.map((card) => ( + + {`${card.name} (${formatRub(card.balance)})`} + + )); + + return ( + ( + + Карта + + + + + {formState.errors['bank-card']?.message} + + + )} + /> + ); +}; + +export default BankCardField; diff --git a/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/CashField.tsx b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/CashField.tsx new file mode 100644 index 0000000..7907ad1 --- /dev/null +++ b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/CashField.tsx @@ -0,0 +1,57 @@ +import { + FormControl, + FormHelperText, + InputLabel, + MenuItem, + Select, +} from '@mui/material'; +import { Controller, useFormContext } from 'react-hook-form'; + +import { useCash } from '@/entity/cash'; +import { formatRub } from '@/shared/utils'; + +import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema'; + +const CashField = () => { + const { control, formState } = useFormContext>(); + const { data: cash } = useCash(); + + const cashOptions = cash?.map((cashItem) => ( + + {`${cashItem.name} (${formatRub(cashItem.balance)})`} + + )); + + return ( + ( + + Наличные + + + + + {formState.errors.cash?.message} + + + )} + /> + ); +}; + +export default CashField; diff --git a/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/CountField.tsx b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/CountField.tsx new file mode 100644 index 0000000..2adeb7e --- /dev/null +++ b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/CountField.tsx @@ -0,0 +1,33 @@ +import { TextField } from '@mui/material'; +import { Controller, useFormContext } from 'react-hook-form'; + +import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema'; + +const CountField = () => { + const { control, formState } = useFormContext>(); + + return ( + ( + { + field.onChange(Number(event.target.value)); + }} + size="small" + type="number" + /> + )} + /> + ); +}; + +export default CountField; diff --git a/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/DateField.tsx b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/DateField.tsx new file mode 100644 index 0000000..5e5311f --- /dev/null +++ b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/DateField.tsx @@ -0,0 +1,37 @@ +import type { FC } from 'react'; + +import { DatePicker } from '@mui/x-date-pickers'; +import { Controller, useFormContext } from 'react-hook-form'; + +import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema'; + +const DateField: FC = () => { + const { control, formState } = useFormContext>(); + + return ( + ( + + )} + /> + ); +}; + +export default DateField; diff --git a/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/DescriptionField.tsx b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/DescriptionField.tsx new file mode 100644 index 0000000..cd3640f --- /dev/null +++ b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/DescriptionField.tsx @@ -0,0 +1,29 @@ +import type { FC } from 'react'; + +import { TextField } from '@mui/material'; +import { Controller, useFormContext } from 'react-hook-form'; + +import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema'; + +const DescriptionField: FC = () => { + const { control, formState } = useFormContext>(); + + return ( + ( + + )} + /> + ); +}; + +export default DescriptionField; diff --git a/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/PaymentTypeField.tsx b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/PaymentTypeField.tsx new file mode 100644 index 0000000..4f5d6ea --- /dev/null +++ b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/PaymentTypeField.tsx @@ -0,0 +1,69 @@ +import type { FC } from 'react'; + +import { + FormControl, + FormHelperText, + InputLabel, + MenuItem, + Select, +} from '@mui/material'; +import { useMemo } from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; + +import { useBankCards } from '@/entity/cards'; +import { useCash } from '@/entity/cash'; + +import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema'; + +const PaymentTypeField: FC = () => { + const { control, formState } = useFormContext>(); + + const { data: bankCards } = useBankCards(); + const { data: cash } = useCash(); + + const paymentTypeOptions = useMemo(() => { + const options = [Без оплаты]; + + if (bankCards && bankCards.length > 0) { + options.push(Банковская карта); + } + + if (cash && cash.length > 0) { + options.push(Наличные); + } + + return options; + }, [bankCards, cash]); + + return ( + ( + + Способ оплаты + + + + + {formState.errors.paymentType?.message} + + + )} + /> + ); +}; + +export default PaymentTypeField; diff --git a/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/TitleField.tsx b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/TitleField.tsx new file mode 100644 index 0000000..11e18b3 --- /dev/null +++ b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/fields/TitleField.tsx @@ -0,0 +1,30 @@ +import type { FC } from 'react'; + +import { TextField } from '@mui/material'; +import { Controller, useFormContext } from 'react-hook-form'; + +import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema'; + +const TitleField: FC = () => { + const { control, formState } = useFormContext>(); + + return ( + ( + + )} + /> + ); +}; + +export default TitleField; diff --git a/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/hooks/useAddExpenseItemForm.ts b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/hooks/useAddExpenseItemForm.ts new file mode 100644 index 0000000..a0f9efb --- /dev/null +++ b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/hooks/useAddExpenseItemForm.ts @@ -0,0 +1,48 @@ +import type { SubmitHandler } from 'react-hook-form'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; + +import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema'; + +import { AddExpenseItemFormSchema } from '../AddExpenseItemForm.schema'; + +export type UseAddExpenseItemFormArgs = { + disabled?: boolean; + onSubmit: (data: AddExpenseItemFormValues) => Promise | void; +}; + +export const useAddExpenseItemForm = (args: UseAddExpenseItemFormArgs) => { + const { disabled, onSubmit } = args; + + const methods = useForm({ + defaultValues: { + amount: 0, + count: 1, + date: new Date(Date.now()), + description: '', + paymentType: 'none', + title: '', + }, + disabled, + resolver: zodResolver(AddExpenseItemFormSchema), + shouldUnregister: true, + }); + const { + formState, + handleSubmit, + watch, + } = methods; + + const onFormSubmitHandler: SubmitHandler = async (data) => { + await onSubmit(data); + }; + + return { + formState, + handleSubmit, + methods, + onFormSubmitHandler, + watch, + }; +}; diff --git a/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/index.ts b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/index.ts index 27dbfde..2837df8 100644 --- a/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/index.ts +++ b/frontend/src/entity/expenses/item/ui/AddExpenseItemForm/index.ts @@ -1,2 +1,5 @@ -export type { AddExpenseItemFormProps, AddExpenseItemFormValues } from './AddExpenseItemForm.ui'; +export type { AddExpenseItemFormValues } from './AddExpenseItemForm.schema'; +export { AddExpenseItemFormSchema } from './AddExpenseItemForm.schema'; + +export type { AddExpenseItemFormProps } from './AddExpenseItemForm.ui'; export { default as AddExpenseItemForm } from './AddExpenseItemForm.ui'; diff --git a/frontend/src/entity/expenses/item/ui/DeleteExpenseItemForm/DeleteExpenseItemForm.test.tsx b/frontend/src/entity/expenses/item/ui/DeleteExpenseItemForm/DeleteExpenseItemForm.test.tsx index b1475ae..ce37e2f 100644 --- a/frontend/src/entity/expenses/item/ui/DeleteExpenseItemForm/DeleteExpenseItemForm.test.tsx +++ b/frontend/src/entity/expenses/item/ui/DeleteExpenseItemForm/DeleteExpenseItemForm.test.tsx @@ -13,6 +13,6 @@ describe('Test DeleteExpenseItemForm', () => { await DeleteExpenseItemFormHelper.submitForm(); expect(onSubmit).toHaveBeenCalledTimes(1); - expect(onSubmit).toHaveBeenCalledWith(DeleteExpenseItemFormHelper.expenseItem); + expect(onSubmit).toHaveBeenCalledWith(); }); }); diff --git a/frontend/src/entity/expenses/item/ui/DeleteExpenseItemForm/DeleteExpenseItemForm.ui.tsx b/frontend/src/entity/expenses/item/ui/DeleteExpenseItemForm/DeleteExpenseItemForm.ui.tsx index 5f928f0..5ae10d3 100644 --- a/frontend/src/entity/expenses/item/ui/DeleteExpenseItemForm/DeleteExpenseItemForm.ui.tsx +++ b/frontend/src/entity/expenses/item/ui/DeleteExpenseItemForm/DeleteExpenseItemForm.ui.tsx @@ -1,20 +1,14 @@ -import type { - FC, - FormEventHandler, - FormHTMLAttributes, - RefObject, -} from 'react'; +import type { FC, FormHTMLAttributes, RefObject } from 'react'; import { Button, Typography } from '@mui/material'; import { DataTestId } from '@/shared/constants'; -import type { ExpenseListItem } from '../../types'; +import type { UseDeleteExpenseItemFormArgs } from './hooks/useDeleteExpenseItemForm'; -export type DeleteExpenseItemFormProps = BaseFormProps & { - disabled?: boolean; - item: ExpenseListItem; - onSubmit?: (item: ExpenseListItem) => Promise | void; +import { useDeleteExpenseItemForm } from './hooks/useDeleteExpenseItemForm'; + +export type DeleteExpenseItemFormProps = BaseFormProps & UseDeleteExpenseItemFormArgs & { ref?: RefObject; }; @@ -23,20 +17,17 @@ type BaseFormProps = Omit, 'onSubmit'>; const DeleteExpenseItemForm: FC = (props) => { const { disabled, - item, onSubmit, ref, ...restProps } = props; - const onFormSubmitHandler: FormEventHandler = (event) => { - event.preventDefault(); - onSubmit?.(item); - }; + + const { formState, handleSubmit, onFormSubmitHandler } = useDeleteExpenseItemForm({ disabled, onSubmit }); return (
@@ -47,9 +38,8 @@ const DeleteExpenseItemForm: FC = (props) => { - + + Редактировать элемент расхода + + + + + + + + + + + + + + + {watch('paymentType') === 'BANK_CARD' && } + + {watch('paymentType') === 'CASH' && } + + + + ); }; diff --git a/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/AmountField.tsx b/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/AmountField.tsx new file mode 100644 index 0000000..6cdd3ed --- /dev/null +++ b/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/AmountField.tsx @@ -0,0 +1,33 @@ +import { TextField } from '@mui/material'; +import { Controller, useFormContext } from 'react-hook-form'; + +import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema'; + +const AmountField = () => { + const { control, formState } = useFormContext>(); + + return ( + ( + { + field.onChange(Number(event.target.value)); + }} + required + size="small" + type="number" + /> + )} + /> + ); +}; + +export default AmountField; diff --git a/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/BankCardField.tsx b/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/BankCardField.tsx new file mode 100644 index 0000000..7a07169 --- /dev/null +++ b/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/BankCardField.tsx @@ -0,0 +1,58 @@ +import { + FormControl, + FormHelperText, + InputLabel, + MenuItem, + Select, +} from '@mui/material'; +import { Controller, useFormContext } from 'react-hook-form'; + +import { useBankCards } from '@/entity/cards'; +import { formatRub } from '@/shared/utils'; + +import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema'; + +const BankCardField = () => { + const { control, formState } = useFormContext>(); + + const { data: bankCards } = useBankCards(); + + const bankCardOptions = bankCards?.map((card) => ( + + {`${card.name} (${formatRub(card.balance)})`} + + )); + + return ( + ( + + Карта + + + + + {formState.errors['bank-card']?.message} + + + )} + /> + ); +}; + +export default BankCardField; diff --git a/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/CashField.tsx b/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/CashField.tsx new file mode 100644 index 0000000..d4db994 --- /dev/null +++ b/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/CashField.tsx @@ -0,0 +1,57 @@ +import { + FormControl, + FormHelperText, + InputLabel, + MenuItem, + Select, +} from '@mui/material'; +import { Controller, useFormContext } from 'react-hook-form'; + +import { useCash } from '@/entity/cash'; +import { formatRub } from '@/shared/utils'; + +import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema'; + +const CashField = () => { + const { control, formState } = useFormContext>(); + const { data: cash } = useCash(); + + const cashOptions = cash?.map((cashItem) => ( + + {`${cashItem.name} (${formatRub(cashItem.balance)})`} + + )); + + return ( + ( + + Наличные + + + + + {formState.errors.cash?.message} + + + )} + /> + ); +}; + +export default CashField; diff --git a/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/CountField.tsx b/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/CountField.tsx new file mode 100644 index 0000000..0167bd1 --- /dev/null +++ b/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/CountField.tsx @@ -0,0 +1,33 @@ +import { TextField } from '@mui/material'; +import { Controller, useFormContext } from 'react-hook-form'; + +import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema'; + +const CountField = () => { + const { control, formState } = useFormContext>(); + + return ( + ( + { + field.onChange(Number(event.target.value)); + }} + size="small" + type="number" + /> + )} + /> + ); +}; + +export default CountField; diff --git a/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/DateField.tsx b/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/DateField.tsx new file mode 100644 index 0000000..0ce469e --- /dev/null +++ b/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/DateField.tsx @@ -0,0 +1,36 @@ +import type { FC } from 'react'; + +import { DatePicker } from '@mui/x-date-pickers'; +import { Controller, useFormContext } from 'react-hook-form'; + +import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema'; + +const DateField: FC = () => { + const { control, formState } = useFormContext>(); + + return ( + ( + + )} + /> + ); +}; + +export default DateField; diff --git a/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/DescriptionField.tsx b/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/DescriptionField.tsx new file mode 100644 index 0000000..1a2e151 --- /dev/null +++ b/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/DescriptionField.tsx @@ -0,0 +1,29 @@ +import type { FC } from 'react'; + +import { TextField } from '@mui/material'; +import { Controller, useFormContext } from 'react-hook-form'; + +import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema'; + +const DescriptionField: FC = () => { + const { control, formState } = useFormContext>(); + + return ( + ( + + )} + /> + ); +}; + +export default DescriptionField; diff --git a/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/PaymentTypeField.tsx b/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/PaymentTypeField.tsx new file mode 100644 index 0000000..1a1a368 --- /dev/null +++ b/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/PaymentTypeField.tsx @@ -0,0 +1,69 @@ +import type { FC } from 'react'; + +import { + FormControl, + FormHelperText, + InputLabel, + MenuItem, + Select, +} from '@mui/material'; +import { useMemo } from 'react'; +import { Controller, useFormContext } from 'react-hook-form'; + +import { useBankCards } from '@/entity/cards'; +import { useCash } from '@/entity/cash'; + +import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema'; + +const PaymentTypeField: FC = () => { + const { control, formState } = useFormContext>(); + + const { data: bankCards } = useBankCards(); + const { data: cash } = useCash(); + + const paymentTypeOptions = useMemo(() => { + const options = [Без оплаты]; + + if (bankCards && bankCards.length > 0) { + options.push(Банковская карта); + } + + if (cash && cash.length > 0) { + options.push(Наличные); + } + + return options; + }, [bankCards, cash]); + + return ( + ( + + Способ оплаты + + + + + {formState.errors.paymentType?.message} + + + )} + /> + ); +}; + +export default PaymentTypeField; diff --git a/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/TitleField.tsx b/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/TitleField.tsx new file mode 100644 index 0000000..1fafc63 --- /dev/null +++ b/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/fields/TitleField.tsx @@ -0,0 +1,30 @@ +import type { FC } from 'react'; + +import { TextField } from '@mui/material'; +import { Controller, useFormContext } from 'react-hook-form'; + +import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema'; + +const TitleField: FC = () => { + const { control, formState } = useFormContext>(); + + return ( + ( + + )} + /> + ); +}; + +export default TitleField; diff --git a/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/hooks/useEditExpenseItemForm.ts b/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/hooks/useEditExpenseItemForm.ts new file mode 100644 index 0000000..a64da28 --- /dev/null +++ b/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/hooks/useEditExpenseItemForm.ts @@ -0,0 +1,55 @@ +import type { SubmitHandler } from 'react-hook-form'; + +import { zodResolver } from '@hookform/resolvers/zod'; +import { useForm } from 'react-hook-form'; + +import type { ExpenseListItem } from '@/entity/expenses/item'; + +import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema'; + +import { EditExpenseItemFormSchema } from '../EditExpenseItemForm.schema'; + +export type UseEditExpenseItemFormArgs = { + disabled?: boolean; + item: ExpenseListItem; + onSubmit: (data: EditExpenseItemFormValues) => Promise | void; +}; + +export const useEditExpenseItemForm = (args: UseEditExpenseItemFormArgs) => { + const { disabled, item, onSubmit } = args; + + const methods = useForm({ + defaultValues: { + amount: item.amount, + // eslint-disable-next-line no-undefined + 'bank-card': item.payment?.type === 'BANK_CARD' ? item.payment.id ?? undefined : undefined, + // eslint-disable-next-line no-undefined + cash: item.payment?.type === 'CASH' ? item.payment.id ?? undefined : undefined, + count: item.count ?? 1, + date: new Date(item.date), + description: item.description ?? '', + paymentType: item.payment?.type ?? 'none', + title: item.title, + }, + disabled, + resolver: zodResolver(EditExpenseItemFormSchema), + shouldUnregister: true, + }); + const { + formState, + handleSubmit, + watch, + } = methods; + + const onFormSubmitHandler: SubmitHandler = async (data) => { + await onSubmit(data); + }; + + return { + formState, + handleSubmit, + methods, + onFormSubmitHandler, + watch, + }; +}; diff --git a/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/index.ts b/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/index.ts new file mode 100644 index 0000000..d46cc4b --- /dev/null +++ b/frontend/src/entity/expenses/item/ui/EditExpenseItemForm/index.ts @@ -0,0 +1,4 @@ +export type { EditExpenseItemFormValues } from './EditExpenseItemForm.schema'; +export { EditExpenseItemFormSchema } from './EditExpenseItemForm.schema'; +export type { EditExpenseItemFormProps } from './EditExpenseItemForm.ui'; +export { default as EditExpenseItemForm } from './EditExpenseItemForm.ui'; diff --git a/frontend/src/features/expenses/items/AddExpenseItemButton/hooks/useAddExpenseItemButton.ts b/frontend/src/features/expenses/items/AddExpenseItemButton/hooks/useAddExpenseItemButton.ts index 01731c9..bc1fe8a 100644 --- a/frontend/src/features/expenses/items/AddExpenseItemButton/hooks/useAddExpenseItemButton.ts +++ b/frontend/src/features/expenses/items/AddExpenseItemButton/hooks/useAddExpenseItemButton.ts @@ -1,5 +1,6 @@ import type { ButtonHTMLAttributes, MouseEventHandler } from 'react'; +import { enqueueSnackbar } from 'notistack'; import { useState } from 'react'; import type { AddExpenseItemFormValues } from '@/entity/expenses/item'; @@ -26,10 +27,30 @@ export const useAddExpenseItemButton = (args: UseAddExpenseItemButtonArgs) => { }; const onFormSubmitHandler = async (data: AddExpenseItemFormValues) => { - // todo добавить обработку ошибок (код + тест) - await addExpenseItemMutation(data); + try { + const payment = data.paymentType === 'none' + ? null + : { + id: data.paymentType === 'BANK_CARD' ? data['bank-card'] : data.cash, + type: data.paymentType, + }; + await addExpenseItemMutation({ + amount: data.amount, + count: data.count, + currency: 'RUB', + date: data.date.toISOString(), + description: data.description ?? null, + // @ts-ignore TODO + payment, + title: data.title, + }); - setIsOpen(false); + enqueueSnackbar(`Расход "${data.title}" успешно добавлен`, { variant: 'success' }); + + setIsOpen(false); + } catch { + enqueueSnackbar('Ошибка при добавлении расхода', { variant: 'error' }); + } }; return { diff --git a/frontend/src/features/expenses/items/AddExpenseItemButton/ui/AddExpenseItemButton.test.tsx b/frontend/src/features/expenses/items/AddExpenseItemButton/ui/AddExpenseItemButton.test.tsx index b263123..1346290 100644 --- a/frontend/src/features/expenses/items/AddExpenseItemButton/ui/AddExpenseItemButton.test.tsx +++ b/frontend/src/features/expenses/items/AddExpenseItemButton/ui/AddExpenseItemButton.test.tsx @@ -1,3 +1,4 @@ +import { mockEnqueueSnackbar } from '@mocks/jest/helpers'; import { server } from '@mocks/jest/server'; import { screen, waitFor } from '@testing-library/react'; import { AddExpenseItemButtonHelper, AddExpenseItemFormHelper } from '@tests/helpers'; @@ -22,7 +23,7 @@ describe('Test AddExpenseItemButton', () => { expect(modalElem).toBeVisible(); }); - describe('should close modal after submit and send request', () => { + describe('should close modal after success request', () => { let modalElem: HTMLElement; const requestSpy = jest.fn(); @@ -36,11 +37,10 @@ describe('Test AddExpenseItemButton', () => { date: '29.08.2025', description: 'Описание элемента', title: 'Название элемента', - }, { autoSubmit: false }); + }); }); - test('should send correct request', async () => { - await AddExpenseItemFormHelper.submitForm(); + test('should send correct request', () => { expect(requestSpy).toHaveBeenCalledTimes(1); expect(requestSpy).toHaveBeenCalledWith({ amount: 12, @@ -54,10 +54,41 @@ describe('Test AddExpenseItemButton', () => { }); test('should close modal after submit', async () => { - await AddExpenseItemFormHelper.submitForm(); await waitFor(() => { expect(modalElem).not.toBeInTheDocument(); }); }); + + test('should show success notification', () => { + expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1); + expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Расход "Название элемента" успешно добавлен', { variant: 'success' }); + }); + }); + + describe('should not close modal after failed request', () => { + let modalElem: HTMLElement; + const requestSpy = jest.fn(); + + beforeEach(async () => { + server.use(AddExpenseItemButtonHelper.submitHandler(requestSpy, 500)); + modalElem = (await AddExpenseItemButtonHelper.openModal()).modalElem; + + await AddExpenseItemFormHelper.fillForm({ + amount: '12', + count: '1', + date: '29.08.2025', + description: 'Описание элемента', + title: 'Название элемента', + }); + }); + + test('should not close modal after submit', () => { + expect(modalElem).toBeInTheDocument(); + }); + + test('should show error notification', () => { + expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1); + expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Ошибка при добавлении расхода', { variant: 'error' }); + }); }); }); diff --git a/frontend/src/features/expenses/items/AddExpenseItemButton/ui/AddExpenseItemButton.ui.tsx b/frontend/src/features/expenses/items/AddExpenseItemButton/ui/AddExpenseItemButton.ui.tsx index 59f8820..17395ca 100644 --- a/frontend/src/features/expenses/items/AddExpenseItemButton/ui/AddExpenseItemButton.ui.tsx +++ b/frontend/src/features/expenses/items/AddExpenseItemButton/ui/AddExpenseItemButton.ui.tsx @@ -44,7 +44,7 @@ const AddExpenseItemButton: FC = (props) => { Добавить элемент - + diff --git a/frontend/src/features/expenses/items/DeleteExpenseItemButton/hooks/useDeleteExpenseItemButton.ts b/frontend/src/features/expenses/items/DeleteExpenseItemButton/hooks/useDeleteExpenseItemButton.ts index 0b730c8..dd3a1a4 100644 --- a/frontend/src/features/expenses/items/DeleteExpenseItemButton/hooks/useDeleteExpenseItemButton.ts +++ b/frontend/src/features/expenses/items/DeleteExpenseItemButton/hooks/useDeleteExpenseItemButton.ts @@ -1,5 +1,6 @@ import type { MouseEventHandler } from 'react'; +import { enqueueSnackbar } from 'notistack'; import { useState } from 'react'; import { type ExpenseListItem, useDeleteExpenseItem } from '@/entity/expenses/item'; @@ -27,8 +28,14 @@ export const useDeleteExpenseItemButton = (args: UseDeleteExpenseItemButtonArgs) }); const onFormSubmitHandler = async () => { - await deleteExpenseItemMutation(); - setIsOpen(false); + try { + await deleteExpenseItemMutation(); + + enqueueSnackbar(`Расход "${item.title}" удален`, { variant: 'success' }); + setIsOpen(false); + } catch { + enqueueSnackbar(`Ошибка при удалении расхода "${item.title}"`, { variant: 'error' }); + } }; return { diff --git a/frontend/src/features/expenses/items/DeleteExpenseItemButton/ui/DeleteExpenseItemButton.test.tsx b/frontend/src/features/expenses/items/DeleteExpenseItemButton/ui/DeleteExpenseItemButton.test.tsx index a3b5fb5..4cfe7b6 100644 --- a/frontend/src/features/expenses/items/DeleteExpenseItemButton/ui/DeleteExpenseItemButton.test.tsx +++ b/frontend/src/features/expenses/items/DeleteExpenseItemButton/ui/DeleteExpenseItemButton.test.tsx @@ -1,6 +1,6 @@ +import { mockEnqueueSnackbar } from '@mocks/jest/helpers'; import { server } from '@mocks/jest/server'; import { screen, waitFor } from '@testing-library/react'; -import userEvent from '@testing-library/user-event'; import { DeleteExpenseItemButtonHelper, DeleteExpenseItemFormHelper } from '@tests/helpers'; describe('Test DeleteExpenseItemButton', () => { @@ -23,15 +23,51 @@ describe('Test DeleteExpenseItemButton', () => { expect(modalElem).toBeVisible(); }); - test('should close modal after submit', async () => { - server.use(DeleteExpenseItemButtonHelper.submitHandler); - const { modalElem } = await DeleteExpenseItemButtonHelper.openModal(); - const { submitButtonElem } = DeleteExpenseItemFormHelper.getFormElements(); + describe('should close modal after success request', () => { + let modalElem: HTMLElement; + const requestSpy = jest.fn(); - expect(submitButtonElem).toBeEnabled(); - await userEvent.click(submitButtonElem); - await waitFor(() => { - expect(modalElem).not.toBeInTheDocument(); + beforeEach(async () => { + server.use(DeleteExpenseItemButtonHelper.submitHandler(requestSpy)); + modalElem = (await DeleteExpenseItemButtonHelper.openModal()).modalElem; + + await DeleteExpenseItemFormHelper.submitForm(); + }); + + test('should send correct request', () => { + expect(requestSpy).toHaveBeenCalledTimes(1); + }); + + test('should close modal after submit', async () => { + await waitFor(() => { + expect(modalElem).not.toBeInTheDocument(); + }); + }); + + test('should show success notification', () => { + expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1); + expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Расход "Mock item" удален', { variant: 'success' }); + }); + }); + + describe('should not close modal after failed request', () => { + let modalElem: HTMLElement; + const requestSpy = jest.fn(); + + beforeEach(async () => { + server.use(DeleteExpenseItemButtonHelper.submitHandler(requestSpy, 500)); + modalElem = (await DeleteExpenseItemButtonHelper.openModal()).modalElem; + + await DeleteExpenseItemFormHelper.submitForm(); + }); + + test('should not close modal after submit', () => { + expect(modalElem).toBeInTheDocument(); + }); + + test('should show error notification', () => { + expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1); + expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Ошибка при удалении расхода "Mock item"', { variant: 'error' }); }); }); }); diff --git a/frontend/src/features/expenses/items/DeleteExpenseItemButton/ui/DeleteExpenseItemButton.ui.tsx b/frontend/src/features/expenses/items/DeleteExpenseItemButton/ui/DeleteExpenseItemButton.ui.tsx index aeb93af..b2552dc 100644 --- a/frontend/src/features/expenses/items/DeleteExpenseItemButton/ui/DeleteExpenseItemButton.ui.tsx +++ b/frontend/src/features/expenses/items/DeleteExpenseItemButton/ui/DeleteExpenseItemButton.ui.tsx @@ -44,7 +44,7 @@ const DeleteExpenseItemButton: FC = (props) => { - + ); diff --git a/frontend/src/features/expenses/items/EditExpenseItemButton/hooks/useEditExpenseItemButton.ts b/frontend/src/features/expenses/items/EditExpenseItemButton/hooks/useEditExpenseItemButton.ts index c3f23cf..5ca2273 100644 --- a/frontend/src/features/expenses/items/EditExpenseItemButton/hooks/useEditExpenseItemButton.ts +++ b/frontend/src/features/expenses/items/EditExpenseItemButton/hooks/useEditExpenseItemButton.ts @@ -1,5 +1,6 @@ import type { MouseEventHandler } from 'react'; +import { enqueueSnackbar } from 'notistack'; import { useState } from 'react'; import { type EditExpenseItemFormValues, type ExpenseListItem, useEditExpenseItem } from '@/entity/expenses/item'; @@ -27,9 +28,30 @@ export const useEditExpenseItemButton = (args: UseAddExpenseItemButtonArgs) => { }; const onFormSubmitHandler = async (data: EditExpenseItemFormValues) => { - // todo добавить обработку ошибок (код + тест) - await editExpenseItemMutation(data); - setIsOpen(false); + try { + const payment = data.paymentType === 'none' + ? null + : { + id: data.paymentType === 'BANK_CARD' ? data['bank-card'] : data.cash, + type: data.paymentType, + }; + await editExpenseItemMutation({ + amount: data.amount, + count: data.count, + currency: 'RUB', + date: data.date.toISOString(), + description: data.description ?? null, + // @ts-ignore TODO + payment, + title: data.title, + }); + + enqueueSnackbar(`Расход "${data.title}" успешно сохранен`, { variant: 'success' }); + + setIsOpen(false); + } catch { + enqueueSnackbar('Ошибка при сохранении расхода', { variant: 'error' }); + } }; return { diff --git a/frontend/src/features/expenses/items/EditExpenseItemButton/ui/EditExpenseItemButton.test.tsx b/frontend/src/features/expenses/items/EditExpenseItemButton/ui/EditExpenseItemButton.test.tsx index b66e430..52bae57 100644 --- a/frontend/src/features/expenses/items/EditExpenseItemButton/ui/EditExpenseItemButton.test.tsx +++ b/frontend/src/features/expenses/items/EditExpenseItemButton/ui/EditExpenseItemButton.test.tsx @@ -1,3 +1,4 @@ +import { mockEnqueueSnackbar } from '@mocks/jest/helpers'; import { server } from '@mocks/jest/server'; import { screen, waitFor } from '@testing-library/react'; import { EditExpenseItemButtonHelper, EditExpenseItemFormHelper } from '@tests/helpers'; @@ -22,7 +23,7 @@ describe('TestEditExpenseItemButton', () => { expect(modalElem).toBeVisible(); }); - describe('should close modal after submit and send request', () => { + describe('should close modal after success request', () => { let modalElem: HTMLElement; const requestSpy = jest.fn(); @@ -35,12 +36,12 @@ describe('TestEditExpenseItemButton', () => { count: 1, date: '29.08.2025', description: 'Описание элемента', + paymentType: 'none', title: 'Название элемента', - }, { autoSubmit: false }); + }); }); - test('should send correct request', async () => { - await EditExpenseItemFormHelper.submitForm(); + test('should send correct request', () => { expect(requestSpy).toHaveBeenCalledTimes(1); expect(requestSpy).toHaveBeenCalledWith({ amount: 12, @@ -48,15 +49,48 @@ describe('TestEditExpenseItemButton', () => { currency: 'RUB', date: '2025-08-29T00:00:00.000Z', description: 'Описание элемента', + payment: null, title: 'Название элемента', }); }); test('should close modal after submit', async () => { - await EditExpenseItemFormHelper.submitForm(); await waitFor(() => { expect(modalElem).not.toBeInTheDocument(); }); }); + + test('should show success notification', () => { + expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1); + expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Расход "Название элемента" успешно сохранен', { variant: 'success' }); + }); + }); + + describe('should not close modal after failed request', () => { + let modalElem: HTMLElement; + const requestSpy = jest.fn(); + + beforeEach(async () => { + server.use(EditExpenseItemButtonHelper.submitHandler(requestSpy, 500)); + modalElem = (await EditExpenseItemButtonHelper.openModal()).modalElem; + + await EditExpenseItemFormHelper.fillForm({ + amount: 12, + count: 1, + date: '29.08.2025', + description: 'Описание элемента', + paymentType: 'none', + title: 'Название элемента', + }); + }); + + test('should not close modal after submit', () => { + expect(modalElem).toBeInTheDocument(); + }); + + test('should show error notification', () => { + expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1); + expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Ошибка при сохранении расхода', { variant: 'error' }); + }); }); }); diff --git a/frontend/src/features/expenses/items/EditExpenseItemButton/ui/EditExpenseItemButton.ui.tsx b/frontend/src/features/expenses/items/EditExpenseItemButton/ui/EditExpenseItemButton.ui.tsx index 4e11ccb..d89e8e2 100644 --- a/frontend/src/features/expenses/items/EditExpenseItemButton/ui/EditExpenseItemButton.ui.tsx +++ b/frontend/src/features/expenses/items/EditExpenseItemButton/ui/EditExpenseItemButton.ui.tsx @@ -45,7 +45,7 @@ const EditExpenseItemButton: FC = (props) => { Редактировать - + diff --git a/frontend/src/pages/Expense/ui/ExpensePage.ui.tsx b/frontend/src/pages/Expense/ui/ExpensePage.ui.tsx index 2a2baf4..9a2b5e7 100644 --- a/frontend/src/pages/Expense/ui/ExpensePage.ui.tsx +++ b/frontend/src/pages/Expense/ui/ExpensePage.ui.tsx @@ -32,7 +32,12 @@ const ExpensePage = () => { return ( - Списки расходов + Элементы расхода + {' '} + + " + {expenseList?.title} + " diff --git a/frontend/tests/helpers/expense/item/AddExpenseItemButton.tsx b/frontend/tests/helpers/expense/item/AddExpenseItemButton.tsx index 93582df..8e91ff4 100644 --- a/frontend/tests/helpers/expense/item/AddExpenseItemButton.tsx +++ b/frontend/tests/helpers/expense/item/AddExpenseItemButton.tsx @@ -15,7 +15,7 @@ const expenseItem = { count: 12, createdAt: '2025-08-29T07:57:40.503Z', currency: 'RUB', - date: '2025-08-22T00:00:03.000Z', + date: '2025-08-22T00:00:00.000Z', description: 'Mock description', expenseListId: '10cc093c-3875-4d33-a40a-df526266e262', id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6', @@ -48,7 +48,7 @@ const openModal = async () => { }; }; -const submitHandler = (requestSpy: jest.Mock) => http.post< +const submitHandler = (requestSpy: jest.Mock, status = 200) => http.post< AddExpenseItemPath, AddExpenseItemBody, AddExpenseItemSuccessResponse @@ -58,7 +58,7 @@ const submitHandler = (requestSpy: jest.Mock) => http.post< const body = await request.json(); requestSpy(body); - return HttpResponse.json(expenseItem); + return HttpResponse.json(expenseItem, { status }); }, ); diff --git a/frontend/tests/helpers/expense/item/DeleteExpenseItemButton.tsx b/frontend/tests/helpers/expense/item/DeleteExpenseItemButton.tsx index 4f15c50..6c03ee5 100644 --- a/frontend/tests/helpers/expense/item/DeleteExpenseItemButton.tsx +++ b/frontend/tests/helpers/expense/item/DeleteExpenseItemButton.tsx @@ -13,7 +13,7 @@ const expenseItem = { count: 12, createdAt: '2025-08-29T07:57:40.503Z', currency: 'RUB', - date: '2025-08-22T00:00:03.000Z', + date: '2025-08-22T00:00:00.000Z', description: 'Mock description', expenseListId: '10cc093c-3875-4d33-a40a-df526266e262', id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6', @@ -45,9 +45,13 @@ const openModal = async () => { }; }; -const submitHandler = http.delete( +const submitHandler = (requestSpy: jest.Mock, status = 200) => http.delete( `/expenses/list/${expenseItem.expenseListId}/items/${expenseItem.id}`, - () => HttpResponse.json(expenseItem), + () => { + requestSpy(expenseItem); + + return HttpResponse.json(expenseItem, { status }); + }, ); export const DeleteExpenseItemButtonHelper = { diff --git a/frontend/tests/helpers/expense/item/DeleteExpenseItemForm.tsx b/frontend/tests/helpers/expense/item/DeleteExpenseItemForm.tsx index 5b5b2c7..eb619e3 100644 --- a/frontend/tests/helpers/expense/item/DeleteExpenseItemForm.tsx +++ b/frontend/tests/helpers/expense/item/DeleteExpenseItemForm.tsx @@ -11,7 +11,7 @@ const expenseItem = { count: 12, createdAt: '2025-08-29T07:57:40.503Z', currency: 'RUB', - date: '2025-08-22T00:00:03.000Z', + date: '2025-08-22T00:00:00.000Z', description: 'Mock description', expenseListId: '10cc093c-3875-4d33-a40a-df526266e262', id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6', @@ -25,7 +25,6 @@ const renderForm = (props?: Partial) => { render( , diff --git a/frontend/tests/helpers/expense/item/EditExpenseItemButton.tsx b/frontend/tests/helpers/expense/item/EditExpenseItemButton.tsx index addc818..8f5d21a 100644 --- a/frontend/tests/helpers/expense/item/EditExpenseItemButton.tsx +++ b/frontend/tests/helpers/expense/item/EditExpenseItemButton.tsx @@ -1,3 +1,5 @@ +import { getBankCardList, getCashList } from '@mocks/browser/handlers'; +import { server } from '@mocks/jest/server'; import { createAppWrapper } from '@mocks/jest/wrappers'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -17,7 +19,7 @@ const expenseItem = { count: 12, createdAt: '2025-08-29T07:57:40.503Z', currency: 'RUB', - date: '2025-08-22T00:00:03.000Z', + date: '2025-08-22T00:00:00.000Z', description: 'Mock description', expenseListId: '10cc093c-3875-4d33-a40a-df526266e262', id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6', @@ -27,6 +29,7 @@ const expenseItem = { }; const renderButton = () => { + server.use(getBankCardList, getCashList); render( , { wrapper: createAppWrapper() }, @@ -49,7 +52,7 @@ const openModal = async () => { }; }; -const submitHandler = (requestSpy: jest.Mock) => http.patch< +const submitHandler = (requestSpy: jest.Mock, status = 200) => http.patch< EditExpenseItemPath, EditExpenseItemBody, EditExpenseItemSuccessResponse @@ -59,7 +62,7 @@ const submitHandler = (requestSpy: jest.Mock) => http.patch< const body = await request.json(); requestSpy(body); - return HttpResponse.json(expenseItem); + return HttpResponse.json(expenseItem, { status }); }, ); diff --git a/frontend/tests/helpers/expense/item/EditExpenseItemForm.tsx b/frontend/tests/helpers/expense/item/EditExpenseItemForm.tsx index 5c287ef..9c3b0f5 100644 --- a/frontend/tests/helpers/expense/item/EditExpenseItemForm.tsx +++ b/frontend/tests/helpers/expense/item/EditExpenseItemForm.tsx @@ -1,4 +1,6 @@ +import { getBankCardList, getCashList } from '@mocks/browser/handlers'; import { typeDateField, typeTextField } from '@mocks/jest/helpers'; +import { server } from '@mocks/jest/server'; import { createAppWrapper } from '@mocks/jest/wrappers'; import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; @@ -13,7 +15,7 @@ const expenseItem = { count: 12, createdAt: '2025-08-22T07:57:40.503Z', currency: 'RUB', - date: '2025-08-22T00:00:03.000Z', + date: '2025-08-22T00:00:00.000Z', description: 'Mock description', expenseListId: '10cc093c-3875-4d33-a40a-df526266e262', id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6', @@ -23,6 +25,7 @@ const expenseItem = { }; const renderForm = (props?: Partial) => { + server.use(getBankCardList, getCashList); const onSubmit = jest.fn(); render( @@ -46,14 +49,27 @@ const getFormElements = () => { const dateInputElem = screen.getAllByLabelText(/Дата/i)[1]; const amountInputElem = screen.getByLabelText(/Сумма/); const countInputElem = screen.getByLabelText(/Количество/); - const submitButtonElem = screen.getByRole('button', { name: /Сохранить/i }); + const submitButtonElem = screen.getByTestId(DataTestId.EDIT_EXPENSE_ITEM_SUBMIT_BUTTON); + + const paymentTypeSelect = screen.getByTestId('payment-type-select'); + + const bankCardSelect = screen.queryByTestId('bank-card-select'); + const bankCardSelectInput = screen.queryByTestId('bank-card-select-input'); + + const cashSelect = screen.queryByTestId('cash-select'); + const cashSelectInput = screen.queryByTestId('cash-select-input'); return { amountInputElem, + bankCardSelect, + bankCardSelectInput, + cashSelect, + cashSelectInput, countInputElem, dateInputElem, descriptionInputElem, formElem, + paymentTypeSelect, submitButtonElem, titleInputElem, }; @@ -65,7 +81,7 @@ const submitForm = async () => { }; const fillForm = async ( - data: Omit, + data: Omit & { date: string }, options: { autoSubmit?: boolean } = {}, ) => { const {