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: 'Ежемесячная плата за интернет',
|
||||
expenseListId: '1',
|
||||
id: '1',
|
||||
payment: null,
|
||||
title: 'Счет за интернет',
|
||||
updatedAt: '2024-04-01T12:00:00Z',
|
||||
},
|
||||
@ -20,6 +21,7 @@ export const items1 = [
|
||||
description: 'Продукты на неделю',
|
||||
expenseListId: '1',
|
||||
id: '2',
|
||||
payment: null,
|
||||
title: 'Покупка продуктов',
|
||||
updatedAt: '2024-04-02T12:00:00Z',
|
||||
},
|
||||
@ -39,6 +41,7 @@ export const items4 = [
|
||||
description: null,
|
||||
expenseListId: '4',
|
||||
id: '3',
|
||||
payment: null,
|
||||
title: 'Бензин',
|
||||
updatedAt: '2024-04-03T12:00:00Z',
|
||||
},
|
||||
@ -54,6 +57,7 @@ export const items5 = [
|
||||
description: 'Покупка нового ноутбука',
|
||||
expenseListId: '5',
|
||||
id: '4',
|
||||
payment: null,
|
||||
title: 'Ноутбук',
|
||||
updatedAt: '2024-04-04T12:00:00Z',
|
||||
},
|
||||
@ -66,6 +70,7 @@ export const items5 = [
|
||||
description: 'Механическая мышка',
|
||||
expenseListId: '5',
|
||||
id: '5',
|
||||
payment: null,
|
||||
title: 'Мышка',
|
||||
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 { getBankCardList } from './bank-cards/list/get';
|
||||
export { getCashList } from './cash/list/get';
|
||||
export { getExpenseItemListById } from './expense/list/:id/get';
|
||||
export { getExpenseList } from './expense/list/get';
|
||||
|
||||
@ -12,6 +12,7 @@ export default [
|
||||
rules: {
|
||||
'@stylistic/jsx-sort-props': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
'@typescript-eslint/sort-type-constituents': 'off',
|
||||
'@typescript-eslint/strict-boolean-expressions': 'off',
|
||||
'func-style': 'off',
|
||||
'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 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 AddExpenseItemForm from './AddExpenseItemForm.ui';
|
||||
|
||||
const renderForm = (props?: Partial<AddExpenseItemFormProps>) => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
render(<AddExpenseItemForm data-testid="add-expense-item-form" onSubmit={onSubmit} {...props} />);
|
||||
|
||||
const getExpenseItemFormElements = () => {
|
||||
const titleInputElem = screen.getByLabelText('Название');
|
||||
const descriptionInputElem = screen.getByLabelText('Описание');
|
||||
const dateInputElem = screen.getByLabelText('Дата');
|
||||
const amountInputElem = screen.getByLabelText('Сумма');
|
||||
const countInputElem = screen.getByLabelText('Количество');
|
||||
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 {
|
||||
amountInputElem,
|
||||
@ -48,33 +155,96 @@ describe('Test AddExpenseItemForm', () => {
|
||||
Object.values(formElems).forEach(assertion);
|
||||
});
|
||||
|
||||
test('should call onSubmit with values', async () => {
|
||||
const {
|
||||
amountInputElem,
|
||||
countInputElem,
|
||||
dateInputElem,
|
||||
descriptionInputElem,
|
||||
onSubmit,
|
||||
submitButtonElem,
|
||||
titleInputElem,
|
||||
} = renderForm();
|
||||
describe('should call onSubmit with values', () => {
|
||||
const setup = async () => {
|
||||
const {
|
||||
onSubmit,
|
||||
submitButtonElem,
|
||||
} = renderForm();
|
||||
|
||||
await userEvent.type(titleInputElem, 'Название элемента');
|
||||
await userEvent.type(descriptionInputElem, 'Описание элемента');
|
||||
await userEvent.type(dateInputElem, '2025-08-29');
|
||||
await userEvent.type(amountInputElem, '12');
|
||||
await userEvent.type(countInputElem, '1');
|
||||
await fillAddExpenseItemForm({
|
||||
amount: '12',
|
||||
count: '1',
|
||||
date: '2025-08-29',
|
||||
description: 'Описание элемента',
|
||||
title: 'Название элемента',
|
||||
});
|
||||
|
||||
await userEvent.click(submitButtonElem);
|
||||
return {
|
||||
onSubmit,
|
||||
submitButtonElem,
|
||||
};
|
||||
};
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
amount: 12,
|
||||
count: 1,
|
||||
currency: 'RUB',
|
||||
date: '2025-08-29T00:00:00.000Z',
|
||||
description: 'Описание элемента',
|
||||
title: 'Название элемента',
|
||||
test('no payment', async () => {
|
||||
const { onSubmit, submitButtonElem } = await setup();
|
||||
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: 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 {
|
||||
ChangeEventHandler,
|
||||
FC,
|
||||
FormEventHandler,
|
||||
FormHTMLAttributes,
|
||||
RefObject,
|
||||
} 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 { Select } from '@/shared/ui/Select';
|
||||
import { formatRub } from '@/shared/utils';
|
||||
|
||||
import classes from './AddExpenseItemForm.module.css';
|
||||
|
||||
@ -21,6 +33,10 @@ export type AddExpenseItemFormValues = {
|
||||
currency: string;
|
||||
date: string;
|
||||
description: string;
|
||||
payment: null | {
|
||||
id: string;
|
||||
type: 'BANK_CARD' | 'CASH';
|
||||
};
|
||||
title: string;
|
||||
};
|
||||
|
||||
@ -34,9 +50,10 @@ const AddExpenseItemForm: FC<AddExpenseItemFormProps> = (props) => {
|
||||
...formProps
|
||||
} = props;
|
||||
|
||||
const [paymentType, setPaymentType] = useState<NonNullable<ExpenseListItem['payment']>['type']>(null);
|
||||
|
||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.currentTarget);
|
||||
|
||||
const title = formData.get('title');
|
||||
@ -44,6 +61,28 @@ const AddExpenseItemForm: FC<AddExpenseItemFormProps> = (props) => {
|
||||
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?.({
|
||||
@ -52,10 +91,63 @@ const AddExpenseItemForm: FC<AddExpenseItemFormProps> = (props) => {
|
||||
currency: 'RUB',
|
||||
date: new Date(date).toISOString(),
|
||||
description,
|
||||
// @ts-ignore todo
|
||||
payment: getPayment(),
|
||||
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 (
|
||||
<form
|
||||
@ -80,6 +172,7 @@ const AddExpenseItemForm: FC<AddExpenseItemFormProps> = (props) => {
|
||||
/>
|
||||
|
||||
<Input
|
||||
defaultValue={new Date().toISOString().split('T')[0]}
|
||||
disabled={disabled}
|
||||
label="Дата"
|
||||
name="date"
|
||||
@ -97,12 +190,24 @@ const AddExpenseItemForm: FC<AddExpenseItemFormProps> = (props) => {
|
||||
/>
|
||||
|
||||
<Input
|
||||
defaultValue={1}
|
||||
disabled={disabled}
|
||||
label="Количество"
|
||||
name="count"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<Select
|
||||
label="Способ оплаты"
|
||||
name="paymentMethod"
|
||||
onChange={onChangePaymentHandler}
|
||||
options={paymentOptions}
|
||||
/>
|
||||
|
||||
{paymentType === 'BANK_CARD' && renderCardSelect()}
|
||||
|
||||
{paymentType === 'CASH' && renderCashSelect()}
|
||||
|
||||
<button disabled={disabled} type="submit">Добавить</button>
|
||||
</form>
|
||||
);
|
||||
|
||||
@ -14,6 +14,7 @@ const deletingItem = {
|
||||
description: 'Mock description',
|
||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||
payment: null,
|
||||
title: 'Mock item',
|
||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||
};
|
||||
|
||||
@ -14,6 +14,7 @@ const editingItem = {
|
||||
description: 'Mock description',
|
||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||
payment: null,
|
||||
title: 'Mock item',
|
||||
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 { createReactQueryWrapper } from '@mocks/jest/wrappers';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
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';
|
||||
|
||||
const renderButton = () => {
|
||||
server.use(getBankCardList, getCashList);
|
||||
render(
|
||||
<AddExpenseItemButton data-testid="test-button" listId="123" />,
|
||||
{ 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<
|
||||
AddExpenseItemPath,
|
||||
AddExpenseItemBody,
|
||||
@ -58,6 +58,7 @@ const submitHandler = (requestSpy: jest.Mock) => http.post<
|
||||
description: 'Mock description',
|
||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||
payment: null,
|
||||
title: 'Mock item',
|
||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||
});
|
||||
@ -94,7 +95,7 @@ describe('Test AddExpenseItemButton', () => {
|
||||
modalElem = (await openModal()).modalElem;
|
||||
submitButtonElem = screen.getByText('Добавить');
|
||||
|
||||
await fillForm({
|
||||
await fillAddExpenseItemForm({
|
||||
amount: '12',
|
||||
count: '1',
|
||||
date: '2025-08-29',
|
||||
@ -112,6 +113,7 @@ describe('Test AddExpenseItemButton', () => {
|
||||
currency: 'RUB',
|
||||
date: '2025-08-29T00:00:00.000Z',
|
||||
description: 'Описание элемента',
|
||||
payment: null,
|
||||
title: 'Название элемента',
|
||||
});
|
||||
});
|
||||
|
||||
@ -17,6 +17,7 @@ const deletedItem = {
|
||||
description: 'Mock description',
|
||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||
payment: null,
|
||||
title: 'Mock item',
|
||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||
};
|
||||
|
||||
@ -21,6 +21,7 @@ const editingItem = {
|
||||
description: 'Mock description',
|
||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||
payment: null,
|
||||
title: 'Mock item',
|
||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||
};
|
||||
|
||||
@ -486,6 +486,19 @@ export interface components {
|
||||
*/
|
||||
title: string;
|
||||
};
|
||||
ExpensePaymentDto: {
|
||||
/**
|
||||
* @description Способ оплаты
|
||||
* @example CASH
|
||||
* @enum {string|null}
|
||||
*/
|
||||
type: "BANK_CARD" | "CASH" | null;
|
||||
/**
|
||||
* @description ID карты или наличных
|
||||
* @example 123
|
||||
*/
|
||||
id: string | null;
|
||||
};
|
||||
ExpenseListItemDto: {
|
||||
/** @description ID */
|
||||
id: string;
|
||||
@ -524,6 +537,8 @@ export interface components {
|
||||
* @example 2
|
||||
*/
|
||||
count: number | null;
|
||||
/** @description Оплата */
|
||||
payment: components["schemas"]["ExpensePaymentDto"] | null;
|
||||
/**
|
||||
* Format: date-time
|
||||
* @description Дата создания
|
||||
@ -598,6 +613,8 @@ export interface components {
|
||||
* @example 2
|
||||
*/
|
||||
count: number | null;
|
||||
/** @description Оплата */
|
||||
payment: components["schemas"]["ExpensePaymentDto"] | null;
|
||||
};
|
||||
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",
|
||||
"rsbuild.config.ts",
|
||||
"jest.config.ts"
|
||||
],
|
||||
"exclude": [
|
||||
"src/shared/api"
|
||||
]
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user