From 10c0e5cf7caf0492578092d2b2a286ce7cf7b303 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Thu, 28 Aug 2025 05:05:29 +0300 Subject: [PATCH] feat(frontend): add expenses --- frontend/eslint.config.ts | 5 + frontend/package-lock.json | 16 +- frontend/package.json | 2 +- frontend/src/app/App/ui/App.tsx | 13 +- frontend/src/app/App/ui/normalize.css | 4 + frontend/src/entity/account/api/login.ts | 12 + frontend/src/entity/account/api/logout.ts | 11 + .../useRegister.ts => api/register.ts} | 12 +- .../account/{login => hooks}/useLogin.ts | 15 +- .../account/{logout => hooks}/useLogout.ts | 14 +- .../src/entity/account/hooks/useRegister.ts | 11 + frontend/src/entity/account/index.ts | 6 +- .../expenses/item/api/addExpenseItem.ts | 13 + .../expenses/item/api/deleteExpenseItem.ts | 12 + .../expenses/item/api/editExpenseItem.ts | 13 + .../expenses/item/hooks/useAddExpenseItem.ts | 15 + .../item/hooks/useDeleteExpenseItem.ts | 17 + .../expenses/item/hooks/useEditExpenseItem.ts | 15 + frontend/src/entity/expenses/item/index.ts | 5 + frontend/src/entity/expenses/item/types.ts | 3 + .../expenses/list/api/addExpensesList.ts | 12 + .../expenses/list/api/deleteExpensesList.ts | 12 + .../expenses/list/api/editExpensesList.ts | 17 + .../expenses/list/api/getExpenseList.ts | 12 + .../expenses/list/api/getExpenseLists.ts | 11 + .../expenses/list/hooks/useAddExpensesList.ts | 15 + .../list/hooks/useDeleteExpensesList.ts | 15 + .../list/hooks/useEditExpensesList.ts | 15 + .../expenses/list/hooks/useExpenseList.ts | 10 + .../expenses/list/hooks/useExpensesList.ts | 8 + frontend/src/entity/expenses/list/index.ts | 7 + frontend/src/entity/expenses/list/types.ts | 3 + .../hooks/useAddExpenseItemButton.ts | 49 ++ .../items/AddExpenseItemButton/index.ts | 1 + .../ui/AddExpenseItemButton.module.css | 5 + .../ui/AddExpenseItemButton.ui.tsx | 80 +++ .../hooks/useDeleteExpenseItemButton.ts | 35 ++ .../items/DeleteExpenseItemButton/index.ts | 1 + .../ui/DeleteExpenseItemButton.ui.tsx | 38 ++ .../hooks/useEditExpenseItemButton.ts | 50 ++ .../items/EditExpenseItemButton/index.ts | 1 + .../ui/EditExpenseItemButton.module.css | 5 + .../ui/EditExpenseItemButton.ui.tsx | 81 +++ .../hooks/useAddExpensesListButton.ts | 43 ++ .../list/AddExpensesListButton/index.ts | 1 + .../ui/AddExpensesListButton.ui.tsx | 42 ++ .../hooks/useDeleteExpensesListButton.ts | 36 ++ .../list/DeleteExpensesListButton/index.ts | 1 + .../ui/DeleteExpensesListButton.ui.tsx | 51 ++ .../hooks/useEditExpensesListButton.ts | 44 ++ .../list/EditExpensesListButton/index.ts | 1 + .../ui/EditExpensesListButton.ui.tsx | 49 ++ frontend/src/pages/Expense/index.ts | 1 + .../src/pages/Expense/ui/ExpensePage.ui.tsx | 45 ++ frontend/src/pages/Expenses/index.ts | 1 + .../src/pages/Expenses/ui/ExpensesPage.ui.tsx | 38 ++ frontend/src/pages/Home/ui/HomePage.ui.tsx | 52 +- frontend/src/shared/api/client.ts | 17 +- frontend/src/shared/api/schema.ts | 541 +++++++++++++++++- frontend/src/shared/router/index.ts | 1 + frontend/src/shared/router/types.ts | 6 + .../src/shared/ui/{input => Input}/index.ts | 0 .../ui/Input.styles.module.css | 0 .../ui/{input => Input}/ui/Input.ui.tsx | 0 frontend/src/shared/ui/Modal/index.ts | 1 + .../src/shared/ui/Modal/ui/Modal.module.css | 27 + frontend/src/shared/ui/Modal/ui/Modal.ui.tsx | 29 + frontend/src/shared/ui/Spinner/index.ts | 1 + .../shared/ui/Spinner/ui/Spinner.module.css | 19 + .../src/shared/ui/Spinner/ui/Spinner.ui.tsx | 7 + frontend/src/widgets/AppLayout/index.ts | 1 + .../widgets/AppLayout/ui/AppLayout.module.css | 7 + .../src/widgets/AppLayout/ui/AppLayout.ui.tsx | 25 + frontend/src/widgets/Sidebar/index.ts | 1 + .../src/widgets/Sidebar/ui/SideBar.module.css | 5 + .../src/widgets/Sidebar/ui/SideBar.ui.tsx | 25 + .../auth/LoginForm/ui/LoginForm.ui.tsx | 2 +- .../auth/RegisterForm/ui/RegisterForm.ui.tsx | 2 +- .../expenses/ExpenseItemsTable/index.ts | 1 + .../ui/ExpenseItemsTable.ui.tsx | 81 +++ .../widgets/expenses/ExpensesTable/index.ts | 1 + .../ExpensesTable/ui/ExpensesTable.module.css | 3 + .../ExpensesTable/ui/ExpensesTable.ui.tsx | 67 +++ 83 files changed, 1863 insertions(+), 109 deletions(-) create mode 100644 frontend/src/entity/account/api/login.ts create mode 100644 frontend/src/entity/account/api/logout.ts rename frontend/src/entity/account/{register/useRegister.ts => api/register.ts} (56%) rename frontend/src/entity/account/{login => hooks}/useLogin.ts (55%) rename frontend/src/entity/account/{logout => hooks}/useLogout.ts (56%) create mode 100644 frontend/src/entity/account/hooks/useRegister.ts create mode 100644 frontend/src/entity/expenses/item/api/addExpenseItem.ts create mode 100644 frontend/src/entity/expenses/item/api/deleteExpenseItem.ts create mode 100644 frontend/src/entity/expenses/item/api/editExpenseItem.ts create mode 100644 frontend/src/entity/expenses/item/hooks/useAddExpenseItem.ts create mode 100644 frontend/src/entity/expenses/item/hooks/useDeleteExpenseItem.ts create mode 100644 frontend/src/entity/expenses/item/hooks/useEditExpenseItem.ts create mode 100644 frontend/src/entity/expenses/item/index.ts create mode 100644 frontend/src/entity/expenses/item/types.ts create mode 100644 frontend/src/entity/expenses/list/api/addExpensesList.ts create mode 100644 frontend/src/entity/expenses/list/api/deleteExpensesList.ts create mode 100644 frontend/src/entity/expenses/list/api/editExpensesList.ts create mode 100644 frontend/src/entity/expenses/list/api/getExpenseList.ts create mode 100644 frontend/src/entity/expenses/list/api/getExpenseLists.ts create mode 100644 frontend/src/entity/expenses/list/hooks/useAddExpensesList.ts create mode 100644 frontend/src/entity/expenses/list/hooks/useDeleteExpensesList.ts create mode 100644 frontend/src/entity/expenses/list/hooks/useEditExpensesList.ts create mode 100644 frontend/src/entity/expenses/list/hooks/useExpenseList.ts create mode 100644 frontend/src/entity/expenses/list/hooks/useExpensesList.ts create mode 100644 frontend/src/entity/expenses/list/index.ts create mode 100644 frontend/src/entity/expenses/list/types.ts create mode 100644 frontend/src/features/expenses/items/AddExpenseItemButton/hooks/useAddExpenseItemButton.ts create mode 100644 frontend/src/features/expenses/items/AddExpenseItemButton/index.ts create mode 100644 frontend/src/features/expenses/items/AddExpenseItemButton/ui/AddExpenseItemButton.module.css create mode 100644 frontend/src/features/expenses/items/AddExpenseItemButton/ui/AddExpenseItemButton.ui.tsx create mode 100644 frontend/src/features/expenses/items/DeleteExpenseItemButton/hooks/useDeleteExpenseItemButton.ts create mode 100644 frontend/src/features/expenses/items/DeleteExpenseItemButton/index.ts create mode 100644 frontend/src/features/expenses/items/DeleteExpenseItemButton/ui/DeleteExpenseItemButton.ui.tsx create mode 100644 frontend/src/features/expenses/items/EditExpenseItemButton/hooks/useEditExpenseItemButton.ts create mode 100644 frontend/src/features/expenses/items/EditExpenseItemButton/index.ts create mode 100644 frontend/src/features/expenses/items/EditExpenseItemButton/ui/EditExpenseItemButton.module.css create mode 100644 frontend/src/features/expenses/items/EditExpenseItemButton/ui/EditExpenseItemButton.ui.tsx create mode 100644 frontend/src/features/expenses/list/AddExpensesListButton/hooks/useAddExpensesListButton.ts create mode 100644 frontend/src/features/expenses/list/AddExpensesListButton/index.ts create mode 100644 frontend/src/features/expenses/list/AddExpensesListButton/ui/AddExpensesListButton.ui.tsx create mode 100644 frontend/src/features/expenses/list/DeleteExpensesListButton/hooks/useDeleteExpensesListButton.ts create mode 100644 frontend/src/features/expenses/list/DeleteExpensesListButton/index.ts create mode 100644 frontend/src/features/expenses/list/DeleteExpensesListButton/ui/DeleteExpensesListButton.ui.tsx create mode 100644 frontend/src/features/expenses/list/EditExpensesListButton/hooks/useEditExpensesListButton.ts create mode 100644 frontend/src/features/expenses/list/EditExpensesListButton/index.ts create mode 100644 frontend/src/features/expenses/list/EditExpensesListButton/ui/EditExpensesListButton.ui.tsx create mode 100644 frontend/src/pages/Expense/index.ts create mode 100644 frontend/src/pages/Expense/ui/ExpensePage.ui.tsx create mode 100644 frontend/src/pages/Expenses/index.ts create mode 100644 frontend/src/pages/Expenses/ui/ExpensesPage.ui.tsx rename frontend/src/shared/ui/{input => Input}/index.ts (100%) rename frontend/src/shared/ui/{input => Input}/ui/Input.styles.module.css (100%) rename frontend/src/shared/ui/{input => Input}/ui/Input.ui.tsx (100%) create mode 100644 frontend/src/shared/ui/Modal/index.ts create mode 100644 frontend/src/shared/ui/Modal/ui/Modal.module.css create mode 100644 frontend/src/shared/ui/Modal/ui/Modal.ui.tsx create mode 100644 frontend/src/shared/ui/Spinner/index.ts create mode 100644 frontend/src/shared/ui/Spinner/ui/Spinner.module.css create mode 100644 frontend/src/shared/ui/Spinner/ui/Spinner.ui.tsx create mode 100644 frontend/src/widgets/AppLayout/index.ts create mode 100644 frontend/src/widgets/AppLayout/ui/AppLayout.module.css create mode 100644 frontend/src/widgets/AppLayout/ui/AppLayout.ui.tsx create mode 100644 frontend/src/widgets/Sidebar/index.ts create mode 100644 frontend/src/widgets/Sidebar/ui/SideBar.module.css create mode 100644 frontend/src/widgets/Sidebar/ui/SideBar.ui.tsx create mode 100644 frontend/src/widgets/expenses/ExpenseItemsTable/index.ts create mode 100644 frontend/src/widgets/expenses/ExpenseItemsTable/ui/ExpenseItemsTable.ui.tsx create mode 100644 frontend/src/widgets/expenses/ExpensesTable/index.ts create mode 100644 frontend/src/widgets/expenses/ExpensesTable/ui/ExpensesTable.module.css create mode 100644 frontend/src/widgets/expenses/ExpensesTable/ui/ExpensesTable.ui.tsx diff --git a/frontend/eslint.config.ts b/frontend/eslint.config.ts index 5ed4703..9130106 100644 --- a/frontend/eslint.config.ts +++ b/frontend/eslint.config.ts @@ -77,6 +77,11 @@ export default [ '@typescript-eslint/await-thenable': 'off', '@typescript-eslint/no-misused-promises': 'off', '@typescript-eslint/prefer-nullish-coalescing': 'off', + '@typescript-eslint/no-unnecessary-condition': 'off', + 'jsx-a11y/click-events-have-key-events': 'off', + 'jsx-a11y/no-static-element-interactions': 'off', + '@stylistic/indent': 'off', + '@stylistic/jsx-one-expression-per-line': 'off', }, }, { diff --git a/frontend/package-lock.json b/frontend/package-lock.json index ede4c7f..4439f27 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -23,7 +23,7 @@ "@types/react": "19.1.9", "@types/react-dom": "19.1.7", "@typescript-eslint/parser": "8.39.0", - "eslint": "9.33.0", + "eslint": "9.34.0", "eslint-config-ksv741": "0.2.0", "openapi-typescript": "7.9.1", "typescript": "5.9.2" @@ -203,9 +203,9 @@ } }, "node_modules/@eslint/js": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", - "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz", + "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==", "dev": true, "license": "MIT", "engines": { @@ -2666,9 +2666,9 @@ } }, "node_modules/eslint": { - "version": "9.33.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", - "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", + "version": "9.34.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz", + "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==", "dev": true, "license": "MIT", "dependencies": { @@ -2678,7 +2678,7 @@ "@eslint/config-helpers": "^0.3.1", "@eslint/core": "^0.15.2", "@eslint/eslintrc": "^3.3.1", - "@eslint/js": "9.33.0", + "@eslint/js": "9.34.0", "@eslint/plugin-kit": "^0.3.5", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", diff --git a/frontend/package.json b/frontend/package.json index 3c1f8e6..631e651 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -27,7 +27,7 @@ "@types/react": "19.1.9", "@types/react-dom": "19.1.7", "@typescript-eslint/parser": "8.39.0", - "eslint": "9.33.0", + "eslint": "9.34.0", "eslint-config-ksv741": "0.2.0", "openapi-typescript": "7.9.1", "typescript": "5.9.2" diff --git a/frontend/src/app/App/ui/App.tsx b/frontend/src/app/App/ui/App.tsx index d8f19c2..81d4bef 100644 --- a/frontend/src/app/App/ui/App.tsx +++ b/frontend/src/app/App/ui/App.tsx @@ -7,8 +7,11 @@ import { } from 'react-router'; import { LoginPage, RegisterPage } from '@/pages/Authorization'; +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'; const queryClient = new QueryClient(); @@ -19,13 +22,17 @@ const App = () => ( }> - } path="/" /> + } path={PAGES.Home} /> + + } path={PAGES.Expenses} /> + + } path={PAGES.Expense} /> }> - } path="/login" /> + } path={PAGES.Login} /> - } path="/register" /> + } path={PAGES.Register} /> diff --git a/frontend/src/app/App/ui/normalize.css b/frontend/src/app/App/ui/normalize.css index 192eb9c..b7104b6 100644 --- a/frontend/src/app/App/ui/normalize.css +++ b/frontend/src/app/App/ui/normalize.css @@ -347,3 +347,7 @@ template { [hidden] { display: none; } + +* { + box-sizing: border-box; +} diff --git a/frontend/src/entity/account/api/login.ts b/frontend/src/entity/account/api/login.ts new file mode 100644 index 0000000..fcb38cd --- /dev/null +++ b/frontend/src/entity/account/api/login.ts @@ -0,0 +1,12 @@ +import { api } from '@/shared/api/client'; + +import type { paths } from '@/shared/api/schema'; + +type LoginRequestBody = paths['/auth/login']['post']['requestBody']['content']['application/json']; +type LoginSuccessResponse = paths['/auth/login']['post']['responses']['200']['content']['application/json']; + +export async function login(data: LoginRequestBody): Promise { + const { data: result } = await api.post('/auth/login', data); + + return result; +} diff --git a/frontend/src/entity/account/api/logout.ts b/frontend/src/entity/account/api/logout.ts new file mode 100644 index 0000000..7b171c8 --- /dev/null +++ b/frontend/src/entity/account/api/logout.ts @@ -0,0 +1,11 @@ +import { api } from '@/shared/api/client'; + +import type { paths } from '@/shared/api/schema'; + +type LogoutSuccessResponse = paths['/auth/logout']['post']['responses']['200']['content']['application/json']; + +export async function logout() { + const { data: result } = await api.post('/auth/logout'); + + return result; +} diff --git a/frontend/src/entity/account/register/useRegister.ts b/frontend/src/entity/account/api/register.ts similarity index 56% rename from frontend/src/entity/account/register/useRegister.ts rename to frontend/src/entity/account/api/register.ts index de5cdc6..76e7fc4 100644 --- a/frontend/src/entity/account/register/useRegister.ts +++ b/frontend/src/entity/account/api/register.ts @@ -1,5 +1,3 @@ -import { useMutation } from '@tanstack/react-query'; - import { api } from '@/shared/api/client'; import type { paths } from '@/shared/api/schema'; @@ -7,16 +5,8 @@ import type { paths } from '@/shared/api/schema'; type RegisterRequestBody = paths['/auth/register']['post']['requestBody']['content']['application/json']; type RegisterSuccessResponse = paths['/auth/register']['post']['responses']['200']['content']['application/json']; -async function register(data: RegisterRequestBody) { +export async function register(data: RegisterRequestBody) { const { data: result } = await api.post('auth/register', data); return result; } - -export const useRegister = () => useMutation({ - mutationFn: register, - onError: (error) => { - // eslint-disable-next-line no-console - console.log('Неуспешная регистрация', error); - }, -}); diff --git a/frontend/src/entity/account/login/useLogin.ts b/frontend/src/entity/account/hooks/useLogin.ts similarity index 55% rename from frontend/src/entity/account/login/useLogin.ts rename to frontend/src/entity/account/hooks/useLogin.ts index 5ffa304..877df9a 100644 --- a/frontend/src/entity/account/login/useLogin.ts +++ b/frontend/src/entity/account/hooks/useLogin.ts @@ -2,19 +2,10 @@ import { useMutation } from '@tanstack/react-query'; import { useContext } from 'react'; import { useNavigate } from 'react-router'; -import { api } from '@/shared/api/client'; import { AuthContext } from '@/shared/context/AuthContext'; +import { PAGES } from '@/shared/router'; -import type { paths } from '@/shared/api/schema'; - -type LoginRequestBody = paths['/auth/login']['post']['requestBody']['content']['application/json']; -type LoginSuccessResponse = paths['/auth/login']['post']['responses']['200']['content']['application/json']; - -async function login(data: LoginRequestBody): Promise { - const { data: result } = await api.post('/auth/login', data); - - return result; -} +import { login } from '../api/login'; export const useLogin = () => { const navigate = useNavigate(); @@ -25,7 +16,7 @@ export const useLogin = () => { onSuccess: async (data) => { setAccessToken(data.accessToken); localStorage.setItem('accessToken', data.accessToken); - await navigate('/another'); + await navigate(PAGES.Home); }, onError: (error) => { // eslint-disable-next-line diff --git a/frontend/src/entity/account/logout/useLogout.ts b/frontend/src/entity/account/hooks/useLogout.ts similarity index 56% rename from frontend/src/entity/account/logout/useLogout.ts rename to frontend/src/entity/account/hooks/useLogout.ts index 480d30d..1929b19 100644 --- a/frontend/src/entity/account/logout/useLogout.ts +++ b/frontend/src/entity/account/hooks/useLogout.ts @@ -2,18 +2,10 @@ import { useMutation } from '@tanstack/react-query'; import { useContext } from 'react'; import { useNavigate } from 'react-router'; -import { api } from '@/shared/api/client'; import { AuthContext } from '@/shared/context/AuthContext'; +import { PAGES } from '@/shared/router'; -import type { paths } from '@/shared/api/schema'; - -type LogoutSuccessResponse = paths['/auth/logout']['post']['responses']['200']['content']['application/json']; - -async function logout() { - const { data: result } = await api.post('/auth/logout'); - - return result; -} +import { logout } from '../api/logout'; export const useLogout = () => { const navigate = useNavigate(); @@ -24,7 +16,7 @@ export const useLogout = () => { onSuccess: async () => { setAccessToken(''); localStorage.removeItem('accessToken'); - await navigate('/login'); + await navigate(PAGES.Login); }, }); }; diff --git a/frontend/src/entity/account/hooks/useRegister.ts b/frontend/src/entity/account/hooks/useRegister.ts new file mode 100644 index 0000000..82839b3 --- /dev/null +++ b/frontend/src/entity/account/hooks/useRegister.ts @@ -0,0 +1,11 @@ +import { useMutation } from '@tanstack/react-query'; + +import { register } from '../api/register'; + +export const useRegister = () => useMutation({ + mutationFn: register, + onError: (error) => { + // eslint-disable-next-line no-console + console.log('Неуспешная регистрация', error); + }, +}); diff --git a/frontend/src/entity/account/index.ts b/frontend/src/entity/account/index.ts index 859b060..23f8cd8 100644 --- a/frontend/src/entity/account/index.ts +++ b/frontend/src/entity/account/index.ts @@ -1,3 +1,3 @@ -export { useLogin } from './login/useLogin'; -export { useRegister } from './register/useRegister'; -export { useLogout } from './logout/useLogout'; +export { useLogin } from './hooks/useLogin'; +export { useRegister } from './hooks/useRegister'; +export { useLogout } from './hooks/useLogout'; diff --git a/frontend/src/entity/expenses/item/api/addExpenseItem.ts b/frontend/src/entity/expenses/item/api/addExpenseItem.ts new file mode 100644 index 0000000..8e0bec7 --- /dev/null +++ b/frontend/src/entity/expenses/item/api/addExpenseItem.ts @@ -0,0 +1,13 @@ +import { api } from '@/shared/api/client'; + +import type { paths } from '@/shared/api/schema'; + +export type AddExpenseItemBody = paths['/expenses/list/{id}/items']['post']['requestBody']['content']['application/json']; +export type AddExpenseItemPath = paths['/expenses/list/{id}/items']['post']['parameters']['path']; +export type AddExpenseItemSuccessResponse = paths['/expenses/list/{id}/items']['post']['responses']['200']['content']['application/json']; + +export async function addExpenseItem(path: AddExpenseItemPath, data: AddExpenseItemBody) { + const { data: result } = await api.post(`/expenses/list/${path.id}/items`, data); + + return result; +} diff --git a/frontend/src/entity/expenses/item/api/deleteExpenseItem.ts b/frontend/src/entity/expenses/item/api/deleteExpenseItem.ts new file mode 100644 index 0000000..c77ecfb --- /dev/null +++ b/frontend/src/entity/expenses/item/api/deleteExpenseItem.ts @@ -0,0 +1,12 @@ +import { api } from '@/shared/api/client'; + +import type { paths } from '@/shared/api/schema'; + +export type DeleteExpenseItemPath = paths['/expenses/list/{listId}/items/{id}']['delete']['parameters']['path']; +export type DeleteExpenseItemSuccessResponse = paths['/expenses/list/{id}/items']['post']['responses']['200']['content']['application/json']; + +export async function deleteExpenseItem(path: DeleteExpenseItemPath) { + const { data: result } = await api.delete(`/expenses/list/${path.listId}/items/${path.id}`); + + return result; +} diff --git a/frontend/src/entity/expenses/item/api/editExpenseItem.ts b/frontend/src/entity/expenses/item/api/editExpenseItem.ts new file mode 100644 index 0000000..363e2cd --- /dev/null +++ b/frontend/src/entity/expenses/item/api/editExpenseItem.ts @@ -0,0 +1,13 @@ +import { api } from '@/shared/api/client'; + +import type { paths } from '@/shared/api/schema'; + +export type EditExpenseItemBody = paths['/expenses/list/{listId}/items/{id}']['patch']['requestBody']['content']['application/json']; +export type EditExpenseItemPath = paths['/expenses/list/{listId}/items/{id}']['patch']['parameters']['path']; +export type EditExpenseItemSuccessResponse = paths['/expenses/list/{listId}/items/{id}']['patch']['responses']['200']['content']['application/json']; + +export async function editExpenseItem(path: EditExpenseItemPath, data: EditExpenseItemBody) { + const { data: result } = await api.patch(`/expenses/list/${path.listId}/items/${path.id}`, data); + + return result; +} diff --git a/frontend/src/entity/expenses/item/hooks/useAddExpenseItem.ts b/frontend/src/entity/expenses/item/hooks/useAddExpenseItem.ts new file mode 100644 index 0000000..d2606fa --- /dev/null +++ b/frontend/src/entity/expenses/item/hooks/useAddExpenseItem.ts @@ -0,0 +1,15 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { addExpenseItem, type AddExpenseItemBody, type AddExpenseItemPath } from '../api/addExpenseItem'; + +export const useAddExpenseItem = (path: AddExpenseItemPath) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (data: AddExpenseItemBody) => addExpenseItem(path, data), + mutationKey: ['expenseList', path.id], + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['expenseList', path.id] }); + }, + }); +}; diff --git a/frontend/src/entity/expenses/item/hooks/useDeleteExpenseItem.ts b/frontend/src/entity/expenses/item/hooks/useDeleteExpenseItem.ts new file mode 100644 index 0000000..4a1a082 --- /dev/null +++ b/frontend/src/entity/expenses/item/hooks/useDeleteExpenseItem.ts @@ -0,0 +1,17 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { deleteExpenseItem } from '../api/deleteExpenseItem'; + +import type { DeleteExpenseItemPath } from '../api/deleteExpenseItem'; + +export const useDeleteExpenseItem = (path: DeleteExpenseItemPath) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async () => deleteExpenseItem(path), + mutationKey: ['expenseList', path.listId], + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['expenseList', path.listId] }); + }, + }); +}; diff --git a/frontend/src/entity/expenses/item/hooks/useEditExpenseItem.ts b/frontend/src/entity/expenses/item/hooks/useEditExpenseItem.ts new file mode 100644 index 0000000..c7e58cd --- /dev/null +++ b/frontend/src/entity/expenses/item/hooks/useEditExpenseItem.ts @@ -0,0 +1,15 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { editExpenseItem, type EditExpenseItemBody, type EditExpenseItemPath } from '../api/editExpenseItem'; + +export const useEditExpenseItem = (path: EditExpenseItemPath) => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async (data: EditExpenseItemBody) => editExpenseItem(path, data), + mutationKey: ['expenseList', path.listId], + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['expenseList', path.listId] }); + }, + }); +}; diff --git a/frontend/src/entity/expenses/item/index.ts b/frontend/src/entity/expenses/item/index.ts new file mode 100644 index 0000000..21d2e68 --- /dev/null +++ b/frontend/src/entity/expenses/item/index.ts @@ -0,0 +1,5 @@ +export { useAddExpenseItem } from './hooks/useAddExpenseItem'; +export { useDeleteExpenseItem } from './hooks/useDeleteExpenseItem'; +export { useEditExpenseItem } from './hooks/useEditExpenseItem'; + +export type { ExpenseListItem } from './types'; diff --git a/frontend/src/entity/expenses/item/types.ts b/frontend/src/entity/expenses/item/types.ts new file mode 100644 index 0000000..37537d7 --- /dev/null +++ b/frontend/src/entity/expenses/item/types.ts @@ -0,0 +1,3 @@ +import type { paths } from '@/shared/api/schema'; + +export type ExpenseListItem = NonNullable[number]; diff --git a/frontend/src/entity/expenses/list/api/addExpensesList.ts b/frontend/src/entity/expenses/list/api/addExpensesList.ts new file mode 100644 index 0000000..4c22573 --- /dev/null +++ b/frontend/src/entity/expenses/list/api/addExpensesList.ts @@ -0,0 +1,12 @@ +import { api } from '@/shared/api/client'; + +import type { paths } from '@/shared/api/schema'; + +type AddExpenseListBody = paths['/expenses/list']['post']['requestBody']['content']['application/json']; +type AddExpenseListSuccessResponse = paths['/expenses/list']['post']['responses']['200']['content']['application/json']; + +export async function addExpensesList(data: AddExpenseListBody) { + const { data: result } = await api.post('/expenses/list', data); + + return result; +} diff --git a/frontend/src/entity/expenses/list/api/deleteExpensesList.ts b/frontend/src/entity/expenses/list/api/deleteExpensesList.ts new file mode 100644 index 0000000..ad67ae0 --- /dev/null +++ b/frontend/src/entity/expenses/list/api/deleteExpensesList.ts @@ -0,0 +1,12 @@ +import { api } from '@/shared/api/client'; + +import type { paths } from '@/shared/api/schema'; + +type DeleteExpenseListPath = paths['/expenses/list/{id}']['delete']['parameters']['path']; +type DeleteExpenseSuccessResponse = paths['/expenses/list/{id}']['delete']['responses']['200']['content']['application/json']; +export async function deleteExpensesList(data: DeleteExpenseListPath) { + const { id } = data; + const { data: result } = await api.delete(`/expenses/list/${id}`); + + return result; +} diff --git a/frontend/src/entity/expenses/list/api/editExpensesList.ts b/frontend/src/entity/expenses/list/api/editExpensesList.ts new file mode 100644 index 0000000..1004f33 --- /dev/null +++ b/frontend/src/entity/expenses/list/api/editExpensesList.ts @@ -0,0 +1,17 @@ +import { api } from '@/shared/api/client'; + +import type { paths } from '@/shared/api/schema'; + +type EditExpenseListPath = paths['/expenses/list/{id}']['patch']['parameters']['path']; +type EditExpenseListBody = paths['/expenses/list/{id}']['patch']['requestBody']['content']['application/json']; +type EditExpenseListSuccessResponse = paths['/expenses/list/{id}']['patch']['responses']['200']['content']['application/json']; + +export async function editExpensesList({ data, path }: { + path: EditExpenseListPath; + data: EditExpenseListBody; +}) { + const { id } = path; + const { data: result } = await api.patch(`/expenses/list/${id}`, data); + + return result; +} diff --git a/frontend/src/entity/expenses/list/api/getExpenseList.ts b/frontend/src/entity/expenses/list/api/getExpenseList.ts new file mode 100644 index 0000000..98a4a38 --- /dev/null +++ b/frontend/src/entity/expenses/list/api/getExpenseList.ts @@ -0,0 +1,12 @@ +import { api } from '@/shared/api/client'; + +import type { paths } from '@/shared/api/schema'; + +type ExpenseListPath = paths['/expenses/list/{id}']['get']['parameters']['path']; +type ExpenseListResponse = paths['/expenses/list/{id}']['get']['responses']['200']['content']['application/json']; + +export async function getExpenseList(path: ExpenseListPath) { + const { data: result } = await api.get(`/expenses/list/${path.id}`); + + return result; +} diff --git a/frontend/src/entity/expenses/list/api/getExpenseLists.ts b/frontend/src/entity/expenses/list/api/getExpenseLists.ts new file mode 100644 index 0000000..576f874 --- /dev/null +++ b/frontend/src/entity/expenses/list/api/getExpenseLists.ts @@ -0,0 +1,11 @@ +import { api } from '@/shared/api/client'; + +import type { paths } from '@/shared/api/schema'; + +type ExpensesListResponse = paths['/expenses/list']['get']['responses']['200']['content']['application/json']; + +export async function getExpenseLists() { + const { data: result } = await api.get('/expenses/list'); + + return result; +} diff --git a/frontend/src/entity/expenses/list/hooks/useAddExpensesList.ts b/frontend/src/entity/expenses/list/hooks/useAddExpensesList.ts new file mode 100644 index 0000000..5e60b97 --- /dev/null +++ b/frontend/src/entity/expenses/list/hooks/useAddExpensesList.ts @@ -0,0 +1,15 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { addExpensesList } from '../api/addExpensesList'; + +export const useAddExpensesList = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: addExpensesList, + mutationKey: ['expensesList'], + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['expensesList'] }); + }, + }); +}; diff --git a/frontend/src/entity/expenses/list/hooks/useDeleteExpensesList.ts b/frontend/src/entity/expenses/list/hooks/useDeleteExpensesList.ts new file mode 100644 index 0000000..ca031f4 --- /dev/null +++ b/frontend/src/entity/expenses/list/hooks/useDeleteExpensesList.ts @@ -0,0 +1,15 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { deleteExpensesList } from '../api/deleteExpensesList'; + +export const useDeleteExpensesList = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: deleteExpensesList, + mutationKey: ['expensesList'], + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['expensesList'] }); + }, + }); +}; diff --git a/frontend/src/entity/expenses/list/hooks/useEditExpensesList.ts b/frontend/src/entity/expenses/list/hooks/useEditExpensesList.ts new file mode 100644 index 0000000..cdfefe6 --- /dev/null +++ b/frontend/src/entity/expenses/list/hooks/useEditExpensesList.ts @@ -0,0 +1,15 @@ +import { useMutation, useQueryClient } from '@tanstack/react-query'; + +import { editExpensesList } from '../api/editExpensesList'; + +export const useEditExpensesList = () => { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: editExpensesList, + mutationKey: ['expensesList'], + onSuccess: async () => { + await queryClient.invalidateQueries({ queryKey: ['expensesList'] }); + }, + }); +}; diff --git a/frontend/src/entity/expenses/list/hooks/useExpenseList.ts b/frontend/src/entity/expenses/list/hooks/useExpenseList.ts new file mode 100644 index 0000000..fff25ee --- /dev/null +++ b/frontend/src/entity/expenses/list/hooks/useExpenseList.ts @@ -0,0 +1,10 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getExpenseList } from '../api/getExpenseList'; + +import type { ExpenseList } from '../types'; + +export const useExpenseList = (id: ExpenseList['id']) => useQuery({ + queryFn: async () => getExpenseList({ id }), + queryKey: ['expenseList', id], +}); diff --git a/frontend/src/entity/expenses/list/hooks/useExpensesList.ts b/frontend/src/entity/expenses/list/hooks/useExpensesList.ts new file mode 100644 index 0000000..de6fbbb --- /dev/null +++ b/frontend/src/entity/expenses/list/hooks/useExpensesList.ts @@ -0,0 +1,8 @@ +import { useQuery } from '@tanstack/react-query'; + +import { getExpenseLists } from '../api/getExpenseLists'; + +export const useExpensesList = () => useQuery({ + queryFn: getExpenseLists, + queryKey: ['expensesList'], +}); diff --git a/frontend/src/entity/expenses/list/index.ts b/frontend/src/entity/expenses/list/index.ts new file mode 100644 index 0000000..31cedc5 --- /dev/null +++ b/frontend/src/entity/expenses/list/index.ts @@ -0,0 +1,7 @@ +export type { ExpenseList } from './types'; +export { useDeleteExpensesList } from './hooks/useDeleteExpensesList'; +export { useAddExpensesList } from './hooks/useAddExpensesList'; +export { useExpensesList } from './hooks/useExpensesList'; +export { useEditExpensesList } from './hooks/useEditExpensesList'; + +export { useExpenseList } from './hooks/useExpenseList'; diff --git a/frontend/src/entity/expenses/list/types.ts b/frontend/src/entity/expenses/list/types.ts new file mode 100644 index 0000000..44c287b --- /dev/null +++ b/frontend/src/entity/expenses/list/types.ts @@ -0,0 +1,3 @@ +import type { paths } from '@/shared/api/schema'; + +export type ExpenseList = paths['/expenses/list/{id}']['get']['responses']['200']['content']['application/json']; diff --git a/frontend/src/features/expenses/items/AddExpenseItemButton/hooks/useAddExpenseItemButton.ts b/frontend/src/features/expenses/items/AddExpenseItemButton/hooks/useAddExpenseItemButton.ts new file mode 100644 index 0000000..e29c7be --- /dev/null +++ b/frontend/src/features/expenses/items/AddExpenseItemButton/hooks/useAddExpenseItemButton.ts @@ -0,0 +1,49 @@ +import { type FormEventHandler, useState } from 'react'; + +import { useAddExpenseItem } from '@/entity/expenses/item'; + +import type { ExpenseList } from '@/entity/expenses/list'; + +type UseAddExpenseItemButtonArgs = { + listId: ExpenseList['id']; +}; +export const useAddExpenseItemButton = (args: UseAddExpenseItemButtonArgs) => { + const { listId } = args; + + const [isOpen, setIsOpen] = useState(false); + const { mutateAsync: addExpenseItemMutation, isPending } = useAddExpenseItem({ id: listId }); + const onCloseModalHandler = () => setIsOpen(false); + + const onClickHandler = () => setIsOpen(true); + + const onFormSubmitHandler: FormEventHandler = async (event) => { + event.preventDefault(); + + const formData = new FormData(event.currentTarget); + const title = formData.get('title'); + const date = formData.get('date'); + const amount = formData.get('amount'); + const description = formData.get('description'); + const count = formData.get('count'); + + if (typeof title === 'string' && typeof date === 'string' && typeof amount === 'string' && typeof description === 'string' && typeof count === 'string') { + await addExpenseItemMutation({ + title, + date: new Date(date).toISOString(), + amount: Number(amount), + description, + currency: 'RUB', + count: Number(count), + }); + setIsOpen(false); + } + }; + + return { + isOpen, + onFormSubmitHandler, + onCloseModalHandler, + isPending, + onClickHandler, + }; +}; diff --git a/frontend/src/features/expenses/items/AddExpenseItemButton/index.ts b/frontend/src/features/expenses/items/AddExpenseItemButton/index.ts new file mode 100644 index 0000000..0153779 --- /dev/null +++ b/frontend/src/features/expenses/items/AddExpenseItemButton/index.ts @@ -0,0 +1 @@ +export { default as AddExpenseItemButton } from './ui/AddExpenseItemButton.ui'; diff --git a/frontend/src/features/expenses/items/AddExpenseItemButton/ui/AddExpenseItemButton.module.css b/frontend/src/features/expenses/items/AddExpenseItemButton/ui/AddExpenseItemButton.module.css new file mode 100644 index 0000000..a97ffec --- /dev/null +++ b/frontend/src/features/expenses/items/AddExpenseItemButton/ui/AddExpenseItemButton.module.css @@ -0,0 +1,5 @@ +.form { + display: flex; + flex-direction: column; + gap: 10px; +} diff --git a/frontend/src/features/expenses/items/AddExpenseItemButton/ui/AddExpenseItemButton.ui.tsx b/frontend/src/features/expenses/items/AddExpenseItemButton/ui/AddExpenseItemButton.ui.tsx new file mode 100644 index 0000000..10fd29e --- /dev/null +++ b/frontend/src/features/expenses/items/AddExpenseItemButton/ui/AddExpenseItemButton.ui.tsx @@ -0,0 +1,80 @@ +import { Input } from '@/shared/ui/Input'; +import { Modal } from '@/shared/ui/Modal'; + +import { useAddExpenseItemButton } from '../hooks/useAddExpenseItemButton'; + +import classes from './AddExpenseItemButton.module.css'; + +import type { ExpenseList } from '@/entity/expenses/list'; +import type { FC } from 'react'; + +type AddExpenseItemButtonProps = { + listId: ExpenseList['id']; +}; + +const AddExpenseItemButton: FC = (props) => { + const { listId } = props; + + const { + isOpen, + isPending, + onClickHandler, + onCloseModalHandler, + onFormSubmitHandler, + } = useAddExpenseItemButton({ listId }); + + return ( + <> + + + +
+

Добавить список расходов

+ + + + + + + + + + + + +
+
+ + + ); +}; + +export default AddExpenseItemButton; diff --git a/frontend/src/features/expenses/items/DeleteExpenseItemButton/hooks/useDeleteExpenseItemButton.ts b/frontend/src/features/expenses/items/DeleteExpenseItemButton/hooks/useDeleteExpenseItemButton.ts new file mode 100644 index 0000000..205551e --- /dev/null +++ b/frontend/src/features/expenses/items/DeleteExpenseItemButton/hooks/useDeleteExpenseItemButton.ts @@ -0,0 +1,35 @@ +import { type FormEventHandler, useState } from 'react'; + +import { type ExpenseListItem, useDeleteExpenseItem } from '@/entity/expenses/item'; + +type UseDeleteExpenseItemButtonArgs = { + item: ExpenseListItem; +}; + +export const useDeleteExpenseItemButton = (args: UseDeleteExpenseItemButtonArgs) => { + const { item } = args; + const [isOpen, setIsOpen] = useState(false); + + const onCloseModalHandler = () => setIsOpen(false); + const onClickHandler = () => setIsOpen(true); + + const { mutateAsync: deleteExpenseItemMutation, isPending } = useDeleteExpenseItem({ + listId: item.expenseListId, + id: item.id, + }); + + const onFormSubmitHandler: FormEventHandler = async (event) => { + event.preventDefault(); + + await deleteExpenseItemMutation(); + setIsOpen(false); + }; + + return { + isOpen, + onCloseModalHandler, + onClickHandler, + isPending, + onFormSubmitHandler, + }; +}; diff --git a/frontend/src/features/expenses/items/DeleteExpenseItemButton/index.ts b/frontend/src/features/expenses/items/DeleteExpenseItemButton/index.ts new file mode 100644 index 0000000..529babe --- /dev/null +++ b/frontend/src/features/expenses/items/DeleteExpenseItemButton/index.ts @@ -0,0 +1 @@ +export { default as DeleteExpenseItemButton } from './ui/DeleteExpenseItemButton.ui'; diff --git a/frontend/src/features/expenses/items/DeleteExpenseItemButton/ui/DeleteExpenseItemButton.ui.tsx b/frontend/src/features/expenses/items/DeleteExpenseItemButton/ui/DeleteExpenseItemButton.ui.tsx new file mode 100644 index 0000000..f01da98 --- /dev/null +++ b/frontend/src/features/expenses/items/DeleteExpenseItemButton/ui/DeleteExpenseItemButton.ui.tsx @@ -0,0 +1,38 @@ +import { Modal } from '@/shared/ui/Modal'; + +import { useDeleteExpenseItemButton } from '../hooks/useDeleteExpenseItemButton'; + +import type { ExpenseListItem } from '@/entity/expenses/item'; +import type { FC } from 'react'; + +type DeleteExpenseItemButtonProps = { + item: ExpenseListItem; +}; +const DeleteExpenseItemButton: FC = (props) => { + const { item } = props; + const { + isOpen, + onCloseModalHandler, + onClickHandler, + isPending, + onFormSubmitHandler, + } = useDeleteExpenseItemButton({ item }); + + return ( + <> + + + +
+

Удалить элемент ?

+ + +
+
+ + ); +}; + +export default DeleteExpenseItemButton; diff --git a/frontend/src/features/expenses/items/EditExpenseItemButton/hooks/useEditExpenseItemButton.ts b/frontend/src/features/expenses/items/EditExpenseItemButton/hooks/useEditExpenseItemButton.ts new file mode 100644 index 0000000..438a8b5 --- /dev/null +++ b/frontend/src/features/expenses/items/EditExpenseItemButton/hooks/useEditExpenseItemButton.ts @@ -0,0 +1,50 @@ +import { type FormEventHandler, useState } from 'react'; + +import { type ExpenseListItem, useEditExpenseItem } from '@/entity/expenses/item'; + +type UseAddExpenseItemButtonArgs = { + item: ExpenseListItem; +}; +export const useAddExpenseItemButton = (args: UseAddExpenseItemButtonArgs) => { + const { item } = args; + + const [isOpen, setIsOpen] = useState(false); + const { mutateAsync: editExpenseItemMutation, isPending } = useEditExpenseItem({ + id: item.id, + listId: item.expenseListId, + }); + const onCloseModalHandler = () => setIsOpen(false); + + const onClickHandler = () => setIsOpen(true); + + const onFormSubmitHandler: FormEventHandler = async (event) => { + event.preventDefault(); + + const formData = new FormData(event.currentTarget); + const title = formData.get('title'); + const date = formData.get('date'); + const amount = formData.get('amount'); + const description = formData.get('description'); + const count = formData.get('count'); + + if (typeof title === 'string' && typeof date === 'string' && typeof amount === 'string' && typeof description === 'string' && typeof count === 'string') { + await editExpenseItemMutation({ + title, + date: new Date(date).toISOString(), + amount: Number(amount), + description, + currency: 'RUB', + count: Number(count), + }); + setIsOpen(false); + } + }; + + return { + isOpen, + onFormSubmitHandler, + onCloseModalHandler, + isPending, + onClickHandler, + }; +}; diff --git a/frontend/src/features/expenses/items/EditExpenseItemButton/index.ts b/frontend/src/features/expenses/items/EditExpenseItemButton/index.ts new file mode 100644 index 0000000..df3bda4 --- /dev/null +++ b/frontend/src/features/expenses/items/EditExpenseItemButton/index.ts @@ -0,0 +1 @@ +export { default as EditExpenseItemButton } from './ui/EditExpenseItemButton.ui'; diff --git a/frontend/src/features/expenses/items/EditExpenseItemButton/ui/EditExpenseItemButton.module.css b/frontend/src/features/expenses/items/EditExpenseItemButton/ui/EditExpenseItemButton.module.css new file mode 100644 index 0000000..a97ffec --- /dev/null +++ b/frontend/src/features/expenses/items/EditExpenseItemButton/ui/EditExpenseItemButton.module.css @@ -0,0 +1,5 @@ +.form { + display: flex; + flex-direction: column; + gap: 10px; +} diff --git a/frontend/src/features/expenses/items/EditExpenseItemButton/ui/EditExpenseItemButton.ui.tsx b/frontend/src/features/expenses/items/EditExpenseItemButton/ui/EditExpenseItemButton.ui.tsx new file mode 100644 index 0000000..d9788ad --- /dev/null +++ b/frontend/src/features/expenses/items/EditExpenseItemButton/ui/EditExpenseItemButton.ui.tsx @@ -0,0 +1,81 @@ +import { Input } from '@/shared/ui/Input'; +import { Modal } from '@/shared/ui/Modal'; + +import { useAddExpenseItemButton } from '../hooks/useEditExpenseItemButton'; + +import classes from './EditExpenseItemButton.module.css'; + +import type { ExpenseListItem } from '@/entity/expenses/item'; +import type { FC } from 'react'; + +type EditExpenseItemButtonProps = { + item: ExpenseListItem; +}; + +const EditExpenseItemButton: FC = (props) => { + const { item } = props; + + const { + isOpen, + onCloseModalHandler, + onClickHandler, + isPending, + onFormSubmitHandler, + } = useAddExpenseItemButton({ item }); + + return ( + <> + + + +
+

Добавить список расходов

+ + + + + + + + + + + + +
+
+ + ); +}; + +export default EditExpenseItemButton; diff --git a/frontend/src/features/expenses/list/AddExpensesListButton/hooks/useAddExpensesListButton.ts b/frontend/src/features/expenses/list/AddExpensesListButton/hooks/useAddExpensesListButton.ts new file mode 100644 index 0000000..30845ea --- /dev/null +++ b/frontend/src/features/expenses/list/AddExpensesListButton/hooks/useAddExpensesListButton.ts @@ -0,0 +1,43 @@ +import { type FormEventHandler, type MouseEventHandler, useState } from 'react'; + +import { useAddExpensesList } from '@/entity/expenses/list'; + +type UseAddExpensesListButtonArgs = { + onClick?: MouseEventHandler; +}; + +export const useAddExpensesListButton = (args: UseAddExpensesListButtonArgs) => { + const { onClick } = args; + const [isOpen, setIsOpen] = useState(false); + + const { mutateAsync: addExpensesListMutation, isPending } = useAddExpensesList(); + + const onCloseModalHandler = () => { + setIsOpen(false); + }; + + const onClickHandler: MouseEventHandler = (event) => { + onClick?.(event); + setIsOpen(true); + }; + + const onFormSubmitHandler: FormEventHandler = async (event) => { + event.preventDefault(); + + const formData = new FormData(event.currentTarget); + const title = formData.get('title'); + + if (typeof title === 'string') { + await addExpensesListMutation({ title }); + setIsOpen(false); + } + }; + + return { + isOpen, + isPending, + onCloseModalHandler, + onClickHandler, + onFormSubmitHandler, + }; +}; diff --git a/frontend/src/features/expenses/list/AddExpensesListButton/index.ts b/frontend/src/features/expenses/list/AddExpensesListButton/index.ts new file mode 100644 index 0000000..814a8e1 --- /dev/null +++ b/frontend/src/features/expenses/list/AddExpensesListButton/index.ts @@ -0,0 +1 @@ +export { default as AddExpensesListButton } from './ui/AddExpensesListButton.ui'; diff --git a/frontend/src/features/expenses/list/AddExpensesListButton/ui/AddExpensesListButton.ui.tsx b/frontend/src/features/expenses/list/AddExpensesListButton/ui/AddExpensesListButton.ui.tsx new file mode 100644 index 0000000..59753d6 --- /dev/null +++ b/frontend/src/features/expenses/list/AddExpensesListButton/ui/AddExpensesListButton.ui.tsx @@ -0,0 +1,42 @@ +import { Input } from '@/shared/ui/Input'; +import { Modal } from '@/shared/ui/Modal'; + +import { useAddExpensesListButton } from '../hooks/useAddExpensesListButton'; + +import type { ButtonHTMLAttributes, FC, Ref } from 'react'; + +type AddExpensesListButtonProps = ButtonHTMLAttributes & { + ref?: Ref; +}; + +const AddExpensesListButton: FC = (props) => { + const { onClick, ...restProps } = props; + + const { + isOpen, + isPending, + onCloseModalHandler, + onClickHandler, + onFormSubmitHandler, + } = useAddExpensesListButton({ onClick }); + + return ( + <> + + + +
+

Добавить список расходов

+ + + + +
+
+ + ); +}; + +export default AddExpensesListButton; diff --git a/frontend/src/features/expenses/list/DeleteExpensesListButton/hooks/useDeleteExpensesListButton.ts b/frontend/src/features/expenses/list/DeleteExpensesListButton/hooks/useDeleteExpensesListButton.ts new file mode 100644 index 0000000..d54f922 --- /dev/null +++ b/frontend/src/features/expenses/list/DeleteExpensesListButton/hooks/useDeleteExpensesListButton.ts @@ -0,0 +1,36 @@ +import { type FormEventHandler, useState } from 'react'; + +import { type ExpenseList, useDeleteExpensesList } from '@/entity/expenses/list'; + +type UseDeleteExpensesListButtonArgs = { + id: ExpenseList['id']; +}; + +export const useDeleteExpensesListButton = (args: UseDeleteExpensesListButtonArgs) => { + const { id } = args; + const [isOpen, setIsOpen] = useState(false); + + const { mutateAsync: deleteExpensesListAsync, isPending } = useDeleteExpensesList(); + + const onClickHandler = () => { + setIsOpen(true); + }; + + const onCloseModalHandler = () => { + setIsOpen(false); + }; + + const onFormSubmitHandler: FormEventHandler = async (event) => { + event.preventDefault(); + await deleteExpensesListAsync({ id }); + setIsOpen(false); + }; + + return { + isOpen, + isPending, + onClickHandler, + onCloseModalHandler, + onFormSubmitHandler, + }; +}; diff --git a/frontend/src/features/expenses/list/DeleteExpensesListButton/index.ts b/frontend/src/features/expenses/list/DeleteExpensesListButton/index.ts new file mode 100644 index 0000000..9d3253f --- /dev/null +++ b/frontend/src/features/expenses/list/DeleteExpensesListButton/index.ts @@ -0,0 +1 @@ +export { default as DeleteExpensesListButton } from './ui/DeleteExpensesListButton.ui'; diff --git a/frontend/src/features/expenses/list/DeleteExpensesListButton/ui/DeleteExpensesListButton.ui.tsx b/frontend/src/features/expenses/list/DeleteExpensesListButton/ui/DeleteExpensesListButton.ui.tsx new file mode 100644 index 0000000..3c72b02 --- /dev/null +++ b/frontend/src/features/expenses/list/DeleteExpensesListButton/ui/DeleteExpensesListButton.ui.tsx @@ -0,0 +1,51 @@ +import { Modal } from '@/shared/ui/Modal'; + +import { useDeleteExpensesListButton } from '../hooks/useDeleteExpensesListButton'; + +import type { ExpenseList } from '@/entity/expenses/list'; +import type { ButtonHTMLAttributes, Ref } from 'react'; + +type DeleteExpensesListButtonProps = ButtonHTMLAttributes & { + list: ExpenseList; + ref?: Ref; +}; + +const DeleteExpensesListButton = (props: DeleteExpensesListButtonProps) => { + const { list, ...restProps } = props; + const { id, title } = list; + const { + isOpen, + isPending, + onCloseModalHandler, + onClickHandler, + onFormSubmitHandler, + } = useDeleteExpensesListButton({ id }); + + return ( + <> + + + +
+

Удалить список расходов

+ +

+ Вы действительно хотите удалить список расходов + {' '} + + {`"${title}"`} + + {' '} + ? +

+ + +
+
+ + ); +}; + +export default DeleteExpensesListButton; diff --git a/frontend/src/features/expenses/list/EditExpensesListButton/hooks/useEditExpensesListButton.ts b/frontend/src/features/expenses/list/EditExpensesListButton/hooks/useEditExpensesListButton.ts new file mode 100644 index 0000000..1cdcf53 --- /dev/null +++ b/frontend/src/features/expenses/list/EditExpensesListButton/hooks/useEditExpensesListButton.ts @@ -0,0 +1,44 @@ +import { type FormEventHandler, useState } from 'react'; + +import { type ExpenseList, useEditExpensesList } from '@/entity/expenses/list'; + +type UseEditExpensesListButtonArgs = { + id: ExpenseList['id']; +}; + +export const useEditExpensesListButton = (args: UseEditExpensesListButtonArgs) => { + const { id } = args; + const [isOpen, setIsOpen] = useState(false); + + const { mutateAsync: editExpensesListAsync, isPending } = useEditExpensesList(); + + const onClickHandler = () => { + setIsOpen(true); + }; + + const onCloseModalHandler = () => { + setIsOpen(false); + }; + + const onFormSubmitHandler: FormEventHandler = async (event) => { + event.preventDefault(); + const formData = new FormData(event.currentTarget); + const title = formData.get('title'); + + if (typeof title === 'string') { + await editExpensesListAsync({ + path: { id }, + data: { title }, + }); + setIsOpen(false); + } + }; + + return { + isOpen, + isPending, + onClickHandler, + onCloseModalHandler, + onFormSubmitHandler, + }; +}; diff --git a/frontend/src/features/expenses/list/EditExpensesListButton/index.ts b/frontend/src/features/expenses/list/EditExpensesListButton/index.ts new file mode 100644 index 0000000..0408ef0 --- /dev/null +++ b/frontend/src/features/expenses/list/EditExpensesListButton/index.ts @@ -0,0 +1 @@ +export { default as EditExpensesListButton } from './ui/EditExpensesListButton.ui'; diff --git a/frontend/src/features/expenses/list/EditExpensesListButton/ui/EditExpensesListButton.ui.tsx b/frontend/src/features/expenses/list/EditExpensesListButton/ui/EditExpensesListButton.ui.tsx new file mode 100644 index 0000000..2204e4a --- /dev/null +++ b/frontend/src/features/expenses/list/EditExpensesListButton/ui/EditExpensesListButton.ui.tsx @@ -0,0 +1,49 @@ +import { Input } from '@/shared/ui/Input'; +import { Modal } from '@/shared/ui/Modal'; + +import { useEditExpensesListButton } from '../hooks/useEditExpensesListButton'; + +import type { ExpenseList } from '@/entity/expenses/list'; +import type { ButtonHTMLAttributes, FC, Ref } from 'react'; + +type EditExpensesListButtonProps = ButtonHTMLAttributes & { + list: ExpenseList; + ref?: Ref; +}; + +const EditExpensesListButton: FC = (props) => { + const { list, ...restProps } = props; + + const { + isOpen, + isPending, + onCloseModalHandler, + onClickHandler, + onFormSubmitHandler, + } = useEditExpensesListButton({ id: list.id }); + + return ( + <> + + + +
+

Редактировать список расходов

+ + + + +
+
+ + ); +}; + +export default EditExpensesListButton; diff --git a/frontend/src/pages/Expense/index.ts b/frontend/src/pages/Expense/index.ts new file mode 100644 index 0000000..3873297 --- /dev/null +++ b/frontend/src/pages/Expense/index.ts @@ -0,0 +1 @@ +export { default as ExpensePage } from './ui/ExpensePage.ui'; diff --git a/frontend/src/pages/Expense/ui/ExpensePage.ui.tsx b/frontend/src/pages/Expense/ui/ExpensePage.ui.tsx new file mode 100644 index 0000000..183a143 --- /dev/null +++ b/frontend/src/pages/Expense/ui/ExpensePage.ui.tsx @@ -0,0 +1,45 @@ +import { useParams } from 'react-router'; + +import { useExpenseList } from '@/entity/expenses/list'; +import { AddExpenseItemButton } from '@/features/expenses/items/AddExpenseItemButton'; +import { Spinner } from '@/shared/ui/Spinner'; +import { AppLayout } from '@/widgets/AppLayout'; +import { ExpenseItemsTable } from '@/widgets/expenses/ExpenseItemsTable'; + +import type { ExpensePageParams } from '@/shared/router'; + +const ExpensePage = () => { + // eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion + const { id } = useParams() as ExpensePageParams; + + const { data: expenseList, isPending } = useExpenseList(id); + + const renderContent = () => { + if (isPending) { + return ; + } + + if (expenseList?.items?.length && expenseList.items.length > 0) { + return ( + + ); + } + + return

Списков расходов нет

; + }; + + return ( + +
+

Списки расходов

+ + + + {renderContent()} + +
+
+ ); +}; + +export default ExpensePage; diff --git a/frontend/src/pages/Expenses/index.ts b/frontend/src/pages/Expenses/index.ts new file mode 100644 index 0000000..486ac9d --- /dev/null +++ b/frontend/src/pages/Expenses/index.ts @@ -0,0 +1 @@ +export { default as ExpensesPage } from './ui/ExpensesPage.ui'; diff --git a/frontend/src/pages/Expenses/ui/ExpensesPage.ui.tsx b/frontend/src/pages/Expenses/ui/ExpensesPage.ui.tsx new file mode 100644 index 0000000..b930ae6 --- /dev/null +++ b/frontend/src/pages/Expenses/ui/ExpensesPage.ui.tsx @@ -0,0 +1,38 @@ +import { useExpensesList } from '@/entity/expenses/list'; +import { AddExpensesListButton } from '@/features/expenses/list/AddExpensesListButton'; +import { Spinner } from '@/shared/ui/Spinner'; +import { AppLayout } from '@/widgets/AppLayout'; +import { ExpensesTable } from '@/widgets/expenses/ExpensesTable'; + +const ExpensesPage = () => { + const { data: expensesList, isPending } = useExpensesList(); + + const renderContent = () => { + if (isPending) { + return ; + } + + if (expensesList && expensesList.length > 0) { + return ; + } + + return

Списков расходов нет

; + }; + + return ( + +
+

Списки расходов

+ +
+ +
+ + {renderContent()} + +
+
+ ); +}; + +export default ExpensesPage; diff --git a/frontend/src/pages/Home/ui/HomePage.ui.tsx b/frontend/src/pages/Home/ui/HomePage.ui.tsx index 397cf1a..5c2abfe 100644 --- a/frontend/src/pages/Home/ui/HomePage.ui.tsx +++ b/frontend/src/pages/Home/ui/HomePage.ui.tsx @@ -1,51 +1,11 @@ -import { useContext, useState } from 'react'; +import { AppLayout } from '@/widgets/AppLayout'; -import { api } from '@/shared/api/client'; -import { AuthContext } from '@/shared/context/AuthContext'; - -import type { paths } from '@/shared/api/schema'; - -type Response = paths['/auth/me']['get']['responses']['200']['content']['application/json']; - -const fetchProfile = async () => { - const { data } = await api.get('/auth/me'); - - return data; -}; - -const HomePage = () => { - const [info, setInfo] = useState(); - const { accessToken } = useContext(AuthContext); - - const onClick = () => { - if (!accessToken) { - return; - } - - fetchProfile() - .then((data) => { - // eslint-disable-next-line no-console - console.log('DATA', data); - - setInfo(data); - }); - }; - - return ( +const HomePage = () => ( +
- Home page only for auth user - - -
- - - ID: - {info?.id} - +

Home Page

- ); -}; +
+); export default HomePage; diff --git a/frontend/src/shared/api/client.ts b/frontend/src/shared/api/client.ts index 3468243..c513ef9 100644 --- a/frontend/src/shared/api/client.ts +++ b/frontend/src/shared/api/client.ts @@ -1,5 +1,7 @@ import axios from 'axios'; +import { PAGES } from '@/shared/router'; + import type { paths } from './schema'; const BASE_URL = 'http://localhost:3000'; @@ -32,11 +34,16 @@ api.interceptors.response.use((response) => response, async (error) => { if (error.response?.status === 401 && !originalRequest._retry) { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access originalRequest._retry = true; - const { data: result } = await axios.post(`${BASE_URL}/auth/refresh`, null, { - withCredentials: true, - }); - const { accessToken } = result; - localStorage.setItem('accessToken', accessToken); + try { + const { data: result } = await axios.post(`${BASE_URL}/auth/refresh`, null, { + withCredentials: true, + }); + const { accessToken } = result; + localStorage.setItem('accessToken', accessToken); + } catch { + localStorage.removeItem('accessToken'); + location.replace(PAGES.Login); + } // eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access return api(error.config); diff --git a/frontend/src/shared/api/schema.ts b/frontend/src/shared/api/schema.ts index 508effe..bc16bb6 100644 --- a/frontend/src/shared/api/schema.ts +++ b/frontend/src/shared/api/schema.ts @@ -75,7 +75,7 @@ export interface paths { * Получить профиль * @description Получить данные о своем профиле */ - get: operations["AuthController_me"]; + get: operations["AuthController_profile"]; put?: never; post?: never; delete?: never; @@ -101,6 +101,106 @@ export interface paths { patch?: never; trace?: never; }; + "/expenses/list": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Найти все листы расходов + * @description Найти все листы расходов + */ + get: operations["ExpensesController_findAllLists"]; + put?: never; + /** + * Создать лист расходов + * @description Создать лист расходов + */ + post: operations["ExpensesController_createList"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/expenses/list/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Найти лист расходов + * @description Найти лист расходов + */ + get: operations["ExpensesController_findList"]; + put?: never; + post?: never; + /** + * Удалить лист расходов + * @description Удалить лист расходов + */ + delete: operations["ExpensesController_deleteList"]; + options?: never; + head?: never; + /** + * Редактировать лист расходов + * @description Редактировать лист расходов + */ + patch: operations["ExpensesController_editList"]; + trace?: never; + }; + "/expenses/list/{id}/items": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** + * Создать элемент расхода + * @description Создать элемент расхода + */ + post: operations["ExpensesController_createListItem"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/expenses/list/{listId}/items/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** + * Найти элемент расхода + * @description Найти элемент расхода + */ + get: operations["ExpensesController_findListItem"]; + put?: never; + post?: never; + /** + * Удалить элемент расхода + * @description Удалить элемент расхода + */ + delete: operations["ExpensesController_deleteListItem"]; + options?: never; + head?: never; + /** + * Редактировать элемент расхода + * @description Редактировать элемент расхода + */ + patch: operations["ExpensesController_editListItem"]; + trace?: never; + }; } export type webhooks = Record; export interface components { @@ -156,11 +256,18 @@ export interface components { /** @description Access токен */ accessToken: string; }; - IncorrectLoginDto: { + LoginValidationDto: { /** @example 400 */ statusCode: number; - /** @example Неверный логин или пароль */ - message: Record; + /** @example [ + * "Email должен быть строкой.", + * "Некорректный формат email.", + * "Email обязателен для заполнения.", + * "Пароль должен быть строкой.", + * "Пароль обязателен для заполнения.", + * "Пароль должен содержать минимум 6 символов." + * ] */ + message: string[]; /** @example Bad Request */ error: string; }; @@ -168,6 +275,161 @@ export interface components { /** @description Индетификатор аккаунта */ id: string; }; + CreateExpenseListDto: { + /** + * @description Название листа + * @example Ежедневные покупки + */ + title: string; + }; + ExpenseListItemDto: { + /** @description ID */ + id: string; + /** @description ID списка */ + expenseListId: string; + /** + * @description Заголовок расхода + * @example Кофе + */ + title: string; + /** + * @description Описание + * @example Перед работой + */ + description: string | null; + /** + * Format: date-time + * @description Дата + * @example 2025-08-22T04:12:03.726Z + */ + date: string; + /** + * @description Сумма расхода + * @example 175.5 + */ + amount: number; + /** + * @description Валюта расхода + * @default RUB + * @example RUB + */ + currency: string | null; + /** + * @description Количество + * @default 1 + * @example 2 + */ + count: number | null; + /** + * Format: date-time + * @description Дата создания + */ + createdAt: string; + /** + * Format: date-time + * @description Дата обновления + */ + updatedAt: string; + }; + ExpenseListDto: { + /** @description ID */ + id: string; + /** @description Название листа */ + title: string; + /** @description Список расходов */ + items: components["schemas"]["ExpenseListItemDto"][] | null; + /** + * Format: date-time + * @description Дата создания + */ + createdAt: string; + /** + * Format: date-time + * @description Дата обновления + */ + updatedAt: string; + }; + EditExpenseListDto: { + /** + * @description Название листа + * @example Ежедневные покупки + */ + title: string; + }; + DeletedExpenseListDto: { + /** @description ID */ + id: string; + }; + CreateExpenseListItemDto: { + /** + * @description Заголовок расхода + * @example Кофе + */ + title: string; + /** + * @description Описание + * @example Перед работой + */ + description: string | null; + /** + * Format: date-time + * @description Дата + * @example 2025-08-22T04:12:03.726Z + */ + date: string; + /** + * @description Сумма расхода + * @example 175.5 + */ + amount: number; + /** + * @description Валюта расхода + * @default RUB + * @example RUB + */ + currency: string | null; + /** + * @description Количество + * @default 1 + * @example 2 + */ + count: number | null; + }; + EditExpenseListItemDto: { + /** + * @description Заголовок расхода + * @example Кофе + */ + title: string; + /** + * @description Описание + * @example Перед работой + */ + description: string | null; + /** + * Format: date-time + * @description Дата + * @example 2025-08-22T04:12:03.726Z + */ + date: string; + /** + * @description Сумма расхода + * @example 175.5 + */ + amount: number; + /** + * @description Валюта расхода + * @default RUB + * @example RUB + */ + currency: string | null; + /** + * @description Количество + * @default 1 + * @example 2 + */ + count: number | null; + }; }; responses: never; parameters: never; @@ -241,13 +503,13 @@ export interface operations { "application/json": components["schemas"]["SuccessLoginDto"]; }; }; - /** @description Неверный логин или пароль */ + /** @description Ошибка валидации */ 400: { headers: { [name: string]: unknown; }; content: { - "application/json": components["schemas"]["IncorrectLoginDto"]; + "application/json": components["schemas"]["LoginValidationDto"]; }; }; }; @@ -272,7 +534,7 @@ export interface operations { }; }; }; - AuthController_me: { + AuthController_profile: { parameters: { query?: never; header?: never; @@ -332,4 +594,269 @@ export interface operations { }; }; }; + ExpensesController_findAllLists: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Успешно создан лист расходов */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ExpenseListDto"][]; + }; + }; + }; + }; + ExpensesController_createList: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateExpenseListDto"]; + }; + }; + responses: { + /** @description Успешно создан лист расходов */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ExpenseListDto"]; + }; + }; + }; + }; + ExpensesController_findList: { + 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"]["ExpenseListDto"]; + }; + }; + /** @description Лист расходов не найден */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + ExpensesController_deleteList: { + 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"]["DeletedExpenseListDto"]; + }; + }; + /** @description Лист расходов не найден */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + ExpensesController_editList: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID листа расходов */ + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["EditExpenseListDto"]; + }; + }; + responses: { + /** @description Успешно изменен лист расходов */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ExpenseListDto"]; + }; + }; + /** @description Лист расходов не найден */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + ExpensesController_createListItem: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID листа расходов */ + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateExpenseListItemDto"]; + }; + }; + responses: { + /** @description Успешно создан элемент расхода */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ExpenseListItemDto"]; + }; + }; + }; + }; + ExpensesController_findListItem: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID листа расходов */ + listId: string; + /** @description ID элемента */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Успешно найден элемент расхода */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ExpenseListItemDto"]; + }; + }; + /** @description Позиция расходов не найдена */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + ExpensesController_deleteListItem: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID листа расходов */ + listId: string; + /** @description ID элемента */ + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description Успешно удален элемент расхода */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ExpenseListItemDto"]; + }; + }; + /** @description Позиция расходов не найдена */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + ExpensesController_editListItem: { + parameters: { + query?: never; + header?: never; + path: { + /** @description ID листа расходов */ + listId: string; + /** @description ID элемента */ + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["EditExpenseListItemDto"]; + }; + }; + responses: { + /** @description Успешно изменен элемент расхода */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ExpenseListItemDto"]; + }; + }; + /** @description Позиция расходов не найдена */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; } diff --git a/frontend/src/shared/router/index.ts b/frontend/src/shared/router/index.ts index c814d5b..1fa5f4e 100644 --- a/frontend/src/shared/router/index.ts +++ b/frontend/src/shared/router/index.ts @@ -1 +1,2 @@ export { PAGES } from './types'; +export type { ExpensePageParams } from './types'; diff --git a/frontend/src/shared/router/types.ts b/frontend/src/shared/router/types.ts index 51463c9..3f18cf6 100644 --- a/frontend/src/shared/router/types.ts +++ b/frontend/src/shared/router/types.ts @@ -2,4 +2,10 @@ export enum PAGES { Home = '/', Login = '/login', Register = '/register', + Expenses = '/expenses', + Expense = '/expenses/:id', } + +export type ExpensePageParams = { + id: string; +}; diff --git a/frontend/src/shared/ui/input/index.ts b/frontend/src/shared/ui/Input/index.ts similarity index 100% rename from frontend/src/shared/ui/input/index.ts rename to frontend/src/shared/ui/Input/index.ts diff --git a/frontend/src/shared/ui/input/ui/Input.styles.module.css b/frontend/src/shared/ui/Input/ui/Input.styles.module.css similarity index 100% rename from frontend/src/shared/ui/input/ui/Input.styles.module.css rename to frontend/src/shared/ui/Input/ui/Input.styles.module.css diff --git a/frontend/src/shared/ui/input/ui/Input.ui.tsx b/frontend/src/shared/ui/Input/ui/Input.ui.tsx similarity index 100% rename from frontend/src/shared/ui/input/ui/Input.ui.tsx rename to frontend/src/shared/ui/Input/ui/Input.ui.tsx diff --git a/frontend/src/shared/ui/Modal/index.ts b/frontend/src/shared/ui/Modal/index.ts new file mode 100644 index 0000000..3277cc9 --- /dev/null +++ b/frontend/src/shared/ui/Modal/index.ts @@ -0,0 +1 @@ +export { default as Modal } from './ui/Modal.ui'; diff --git a/frontend/src/shared/ui/Modal/ui/Modal.module.css b/frontend/src/shared/ui/Modal/ui/Modal.module.css new file mode 100644 index 0000000..3f766bf --- /dev/null +++ b/frontend/src/shared/ui/Modal/ui/Modal.module.css @@ -0,0 +1,27 @@ +.wrapper { + display: flex; + width: 100%; + height: 100%; + position: fixed; + top: 0; + left: 0; + background-color: rgba(0, 0, 0, 0.5); + justify-content: center; + align-items: center; +} + +.content { + background-color: white; + z-index: 2; + border-radius: 10px; + position: relative; + padding: 30px; +} + +.closeIcon { + position: absolute; + top: 10px; + right: 10px; + cursor: pointer; + font-size: 20px; +} diff --git a/frontend/src/shared/ui/Modal/ui/Modal.ui.tsx b/frontend/src/shared/ui/Modal/ui/Modal.ui.tsx new file mode 100644 index 0000000..0648d05 --- /dev/null +++ b/frontend/src/shared/ui/Modal/ui/Modal.ui.tsx @@ -0,0 +1,29 @@ +import classes from './Modal.module.css'; + +import type { FC, ReactNode } from 'react'; + +type ModalProps = { + open: boolean; + onClose: () => void; + children: ReactNode; +}; + +const Modal: FC = (props) => { + const { open, onClose, children } = props; + + if (!open) { + return null; + } + + return ( +
+
{ e.stopPropagation(); }}> + × + + {children} +
+
+ ); +}; + +export default Modal; diff --git a/frontend/src/shared/ui/Spinner/index.ts b/frontend/src/shared/ui/Spinner/index.ts new file mode 100644 index 0000000..b8aa75d --- /dev/null +++ b/frontend/src/shared/ui/Spinner/index.ts @@ -0,0 +1 @@ +export { default as Spinner } from './ui/Spinner.ui'; diff --git a/frontend/src/shared/ui/Spinner/ui/Spinner.module.css b/frontend/src/shared/ui/Spinner/ui/Spinner.module.css new file mode 100644 index 0000000..aaf9610 --- /dev/null +++ b/frontend/src/shared/ui/Spinner/ui/Spinner.module.css @@ -0,0 +1,19 @@ +.loader { + width: 48px; + height: 48px; + border: 5px solid dimgrey; + border-bottom-color: transparent; + border-radius: 50%; + display: inline-block; + box-sizing: border-box; + animation: rotation 1s linear infinite; +} + +@keyframes rotation { + 0% { + transform: rotate(0deg); + } + 100% { + transform: rotate(360deg); + } +} diff --git a/frontend/src/shared/ui/Spinner/ui/Spinner.ui.tsx b/frontend/src/shared/ui/Spinner/ui/Spinner.ui.tsx new file mode 100644 index 0000000..5b0d890 --- /dev/null +++ b/frontend/src/shared/ui/Spinner/ui/Spinner.ui.tsx @@ -0,0 +1,7 @@ +import classes from './Spinner.module.css'; + +const Spinner = () => ( + +); + +export default Spinner; diff --git a/frontend/src/widgets/AppLayout/index.ts b/frontend/src/widgets/AppLayout/index.ts new file mode 100644 index 0000000..e76f237 --- /dev/null +++ b/frontend/src/widgets/AppLayout/index.ts @@ -0,0 +1 @@ +export { default as AppLayout } from './ui/AppLayout.ui'; diff --git a/frontend/src/widgets/AppLayout/ui/AppLayout.module.css b/frontend/src/widgets/AppLayout/ui/AppLayout.module.css new file mode 100644 index 0000000..48f92aa --- /dev/null +++ b/frontend/src/widgets/AppLayout/ui/AppLayout.module.css @@ -0,0 +1,7 @@ +.wrapper { + display: flex; +} + +.content { + flex-grow: 1; +} diff --git a/frontend/src/widgets/AppLayout/ui/AppLayout.ui.tsx b/frontend/src/widgets/AppLayout/ui/AppLayout.ui.tsx new file mode 100644 index 0000000..e971942 --- /dev/null +++ b/frontend/src/widgets/AppLayout/ui/AppLayout.ui.tsx @@ -0,0 +1,25 @@ +import { SideBar } from '../../Sidebar'; + +import classes from './AppLayout.module.css'; + +import type { FC, ReactNode } from 'react'; + +type AppLayoutProps = { + children: ReactNode; +}; + +const AppLayout: FC = (props) => { + const { children } = props; + + return ( +
+ + +
+ {children} +
+
+ ); +}; + +export default AppLayout; diff --git a/frontend/src/widgets/Sidebar/index.ts b/frontend/src/widgets/Sidebar/index.ts new file mode 100644 index 0000000..af10ba8 --- /dev/null +++ b/frontend/src/widgets/Sidebar/index.ts @@ -0,0 +1 @@ +export { default as SideBar } from './ui/SideBar.ui'; diff --git a/frontend/src/widgets/Sidebar/ui/SideBar.module.css b/frontend/src/widgets/Sidebar/ui/SideBar.module.css new file mode 100644 index 0000000..d73b8ea --- /dev/null +++ b/frontend/src/widgets/Sidebar/ui/SideBar.module.css @@ -0,0 +1,5 @@ +.wrapper { + width: 200px; + height: 100vh; + border: 1px solid dimgrey; +} diff --git a/frontend/src/widgets/Sidebar/ui/SideBar.ui.tsx b/frontend/src/widgets/Sidebar/ui/SideBar.ui.tsx new file mode 100644 index 0000000..9b10731 --- /dev/null +++ b/frontend/src/widgets/Sidebar/ui/SideBar.ui.tsx @@ -0,0 +1,25 @@ +import { NavLink } from 'react-router'; + +import { PAGES } from '@/shared/router'; + +import classes from './SideBar.module.css'; + +const SideBar = () => ( +
+
    +
  • + + Главная + +
  • + +
  • + + Расходы + +
  • +
+
+); + +export default SideBar; diff --git a/frontend/src/widgets/auth/LoginForm/ui/LoginForm.ui.tsx b/frontend/src/widgets/auth/LoginForm/ui/LoginForm.ui.tsx index b43473d..8d42fcc 100644 --- a/frontend/src/widgets/auth/LoginForm/ui/LoginForm.ui.tsx +++ b/frontend/src/widgets/auth/LoginForm/ui/LoginForm.ui.tsx @@ -1,4 +1,4 @@ -import { Input } from '@/shared/ui/input'; +import { Input } from '@/shared/ui/Input'; import { useLoginForm } from './hooks/useLoginForm'; import classes from './LoginForm.module.css'; diff --git a/frontend/src/widgets/auth/RegisterForm/ui/RegisterForm.ui.tsx b/frontend/src/widgets/auth/RegisterForm/ui/RegisterForm.ui.tsx index 78eae8a..393db35 100644 --- a/frontend/src/widgets/auth/RegisterForm/ui/RegisterForm.ui.tsx +++ b/frontend/src/widgets/auth/RegisterForm/ui/RegisterForm.ui.tsx @@ -1,4 +1,4 @@ -import { Input } from '@/shared/ui/input'; +import { Input } from '@/shared/ui/Input'; import { useRegisterForm } from './hooks/useRegisterForm'; import classes from './RegisterForm.module.css'; diff --git a/frontend/src/widgets/expenses/ExpenseItemsTable/index.ts b/frontend/src/widgets/expenses/ExpenseItemsTable/index.ts new file mode 100644 index 0000000..839e3fb --- /dev/null +++ b/frontend/src/widgets/expenses/ExpenseItemsTable/index.ts @@ -0,0 +1 @@ +export { default as ExpenseItemsTable } from './ui/ExpenseItemsTable.ui'; diff --git a/frontend/src/widgets/expenses/ExpenseItemsTable/ui/ExpenseItemsTable.ui.tsx b/frontend/src/widgets/expenses/ExpenseItemsTable/ui/ExpenseItemsTable.ui.tsx new file mode 100644 index 0000000..4bbb988 --- /dev/null +++ b/frontend/src/widgets/expenses/ExpenseItemsTable/ui/ExpenseItemsTable.ui.tsx @@ -0,0 +1,81 @@ +import { DeleteExpenseItemButton } from '@/features/expenses/items/DeleteExpenseItemButton'; +import { EditExpenseItemButton } from '@/features/expenses/items/EditExpenseItemButton'; +import classes from '@/widgets/expenses/ExpensesTable/ui/ExpensesTable.module.css'; + +import type { ExpenseList } from '@/entity/expenses/list'; +import type { FC } from 'react'; + +type ExpenseItemsTableProps = { + list: ExpenseList; +}; + +const ExpenseItemsTable: FC = (props) => { + const { list } = props; + const { items } = list; + + return ( + + + + + + + + + + + + + + + + + + + + + + + {items?.map((item) => ( + + + + + + + + + + + + + + + + + + ))} + +
ИдентификаторНазваниеОписаниеДатаСуммаВалютаКоличествоДействия
+ {item.id} + + {item.title} + + {item.description} + + {item.date} + + {item.amount} + + {item.currency} + + {item.count} + + + + +
+ ); +}; + +export default ExpenseItemsTable; diff --git a/frontend/src/widgets/expenses/ExpensesTable/index.ts b/frontend/src/widgets/expenses/ExpensesTable/index.ts new file mode 100644 index 0000000..5aae7e9 --- /dev/null +++ b/frontend/src/widgets/expenses/ExpensesTable/index.ts @@ -0,0 +1 @@ +export { default as ExpensesTable } from './ui/ExpensesTable.ui'; diff --git a/frontend/src/widgets/expenses/ExpensesTable/ui/ExpensesTable.module.css b/frontend/src/widgets/expenses/ExpensesTable/ui/ExpensesTable.module.css new file mode 100644 index 0000000..77af1d2 --- /dev/null +++ b/frontend/src/widgets/expenses/ExpensesTable/ui/ExpensesTable.module.css @@ -0,0 +1,3 @@ +.table, th, td { + border: 1px solid; +} diff --git a/frontend/src/widgets/expenses/ExpensesTable/ui/ExpensesTable.ui.tsx b/frontend/src/widgets/expenses/ExpensesTable/ui/ExpensesTable.ui.tsx new file mode 100644 index 0000000..8dbe045 --- /dev/null +++ b/frontend/src/widgets/expenses/ExpensesTable/ui/ExpensesTable.ui.tsx @@ -0,0 +1,67 @@ +import { NavLink } from 'react-router'; + +import { useExpensesList } from '@/entity/expenses/list'; +import { DeleteExpensesListButton } from '@/features/expenses/list/DeleteExpensesListButton'; +import { EditExpensesListButton } from '@/features/expenses/list/EditExpensesListButton'; + +import classes from './ExpensesTable.module.css'; + +const ExpensesTable = () => { + const { data: expensesList, isPending, isLoading } = useExpensesList(); + + if (isPending || isLoading) { + return ( + Loading... + ); + } + + return ( + + + + + + + + + + + + + + + + + {expensesList?.map((expense) => ( + + + + + + + + + + + + ))} + +
ПерейтиИдентификаторНазваниеКоличество элементовДействия
+ + Ссылка + + + {expense.id} + + {expense.title} + + {expense.items?.length || 0} + + + + +
+ ); +}; + +export default ExpensesTable;