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/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',
|
||||
},
|
||||
},
|
||||
{
|
||||
|
||||
16
frontend/package-lock.json
generated
16
frontend/package-lock.json
generated
@ -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",
|
||||
|
||||
@ -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"
|
||||
|
||||
@ -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 = () => (
|
||||
<BrowserRouter>
|
||||
<Routes>
|
||||
<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 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>
|
||||
</Routes>
|
||||
</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] {
|
||||
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 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<RegisterSuccessResponse>('auth/register', data);
|
||||
|
||||
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 { 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<LoginSuccessResponse> {
|
||||
const { data: result } = await api.post<LoginSuccessResponse>('/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
|
||||
@ -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<LogoutSuccessResponse>('/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);
|
||||
},
|
||||
});
|
||||
};
|
||||
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 { useRegister } from './register/useRegister';
|
||||
export { useLogout } from './logout/useLogout';
|
||||
export { useLogin } from './hooks/useLogin';
|
||||
export { useRegister } from './hooks/useRegister';
|
||||
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';
|
||||
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<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 (
|
||||
const HomePage = () => (
|
||||
<AppLayout>
|
||||
<div>
|
||||
Home page only for auth user
|
||||
<button type="button" onClick={onClick}>
|
||||
get profile
|
||||
</button>
|
||||
|
||||
<br />
|
||||
|
||||
<b>
|
||||
ID:
|
||||
{info?.id}
|
||||
</b>
|
||||
<h1>Home Page</h1>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
</AppLayout>
|
||||
);
|
||||
|
||||
export default HomePage;
|
||||
|
||||
@ -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<RefreshTokensSuccessResponse>(`${BASE_URL}/auth/refresh`, null, {
|
||||
withCredentials: true,
|
||||
});
|
||||
const { accessToken } = result;
|
||||
localStorage.setItem('accessToken', accessToken);
|
||||
try {
|
||||
const { data: result } = await axios.post<RefreshTokensSuccessResponse>(`${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);
|
||||
|
||||
@ -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<string, never>;
|
||||
export interface components {
|
||||
@ -156,11 +256,18 @@ export interface components {
|
||||
/** @description Access токен */
|
||||
accessToken: string;
|
||||
};
|
||||
IncorrectLoginDto: {
|
||||
LoginValidationDto: {
|
||||
/** @example 400 */
|
||||
statusCode: number;
|
||||
/** @example Неверный логин или пароль */
|
||||
message: Record<string, never>;
|
||||
/** @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;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
@ -1 +1,2 @@
|
||||
export { PAGES } from './types';
|
||||
export type { ExpensePageParams } from './types';
|
||||
|
||||
@ -2,4 +2,10 @@ export enum PAGES {
|
||||
Home = '/',
|
||||
Login = '/login',
|
||||
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 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 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