feat(frontend): add payment type

This commit is contained in:
Sergey Krylov 2025-09-25 04:59:12 +03:00
parent 3fba931281
commit 6ed6cf8782
20 changed files with 464 additions and 40 deletions

View 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',
},
];

View 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',
},
];

View File

@ -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',
}, },

View File

@ -0,0 +1,2 @@
export { bankCardList } from './bankCards/list';
export { cashList } from './cash/list';

View 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),
);

View 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),
);

View File

@ -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';

View File

@ -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', {

View File

@ -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: 'Название элемента',
});
}); });
}); });
}); });

View File

@ -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>
); );

View File

@ -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',
}; };

View File

@ -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',
}; };

View File

@ -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: 'Название элемента',
}); });
}); });

View File

@ -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',
}; };

View File

@ -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',
}; };

View File

@ -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: {
/** /**

View File

@ -0,0 +1 @@
export { default as Select } from './ui/Select.ui';

View File

@ -0,0 +1,4 @@
.wrapper {
display: flex;
flex-direction: column;
}

View 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;

View File

@ -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"
] ]
} }