feat(frontend): add expenses
This commit is contained in:
parent
d60aae26e6
commit
10c0e5cf7c
@ -77,6 +77,11 @@ export default [
|
|||||||
'@typescript-eslint/await-thenable': 'off',
|
'@typescript-eslint/await-thenable': 'off',
|
||||||
'@typescript-eslint/no-misused-promises': 'off',
|
'@typescript-eslint/no-misused-promises': 'off',
|
||||||
'@typescript-eslint/prefer-nullish-coalescing': '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',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
16
frontend/package-lock.json
generated
16
frontend/package-lock.json
generated
@ -23,7 +23,7 @@
|
|||||||
"@types/react": "19.1.9",
|
"@types/react": "19.1.9",
|
||||||
"@types/react-dom": "19.1.7",
|
"@types/react-dom": "19.1.7",
|
||||||
"@typescript-eslint/parser": "8.39.0",
|
"@typescript-eslint/parser": "8.39.0",
|
||||||
"eslint": "9.33.0",
|
"eslint": "9.34.0",
|
||||||
"eslint-config-ksv741": "0.2.0",
|
"eslint-config-ksv741": "0.2.0",
|
||||||
"openapi-typescript": "7.9.1",
|
"openapi-typescript": "7.9.1",
|
||||||
"typescript": "5.9.2"
|
"typescript": "5.9.2"
|
||||||
@ -203,9 +203,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@eslint/js": {
|
"node_modules/@eslint/js": {
|
||||||
"version": "9.33.0",
|
"version": "9.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz",
|
||||||
"integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==",
|
"integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"engines": {
|
"engines": {
|
||||||
@ -2666,9 +2666,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/eslint": {
|
"node_modules/eslint": {
|
||||||
"version": "9.33.0",
|
"version": "9.34.0",
|
||||||
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz",
|
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz",
|
||||||
"integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==",
|
"integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@ -2678,7 +2678,7 @@
|
|||||||
"@eslint/config-helpers": "^0.3.1",
|
"@eslint/config-helpers": "^0.3.1",
|
||||||
"@eslint/core": "^0.15.2",
|
"@eslint/core": "^0.15.2",
|
||||||
"@eslint/eslintrc": "^3.3.1",
|
"@eslint/eslintrc": "^3.3.1",
|
||||||
"@eslint/js": "9.33.0",
|
"@eslint/js": "9.34.0",
|
||||||
"@eslint/plugin-kit": "^0.3.5",
|
"@eslint/plugin-kit": "^0.3.5",
|
||||||
"@humanfs/node": "^0.16.6",
|
"@humanfs/node": "^0.16.6",
|
||||||
"@humanwhocodes/module-importer": "^1.0.1",
|
"@humanwhocodes/module-importer": "^1.0.1",
|
||||||
|
|||||||
@ -27,7 +27,7 @@
|
|||||||
"@types/react": "19.1.9",
|
"@types/react": "19.1.9",
|
||||||
"@types/react-dom": "19.1.7",
|
"@types/react-dom": "19.1.7",
|
||||||
"@typescript-eslint/parser": "8.39.0",
|
"@typescript-eslint/parser": "8.39.0",
|
||||||
"eslint": "9.33.0",
|
"eslint": "9.34.0",
|
||||||
"eslint-config-ksv741": "0.2.0",
|
"eslint-config-ksv741": "0.2.0",
|
||||||
"openapi-typescript": "7.9.1",
|
"openapi-typescript": "7.9.1",
|
||||||
"typescript": "5.9.2"
|
"typescript": "5.9.2"
|
||||||
|
|||||||
@ -7,8 +7,11 @@ import {
|
|||||||
} from 'react-router';
|
} from 'react-router';
|
||||||
|
|
||||||
import { LoginPage, RegisterPage } from '@/pages/Authorization';
|
import { LoginPage, RegisterPage } from '@/pages/Authorization';
|
||||||
|
import { ExpensePage } from '@/pages/Expense';
|
||||||
|
import { ExpensesPage } from '@/pages/Expenses';
|
||||||
import { HomePage } from '@/pages/Home';
|
import { HomePage } from '@/pages/Home';
|
||||||
import { AuthContextProvider } from '@/shared/context/AuthContext';
|
import { AuthContextProvider } from '@/shared/context/AuthContext';
|
||||||
|
import { PAGES } from '@/shared/router';
|
||||||
import { AuthRoute } from '@/shared/ui/AuthRoute';
|
import { AuthRoute } from '@/shared/ui/AuthRoute';
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
@ -19,13 +22,17 @@ const App = () => (
|
|||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route element={<AuthRoute />}>
|
<Route element={<AuthRoute />}>
|
||||||
<Route element={<HomePage />} path="/" />
|
<Route element={<HomePage />} path={PAGES.Home} />
|
||||||
|
|
||||||
|
<Route element={<ExpensesPage />} path={PAGES.Expenses} />
|
||||||
|
|
||||||
|
<Route element={<ExpensePage />} path={PAGES.Expense} />
|
||||||
</Route>
|
</Route>
|
||||||
|
|
||||||
<Route element={<AuthRoute needAuth={false} to="/" />}>
|
<Route element={<AuthRoute needAuth={false} to="/" />}>
|
||||||
<Route element={<LoginPage />} path="/login" />
|
<Route element={<LoginPage />} path={PAGES.Login} />
|
||||||
|
|
||||||
<Route element={<RegisterPage />} path="/register" />
|
<Route element={<RegisterPage />} path={PAGES.Register} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</BrowserRouter>
|
</BrowserRouter>
|
||||||
|
|||||||
4
frontend/src/app/App/ui/normalize.css
vendored
4
frontend/src/app/App/ui/normalize.css
vendored
@ -347,3 +347,7 @@ template {
|
|||||||
[hidden] {
|
[hidden] {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|||||||
12
frontend/src/entity/account/api/login.ts
Normal file
12
frontend/src/entity/account/api/login.ts
Normal file
@ -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<LoginSuccessResponse> {
|
||||||
|
const { data: result } = await api.post<LoginSuccessResponse>('/auth/login', data);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
11
frontend/src/entity/account/api/logout.ts
Normal file
11
frontend/src/entity/account/api/logout.ts
Normal file
@ -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<LogoutSuccessResponse>('/auth/logout');
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@ -1,5 +1,3 @@
|
|||||||
import { useMutation } from '@tanstack/react-query';
|
|
||||||
|
|
||||||
import { api } from '@/shared/api/client';
|
import { api } from '@/shared/api/client';
|
||||||
|
|
||||||
import type { paths } from '@/shared/api/schema';
|
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 RegisterRequestBody = paths['/auth/register']['post']['requestBody']['content']['application/json'];
|
||||||
type RegisterSuccessResponse = paths['/auth/register']['post']['responses']['200']['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<RegisterSuccessResponse>('auth/register', data);
|
const { data: result } = await api.post<RegisterSuccessResponse>('auth/register', data);
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useRegister = () => useMutation({
|
|
||||||
mutationFn: register,
|
|
||||||
onError: (error) => {
|
|
||||||
// eslint-disable-next-line no-console
|
|
||||||
console.log('Неуспешная регистрация', error);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@ -2,19 +2,10 @@ import { useMutation } from '@tanstack/react-query';
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
import { api } from '@/shared/api/client';
|
|
||||||
import { AuthContext } from '@/shared/context/AuthContext';
|
import { AuthContext } from '@/shared/context/AuthContext';
|
||||||
|
import { PAGES } from '@/shared/router';
|
||||||
|
|
||||||
import type { paths } from '@/shared/api/schema';
|
import { login } from '../api/login';
|
||||||
|
|
||||||
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<LoginSuccessResponse> {
|
|
||||||
const { data: result } = await api.post<LoginSuccessResponse>('/auth/login', data);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useLogin = () => {
|
export const useLogin = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -25,7 +16,7 @@ export const useLogin = () => {
|
|||||||
onSuccess: async (data) => {
|
onSuccess: async (data) => {
|
||||||
setAccessToken(data.accessToken);
|
setAccessToken(data.accessToken);
|
||||||
localStorage.setItem('accessToken', data.accessToken);
|
localStorage.setItem('accessToken', data.accessToken);
|
||||||
await navigate('/another');
|
await navigate(PAGES.Home);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
@ -2,18 +2,10 @@ import { useMutation } from '@tanstack/react-query';
|
|||||||
import { useContext } from 'react';
|
import { useContext } from 'react';
|
||||||
import { useNavigate } from 'react-router';
|
import { useNavigate } from 'react-router';
|
||||||
|
|
||||||
import { api } from '@/shared/api/client';
|
|
||||||
import { AuthContext } from '@/shared/context/AuthContext';
|
import { AuthContext } from '@/shared/context/AuthContext';
|
||||||
|
import { PAGES } from '@/shared/router';
|
||||||
|
|
||||||
import type { paths } from '@/shared/api/schema';
|
import { logout } from '../api/logout';
|
||||||
|
|
||||||
type LogoutSuccessResponse = paths['/auth/logout']['post']['responses']['200']['content']['application/json'];
|
|
||||||
|
|
||||||
async function logout() {
|
|
||||||
const { data: result } = await api.post<LogoutSuccessResponse>('/auth/logout');
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const useLogout = () => {
|
export const useLogout = () => {
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -24,7 +16,7 @@ export const useLogout = () => {
|
|||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
setAccessToken('');
|
setAccessToken('');
|
||||||
localStorage.removeItem('accessToken');
|
localStorage.removeItem('accessToken');
|
||||||
await navigate('/login');
|
await navigate(PAGES.Login);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
11
frontend/src/entity/account/hooks/useRegister.ts
Normal file
11
frontend/src/entity/account/hooks/useRegister.ts
Normal file
@ -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);
|
||||||
|
},
|
||||||
|
});
|
||||||
@ -1,3 +1,3 @@
|
|||||||
export { useLogin } from './login/useLogin';
|
export { useLogin } from './hooks/useLogin';
|
||||||
export { useRegister } from './register/useRegister';
|
export { useRegister } from './hooks/useRegister';
|
||||||
export { useLogout } from './logout/useLogout';
|
export { useLogout } from './hooks/useLogout';
|
||||||
|
|||||||
13
frontend/src/entity/expenses/item/api/addExpenseItem.ts
Normal file
13
frontend/src/entity/expenses/item/api/addExpenseItem.ts
Normal file
@ -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<AddExpenseItemSuccessResponse>(`/expenses/list/${path.id}/items`, data);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
12
frontend/src/entity/expenses/item/api/deleteExpenseItem.ts
Normal file
12
frontend/src/entity/expenses/item/api/deleteExpenseItem.ts
Normal file
@ -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<DeleteExpenseItemSuccessResponse>(`/expenses/list/${path.listId}/items/${path.id}`);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
13
frontend/src/entity/expenses/item/api/editExpenseItem.ts
Normal file
13
frontend/src/entity/expenses/item/api/editExpenseItem.ts
Normal file
@ -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<EditExpenseItemSuccessResponse>(`/expenses/list/${path.listId}/items/${path.id}`, data);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
15
frontend/src/entity/expenses/item/hooks/useAddExpenseItem.ts
Normal file
15
frontend/src/entity/expenses/item/hooks/useAddExpenseItem.ts
Normal file
@ -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] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -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] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -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] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
5
frontend/src/entity/expenses/item/index.ts
Normal file
5
frontend/src/entity/expenses/item/index.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export { useAddExpenseItem } from './hooks/useAddExpenseItem';
|
||||||
|
export { useDeleteExpenseItem } from './hooks/useDeleteExpenseItem';
|
||||||
|
export { useEditExpenseItem } from './hooks/useEditExpenseItem';
|
||||||
|
|
||||||
|
export type { ExpenseListItem } from './types';
|
||||||
3
frontend/src/entity/expenses/item/types.ts
Normal file
3
frontend/src/entity/expenses/item/types.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import type { paths } from '@/shared/api/schema';
|
||||||
|
|
||||||
|
export type ExpenseListItem = NonNullable<paths['/expenses/list/{id}']['get']['responses']['200']['content']['application/json']['items']>[number];
|
||||||
12
frontend/src/entity/expenses/list/api/addExpensesList.ts
Normal file
12
frontend/src/entity/expenses/list/api/addExpensesList.ts
Normal file
@ -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<AddExpenseListSuccessResponse>('/expenses/list', data);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
12
frontend/src/entity/expenses/list/api/deleteExpensesList.ts
Normal file
12
frontend/src/entity/expenses/list/api/deleteExpensesList.ts
Normal file
@ -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<DeleteExpenseSuccessResponse>(`/expenses/list/${id}`);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
17
frontend/src/entity/expenses/list/api/editExpensesList.ts
Normal file
17
frontend/src/entity/expenses/list/api/editExpensesList.ts
Normal file
@ -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<EditExpenseListSuccessResponse>(`/expenses/list/${id}`, data);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
12
frontend/src/entity/expenses/list/api/getExpenseList.ts
Normal file
12
frontend/src/entity/expenses/list/api/getExpenseList.ts
Normal file
@ -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<ExpenseListResponse>(`/expenses/list/${path.id}`);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
11
frontend/src/entity/expenses/list/api/getExpenseLists.ts
Normal file
11
frontend/src/entity/expenses/list/api/getExpenseLists.ts
Normal file
@ -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<ExpensesListResponse>('/expenses/list');
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
@ -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'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -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'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@ -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'] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
10
frontend/src/entity/expenses/list/hooks/useExpenseList.ts
Normal file
10
frontend/src/entity/expenses/list/hooks/useExpenseList.ts
Normal file
@ -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],
|
||||||
|
});
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
|
||||||
|
import { getExpenseLists } from '../api/getExpenseLists';
|
||||||
|
|
||||||
|
export const useExpensesList = () => useQuery({
|
||||||
|
queryFn: getExpenseLists,
|
||||||
|
queryKey: ['expensesList'],
|
||||||
|
});
|
||||||
7
frontend/src/entity/expenses/list/index.ts
Normal file
7
frontend/src/entity/expenses/list/index.ts
Normal file
@ -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';
|
||||||
3
frontend/src/entity/expenses/list/types.ts
Normal file
3
frontend/src/entity/expenses/list/types.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
import type { paths } from '@/shared/api/schema';
|
||||||
|
|
||||||
|
export type ExpenseList = paths['/expenses/list/{id}']['get']['responses']['200']['content']['application/json'];
|
||||||
@ -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<HTMLFormElement> = 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export { default as AddExpenseItemButton } from './ui/AddExpenseItemButton.ui';
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
@ -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<AddExpenseItemButtonProps> = (props) => {
|
||||||
|
const { listId } = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
isPending,
|
||||||
|
onClickHandler,
|
||||||
|
onCloseModalHandler,
|
||||||
|
onFormSubmitHandler,
|
||||||
|
} = useAddExpenseItemButton({ listId });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button type="button" onClick={onClickHandler}>
|
||||||
|
Добавить элемент
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Modal open={isOpen} onClose={onCloseModalHandler}>
|
||||||
|
<form className={classes.form} onSubmit={onFormSubmitHandler}>
|
||||||
|
<h2>Добавить список расходов</h2>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
required
|
||||||
|
disabled={isPending}
|
||||||
|
label="Название"
|
||||||
|
name="title"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
disabled={isPending}
|
||||||
|
label="Описание"
|
||||||
|
name="description"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
required
|
||||||
|
disabled={isPending}
|
||||||
|
label="Дата"
|
||||||
|
name="date"
|
||||||
|
type="date"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
required
|
||||||
|
disabled={isPending}
|
||||||
|
label="Сумма"
|
||||||
|
name="amount"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
disabled={isPending}
|
||||||
|
label="Количество"
|
||||||
|
name="count"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button disabled={isPending} type="submit">Добавить</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddExpenseItemButton;
|
||||||
@ -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<HTMLFormElement> = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
await deleteExpenseItemMutation();
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isOpen,
|
||||||
|
onCloseModalHandler,
|
||||||
|
onClickHandler,
|
||||||
|
isPending,
|
||||||
|
onFormSubmitHandler,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export { default as DeleteExpenseItemButton } from './ui/DeleteExpenseItemButton.ui';
|
||||||
@ -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<DeleteExpenseItemButtonProps> = (props) => {
|
||||||
|
const { item } = props;
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
onCloseModalHandler,
|
||||||
|
onClickHandler,
|
||||||
|
isPending,
|
||||||
|
onFormSubmitHandler,
|
||||||
|
} = useDeleteExpenseItemButton({ item });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button type="button" onClick={onClickHandler}>
|
||||||
|
Удалить
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Modal open={isOpen} onClose={onCloseModalHandler}>
|
||||||
|
<form onSubmit={onFormSubmitHandler}>
|
||||||
|
<h2>Удалить элемент ?</h2>
|
||||||
|
|
||||||
|
<button disabled={isPending} type="submit">Удалить</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteExpenseItemButton;
|
||||||
@ -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<HTMLFormElement> = 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export { default as EditExpenseItemButton } from './ui/EditExpenseItemButton.ui';
|
||||||
@ -0,0 +1,5 @@
|
|||||||
|
.form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
@ -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<EditExpenseItemButtonProps> = (props) => {
|
||||||
|
const { item } = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
onCloseModalHandler,
|
||||||
|
onClickHandler,
|
||||||
|
isPending,
|
||||||
|
onFormSubmitHandler,
|
||||||
|
} = useAddExpenseItemButton({ item });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button type="button" onClick={onClickHandler}>
|
||||||
|
Редактировать
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Modal open={isOpen} onClose={onCloseModalHandler}>
|
||||||
|
<form className={classes.form} onSubmit={onFormSubmitHandler}>
|
||||||
|
<h2>Добавить список расходов</h2>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
defaultValue={item.title}
|
||||||
|
disabled={isPending}
|
||||||
|
label="Название"
|
||||||
|
name="title"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
defaultValue={item.description ?? ''}
|
||||||
|
disabled={isPending}
|
||||||
|
label="Описание"
|
||||||
|
name="description"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
defaultValue={new Date(item.date).toISOString().split('T')[0]}
|
||||||
|
disabled={isPending}
|
||||||
|
label="Дата"
|
||||||
|
name="date"
|
||||||
|
type="date"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
defaultValue={item.amount}
|
||||||
|
disabled={isPending}
|
||||||
|
label="Сумма"
|
||||||
|
name="amount"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
defaultValue={item.count ?? 1}
|
||||||
|
disabled={isPending}
|
||||||
|
label="Количество"
|
||||||
|
name="count"
|
||||||
|
type="number"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button disabled={isPending} type="submit">Сохранить</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditExpenseItemButton;
|
||||||
@ -0,0 +1,43 @@
|
|||||||
|
import { type FormEventHandler, type MouseEventHandler, useState } from 'react';
|
||||||
|
|
||||||
|
import { useAddExpensesList } from '@/entity/expenses/list';
|
||||||
|
|
||||||
|
type UseAddExpensesListButtonArgs = {
|
||||||
|
onClick?: MouseEventHandler<HTMLButtonElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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<HTMLButtonElement> = (event) => {
|
||||||
|
onClick?.(event);
|
||||||
|
setIsOpen(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export { default as AddExpensesListButton } from './ui/AddExpensesListButton.ui';
|
||||||
@ -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<HTMLButtonElement> & {
|
||||||
|
ref?: Ref<HTMLButtonElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const AddExpensesListButton: FC<AddExpensesListButtonProps> = (props) => {
|
||||||
|
const { onClick, ...restProps } = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
isPending,
|
||||||
|
onCloseModalHandler,
|
||||||
|
onClickHandler,
|
||||||
|
onFormSubmitHandler,
|
||||||
|
} = useAddExpensesListButton({ onClick });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button type="button" onClick={onClickHandler} {...restProps}>
|
||||||
|
Добавить
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Modal open={isOpen} onClose={onCloseModalHandler}>
|
||||||
|
<form onSubmit={onFormSubmitHandler}>
|
||||||
|
<h2>Добавить список расходов</h2>
|
||||||
|
|
||||||
|
<Input disabled={isPending} label="Название" name="title" />
|
||||||
|
|
||||||
|
<button disabled={isPending} type="submit">Добавить</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AddExpensesListButton;
|
||||||
@ -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<HTMLFormElement> = async (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
await deleteExpensesListAsync({ id });
|
||||||
|
setIsOpen(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
isOpen,
|
||||||
|
isPending,
|
||||||
|
onClickHandler,
|
||||||
|
onCloseModalHandler,
|
||||||
|
onFormSubmitHandler,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export { default as DeleteExpensesListButton } from './ui/DeleteExpensesListButton.ui';
|
||||||
@ -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<HTMLButtonElement> & {
|
||||||
|
list: ExpenseList;
|
||||||
|
ref?: Ref<HTMLButtonElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DeleteExpensesListButton = (props: DeleteExpensesListButtonProps) => {
|
||||||
|
const { list, ...restProps } = props;
|
||||||
|
const { id, title } = list;
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
isPending,
|
||||||
|
onCloseModalHandler,
|
||||||
|
onClickHandler,
|
||||||
|
onFormSubmitHandler,
|
||||||
|
} = useDeleteExpensesListButton({ id });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button type="button" onClick={onClickHandler} {...restProps}>
|
||||||
|
Удалить
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Modal open={isOpen} onClose={onCloseModalHandler}>
|
||||||
|
<form onSubmit={onFormSubmitHandler}>
|
||||||
|
<h2>Удалить список расходов</h2>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
Вы действительно хотите удалить список расходов
|
||||||
|
{' '}
|
||||||
|
|
||||||
|
{`"${title}"`}
|
||||||
|
|
||||||
|
{' '}
|
||||||
|
?
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<button disabled={isPending} type="submit">Удалить</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DeleteExpensesListButton;
|
||||||
@ -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<HTMLFormElement> = 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,
|
||||||
|
};
|
||||||
|
};
|
||||||
@ -0,0 +1 @@
|
|||||||
|
export { default as EditExpensesListButton } from './ui/EditExpensesListButton.ui';
|
||||||
@ -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<HTMLButtonElement> & {
|
||||||
|
list: ExpenseList;
|
||||||
|
ref?: Ref<HTMLButtonElement>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditExpensesListButton: FC<EditExpensesListButtonProps> = (props) => {
|
||||||
|
const { list, ...restProps } = props;
|
||||||
|
|
||||||
|
const {
|
||||||
|
isOpen,
|
||||||
|
isPending,
|
||||||
|
onCloseModalHandler,
|
||||||
|
onClickHandler,
|
||||||
|
onFormSubmitHandler,
|
||||||
|
} = useEditExpensesListButton({ id: list.id });
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button type="button" onClick={onClickHandler} {...restProps}>
|
||||||
|
Редактировать
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<Modal open={isOpen} onClose={onCloseModalHandler}>
|
||||||
|
<form onSubmit={onFormSubmitHandler}>
|
||||||
|
<h2>Редактировать список расходов</h2>
|
||||||
|
|
||||||
|
<Input
|
||||||
|
defaultValue={list.title}
|
||||||
|
disabled={isPending}
|
||||||
|
label="Название"
|
||||||
|
name="title"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<button disabled={isPending} type="submit">Сохранить</button>
|
||||||
|
</form>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default EditExpensesListButton;
|
||||||
1
frontend/src/pages/Expense/index.ts
Normal file
1
frontend/src/pages/Expense/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as ExpensePage } from './ui/ExpensePage.ui';
|
||||||
45
frontend/src/pages/Expense/ui/ExpensePage.ui.tsx
Normal file
45
frontend/src/pages/Expense/ui/ExpensePage.ui.tsx
Normal file
@ -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 <Spinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expenseList?.items?.length && expenseList.items.length > 0) {
|
||||||
|
return (
|
||||||
|
<ExpenseItemsTable list={expenseList} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return <p>Списков расходов нет</p>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout>
|
||||||
|
<div>
|
||||||
|
<h1>Списки расходов</h1>
|
||||||
|
|
||||||
|
<AddExpenseItemButton listId={id} />
|
||||||
|
|
||||||
|
{renderContent()}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpensePage;
|
||||||
1
frontend/src/pages/Expenses/index.ts
Normal file
1
frontend/src/pages/Expenses/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as ExpensesPage } from './ui/ExpensesPage.ui';
|
||||||
38
frontend/src/pages/Expenses/ui/ExpensesPage.ui.tsx
Normal file
38
frontend/src/pages/Expenses/ui/ExpensesPage.ui.tsx
Normal file
@ -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 <Spinner />;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (expensesList && expensesList.length > 0) {
|
||||||
|
return <ExpensesTable />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <p>Списков расходов нет</p>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppLayout>
|
||||||
|
<div>
|
||||||
|
<h1>Списки расходов</h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<AddExpensesListButton />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{renderContent()}
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</AppLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpensesPage;
|
||||||
@ -1,51 +1,11 @@
|
|||||||
import { useContext, useState } from 'react';
|
import { AppLayout } from '@/widgets/AppLayout';
|
||||||
|
|
||||||
import { api } from '@/shared/api/client';
|
const HomePage = () => (
|
||||||
import { AuthContext } from '@/shared/context/AuthContext';
|
<AppLayout>
|
||||||
|
|
||||||
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<Response>('/auth/me');
|
|
||||||
|
|
||||||
return data;
|
|
||||||
};
|
|
||||||
|
|
||||||
const HomePage = () => {
|
|
||||||
const [info, setInfo] = useState<Response>();
|
|
||||||
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 (
|
|
||||||
<div>
|
<div>
|
||||||
Home page only for auth user
|
<h1>Home Page</h1>
|
||||||
<button type="button" onClick={onClick}>
|
|
||||||
get profile
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<b>
|
|
||||||
ID:
|
|
||||||
{info?.id}
|
|
||||||
</b>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</AppLayout>
|
||||||
};
|
);
|
||||||
|
|
||||||
export default HomePage;
|
export default HomePage;
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
|
|
||||||
|
import { PAGES } from '@/shared/router';
|
||||||
|
|
||||||
import type { paths } from './schema';
|
import type { paths } from './schema';
|
||||||
|
|
||||||
const BASE_URL = 'http://localhost:3000';
|
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) {
|
if (error.response?.status === 401 && !originalRequest._retry) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||||
originalRequest._retry = true;
|
originalRequest._retry = true;
|
||||||
const { data: result } = await axios.post<RefreshTokensSuccessResponse>(`${BASE_URL}/auth/refresh`, null, {
|
try {
|
||||||
withCredentials: true,
|
const { data: result } = await axios.post<RefreshTokensSuccessResponse>(`${BASE_URL}/auth/refresh`, null, {
|
||||||
});
|
withCredentials: true,
|
||||||
const { accessToken } = result;
|
});
|
||||||
localStorage.setItem('accessToken', accessToken);
|
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
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access
|
||||||
return api(error.config);
|
return api(error.config);
|
||||||
|
|||||||
@ -75,7 +75,7 @@ export interface paths {
|
|||||||
* Получить профиль
|
* Получить профиль
|
||||||
* @description Получить данные о своем профиле
|
* @description Получить данные о своем профиле
|
||||||
*/
|
*/
|
||||||
get: operations["AuthController_me"];
|
get: operations["AuthController_profile"];
|
||||||
put?: never;
|
put?: never;
|
||||||
post?: never;
|
post?: never;
|
||||||
delete?: never;
|
delete?: never;
|
||||||
@ -101,6 +101,106 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: 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<string, never>;
|
export type webhooks = Record<string, never>;
|
||||||
export interface components {
|
export interface components {
|
||||||
@ -156,11 +256,18 @@ export interface components {
|
|||||||
/** @description Access токен */
|
/** @description Access токен */
|
||||||
accessToken: string;
|
accessToken: string;
|
||||||
};
|
};
|
||||||
IncorrectLoginDto: {
|
LoginValidationDto: {
|
||||||
/** @example 400 */
|
/** @example 400 */
|
||||||
statusCode: number;
|
statusCode: number;
|
||||||
/** @example Неверный логин или пароль */
|
/** @example [
|
||||||
message: Record<string, never>;
|
* "Email должен быть строкой.",
|
||||||
|
* "Некорректный формат email.",
|
||||||
|
* "Email обязателен для заполнения.",
|
||||||
|
* "Пароль должен быть строкой.",
|
||||||
|
* "Пароль обязателен для заполнения.",
|
||||||
|
* "Пароль должен содержать минимум 6 символов."
|
||||||
|
* ] */
|
||||||
|
message: string[];
|
||||||
/** @example Bad Request */
|
/** @example Bad Request */
|
||||||
error: string;
|
error: string;
|
||||||
};
|
};
|
||||||
@ -168,6 +275,161 @@ export interface components {
|
|||||||
/** @description Индетификатор аккаунта */
|
/** @description Индетификатор аккаунта */
|
||||||
id: string;
|
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;
|
responses: never;
|
||||||
parameters: never;
|
parameters: never;
|
||||||
@ -241,13 +503,13 @@ export interface operations {
|
|||||||
"application/json": components["schemas"]["SuccessLoginDto"];
|
"application/json": components["schemas"]["SuccessLoginDto"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
/** @description Неверный логин или пароль */
|
/** @description Ошибка валидации */
|
||||||
400: {
|
400: {
|
||||||
headers: {
|
headers: {
|
||||||
[name: string]: unknown;
|
[name: string]: unknown;
|
||||||
};
|
};
|
||||||
content: {
|
content: {
|
||||||
"application/json": components["schemas"]["IncorrectLoginDto"];
|
"application/json": components["schemas"]["LoginValidationDto"];
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@ -272,7 +534,7 @@ export interface operations {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
AuthController_me: {
|
AuthController_profile: {
|
||||||
parameters: {
|
parameters: {
|
||||||
query?: never;
|
query?: never;
|
||||||
header?: 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;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1 +1,2 @@
|
|||||||
export { PAGES } from './types';
|
export { PAGES } from './types';
|
||||||
|
export type { ExpensePageParams } from './types';
|
||||||
|
|||||||
@ -2,4 +2,10 @@ export enum PAGES {
|
|||||||
Home = '/',
|
Home = '/',
|
||||||
Login = '/login',
|
Login = '/login',
|
||||||
Register = '/register',
|
Register = '/register',
|
||||||
|
Expenses = '/expenses',
|
||||||
|
Expense = '/expenses/:id',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ExpensePageParams = {
|
||||||
|
id: string;
|
||||||
|
};
|
||||||
|
|||||||
1
frontend/src/shared/ui/Modal/index.ts
Normal file
1
frontend/src/shared/ui/Modal/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as Modal } from './ui/Modal.ui';
|
||||||
27
frontend/src/shared/ui/Modal/ui/Modal.module.css
Normal file
27
frontend/src/shared/ui/Modal/ui/Modal.module.css
Normal file
@ -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;
|
||||||
|
}
|
||||||
29
frontend/src/shared/ui/Modal/ui/Modal.ui.tsx
Normal file
29
frontend/src/shared/ui/Modal/ui/Modal.ui.tsx
Normal file
@ -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<ModalProps> = (props) => {
|
||||||
|
const { open, onClose, children } = props;
|
||||||
|
|
||||||
|
if (!open) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.wrapper} onClick={onClose}>
|
||||||
|
<div className={classes.content} onClick={(e) => { e.stopPropagation(); }}>
|
||||||
|
<span className={classes.closeIcon} onClick={onClose}>×</span>
|
||||||
|
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Modal;
|
||||||
1
frontend/src/shared/ui/Spinner/index.ts
Normal file
1
frontend/src/shared/ui/Spinner/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as Spinner } from './ui/Spinner.ui';
|
||||||
19
frontend/src/shared/ui/Spinner/ui/Spinner.module.css
Normal file
19
frontend/src/shared/ui/Spinner/ui/Spinner.module.css
Normal file
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
7
frontend/src/shared/ui/Spinner/ui/Spinner.ui.tsx
Normal file
7
frontend/src/shared/ui/Spinner/ui/Spinner.ui.tsx
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import classes from './Spinner.module.css';
|
||||||
|
|
||||||
|
const Spinner = () => (
|
||||||
|
<span className={classes.loader}></span>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default Spinner;
|
||||||
1
frontend/src/widgets/AppLayout/index.ts
Normal file
1
frontend/src/widgets/AppLayout/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as AppLayout } from './ui/AppLayout.ui';
|
||||||
7
frontend/src/widgets/AppLayout/ui/AppLayout.module.css
Normal file
7
frontend/src/widgets/AppLayout/ui/AppLayout.module.css
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
.wrapper {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
25
frontend/src/widgets/AppLayout/ui/AppLayout.ui.tsx
Normal file
25
frontend/src/widgets/AppLayout/ui/AppLayout.ui.tsx
Normal file
@ -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<AppLayoutProps> = (props) => {
|
||||||
|
const { children } = props;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={classes.wrapper}>
|
||||||
|
<SideBar />
|
||||||
|
|
||||||
|
<div className={classes.content}>
|
||||||
|
{children}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default AppLayout;
|
||||||
1
frontend/src/widgets/Sidebar/index.ts
Normal file
1
frontend/src/widgets/Sidebar/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as SideBar } from './ui/SideBar.ui';
|
||||||
5
frontend/src/widgets/Sidebar/ui/SideBar.module.css
Normal file
5
frontend/src/widgets/Sidebar/ui/SideBar.module.css
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.wrapper {
|
||||||
|
width: 200px;
|
||||||
|
height: 100vh;
|
||||||
|
border: 1px solid dimgrey;
|
||||||
|
}
|
||||||
25
frontend/src/widgets/Sidebar/ui/SideBar.ui.tsx
Normal file
25
frontend/src/widgets/Sidebar/ui/SideBar.ui.tsx
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
import { NavLink } from 'react-router';
|
||||||
|
|
||||||
|
import { PAGES } from '@/shared/router';
|
||||||
|
|
||||||
|
import classes from './SideBar.module.css';
|
||||||
|
|
||||||
|
const SideBar = () => (
|
||||||
|
<div className={classes.wrapper}>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<NavLink to={PAGES.Home}>
|
||||||
|
Главная
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li>
|
||||||
|
<NavLink to={PAGES.Expenses}>
|
||||||
|
Расходы
|
||||||
|
</NavLink>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default SideBar;
|
||||||
@ -1,4 +1,4 @@
|
|||||||
import { Input } from '@/shared/ui/input';
|
import { Input } from '@/shared/ui/Input';
|
||||||
|
|
||||||
import { useLoginForm } from './hooks/useLoginForm';
|
import { useLoginForm } from './hooks/useLoginForm';
|
||||||
import classes from './LoginForm.module.css';
|
import classes from './LoginForm.module.css';
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Input } from '@/shared/ui/input';
|
import { Input } from '@/shared/ui/Input';
|
||||||
|
|
||||||
import { useRegisterForm } from './hooks/useRegisterForm';
|
import { useRegisterForm } from './hooks/useRegisterForm';
|
||||||
import classes from './RegisterForm.module.css';
|
import classes from './RegisterForm.module.css';
|
||||||
|
|||||||
1
frontend/src/widgets/expenses/ExpenseItemsTable/index.ts
Normal file
1
frontend/src/widgets/expenses/ExpenseItemsTable/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as ExpenseItemsTable } from './ui/ExpenseItemsTable.ui';
|
||||||
@ -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<ExpenseItemsTableProps> = (props) => {
|
||||||
|
const { list } = props;
|
||||||
|
const { items } = list;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className={classes.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Идентификатор</th>
|
||||||
|
|
||||||
|
<th>Название</th>
|
||||||
|
|
||||||
|
<th>Описание</th>
|
||||||
|
|
||||||
|
<th>Дата</th>
|
||||||
|
|
||||||
|
<th>Сумма</th>
|
||||||
|
|
||||||
|
<th>Валюта</th>
|
||||||
|
|
||||||
|
<th>Количество</th>
|
||||||
|
|
||||||
|
<th>Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{items?.map((item) => (
|
||||||
|
<tr key={item.id}>
|
||||||
|
<td>
|
||||||
|
{item.id}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{item.title}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{item.description}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{item.date}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{item.amount}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{item.currency}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{item.count}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<EditExpenseItemButton item={item} />
|
||||||
|
|
||||||
|
<DeleteExpenseItemButton item={item} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpenseItemsTable;
|
||||||
1
frontend/src/widgets/expenses/ExpensesTable/index.ts
Normal file
1
frontend/src/widgets/expenses/ExpensesTable/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export { default as ExpensesTable } from './ui/ExpensesTable.ui';
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
.table, th, td {
|
||||||
|
border: 1px solid;
|
||||||
|
}
|
||||||
@ -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 (
|
||||||
|
<span>Loading...</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<table className={classes.table}>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Перейти</th>
|
||||||
|
|
||||||
|
<th>Идентификатор</th>
|
||||||
|
|
||||||
|
<th>Название</th>
|
||||||
|
|
||||||
|
<th>Количество элементов</th>
|
||||||
|
|
||||||
|
<th>Действия</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
|
||||||
|
<tbody>
|
||||||
|
{expensesList?.map((expense) => (
|
||||||
|
<tr key={expense.id}>
|
||||||
|
<td>
|
||||||
|
<NavLink to={`/expenses/${expense.id}`}>
|
||||||
|
Ссылка
|
||||||
|
</NavLink>
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{expense.id}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{expense.title}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
{expense.items?.length || 0}
|
||||||
|
</td>
|
||||||
|
|
||||||
|
<td>
|
||||||
|
<EditExpensesListButton list={expense} />
|
||||||
|
|
||||||
|
<DeleteExpensesListButton list={expense} />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ExpensesTable;
|
||||||
Loading…
x
Reference in New Issue
Block a user