feat(frontend): add cash
This commit is contained in:
parent
e7c0a48ed1
commit
59ff36cb18
@ -1,44 +1,16 @@
|
||||
import './normalize.css';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import {
|
||||
BrowserRouter,
|
||||
Route,
|
||||
Routes,
|
||||
} from 'react-router';
|
||||
|
||||
import { LoginPage, RegisterPage } from '@/pages/Authorization';
|
||||
import { BankCardsPage } from '@/pages/BankCard';
|
||||
import { ExpensePage } from '@/pages/Expense';
|
||||
import { ExpensesPage } from '@/pages/Expenses';
|
||||
import { HomePage } from '@/pages/Home';
|
||||
import { AuthContextProvider } from '@/shared/context/AuthContext';
|
||||
import { PAGES } from '@/shared/router';
|
||||
import { AuthRoute } from '@/shared/ui/AuthRoute';
|
||||
|
||||
import Routing from './Routing';
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const App = () => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<AuthContextProvider>
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route element={<AuthRoute />}>
|
||||
<Route element={<HomePage />} path={PAGES.Home} />
|
||||
|
||||
<Route element={<ExpensesPage />} path={PAGES.Expenses} />
|
||||
|
||||
<Route element={<ExpensePage />} path={PAGES.Expense} />
|
||||
|
||||
<Route element={<BankCardsPage />} path={PAGES.BankCards} />
|
||||
</Route>
|
||||
|
||||
<Route element={<AuthRoute needAuth={false} to="/" />}>
|
||||
<Route element={<LoginPage />} path={PAGES.Login} />
|
||||
|
||||
<Route element={<RegisterPage />} path={PAGES.Register} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
<Routing />
|
||||
</AuthContextProvider>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
|
||||
36
frontend/src/app/App/ui/Routing.tsx
Normal file
36
frontend/src/app/App/ui/Routing.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import { BrowserRouter, Route, Routes } from 'react-router';
|
||||
|
||||
import { LoginPage, RegisterPage } from '@/pages/Authorization';
|
||||
import { BankCardsPage } from '@/pages/BankCard';
|
||||
import { CashPage } from '@/pages/Cash';
|
||||
import { ExpensePage } from '@/pages/Expense';
|
||||
import { ExpensesPage } from '@/pages/Expenses';
|
||||
import { HomePage } from '@/pages/Home';
|
||||
import { PAGES } from '@/shared/router';
|
||||
import { AuthRoute } from '@/shared/ui/AuthRoute';
|
||||
|
||||
const Routing = () => (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<Route element={<AuthRoute />}>
|
||||
<Route element={<HomePage />} path={PAGES.Home} />
|
||||
|
||||
<Route element={<ExpensesPage />} path={PAGES.Expenses} />
|
||||
|
||||
<Route element={<ExpensePage />} path={PAGES.Expense} />
|
||||
|
||||
<Route element={<BankCardsPage />} path={PAGES.BankCards} />
|
||||
|
||||
<Route element={<CashPage />} path={PAGES.Cash} />
|
||||
</Route>
|
||||
|
||||
<Route element={<AuthRoute needAuth={false} to="/" />}>
|
||||
<Route element={<LoginPage />} path={PAGES.Login} />
|
||||
|
||||
<Route element={<RegisterPage />} path={PAGES.Register} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</BrowserRouter>
|
||||
);
|
||||
|
||||
export default Routing;
|
||||
12
frontend/src/entity/cash/api/addCash.ts
Normal file
12
frontend/src/entity/cash/api/addCash.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { api } from '@/shared/api/client';
|
||||
|
||||
import type { paths } from '@/shared/api/schema';
|
||||
|
||||
export type AddCashBody = paths['/cash']['post']['requestBody']['content']['application/json'];
|
||||
export type AdCashResponse = paths['/cash']['post']['responses']['200']['content']['application/json'];
|
||||
|
||||
export async function addCash(data: AddCashBody) {
|
||||
const { data: result } = await api.post<AdCashResponse>('/cash', data);
|
||||
|
||||
return result;
|
||||
}
|
||||
12
frontend/src/entity/cash/api/deleteCash.ts
Normal file
12
frontend/src/entity/cash/api/deleteCash.ts
Normal file
@ -0,0 +1,12 @@
|
||||
import { api } from '@/shared/api/client';
|
||||
|
||||
import type { paths } from '@/shared/api/schema';
|
||||
|
||||
export type DeleteCashPath = paths['/cash/{id}']['delete']['parameters']['path'];
|
||||
export type DeleteCashResponse = paths['/cash/{id}']['delete']['responses']['200']['content']['application/json'];
|
||||
|
||||
export async function deleteCash(path: DeleteCashPath) {
|
||||
const { data: result } = await api.delete<DeleteCashResponse>(`/cash/${path.id}`);
|
||||
|
||||
return result;
|
||||
}
|
||||
13
frontend/src/entity/cash/api/editCash.ts
Normal file
13
frontend/src/entity/cash/api/editCash.ts
Normal file
@ -0,0 +1,13 @@
|
||||
import { api } from '@/shared/api/client';
|
||||
|
||||
import type { paths } from '@/shared/api/schema';
|
||||
|
||||
export type EditCashPath = paths['/cash/{id}']['patch']['parameters']['path'];
|
||||
export type EditCashBody = paths['/cash/{id}']['patch']['requestBody']['content']['application/json'];
|
||||
export type EditCashResponse = paths['/cash/{id}']['patch']['responses']['200']['content']['application/json'];
|
||||
|
||||
export async function editCash({ path, data }: { path: EditCashPath; data: EditCashBody }) {
|
||||
const { data: result } = await api.patch<EditCashResponse>(`/cash/${path.id}`, data);
|
||||
|
||||
return result;
|
||||
}
|
||||
11
frontend/src/entity/cash/api/getCash.ts
Normal file
11
frontend/src/entity/cash/api/getCash.ts
Normal file
@ -0,0 +1,11 @@
|
||||
import { api } from '@/shared/api/client';
|
||||
|
||||
import type { paths } from '@/shared/api/schema';
|
||||
|
||||
type CashResponse = paths['/cash']['get']['responses']['200']['content']['application/json'];
|
||||
|
||||
export async function getCash() {
|
||||
const { data: result } = await api.get<CashResponse>('/cash');
|
||||
|
||||
return result;
|
||||
}
|
||||
17
frontend/src/entity/cash/hooks/useAddCash.ts
Normal file
17
frontend/src/entity/cash/hooks/useAddCash.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { QueryKeys } from '@/shared/api/queryKeys';
|
||||
|
||||
import { addCash } from '../api/addCash';
|
||||
|
||||
export const useAddCash = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: addCash,
|
||||
mutationKey: [QueryKeys.Cash],
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: [QueryKeys.Cash] });
|
||||
},
|
||||
});
|
||||
};
|
||||
10
frontend/src/entity/cash/hooks/useCash.ts
Normal file
10
frontend/src/entity/cash/hooks/useCash.ts
Normal file
@ -0,0 +1,10 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { QueryKeys } from '@/shared/api/queryKeys';
|
||||
|
||||
import { getCash } from '../api/getCash';
|
||||
|
||||
export const useCash = () => useQuery({
|
||||
queryFn: async () => getCash(),
|
||||
queryKey: [QueryKeys.Cash],
|
||||
});
|
||||
17
frontend/src/entity/cash/hooks/useDeleteCash.ts
Normal file
17
frontend/src/entity/cash/hooks/useDeleteCash.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { QueryKeys } from '@/shared/api/queryKeys';
|
||||
|
||||
import { deleteCash } from '../api/deleteCash';
|
||||
|
||||
export const useDeleteCash = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: deleteCash,
|
||||
mutationKey: [QueryKeys.Cash],
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: [QueryKeys.Cash] });
|
||||
},
|
||||
});
|
||||
};
|
||||
17
frontend/src/entity/cash/hooks/useEditCash.ts
Normal file
17
frontend/src/entity/cash/hooks/useEditCash.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
|
||||
import { QueryKeys } from '@/shared/api/queryKeys';
|
||||
|
||||
import { editCash } from '../api/editCash';
|
||||
|
||||
export const useEditCash = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: editCash,
|
||||
mutationKey: [QueryKeys.Cash],
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: [QueryKeys.Cash] });
|
||||
},
|
||||
});
|
||||
};
|
||||
16
frontend/src/entity/cash/index.ts
Normal file
16
frontend/src/entity/cash/index.ts
Normal file
@ -0,0 +1,16 @@
|
||||
export { useAddCash } from './hooks/useAddCash';
|
||||
export { useCash } from './hooks/useCash';
|
||||
export { useDeleteCash } from './hooks/useDeleteCash';
|
||||
export { useEditCash } from './hooks/useEditCash';
|
||||
|
||||
export { AddCashForm } from './ui/AddCashForm';
|
||||
export { DeleteCashForm } from './ui/DeleteCashForm';
|
||||
export { EditCashForm } from './ui/EditCashForm';
|
||||
|
||||
export type { CashList, CashItem } from './types';
|
||||
|
||||
export type { AddCashFormValues } from './ui/AddCashForm';
|
||||
export type { EditCashFormValues } from './ui/EditCashForm';
|
||||
export type { AddCashBody, AdCashResponse } from './api/addCash';
|
||||
export type { DeleteCashPath, DeleteCashResponse } from './api/deleteCash';
|
||||
export type { EditCashBody, EditCashPath, EditCashResponse } from './api/editCash';
|
||||
4
frontend/src/entity/cash/types/index.ts
Normal file
4
frontend/src/entity/cash/types/index.ts
Normal file
@ -0,0 +1,4 @@
|
||||
import type { paths } from '@/shared/api/schema';
|
||||
|
||||
export type CashList = paths['/cash']['get']['responses']['200']['content']['application/json'];
|
||||
export type CashItem = paths['/cash/{id}']['get']['responses']['200']['content']['application/json'];
|
||||
2
frontend/src/entity/cash/ui/AddCashForm/index.ts
Normal file
2
frontend/src/entity/cash/ui/AddCashForm/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export type { AddCashFormValues } from './ui/AddCashForm.ui';
|
||||
export { default as AddCashForm } from './ui/AddCashForm.ui';
|
||||
@ -0,0 +1,5 @@
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
@ -0,0 +1,64 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import AddCashForm from './AddCashForm.ui';
|
||||
|
||||
import type { AddCashFormProps } from './AddCashForm.ui';
|
||||
|
||||
const renderForm = (props?: Partial<AddCashFormProps>) => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
render(<AddCashForm data-testid="add-cash-form" onSubmit={onSubmit} {...props} />);
|
||||
|
||||
const nameInputElem = screen.getByLabelText('Название');
|
||||
const balanceInputElem = screen.getByLabelText('Баланс');
|
||||
const submitButtonElem = screen.getByText('Добавить');
|
||||
|
||||
return {
|
||||
nameInputElem,
|
||||
balanceInputElem,
|
||||
submitButtonElem,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Test AddCashForm', () => {
|
||||
test('should render form', () => {
|
||||
renderForm();
|
||||
const form = screen.getByTestId('add-cash-form');
|
||||
expect(form).toBeInTheDocument();
|
||||
expect(form).toBeVisible();
|
||||
});
|
||||
|
||||
test.each([
|
||||
['enabled', {}, (el: HTMLElement) => {
|
||||
expect(el).toBeEnabled();
|
||||
}],
|
||||
['disabled', { disabled: true }, (el: HTMLElement) => {
|
||||
expect(el).toBeDisabled();
|
||||
}],
|
||||
])('should render fields %s', (_state, options, assertion) => {
|
||||
const { onSubmit, ...formElems } = renderForm(options);
|
||||
Object.values(formElems).forEach(assertion);
|
||||
});
|
||||
|
||||
test('should call onSubmit with values', async () => {
|
||||
const {
|
||||
nameInputElem,
|
||||
balanceInputElem,
|
||||
submitButtonElem,
|
||||
onSubmit,
|
||||
} = renderForm();
|
||||
|
||||
await userEvent.type(nameInputElem, 'Наличные');
|
||||
await userEvent.type(balanceInputElem, '500');
|
||||
|
||||
await userEvent.click(submitButtonElem);
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
name: 'Наличные',
|
||||
balance: 500,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,79 @@
|
||||
import { Input } from '@/shared/ui/Input';
|
||||
|
||||
import classes from './AddCashForm.module.css';
|
||||
|
||||
import type {
|
||||
FC,
|
||||
FormEventHandler,
|
||||
FormHTMLAttributes,
|
||||
RefObject,
|
||||
} from 'react';
|
||||
|
||||
export type AddCashFormValues = {
|
||||
name: string;
|
||||
balance: number;
|
||||
};
|
||||
|
||||
type BaseFormProps = Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;
|
||||
|
||||
export type AddCashFormProps = BaseFormProps & {
|
||||
disabled?: boolean;
|
||||
onSubmit?: (data: AddCashFormValues) => Promise<void> | void;
|
||||
ref?: RefObject<HTMLFormElement>;
|
||||
};
|
||||
|
||||
const AddCashForm: FC<AddCashFormProps> = (props) => {
|
||||
const {
|
||||
ref,
|
||||
disabled,
|
||||
onSubmit,
|
||||
...formProps
|
||||
} = props;
|
||||
|
||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.currentTarget);
|
||||
|
||||
const name = formData.get('name');
|
||||
const balance = formData.get('balance');
|
||||
|
||||
if (typeof name === 'string' && typeof balance === 'string') {
|
||||
onSubmit?.({
|
||||
name,
|
||||
balance: Number(balance),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
ref={ref}
|
||||
className={classes.form}
|
||||
onSubmit={onFormSubmitHandler}
|
||||
{...formProps}
|
||||
>
|
||||
<h2>Добавить наличные</h2>
|
||||
|
||||
<Input
|
||||
required
|
||||
disabled={disabled}
|
||||
label="Название"
|
||||
name="name"
|
||||
/>
|
||||
|
||||
<Input
|
||||
defaultValue={0}
|
||||
disabled={disabled}
|
||||
label="Баланс"
|
||||
name="balance"
|
||||
step="0.01"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<button disabled={disabled} type="submit">Добавить</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddCashForm;
|
||||
1
frontend/src/entity/cash/ui/DeleteCashForm/index.ts
Normal file
1
frontend/src/entity/cash/ui/DeleteCashForm/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as DeleteCashForm } from './ui/DeleteCashForm.ui';
|
||||
@ -0,0 +1,55 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import DeleteCashForm from './DeleteCashForm.ui';
|
||||
|
||||
import type { DeleteCashFormProps } from './DeleteCashForm.ui';
|
||||
|
||||
const deletingCash: DeleteCashFormProps['cash'] = {
|
||||
id: 'af6da4e1-f24f-4b43-8556-fa885626897d',
|
||||
name: 'Наличные',
|
||||
balance: 1000,
|
||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||
createdAt: '2025-08-29T07:57:40.503Z',
|
||||
};
|
||||
|
||||
const renderForm = (props?: Partial<DeleteCashFormProps>) => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
render(
|
||||
<DeleteCashForm
|
||||
cash={deletingCash}
|
||||
data-testid="delete-cash-form"
|
||||
onSubmit={onSubmit}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
|
||||
const submitButtonElem = screen.getByText('Удалить');
|
||||
|
||||
return {
|
||||
submitButtonElem,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Test DeleteCashForm', () => {
|
||||
test('should render form', () => {
|
||||
renderForm();
|
||||
const form = screen.getByTestId('delete-cash-form');
|
||||
expect(form).toBeInTheDocument();
|
||||
expect(form).toBeVisible();
|
||||
});
|
||||
|
||||
test('should call onSubmit with values', async () => {
|
||||
const {
|
||||
submitButtonElem,
|
||||
onSubmit,
|
||||
} = renderForm();
|
||||
|
||||
await userEvent.click(submitButtonElem);
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit).toHaveBeenCalledWith(deletingCash);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,53 @@
|
||||
import type { CashItem } from '../../../types';
|
||||
import type {
|
||||
FC,
|
||||
FormEventHandler,
|
||||
FormHTMLAttributes,
|
||||
RefObject,
|
||||
} from 'react';
|
||||
|
||||
type BaseFormProps = Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;
|
||||
|
||||
export type DeleteCashFormProps = BaseFormProps & {
|
||||
cash: CashItem;
|
||||
disabled?: boolean;
|
||||
onSubmit?: (cash: CashItem) => Promise<void> | void;
|
||||
ref?: RefObject<HTMLFormElement>;
|
||||
};
|
||||
|
||||
const DeleteCashForm: FC<DeleteCashFormProps> = (props) => {
|
||||
const {
|
||||
cash,
|
||||
disabled,
|
||||
onSubmit,
|
||||
ref,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = (event) => {
|
||||
event.preventDefault();
|
||||
onSubmit?.(cash);
|
||||
};
|
||||
|
||||
return (
|
||||
<form ref={ref} onSubmit={onFormSubmitHandler} {...restProps}>
|
||||
<h2>Удалить наличные</h2>
|
||||
|
||||
<p>
|
||||
Вы действительно хотите удалить наличные
|
||||
{' '}
|
||||
|
||||
<b>
|
||||
{`"${cash.name}"`}
|
||||
</b>
|
||||
|
||||
{' '}
|
||||
?
|
||||
</p>
|
||||
|
||||
<button disabled={disabled} type="submit">Удалить</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteCashForm;
|
||||
2
frontend/src/entity/cash/ui/EditCashForm/index.ts
Normal file
2
frontend/src/entity/cash/ui/EditCashForm/index.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export type { EditCashFormValues } from './ui/EditCashForm.ui';
|
||||
export { default as EditCashForm } from './ui/EditCashForm.ui';
|
||||
@ -0,0 +1,5 @@
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
@ -0,0 +1,91 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import EditCashForm from './EditCashForm.ui';
|
||||
|
||||
import type { EditCashFormProps } from './EditCashForm.ui';
|
||||
|
||||
const editingCash = {
|
||||
id: 'af6da4e1-f24f-4b43-8556-fa885626897d',
|
||||
name: 'Наличные',
|
||||
balance: 1000,
|
||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||
createdAt: '2025-08-29T07:57:40.503Z',
|
||||
};
|
||||
|
||||
const renderForm = (props?: Partial<EditCashFormProps>) => {
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
render(
|
||||
<EditCashForm
|
||||
cash={editingCash}
|
||||
data-testid="edit-bank-card-form"
|
||||
onSubmit={onSubmit}
|
||||
{...props}
|
||||
/>,
|
||||
);
|
||||
|
||||
const titleInputElem = screen.getByLabelText('Название');
|
||||
const balanceInputElem = screen.getByLabelText('Баланс');
|
||||
const submitButtonElem = screen.getByText('Сохранить');
|
||||
|
||||
return {
|
||||
titleInputElem,
|
||||
balanceInputElem,
|
||||
submitButtonElem,
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
describe('Test EditCashForm', () => {
|
||||
test('should render form', () => {
|
||||
renderForm();
|
||||
const form = screen.getByTestId('edit-bank-card-form');
|
||||
expect(form).toBeInTheDocument();
|
||||
expect(form).toBeVisible();
|
||||
});
|
||||
|
||||
test('should inputs have default value', () => {
|
||||
const {
|
||||
titleInputElem,
|
||||
balanceInputElem,
|
||||
} = renderForm();
|
||||
expect(titleInputElem).toHaveValue('Наличные');
|
||||
expect(balanceInputElem).toHaveValue(1000);
|
||||
});
|
||||
|
||||
test.each([
|
||||
['enabled', {}, (el: HTMLElement) => {
|
||||
expect(el).toBeEnabled();
|
||||
}],
|
||||
['disabled', { disabled: true }, (el: HTMLElement) => {
|
||||
expect(el).toBeDisabled();
|
||||
}],
|
||||
])('should render fields %s', (_state, options, assertion) => {
|
||||
const { onSubmit, ...formElems } = renderForm(options);
|
||||
Object.values(formElems).forEach(assertion);
|
||||
});
|
||||
|
||||
test('should call onSubmit with values', async () => {
|
||||
const {
|
||||
titleInputElem,
|
||||
balanceInputElem,
|
||||
submitButtonElem,
|
||||
onSubmit,
|
||||
} = renderForm();
|
||||
|
||||
await userEvent.clear(titleInputElem);
|
||||
await userEvent.type(titleInputElem, 'Новое название наличных');
|
||||
|
||||
await userEvent.clear(balanceInputElem);
|
||||
await userEvent.type(balanceInputElem, '5000');
|
||||
|
||||
await userEvent.click(submitButtonElem);
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
name: 'Новое название наличных',
|
||||
balance: 5000,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,83 @@
|
||||
import { Input } from '@/shared/ui/Input';
|
||||
|
||||
import classes from './EditCashForm.module.css';
|
||||
|
||||
import type { CashItem } from '@/entity/cash';
|
||||
import type {
|
||||
FC,
|
||||
FormEventHandler,
|
||||
FormHTMLAttributes,
|
||||
RefObject,
|
||||
} from 'react';
|
||||
|
||||
type BaseFormProps = Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;
|
||||
|
||||
export type EditCashFormValues = {
|
||||
name: string;
|
||||
balance: number;
|
||||
};
|
||||
|
||||
export type EditCashFormProps = BaseFormProps & {
|
||||
cash: CashItem;
|
||||
disabled?: boolean;
|
||||
onSubmit?: (data: EditCashFormValues) => Promise<void> | void;
|
||||
ref?: RefObject<HTMLFormElement>;
|
||||
};
|
||||
|
||||
const EditCashForm: FC<EditCashFormProps> = (props) => {
|
||||
const {
|
||||
cash,
|
||||
disabled,
|
||||
onSubmit,
|
||||
ref,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = (event) => {
|
||||
event.preventDefault();
|
||||
|
||||
const formData = new FormData(event.currentTarget);
|
||||
|
||||
const name = formData.get('name');
|
||||
const balance = formData.get('balance');
|
||||
|
||||
if (typeof name === 'string' && typeof balance === 'string') {
|
||||
onSubmit?.({
|
||||
name,
|
||||
balance: Number(balance),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
ref={ref}
|
||||
className={classes.form}
|
||||
onSubmit={onFormSubmitHandler}
|
||||
{...restProps}
|
||||
>
|
||||
<h2>Редактировать наличные</h2>
|
||||
|
||||
<Input
|
||||
required
|
||||
defaultValue={cash.name}
|
||||
disabled={disabled}
|
||||
label="Название"
|
||||
name="name"
|
||||
/>
|
||||
|
||||
<Input
|
||||
defaultValue={cash.balance}
|
||||
disabled={disabled}
|
||||
label="Баланс"
|
||||
name="balance"
|
||||
step="0.01"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<button disabled={disabled} type="submit">Сохранить</button>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditCashForm;
|
||||
@ -0,0 +1,38 @@
|
||||
import { type MouseEventHandler, useState } from 'react';
|
||||
|
||||
import { useAddCash } from '@/entity/cash';
|
||||
|
||||
import type { AddCashFormValues } from '@/entity/cash';
|
||||
|
||||
type UseAddCashButtonArgs = {
|
||||
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
export const useAddCashButton = (args: UseAddCashButtonArgs) => {
|
||||
const { onClick } = args;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const { mutateAsync: addCashMutation, isPending } = useAddCash();
|
||||
|
||||
const onCloseModalHandler = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const onClickHandler: MouseEventHandler<HTMLButtonElement> = (event) => {
|
||||
onClick?.(event);
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const onFormSubmitHandler = async (data: AddCashFormValues) => {
|
||||
await addCashMutation(data);
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
isPending,
|
||||
onCloseModalHandler,
|
||||
onClickHandler,
|
||||
onFormSubmitHandler,
|
||||
};
|
||||
};
|
||||
1
frontend/src/features/cash/AddCashButton/index.ts
Normal file
1
frontend/src/features/cash/AddCashButton/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as AddCashButton } from './ui/AddCashButton.ui';
|
||||
@ -0,0 +1,107 @@
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
import { server } from '@mocks/jest/server';
|
||||
import { createReactQueryWrapper } from '@mocks/jest/wrappers';
|
||||
|
||||
import AddCashButton from './AddCashButton.ui';
|
||||
|
||||
import type { AddCashBody, AdCashResponse } from '@/entity/cash';
|
||||
|
||||
const renderButton = () => {
|
||||
render(
|
||||
<AddCashButton data-testid="test-button" />,
|
||||
{ wrapper: createReactQueryWrapper() },
|
||||
);
|
||||
|
||||
const buttonElem = screen.getByTestId('test-button');
|
||||
|
||||
return {
|
||||
buttonElem,
|
||||
};
|
||||
};
|
||||
|
||||
const openModal = async () => {
|
||||
const { buttonElem } = renderButton();
|
||||
await userEvent.click(buttonElem);
|
||||
const modalElem = screen.getByRole('dialog');
|
||||
|
||||
return {
|
||||
modalElem,
|
||||
};
|
||||
};
|
||||
|
||||
const fillForm = async (data: AddCashBody) => {
|
||||
await userEvent.type(screen.getByLabelText('Название'), data.name);
|
||||
await userEvent.type(screen.getByLabelText('Баланс'), String(data.balance));
|
||||
};
|
||||
|
||||
const submitHandler = (requestSpy: jest.Mock) => http.post<never, AddCashBody, AdCashResponse>(
|
||||
'/cash',
|
||||
async ({ request }) => {
|
||||
const body = await request.json();
|
||||
requestSpy(body);
|
||||
|
||||
return HttpResponse.json({
|
||||
id: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||
name: 'Наличные',
|
||||
balance: 999,
|
||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||
createdAt: '2025-08-29T07:57:40.503Z',
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe('Test AddCashButton', () => {
|
||||
test('should correct render button', () => {
|
||||
const { buttonElem } = renderButton();
|
||||
|
||||
expect(buttonElem).toBeInTheDocument();
|
||||
expect(buttonElem).toBeVisible();
|
||||
expect(buttonElem).toBeEnabled();
|
||||
expect(buttonElem).toHaveTextContent('Добавить');
|
||||
|
||||
const modal = screen.queryByRole('dialog');
|
||||
expect(modal).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should open modal after click', async () => {
|
||||
const { modalElem } = await openModal();
|
||||
|
||||
expect(modalElem).toBeInTheDocument();
|
||||
expect(modalElem).toBeVisible();
|
||||
});
|
||||
|
||||
describe('should close modal after submit and send request', () => {
|
||||
let modalElem: HTMLElement;
|
||||
let submitButtonElem: HTMLElement;
|
||||
const requestSpy = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
server.use(submitHandler(requestSpy));
|
||||
modalElem = (await openModal()).modalElem;
|
||||
submitButtonElem = within(modalElem).getByText('Добавить');
|
||||
|
||||
await fillForm({
|
||||
balance: 999,
|
||||
name: 'Наличные',
|
||||
});
|
||||
});
|
||||
|
||||
test('should send correct request', async () => {
|
||||
await userEvent.click(submitButtonElem);
|
||||
expect(requestSpy).toHaveBeenCalledTimes(1);
|
||||
expect(requestSpy).toHaveBeenCalledWith({
|
||||
balance: 999,
|
||||
name: 'Наличные',
|
||||
});
|
||||
});
|
||||
|
||||
test('should close modal after submit', async () => {
|
||||
expect(submitButtonElem).toBeEnabled();
|
||||
await userEvent.click(submitButtonElem);
|
||||
expect(modalElem).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,41 @@
|
||||
import { AddCashForm } from '@/entity/cash';
|
||||
import { Modal } from '@/shared/ui/Modal';
|
||||
|
||||
import { useAddCashButton } from '../hooks/useAddCashButton';
|
||||
|
||||
import type { ButtonHTMLAttributes, FC, Ref } from 'react';
|
||||
|
||||
type AddCashButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
ref?: Ref<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
const AddCashButton: FC<AddCashButtonProps> = (props) => {
|
||||
const { ref, onClick, ...restProps } = props;
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
isPending,
|
||||
onCloseModalHandler,
|
||||
onClickHandler,
|
||||
onFormSubmitHandler,
|
||||
} = useAddCashButton({ onClick });
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
ref={ref}
|
||||
type="button"
|
||||
onClick={onClickHandler}
|
||||
{...restProps}
|
||||
>
|
||||
Добавить
|
||||
</button>
|
||||
|
||||
<Modal open={isOpen} onClose={onCloseModalHandler}>
|
||||
<AddCashForm disabled={isPending} onSubmit={onFormSubmitHandler} />
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddCashButton;
|
||||
@ -0,0 +1,37 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useDeleteCash } from '@/entity/cash';
|
||||
|
||||
import type { CashItem } from '@/entity/cash';
|
||||
|
||||
type UseDeleteCashButtonArgs = {
|
||||
id: CashItem['id'];
|
||||
};
|
||||
|
||||
export const useDeleteCashButton = (args: UseDeleteCashButtonArgs) => {
|
||||
const { id } = args;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const { mutateAsync: deleteCashAsync, isPending } = useDeleteCash();
|
||||
|
||||
const onClickHandler = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const onCloseModalHandler = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const onFormSubmitHandler = async () => {
|
||||
await deleteCashAsync({ id });
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
isPending,
|
||||
onClickHandler,
|
||||
onCloseModalHandler,
|
||||
onFormSubmitHandler,
|
||||
};
|
||||
};
|
||||
1
frontend/src/features/cash/DeleteCashButton/index.ts
Normal file
1
frontend/src/features/cash/DeleteCashButton/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as DeleteCashButton } from './ui/DeleteCashButton.ui';
|
||||
@ -0,0 +1,76 @@
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
import { server } from '@mocks/jest/server';
|
||||
import { createReactQueryWrapper } from '@mocks/jest/wrappers';
|
||||
|
||||
import DeleteCashButton from './DeleteCashButton.ui';
|
||||
|
||||
import type { CashItem } from '@/entity/cash';
|
||||
import type { DeleteExpenseListPath } from '@/entity/expenses/list';
|
||||
|
||||
const deletingCash: CashItem = {
|
||||
id: 'af6da4e1-f24f-4b43-8556-fa885626897d',
|
||||
name: 'Наличные',
|
||||
balance: 1000,
|
||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||
createdAt: '2025-08-29T07:57:40.503Z',
|
||||
};
|
||||
|
||||
const renderButton = () => {
|
||||
const testId = 'delete-cash-button';
|
||||
render(
|
||||
<DeleteCashButton cash={deletingCash} data-testid={testId} />,
|
||||
{ wrapper: createReactQueryWrapper() },
|
||||
);
|
||||
|
||||
const buttonElem = screen.getByTestId(testId);
|
||||
|
||||
return {
|
||||
buttonElem,
|
||||
};
|
||||
};
|
||||
|
||||
const openModal = async () => {
|
||||
const { buttonElem } = renderButton();
|
||||
await userEvent.click(buttonElem);
|
||||
const modalElem = screen.getByRole('dialog');
|
||||
|
||||
return {
|
||||
modalElem,
|
||||
};
|
||||
};
|
||||
|
||||
const submitHandler = http.delete<DeleteExpenseListPath>(`/cash/${deletingCash.id}`, () => HttpResponse.json(deletingCash));
|
||||
|
||||
describe('Test DeleteCashButton', () => {
|
||||
test('should correct render button', () => {
|
||||
const { buttonElem } = renderButton();
|
||||
|
||||
expect(buttonElem).toBeInTheDocument();
|
||||
expect(buttonElem).toBeVisible();
|
||||
expect(buttonElem).toBeEnabled();
|
||||
expect(buttonElem).toHaveTextContent('Удалить');
|
||||
|
||||
const modal = screen.queryByRole('dialog');
|
||||
expect(modal).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should open modal after click', async () => {
|
||||
const { modalElem } = await openModal();
|
||||
|
||||
expect(modalElem).toBeInTheDocument();
|
||||
expect(modalElem).toBeVisible();
|
||||
});
|
||||
|
||||
test('should close modal after submit', async () => {
|
||||
server.use(submitHandler);
|
||||
const { modalElem } = await openModal();
|
||||
const submitButtonElem = within(modalElem).getByText('Удалить');
|
||||
|
||||
expect(submitButtonElem).toBeEnabled();
|
||||
await userEvent.click(submitButtonElem);
|
||||
expect(modalElem).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,47 @@
|
||||
import { DeleteCashForm } from '@/entity/cash';
|
||||
import { Modal } from '@/shared/ui/Modal';
|
||||
|
||||
import { useDeleteCashButton } from '../hooks/useDeleteCashButton';
|
||||
|
||||
import type { CashItem } from '@/entity/cash';
|
||||
import type { ButtonHTMLAttributes, FC, Ref } from 'react';
|
||||
|
||||
export type DeleteCashButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
cash: CashItem;
|
||||
ref?: Ref<HTMLButtonElement>;
|
||||
};
|
||||
|
||||
const DeleteCashButton: FC<DeleteCashButtonProps> = (props) => {
|
||||
const {
|
||||
cash,
|
||||
ref,
|
||||
onClick,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
isPending,
|
||||
onCloseModalHandler,
|
||||
onClickHandler,
|
||||
onFormSubmitHandler,
|
||||
} = useDeleteCashButton({ id: cash.id });
|
||||
|
||||
return (
|
||||
<>
|
||||
<button type="button" onClick={onClickHandler} {...restProps}>
|
||||
Удалить
|
||||
</button>
|
||||
|
||||
<Modal open={isOpen} onClose={onCloseModalHandler}>
|
||||
<DeleteCashForm
|
||||
cash={cash}
|
||||
disabled={isPending}
|
||||
onSubmit={onFormSubmitHandler}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default DeleteCashButton;
|
||||
@ -0,0 +1,40 @@
|
||||
import { useState } from 'react';
|
||||
|
||||
import { useEditCash } from '@/entity/cash';
|
||||
|
||||
import type { CashItem, EditCashFormValues } from '@/entity/cash';
|
||||
|
||||
type UseEditCashButtonArgs = {
|
||||
cash: CashItem;
|
||||
};
|
||||
|
||||
export const useEditCashButton = (args: UseEditCashButtonArgs) => {
|
||||
const { cash } = args;
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const { mutateAsync: editCashAsync, isPending } = useEditCash();
|
||||
|
||||
const onClickHandler = () => {
|
||||
setIsOpen(true);
|
||||
};
|
||||
|
||||
const onCloseModalHandler = () => {
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
const onFormSubmitHandler = async (data: EditCashFormValues) => {
|
||||
await editCashAsync({
|
||||
data,
|
||||
path: { id: cash.id },
|
||||
});
|
||||
setIsOpen(false);
|
||||
};
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
isPending,
|
||||
onClickHandler,
|
||||
onCloseModalHandler,
|
||||
onFormSubmitHandler,
|
||||
};
|
||||
};
|
||||
1
frontend/src/features/cash/EditCashButton/index.ts
Normal file
1
frontend/src/features/cash/EditCashButton/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as EditCashButton } from './ui/EditCashButton.ui';
|
||||
@ -0,0 +1,127 @@
|
||||
import { render, screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { http, HttpResponse } from 'msw';
|
||||
|
||||
import { server } from '@mocks/jest/server';
|
||||
import { createReactQueryWrapper } from '@mocks/jest/wrappers';
|
||||
|
||||
import EditCashButton from './EditCashButton.ui';
|
||||
|
||||
import type {
|
||||
EditCashBody,
|
||||
EditCashPath,
|
||||
EditCashResponse,
|
||||
CashItem,
|
||||
} from '@/entity/cash';
|
||||
|
||||
const editingCash: CashItem = {
|
||||
id: 'af6da4e1-f24f-4b43-8556-fa885626897d',
|
||||
name: 'Наличные',
|
||||
balance: 1000,
|
||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||
createdAt: '2025-08-29T07:57:40.503Z',
|
||||
};
|
||||
|
||||
const fillForm = async (data: EditCashBody) => {
|
||||
const nameInputElem = screen.getByLabelText('Название');
|
||||
const balanceInputElem = screen.getByLabelText('Баланс');
|
||||
|
||||
await userEvent.clear(nameInputElem);
|
||||
await userEvent.type(nameInputElem, data.name);
|
||||
|
||||
await userEvent.clear(balanceInputElem);
|
||||
await userEvent.type(balanceInputElem, String(data.balance));
|
||||
};
|
||||
|
||||
const renderButton = () => {
|
||||
const testId = 'edit-cash-button';
|
||||
render(
|
||||
<EditCashButton cash={editingCash} data-testid={testId} />,
|
||||
{ wrapper: createReactQueryWrapper() },
|
||||
);
|
||||
|
||||
const buttonElem = screen.getByTestId(testId);
|
||||
|
||||
return {
|
||||
buttonElem,
|
||||
};
|
||||
};
|
||||
|
||||
const openModal = async () => {
|
||||
const { buttonElem } = renderButton();
|
||||
await userEvent.click(buttonElem);
|
||||
const modalElem = screen.getByRole('dialog');
|
||||
|
||||
return {
|
||||
modalElem,
|
||||
};
|
||||
};
|
||||
|
||||
const submitHandler = (requestSpy: jest.Mock) => http.patch<EditCashPath, EditCashBody, EditCashResponse>(
|
||||
`/cash/${editingCash.id}`,
|
||||
async ({ request }) => {
|
||||
const body = await request.json();
|
||||
requestSpy(body);
|
||||
|
||||
return HttpResponse.json({
|
||||
id: 'af6da4e1-f24f-4b43-8556-fa885626897d',
|
||||
name: 'Наличные',
|
||||
balance: 1000,
|
||||
updatedAt: '2025-08-29T07:57:40.503Z',
|
||||
createdAt: '2025-08-29T07:57:40.503Z',
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
describe('Test EditCashButton', () => {
|
||||
test('should correct render button', () => {
|
||||
const { buttonElem } = renderButton();
|
||||
|
||||
expect(buttonElem).toBeInTheDocument();
|
||||
expect(buttonElem).toBeVisible();
|
||||
expect(buttonElem).toBeEnabled();
|
||||
expect(buttonElem).toHaveTextContent('Редактировать');
|
||||
|
||||
const modal = screen.queryByRole('dialog');
|
||||
expect(modal).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should open modal after click', async () => {
|
||||
const { modalElem } = await openModal();
|
||||
|
||||
expect(modalElem).toBeInTheDocument();
|
||||
expect(modalElem).toBeVisible();
|
||||
});
|
||||
|
||||
describe('should close modal after submit and send request', () => {
|
||||
let modalElem: HTMLElement;
|
||||
let submitButtonElem: HTMLElement;
|
||||
const requestSpy = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
server.use(submitHandler(requestSpy));
|
||||
modalElem = (await openModal()).modalElem;
|
||||
submitButtonElem = within(modalElem).getByText('Сохранить');
|
||||
|
||||
await fillForm({
|
||||
balance: 999,
|
||||
name: 'Наличные',
|
||||
});
|
||||
});
|
||||
|
||||
test('should send correct request', async () => {
|
||||
await userEvent.click(submitButtonElem);
|
||||
expect(requestSpy).toHaveBeenCalledTimes(1);
|
||||
expect(requestSpy).toHaveBeenCalledWith({
|
||||
balance: 999,
|
||||
name: 'Наличные',
|
||||
});
|
||||
});
|
||||
|
||||
test('should close modal after submit', async () => {
|
||||
expect(submitButtonElem).toBeEnabled();
|
||||
await userEvent.click(submitButtonElem);
|
||||
expect(modalElem).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,46 @@
|
||||
import { EditCashForm } from '@/entity/cash';
|
||||
import { Modal } from '@/shared/ui/Modal';
|
||||
|
||||
import { useEditCashButton } from '../hooks/useEditCashButton';
|
||||
|
||||
import type { CashItem } from '@/entity/cash';
|
||||
import type { ButtonHTMLAttributes, FC, Ref } from 'react';
|
||||
|
||||
export type EditCashButtonProps = ButtonHTMLAttributes<HTMLButtonElement> & {
|
||||
cash: CashItem;
|
||||
ref?: Ref<HTMLButtonElement>;
|
||||
};
|
||||
const EditCashButton: FC<EditCashButtonProps> = (props) => {
|
||||
const {
|
||||
cash,
|
||||
ref,
|
||||
onClick,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const {
|
||||
isOpen,
|
||||
isPending,
|
||||
onCloseModalHandler,
|
||||
onClickHandler,
|
||||
onFormSubmitHandler,
|
||||
} = useEditCashButton({ cash });
|
||||
|
||||
return (
|
||||
<>
|
||||
<button type="button" onClick={onClickHandler} {...restProps}>
|
||||
Редактировать
|
||||
</button>
|
||||
|
||||
<Modal open={isOpen} onClose={onCloseModalHandler}>
|
||||
<EditCashForm
|
||||
cash={cash}
|
||||
disabled={isPending}
|
||||
onSubmit={onFormSubmitHandler}
|
||||
/>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditCashButton;
|
||||
1
frontend/src/pages/Cash/index.ts
Normal file
1
frontend/src/pages/Cash/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as CashPage } from './ui/CashPage.ui';
|
||||
40
frontend/src/pages/Cash/ui/CashPage.ui.tsx
Normal file
40
frontend/src/pages/Cash/ui/CashPage.ui.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { useCash } from '@/entity/cash';
|
||||
import { AddCashButton } from '@/features/cash/AddCashButton';
|
||||
import { Spinner } from '@/shared/ui/Spinner';
|
||||
import { AppLayout } from '@/widgets/AppLayout';
|
||||
import { CashTable } from '@/widgets/cash/CashTable';
|
||||
|
||||
import type { FC } from 'react';
|
||||
|
||||
const CashPage: FC = () => {
|
||||
const { isPending, data: cashList } = useCash();
|
||||
|
||||
const renderContent = () => {
|
||||
if (isPending) {
|
||||
return <Spinner />;
|
||||
}
|
||||
|
||||
if (cashList && cashList.length > 0) {
|
||||
return <CashTable items={cashList} />;
|
||||
}
|
||||
|
||||
return <p>Нет добавленных наличных</p>;
|
||||
};
|
||||
|
||||
return (
|
||||
<AppLayout>
|
||||
<div>
|
||||
<h1>Мои наличные</h1>
|
||||
|
||||
<div>
|
||||
<AddCashButton />
|
||||
</div>
|
||||
|
||||
{renderContent()}
|
||||
|
||||
</div>
|
||||
</AppLayout>
|
||||
);
|
||||
};
|
||||
|
||||
export default CashPage;
|
||||
@ -4,4 +4,5 @@ export enum QueryKeys {
|
||||
BankCard = 'BankCard',
|
||||
ExpensesList = 'ExpensesList',
|
||||
ExpenseList = 'ExpenseList',
|
||||
Cash = 'Cash',
|
||||
}
|
||||
|
||||
@ -253,6 +253,58 @@ export interface paths {
|
||||
patch: operations["BankCardController_editBankCard"];
|
||||
trace?: never;
|
||||
};
|
||||
"/cash": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/**
|
||||
* Найти все наличные
|
||||
* @description Найти все наличные
|
||||
*/
|
||||
get: operations["CashController_getAllCashByUserId"];
|
||||
put?: never;
|
||||
/**
|
||||
* Создать наличные
|
||||
* @description Создать наличные
|
||||
*/
|
||||
post: operations["CashController_createCash"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/cash/{id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/**
|
||||
* Найти наличные
|
||||
* @description Найти наличные
|
||||
*/
|
||||
get: operations["CashController_getCashById"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
/**
|
||||
* Удалить наличные
|
||||
* @description Удалить наличные
|
||||
*/
|
||||
delete: operations["CashController_deleteCash"];
|
||||
options?: never;
|
||||
head?: never;
|
||||
/**
|
||||
* Редактировать наличные
|
||||
* @description Редактировать наличные
|
||||
*/
|
||||
patch: operations["CashController_editCash"];
|
||||
trace?: never;
|
||||
};
|
||||
}
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
@ -530,6 +582,54 @@ export interface components {
|
||||
*/
|
||||
balance: number;
|
||||
};
|
||||
CreateCashDto: {
|
||||
/**
|
||||
* @description Название
|
||||
* @example Наличные
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @description Баланс
|
||||
* @example 10000
|
||||
*/
|
||||
balance: number;
|
||||
};
|
||||
CashDto: {
|
||||
/** @description ID */
|
||||
id: string;
|
||||
/**
|
||||
* @description Название
|
||||
* @example Наличные
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @description Баланс
|
||||
* @example 10000
|
||||
*/
|
||||
balance: number;
|
||||
/**
|
||||
* Format: date-time
|
||||
* @description Дата создания
|
||||
*/
|
||||
createdAt: string;
|
||||
/**
|
||||
* Format: date-time
|
||||
* @description Дата обновления
|
||||
*/
|
||||
updatedAt: string;
|
||||
};
|
||||
EditCashDto: {
|
||||
/**
|
||||
* @description Название
|
||||
* @example Наличные
|
||||
*/
|
||||
name: string;
|
||||
/**
|
||||
* @description Баланс
|
||||
* @example 10000
|
||||
*/
|
||||
balance: number;
|
||||
};
|
||||
};
|
||||
responses: never;
|
||||
parameters: never;
|
||||
@ -1097,4 +1197,142 @@ export interface operations {
|
||||
};
|
||||
};
|
||||
};
|
||||
CashController_getAllCashByUserId: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Успешно найдены наличные */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["CashDto"][];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
CashController_createCash: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["CreateCashDto"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Успешно созданы наличные */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["CashDto"];
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
CashController_getCashById: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
/** @description ID наличных */
|
||||
id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Успешно найдены наличные */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["CashDto"];
|
||||
};
|
||||
};
|
||||
/** @description Кэш не найден */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
CashController_deleteCash: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
/** @description ID наличных */
|
||||
id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Успешно удалены наличные */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["CashDto"];
|
||||
};
|
||||
};
|
||||
/** @description Кэш не найден */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
CashController_editCash: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
/** @description ID наличных */
|
||||
id: string;
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["EditCashDto"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Успешно отредактированы наличные */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["CashDto"];
|
||||
};
|
||||
};
|
||||
/** @description Кэш не найден */
|
||||
404: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ export enum PAGES {
|
||||
Expenses = '/expenses',
|
||||
Expense = '/expenses/:id',
|
||||
BankCards = '/bank-cards',
|
||||
Cash = '/cash',
|
||||
}
|
||||
|
||||
export type ExpensePageParams = {
|
||||
|
||||
@ -24,6 +24,12 @@ const SideBar = () => (
|
||||
Карты
|
||||
</NavLink>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<NavLink to={PAGES.Cash}>
|
||||
Наличные
|
||||
</NavLink>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
|
||||
1
frontend/src/widgets/cash/CashTable/index.ts
Normal file
1
frontend/src/widgets/cash/CashTable/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export { default as CashTable } from './ui/CashTable.ui';
|
||||
@ -0,0 +1,3 @@
|
||||
.table, th, td {
|
||||
border: 1px solid;
|
||||
}
|
||||
60
frontend/src/widgets/cash/CashTable/ui/CashTable.ui.tsx
Normal file
60
frontend/src/widgets/cash/CashTable/ui/CashTable.ui.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import { DeleteCashButton } from '@/features/cash/DeleteCashButton';
|
||||
import { EditCashButton } from '@/features/cash/EditCashButton';
|
||||
import { formatRub } from '@/shared/utils';
|
||||
|
||||
import classes from './CashTable.module.css';
|
||||
|
||||
import type { BankCards } from '@/entity/cards';
|
||||
import type { FC } from 'react';
|
||||
|
||||
type CashTableProps = {
|
||||
items: BankCards;
|
||||
};
|
||||
|
||||
const CashTable: FC<CashTableProps> = (props) => {
|
||||
const {
|
||||
items,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<table className={classes.table}>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Идентификатор</th>
|
||||
|
||||
<th>Название</th>
|
||||
|
||||
<th>Баланс</th>
|
||||
|
||||
<th>Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
{items.map((item) => (
|
||||
<tr key={item.id}>
|
||||
<td>
|
||||
{item.id}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{item.name}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
{formatRub(item.balance)}
|
||||
</td>
|
||||
|
||||
<td>
|
||||
<EditCashButton cash={item} />
|
||||
|
||||
<DeleteCashButton cash={item} />
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
};
|
||||
|
||||
export default CashTable;
|
||||
Loading…
x
Reference in New Issue
Block a user