feat(frontend): add payment type
This commit is contained in:
parent
3fba931281
commit
6ed6cf8782
23
frontend/__mocks__/browser/data/bankCards/list.ts
Normal file
23
frontend/__mocks__/browser/data/bankCards/list.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
export const bankCardList = [
|
||||||
|
{
|
||||||
|
balance: 0,
|
||||||
|
createdAt: '2025-09-09T01:58:06.993Z',
|
||||||
|
id: 'e2a7beee-2a1f-4881-97c5-0a7793c78adf',
|
||||||
|
name: 'Сбербанк',
|
||||||
|
updatedAt: '2025-09-09T01:58:06.993Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
balance: 100,
|
||||||
|
createdAt: '2025-09-09T01:59:47.698Z',
|
||||||
|
id: '4360f93c-0e8e-4285-9b0d-ba5701f43013',
|
||||||
|
name: 'Тинькофф !',
|
||||||
|
updatedAt: '2025-09-11T02:33:10.293Z',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
balance: 200,
|
||||||
|
createdAt: '2025-09-10T02:59:43.228Z',
|
||||||
|
id: 'b9f8eef6-5e8a-4e64-9e10-d0d6fabf05dc',
|
||||||
|
name: 'Альфа-Банк',
|
||||||
|
updatedAt: '2025-09-11T02:33:07.601Z',
|
||||||
|
},
|
||||||
|
];
|
||||||
18
frontend/__mocks__/browser/data/cash/list.ts
Normal file
18
frontend/__mocks__/browser/data/cash/list.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
export const cashList = [
|
||||||
|
{
|
||||||
|
balance: 41300,
|
||||||
|
createdAt: '2025-09-12T03:04:42.222Z',
|
||||||
|
id: '07d99f18-0618-444f-af5a-c5084d79bc31',
|
||||||
|
name: 'Аренда квартиры',
|
||||||
|
updatedAt: '2025-09-12T04:06:48.224Z',
|
||||||
|
userId: 'a94c46ad-64bb-4d6a-ba0f-85a466f57537',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
balance: 19000,
|
||||||
|
createdAt: '2025-09-12T04:07:01.713Z',
|
||||||
|
id: 'f2d52442-c260-470c-9111-0da736f4daaf',
|
||||||
|
name: 'Свободные',
|
||||||
|
updatedAt: '2025-09-18T02:38:22.869Z',
|
||||||
|
userId: 'a94c46ad-64bb-4d6a-ba0f-85a466f57537',
|
||||||
|
},
|
||||||
|
];
|
||||||
@ -8,6 +8,7 @@ export const items1 = [
|
|||||||
description: 'Ежемесячная плата за интернет',
|
description: 'Ежемесячная плата за интернет',
|
||||||
expenseListId: '1',
|
expenseListId: '1',
|
||||||
id: '1',
|
id: '1',
|
||||||
|
payment: null,
|
||||||
title: 'Счет за интернет',
|
title: 'Счет за интернет',
|
||||||
updatedAt: '2024-04-01T12:00:00Z',
|
updatedAt: '2024-04-01T12:00:00Z',
|
||||||
},
|
},
|
||||||
@ -20,6 +21,7 @@ export const items1 = [
|
|||||||
description: 'Продукты на неделю',
|
description: 'Продукты на неделю',
|
||||||
expenseListId: '1',
|
expenseListId: '1',
|
||||||
id: '2',
|
id: '2',
|
||||||
|
payment: null,
|
||||||
title: 'Покупка продуктов',
|
title: 'Покупка продуктов',
|
||||||
updatedAt: '2024-04-02T12:00:00Z',
|
updatedAt: '2024-04-02T12:00:00Z',
|
||||||
},
|
},
|
||||||
@ -39,6 +41,7 @@ export const items4 = [
|
|||||||
description: null,
|
description: null,
|
||||||
expenseListId: '4',
|
expenseListId: '4',
|
||||||
id: '3',
|
id: '3',
|
||||||
|
payment: null,
|
||||||
title: 'Бензин',
|
title: 'Бензин',
|
||||||
updatedAt: '2024-04-03T12:00:00Z',
|
updatedAt: '2024-04-03T12:00:00Z',
|
||||||
},
|
},
|
||||||
@ -54,6 +57,7 @@ export const items5 = [
|
|||||||
description: 'Покупка нового ноутбука',
|
description: 'Покупка нового ноутбука',
|
||||||
expenseListId: '5',
|
expenseListId: '5',
|
||||||
id: '4',
|
id: '4',
|
||||||
|
payment: null,
|
||||||
title: 'Ноутбук',
|
title: 'Ноутбук',
|
||||||
updatedAt: '2024-04-04T12:00:00Z',
|
updatedAt: '2024-04-04T12:00:00Z',
|
||||||
},
|
},
|
||||||
@ -66,6 +70,7 @@ export const items5 = [
|
|||||||
description: 'Механическая мышка',
|
description: 'Механическая мышка',
|
||||||
expenseListId: '5',
|
expenseListId: '5',
|
||||||
id: '5',
|
id: '5',
|
||||||
|
payment: null,
|
||||||
title: 'Мышка',
|
title: 'Мышка',
|
||||||
updatedAt: '2024-04-05T12:00:00Z',
|
updatedAt: '2024-04-05T12:00:00Z',
|
||||||
},
|
},
|
||||||
|
|||||||
2
frontend/__mocks__/browser/data/index.ts
Normal file
2
frontend/__mocks__/browser/data/index.ts
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
export { bankCardList } from './bankCards/list';
|
||||||
|
export { cashList } from './cash/list';
|
||||||
11
frontend/__mocks__/browser/handlers/bank-cards/list/get.ts
Normal file
11
frontend/__mocks__/browser/handlers/bank-cards/list/get.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { http, HttpResponse } from 'msw';
|
||||||
|
|
||||||
|
import type { paths } from '@/shared/api/schema';
|
||||||
|
|
||||||
|
import { bankCardList } from '../../../data/bankCards/list';
|
||||||
|
|
||||||
|
type BankCardsResponse = paths['/bank-card']['get']['responses']['200']['content']['application/json'];
|
||||||
|
export const getBankCardList = http.get<never, never, BankCardsResponse>(
|
||||||
|
'/bank-card',
|
||||||
|
() => HttpResponse.json(bankCardList),
|
||||||
|
);
|
||||||
11
frontend/__mocks__/browser/handlers/cash/list/get.ts
Normal file
11
frontend/__mocks__/browser/handlers/cash/list/get.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { http, HttpResponse } from 'msw';
|
||||||
|
|
||||||
|
import type { paths } from '@/shared/api/schema';
|
||||||
|
|
||||||
|
import { cashList } from '../../../data/cash/list';
|
||||||
|
|
||||||
|
type CashResponse = paths['/cash']['get']['responses']['200']['content']['application/json'];
|
||||||
|
export const getCashList = http.get<never, never, CashResponse>(
|
||||||
|
'/cash',
|
||||||
|
() => HttpResponse.json(cashList),
|
||||||
|
);
|
||||||
@ -1,3 +1,5 @@
|
|||||||
export { loginHandler } from './auth/login/post';
|
export { loginHandler } from './auth/login/post';
|
||||||
|
export { getBankCardList } from './bank-cards/list/get';
|
||||||
|
export { getCashList } from './cash/list/get';
|
||||||
export { getExpenseItemListById } from './expense/list/:id/get';
|
export { getExpenseItemListById } from './expense/list/:id/get';
|
||||||
export { getExpenseList } from './expense/list/get';
|
export { getExpenseList } from './expense/list/get';
|
||||||
|
|||||||
@ -12,6 +12,7 @@ export default [
|
|||||||
rules: {
|
rules: {
|
||||||
'@stylistic/jsx-sort-props': 'off',
|
'@stylistic/jsx-sort-props': 'off',
|
||||||
'@typescript-eslint/no-floating-promises': 'off',
|
'@typescript-eslint/no-floating-promises': 'off',
|
||||||
|
'@typescript-eslint/sort-type-constituents': 'off',
|
||||||
'@typescript-eslint/strict-boolean-expressions': 'off',
|
'@typescript-eslint/strict-boolean-expressions': 'off',
|
||||||
'func-style': 'off',
|
'func-style': 'off',
|
||||||
'import/no-extraneous-dependencies': ['error', {
|
'import/no-extraneous-dependencies': ['error', {
|
||||||
|
|||||||
@ -1,21 +1,128 @@
|
|||||||
|
import { bankCardList, cashList } from '@mocks/browser/data';
|
||||||
|
import { getBankCardList, getCashList } from '@mocks/browser/handlers';
|
||||||
|
import { server } from '@mocks/jest/server';
|
||||||
|
import { createReactQueryWrapper } from '@mocks/jest/wrappers';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
|
import type { BankCard } from '@/entity/cards';
|
||||||
|
import type { CashItem } from '@/entity/cash';
|
||||||
|
|
||||||
import type { AddExpenseItemFormProps } from './AddExpenseItemForm.ui';
|
import type { AddExpenseItemFormProps } from './AddExpenseItemForm.ui';
|
||||||
|
|
||||||
import AddExpenseItemForm from './AddExpenseItemForm.ui';
|
import AddExpenseItemForm from './AddExpenseItemForm.ui';
|
||||||
|
|
||||||
const renderForm = (props?: Partial<AddExpenseItemFormProps>) => {
|
const getExpenseItemFormElements = () => {
|
||||||
const onSubmit = jest.fn();
|
|
||||||
|
|
||||||
render(<AddExpenseItemForm data-testid="add-expense-item-form" onSubmit={onSubmit} {...props} />);
|
|
||||||
|
|
||||||
const titleInputElem = screen.getByLabelText('Название');
|
const titleInputElem = screen.getByLabelText('Название');
|
||||||
const descriptionInputElem = screen.getByLabelText('Описание');
|
const descriptionInputElem = screen.getByLabelText('Описание');
|
||||||
const dateInputElem = screen.getByLabelText('Дата');
|
const dateInputElem = screen.getByLabelText('Дата');
|
||||||
const amountInputElem = screen.getByLabelText('Сумма');
|
const amountInputElem = screen.getByLabelText('Сумма');
|
||||||
const countInputElem = screen.getByLabelText('Количество');
|
const countInputElem = screen.getByLabelText('Количество');
|
||||||
const submitButtonElem = screen.getByText('Добавить');
|
const submitButtonElem = screen.getByText('Добавить');
|
||||||
|
const paymentTypeSelect = screen.getByLabelText('Способ оплаты');
|
||||||
|
const bankCardSelect = screen.queryByLabelText('Карта');
|
||||||
|
const cashSelect = screen.queryByLabelText('Наличные');
|
||||||
|
|
||||||
|
return {
|
||||||
|
amountInputElem,
|
||||||
|
bankCardSelect,
|
||||||
|
cashSelect,
|
||||||
|
countInputElem,
|
||||||
|
dateInputElem,
|
||||||
|
descriptionInputElem,
|
||||||
|
paymentTypeSelect,
|
||||||
|
submitButtonElem,
|
||||||
|
titleInputElem,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
type ExpenseFormValues = {
|
||||||
|
amount?: string;
|
||||||
|
count?: string;
|
||||||
|
date?: string;
|
||||||
|
description?: string;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-duplicate-type-constituents
|
||||||
|
paymentId?: BankCard['id'] | CashItem['id'];
|
||||||
|
paymentType?: 'BANK_CARD' | 'CASH' | 'none';
|
||||||
|
title?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
// eslint-disable-next-line jest/no-export
|
||||||
|
export const fillAddExpenseItemForm = async (data: ExpenseFormValues) => {
|
||||||
|
const {
|
||||||
|
amount,
|
||||||
|
count,
|
||||||
|
date,
|
||||||
|
description,
|
||||||
|
paymentId,
|
||||||
|
paymentType = 'none',
|
||||||
|
title,
|
||||||
|
} = data;
|
||||||
|
const {
|
||||||
|
amountInputElem,
|
||||||
|
// bankCardSelect,
|
||||||
|
// cashSelect,
|
||||||
|
countInputElem,
|
||||||
|
dateInputElem,
|
||||||
|
descriptionInputElem,
|
||||||
|
paymentTypeSelect,
|
||||||
|
titleInputElem,
|
||||||
|
} = getExpenseItemFormElements();
|
||||||
|
|
||||||
|
if (title) {
|
||||||
|
await userEvent.clear(titleInputElem);
|
||||||
|
await userEvent.type(titleInputElem, title);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (description) {
|
||||||
|
await userEvent.clear(descriptionInputElem);
|
||||||
|
await userEvent.type(descriptionInputElem, description);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (date) {
|
||||||
|
await userEvent.clear(dateInputElem);
|
||||||
|
await userEvent.type(dateInputElem, date);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (amount) {
|
||||||
|
await userEvent.clear(amountInputElem);
|
||||||
|
await userEvent.type(amountInputElem, amount);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count) {
|
||||||
|
await userEvent.clear(countInputElem);
|
||||||
|
await userEvent.type(countInputElem, count);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (paymentType !== 'none') {
|
||||||
|
await userEvent.selectOptions(paymentTypeSelect, paymentType);
|
||||||
|
const { bankCardSelect, cashSelect } = getExpenseItemFormElements();
|
||||||
|
|
||||||
|
if (paymentType === 'BANK_CARD' && bankCardSelect && paymentId) {
|
||||||
|
await userEvent.selectOptions(bankCardSelect, paymentId);
|
||||||
|
} else if (paymentType === 'CASH' && cashSelect && paymentId) {
|
||||||
|
await userEvent.selectOptions(cashSelect, paymentId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderForm = (props?: Partial<AddExpenseItemFormProps>) => {
|
||||||
|
server.use(getBankCardList, getCashList);
|
||||||
|
const onSubmit = jest.fn();
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AddExpenseItemForm data-testid="add-expense-item-form" onSubmit={onSubmit} {...props} />,
|
||||||
|
{ wrapper: createReactQueryWrapper() },
|
||||||
|
);
|
||||||
|
|
||||||
|
const {
|
||||||
|
amountInputElem,
|
||||||
|
countInputElem,
|
||||||
|
dateInputElem,
|
||||||
|
descriptionInputElem,
|
||||||
|
submitButtonElem,
|
||||||
|
titleInputElem,
|
||||||
|
} = getExpenseItemFormElements();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
amountInputElem,
|
amountInputElem,
|
||||||
@ -48,33 +155,96 @@ describe('Test AddExpenseItemForm', () => {
|
|||||||
Object.values(formElems).forEach(assertion);
|
Object.values(formElems).forEach(assertion);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should call onSubmit with values', async () => {
|
describe('should call onSubmit with values', () => {
|
||||||
const {
|
const setup = async () => {
|
||||||
amountInputElem,
|
const {
|
||||||
countInputElem,
|
onSubmit,
|
||||||
dateInputElem,
|
submitButtonElem,
|
||||||
descriptionInputElem,
|
} = renderForm();
|
||||||
onSubmit,
|
|
||||||
submitButtonElem,
|
|
||||||
titleInputElem,
|
|
||||||
} = renderForm();
|
|
||||||
|
|
||||||
await userEvent.type(titleInputElem, 'Название элемента');
|
await fillAddExpenseItemForm({
|
||||||
await userEvent.type(descriptionInputElem, 'Описание элемента');
|
amount: '12',
|
||||||
await userEvent.type(dateInputElem, '2025-08-29');
|
count: '1',
|
||||||
await userEvent.type(amountInputElem, '12');
|
date: '2025-08-29',
|
||||||
await userEvent.type(countInputElem, '1');
|
description: 'Описание элемента',
|
||||||
|
title: 'Название элемента',
|
||||||
|
});
|
||||||
|
|
||||||
await userEvent.click(submitButtonElem);
|
return {
|
||||||
|
onSubmit,
|
||||||
|
submitButtonElem,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
test('no payment', async () => {
|
||||||
expect(onSubmit).toHaveBeenCalledWith({
|
const { onSubmit, submitButtonElem } = await setup();
|
||||||
amount: 12,
|
await userEvent.click(submitButtonElem);
|
||||||
count: 1,
|
|
||||||
currency: 'RUB',
|
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||||
date: '2025-08-29T00:00:00.000Z',
|
expect(onSubmit).toHaveBeenCalledWith({
|
||||||
description: 'Описание элемента',
|
amount: 12,
|
||||||
title: 'Название элемента',
|
count: 1,
|
||||||
|
currency: 'RUB',
|
||||||
|
date: '2025-08-29T00:00:00.000Z',
|
||||||
|
description: 'Описание элемента',
|
||||||
|
payment: null,
|
||||||
|
title: 'Название элемента',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('bank card payment', async () => {
|
||||||
|
const { onSubmit, submitButtonElem } = await setup();
|
||||||
|
await fillAddExpenseItemForm({ paymentType: 'BANK_CARD' });
|
||||||
|
const { bankCardSelect } = getExpenseItemFormElements();
|
||||||
|
expect(bankCardSelect).toBeVisible();
|
||||||
|
expect(bankCardSelect).toBeEnabled();
|
||||||
|
expect(bankCardSelect).toHaveValue(bankCardList[0].id);
|
||||||
|
|
||||||
|
await fillAddExpenseItemForm({ paymentId: bankCardList[1].id, paymentType: 'BANK_CARD' });
|
||||||
|
|
||||||
|
await userEvent.click(submitButtonElem);
|
||||||
|
|
||||||
|
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onSubmit).toHaveBeenCalledWith({
|
||||||
|
amount: 12,
|
||||||
|
count: 1,
|
||||||
|
currency: 'RUB',
|
||||||
|
date: '2025-08-29T00:00:00.000Z',
|
||||||
|
description: 'Описание элемента',
|
||||||
|
payment: {
|
||||||
|
id: bankCardList[1].id,
|
||||||
|
type: 'BANK_CARD',
|
||||||
|
},
|
||||||
|
title: 'Название элемента',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('cash payment', async () => {
|
||||||
|
const { onSubmit, submitButtonElem } = await setup();
|
||||||
|
|
||||||
|
await fillAddExpenseItemForm({ paymentType: 'CASH' });
|
||||||
|
const { cashSelect } = getExpenseItemFormElements();
|
||||||
|
expect(cashSelect).toBeVisible();
|
||||||
|
expect(cashSelect).toBeEnabled();
|
||||||
|
expect(cashSelect).toHaveValue(cashList[0].id);
|
||||||
|
|
||||||
|
await fillAddExpenseItemForm({ paymentId: cashList[1].id, paymentType: 'CASH' });
|
||||||
|
|
||||||
|
await userEvent.click(submitButtonElem);
|
||||||
|
|
||||||
|
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||||
|
expect(onSubmit).toHaveBeenCalledWith({
|
||||||
|
amount: 12,
|
||||||
|
count: 1,
|
||||||
|
currency: 'RUB',
|
||||||
|
date: '2025-08-29T00:00:00.000Z',
|
||||||
|
description: 'Описание элемента',
|
||||||
|
payment: {
|
||||||
|
id: cashList[1].id,
|
||||||
|
type: 'CASH',
|
||||||
|
},
|
||||||
|
title: 'Название элемента',
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -1,11 +1,23 @@
|
|||||||
import type {
|
import type {
|
||||||
|
ChangeEventHandler,
|
||||||
FC,
|
FC,
|
||||||
FormEventHandler,
|
FormEventHandler,
|
||||||
FormHTMLAttributes,
|
FormHTMLAttributes,
|
||||||
RefObject,
|
RefObject,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
|
import {
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
|
||||||
|
import type { ExpenseListItem } from '@/entity/expenses/item';
|
||||||
|
|
||||||
|
import { useBankCards } from '@/entity/cards';
|
||||||
|
import { useCash } from '@/entity/cash';
|
||||||
import { Input } from '@/shared/ui/Input';
|
import { Input } from '@/shared/ui/Input';
|
||||||
|
import { Select } from '@/shared/ui/Select';
|
||||||
|
import { formatRub } from '@/shared/utils';
|
||||||
|
|
||||||
import classes from './AddExpenseItemForm.module.css';
|
import classes from './AddExpenseItemForm.module.css';
|
||||||
|
|
||||||
@ -21,6 +33,10 @@ export type AddExpenseItemFormValues = {
|
|||||||
currency: string;
|
currency: string;
|
||||||
date: string;
|
date: string;
|
||||||
description: string;
|
description: string;
|
||||||
|
payment: null | {
|
||||||
|
id: string;
|
||||||
|
type: 'BANK_CARD' | 'CASH';
|
||||||
|
};
|
||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -34,9 +50,10 @@ const AddExpenseItemForm: FC<AddExpenseItemFormProps> = (props) => {
|
|||||||
...formProps
|
...formProps
|
||||||
} = props;
|
} = props;
|
||||||
|
|
||||||
|
const [paymentType, setPaymentType] = useState<NonNullable<ExpenseListItem['payment']>['type']>(null);
|
||||||
|
|
||||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = (event) => {
|
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = (event) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
|
|
||||||
const formData = new FormData(event.currentTarget);
|
const formData = new FormData(event.currentTarget);
|
||||||
|
|
||||||
const title = formData.get('title');
|
const title = formData.get('title');
|
||||||
@ -44,6 +61,28 @@ const AddExpenseItemForm: FC<AddExpenseItemFormProps> = (props) => {
|
|||||||
const amount = formData.get('amount');
|
const amount = formData.get('amount');
|
||||||
const description = formData.get('description');
|
const description = formData.get('description');
|
||||||
const count = formData.get('count');
|
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') {
|
if (typeof title === 'string' && typeof date === 'string' && typeof amount === 'string' && typeof description === 'string' && typeof count === 'string') {
|
||||||
onSubmit?.({
|
onSubmit?.({
|
||||||
@ -52,10 +91,63 @@ const AddExpenseItemForm: FC<AddExpenseItemFormProps> = (props) => {
|
|||||||
currency: 'RUB',
|
currency: 'RUB',
|
||||||
date: new Date(date).toISOString(),
|
date: new Date(date).toISOString(),
|
||||||
description,
|
description,
|
||||||
|
// @ts-ignore todo
|
||||||
|
payment: getPayment(),
|
||||||
title,
|
title,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
const { data: bankCards } = useBankCards();
|
||||||
|
const { data: cash } = useCash();
|
||||||
|
|
||||||
|
const bankCardOptions = bankCards?.map((card) => ({
|
||||||
|
label: `${card.name} (${formatRub(card.balance)})`,
|
||||||
|
value: card.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const cashOptions = cash?.map((cashItem) => ({
|
||||||
|
label: `${cashItem.name} (${formatRub(cashItem.balance)})`,
|
||||||
|
value: cashItem.id,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const paymentOptions = useMemo(() => {
|
||||||
|
const options = [{ label: 'Без оплаты', value: 'none' }];
|
||||||
|
|
||||||
|
if (bankCardOptions && bankCardOptions.length > 0) {
|
||||||
|
options.push({ label: 'Банковская карта', value: 'BANK_CARD' });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cashOptions && cashOptions.length > 0) {
|
||||||
|
options.push({ label: 'Наличные', value: 'CASH' });
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}, [bankCardOptions, cashOptions]);
|
||||||
|
|
||||||
|
const onChangePaymentHandler: ChangeEventHandler<HTMLSelectElement> = (event) => {
|
||||||
|
const { value } = event.target;
|
||||||
|
if (value === 'none') {
|
||||||
|
setPaymentType(null);
|
||||||
|
} else {
|
||||||
|
// @ts-ignore todo
|
||||||
|
setPaymentType(value);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderCardSelect = () => (
|
||||||
|
<Select
|
||||||
|
label="Карта"
|
||||||
|
name="bank-card"
|
||||||
|
options={bankCardOptions ?? []}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
const renderCashSelect = () => (
|
||||||
|
<Select
|
||||||
|
label="Наличные"
|
||||||
|
name="cash"
|
||||||
|
options={cashOptions ?? []}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
@ -80,6 +172,7 @@ const AddExpenseItemForm: FC<AddExpenseItemFormProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
|
defaultValue={new Date().toISOString().split('T')[0]}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label="Дата"
|
label="Дата"
|
||||||
name="date"
|
name="date"
|
||||||
@ -97,12 +190,24 @@ const AddExpenseItemForm: FC<AddExpenseItemFormProps> = (props) => {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<Input
|
<Input
|
||||||
|
defaultValue={1}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
label="Количество"
|
label="Количество"
|
||||||
name="count"
|
name="count"
|
||||||
type="number"
|
type="number"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<Select
|
||||||
|
label="Способ оплаты"
|
||||||
|
name="paymentMethod"
|
||||||
|
onChange={onChangePaymentHandler}
|
||||||
|
options={paymentOptions}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{paymentType === 'BANK_CARD' && renderCardSelect()}
|
||||||
|
|
||||||
|
{paymentType === 'CASH' && renderCashSelect()}
|
||||||
|
|
||||||
<button disabled={disabled} type="submit">Добавить</button>
|
<button disabled={disabled} type="submit">Добавить</button>
|
||||||
</form>
|
</form>
|
||||||
);
|
);
|
||||||
|
|||||||
@ -14,6 +14,7 @@ const deletingItem = {
|
|||||||
description: 'Mock description',
|
description: 'Mock description',
|
||||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||||
|
payment: null,
|
||||||
title: 'Mock item',
|
title: 'Mock item',
|
||||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -14,6 +14,7 @@ const editingItem = {
|
|||||||
description: 'Mock description',
|
description: 'Mock description',
|
||||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||||
|
payment: null,
|
||||||
title: 'Mock item',
|
title: 'Mock item',
|
||||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,14 +1,22 @@
|
|||||||
|
import { getBankCardList, getCashList } from '@mocks/browser/handlers';
|
||||||
import { server } from '@mocks/jest/server';
|
import { server } from '@mocks/jest/server';
|
||||||
import { createReactQueryWrapper } from '@mocks/jest/wrappers';
|
import { createReactQueryWrapper } from '@mocks/jest/wrappers';
|
||||||
import { render, screen } from '@testing-library/react';
|
import { render, screen } from '@testing-library/react';
|
||||||
import userEvent from '@testing-library/user-event';
|
import userEvent from '@testing-library/user-event';
|
||||||
import { http, HttpResponse } from 'msw';
|
import { http, HttpResponse } from 'msw';
|
||||||
|
|
||||||
import type { AddExpenseItemBody, AddExpenseItemPath, AddExpenseItemSuccessResponse } from '@/entity/expenses/item';
|
import type {
|
||||||
|
AddExpenseItemBody,
|
||||||
|
AddExpenseItemPath,
|
||||||
|
AddExpenseItemSuccessResponse,
|
||||||
|
} from '@/entity/expenses/item';
|
||||||
|
|
||||||
|
import { fillAddExpenseItemForm } from '@/entity/expenses/item/ui/AddExpenseItemForm/AddExpenseItemForm.test';
|
||||||
|
|
||||||
import AddExpenseItemButton from './AddExpenseItemButton.ui';
|
import AddExpenseItemButton from './AddExpenseItemButton.ui';
|
||||||
|
|
||||||
const renderButton = () => {
|
const renderButton = () => {
|
||||||
|
server.use(getBankCardList, getCashList);
|
||||||
render(
|
render(
|
||||||
<AddExpenseItemButton data-testid="test-button" listId="123" />,
|
<AddExpenseItemButton data-testid="test-button" listId="123" />,
|
||||||
{ wrapper: createReactQueryWrapper() },
|
{ wrapper: createReactQueryWrapper() },
|
||||||
@ -31,14 +39,6 @@ const openModal = async () => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const fillForm = async (data: { amount: string; count: string; date: string; description: string; title: string }) => {
|
|
||||||
await userEvent.type(screen.getByLabelText('Название'), data.title);
|
|
||||||
await userEvent.type(screen.getByLabelText('Описание'), data.description);
|
|
||||||
await userEvent.type(screen.getByLabelText('Дата'), data.date);
|
|
||||||
await userEvent.type(screen.getByLabelText('Сумма'), data.amount);
|
|
||||||
await userEvent.type(screen.getByLabelText('Количество'), data.count);
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitHandler = (requestSpy: jest.Mock) => http.post<
|
const submitHandler = (requestSpy: jest.Mock) => http.post<
|
||||||
AddExpenseItemPath,
|
AddExpenseItemPath,
|
||||||
AddExpenseItemBody,
|
AddExpenseItemBody,
|
||||||
@ -58,6 +58,7 @@ const submitHandler = (requestSpy: jest.Mock) => http.post<
|
|||||||
description: 'Mock description',
|
description: 'Mock description',
|
||||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||||
|
payment: null,
|
||||||
title: 'Mock item',
|
title: 'Mock item',
|
||||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||||
});
|
});
|
||||||
@ -94,7 +95,7 @@ describe('Test AddExpenseItemButton', () => {
|
|||||||
modalElem = (await openModal()).modalElem;
|
modalElem = (await openModal()).modalElem;
|
||||||
submitButtonElem = screen.getByText('Добавить');
|
submitButtonElem = screen.getByText('Добавить');
|
||||||
|
|
||||||
await fillForm({
|
await fillAddExpenseItemForm({
|
||||||
amount: '12',
|
amount: '12',
|
||||||
count: '1',
|
count: '1',
|
||||||
date: '2025-08-29',
|
date: '2025-08-29',
|
||||||
@ -112,6 +113,7 @@ describe('Test AddExpenseItemButton', () => {
|
|||||||
currency: 'RUB',
|
currency: 'RUB',
|
||||||
date: '2025-08-29T00:00:00.000Z',
|
date: '2025-08-29T00:00:00.000Z',
|
||||||
description: 'Описание элемента',
|
description: 'Описание элемента',
|
||||||
|
payment: null,
|
||||||
title: 'Название элемента',
|
title: 'Название элемента',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@ -17,6 +17,7 @@ const deletedItem = {
|
|||||||
description: 'Mock description',
|
description: 'Mock description',
|
||||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||||
|
payment: null,
|
||||||
title: 'Mock item',
|
title: 'Mock item',
|
||||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -21,6 +21,7 @@ const editingItem = {
|
|||||||
description: 'Mock description',
|
description: 'Mock description',
|
||||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||||
|
payment: null,
|
||||||
title: 'Mock item',
|
title: 'Mock item',
|
||||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||||
};
|
};
|
||||||
|
|||||||
@ -486,6 +486,19 @@ export interface components {
|
|||||||
*/
|
*/
|
||||||
title: string;
|
title: string;
|
||||||
};
|
};
|
||||||
|
ExpensePaymentDto: {
|
||||||
|
/**
|
||||||
|
* @description Способ оплаты
|
||||||
|
* @example CASH
|
||||||
|
* @enum {string|null}
|
||||||
|
*/
|
||||||
|
type: "BANK_CARD" | "CASH" | null;
|
||||||
|
/**
|
||||||
|
* @description ID карты или наличных
|
||||||
|
* @example 123
|
||||||
|
*/
|
||||||
|
id: string | null;
|
||||||
|
};
|
||||||
ExpenseListItemDto: {
|
ExpenseListItemDto: {
|
||||||
/** @description ID */
|
/** @description ID */
|
||||||
id: string;
|
id: string;
|
||||||
@ -524,6 +537,8 @@ export interface components {
|
|||||||
* @example 2
|
* @example 2
|
||||||
*/
|
*/
|
||||||
count: number | null;
|
count: number | null;
|
||||||
|
/** @description Оплата */
|
||||||
|
payment: components["schemas"]["ExpensePaymentDto"] | null;
|
||||||
/**
|
/**
|
||||||
* Format: date-time
|
* Format: date-time
|
||||||
* @description Дата создания
|
* @description Дата создания
|
||||||
@ -598,6 +613,8 @@ export interface components {
|
|||||||
* @example 2
|
* @example 2
|
||||||
*/
|
*/
|
||||||
count: number | null;
|
count: number | null;
|
||||||
|
/** @description Оплата */
|
||||||
|
payment: components["schemas"]["ExpensePaymentDto"] | null;
|
||||||
};
|
};
|
||||||
EditExpenseListItemDto: {
|
EditExpenseListItemDto: {
|
||||||
/**
|
/**
|
||||||
|
|||||||
1
frontend/src/shared/ui/Select/index.ts
Normal file
1
frontend/src/shared/ui/Select/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as Select } from './ui/Select.ui';
|
||||||
4
frontend/src/shared/ui/Select/ui/Select.module.css
Normal file
4
frontend/src/shared/ui/Select/ui/Select.module.css
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
45
frontend/src/shared/ui/Select/ui/Select.ui.tsx
Normal file
45
frontend/src/shared/ui/Select/ui/Select.ui.tsx
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import type { FC, HTMLProps, RefObject } from 'react';
|
||||||
|
|
||||||
|
import classes from './Select.module.css';
|
||||||
|
|
||||||
|
type Option = {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type SelectProps = HTMLProps<HTMLSelectElement> & {
|
||||||
|
label: string;
|
||||||
|
options: Option[];
|
||||||
|
ref?: RefObject<HTMLSelectElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Select: FC<SelectProps> = (props) => {
|
||||||
|
const {
|
||||||
|
id,
|
||||||
|
label,
|
||||||
|
options,
|
||||||
|
ref,
|
||||||
|
...refProps
|
||||||
|
} = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<label className={classes.wrapper} htmlFor={id}>
|
||||||
|
{label}
|
||||||
|
|
||||||
|
<select
|
||||||
|
id={id}
|
||||||
|
ref={ref}
|
||||||
|
{...refProps}
|
||||||
|
>
|
||||||
|
{options.map((option) => (
|
||||||
|
<option key={option.value} value={option.value}>
|
||||||
|
{option.label}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Select;
|
||||||
@ -33,5 +33,8 @@
|
|||||||
"eslint.config.ts",
|
"eslint.config.ts",
|
||||||
"rsbuild.config.ts",
|
"rsbuild.config.ts",
|
||||||
"jest.config.ts"
|
"jest.config.ts"
|
||||||
|
],
|
||||||
|
"exclude": [
|
||||||
|
"src/shared/api"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user