From 59ff36cb18629bcf63d70d3b2f0132000e3b7131 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Fri, 12 Sep 2025 07:28:01 +0300 Subject: [PATCH] feat(frontend): add cash --- frontend/src/app/App/ui/App.tsx | 34 +-- frontend/src/app/App/ui/Routing.tsx | 36 +++ frontend/src/entity/cash/api/addCash.ts | 12 + frontend/src/entity/cash/api/deleteCash.ts | 12 + frontend/src/entity/cash/api/editCash.ts | 13 + frontend/src/entity/cash/api/getCash.ts | 11 + frontend/src/entity/cash/hooks/useAddCash.ts | 17 ++ frontend/src/entity/cash/hooks/useCash.ts | 10 + .../src/entity/cash/hooks/useDeleteCash.ts | 17 ++ frontend/src/entity/cash/hooks/useEditCash.ts | 17 ++ frontend/src/entity/cash/index.ts | 16 ++ frontend/src/entity/cash/types/index.ts | 4 + .../src/entity/cash/ui/AddCashForm/index.ts | 2 + .../ui/AddCashForm/ui/AddCashForm.module.css | 5 + .../ui/AddCashForm/ui/AddCashForm.test.tsx | 64 +++++ .../cash/ui/AddCashForm/ui/AddCashForm.ui.tsx | 79 ++++++ .../entity/cash/ui/DeleteCashForm/index.ts | 1 + .../DeleteCashForm/ui/DeleteCashForm.test.tsx | 55 ++++ .../DeleteCashForm/ui/DeleteCashForm.ui.tsx | 53 ++++ .../src/entity/cash/ui/EditCashForm/index.ts | 2 + .../EditCashForm/ui/EditCashForm.module.css | 5 + .../ui/EditCashForm/ui/EditCashForm.test.tsx | 91 +++++++ .../ui/EditCashForm/ui/EditCashForm.ui.tsx | 83 ++++++ .../AddCashButton/hooks/useAddCashButton.ts | 38 +++ .../src/features/cash/AddCashButton/index.ts | 1 + .../AddCashButton/ui/AddCashButton.test.tsx | 107 ++++++++ .../AddCashButton/ui/AddCashButton.ui.tsx | 41 +++ .../hooks/useDeleteCashButton.ts | 37 +++ .../features/cash/DeleteCashButton/index.ts | 1 + .../ui/DeleteCashButton.test.tsx | 76 ++++++ .../ui/DeleteCashButton.ui.tsx | 47 ++++ .../EditCashButton/hooks/useEditCashButton.ts | 40 +++ .../src/features/cash/EditCashButton/index.ts | 1 + .../EditCashButton/ui/EditCashButton.test.tsx | 127 ++++++++++ .../EditCashButton/ui/EditCashButton.ui.tsx | 46 ++++ frontend/src/pages/Cash/index.ts | 1 + frontend/src/pages/Cash/ui/CashPage.ui.tsx | 40 +++ frontend/src/shared/api/queryKeys.ts | 1 + frontend/src/shared/api/schema.ts | 238 ++++++++++++++++++ frontend/src/shared/router/types.ts | 1 + .../src/widgets/Sidebar/ui/SideBar.ui.tsx | 6 + frontend/src/widgets/cash/CashTable/index.ts | 1 + .../cash/CashTable/ui/CashTable.module.css | 3 + .../cash/CashTable/ui/CashTable.ui.tsx | 60 +++++ 44 files changed, 1521 insertions(+), 31 deletions(-) create mode 100644 frontend/src/app/App/ui/Routing.tsx create mode 100644 frontend/src/entity/cash/api/addCash.ts create mode 100644 frontend/src/entity/cash/api/deleteCash.ts create mode 100644 frontend/src/entity/cash/api/editCash.ts create mode 100644 frontend/src/entity/cash/api/getCash.ts create mode 100644 frontend/src/entity/cash/hooks/useAddCash.ts create mode 100644 frontend/src/entity/cash/hooks/useCash.ts create mode 100644 frontend/src/entity/cash/hooks/useDeleteCash.ts create mode 100644 frontend/src/entity/cash/hooks/useEditCash.ts create mode 100644 frontend/src/entity/cash/index.ts create mode 100644 frontend/src/entity/cash/types/index.ts create mode 100644 frontend/src/entity/cash/ui/AddCashForm/index.ts create mode 100644 frontend/src/entity/cash/ui/AddCashForm/ui/AddCashForm.module.css create mode 100644 frontend/src/entity/cash/ui/AddCashForm/ui/AddCashForm.test.tsx create mode 100644 frontend/src/entity/cash/ui/AddCashForm/ui/AddCashForm.ui.tsx create mode 100644 frontend/src/entity/cash/ui/DeleteCashForm/index.ts create mode 100644 frontend/src/entity/cash/ui/DeleteCashForm/ui/DeleteCashForm.test.tsx create mode 100644 frontend/src/entity/cash/ui/DeleteCashForm/ui/DeleteCashForm.ui.tsx create mode 100644 frontend/src/entity/cash/ui/EditCashForm/index.ts create mode 100644 frontend/src/entity/cash/ui/EditCashForm/ui/EditCashForm.module.css create mode 100644 frontend/src/entity/cash/ui/EditCashForm/ui/EditCashForm.test.tsx create mode 100644 frontend/src/entity/cash/ui/EditCashForm/ui/EditCashForm.ui.tsx create mode 100644 frontend/src/features/cash/AddCashButton/hooks/useAddCashButton.ts create mode 100644 frontend/src/features/cash/AddCashButton/index.ts create mode 100644 frontend/src/features/cash/AddCashButton/ui/AddCashButton.test.tsx create mode 100644 frontend/src/features/cash/AddCashButton/ui/AddCashButton.ui.tsx create mode 100644 frontend/src/features/cash/DeleteCashButton/hooks/useDeleteCashButton.ts create mode 100644 frontend/src/features/cash/DeleteCashButton/index.ts create mode 100644 frontend/src/features/cash/DeleteCashButton/ui/DeleteCashButton.test.tsx create mode 100644 frontend/src/features/cash/DeleteCashButton/ui/DeleteCashButton.ui.tsx create mode 100644 frontend/src/features/cash/EditCashButton/hooks/useEditCashButton.ts create mode 100644 frontend/src/features/cash/EditCashButton/index.ts create mode 100644 frontend/src/features/cash/EditCashButton/ui/EditCashButton.test.tsx create mode 100644 frontend/src/features/cash/EditCashButton/ui/EditCashButton.ui.tsx create mode 100644 frontend/src/pages/Cash/index.ts create mode 100644 frontend/src/pages/Cash/ui/CashPage.ui.tsx create mode 100644 frontend/src/widgets/cash/CashTable/index.ts create mode 100644 frontend/src/widgets/cash/CashTable/ui/CashTable.module.css create mode 100644 frontend/src/widgets/cash/CashTable/ui/CashTable.ui.tsx diff --git a/frontend/src/app/App/ui/App.tsx b/frontend/src/app/App/ui/App.tsx index 6ab0f55..f4cafa2 100644 --- a/frontend/src/app/App/ui/App.tsx +++ b/frontend/src/app/App/ui/App.tsx @@ -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 = () => ( - - - }> - } path={PAGES.Home} /> - - } path={PAGES.Expenses} /> - - } path={PAGES.Expense} /> - - } path={PAGES.BankCards} /> - - - }> - } path={PAGES.Login} /> - - } path={PAGES.Register} /> - - - + ); diff --git a/frontend/src/app/App/ui/Routing.tsx b/frontend/src/app/App/ui/Routing.tsx new file mode 100644 index 0000000..b1b60dc --- /dev/null +++ b/frontend/src/app/App/ui/Routing.tsx @@ -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 = () => ( + + + }> + } path={PAGES.Home} /> + + } path={PAGES.Expenses} /> + + } path={PAGES.Expense} /> + + } path={PAGES.BankCards} /> + + } path={PAGES.Cash} /> + + + }> + } path={PAGES.Login} /> + + } path={PAGES.Register} /> + + + +); + +export default Routing; diff --git a/frontend/src/entity/cash/api/addCash.ts b/frontend/src/entity/cash/api/addCash.ts new file mode 100644 index 0000000..aba6279 --- /dev/null +++ b/frontend/src/entity/cash/api/addCash.ts @@ -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('/cash', data); + + return result; +} diff --git a/frontend/src/entity/cash/api/deleteCash.ts b/frontend/src/entity/cash/api/deleteCash.ts new file mode 100644 index 0000000..9386fd3 --- /dev/null +++ b/frontend/src/entity/cash/api/deleteCash.ts @@ -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(`/cash/${path.id}`); + + return result; +} diff --git a/frontend/src/entity/cash/api/editCash.ts b/frontend/src/entity/cash/api/editCash.ts new file mode 100644 index 0000000..481e8ed --- /dev/null +++ b/frontend/src/entity/cash/api/editCash.ts @@ -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(`/cash/${path.id}`, data); + + return result; +} diff --git a/frontend/src/entity/cash/api/getCash.ts b/frontend/src/entity/cash/api/getCash.ts new file mode 100644 index 0000000..7423df4 --- /dev/null +++ b/frontend/src/entity/cash/api/getCash.ts @@ -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('/cash'); + + return result; +} diff --git a/frontend/src/entity/cash/hooks/useAddCash.ts b/frontend/src/entity/cash/hooks/useAddCash.ts new file mode 100644 index 0000000..e866c16 --- /dev/null +++ b/frontend/src/entity/cash/hooks/useAddCash.ts @@ -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] }); + }, + }); +}; diff --git a/frontend/src/entity/cash/hooks/useCash.ts b/frontend/src/entity/cash/hooks/useCash.ts new file mode 100644 index 0000000..bb29a09 --- /dev/null +++ b/frontend/src/entity/cash/hooks/useCash.ts @@ -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], +}); diff --git a/frontend/src/entity/cash/hooks/useDeleteCash.ts b/frontend/src/entity/cash/hooks/useDeleteCash.ts new file mode 100644 index 0000000..8fd7c5f --- /dev/null +++ b/frontend/src/entity/cash/hooks/useDeleteCash.ts @@ -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] }); + }, + }); +}; diff --git a/frontend/src/entity/cash/hooks/useEditCash.ts b/frontend/src/entity/cash/hooks/useEditCash.ts new file mode 100644 index 0000000..e92924b --- /dev/null +++ b/frontend/src/entity/cash/hooks/useEditCash.ts @@ -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] }); + }, + }); +}; diff --git a/frontend/src/entity/cash/index.ts b/frontend/src/entity/cash/index.ts new file mode 100644 index 0000000..933beb4 --- /dev/null +++ b/frontend/src/entity/cash/index.ts @@ -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'; diff --git a/frontend/src/entity/cash/types/index.ts b/frontend/src/entity/cash/types/index.ts new file mode 100644 index 0000000..edbf3a2 --- /dev/null +++ b/frontend/src/entity/cash/types/index.ts @@ -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']; diff --git a/frontend/src/entity/cash/ui/AddCashForm/index.ts b/frontend/src/entity/cash/ui/AddCashForm/index.ts new file mode 100644 index 0000000..4997c4c --- /dev/null +++ b/frontend/src/entity/cash/ui/AddCashForm/index.ts @@ -0,0 +1,2 @@ +export type { AddCashFormValues } from './ui/AddCashForm.ui'; +export { default as AddCashForm } from './ui/AddCashForm.ui'; diff --git a/frontend/src/entity/cash/ui/AddCashForm/ui/AddCashForm.module.css b/frontend/src/entity/cash/ui/AddCashForm/ui/AddCashForm.module.css new file mode 100644 index 0000000..a97ffec --- /dev/null +++ b/frontend/src/entity/cash/ui/AddCashForm/ui/AddCashForm.module.css @@ -0,0 +1,5 @@ +.form { + display: flex; + flex-direction: column; + gap: 10px; +} diff --git a/frontend/src/entity/cash/ui/AddCashForm/ui/AddCashForm.test.tsx b/frontend/src/entity/cash/ui/AddCashForm/ui/AddCashForm.test.tsx new file mode 100644 index 0000000..9e42356 --- /dev/null +++ b/frontend/src/entity/cash/ui/AddCashForm/ui/AddCashForm.test.tsx @@ -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) => { + const onSubmit = jest.fn(); + + render(); + + 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, + }); + }); +}); diff --git a/frontend/src/entity/cash/ui/AddCashForm/ui/AddCashForm.ui.tsx b/frontend/src/entity/cash/ui/AddCashForm/ui/AddCashForm.ui.tsx new file mode 100644 index 0000000..602f386 --- /dev/null +++ b/frontend/src/entity/cash/ui/AddCashForm/ui/AddCashForm.ui.tsx @@ -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, 'onSubmit'>; + +export type AddCashFormProps = BaseFormProps & { + disabled?: boolean; + onSubmit?: (data: AddCashFormValues) => Promise | void; + ref?: RefObject; +}; + +const AddCashForm: FC = (props) => { + const { + ref, + disabled, + onSubmit, + ...formProps + } = props; + + const onFormSubmitHandler: FormEventHandler = (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 ( +
+

Добавить наличные

+ + + + + + +
+ ); +}; + +export default AddCashForm; diff --git a/frontend/src/entity/cash/ui/DeleteCashForm/index.ts b/frontend/src/entity/cash/ui/DeleteCashForm/index.ts new file mode 100644 index 0000000..bed4a22 --- /dev/null +++ b/frontend/src/entity/cash/ui/DeleteCashForm/index.ts @@ -0,0 +1 @@ +export { default as DeleteCashForm } from './ui/DeleteCashForm.ui'; diff --git a/frontend/src/entity/cash/ui/DeleteCashForm/ui/DeleteCashForm.test.tsx b/frontend/src/entity/cash/ui/DeleteCashForm/ui/DeleteCashForm.test.tsx new file mode 100644 index 0000000..0bb6c73 --- /dev/null +++ b/frontend/src/entity/cash/ui/DeleteCashForm/ui/DeleteCashForm.test.tsx @@ -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) => { + const onSubmit = jest.fn(); + + render( + , + ); + + 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); + }); +}); diff --git a/frontend/src/entity/cash/ui/DeleteCashForm/ui/DeleteCashForm.ui.tsx b/frontend/src/entity/cash/ui/DeleteCashForm/ui/DeleteCashForm.ui.tsx new file mode 100644 index 0000000..98091ca --- /dev/null +++ b/frontend/src/entity/cash/ui/DeleteCashForm/ui/DeleteCashForm.ui.tsx @@ -0,0 +1,53 @@ +import type { CashItem } from '../../../types'; +import type { + FC, + FormEventHandler, + FormHTMLAttributes, + RefObject, +} from 'react'; + +type BaseFormProps = Omit, 'onSubmit'>; + +export type DeleteCashFormProps = BaseFormProps & { + cash: CashItem; + disabled?: boolean; + onSubmit?: (cash: CashItem) => Promise | void; + ref?: RefObject; +}; + +const DeleteCashForm: FC = (props) => { + const { + cash, + disabled, + onSubmit, + ref, + ...restProps + } = props; + + const onFormSubmitHandler: FormEventHandler = (event) => { + event.preventDefault(); + onSubmit?.(cash); + }; + + return ( +
+

Удалить наличные

+ +

+ Вы действительно хотите удалить наличные + {' '} + + + {`"${cash.name}"`} + + + {' '} + ? +

+ + +
+ ); +}; + +export default DeleteCashForm; diff --git a/frontend/src/entity/cash/ui/EditCashForm/index.ts b/frontend/src/entity/cash/ui/EditCashForm/index.ts new file mode 100644 index 0000000..a4f20fd --- /dev/null +++ b/frontend/src/entity/cash/ui/EditCashForm/index.ts @@ -0,0 +1,2 @@ +export type { EditCashFormValues } from './ui/EditCashForm.ui'; +export { default as EditCashForm } from './ui/EditCashForm.ui'; diff --git a/frontend/src/entity/cash/ui/EditCashForm/ui/EditCashForm.module.css b/frontend/src/entity/cash/ui/EditCashForm/ui/EditCashForm.module.css new file mode 100644 index 0000000..a97ffec --- /dev/null +++ b/frontend/src/entity/cash/ui/EditCashForm/ui/EditCashForm.module.css @@ -0,0 +1,5 @@ +.form { + display: flex; + flex-direction: column; + gap: 10px; +} diff --git a/frontend/src/entity/cash/ui/EditCashForm/ui/EditCashForm.test.tsx b/frontend/src/entity/cash/ui/EditCashForm/ui/EditCashForm.test.tsx new file mode 100644 index 0000000..2e7bbae --- /dev/null +++ b/frontend/src/entity/cash/ui/EditCashForm/ui/EditCashForm.test.tsx @@ -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) => { + const onSubmit = jest.fn(); + + render( + , + ); + + 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, + }); + }); +}); diff --git a/frontend/src/entity/cash/ui/EditCashForm/ui/EditCashForm.ui.tsx b/frontend/src/entity/cash/ui/EditCashForm/ui/EditCashForm.ui.tsx new file mode 100644 index 0000000..0be6c42 --- /dev/null +++ b/frontend/src/entity/cash/ui/EditCashForm/ui/EditCashForm.ui.tsx @@ -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, 'onSubmit'>; + +export type EditCashFormValues = { + name: string; + balance: number; +}; + +export type EditCashFormProps = BaseFormProps & { + cash: CashItem; + disabled?: boolean; + onSubmit?: (data: EditCashFormValues) => Promise | void; + ref?: RefObject; +}; + +const EditCashForm: FC = (props) => { + const { + cash, + disabled, + onSubmit, + ref, + ...restProps + } = props; + + const onFormSubmitHandler: FormEventHandler = (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 ( +
+

Редактировать наличные

+ + + + + + +
+ ); +}; + +export default EditCashForm; diff --git a/frontend/src/features/cash/AddCashButton/hooks/useAddCashButton.ts b/frontend/src/features/cash/AddCashButton/hooks/useAddCashButton.ts new file mode 100644 index 0000000..0e37dc3 --- /dev/null +++ b/frontend/src/features/cash/AddCashButton/hooks/useAddCashButton.ts @@ -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; +}; + +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 = (event) => { + onClick?.(event); + setIsOpen(true); + }; + + const onFormSubmitHandler = async (data: AddCashFormValues) => { + await addCashMutation(data); + setIsOpen(false); + }; + + return { + isOpen, + isPending, + onCloseModalHandler, + onClickHandler, + onFormSubmitHandler, + }; +}; diff --git a/frontend/src/features/cash/AddCashButton/index.ts b/frontend/src/features/cash/AddCashButton/index.ts new file mode 100644 index 0000000..3b5e574 --- /dev/null +++ b/frontend/src/features/cash/AddCashButton/index.ts @@ -0,0 +1 @@ +export { default as AddCashButton } from './ui/AddCashButton.ui'; diff --git a/frontend/src/features/cash/AddCashButton/ui/AddCashButton.test.tsx b/frontend/src/features/cash/AddCashButton/ui/AddCashButton.test.tsx new file mode 100644 index 0000000..7d8b912 --- /dev/null +++ b/frontend/src/features/cash/AddCashButton/ui/AddCashButton.test.tsx @@ -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( + , + { 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( + '/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(); + }); + }); +}); diff --git a/frontend/src/features/cash/AddCashButton/ui/AddCashButton.ui.tsx b/frontend/src/features/cash/AddCashButton/ui/AddCashButton.ui.tsx new file mode 100644 index 0000000..59c67bd --- /dev/null +++ b/frontend/src/features/cash/AddCashButton/ui/AddCashButton.ui.tsx @@ -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 & { + ref?: Ref; +}; + +const AddCashButton: FC = (props) => { + const { ref, onClick, ...restProps } = props; + + const { + isOpen, + isPending, + onCloseModalHandler, + onClickHandler, + onFormSubmitHandler, + } = useAddCashButton({ onClick }); + + return ( + <> + + + + + + + ); +}; + +export default AddCashButton; diff --git a/frontend/src/features/cash/DeleteCashButton/hooks/useDeleteCashButton.ts b/frontend/src/features/cash/DeleteCashButton/hooks/useDeleteCashButton.ts new file mode 100644 index 0000000..04837aa --- /dev/null +++ b/frontend/src/features/cash/DeleteCashButton/hooks/useDeleteCashButton.ts @@ -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, + }; +}; diff --git a/frontend/src/features/cash/DeleteCashButton/index.ts b/frontend/src/features/cash/DeleteCashButton/index.ts new file mode 100644 index 0000000..eeca64e --- /dev/null +++ b/frontend/src/features/cash/DeleteCashButton/index.ts @@ -0,0 +1 @@ +export { default as DeleteCashButton } from './ui/DeleteCashButton.ui'; diff --git a/frontend/src/features/cash/DeleteCashButton/ui/DeleteCashButton.test.tsx b/frontend/src/features/cash/DeleteCashButton/ui/DeleteCashButton.test.tsx new file mode 100644 index 0000000..14ff6c4 --- /dev/null +++ b/frontend/src/features/cash/DeleteCashButton/ui/DeleteCashButton.test.tsx @@ -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( + , + { 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(`/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(); + }); +}); diff --git a/frontend/src/features/cash/DeleteCashButton/ui/DeleteCashButton.ui.tsx b/frontend/src/features/cash/DeleteCashButton/ui/DeleteCashButton.ui.tsx new file mode 100644 index 0000000..63eb8ff --- /dev/null +++ b/frontend/src/features/cash/DeleteCashButton/ui/DeleteCashButton.ui.tsx @@ -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 & { + cash: CashItem; + ref?: Ref; +}; + +const DeleteCashButton: FC = (props) => { + const { + cash, + ref, + onClick, + ...restProps + } = props; + + const { + isOpen, + isPending, + onCloseModalHandler, + onClickHandler, + onFormSubmitHandler, + } = useDeleteCashButton({ id: cash.id }); + + return ( + <> + + + + + + + ); +}; + +export default DeleteCashButton; diff --git a/frontend/src/features/cash/EditCashButton/hooks/useEditCashButton.ts b/frontend/src/features/cash/EditCashButton/hooks/useEditCashButton.ts new file mode 100644 index 0000000..8f9f4d7 --- /dev/null +++ b/frontend/src/features/cash/EditCashButton/hooks/useEditCashButton.ts @@ -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, + }; +}; diff --git a/frontend/src/features/cash/EditCashButton/index.ts b/frontend/src/features/cash/EditCashButton/index.ts new file mode 100644 index 0000000..116a89f --- /dev/null +++ b/frontend/src/features/cash/EditCashButton/index.ts @@ -0,0 +1 @@ +export { default as EditCashButton } from './ui/EditCashButton.ui'; diff --git a/frontend/src/features/cash/EditCashButton/ui/EditCashButton.test.tsx b/frontend/src/features/cash/EditCashButton/ui/EditCashButton.test.tsx new file mode 100644 index 0000000..4b8c85c --- /dev/null +++ b/frontend/src/features/cash/EditCashButton/ui/EditCashButton.test.tsx @@ -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( + , + { 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( + `/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(); + }); + }); +}); diff --git a/frontend/src/features/cash/EditCashButton/ui/EditCashButton.ui.tsx b/frontend/src/features/cash/EditCashButton/ui/EditCashButton.ui.tsx new file mode 100644 index 0000000..772413b --- /dev/null +++ b/frontend/src/features/cash/EditCashButton/ui/EditCashButton.ui.tsx @@ -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 & { + cash: CashItem; + ref?: Ref; +}; +const EditCashButton: FC = (props) => { + const { + cash, + ref, + onClick, + ...restProps + } = props; + + const { + isOpen, + isPending, + onCloseModalHandler, + onClickHandler, + onFormSubmitHandler, + } = useEditCashButton({ cash }); + + return ( + <> + + + + + + + ); +}; + +export default EditCashButton; diff --git a/frontend/src/pages/Cash/index.ts b/frontend/src/pages/Cash/index.ts new file mode 100644 index 0000000..aac6cdb --- /dev/null +++ b/frontend/src/pages/Cash/index.ts @@ -0,0 +1 @@ +export { default as CashPage } from './ui/CashPage.ui'; diff --git a/frontend/src/pages/Cash/ui/CashPage.ui.tsx b/frontend/src/pages/Cash/ui/CashPage.ui.tsx new file mode 100644 index 0000000..cc7a9c6 --- /dev/null +++ b/frontend/src/pages/Cash/ui/CashPage.ui.tsx @@ -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 ; + } + + if (cashList && cashList.length > 0) { + return ; + } + + return

Нет добавленных наличных

; + }; + + return ( + +
+

Мои наличные

+ +
+ +
+ + {renderContent()} + +
+
+ ); +}; + +export default CashPage; diff --git a/frontend/src/shared/api/queryKeys.ts b/frontend/src/shared/api/queryKeys.ts index 7fc9572..9df3012 100644 --- a/frontend/src/shared/api/queryKeys.ts +++ b/frontend/src/shared/api/queryKeys.ts @@ -4,4 +4,5 @@ export enum QueryKeys { BankCard = 'BankCard', ExpensesList = 'ExpensesList', ExpenseList = 'ExpenseList', + Cash = 'Cash', } diff --git a/frontend/src/shared/api/schema.ts b/frontend/src/shared/api/schema.ts index a90424e..4d62710 100644 --- a/frontend/src/shared/api/schema.ts +++ b/frontend/src/shared/api/schema.ts @@ -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; 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; + }; + }; + }; } diff --git a/frontend/src/shared/router/types.ts b/frontend/src/shared/router/types.ts index 65e53dc..37865ed 100644 --- a/frontend/src/shared/router/types.ts +++ b/frontend/src/shared/router/types.ts @@ -5,6 +5,7 @@ export enum PAGES { Expenses = '/expenses', Expense = '/expenses/:id', BankCards = '/bank-cards', + Cash = '/cash', } export type ExpensePageParams = { diff --git a/frontend/src/widgets/Sidebar/ui/SideBar.ui.tsx b/frontend/src/widgets/Sidebar/ui/SideBar.ui.tsx index b1c0c4e..5f9ab6f 100644 --- a/frontend/src/widgets/Sidebar/ui/SideBar.ui.tsx +++ b/frontend/src/widgets/Sidebar/ui/SideBar.ui.tsx @@ -24,6 +24,12 @@ const SideBar = () => ( Карты + +
  • + + Наличные + +
  • ); diff --git a/frontend/src/widgets/cash/CashTable/index.ts b/frontend/src/widgets/cash/CashTable/index.ts new file mode 100644 index 0000000..c6c2d61 --- /dev/null +++ b/frontend/src/widgets/cash/CashTable/index.ts @@ -0,0 +1 @@ +export { default as CashTable } from './ui/CashTable.ui'; diff --git a/frontend/src/widgets/cash/CashTable/ui/CashTable.module.css b/frontend/src/widgets/cash/CashTable/ui/CashTable.module.css new file mode 100644 index 0000000..77af1d2 --- /dev/null +++ b/frontend/src/widgets/cash/CashTable/ui/CashTable.module.css @@ -0,0 +1,3 @@ +.table, th, td { + border: 1px solid; +} diff --git a/frontend/src/widgets/cash/CashTable/ui/CashTable.ui.tsx b/frontend/src/widgets/cash/CashTable/ui/CashTable.ui.tsx new file mode 100644 index 0000000..8031d68 --- /dev/null +++ b/frontend/src/widgets/cash/CashTable/ui/CashTable.ui.tsx @@ -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 = (props) => { + const { + items, + } = props; + + return ( + + + + + + + + + + + + + + + {items.map((item) => ( + + + + + + + + + + ))} + +
    ИдентификаторНазваниеБалансДействия
    + {item.id} + + {item.name} + + {formatRub(item.balance)} + + + + +
    + ); +}; + +export default CashTable;