Compare commits
No commits in common. "af72e8dc927c91d3744e0b769a9b9333dbb3a005" and "5a5c308e45b38cd1cf9cbaf05d84b11163eb8c4c" have entirely different histories.
af72e8dc92
...
5a5c308e45
@ -1,13 +0,0 @@
|
||||
const mockEnqueueSnackbar = jest.fn();
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
jest.mock('notistack', () => ({
|
||||
...jest.requireActual('notistack'),
|
||||
enqueueSnackbar: mockEnqueueSnackbar,
|
||||
}));
|
||||
|
||||
beforeEach(() => {
|
||||
mockEnqueueSnackbar.mockClear();
|
||||
});
|
||||
|
||||
export { mockEnqueueSnackbar };
|
||||
@ -1,4 +1,3 @@
|
||||
export { mockEnqueueSnackbar } from './enqueueSnackbar';
|
||||
export { selectOption } from './selectOption';
|
||||
export { typeDateField } from './typeDateField';
|
||||
export { typeTextField } from './typeTextField';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { screen, within } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
export const selectOption = async (selectElement: HTMLElement | null, value?: string) => {
|
||||
export const selectOption = async (selectElement: HTMLElement | null, value: string) => {
|
||||
if (!selectElement) {
|
||||
throw new Error('Не найден элемент выпадающего списка');
|
||||
}
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
import type { PropsWithChildren } from 'react';
|
||||
|
||||
import { SnackbarProvider } from 'notistack';
|
||||
|
||||
import { createLocalizationWrapper } from './LocalizationWrapper';
|
||||
import { createReactQueryWrapper } from './ReactQueryWrapper';
|
||||
|
||||
@ -12,8 +10,6 @@ export const createAppWrapper = () => ({ children }: PropsWithChildren) => {
|
||||
return (
|
||||
<ReactQueryWrapper>
|
||||
<LocalizationWrapper>
|
||||
<SnackbarProvider />
|
||||
|
||||
{children}
|
||||
</LocalizationWrapper>
|
||||
</ReactQueryWrapper>
|
||||
|
||||
1
frontend/configs/jest/setupEnv.ts
Normal file
1
frontend/configs/jest/setupEnv.ts
Normal file
@ -0,0 +1 @@
|
||||
process.env.TZ = 'Europe/Moscow';
|
||||
@ -1,6 +1,8 @@
|
||||
import '@testing-library/jest-dom';
|
||||
import { server } from '@mocks/jest/server';
|
||||
|
||||
import './setupEnv';
|
||||
|
||||
beforeAll(() => {
|
||||
server.listen();
|
||||
});
|
||||
@ -10,14 +12,3 @@ afterEach(() => {
|
||||
afterAll(() => {
|
||||
server.close();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers({
|
||||
advanceTimers: 0.1,
|
||||
now: new Date('2025-01-01T00:00:00.000Z'),
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
@ -12,8 +12,6 @@ export default [
|
||||
rules: {
|
||||
'@stylistic/jsx-sort-props': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'off',
|
||||
'@typescript-eslint/no-misused-promises': 'off',
|
||||
'@typescript-eslint/prefer-nullish-coalescing': 'off',
|
||||
'@typescript-eslint/sort-type-constituents': 'off',
|
||||
'@typescript-eslint/strict-boolean-expressions': 'off',
|
||||
'func-style': 'off',
|
||||
|
||||
89
frontend/package-lock.json
generated
89
frontend/package-lock.json
generated
@ -11,7 +11,6 @@
|
||||
"@emotion/react": "11.14.0",
|
||||
"@emotion/styled": "11.14.1",
|
||||
"@fontsource/roboto": "5.2.8",
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@mui/icons-material": "7.3.4",
|
||||
"@mui/material": "7.3.4",
|
||||
"@mui/x-date-pickers": "8.12.0",
|
||||
@ -19,12 +18,9 @@
|
||||
"axios": "1.12.2",
|
||||
"date-fns": "4.1.0",
|
||||
"date-fns-tz": "3.2.0",
|
||||
"notistack": "3.0.2",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-hook-form": "7.64.0",
|
||||
"react-router": "7.9.4",
|
||||
"zod": "4.1.12"
|
||||
"react-router": "7.9.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rsbuild/core": "1.5.14",
|
||||
@ -1106,18 +1102,6 @@
|
||||
"url": "https://github.com/sponsors/ayuhito"
|
||||
}
|
||||
},
|
||||
"node_modules/@hookform/resolvers": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-5.2.2.tgz",
|
||||
"integrity": "sha512-A/IxlMLShx3KjV/HeTcTfaMxdwy690+L/ZADoeaTltLx+CVuzkeVIPuybK3jrRfw7YZnmdKsVVHAlEPIAEUNlA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@standard-schema/utils": "^0.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react-hook-form": "^7.55.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
@ -3598,12 +3582,6 @@
|
||||
"@sinonjs/commons": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@standard-schema/utils": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@standard-schema/utils/-/utils-0.3.0.tgz",
|
||||
"integrity": "sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@stylistic/eslint-plugin": {
|
||||
"version": "5.3.1",
|
||||
"resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-5.3.1.tgz",
|
||||
@ -7991,15 +7969,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/goober": {
|
||||
"version": "2.1.18",
|
||||
"resolved": "https://registry.npmjs.org/goober/-/goober-2.1.18.tgz",
|
||||
"integrity": "sha512-2vFqsaDVIT9Gz7N6kAL++pLpp41l3PfDuusHcjnGLfR6+huZkl6ziX+zgVC3ZxpqWhzH6pyDdGrCeDhMIvwaxw==",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"csstype": "^3.0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
|
||||
@ -11390,37 +11359,6 @@
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/notistack": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/notistack/-/notistack-3.0.2.tgz",
|
||||
"integrity": "sha512-0R+/arLYbK5Hh7mEfR2adt0tyXJcCC9KkA2hc56FeWik2QN6Bm/S4uW+BjzDARsJth5u06nTjelSw/VSnB1YEA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"clsx": "^1.1.0",
|
||||
"goober": "^2.0.33"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0",
|
||||
"npm": ">=6.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/notistack"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^17.0.0 || ^18.0.0 || ^19.0.0",
|
||||
"react-dom": "^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/notistack/node_modules/clsx": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.2.1.tgz",
|
||||
"integrity": "sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/npm-run-path": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz",
|
||||
@ -12103,22 +12041,6 @@
|
||||
"react": "^19.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-hook-form": {
|
||||
"version": "7.64.0",
|
||||
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.64.0.tgz",
|
||||
"integrity": "sha512-fnN+vvTiMLnRqKNTVhDysdrUay0kUUAymQnFIznmgDvapjveUWOOPqMNzPg+A+0yf9DuE2h6xzBjN1s+Qx8wcg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/react-hook-form"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.8.0 || ^17 || ^18 || ^19"
|
||||
}
|
||||
},
|
||||
"node_modules/react-is": {
|
||||
"version": "16.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
|
||||
@ -14258,15 +14180,6 @@
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "4.1.12",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.12.tgz",
|
||||
"integrity": "sha512-JInaHOamG8pt5+Ey8kGmdcAcg3OL9reK8ltczgHTAwNhMys/6ThXHityHxVV2p3fkw/c+MAvBHFVYHFZDmjMCQ==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,7 +17,6 @@
|
||||
"@emotion/react": "11.14.0",
|
||||
"@emotion/styled": "11.14.1",
|
||||
"@fontsource/roboto": "5.2.8",
|
||||
"@hookform/resolvers": "5.2.2",
|
||||
"@mui/icons-material": "7.3.4",
|
||||
"@mui/material": "7.3.4",
|
||||
"@mui/x-date-pickers": "8.12.0",
|
||||
@ -25,12 +24,9 @@
|
||||
"axios": "1.12.2",
|
||||
"date-fns": "4.1.0",
|
||||
"date-fns-tz": "3.2.0",
|
||||
"notistack": "3.0.2",
|
||||
"react": "19.2.0",
|
||||
"react-dom": "19.2.0",
|
||||
"react-hook-form": "7.64.0",
|
||||
"react-router": "7.9.4",
|
||||
"zod": "4.1.12"
|
||||
"react-router": "7.9.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rsbuild/core": "1.5.14",
|
||||
|
||||
@ -1,25 +1,17 @@
|
||||
import CssBaseline from '@mui/material/CssBaseline';
|
||||
import './normalize.css';
|
||||
import { LocalizationProvider } from '@mui/x-date-pickers';
|
||||
import { AdapterDateFns } from '@mui/x-date-pickers/AdapterDateFns';
|
||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ru } from 'date-fns/locale/ru';
|
||||
import { SnackbarProvider } from 'notistack';
|
||||
|
||||
import { AuthContextProvider } from '@/shared/context/AuthContext';
|
||||
import { setupZod } from '@/shared/utils';
|
||||
|
||||
import Routing from './Routing';
|
||||
|
||||
setupZod();
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const App = () => (
|
||||
<QueryClientProvider client={queryClient}>
|
||||
<CssBaseline />
|
||||
|
||||
<SnackbarProvider anchorOrigin={{ horizontal: 'right', vertical: 'bottom' }} />
|
||||
|
||||
<AuthContextProvider>
|
||||
<LocalizationProvider adapterLocale={ru} dateAdapter={AdapterDateFns}>
|
||||
<Routing />
|
||||
|
||||
353
frontend/src/app/App/ui/normalize.css
vendored
Normal file
353
frontend/src/app/App/ui/normalize.css
vendored
Normal file
@ -0,0 +1,353 @@
|
||||
/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
|
||||
|
||||
/* Document
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Correct the line height in all browsers.
|
||||
* 2. Prevent adjustments of font size after orientation changes in iOS.
|
||||
*/
|
||||
|
||||
html {
|
||||
line-height: 1.15; /* 1 */
|
||||
-webkit-text-size-adjust: 100%; /* 2 */
|
||||
}
|
||||
|
||||
/* Sections
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the margin in all browsers.
|
||||
*/
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the `main` element consistently in IE.
|
||||
*/
|
||||
|
||||
main {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the font size and margin on `h1` elements within `section` and
|
||||
* `article` contexts in Chrome, Firefox, and Safari.
|
||||
*/
|
||||
|
||||
h1 {
|
||||
font-size: 2em;
|
||||
margin: 0.67em 0;
|
||||
}
|
||||
|
||||
/* Grouping content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in Firefox.
|
||||
* 2. Show the overflow in Edge and IE.
|
||||
*/
|
||||
|
||||
hr {
|
||||
box-sizing: content-box; /* 1 */
|
||||
height: 0; /* 1 */
|
||||
overflow: visible; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
pre {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/* Text-level semantics
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the gray background on active links in IE 10.
|
||||
*/
|
||||
|
||||
a {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Remove the bottom border in Chrome 57-
|
||||
* 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
|
||||
*/
|
||||
|
||||
abbr[title] {
|
||||
border-bottom: none; /* 1 */
|
||||
text-decoration: underline; /* 2 */
|
||||
text-decoration: underline dotted; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font weight in Chrome, Edge, and Safari.
|
||||
*/
|
||||
|
||||
b,
|
||||
strong {
|
||||
font-weight: bolder;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inheritance and scaling of font size in all browsers.
|
||||
* 2. Correct the odd `em` font sizing in all browsers.
|
||||
*/
|
||||
|
||||
code,
|
||||
kbd,
|
||||
samp {
|
||||
font-family: monospace, monospace; /* 1 */
|
||||
font-size: 1em; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct font size in all browsers.
|
||||
*/
|
||||
|
||||
small {
|
||||
font-size: 80%;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prevent `sub` and `sup` elements from affecting the line height in
|
||||
* all browsers.
|
||||
*/
|
||||
|
||||
sub,
|
||||
sup {
|
||||
font-size: 75%;
|
||||
line-height: 0;
|
||||
position: relative;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
sub {
|
||||
bottom: -0.25em;
|
||||
}
|
||||
|
||||
sup {
|
||||
top: -0.5em;
|
||||
}
|
||||
|
||||
/* Embedded content
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Remove the border on images inside links in IE 10.
|
||||
*/
|
||||
|
||||
img {
|
||||
border-style: none;
|
||||
}
|
||||
|
||||
/* Forms
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* 1. Change the font styles in all browsers.
|
||||
* 2. Remove the margin in Firefox and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
input,
|
||||
optgroup,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit; /* 1 */
|
||||
font-size: 100%; /* 1 */
|
||||
line-height: 1.15; /* 1 */
|
||||
margin: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the overflow in IE.
|
||||
* 1. Show the overflow in Edge.
|
||||
*/
|
||||
|
||||
button,
|
||||
input { /* 1 */
|
||||
overflow: visible;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inheritance of text transform in Edge, Firefox, and IE.
|
||||
* 1. Remove the inheritance of text transform in Firefox.
|
||||
*/
|
||||
|
||||
button,
|
||||
select { /* 1 */
|
||||
text-transform: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the inability to style clickable types in iOS and Safari.
|
||||
*/
|
||||
|
||||
button,
|
||||
[type="button"],
|
||||
[type="reset"],
|
||||
[type="submit"] {
|
||||
-webkit-appearance: button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner border and padding in Firefox.
|
||||
*/
|
||||
|
||||
button::-moz-focus-inner,
|
||||
[type="button"]::-moz-focus-inner,
|
||||
[type="reset"]::-moz-focus-inner,
|
||||
[type="submit"]::-moz-focus-inner {
|
||||
border-style: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restore the focus styles unset by the previous rule.
|
||||
*/
|
||||
|
||||
button:-moz-focusring,
|
||||
[type="button"]:-moz-focusring,
|
||||
[type="reset"]:-moz-focusring,
|
||||
[type="submit"]:-moz-focusring {
|
||||
outline: 1px dotted ButtonText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the padding in Firefox.
|
||||
*/
|
||||
|
||||
fieldset {
|
||||
padding: 0.35em 0.75em 0.625em;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the text wrapping in Edge and IE.
|
||||
* 2. Correct the color inheritance from `fieldset` elements in IE.
|
||||
* 3. Remove the padding so developers are not caught out when they zero out
|
||||
* `fieldset` elements in all browsers.
|
||||
*/
|
||||
|
||||
legend {
|
||||
box-sizing: border-box; /* 1 */
|
||||
color: inherit; /* 2 */
|
||||
display: table; /* 1 */
|
||||
max-width: 100%; /* 1 */
|
||||
padding: 0; /* 3 */
|
||||
white-space: normal; /* 1 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct vertical alignment in Chrome, Firefox, and Opera.
|
||||
*/
|
||||
|
||||
progress {
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the default vertical scrollbar in IE 10+.
|
||||
*/
|
||||
|
||||
textarea {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Add the correct box sizing in IE 10.
|
||||
* 2. Remove the padding in IE 10.
|
||||
*/
|
||||
|
||||
[type="checkbox"],
|
||||
[type="radio"] {
|
||||
box-sizing: border-box; /* 1 */
|
||||
padding: 0; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Correct the cursor style of increment and decrement buttons in Chrome.
|
||||
*/
|
||||
|
||||
[type="number"]::-webkit-inner-spin-button,
|
||||
[type="number"]::-webkit-outer-spin-button {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the odd appearance in Chrome and Safari.
|
||||
* 2. Correct the outline style in Safari.
|
||||
*/
|
||||
|
||||
[type="search"] {
|
||||
-webkit-appearance: textfield; /* 1 */
|
||||
outline-offset: -2px; /* 2 */
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the inner padding in Chrome and Safari on macOS.
|
||||
*/
|
||||
|
||||
[type="search"]::-webkit-search-decoration {
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* 1. Correct the inability to style clickable types in iOS and Safari.
|
||||
* 2. Change font properties to `inherit` in Safari.
|
||||
*/
|
||||
|
||||
::-webkit-file-upload-button {
|
||||
-webkit-appearance: button; /* 1 */
|
||||
font: inherit; /* 2 */
|
||||
}
|
||||
|
||||
/* Interactive
|
||||
========================================================================== */
|
||||
|
||||
/*
|
||||
* Add the correct display in Edge, IE 10+, and Firefox.
|
||||
*/
|
||||
|
||||
details {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*
|
||||
* Add the correct display in all browsers.
|
||||
*/
|
||||
|
||||
summary {
|
||||
display: list-item;
|
||||
}
|
||||
|
||||
/* Misc
|
||||
========================================================================== */
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10+.
|
||||
*/
|
||||
|
||||
template {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the correct display in IE 10.
|
||||
*/
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@ -8,6 +8,11 @@ export { useEditExpenseItem } from './hooks/useEditExpenseItem';
|
||||
|
||||
export type { ExpenseListItem } from './types';
|
||||
|
||||
export * from './ui/AddExpenseItemForm';
|
||||
export * from './ui/DeleteExpenseItemForm';
|
||||
export * from './ui/EditExpenseItemForm';
|
||||
export type { AddExpenseItemFormProps, AddExpenseItemFormValues } from './ui/AddExpenseItemForm/AddExpenseItemForm.ui';
|
||||
export { default as AddExpenseItemForm } from './ui/AddExpenseItemForm/AddExpenseItemForm.ui';
|
||||
|
||||
export type { DeleteExpenseItemFormProps } from './ui/DeleteExpenseItemForm/DeleteExpenseItemForm.ui';
|
||||
export { default as DeleteExpenseItemForm } from './ui/DeleteExpenseItemForm/DeleteExpenseItemForm.ui';
|
||||
|
||||
export type { EditExpenseItemFormProps, EditExpenseItemFormValues } from './ui/EditExpenseItemForm/EditExpenseItemForm.ui';
|
||||
export { default as EditExpenseItemForm } from './ui/EditExpenseItemForm/EditExpenseItemForm.ui';
|
||||
|
||||
@ -1,14 +0,0 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
export const AddExpenseItemFormSchema = z.object({
|
||||
amount: z.number(),
|
||||
'bank-card': z.string().optional(),
|
||||
cash: z.string().optional(),
|
||||
count: z.number(),
|
||||
date: z.date(),
|
||||
description: z.string().trim().optional(),
|
||||
paymentType: z.enum(['BANK_CARD', 'CASH', 'none']),
|
||||
title: z.string().trim().min(5),
|
||||
});
|
||||
|
||||
export type AddExpenseItemFormValues = z.infer<typeof AddExpenseItemFormSchema>;
|
||||
@ -55,9 +55,10 @@ describe('Test AddExpenseItemForm', () => {
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
amount: 12,
|
||||
count: 1,
|
||||
date: new Date('2025-08-29T00:00:00.000Z'),
|
||||
currency: 'RUB',
|
||||
date: '2025-08-29T00:00:00.000Z',
|
||||
description: 'Описание элемента',
|
||||
paymentType: 'none',
|
||||
payment: null,
|
||||
title: 'Название элемента',
|
||||
});
|
||||
});
|
||||
@ -79,11 +80,14 @@ describe('Test AddExpenseItemForm', () => {
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
amount: 12,
|
||||
'bank-card': bankCardList[1].id,
|
||||
count: 1,
|
||||
date: new Date('2025-08-29T00:00:00.000Z'),
|
||||
currency: 'RUB',
|
||||
date: '2025-08-29T00:00:00.000Z',
|
||||
description: 'Описание элемента',
|
||||
paymentType: 'BANK_CARD',
|
||||
payment: {
|
||||
id: bankCardList[1].id,
|
||||
type: 'BANK_CARD',
|
||||
},
|
||||
title: 'Название элемента',
|
||||
});
|
||||
});
|
||||
@ -102,11 +106,14 @@ describe('Test AddExpenseItemForm', () => {
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
amount: 12,
|
||||
cash: cashList[1].id,
|
||||
count: 1,
|
||||
date: new Date('2025-08-29T00:00:00.000Z'),
|
||||
currency: 'RUB',
|
||||
date: '2025-08-29T00:00:00.000Z',
|
||||
description: 'Описание элемента',
|
||||
paymentType: 'CASH',
|
||||
payment: {
|
||||
id: cashList[1].id,
|
||||
type: 'CASH',
|
||||
},
|
||||
title: 'Название элемента',
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,27 +1,54 @@
|
||||
import type { FC, FormHTMLAttributes, RefObject } from 'react';
|
||||
import type { SelectChangeEvent } from '@mui/material';
|
||||
import type {
|
||||
FC,
|
||||
FormEventHandler,
|
||||
FormHTMLAttributes,
|
||||
RefObject,
|
||||
} from 'react';
|
||||
|
||||
import { Button, Typography } from '@mui/material';
|
||||
import { FormProvider } from 'react-hook-form';
|
||||
import {
|
||||
Button,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
TextField,
|
||||
Typography,
|
||||
} from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import type { ExpenseListItem } from '@/entity/expenses/item';
|
||||
|
||||
import { useBankCards } from '@/entity/cards';
|
||||
import { useCash } from '@/entity/cash';
|
||||
import { DataTestId } from '@/shared/constants';
|
||||
|
||||
import type { UseAddExpenseItemFormArgs } from './hooks/useAddExpenseItemForm';
|
||||
import { formatRub, getUTCDate } from '@/shared/utils';
|
||||
|
||||
import classes from './AddExpenseItemForm.module.css';
|
||||
import AmountField from './fields/AmountField';
|
||||
import BankCardField from './fields/BankCardField';
|
||||
import CashField from './fields/CashField';
|
||||
import CountField from './fields/CountField';
|
||||
import DateField from './fields/DateField';
|
||||
import DescriptionField from './fields/DescriptionField';
|
||||
import PaymentTypeField from './fields/PaymentTypeField';
|
||||
import TitleField from './fields/TitleField';
|
||||
import { useAddExpenseItemForm } from './hooks/useAddExpenseItemForm';
|
||||
|
||||
export type AddExpenseItemFormProps = BaseFormProps & UseAddExpenseItemFormArgs & {
|
||||
export type AddExpenseItemFormProps = BaseFormProps & {
|
||||
disabled?: boolean;
|
||||
onSubmit?: (data: AddExpenseItemFormValues) => Promise<void> | void;
|
||||
ref?: RefObject<HTMLFormElement>;
|
||||
};
|
||||
|
||||
export type AddExpenseItemFormValues = {
|
||||
amount: number;
|
||||
count: number;
|
||||
currency: string;
|
||||
date: string;
|
||||
description: string;
|
||||
payment: null | {
|
||||
id: string;
|
||||
type: 'BANK_CARD' | 'CASH';
|
||||
};
|
||||
title: string;
|
||||
};
|
||||
|
||||
type BaseFormProps = Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;
|
||||
|
||||
const AddExpenseItemForm: FC<AddExpenseItemFormProps> = (props) => {
|
||||
@ -32,55 +59,214 @@ const AddExpenseItemForm: FC<AddExpenseItemFormProps> = (props) => {
|
||||
...formProps
|
||||
} = props;
|
||||
|
||||
const {
|
||||
formState,
|
||||
handleSubmit,
|
||||
methods,
|
||||
onFormSubmitHandler,
|
||||
watch,
|
||||
} = useAddExpenseItemForm({ disabled, onSubmit });
|
||||
const [paymentType, setPaymentType] = useState<NonNullable<ExpenseListItem['payment']>['type']>(null);
|
||||
|
||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = (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');
|
||||
const paymentMethod = formData.get('paymentMethod');
|
||||
const bankCardId = formData.get('bank-card');
|
||||
const cashId = formData.get('cash');
|
||||
|
||||
const getPayment = () => {
|
||||
switch (paymentMethod) {
|
||||
case 'BANK_CARD':
|
||||
return {
|
||||
id: bankCardId,
|
||||
type: 'BANK_CARD',
|
||||
};
|
||||
case 'CASH':
|
||||
return {
|
||||
id: cashId,
|
||||
type: 'CASH',
|
||||
};
|
||||
case 'none':
|
||||
case null:
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
if (typeof title === 'string' && typeof date === 'string' && typeof amount === 'string' && typeof description === 'string' && typeof count === 'string') {
|
||||
onSubmit?.({
|
||||
amount: Number(amount),
|
||||
count: Number(count),
|
||||
currency: 'RUB',
|
||||
date: getUTCDate(date),
|
||||
description,
|
||||
// @ts-ignore todo
|
||||
payment: getPayment(),
|
||||
title,
|
||||
});
|
||||
}
|
||||
};
|
||||
const { data: bankCards } = useBankCards();
|
||||
const { data: cash } = useCash();
|
||||
|
||||
const bankCardOptions = bankCards?.map((card) => (
|
||||
<MenuItem key={card.id} value={card.id}>
|
||||
{`${card.name} (${formatRub(card.balance)})`}
|
||||
</MenuItem>
|
||||
));
|
||||
|
||||
const cashOptions = cash?.map((cashItem) => (
|
||||
<MenuItem key={cashItem.id} value={cashItem.id}>
|
||||
{`${cashItem.name} (${formatRub(cashItem.balance)})`}
|
||||
</MenuItem>
|
||||
));
|
||||
|
||||
const paymentOptions = useMemo(() => {
|
||||
const options = [<MenuItem key="none" value="none">Без оплаты</MenuItem>];
|
||||
|
||||
if (bankCardOptions && bankCardOptions.length > 0) {
|
||||
options.push(<MenuItem key="BANK_CARD" value="BANK_CARD">Банковская карта</MenuItem>);
|
||||
}
|
||||
|
||||
if (cashOptions && cashOptions.length > 0) {
|
||||
options.push(<MenuItem key="CASH" value="CASH">Наличные</MenuItem>);
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [bankCardOptions, cashOptions]);
|
||||
|
||||
const onChangePaymentHandler = (event: SelectChangeEvent<'BANK_CARD' | 'CASH' | 'none'>) => {
|
||||
const { value } = event.target;
|
||||
if (value === 'none') {
|
||||
setPaymentType(null);
|
||||
} else {
|
||||
// @ts-ignore todo
|
||||
setPaymentType(value);
|
||||
}
|
||||
};
|
||||
|
||||
const renderCardSelect = () => (
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="bank-card-label">Карта</InputLabel>
|
||||
|
||||
<Select
|
||||
data-testid="bank-card-select"
|
||||
defaultValue={bankCards?.[0].id}
|
||||
disabled={disabled}
|
||||
inputProps={{ 'data-testid': 'bank-card-select-input' }}
|
||||
labelId="bank-card-label"
|
||||
name="bank-card"
|
||||
size="small"
|
||||
>
|
||||
{bankCardOptions}
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
|
||||
const renderCashSelect = () => (
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="cash-label">Наличные</InputLabel>
|
||||
|
||||
<Select
|
||||
data-testid="cash-select"
|
||||
defaultValue={cash?.[0].id}
|
||||
disabled={disabled}
|
||||
inputProps={{ 'data-testid': 'cash-select-input' }}
|
||||
labelId="cash-label"
|
||||
name="cash"
|
||||
size="small"
|
||||
>
|
||||
{cashOptions ?? []}
|
||||
</Select>
|
||||
</FormControl>
|
||||
);
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
className={classes.form}
|
||||
data-testid={DataTestId.ADD_EXPENSE_ITEM_FORM}
|
||||
noValidate
|
||||
onSubmit={handleSubmit(onFormSubmitHandler)}
|
||||
onSubmit={onFormSubmitHandler}
|
||||
ref={ref}
|
||||
{...formProps}
|
||||
>
|
||||
<Typography variant="h6">
|
||||
Добавить элемент расхода
|
||||
Добавить список расходов
|
||||
</Typography>
|
||||
|
||||
<TitleField />
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label="Название"
|
||||
name="title"
|
||||
required
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<DescriptionField />
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label="Описание"
|
||||
name="description"
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<DateField />
|
||||
<DatePicker
|
||||
defaultValue={new Date()}
|
||||
disabled={disabled}
|
||||
label="Дата"
|
||||
name="date"
|
||||
slotProps={{ textField: { required: true, size: 'small' } }}
|
||||
/>
|
||||
|
||||
<AmountField />
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
inputMode="numeric"
|
||||
label="Сумма"
|
||||
name="amount"
|
||||
required
|
||||
size="small"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<CountField />
|
||||
<TextField
|
||||
defaultValue={1}
|
||||
disabled={disabled}
|
||||
inputMode="numeric"
|
||||
label="Количество"
|
||||
name="count"
|
||||
size="small"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<PaymentTypeField />
|
||||
<FormControl fullWidth>
|
||||
<InputLabel id="payment-type-label">Способ оплаты</InputLabel>
|
||||
|
||||
{watch('paymentType') === 'BANK_CARD' && <BankCardField />}
|
||||
<Select
|
||||
data-testid="payment-type-select"
|
||||
defaultValue="none"
|
||||
disabled={disabled}
|
||||
inputProps={{ 'data-testid': 'payment-type-select-input' }}
|
||||
labelId="payment-type-label"
|
||||
name="paymentMethod"
|
||||
onChange={onChangePaymentHandler}
|
||||
size="small"
|
||||
>
|
||||
{paymentOptions}
|
||||
</Select>
|
||||
</FormControl>
|
||||
|
||||
{watch('paymentType') === 'CASH' && <CashField />}
|
||||
{paymentType === 'BANK_CARD' && renderCardSelect()}
|
||||
|
||||
{paymentType === 'CASH' && renderCashSelect()}
|
||||
|
||||
<Button
|
||||
data-testid={DataTestId.ADD_EXPENSE_ITEM_SUBMIT_BUTTON}
|
||||
disabled={formState.disabled || formState.isSubmitting}
|
||||
loading={formState.isSubmitting}
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Добавить
|
||||
</Button>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
import { TextField } from '@mui/material';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema';
|
||||
|
||||
const AmountField = () => {
|
||||
const { control, formState } = useFormContext<Pick<AddExpenseItemFormValues, 'amount'>>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
disabled={formState.disabled}
|
||||
name="amount"
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
error={Boolean(formState.errors.amount)}
|
||||
helperText={formState.errors.amount?.message}
|
||||
inputMode="numeric"
|
||||
label="Сумма"
|
||||
onChange={(event) => {
|
||||
field.onChange(Number(event.target.value));
|
||||
}}
|
||||
required
|
||||
size="small"
|
||||
type="number"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AmountField;
|
||||
@ -1,58 +0,0 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
} from '@mui/material';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { useBankCards } from '@/entity/cards';
|
||||
import { formatRub } from '@/shared/utils';
|
||||
|
||||
import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema';
|
||||
|
||||
const BankCardField = () => {
|
||||
const { control, formState } = useFormContext<Pick<AddExpenseItemFormValues, 'bank-card'>>();
|
||||
|
||||
const { data: bankCards } = useBankCards();
|
||||
|
||||
const bankCardOptions = bankCards?.map((card) => (
|
||||
<MenuItem key={card.id} value={card.id}>
|
||||
{`${card.name} (${formatRub(card.balance)})`}
|
||||
</MenuItem>
|
||||
));
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue={bankCards?.[0].id}
|
||||
disabled={formState.disabled}
|
||||
name="bank-card"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
error={Boolean(formState.errors['bank-card'])}
|
||||
fullWidth
|
||||
>
|
||||
<InputLabel id="bank-card-label">Карта</InputLabel>
|
||||
|
||||
<Select
|
||||
{...field}
|
||||
data-testid="bank-card-select"
|
||||
inputProps={{ 'data-testid': 'bank-card-select-input' }}
|
||||
labelId="bank-card-label"
|
||||
size="small"
|
||||
>
|
||||
{bankCardOptions}
|
||||
</Select>
|
||||
|
||||
<FormHelperText>
|
||||
{formState.errors['bank-card']?.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BankCardField;
|
||||
@ -1,57 +0,0 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
} from '@mui/material';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { useCash } from '@/entity/cash';
|
||||
import { formatRub } from '@/shared/utils';
|
||||
|
||||
import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema';
|
||||
|
||||
const CashField = () => {
|
||||
const { control, formState } = useFormContext<Pick<AddExpenseItemFormValues, 'cash'>>();
|
||||
const { data: cash } = useCash();
|
||||
|
||||
const cashOptions = cash?.map((cashItem) => (
|
||||
<MenuItem key={cashItem.id} value={cashItem.id}>
|
||||
{`${cashItem.name} (${formatRub(cashItem.balance)})`}
|
||||
</MenuItem>
|
||||
));
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue={cash?.[0].id}
|
||||
disabled={formState.disabled}
|
||||
name="cash"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
error={Boolean(formState.errors.cash)}
|
||||
fullWidth
|
||||
>
|
||||
<InputLabel id="cash-label">Наличные</InputLabel>
|
||||
|
||||
<Select
|
||||
{...field}
|
||||
data-testid="cash-select"
|
||||
inputProps={{ 'data-testid': 'cash-select-input' }}
|
||||
labelId="cash-label"
|
||||
size="small"
|
||||
>
|
||||
{cashOptions}
|
||||
</Select>
|
||||
|
||||
<FormHelperText>
|
||||
{formState.errors.cash?.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CashField;
|
||||
@ -1,33 +0,0 @@
|
||||
import { TextField } from '@mui/material';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema';
|
||||
|
||||
const CountField = () => {
|
||||
const { control, formState } = useFormContext<Pick<AddExpenseItemFormValues, 'count'>>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
disabled={formState.disabled}
|
||||
name="count"
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
defaultValue={1}
|
||||
error={Boolean(formState.errors.count)}
|
||||
helperText={formState.errors.count?.message}
|
||||
inputMode="numeric"
|
||||
label="Количество"
|
||||
onChange={(event) => {
|
||||
field.onChange(Number(event.target.value));
|
||||
}}
|
||||
size="small"
|
||||
type="number"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CountField;
|
||||
@ -1,37 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema';
|
||||
|
||||
const DateField: FC = () => {
|
||||
const { control, formState } = useFormContext<Pick<AddExpenseItemFormValues, 'date'>>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
disabled={formState.disabled}
|
||||
name="date"
|
||||
render={({ field }) => (
|
||||
<DatePicker
|
||||
{...field}
|
||||
disableFuture
|
||||
format="dd.MM.yyyy"
|
||||
label="Дата"
|
||||
slotProps={{
|
||||
textField: {
|
||||
error: Boolean(formState.errors.date),
|
||||
helperText: formState.errors.date?.message,
|
||||
required: true,
|
||||
size: 'small',
|
||||
},
|
||||
}}
|
||||
timezone="UTC"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateField;
|
||||
@ -1,29 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { TextField } from '@mui/material';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema';
|
||||
|
||||
const DescriptionField: FC = () => {
|
||||
const { control, formState } = useFormContext<Pick<AddExpenseItemFormValues, 'description'>>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
disabled={formState.disabled}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
error={Boolean(formState.errors.description)}
|
||||
helperText={formState.errors.description?.message}
|
||||
label="Описание"
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DescriptionField;
|
||||
@ -1,69 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import {
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
} from '@mui/material';
|
||||
import { useMemo } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { useBankCards } from '@/entity/cards';
|
||||
import { useCash } from '@/entity/cash';
|
||||
|
||||
import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema';
|
||||
|
||||
const PaymentTypeField: FC = () => {
|
||||
const { control, formState } = useFormContext<Pick<AddExpenseItemFormValues, 'paymentType'>>();
|
||||
|
||||
const { data: bankCards } = useBankCards();
|
||||
const { data: cash } = useCash();
|
||||
|
||||
const paymentTypeOptions = useMemo(() => {
|
||||
const options = [<MenuItem key="none" value="none">Без оплаты</MenuItem>];
|
||||
|
||||
if (bankCards && bankCards.length > 0) {
|
||||
options.push(<MenuItem key="BANK_CARD" value="BANK_CARD">Банковская карта</MenuItem>);
|
||||
}
|
||||
|
||||
if (cash && cash.length > 0) {
|
||||
options.push(<MenuItem key="CASH" value="CASH">Наличные</MenuItem>);
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [bankCards, cash]);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
disabled={formState.disabled}
|
||||
name="paymentType"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
error={Boolean(formState.errors.paymentType)}
|
||||
fullWidth
|
||||
>
|
||||
<InputLabel id="payment-type-label">Способ оплаты</InputLabel>
|
||||
|
||||
<Select
|
||||
{...field}
|
||||
data-testid="payment-type-select"
|
||||
inputProps={{ 'data-testid': 'payment-type-select-input' }}
|
||||
labelId="payment-type-label"
|
||||
size="small"
|
||||
>
|
||||
{paymentTypeOptions}
|
||||
</Select>
|
||||
|
||||
<FormHelperText>
|
||||
{formState.errors.paymentType?.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentTypeField;
|
||||
@ -1,30 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { TextField } from '@mui/material';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema';
|
||||
|
||||
const TitleField: FC = () => {
|
||||
const { control, formState } = useFormContext<Pick<AddExpenseItemFormValues, 'title'>>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
disabled={formState.disabled}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
error={Boolean(formState.errors.title)}
|
||||
helperText={formState.errors.title?.message}
|
||||
label="Название"
|
||||
required
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TitleField;
|
||||
@ -1,48 +0,0 @@
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import type { AddExpenseItemFormValues } from '../AddExpenseItemForm.schema';
|
||||
|
||||
import { AddExpenseItemFormSchema } from '../AddExpenseItemForm.schema';
|
||||
|
||||
export type UseAddExpenseItemFormArgs = {
|
||||
disabled?: boolean;
|
||||
onSubmit: (data: AddExpenseItemFormValues) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const useAddExpenseItemForm = (args: UseAddExpenseItemFormArgs) => {
|
||||
const { disabled, onSubmit } = args;
|
||||
|
||||
const methods = useForm<AddExpenseItemFormValues>({
|
||||
defaultValues: {
|
||||
amount: 0,
|
||||
count: 1,
|
||||
date: new Date(Date.now()),
|
||||
description: '',
|
||||
paymentType: 'none',
|
||||
title: '',
|
||||
},
|
||||
disabled,
|
||||
resolver: zodResolver(AddExpenseItemFormSchema),
|
||||
shouldUnregister: true,
|
||||
});
|
||||
const {
|
||||
formState,
|
||||
handleSubmit,
|
||||
watch,
|
||||
} = methods;
|
||||
|
||||
const onFormSubmitHandler: SubmitHandler<AddExpenseItemFormValues> = async (data) => {
|
||||
await onSubmit(data);
|
||||
};
|
||||
|
||||
return {
|
||||
formState,
|
||||
handleSubmit,
|
||||
methods,
|
||||
onFormSubmitHandler,
|
||||
watch,
|
||||
};
|
||||
};
|
||||
@ -1,5 +0,0 @@
|
||||
export type { AddExpenseItemFormValues } from './AddExpenseItemForm.schema';
|
||||
export { AddExpenseItemFormSchema } from './AddExpenseItemForm.schema';
|
||||
|
||||
export type { AddExpenseItemFormProps } from './AddExpenseItemForm.ui';
|
||||
export { default as AddExpenseItemForm } from './AddExpenseItemForm.ui';
|
||||
@ -13,6 +13,6 @@ describe('Test DeleteExpenseItemForm', () => {
|
||||
await DeleteExpenseItemFormHelper.submitForm();
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit).toHaveBeenCalledWith();
|
||||
expect(onSubmit).toHaveBeenCalledWith(DeleteExpenseItemFormHelper.expenseItem);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,14 +1,20 @@
|
||||
import type { FC, FormHTMLAttributes, RefObject } from 'react';
|
||||
import type {
|
||||
FC,
|
||||
FormEventHandler,
|
||||
FormHTMLAttributes,
|
||||
RefObject,
|
||||
} from 'react';
|
||||
|
||||
import { Button, Typography } from '@mui/material';
|
||||
|
||||
import { DataTestId } from '@/shared/constants';
|
||||
|
||||
import type { UseDeleteExpenseItemFormArgs } from './hooks/useDeleteExpenseItemForm';
|
||||
import type { ExpenseListItem } from '../../types';
|
||||
|
||||
import { useDeleteExpenseItemForm } from './hooks/useDeleteExpenseItemForm';
|
||||
|
||||
export type DeleteExpenseItemFormProps = BaseFormProps & UseDeleteExpenseItemFormArgs & {
|
||||
export type DeleteExpenseItemFormProps = BaseFormProps & {
|
||||
disabled?: boolean;
|
||||
item: ExpenseListItem;
|
||||
onSubmit?: (item: ExpenseListItem) => Promise<void> | void;
|
||||
ref?: RefObject<HTMLFormElement>;
|
||||
};
|
||||
|
||||
@ -17,17 +23,20 @@ type BaseFormProps = Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;
|
||||
const DeleteExpenseItemForm: FC<DeleteExpenseItemFormProps> = (props) => {
|
||||
const {
|
||||
disabled,
|
||||
item,
|
||||
onSubmit,
|
||||
ref,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const { formState, handleSubmit, onFormSubmitHandler } = useDeleteExpenseItemForm({ disabled, onSubmit });
|
||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = (event) => {
|
||||
event.preventDefault();
|
||||
onSubmit?.(item);
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
data-testid={DataTestId.DELETE_EXPENSE_ITEM_FORM}
|
||||
onSubmit={handleSubmit(onFormSubmitHandler)}
|
||||
onSubmit={onFormSubmitHandler}
|
||||
ref={ref}
|
||||
{...restProps}
|
||||
>
|
||||
@ -38,8 +47,9 @@ const DeleteExpenseItemForm: FC<DeleteExpenseItemFormProps> = (props) => {
|
||||
<Button
|
||||
color="error"
|
||||
data-testid={DataTestId.DELETE_EXPENSE_ITEM_SUBMIT_BUTTON}
|
||||
disabled={formState.disabled || formState.isSubmitting}
|
||||
loading={formState.isSubmitting}
|
||||
disabled={disabled}
|
||||
fullWidth
|
||||
sx={{ mt: 2 }}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
export type UseDeleteExpenseItemFormArgs = {
|
||||
disabled?: boolean;
|
||||
onSubmit: () => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const useDeleteExpenseItemForm = (args: UseDeleteExpenseItemFormArgs) => {
|
||||
const { disabled, onSubmit } = args;
|
||||
|
||||
const onFormSubmitHandler: SubmitHandler<never> = async () => {
|
||||
await onSubmit();
|
||||
};
|
||||
|
||||
const methods = useForm<never>({
|
||||
disabled,
|
||||
shouldUnregister: true,
|
||||
});
|
||||
const { formState, handleSubmit } = methods;
|
||||
|
||||
return {
|
||||
formState,
|
||||
handleSubmit,
|
||||
onFormSubmitHandler,
|
||||
};
|
||||
};
|
||||
@ -1,2 +0,0 @@
|
||||
export type { DeleteExpenseItemFormProps } from './DeleteExpenseItemForm.ui';
|
||||
export { default as DeleteExpenseItemForm } from './DeleteExpenseItemForm.ui';
|
||||
@ -1,14 +0,0 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
export const EditExpenseItemFormSchema = z.object({
|
||||
amount: z.number(),
|
||||
'bank-card': z.string().optional(),
|
||||
cash: z.string().optional(),
|
||||
count: z.number(),
|
||||
date: z.date(),
|
||||
description: z.string().trim().optional(),
|
||||
paymentType: z.enum(['BANK_CARD', 'CASH', 'none']),
|
||||
title: z.string().trim().min(5),
|
||||
});
|
||||
|
||||
export type EditExpenseItemFormValues = z.infer<typeof EditExpenseItemFormSchema>;
|
||||
@ -20,7 +20,7 @@ describe('Test EditExpenseItemForm', () => {
|
||||
expect(titleInputElem).toHaveValue('Mock item');
|
||||
expect(descriptionInputElem).toHaveValue('Mock description');
|
||||
expect(dateInputElem).toHaveValue('22.08.2025');
|
||||
expect(amountInputElem).toHaveValue(12);
|
||||
expect(amountInputElem).toHaveValue('12');
|
||||
expect(countInputElem).toHaveValue(12);
|
||||
});
|
||||
|
||||
@ -29,14 +29,8 @@ describe('Test EditExpenseItemForm', () => {
|
||||
['disabled', { disabled: true }, (el: HTMLElement) => { expect(el).toBeDisabled(); }],
|
||||
])('should render fields %s', (_state, options, assertion) => {
|
||||
EditExpenseItemFormHelper.renderForm(options);
|
||||
const elems = EditExpenseItemFormHelper.getFormElements();
|
||||
Object.values([
|
||||
elems.amountInputElem,
|
||||
elems.dateInputElem,
|
||||
elems.descriptionInputElem,
|
||||
elems.submitButtonElem,
|
||||
elems.titleInputElem,
|
||||
]).forEach(assertion);
|
||||
const { formElem, ...formElems } = EditExpenseItemFormHelper.getFormElements();
|
||||
Object.values(formElems).forEach(assertion);
|
||||
});
|
||||
|
||||
test('should call onSubmit with values', async () => {
|
||||
@ -47,7 +41,6 @@ describe('Test EditExpenseItemForm', () => {
|
||||
count: 1,
|
||||
date: '29.08.2025',
|
||||
description: 'Описание элемента',
|
||||
paymentType: 'none',
|
||||
title: 'Название элемента',
|
||||
});
|
||||
|
||||
@ -55,9 +48,9 @@ describe('Test EditExpenseItemForm', () => {
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
amount: 12,
|
||||
count: 1,
|
||||
date: new Date('2025-08-29T00:00:00.000Z'),
|
||||
currency: 'RUB',
|
||||
date: '2025-08-29T00:00:00.000Z',
|
||||
description: 'Описание элемента',
|
||||
paymentType: 'none',
|
||||
title: 'Название элемента',
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,31 +1,35 @@
|
||||
import type {
|
||||
FC,
|
||||
FormEventHandler,
|
||||
FormHTMLAttributes,
|
||||
RefObject,
|
||||
} from 'react';
|
||||
|
||||
import { Button, Typography } from '@mui/material';
|
||||
import { FormProvider } from 'react-hook-form';
|
||||
import { Button, TextField, Typography } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
|
||||
import { DataTestId } from '@/shared/constants';
|
||||
import { getUTCDate } from '@/shared/utils';
|
||||
|
||||
import type { UseEditExpenseItemFormArgs } from './hooks/useEditExpenseItemForm';
|
||||
import type { ExpenseListItem } from '../../types';
|
||||
|
||||
import classes from './EditExpenseItemForm.module.css';
|
||||
import AmountField from './fields/AmountField';
|
||||
import BankCardField from './fields/BankCardField';
|
||||
import CashField from './fields/CashField';
|
||||
import CountField from './fields/CountField';
|
||||
import DateField from './fields/DateField';
|
||||
import DescriptionField from './fields/DescriptionField';
|
||||
import PaymentTypeField from './fields/PaymentTypeField';
|
||||
import TitleField from './fields/TitleField';
|
||||
import { useEditExpenseItemForm } from './hooks/useEditExpenseItemForm';
|
||||
|
||||
export type EditExpenseItemFormProps = BaseFormProps & UseEditExpenseItemFormArgs & {
|
||||
export type EditExpenseItemFormProps = BaseFormProps & {
|
||||
disabled?: boolean;
|
||||
item: ExpenseListItem;
|
||||
onSubmit?: (data: EditExpenseItemFormValues) => Promise<void> | void;
|
||||
ref?: RefObject<HTMLFormElement>;
|
||||
};
|
||||
|
||||
export type EditExpenseItemFormValues = {
|
||||
amount: number;
|
||||
count: number;
|
||||
currency: string;
|
||||
date: string;
|
||||
description: string;
|
||||
title: string;
|
||||
};
|
||||
type BaseFormProps = Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;
|
||||
|
||||
const EditExpenseItemForm: FC<EditExpenseItemFormProps> = (props) => {
|
||||
@ -37,55 +41,94 @@ const EditExpenseItemForm: FC<EditExpenseItemFormProps> = (props) => {
|
||||
...formProps
|
||||
} = props;
|
||||
|
||||
const {
|
||||
formState,
|
||||
handleSubmit,
|
||||
methods,
|
||||
onFormSubmitHandler,
|
||||
watch,
|
||||
} = useEditExpenseItemForm({ disabled, item, onSubmit });
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
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 onSubmit?.({
|
||||
amount: Number(amount),
|
||||
count: Number(count),
|
||||
currency: 'RUB',
|
||||
date: getUTCDate(date),
|
||||
description,
|
||||
title,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
className={classes.form}
|
||||
data-testid={DataTestId.EDIT_EXPENSE_ITEM_FORM}
|
||||
noValidate
|
||||
onSubmit={handleSubmit(onFormSubmitHandler)}
|
||||
onSubmit={onFormSubmitHandler}
|
||||
ref={ref}
|
||||
{...formProps}
|
||||
>
|
||||
<Typography variant="h6">
|
||||
Редактировать элемент расхода
|
||||
Добавить список расходов
|
||||
</Typography>
|
||||
|
||||
<TitleField />
|
||||
<TextField
|
||||
defaultValue={item.title}
|
||||
disabled={disabled}
|
||||
label="Название"
|
||||
name="title"
|
||||
required
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<DescriptionField />
|
||||
<TextField
|
||||
defaultValue={item.description ?? ''}
|
||||
disabled={disabled}
|
||||
label="Описание"
|
||||
name="description"
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<DateField />
|
||||
<DatePicker
|
||||
defaultValue={new Date(item.date)}
|
||||
disabled={disabled}
|
||||
label="Дата"
|
||||
name="date"
|
||||
slotProps={{ textField: { required: true, size: 'small' } }}
|
||||
/>
|
||||
|
||||
<AmountField />
|
||||
<TextField
|
||||
defaultValue={item.amount}
|
||||
disabled={disabled}
|
||||
inputMode="numeric"
|
||||
label="Сумма"
|
||||
name="amount"
|
||||
required
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<CountField />
|
||||
|
||||
<PaymentTypeField />
|
||||
|
||||
{watch('paymentType') === 'BANK_CARD' && <BankCardField />}
|
||||
|
||||
{watch('paymentType') === 'CASH' && <CashField />}
|
||||
<TextField
|
||||
defaultValue={item.count ?? 1}
|
||||
disabled={disabled}
|
||||
label="Количество"
|
||||
name="count"
|
||||
size="small"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<Button
|
||||
data-testid={DataTestId.EDIT_EXPENSE_ITEM_SUBMIT_BUTTON}
|
||||
disabled={formState.disabled || formState.isSubmitting}
|
||||
loading={formState.isSubmitting}
|
||||
disabled={disabled}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
import { TextField } from '@mui/material';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema';
|
||||
|
||||
const AmountField = () => {
|
||||
const { control, formState } = useFormContext<Pick<EditExpenseItemFormValues, 'amount'>>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
disabled={formState.disabled}
|
||||
name="amount"
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
error={Boolean(formState.errors.amount)}
|
||||
helperText={formState.errors.amount?.message}
|
||||
inputMode="numeric"
|
||||
label="Сумма"
|
||||
onChange={(event) => {
|
||||
field.onChange(Number(event.target.value));
|
||||
}}
|
||||
required
|
||||
size="small"
|
||||
type="number"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AmountField;
|
||||
@ -1,58 +0,0 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
} from '@mui/material';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { useBankCards } from '@/entity/cards';
|
||||
import { formatRub } from '@/shared/utils';
|
||||
|
||||
import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema';
|
||||
|
||||
const BankCardField = () => {
|
||||
const { control, formState } = useFormContext<Pick<EditExpenseItemFormValues, 'bank-card'>>();
|
||||
|
||||
const { data: bankCards } = useBankCards();
|
||||
|
||||
const bankCardOptions = bankCards?.map((card) => (
|
||||
<MenuItem key={card.id} value={card.id}>
|
||||
{`${card.name} (${formatRub(card.balance)})`}
|
||||
</MenuItem>
|
||||
));
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue={bankCards?.[0].id}
|
||||
disabled={formState.disabled}
|
||||
name="bank-card"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
error={Boolean(formState.errors['bank-card'])}
|
||||
fullWidth
|
||||
>
|
||||
<InputLabel id="bank-card-label">Карта</InputLabel>
|
||||
|
||||
<Select
|
||||
{...field}
|
||||
data-testid="bank-card-select"
|
||||
inputProps={{ 'data-testid': 'bank-card-select-input' }}
|
||||
labelId="bank-card-label"
|
||||
size="small"
|
||||
>
|
||||
{bankCardOptions}
|
||||
</Select>
|
||||
|
||||
<FormHelperText>
|
||||
{formState.errors['bank-card']?.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BankCardField;
|
||||
@ -1,57 +0,0 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
} from '@mui/material';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { useCash } from '@/entity/cash';
|
||||
import { formatRub } from '@/shared/utils';
|
||||
|
||||
import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema';
|
||||
|
||||
const CashField = () => {
|
||||
const { control, formState } = useFormContext<Pick<EditExpenseItemFormValues, 'cash'>>();
|
||||
const { data: cash } = useCash();
|
||||
|
||||
const cashOptions = cash?.map((cashItem) => (
|
||||
<MenuItem key={cashItem.id} value={cashItem.id}>
|
||||
{`${cashItem.name} (${formatRub(cashItem.balance)})`}
|
||||
</MenuItem>
|
||||
));
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue={cash?.[0].id}
|
||||
disabled={formState.disabled}
|
||||
name="cash"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
error={Boolean(formState.errors.cash)}
|
||||
fullWidth
|
||||
>
|
||||
<InputLabel id="cash-label">Наличные</InputLabel>
|
||||
|
||||
<Select
|
||||
{...field}
|
||||
data-testid="cash-select"
|
||||
inputProps={{ 'data-testid': 'cash-select-input' }}
|
||||
labelId="cash-label"
|
||||
size="small"
|
||||
>
|
||||
{cashOptions}
|
||||
</Select>
|
||||
|
||||
<FormHelperText>
|
||||
{formState.errors.cash?.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CashField;
|
||||
@ -1,33 +0,0 @@
|
||||
import { TextField } from '@mui/material';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema';
|
||||
|
||||
const CountField = () => {
|
||||
const { control, formState } = useFormContext<Pick<EditExpenseItemFormValues, 'count'>>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
disabled={formState.disabled}
|
||||
name="count"
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
defaultValue={1}
|
||||
error={Boolean(formState.errors.count)}
|
||||
helperText={formState.errors.count?.message}
|
||||
inputMode="numeric"
|
||||
label="Количество"
|
||||
onChange={(event) => {
|
||||
field.onChange(Number(event.target.value));
|
||||
}}
|
||||
size="small"
|
||||
type="number"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CountField;
|
||||
@ -1,36 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema';
|
||||
|
||||
const DateField: FC = () => {
|
||||
const { control, formState } = useFormContext<Pick<EditExpenseItemFormValues, 'date'>>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
disabled={formState.disabled}
|
||||
name="date"
|
||||
render={({ field }) => (
|
||||
<DatePicker
|
||||
{...field}
|
||||
disableFuture
|
||||
format="dd.MM.yyyy"
|
||||
label="Дата"
|
||||
slotProps={{
|
||||
textField: {
|
||||
error: Boolean(formState.errors.date),
|
||||
helperText: formState.errors.date?.message,
|
||||
required: true,
|
||||
size: 'small',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateField;
|
||||
@ -1,29 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { TextField } from '@mui/material';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema';
|
||||
|
||||
const DescriptionField: FC = () => {
|
||||
const { control, formState } = useFormContext<Pick<EditExpenseItemFormValues, 'description'>>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
disabled={formState.disabled}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
error={Boolean(formState.errors.description)}
|
||||
helperText={formState.errors.description?.message}
|
||||
label="Описание"
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DescriptionField;
|
||||
@ -1,69 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import {
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
} from '@mui/material';
|
||||
import { useMemo } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { useBankCards } from '@/entity/cards';
|
||||
import { useCash } from '@/entity/cash';
|
||||
|
||||
import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema';
|
||||
|
||||
const PaymentTypeField: FC = () => {
|
||||
const { control, formState } = useFormContext<Pick<EditExpenseItemFormValues, 'paymentType'>>();
|
||||
|
||||
const { data: bankCards } = useBankCards();
|
||||
const { data: cash } = useCash();
|
||||
|
||||
const paymentTypeOptions = useMemo(() => {
|
||||
const options = [<MenuItem key="none" value="none">Без оплаты</MenuItem>];
|
||||
|
||||
if (bankCards && bankCards.length > 0) {
|
||||
options.push(<MenuItem key="BANK_CARD" value="BANK_CARD">Банковская карта</MenuItem>);
|
||||
}
|
||||
|
||||
if (cash && cash.length > 0) {
|
||||
options.push(<MenuItem key="CASH" value="CASH">Наличные</MenuItem>);
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [bankCards, cash]);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
disabled={formState.disabled}
|
||||
name="paymentType"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
error={Boolean(formState.errors.paymentType)}
|
||||
fullWidth
|
||||
>
|
||||
<InputLabel id="payment-type-label">Способ оплаты</InputLabel>
|
||||
|
||||
<Select
|
||||
{...field}
|
||||
data-testid="payment-type-select"
|
||||
inputProps={{ 'data-testid': 'payment-type-select-input' }}
|
||||
labelId="payment-type-label"
|
||||
size="small"
|
||||
>
|
||||
{paymentTypeOptions}
|
||||
</Select>
|
||||
|
||||
<FormHelperText>
|
||||
{formState.errors.paymentType?.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default PaymentTypeField;
|
||||
@ -1,30 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { TextField } from '@mui/material';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema';
|
||||
|
||||
const TitleField: FC = () => {
|
||||
const { control, formState } = useFormContext<Pick<EditExpenseItemFormValues, 'title'>>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
disabled={formState.disabled}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
error={Boolean(formState.errors.title)}
|
||||
helperText={formState.errors.title?.message}
|
||||
label="Название"
|
||||
required
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TitleField;
|
||||
@ -1,55 +0,0 @@
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import type { ExpenseListItem } from '@/entity/expenses/item';
|
||||
|
||||
import type { EditExpenseItemFormValues } from '../EditExpenseItemForm.schema';
|
||||
|
||||
import { EditExpenseItemFormSchema } from '../EditExpenseItemForm.schema';
|
||||
|
||||
export type UseEditExpenseItemFormArgs = {
|
||||
disabled?: boolean;
|
||||
item: ExpenseListItem;
|
||||
onSubmit: (data: EditExpenseItemFormValues) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const useEditExpenseItemForm = (args: UseEditExpenseItemFormArgs) => {
|
||||
const { disabled, item, onSubmit } = args;
|
||||
|
||||
const methods = useForm<EditExpenseItemFormValues>({
|
||||
defaultValues: {
|
||||
amount: item.amount,
|
||||
// eslint-disable-next-line no-undefined
|
||||
'bank-card': item.payment?.type === 'BANK_CARD' ? item.payment.id ?? undefined : undefined,
|
||||
// eslint-disable-next-line no-undefined
|
||||
cash: item.payment?.type === 'CASH' ? item.payment.id ?? undefined : undefined,
|
||||
count: item.count ?? 1,
|
||||
date: new Date(item.date),
|
||||
description: item.description ?? '',
|
||||
paymentType: item.payment?.type ?? 'none',
|
||||
title: item.title,
|
||||
},
|
||||
disabled,
|
||||
resolver: zodResolver(EditExpenseItemFormSchema),
|
||||
shouldUnregister: true,
|
||||
});
|
||||
const {
|
||||
formState,
|
||||
handleSubmit,
|
||||
watch,
|
||||
} = methods;
|
||||
|
||||
const onFormSubmitHandler: SubmitHandler<EditExpenseItemFormValues> = async (data) => {
|
||||
await onSubmit(data);
|
||||
};
|
||||
|
||||
return {
|
||||
formState,
|
||||
handleSubmit,
|
||||
methods,
|
||||
onFormSubmitHandler,
|
||||
watch,
|
||||
};
|
||||
};
|
||||
@ -1,4 +0,0 @@
|
||||
export type { EditExpenseItemFormValues } from './EditExpenseItemForm.schema';
|
||||
export { EditExpenseItemFormSchema } from './EditExpenseItemForm.schema';
|
||||
export type { EditExpenseItemFormProps } from './EditExpenseItemForm.ui';
|
||||
export { default as EditExpenseItemForm } from './EditExpenseItemForm.ui';
|
||||
@ -7,7 +7,13 @@ export { useDeleteIncomeItem } from './hooks/useDeleteIncomeItem';
|
||||
export { useEditIncomeItem } from './hooks/useEditIncomeItem';
|
||||
|
||||
export type { IncomeListItem } from './types';
|
||||
export type { AddIncomeItemFormValues } from './ui/AddIncomeItemForm/AddIncomeItemForm.ui';
|
||||
|
||||
export * from './ui/AddIncomeItemForm';
|
||||
export * from './ui/DeleteIncomeItemForm';
|
||||
export * from './ui/EditIncomeItemForm';
|
||||
export type { AddIncomeItemFormProps } from './ui/AddIncomeItemForm/AddIncomeItemForm.ui';
|
||||
export { default as AddIncomeItemForm } from './ui/AddIncomeItemForm/AddIncomeItemForm.ui';
|
||||
export type { DeleteIncomeItemFormProps } from './ui/DeleteIncomeItemForm/DeleteIncomeItemForm.ui';
|
||||
export { default as DeleteIncomeItemForm } from './ui/DeleteIncomeItemForm/DeleteIncomeItemForm.ui';
|
||||
|
||||
export type { EditIncomeItemFormProps, EditIncomeItemFormValues } from './ui/EditIncomeItemForm/EditIncomeItemForm.ui';
|
||||
|
||||
export { default as EditIncomeItemForm } from './ui/EditIncomeItemForm/EditIncomeItemForm.ui';
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
.form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
@ -1,13 +0,0 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
export const AddIncomeItemFormSchema = z.object({
|
||||
amount: z.number(),
|
||||
'bank-card': z.string().optional(),
|
||||
cash: z.string().optional(),
|
||||
date: z.date(),
|
||||
description: z.string().trim().optional(),
|
||||
targetType: z.enum(['BANK_CARD', 'CASH', 'none']),
|
||||
title: z.string().trim().min(5),
|
||||
});
|
||||
|
||||
export type AddIncomeItemFormValues = z.infer<typeof AddIncomeItemFormSchema>;
|
||||
@ -1,4 +1,3 @@
|
||||
import { bankCardList, cashList } from '@mocks/browser/data';
|
||||
import { AddIncomeItemFormHelper } from '@tests/helpers';
|
||||
|
||||
describe('Test AddIncomeItemForm', () => {
|
||||
@ -14,95 +13,27 @@ describe('Test AddIncomeItemForm', () => {
|
||||
['disabled', { disabled: true }, (el: HTMLElement) => { expect(el).toBeDisabled(); }],
|
||||
])('should render fields %s', (_state, options, assertion) => {
|
||||
AddIncomeItemFormHelper.renderForm(options);
|
||||
const elems = AddIncomeItemFormHelper.getFormElements();
|
||||
Object.values([
|
||||
elems.amountInputElem,
|
||||
elems.dateInputElem,
|
||||
elems.descriptionInputElem,
|
||||
elems.submitButtonElem,
|
||||
elems.titleInputElem,
|
||||
]).forEach(assertion);
|
||||
const { formElem, ...formElems } = AddIncomeItemFormHelper.getFormElements();
|
||||
Object.values(formElems).forEach(assertion);
|
||||
});
|
||||
|
||||
describe('should call onSubmit with values', () => {
|
||||
const setup = async () => {
|
||||
test('should call onSubmit with values', async () => {
|
||||
const { onSubmit } = AddIncomeItemFormHelper.renderForm();
|
||||
|
||||
await AddIncomeItemFormHelper.fillForm(
|
||||
{
|
||||
await AddIncomeItemFormHelper.fillForm({
|
||||
amount: 12,
|
||||
date: '29.08.2025',
|
||||
description: 'Описание элемента',
|
||||
targetType: 'none',
|
||||
title: 'Название элемента',
|
||||
},
|
||||
{ autoSubmit: false },
|
||||
);
|
||||
|
||||
return {
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
test('no payment', async () => {
|
||||
const { onSubmit } = await setup();
|
||||
await AddIncomeItemFormHelper.submitForm();
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
amount: 12,
|
||||
date: new Date('2025-08-29T00:00:00.000Z'),
|
||||
currency: 'RUB',
|
||||
date: '2025-08-29T00:00:00.000Z',
|
||||
description: 'Описание элемента',
|
||||
targetType: 'none',
|
||||
title: 'Название элемента',
|
||||
});
|
||||
});
|
||||
|
||||
test('bank card payment', async () => {
|
||||
const { onSubmit } = await setup();
|
||||
await AddIncomeItemFormHelper.fillForm({ targetType: 'BANK_CARD' }, { autoSubmit: false });
|
||||
|
||||
const { bankCardSelect, bankCardSelectInput } = AddIncomeItemFormHelper.getFormElements();
|
||||
expect(bankCardSelect).toBeVisible();
|
||||
expect(bankCardSelectInput).toBeEnabled();
|
||||
expect(bankCardSelectInput).toHaveValue(bankCardList[0].id);
|
||||
|
||||
await AddIncomeItemFormHelper.fillForm({ 'bank-card': bankCardList[1].id, targetType: 'BANK_CARD' }, { autoSubmit: false });
|
||||
expect(bankCardSelectInput).toHaveValue(bankCardList[1].id);
|
||||
|
||||
await AddIncomeItemFormHelper.submitForm();
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
amount: 12,
|
||||
'bank-card': bankCardList[1].id,
|
||||
date: new Date('2025-08-29T00:00:00.000Z'),
|
||||
description: 'Описание элемента',
|
||||
targetType: 'BANK_CARD',
|
||||
title: 'Название элемента',
|
||||
});
|
||||
});
|
||||
|
||||
test('cash payment', async () => {
|
||||
const { onSubmit } = await setup();
|
||||
|
||||
await AddIncomeItemFormHelper.fillForm({ targetType: 'CASH' });
|
||||
const { cashSelect, cashSelectInput } = AddIncomeItemFormHelper.getFormElements();
|
||||
expect(cashSelect).toBeVisible();
|
||||
expect(cashSelectInput).toBeEnabled();
|
||||
expect(cashSelectInput).toHaveValue(cashList[0].id);
|
||||
|
||||
await AddIncomeItemFormHelper.fillForm({ cash: cashList[1].id, targetType: 'CASH' });
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
amount: 12,
|
||||
cash: cashList[1].id,
|
||||
date: new Date('2025-08-29T00:00:00.000Z'),
|
||||
description: 'Описание элемента',
|
||||
targetType: 'CASH',
|
||||
title: 'Название элемента',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,30 +1,32 @@
|
||||
import type {
|
||||
FC,
|
||||
FormEventHandler,
|
||||
FormHTMLAttributes,
|
||||
RefObject,
|
||||
} from 'react';
|
||||
|
||||
import { Button, Typography } from '@mui/material';
|
||||
import { FormProvider } from 'react-hook-form';
|
||||
import { Button, TextField, Typography } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
|
||||
import { DataTestId } from '@/shared/constants';
|
||||
|
||||
import type { UseAddIncomeItemFormArgs } from './hooks/useAddIncomeItemForm';
|
||||
import { getUTCDate } from '@/shared/utils';
|
||||
|
||||
import classes from './AddIncomeItemForm.module.css';
|
||||
import AmountField from './fields/AmountField';
|
||||
import BankCardField from './fields/BankCardField';
|
||||
import CashField from './fields/CashField';
|
||||
import DateField from './fields/DateField';
|
||||
import DescriptionField from './fields/DescriptionField';
|
||||
import TargetTypeField from './fields/TargetTypeField';
|
||||
import TitleField from './fields/TitleField';
|
||||
import { useAddIncomeItemForm } from './hooks/useAddIncomeItemForm';
|
||||
|
||||
export type AddIncomeItemFormProps = BaseFormProps & UseAddIncomeItemFormArgs & {
|
||||
export type AddIncomeItemFormProps = BaseFormProps & {
|
||||
disabled?: boolean;
|
||||
onSubmit?: (data: AddIncomeItemFormValues) => Promise<void> | void;
|
||||
ref?: RefObject<HTMLFormElement>;
|
||||
};
|
||||
|
||||
export type AddIncomeItemFormValues = {
|
||||
amount: number;
|
||||
currency: string;
|
||||
date: string;
|
||||
description: string;
|
||||
title: string;
|
||||
};
|
||||
|
||||
type BaseFormProps = Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;
|
||||
|
||||
const AddIncomeItemForm: FC<AddIncomeItemFormProps> = (props) => {
|
||||
@ -35,53 +37,82 @@ const AddIncomeItemForm: FC<AddIncomeItemFormProps> = (props) => {
|
||||
...formProps
|
||||
} = props;
|
||||
|
||||
const {
|
||||
formState,
|
||||
handleSubmit,
|
||||
methods,
|
||||
onFormSubmitHandler,
|
||||
watch,
|
||||
} = useAddIncomeItemForm({ disabled, onSubmit });
|
||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = (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');
|
||||
|
||||
if (typeof title === 'string' && typeof date === 'string' && typeof amount === 'string' && typeof description === 'string') {
|
||||
onSubmit?.({
|
||||
amount: Number(amount),
|
||||
currency: 'RUB',
|
||||
date: getUTCDate(date),
|
||||
description,
|
||||
title,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
className={classes.form}
|
||||
data-testid={DataTestId.ADD_INCOME_ITEM_FORM}
|
||||
noValidate
|
||||
onSubmit={handleSubmit(onFormSubmitHandler)}
|
||||
onSubmit={onFormSubmitHandler}
|
||||
ref={ref}
|
||||
{...formProps}
|
||||
>
|
||||
<Typography variant="h6">
|
||||
Добавить элемент дохода
|
||||
Добавить список доходов
|
||||
</Typography>
|
||||
|
||||
<TitleField />
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label="Название"
|
||||
name="title"
|
||||
required
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<DescriptionField />
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
label="Описание"
|
||||
name="description"
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<DateField />
|
||||
<DatePicker
|
||||
disabled={disabled}
|
||||
disableFuture
|
||||
format="dd.MM.yyyy"
|
||||
label="Дата"
|
||||
name="date"
|
||||
slotProps={{ textField: { required: true, size: 'small' } }}
|
||||
/>
|
||||
|
||||
<AmountField />
|
||||
|
||||
<TargetTypeField />
|
||||
|
||||
{watch('targetType') === 'BANK_CARD' && <BankCardField />}
|
||||
|
||||
{watch('targetType') === 'CASH' && <CashField />}
|
||||
<TextField
|
||||
disabled={disabled}
|
||||
inputMode="numeric"
|
||||
label="Сумма"
|
||||
name="amount"
|
||||
required
|
||||
size="small"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<Button
|
||||
data-testid={DataTestId.ADD_INCOME_ITEM_SUBMIT_BUTTON}
|
||||
disabled={formState.disabled || formState.isSubmitting}
|
||||
loading={formState.isSubmitting}
|
||||
disabled={disabled}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Добавить
|
||||
</Button>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
import { TextField } from '@mui/material';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import type { AddIncomeItemFormValues } from '../AddIncomeItemForm.schema';
|
||||
|
||||
const AmountField = () => {
|
||||
const { control, formState } = useFormContext<Pick<AddIncomeItemFormValues, 'amount'>>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
disabled={formState.disabled}
|
||||
name="amount"
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
error={Boolean(formState.errors.amount)}
|
||||
helperText={formState.errors.amount?.message}
|
||||
inputMode="numeric"
|
||||
label="Сумма"
|
||||
onChange={(event) => {
|
||||
field.onChange(Number(event.target.value));
|
||||
}}
|
||||
required
|
||||
size="small"
|
||||
type="number"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AmountField;
|
||||
@ -1,58 +0,0 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
} from '@mui/material';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { useBankCards } from '@/entity/cards';
|
||||
import { formatRub } from '@/shared/utils';
|
||||
|
||||
import type { AddIncomeItemFormValues } from '../AddIncomeItemForm.schema';
|
||||
|
||||
const BankCardField = () => {
|
||||
const { control, formState } = useFormContext<Pick<AddIncomeItemFormValues, 'bank-card'>>();
|
||||
|
||||
const { data: bankCards } = useBankCards();
|
||||
|
||||
const bankCardOptions = bankCards?.map((card) => (
|
||||
<MenuItem key={card.id} value={card.id}>
|
||||
{`${card.name} (${formatRub(card.balance)})`}
|
||||
</MenuItem>
|
||||
));
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue={bankCards?.[0].id}
|
||||
disabled={formState.disabled}
|
||||
name="bank-card"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
error={Boolean(formState.errors['bank-card'])}
|
||||
fullWidth
|
||||
>
|
||||
<InputLabel id="bank-card-label">Карта</InputLabel>
|
||||
|
||||
<Select
|
||||
{...field}
|
||||
data-testid="bank-card-select"
|
||||
inputProps={{ 'data-testid': 'bank-card-select-input' }}
|
||||
labelId="bank-card-label"
|
||||
size="small"
|
||||
>
|
||||
{bankCardOptions}
|
||||
</Select>
|
||||
|
||||
<FormHelperText>
|
||||
{formState.errors['bank-card']?.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BankCardField;
|
||||
@ -1,57 +0,0 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
} from '@mui/material';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { useCash } from '@/entity/cash';
|
||||
import { formatRub } from '@/shared/utils';
|
||||
|
||||
import type { AddIncomeItemFormValues } from '../AddIncomeItemForm.schema';
|
||||
|
||||
const CashField = () => {
|
||||
const { control, formState } = useFormContext<Pick<AddIncomeItemFormValues, 'cash'>>();
|
||||
const { data: cash } = useCash();
|
||||
|
||||
const cashOptions = cash?.map((cashItem) => (
|
||||
<MenuItem key={cashItem.id} value={cashItem.id}>
|
||||
{`${cashItem.name} (${formatRub(cashItem.balance)})`}
|
||||
</MenuItem>
|
||||
));
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue={cash?.[0].id}
|
||||
disabled={formState.disabled}
|
||||
name="cash"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
error={Boolean(formState.errors.cash)}
|
||||
fullWidth
|
||||
>
|
||||
<InputLabel id="cash-label">Наличные</InputLabel>
|
||||
|
||||
<Select
|
||||
{...field}
|
||||
data-testid="cash-select"
|
||||
inputProps={{ 'data-testid': 'cash-select-input' }}
|
||||
labelId="cash-label"
|
||||
size="small"
|
||||
>
|
||||
{cashOptions}
|
||||
</Select>
|
||||
|
||||
<FormHelperText>
|
||||
{formState.errors.cash?.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CashField;
|
||||
@ -1,37 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import type { AddIncomeItemFormValues } from '@/entity/incomes/item';
|
||||
|
||||
const DateField: FC = () => {
|
||||
const { control, formState } = useFormContext<Pick<AddIncomeItemFormValues, 'date'>>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
disabled={formState.disabled}
|
||||
name="date"
|
||||
render={({ field }) => (
|
||||
<DatePicker
|
||||
{...field}
|
||||
disableFuture
|
||||
format="dd.MM.yyyy"
|
||||
label="Дата"
|
||||
slotProps={{
|
||||
textField: {
|
||||
error: Boolean(formState.errors.date),
|
||||
helperText: formState.errors.date?.message,
|
||||
required: true,
|
||||
size: 'small',
|
||||
},
|
||||
}}
|
||||
timezone="UTC"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateField;
|
||||
@ -1,29 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { TextField } from '@mui/material';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import type { AddIncomeItemFormValues } from '../AddIncomeItemForm.schema';
|
||||
|
||||
const DescriptionField: FC = () => {
|
||||
const { control, formState } = useFormContext<Pick<AddIncomeItemFormValues, 'description'>>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
disabled={formState.disabled}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
error={Boolean(formState.errors.description)}
|
||||
helperText={formState.errors.description?.message}
|
||||
label="Описание"
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DescriptionField;
|
||||
@ -1,69 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import {
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
} from '@mui/material';
|
||||
import { useMemo } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { useBankCards } from '@/entity/cards';
|
||||
import { useCash } from '@/entity/cash';
|
||||
|
||||
import type { AddIncomeItemFormValues } from '../AddIncomeItemForm.schema';
|
||||
|
||||
const TargetTypeField: FC = () => {
|
||||
const { control, formState } = useFormContext<Pick<AddIncomeItemFormValues, 'targetType'>>();
|
||||
|
||||
const { data: bankCards } = useBankCards();
|
||||
const { data: cash } = useCash();
|
||||
|
||||
const targetTypeOptions = useMemo(() => {
|
||||
const options = [<MenuItem key="none" value="none">Без оплаты</MenuItem>];
|
||||
|
||||
if (bankCards && bankCards.length > 0) {
|
||||
options.push(<MenuItem key="BANK_CARD" value="BANK_CARD">Банковская карта</MenuItem>);
|
||||
}
|
||||
|
||||
if (cash && cash.length > 0) {
|
||||
options.push(<MenuItem key="CASH" value="CASH">Наличные</MenuItem>);
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [bankCards, cash]);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
disabled={formState.disabled}
|
||||
name="targetType"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
error={Boolean(formState.errors.targetType)}
|
||||
fullWidth
|
||||
>
|
||||
<InputLabel id="target-type-label">Способ получения</InputLabel>
|
||||
|
||||
<Select
|
||||
{...field}
|
||||
data-testid="target-type-select"
|
||||
inputProps={{ 'data-testid': 'target-type-select-input' }}
|
||||
labelId="target-type-label"
|
||||
size="small"
|
||||
>
|
||||
{targetTypeOptions}
|
||||
</Select>
|
||||
|
||||
<FormHelperText>
|
||||
{formState.errors.targetType?.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TargetTypeField;
|
||||
@ -1,30 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { TextField } from '@mui/material';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import type { AddIncomeItemFormValues } from '../AddIncomeItemForm.schema';
|
||||
|
||||
const TitleField: FC = () => {
|
||||
const { control, formState } = useFormContext<Pick<AddIncomeItemFormValues, 'title'>>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
disabled={formState.disabled}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
error={Boolean(formState.errors.title)}
|
||||
helperText={formState.errors.title?.message}
|
||||
label="Название"
|
||||
required
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TitleField;
|
||||
@ -1,47 +0,0 @@
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import type { AddIncomeItemFormValues } from '../AddIncomeItemForm.schema';
|
||||
|
||||
import { AddIncomeItemFormSchema } from '../AddIncomeItemForm.schema';
|
||||
|
||||
export type UseAddIncomeItemFormArgs = {
|
||||
disabled?: boolean;
|
||||
onSubmit: (data: AddIncomeItemFormValues) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const useAddIncomeItemForm = (args: UseAddIncomeItemFormArgs) => {
|
||||
const { disabled, onSubmit } = args;
|
||||
|
||||
const methods = useForm<AddIncomeItemFormValues>({
|
||||
defaultValues: {
|
||||
amount: 0,
|
||||
date: new Date(),
|
||||
description: '',
|
||||
targetType: 'none',
|
||||
title: '',
|
||||
},
|
||||
disabled,
|
||||
resolver: zodResolver(AddIncomeItemFormSchema),
|
||||
shouldUnregister: true,
|
||||
});
|
||||
const {
|
||||
formState,
|
||||
handleSubmit,
|
||||
watch,
|
||||
} = methods;
|
||||
|
||||
const onFormSubmitHandler: SubmitHandler<AddIncomeItemFormValues> = async (data) => {
|
||||
await onSubmit(data);
|
||||
};
|
||||
|
||||
return {
|
||||
formState,
|
||||
handleSubmit,
|
||||
methods,
|
||||
onFormSubmitHandler,
|
||||
watch,
|
||||
};
|
||||
};
|
||||
@ -1,5 +0,0 @@
|
||||
export type { AddIncomeItemFormValues } from './AddIncomeItemForm.schema';
|
||||
export { AddIncomeItemFormSchema } from './AddIncomeItemForm.schema';
|
||||
|
||||
export type { AddIncomeItemFormProps } from './AddIncomeItemForm.ui';
|
||||
export { default as AddIncomeItemForm } from './AddIncomeItemForm.ui';
|
||||
@ -13,6 +13,6 @@ describe('Test DeleteIncomeItemForm', () => {
|
||||
await DeleteIncomeItemFormHelper.submitForm();
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit).toHaveBeenCalledWith();
|
||||
expect(onSubmit).toHaveBeenCalledWith(DeleteIncomeItemFormHelper.incomeItem);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type {
|
||||
FC,
|
||||
FormEventHandler,
|
||||
FormHTMLAttributes,
|
||||
RefObject,
|
||||
} from 'react';
|
||||
@ -8,11 +9,12 @@ import { Button, Typography } from '@mui/material';
|
||||
|
||||
import { DataTestId } from '@/shared/constants';
|
||||
|
||||
import type { UseDeleteIncomeItemFormArgs } from './hooks/useDeleteIncomeItemForm';
|
||||
import type { IncomeListItem } from '../../types';
|
||||
|
||||
import { useDeleteIncomeItemForm } from './hooks/useDeleteIncomeItemForm';
|
||||
|
||||
export type DeleteIncomeItemFormProps = BaseFormProps & UseDeleteIncomeItemFormArgs & {
|
||||
export type DeleteIncomeItemFormProps = BaseFormProps & {
|
||||
disabled?: boolean;
|
||||
item: IncomeListItem;
|
||||
onSubmit?: (item: IncomeListItem) => Promise<void> | void;
|
||||
ref?: RefObject<HTMLFormElement>;
|
||||
};
|
||||
|
||||
@ -21,17 +23,20 @@ type BaseFormProps = Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;
|
||||
const DeleteIncomeItemForm: FC<DeleteIncomeItemFormProps> = (props) => {
|
||||
const {
|
||||
disabled,
|
||||
item,
|
||||
onSubmit,
|
||||
ref,
|
||||
...restProps
|
||||
} = props;
|
||||
|
||||
const { formState, handleSubmit, onFormSubmitHandler } = useDeleteIncomeItemForm({ disabled, onSubmit });
|
||||
const onFormSubmitHandler: FormEventHandler<HTMLFormElement> = (event) => {
|
||||
event.preventDefault();
|
||||
onSubmit?.(item);
|
||||
};
|
||||
|
||||
return (
|
||||
<form
|
||||
data-testid={DataTestId.DELETE_INCOME_ITEM_FORM}
|
||||
onSubmit={handleSubmit(onFormSubmitHandler)}
|
||||
onSubmit={onFormSubmitHandler}
|
||||
ref={ref}
|
||||
{...restProps}
|
||||
>
|
||||
@ -42,8 +47,7 @@ const DeleteIncomeItemForm: FC<DeleteIncomeItemFormProps> = (props) => {
|
||||
<Button
|
||||
color="error"
|
||||
data-testid={DataTestId.DELETE_INCOME_ITEM_SUBMIT_BUTTON}
|
||||
disabled={formState.disabled || formState.isSubmitting}
|
||||
loading={formState.isSubmitting}
|
||||
disabled={disabled}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
|
||||
@ -1,28 +0,0 @@
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
export type UseDeleteIncomeItemFormArgs = {
|
||||
disabled?: boolean;
|
||||
onSubmit: () => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const useDeleteIncomeItemForm = (args: UseDeleteIncomeItemFormArgs) => {
|
||||
const { disabled, onSubmit } = args;
|
||||
|
||||
const onFormSubmitHandler: SubmitHandler<never> = async () => {
|
||||
await onSubmit();
|
||||
};
|
||||
|
||||
const methods = useForm<never>({
|
||||
disabled,
|
||||
shouldUnregister: true,
|
||||
});
|
||||
const { formState, handleSubmit } = methods;
|
||||
|
||||
return {
|
||||
formState,
|
||||
handleSubmit,
|
||||
onFormSubmitHandler,
|
||||
};
|
||||
};
|
||||
@ -1,2 +0,0 @@
|
||||
export type { DeleteIncomeItemFormProps } from './DeleteIncomeItemForm.ui';
|
||||
export { default as DeleteIncomeItemForm } from './DeleteIncomeItemForm.ui';
|
||||
@ -1,13 +0,0 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
export const EditIncomeItemFormSchema = z.object({
|
||||
amount: z.number(),
|
||||
'bank-card': z.string().optional(),
|
||||
cash: z.string().optional(),
|
||||
date: z.date(),
|
||||
description: z.string().trim().optional(),
|
||||
targetType: z.enum(['BANK_CARD', 'CASH', 'none']),
|
||||
title: z.string().trim().min(5),
|
||||
});
|
||||
|
||||
export type EditIncomeItemFormValues = z.infer<typeof EditIncomeItemFormSchema>;
|
||||
@ -1,4 +1,3 @@
|
||||
import { bankCardList, cashList } from '@mocks/browser/data';
|
||||
import { EditIncomeItemFormHelper } from '@tests/helpers';
|
||||
|
||||
describe('Test EditIncomeItemForm', () => {
|
||||
@ -28,95 +27,27 @@ describe('Test EditIncomeItemForm', () => {
|
||||
['disabled', { disabled: true }, (el: HTMLElement) => { expect(el).toBeDisabled(); }],
|
||||
])('should render fields %s', (_state, options, assertion) => {
|
||||
EditIncomeItemFormHelper.renderForm(options);
|
||||
const elems = EditIncomeItemFormHelper.getFormElements();
|
||||
Object.values([
|
||||
elems.amountInputElem,
|
||||
elems.dateInputElem,
|
||||
elems.descriptionInputElem,
|
||||
elems.submitButtonElem,
|
||||
elems.titleInputElem,
|
||||
]).forEach(assertion);
|
||||
const { formElem, ...formElems } = EditIncomeItemFormHelper.getFormElements();
|
||||
Object.values(formElems).forEach(assertion);
|
||||
});
|
||||
|
||||
describe('should call onSubmit with values', () => {
|
||||
const setup = async () => {
|
||||
test('should call onSubmit with values', async () => {
|
||||
const { onSubmit } = EditIncomeItemFormHelper.renderForm();
|
||||
|
||||
await EditIncomeItemFormHelper.fillForm(
|
||||
{
|
||||
await EditIncomeItemFormHelper.fillForm({
|
||||
amount: 12,
|
||||
date: '29.08.2025',
|
||||
description: 'Описание элемента',
|
||||
targetType: 'none',
|
||||
title: 'Название элемента',
|
||||
},
|
||||
{ autoSubmit: false },
|
||||
);
|
||||
|
||||
return {
|
||||
onSubmit,
|
||||
};
|
||||
};
|
||||
|
||||
test('no payment', async () => {
|
||||
const { onSubmit } = await setup();
|
||||
await EditIncomeItemFormHelper.submitForm();
|
||||
});
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
amount: 12,
|
||||
date: new Date('2025-08-29T00:00:00.000Z'),
|
||||
currency: 'RUB',
|
||||
date: '2025-08-29T00:00:00.000Z',
|
||||
description: 'Описание элемента',
|
||||
targetType: 'none',
|
||||
title: 'Название элемента',
|
||||
});
|
||||
});
|
||||
|
||||
test('bank card payment', async () => {
|
||||
const { onSubmit } = await setup();
|
||||
await EditIncomeItemFormHelper.fillForm({ targetType: 'BANK_CARD' }, { autoSubmit: false });
|
||||
|
||||
const { bankCardSelect, bankCardSelectInput } = EditIncomeItemFormHelper.getFormElements();
|
||||
expect(bankCardSelect).toBeVisible();
|
||||
expect(bankCardSelectInput).toBeEnabled();
|
||||
expect(bankCardSelectInput).toHaveValue(bankCardList[0].id);
|
||||
|
||||
await EditIncomeItemFormHelper.fillForm({ 'bank-card': bankCardList[1].id, targetType: 'BANK_CARD' }, { autoSubmit: false });
|
||||
expect(bankCardSelectInput).toHaveValue(bankCardList[1].id);
|
||||
|
||||
await EditIncomeItemFormHelper.submitForm();
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
amount: 12,
|
||||
'bank-card': bankCardList[1].id,
|
||||
date: new Date('2025-08-29T00:00:00.000Z'),
|
||||
description: 'Описание элемента',
|
||||
targetType: 'BANK_CARD',
|
||||
title: 'Название элемента',
|
||||
});
|
||||
});
|
||||
|
||||
test('cash payment', async () => {
|
||||
const { onSubmit } = await setup();
|
||||
|
||||
await EditIncomeItemFormHelper.fillForm({ targetType: 'CASH' });
|
||||
const { cashSelect, cashSelectInput } = EditIncomeItemFormHelper.getFormElements();
|
||||
expect(cashSelect).toBeVisible();
|
||||
expect(cashSelectInput).toBeEnabled();
|
||||
expect(cashSelectInput).toHaveValue(cashList[0].id);
|
||||
|
||||
await EditIncomeItemFormHelper.fillForm({ cash: cashList[1].id, targetType: 'CASH' });
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1);
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
amount: 12,
|
||||
cash: cashList[1].id,
|
||||
date: new Date('2025-08-29T00:00:00.000Z'),
|
||||
description: 'Описание элемента',
|
||||
targetType: 'CASH',
|
||||
title: 'Название элемента',
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,26 +1,34 @@
|
||||
import type { FC, FormHTMLAttributes, RefObject } from 'react';
|
||||
import type {
|
||||
FC,
|
||||
FormEventHandler,
|
||||
FormHTMLAttributes,
|
||||
RefObject,
|
||||
} from 'react';
|
||||
|
||||
import { Button, Typography } from '@mui/material';
|
||||
import { FormProvider } from 'react-hook-form';
|
||||
import { Button, TextField, Typography } from '@mui/material';
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
|
||||
import { DataTestId } from '@/shared/constants';
|
||||
import { getUTCDate } from '@/shared/utils';
|
||||
|
||||
import type { UseEditIncomeItemFormArgs } from './hooks/useEditIncomeItemForm';
|
||||
import type { IncomeListItem } from '../../types';
|
||||
|
||||
import classes from './EditIncomeItemForm.module.css';
|
||||
import AmountField from './fields/AmountField';
|
||||
import BankCardField from './fields/BankCardField';
|
||||
import CashField from './fields/CashField';
|
||||
import DateField from './fields/DateField';
|
||||
import DescriptionField from './fields/DescriptionField';
|
||||
import TargetTypeField from './fields/TargetTypeField';
|
||||
import TitleField from './fields/TitleField';
|
||||
import { useEditIncomeItemForm } from './hooks/useEditIncomeItemForm';
|
||||
|
||||
export type EditIncomeItemFormProps = BaseFormProps & UseEditIncomeItemFormArgs & {
|
||||
export type EditIncomeItemFormProps = BaseFormProps & {
|
||||
disabled?: boolean;
|
||||
item: IncomeListItem;
|
||||
onSubmit?: (data: EditIncomeItemFormValues) => Promise<void> | void;
|
||||
ref?: RefObject<HTMLFormElement>;
|
||||
};
|
||||
|
||||
export type EditIncomeItemFormValues = {
|
||||
amount: number;
|
||||
currency: string;
|
||||
date: string;
|
||||
description: string;
|
||||
title: string;
|
||||
};
|
||||
type BaseFormProps = Omit<FormHTMLAttributes<HTMLFormElement>, 'onSubmit'>;
|
||||
|
||||
const EditIncomeItemForm: FC<EditIncomeItemFormProps> = (props) => {
|
||||
@ -32,21 +40,32 @@ const EditIncomeItemForm: FC<EditIncomeItemFormProps> = (props) => {
|
||||
...formProps
|
||||
} = props;
|
||||
|
||||
const {
|
||||
formState,
|
||||
handleSubmit,
|
||||
methods,
|
||||
onFormSubmitHandler,
|
||||
watch,
|
||||
} = useEditIncomeItemForm({ disabled, item, onSubmit });
|
||||
// eslint-disable-next-line @typescript-eslint/no-misused-promises
|
||||
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');
|
||||
|
||||
if (typeof title === 'string' && typeof date === 'string' && typeof amount === 'string' && typeof description === 'string') {
|
||||
await onSubmit?.({
|
||||
amount: Number(amount),
|
||||
currency: 'RUB',
|
||||
date: getUTCDate(date),
|
||||
description,
|
||||
title,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<FormProvider {...methods}>
|
||||
<form
|
||||
className={classes.form}
|
||||
data-testid={DataTestId.EDIT_INCOME_ITEM_FORM}
|
||||
noValidate
|
||||
onSubmit={handleSubmit(onFormSubmitHandler)}
|
||||
onSubmit={onFormSubmitHandler}
|
||||
ref={ref}
|
||||
{...formProps}
|
||||
>
|
||||
@ -54,31 +73,50 @@ const EditIncomeItemForm: FC<EditIncomeItemFormProps> = (props) => {
|
||||
Изменить доход
|
||||
</Typography>
|
||||
|
||||
<TitleField />
|
||||
<TextField
|
||||
defaultValue={item.title}
|
||||
disabled={disabled}
|
||||
label="Название"
|
||||
name="title"
|
||||
required
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<DescriptionField />
|
||||
<TextField
|
||||
defaultValue={item.description ?? ''}
|
||||
disabled={disabled}
|
||||
label="Описание"
|
||||
name="description"
|
||||
size="small"
|
||||
/>
|
||||
|
||||
<DateField />
|
||||
<DatePicker
|
||||
defaultValue={new Date(item.date)}
|
||||
disabled={disabled}
|
||||
label="Дата"
|
||||
name="date"
|
||||
slotProps={{ textField: { required: true, size: 'small' } }}
|
||||
/>
|
||||
|
||||
<AmountField />
|
||||
|
||||
<TargetTypeField />
|
||||
|
||||
{watch('targetType') === 'BANK_CARD' && <BankCardField />}
|
||||
|
||||
{watch('targetType') === 'CASH' && <CashField />}
|
||||
<TextField
|
||||
defaultValue={item.amount}
|
||||
disabled={disabled}
|
||||
inputMode="numeric"
|
||||
label="Сумма"
|
||||
name="amount"
|
||||
size="small"
|
||||
type="number"
|
||||
/>
|
||||
|
||||
<Button
|
||||
data-testid={DataTestId.EDIT_INCOME_ITEM_SUBMIT_BUTTON}
|
||||
disabled={disabled || formState.isSubmitting}
|
||||
loading={formState.isSubmitting}
|
||||
disabled={disabled}
|
||||
type="submit"
|
||||
variant="contained"
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
import { TextField } from '@mui/material';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import type { EditIncomeItemFormValues } from '../EditIncomeItemForm.schema';
|
||||
|
||||
const AmountField = () => {
|
||||
const { control, formState } = useFormContext<Pick<EditIncomeItemFormValues, 'amount'>>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
disabled={formState.disabled}
|
||||
name="amount"
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
error={Boolean(formState.errors.amount)}
|
||||
helperText={formState.errors.amount?.message}
|
||||
inputMode="numeric"
|
||||
label="Сумма"
|
||||
onChange={(event) => {
|
||||
field.onChange(Number(event.target.value));
|
||||
}}
|
||||
required
|
||||
size="small"
|
||||
type="number"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default AmountField;
|
||||
@ -1,58 +0,0 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
} from '@mui/material';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { useBankCards } from '@/entity/cards';
|
||||
import { formatRub } from '@/shared/utils';
|
||||
|
||||
import type { EditIncomeItemFormValues } from '../EditIncomeItemForm.schema';
|
||||
|
||||
const BankCardField = () => {
|
||||
const { control, formState } = useFormContext<Pick<EditIncomeItemFormValues, 'bank-card'>>();
|
||||
|
||||
const { data: bankCards } = useBankCards();
|
||||
|
||||
const bankCardOptions = bankCards?.map((card) => (
|
||||
<MenuItem key={card.id} value={card.id}>
|
||||
{`${card.name} (${formatRub(card.balance)})`}
|
||||
</MenuItem>
|
||||
));
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue={bankCards?.[0].id}
|
||||
disabled={formState.disabled}
|
||||
name="bank-card"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
error={Boolean(formState.errors['bank-card'])}
|
||||
fullWidth
|
||||
>
|
||||
<InputLabel id="bank-card-label">Карта</InputLabel>
|
||||
|
||||
<Select
|
||||
{...field}
|
||||
data-testid="bank-card-select"
|
||||
inputProps={{ 'data-testid': 'bank-card-select-input' }}
|
||||
labelId="bank-card-label"
|
||||
size="small"
|
||||
>
|
||||
{bankCardOptions}
|
||||
</Select>
|
||||
|
||||
<FormHelperText>
|
||||
{formState.errors['bank-card']?.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default BankCardField;
|
||||
@ -1,57 +0,0 @@
|
||||
import {
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
} from '@mui/material';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { useCash } from '@/entity/cash';
|
||||
import { formatRub } from '@/shared/utils';
|
||||
|
||||
import type { EditIncomeItemFormValues } from '../EditIncomeItemForm.schema';
|
||||
|
||||
const CashField = () => {
|
||||
const { control, formState } = useFormContext<Pick<EditIncomeItemFormValues, 'cash'>>();
|
||||
const { data: cash } = useCash();
|
||||
|
||||
const cashOptions = cash?.map((cashItem) => (
|
||||
<MenuItem key={cashItem.id} value={cashItem.id}>
|
||||
{`${cashItem.name} (${formatRub(cashItem.balance)})`}
|
||||
</MenuItem>
|
||||
));
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
defaultValue={cash?.[0].id}
|
||||
disabled={formState.disabled}
|
||||
name="cash"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
error={Boolean(formState.errors.cash)}
|
||||
fullWidth
|
||||
>
|
||||
<InputLabel id="cash-label">Наличные</InputLabel>
|
||||
|
||||
<Select
|
||||
{...field}
|
||||
data-testid="cash-select"
|
||||
inputProps={{ 'data-testid': 'cash-select-input' }}
|
||||
labelId="cash-label"
|
||||
size="small"
|
||||
>
|
||||
{cashOptions}
|
||||
</Select>
|
||||
|
||||
<FormHelperText>
|
||||
{formState.errors.cash?.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default CashField;
|
||||
@ -1,37 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { DatePicker } from '@mui/x-date-pickers';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import type { EditIncomeItemFormValues } from '../EditIncomeItemForm.schema';
|
||||
|
||||
const DateField: FC = () => {
|
||||
const { control, formState } = useFormContext<Pick<EditIncomeItemFormValues, 'date'>>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
disabled={formState.disabled}
|
||||
name="date"
|
||||
render={({ field }) => (
|
||||
<DatePicker
|
||||
{...field}
|
||||
disableFuture
|
||||
format="dd.MM.yyyy"
|
||||
label="Дата"
|
||||
slotProps={{
|
||||
textField: {
|
||||
error: Boolean(formState.errors.date),
|
||||
helperText: formState.errors.date?.message,
|
||||
required: true,
|
||||
size: 'small',
|
||||
},
|
||||
}}
|
||||
timezone="UTC"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DateField;
|
||||
@ -1,29 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { TextField } from '@mui/material';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import type { EditIncomeItemFormValues } from '../EditIncomeItemForm.schema';
|
||||
|
||||
const DescriptionField: FC = () => {
|
||||
const { control, formState } = useFormContext<Pick<EditIncomeItemFormValues, 'description'>>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
disabled={formState.disabled}
|
||||
name="description"
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
error={Boolean(formState.errors.description)}
|
||||
helperText={formState.errors.description?.message}
|
||||
label="Описание"
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default DescriptionField;
|
||||
@ -1,69 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import {
|
||||
FormControl,
|
||||
FormHelperText,
|
||||
InputLabel,
|
||||
MenuItem,
|
||||
Select,
|
||||
} from '@mui/material';
|
||||
import { useMemo } from 'react';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import { useBankCards } from '@/entity/cards';
|
||||
import { useCash } from '@/entity/cash';
|
||||
|
||||
import type { EditIncomeItemFormValues } from '../EditIncomeItemForm.schema';
|
||||
|
||||
const TargetTypeField: FC = () => {
|
||||
const { control, formState } = useFormContext<Pick<EditIncomeItemFormValues, 'targetType'>>();
|
||||
|
||||
const { data: bankCards } = useBankCards();
|
||||
const { data: cash } = useCash();
|
||||
|
||||
const targetTypeOptions = useMemo(() => {
|
||||
const options = [<MenuItem key="none" value="none">Без оплаты</MenuItem>];
|
||||
|
||||
if (bankCards && bankCards.length > 0) {
|
||||
options.push(<MenuItem key="BANK_CARD" value="BANK_CARD">Банковская карта</MenuItem>);
|
||||
}
|
||||
|
||||
if (cash && cash.length > 0) {
|
||||
options.push(<MenuItem key="CASH" value="CASH">Наличные</MenuItem>);
|
||||
}
|
||||
|
||||
return options;
|
||||
}, [bankCards, cash]);
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
disabled={formState.disabled}
|
||||
name="targetType"
|
||||
render={({ field }) => (
|
||||
<FormControl
|
||||
error={Boolean(formState.errors.targetType)}
|
||||
fullWidth
|
||||
>
|
||||
<InputLabel id="target-type-label">Способ получения</InputLabel>
|
||||
|
||||
<Select
|
||||
{...field}
|
||||
data-testid="target-type-select"
|
||||
inputProps={{ 'data-testid': 'target-type-select-input' }}
|
||||
labelId="target-type-label"
|
||||
size="small"
|
||||
>
|
||||
{targetTypeOptions}
|
||||
</Select>
|
||||
|
||||
<FormHelperText>
|
||||
{formState.errors.targetType?.message}
|
||||
</FormHelperText>
|
||||
</FormControl>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TargetTypeField;
|
||||
@ -1,30 +0,0 @@
|
||||
import type { FC } from 'react';
|
||||
|
||||
import { TextField } from '@mui/material';
|
||||
import { Controller, useFormContext } from 'react-hook-form';
|
||||
|
||||
import type { EditIncomeItemFormValues } from '../EditIncomeItemForm.schema';
|
||||
|
||||
const TitleField: FC = () => {
|
||||
const { control, formState } = useFormContext<Pick<EditIncomeItemFormValues, 'title'>>();
|
||||
|
||||
return (
|
||||
<Controller
|
||||
control={control}
|
||||
disabled={formState.disabled}
|
||||
name="title"
|
||||
render={({ field }) => (
|
||||
<TextField
|
||||
{...field}
|
||||
error={Boolean(formState.errors.title)}
|
||||
helperText={formState.errors.title?.message}
|
||||
label="Название"
|
||||
required
|
||||
size="small"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default TitleField;
|
||||
@ -1,54 +0,0 @@
|
||||
import type { SubmitHandler } from 'react-hook-form';
|
||||
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { useForm } from 'react-hook-form';
|
||||
|
||||
import type { IncomeListItem } from '@/entity/incomes/item';
|
||||
|
||||
import type { EditIncomeItemFormValues } from '../EditIncomeItemForm.schema';
|
||||
|
||||
import { EditIncomeItemFormSchema } from '../EditIncomeItemForm.schema';
|
||||
|
||||
export type UseEditIncomeItemFormArgs = {
|
||||
disabled?: boolean;
|
||||
item: IncomeListItem;
|
||||
onSubmit: (data: EditIncomeItemFormValues) => Promise<void> | void;
|
||||
};
|
||||
|
||||
export const useEditIncomeItemForm = (args: UseEditIncomeItemFormArgs) => {
|
||||
const { disabled, item, onSubmit } = args;
|
||||
|
||||
const methods = useForm<EditIncomeItemFormValues>({
|
||||
defaultValues: {
|
||||
amount: item.amount,
|
||||
// eslint-disable-next-line no-undefined
|
||||
'bank-card': item.target?.type === 'BANK_CARD' ? item.target.id : undefined,
|
||||
// eslint-disable-next-line no-undefined
|
||||
cash: item.target?.type === 'CASH' ? item.target.id : undefined,
|
||||
date: new Date(item.date),
|
||||
description: item.description ?? '',
|
||||
targetType: item.target?.type ?? 'none',
|
||||
title: item.title,
|
||||
},
|
||||
disabled,
|
||||
resolver: zodResolver(EditIncomeItemFormSchema),
|
||||
shouldUnregister: true,
|
||||
});
|
||||
const {
|
||||
formState,
|
||||
handleSubmit,
|
||||
watch,
|
||||
} = methods;
|
||||
|
||||
const onFormSubmitHandler: SubmitHandler<EditIncomeItemFormValues> = async (data) => {
|
||||
await onSubmit(data);
|
||||
};
|
||||
|
||||
return {
|
||||
formState,
|
||||
handleSubmit,
|
||||
methods,
|
||||
onFormSubmitHandler,
|
||||
watch,
|
||||
};
|
||||
};
|
||||
@ -1,4 +0,0 @@
|
||||
export type { EditIncomeItemFormValues } from './EditIncomeItemForm.schema';
|
||||
export { EditIncomeItemFormSchema } from './EditIncomeItemForm.schema';
|
||||
export type { EditIncomeItemFormProps } from './EditIncomeItemForm.ui';
|
||||
export { default as EditIncomeItemForm } from './EditIncomeItemForm.ui';
|
||||
@ -1,6 +1,5 @@
|
||||
import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
|
||||
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { AddExpenseItemFormValues } from '@/entity/expenses/item';
|
||||
@ -27,30 +26,10 @@ export const useAddExpenseItemButton = (args: UseAddExpenseItemButtonArgs) => {
|
||||
};
|
||||
|
||||
const onFormSubmitHandler = async (data: AddExpenseItemFormValues) => {
|
||||
try {
|
||||
const payment = data.paymentType === 'none'
|
||||
? null
|
||||
: {
|
||||
id: data.paymentType === 'BANK_CARD' ? data['bank-card'] : data.cash,
|
||||
type: data.paymentType,
|
||||
};
|
||||
await addExpenseItemMutation({
|
||||
amount: data.amount,
|
||||
count: data.count,
|
||||
currency: 'RUB',
|
||||
date: data.date.toISOString(),
|
||||
description: data.description ?? null,
|
||||
// @ts-ignore TODO
|
||||
payment,
|
||||
title: data.title,
|
||||
});
|
||||
|
||||
enqueueSnackbar(`Расход "${data.title}" успешно добавлен`, { variant: 'success' });
|
||||
// todo добавить обработку ошибок (код + тест)
|
||||
await addExpenseItemMutation(data);
|
||||
|
||||
setIsOpen(false);
|
||||
} catch {
|
||||
enqueueSnackbar('Ошибка при добавлении расхода', { variant: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { mockEnqueueSnackbar } from '@mocks/jest/helpers';
|
||||
import { server } from '@mocks/jest/server';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { AddExpenseItemButtonHelper, AddExpenseItemFormHelper } from '@tests/helpers';
|
||||
@ -23,7 +22,7 @@ describe('Test AddExpenseItemButton', () => {
|
||||
expect(modalElem).toBeVisible();
|
||||
});
|
||||
|
||||
describe('should close modal after success request', () => {
|
||||
describe('should close modal after submit and send request', () => {
|
||||
let modalElem: HTMLElement;
|
||||
const requestSpy = jest.fn();
|
||||
|
||||
@ -37,10 +36,11 @@ describe('Test AddExpenseItemButton', () => {
|
||||
date: '29.08.2025',
|
||||
description: 'Описание элемента',
|
||||
title: 'Название элемента',
|
||||
});
|
||||
}, { autoSubmit: false });
|
||||
});
|
||||
|
||||
test('should send correct request', () => {
|
||||
test('should send correct request', async () => {
|
||||
await AddExpenseItemFormHelper.submitForm();
|
||||
expect(requestSpy).toHaveBeenCalledTimes(1);
|
||||
expect(requestSpy).toHaveBeenCalledWith({
|
||||
amount: 12,
|
||||
@ -54,41 +54,10 @@ describe('Test AddExpenseItemButton', () => {
|
||||
});
|
||||
|
||||
test('should close modal after submit', async () => {
|
||||
await AddExpenseItemFormHelper.submitForm();
|
||||
await waitFor(() => {
|
||||
expect(modalElem).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should show success notification', () => {
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1);
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Расход "Название элемента" успешно добавлен', { variant: 'success' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('should not close modal after failed request', () => {
|
||||
let modalElem: HTMLElement;
|
||||
const requestSpy = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
server.use(AddExpenseItemButtonHelper.submitHandler(requestSpy, 500));
|
||||
modalElem = (await AddExpenseItemButtonHelper.openModal()).modalElem;
|
||||
|
||||
await AddExpenseItemFormHelper.fillForm({
|
||||
amount: '12',
|
||||
count: '1',
|
||||
date: '29.08.2025',
|
||||
description: 'Описание элемента',
|
||||
title: 'Название элемента',
|
||||
});
|
||||
});
|
||||
|
||||
test('should not close modal after submit', () => {
|
||||
expect(modalElem).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show error notification', () => {
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1);
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Ошибка при добавлении расхода', { variant: 'error' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -44,7 +44,7 @@ const AddExpenseItemButton: FC<AddExpenseItemButtonProps> = (props) => {
|
||||
Добавить элемент
|
||||
</Button>
|
||||
|
||||
<Modal onClose={onCloseModalHandler} open={isOpen || isPending}>
|
||||
<Modal onClose={onCloseModalHandler} open={isOpen}>
|
||||
<AddExpenseItemForm disabled={isPending} onSubmit={onFormSubmitHandler} />
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import type { MouseEventHandler } from 'react';
|
||||
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { type ExpenseListItem, useDeleteExpenseItem } from '@/entity/expenses/item';
|
||||
@ -28,14 +27,8 @@ export const useDeleteExpenseItemButton = (args: UseDeleteExpenseItemButtonArgs)
|
||||
});
|
||||
|
||||
const onFormSubmitHandler = async () => {
|
||||
try {
|
||||
await deleteExpenseItemMutation();
|
||||
|
||||
enqueueSnackbar(`Расход "${item.title}" удален`, { variant: 'success' });
|
||||
setIsOpen(false);
|
||||
} catch {
|
||||
enqueueSnackbar(`Ошибка при удалении расхода "${item.title}"`, { variant: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { mockEnqueueSnackbar } from '@mocks/jest/helpers';
|
||||
import { server } from '@mocks/jest/server';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { DeleteExpenseItemButtonHelper, DeleteExpenseItemFormHelper } from '@tests/helpers';
|
||||
|
||||
describe('Test DeleteExpenseItemButton', () => {
|
||||
@ -23,51 +23,15 @@ describe('Test DeleteExpenseItemButton', () => {
|
||||
expect(modalElem).toBeVisible();
|
||||
});
|
||||
|
||||
describe('should close modal after success request', () => {
|
||||
let modalElem: HTMLElement;
|
||||
const requestSpy = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
server.use(DeleteExpenseItemButtonHelper.submitHandler(requestSpy));
|
||||
modalElem = (await DeleteExpenseItemButtonHelper.openModal()).modalElem;
|
||||
|
||||
await DeleteExpenseItemFormHelper.submitForm();
|
||||
});
|
||||
|
||||
test('should send correct request', () => {
|
||||
expect(requestSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should close modal after submit', async () => {
|
||||
server.use(DeleteExpenseItemButtonHelper.submitHandler);
|
||||
const { modalElem } = await DeleteExpenseItemButtonHelper.openModal();
|
||||
const { submitButtonElem } = DeleteExpenseItemFormHelper.getFormElements();
|
||||
|
||||
expect(submitButtonElem).toBeEnabled();
|
||||
await userEvent.click(submitButtonElem);
|
||||
await waitFor(() => {
|
||||
expect(modalElem).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should show success notification', () => {
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1);
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Расход "Mock item" удален', { variant: 'success' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('should not close modal after failed request', () => {
|
||||
let modalElem: HTMLElement;
|
||||
const requestSpy = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
server.use(DeleteExpenseItemButtonHelper.submitHandler(requestSpy, 500));
|
||||
modalElem = (await DeleteExpenseItemButtonHelper.openModal()).modalElem;
|
||||
|
||||
await DeleteExpenseItemFormHelper.submitForm();
|
||||
});
|
||||
|
||||
test('should not close modal after submit', () => {
|
||||
expect(modalElem).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show error notification', () => {
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1);
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Ошибка при удалении расхода "Mock item"', { variant: 'error' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -44,7 +44,7 @@ const DeleteExpenseItemButton: FC<DeleteExpenseItemButtonProps> = (props) => {
|
||||
</Button>
|
||||
|
||||
<Modal onClose={onCloseModalHandler} open={isOpen}>
|
||||
<DeleteExpenseItemForm disabled={isPending} onSubmit={onFormSubmitHandler} />
|
||||
<DeleteExpenseItemForm disabled={isPending} item={item} onSubmit={onFormSubmitHandler} />
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import type { MouseEventHandler } from 'react';
|
||||
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { type EditExpenseItemFormValues, type ExpenseListItem, useEditExpenseItem } from '@/entity/expenses/item';
|
||||
@ -28,30 +27,9 @@ export const useEditExpenseItemButton = (args: UseAddExpenseItemButtonArgs) => {
|
||||
};
|
||||
|
||||
const onFormSubmitHandler = async (data: EditExpenseItemFormValues) => {
|
||||
try {
|
||||
const payment = data.paymentType === 'none'
|
||||
? null
|
||||
: {
|
||||
id: data.paymentType === 'BANK_CARD' ? data['bank-card'] : data.cash,
|
||||
type: data.paymentType,
|
||||
};
|
||||
await editExpenseItemMutation({
|
||||
amount: data.amount,
|
||||
count: data.count,
|
||||
currency: 'RUB',
|
||||
date: data.date.toISOString(),
|
||||
description: data.description ?? null,
|
||||
// @ts-ignore TODO
|
||||
payment,
|
||||
title: data.title,
|
||||
});
|
||||
|
||||
enqueueSnackbar(`Расход "${data.title}" успешно сохранен`, { variant: 'success' });
|
||||
|
||||
// todo добавить обработку ошибок (код + тест)
|
||||
await editExpenseItemMutation(data);
|
||||
setIsOpen(false);
|
||||
} catch {
|
||||
enqueueSnackbar('Ошибка при сохранении расхода', { variant: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { mockEnqueueSnackbar } from '@mocks/jest/helpers';
|
||||
import { server } from '@mocks/jest/server';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { EditExpenseItemButtonHelper, EditExpenseItemFormHelper } from '@tests/helpers';
|
||||
@ -23,7 +22,7 @@ describe('TestEditExpenseItemButton', () => {
|
||||
expect(modalElem).toBeVisible();
|
||||
});
|
||||
|
||||
describe('should close modal after success request', () => {
|
||||
describe('should close modal after submit and send request', () => {
|
||||
let modalElem: HTMLElement;
|
||||
const requestSpy = jest.fn();
|
||||
|
||||
@ -36,12 +35,12 @@ describe('TestEditExpenseItemButton', () => {
|
||||
count: 1,
|
||||
date: '29.08.2025',
|
||||
description: 'Описание элемента',
|
||||
paymentType: 'none',
|
||||
title: 'Название элемента',
|
||||
});
|
||||
}, { autoSubmit: false });
|
||||
});
|
||||
|
||||
test('should send correct request', () => {
|
||||
test('should send correct request', async () => {
|
||||
await EditExpenseItemFormHelper.submitForm();
|
||||
expect(requestSpy).toHaveBeenCalledTimes(1);
|
||||
expect(requestSpy).toHaveBeenCalledWith({
|
||||
amount: 12,
|
||||
@ -49,48 +48,15 @@ describe('TestEditExpenseItemButton', () => {
|
||||
currency: 'RUB',
|
||||
date: '2025-08-29T00:00:00.000Z',
|
||||
description: 'Описание элемента',
|
||||
payment: null,
|
||||
title: 'Название элемента',
|
||||
});
|
||||
});
|
||||
|
||||
test('should close modal after submit', async () => {
|
||||
await EditExpenseItemFormHelper.submitForm();
|
||||
await waitFor(() => {
|
||||
expect(modalElem).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should show success notification', () => {
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1);
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Расход "Название элемента" успешно сохранен', { variant: 'success' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('should not close modal after failed request', () => {
|
||||
let modalElem: HTMLElement;
|
||||
const requestSpy = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
server.use(EditExpenseItemButtonHelper.submitHandler(requestSpy, 500));
|
||||
modalElem = (await EditExpenseItemButtonHelper.openModal()).modalElem;
|
||||
|
||||
await EditExpenseItemFormHelper.fillForm({
|
||||
amount: 12,
|
||||
count: 1,
|
||||
date: '29.08.2025',
|
||||
description: 'Описание элемента',
|
||||
paymentType: 'none',
|
||||
title: 'Название элемента',
|
||||
});
|
||||
});
|
||||
|
||||
test('should not close modal after submit', () => {
|
||||
expect(modalElem).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show error notification', () => {
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1);
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Ошибка при сохранении расхода', { variant: 'error' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -45,7 +45,7 @@ const EditExpenseItemButton: FC<EditExpenseItemButtonProps> = (props) => {
|
||||
Редактировать
|
||||
</Button>
|
||||
|
||||
<Modal onClose={onCloseModalHandler} open={isOpen || isPending}>
|
||||
<Modal onClose={onCloseModalHandler} open={isOpen}>
|
||||
<EditExpenseItemForm disabled={isPending} item={item} onSubmit={onFormSubmitHandler} />
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import type { ButtonHTMLAttributes, MouseEventHandler } from 'react';
|
||||
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { useState } from 'react';
|
||||
|
||||
import type { AddIncomeItemFormValues } from '@/entity/incomes/item';
|
||||
@ -27,29 +26,10 @@ export const useAddIncomeItemButton = (args: UseAddIncomeItemButtonArgs) => {
|
||||
};
|
||||
|
||||
const onFormSubmitHandler = async (data: AddIncomeItemFormValues) => {
|
||||
try {
|
||||
const target = data.targetType === 'none'
|
||||
? null
|
||||
: {
|
||||
id: data.targetType === 'BANK_CARD' ? data['bank-card'] : data.cash,
|
||||
type: data.targetType,
|
||||
};
|
||||
await addIncomeItemMutation({
|
||||
amount: data.amount,
|
||||
currency: 'RUB',
|
||||
date: data.date.toISOString(),
|
||||
description: data.description ?? null,
|
||||
// @ts-ignore TODO
|
||||
target,
|
||||
title: data.title,
|
||||
});
|
||||
|
||||
enqueueSnackbar(`Доход "${data.title}" успешно добавлен`, { variant: 'success' });
|
||||
// todo добавить обработку ошибок (код + тест)
|
||||
await addIncomeItemMutation(data);
|
||||
|
||||
setIsOpen(false);
|
||||
} catch {
|
||||
enqueueSnackbar('Ошибка при добавлении дохода', { variant: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { mockEnqueueSnackbar } from '@mocks/jest/helpers';
|
||||
import { server } from '@mocks/jest/server';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { AddIncomeItemButtonHelper, AddIncomeItemFormHelper } from '@tests/helpers';
|
||||
@ -23,7 +22,7 @@ describe('Test AddIncomeItemButton', () => {
|
||||
expect(modalElem).toBeVisible();
|
||||
});
|
||||
|
||||
describe('should close modal after success request', () => {
|
||||
describe('should close modal after submit and send request', () => {
|
||||
let modalElem: HTMLElement;
|
||||
const requestSpy = jest.fn();
|
||||
|
||||
@ -36,56 +35,26 @@ describe('Test AddIncomeItemButton', () => {
|
||||
date: '29.08.2025',
|
||||
description: 'Описание элемента',
|
||||
title: 'Название элемента',
|
||||
});
|
||||
}, { autoSubmit: false });
|
||||
});
|
||||
|
||||
test('should send correct request', () => {
|
||||
test('should send correct request', async () => {
|
||||
await AddIncomeItemFormHelper.submitForm();
|
||||
expect(requestSpy).toHaveBeenCalledTimes(1);
|
||||
expect(requestSpy).toHaveBeenCalledWith({
|
||||
amount: 12,
|
||||
currency: 'RUB',
|
||||
date: '2025-08-29T00:00:00.000Z',
|
||||
description: 'Описание элемента',
|
||||
target: null,
|
||||
title: 'Название элемента',
|
||||
});
|
||||
});
|
||||
|
||||
test('should close modal after submit', async () => {
|
||||
await AddIncomeItemFormHelper.submitForm();
|
||||
await waitFor(() => {
|
||||
expect(modalElem).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should show success notification', () => {
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1);
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Доход "Название элемента" успешно добавлен', { variant: 'success' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('should not close modal after failed request', () => {
|
||||
let modalElem: HTMLElement;
|
||||
const requestSpy = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
server.use(AddIncomeItemButtonHelper.submitHandler(requestSpy, 500));
|
||||
modalElem = (await AddIncomeItemButtonHelper.openModal()).modalElem;
|
||||
|
||||
await AddIncomeItemFormHelper.fillForm({
|
||||
amount: 12,
|
||||
date: '29.08.2025',
|
||||
description: 'Описание элемента',
|
||||
title: 'Название элемента',
|
||||
});
|
||||
});
|
||||
|
||||
test('should not close modal after submit', () => {
|
||||
expect(modalElem).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show error notification', () => {
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1);
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Ошибка при добавлении дохода', { variant: 'error' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -44,7 +44,7 @@ const AddIncomeItemButton: FC<AddIncomeItemButtonProps> = (props) => {
|
||||
Добавить
|
||||
</Button>
|
||||
|
||||
<Modal onClose={onCloseModalHandler} open={isOpen || isPending}>
|
||||
<Modal onClose={onCloseModalHandler} open={isOpen}>
|
||||
<AddIncomeItemForm disabled={isPending} onSubmit={onFormSubmitHandler} />
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import type { MouseEventHandler } from 'react';
|
||||
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { type IncomeListItem, useDeleteIncomeItem } from '@/entity/incomes/item';
|
||||
@ -28,14 +27,8 @@ export const useDeleteIncomeItemButton = (args: UseDeleteIncomeItemButtonArgs) =
|
||||
});
|
||||
|
||||
const onFormSubmitHandler = async () => {
|
||||
try {
|
||||
await deleteIncomeItemMutation();
|
||||
|
||||
enqueueSnackbar(`Доход "${item.title}" удален`, { variant: 'success' });
|
||||
setIsOpen(false);
|
||||
} catch {
|
||||
enqueueSnackbar(`Ошибка при удалении дохода "${item.title}"`, { variant: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { mockEnqueueSnackbar } from '@mocks/jest/helpers';
|
||||
import { server } from '@mocks/jest/server';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { DeleteIncomeItemButtonHelper, DeleteIncomeItemFormHelper } from '@tests/helpers';
|
||||
|
||||
describe('Test DeleteIncomeItemButton', () => {
|
||||
@ -23,51 +23,15 @@ describe('Test DeleteIncomeItemButton', () => {
|
||||
expect(modalElem).toBeVisible();
|
||||
});
|
||||
|
||||
describe('should close modal after success request', () => {
|
||||
let modalElem: HTMLElement;
|
||||
const requestSpy = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
server.use(DeleteIncomeItemButtonHelper.submitHandler(requestSpy));
|
||||
modalElem = (await DeleteIncomeItemButtonHelper.openModal()).modalElem;
|
||||
|
||||
await DeleteIncomeItemFormHelper.submitForm();
|
||||
});
|
||||
|
||||
test('should send correct request', () => {
|
||||
expect(requestSpy).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('should close modal after submit', async () => {
|
||||
server.use(DeleteIncomeItemButtonHelper.submitHandler);
|
||||
const { modalElem } = await DeleteIncomeItemButtonHelper.openModal();
|
||||
const { submitButtonElem } = DeleteIncomeItemFormHelper.getFormElements();
|
||||
|
||||
expect(submitButtonElem).toBeEnabled();
|
||||
await userEvent.click(submitButtonElem);
|
||||
await waitFor(() => {
|
||||
expect(modalElem).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should show success notification', () => {
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1);
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Доход "Mock item" удален', { variant: 'success' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('should not close modal after failed request', () => {
|
||||
let modalElem: HTMLElement;
|
||||
const requestSpy = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
server.use(DeleteIncomeItemButtonHelper.submitHandler(requestSpy, 500));
|
||||
modalElem = (await DeleteIncomeItemButtonHelper.openModal()).modalElem;
|
||||
|
||||
await DeleteIncomeItemFormHelper.submitForm();
|
||||
});
|
||||
|
||||
test('should not close modal after submit', () => {
|
||||
expect(modalElem).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show error notification', () => {
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1);
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Ошибка при удалении дохода "Mock item"', { variant: 'error' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -44,7 +44,7 @@ const DeleteIncomeItemButton: FC<DeleteIncomeItemButtonProps> = (props) => {
|
||||
</Button>
|
||||
|
||||
<Modal onClose={onCloseModalHandler} open={isOpen}>
|
||||
<DeleteIncomeItemForm disabled={isPending} onSubmit={onFormSubmitHandler} />
|
||||
<DeleteIncomeItemForm disabled={isPending} item={item} onSubmit={onFormSubmitHandler} />
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import type { MouseEventHandler } from 'react';
|
||||
|
||||
import { enqueueSnackbar } from 'notistack';
|
||||
import { useState } from 'react';
|
||||
|
||||
import { type EditIncomeItemFormValues, type IncomeListItem, useEditIncomeItem } from '@/entity/incomes/item';
|
||||
@ -28,29 +27,9 @@ export const useIncomeItemButton = (args: UseAddIncomeItemButtonArgs) => {
|
||||
};
|
||||
|
||||
const onFormSubmitHandler = async (data: EditIncomeItemFormValues) => {
|
||||
try {
|
||||
const target = data.targetType === 'none'
|
||||
? null
|
||||
: {
|
||||
id: data.targetType === 'BANK_CARD' ? data['bank-card'] : data.cash,
|
||||
type: data.targetType,
|
||||
};
|
||||
await editIncomeItemMutation({
|
||||
amount: data.amount,
|
||||
currency: 'RUB',
|
||||
date: data.date.toISOString(),
|
||||
description: data.description ?? null,
|
||||
// @ts-ignore TODO
|
||||
target,
|
||||
title: data.title,
|
||||
});
|
||||
|
||||
enqueueSnackbar(`Доход "${data.title}" успешно сохранен`, { variant: 'success' });
|
||||
|
||||
// todo добавить обработку ошибок (код + тест)
|
||||
await editIncomeItemMutation(data);
|
||||
setIsOpen(false);
|
||||
} catch {
|
||||
enqueueSnackbar('Ошибка при сохранении дохода', { variant: 'error' });
|
||||
}
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import { mockEnqueueSnackbar } from '@mocks/jest/helpers';
|
||||
import { server } from '@mocks/jest/server';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { EditIncomeItemButtonHelper, EditIncomeItemFormHelper } from '@tests/helpers';
|
||||
@ -23,7 +22,7 @@ describe('TestEditIncomeItemButton', () => {
|
||||
expect(modalElem).toBeVisible();
|
||||
});
|
||||
|
||||
describe('should close modal after success request', () => {
|
||||
describe('should close modal after submit and send request', () => {
|
||||
let modalElem: HTMLElement;
|
||||
const requestSpy = jest.fn();
|
||||
|
||||
@ -36,56 +35,26 @@ describe('TestEditIncomeItemButton', () => {
|
||||
date: '29.08.2025',
|
||||
description: 'Описание элемента',
|
||||
title: 'Название элемента',
|
||||
});
|
||||
}, { autoSubmit: false });
|
||||
});
|
||||
|
||||
test('should send correct request', () => {
|
||||
test('should send correct request', async () => {
|
||||
await EditIncomeItemFormHelper.submitForm();
|
||||
expect(requestSpy).toHaveBeenCalledTimes(1);
|
||||
expect(requestSpy).toHaveBeenCalledWith({
|
||||
amount: 12,
|
||||
currency: 'RUB',
|
||||
date: '2025-08-29T00:00:00.000Z',
|
||||
description: 'Описание элемента',
|
||||
target: null,
|
||||
title: 'Название элемента',
|
||||
});
|
||||
});
|
||||
|
||||
test('should close modal after submit', async () => {
|
||||
await EditIncomeItemFormHelper.submitForm();
|
||||
await waitFor(() => {
|
||||
expect(modalElem).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
test('should show success notification', () => {
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1);
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Доход "Название элемента" успешно сохранен', { variant: 'success' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('should not close modal after failed request', () => {
|
||||
let modalElem: HTMLElement;
|
||||
const requestSpy = jest.fn();
|
||||
|
||||
beforeEach(async () => {
|
||||
server.use(EditIncomeItemButtonHelper.submitHandler(requestSpy, 500));
|
||||
modalElem = (await EditIncomeItemButtonHelper.openModal()).modalElem;
|
||||
|
||||
await EditIncomeItemFormHelper.fillForm({
|
||||
amount: 12,
|
||||
date: '29.08.2025',
|
||||
description: 'Описание элемента',
|
||||
title: 'Название элемента',
|
||||
});
|
||||
});
|
||||
|
||||
test('should not close modal after submit', () => {
|
||||
expect(modalElem).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('should show error notification', () => {
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledTimes(1);
|
||||
expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Ошибка при сохранении дохода', { variant: 'error' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -45,7 +45,7 @@ const EditIncomeItemButton: FC<EditIncomeItemButtonProps> = (props) => {
|
||||
Редактировать
|
||||
</Button>
|
||||
|
||||
<Modal onClose={onCloseModalHandler} open={isOpen || isPending}>
|
||||
<Modal onClose={onCloseModalHandler} open={isOpen}>
|
||||
<EditIncomeItemForm disabled={isPending} item={item} onSubmit={onFormSubmitHandler} />
|
||||
</Modal>
|
||||
</>
|
||||
|
||||
@ -32,12 +32,7 @@ const ExpensePage = () => {
|
||||
return (
|
||||
<AppLayout>
|
||||
<Typography variant="h4">
|
||||
Элементы расхода
|
||||
{' '}
|
||||
|
||||
"
|
||||
{expenseList?.title}
|
||||
"
|
||||
Списки расходов
|
||||
</Typography>
|
||||
|
||||
<AddExpenseItemButton listId={id} sx={{ mb: 2, mt: 2 }} />
|
||||
|
||||
@ -32,12 +32,7 @@ const IncomePage = () => {
|
||||
return (
|
||||
<AppLayout>
|
||||
<Typography variant="h4">
|
||||
Элементы дохода
|
||||
{' '}
|
||||
|
||||
"
|
||||
{incomeList?.title}
|
||||
"
|
||||
Списки расходов
|
||||
</Typography>
|
||||
|
||||
<AddIncomeItemButton listId={id} sx={{ mb: 2, mt: 2 }} />
|
||||
|
||||
@ -754,19 +754,6 @@ export interface components {
|
||||
*/
|
||||
title: string;
|
||||
};
|
||||
IncomeSourceDto: {
|
||||
/**
|
||||
* @description Источник дохода
|
||||
* @example CASH
|
||||
* @enum {string}
|
||||
*/
|
||||
type: "BANK_CARD" | "CASH";
|
||||
/**
|
||||
* @description ID карты или наличных
|
||||
* @example 123
|
||||
*/
|
||||
id: string;
|
||||
};
|
||||
IncomeListItemDto: {
|
||||
/** @description ID */
|
||||
id: string;
|
||||
@ -799,8 +786,6 @@ export interface components {
|
||||
* @example RUB
|
||||
*/
|
||||
currency: string | null;
|
||||
/** @description Источник */
|
||||
target: components["schemas"]["IncomeSourceDto"] | null;
|
||||
/**
|
||||
* Format: date-time
|
||||
* @description Дата создания
|
||||
@ -869,8 +854,6 @@ export interface components {
|
||||
* @example RUB
|
||||
*/
|
||||
currency: string | null;
|
||||
/** @description Источник */
|
||||
target: components["schemas"]["IncomeSourceDto"] | null;
|
||||
};
|
||||
EditIncomeListItemDto: {
|
||||
/**
|
||||
|
||||
@ -6,4 +6,3 @@ export { formatRub } from './formatters/currency';
|
||||
export { formatDate } from './formatters/date';
|
||||
|
||||
export { getUTCDate } from './helpers/getUTCDate';
|
||||
export { setupZod } from './zod/config';
|
||||
|
||||
@ -1,27 +0,0 @@
|
||||
import * as z from 'zod';
|
||||
|
||||
export const setupZod = () => z.config({
|
||||
customError: (issue) => {
|
||||
if (issue.code === 'invalid_type') {
|
||||
if (typeof issue.input === 'undefined') {
|
||||
return {
|
||||
message: 'Обязательно к заполнению',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message: 'Неверный формат',
|
||||
};
|
||||
}
|
||||
|
||||
if (issue.code === 'too_small') {
|
||||
return {
|
||||
message: `Минимум ${issue.minimum} символов`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
message: 'Ошибка',
|
||||
};
|
||||
},
|
||||
});
|
||||
@ -15,7 +15,7 @@ const expenseItem = {
|
||||
count: 12,
|
||||
createdAt: '2025-08-29T07:57:40.503Z',
|
||||
currency: 'RUB',
|
||||
date: '2025-08-22T00:00:00.000Z',
|
||||
date: '2025-08-22T00:00:03.000Z',
|
||||
description: 'Mock description',
|
||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||
@ -48,7 +48,7 @@ const openModal = async () => {
|
||||
};
|
||||
};
|
||||
|
||||
const submitHandler = (requestSpy: jest.Mock, status = 200) => http.post<
|
||||
const submitHandler = (requestSpy: jest.Mock) => http.post<
|
||||
AddExpenseItemPath,
|
||||
AddExpenseItemBody,
|
||||
AddExpenseItemSuccessResponse
|
||||
@ -58,7 +58,7 @@ const submitHandler = (requestSpy: jest.Mock, status = 200) => http.post<
|
||||
const body = await request.json();
|
||||
requestSpy(body);
|
||||
|
||||
return HttpResponse.json(expenseItem, { status });
|
||||
return HttpResponse.json(expenseItem);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ const expenseItem = {
|
||||
count: 12,
|
||||
createdAt: '2025-08-29T07:57:40.503Z',
|
||||
currency: 'RUB',
|
||||
date: '2025-08-22T00:00:00.000Z',
|
||||
date: '2025-08-22T00:00:03.000Z',
|
||||
description: 'Mock description',
|
||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||
@ -45,13 +45,9 @@ const openModal = async () => {
|
||||
};
|
||||
};
|
||||
|
||||
const submitHandler = (requestSpy: jest.Mock, status = 200) => http.delete<DeleteExpenseItemPath>(
|
||||
const submitHandler = http.delete<DeleteExpenseItemPath>(
|
||||
`/expenses/list/${expenseItem.expenseListId}/items/${expenseItem.id}`,
|
||||
() => {
|
||||
requestSpy(expenseItem);
|
||||
|
||||
return HttpResponse.json(expenseItem, { status });
|
||||
},
|
||||
() => HttpResponse.json(expenseItem),
|
||||
);
|
||||
|
||||
export const DeleteExpenseItemButtonHelper = {
|
||||
|
||||
@ -11,7 +11,7 @@ const expenseItem = {
|
||||
count: 12,
|
||||
createdAt: '2025-08-29T07:57:40.503Z',
|
||||
currency: 'RUB',
|
||||
date: '2025-08-22T00:00:00.000Z',
|
||||
date: '2025-08-22T00:00:03.000Z',
|
||||
description: 'Mock description',
|
||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||
@ -25,6 +25,7 @@ const renderForm = (props?: Partial<DeleteExpenseItemFormProps>) => {
|
||||
|
||||
render(
|
||||
<DeleteExpenseItemForm
|
||||
item={expenseItem}
|
||||
onSubmit={onSubmit}
|
||||
{...props}
|
||||
/>,
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
import { getBankCardList, getCashList } from '@mocks/browser/handlers';
|
||||
import { server } from '@mocks/jest/server';
|
||||
import { createAppWrapper } from '@mocks/jest/wrappers';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
@ -19,7 +17,7 @@ const expenseItem = {
|
||||
count: 12,
|
||||
createdAt: '2025-08-29T07:57:40.503Z',
|
||||
currency: 'RUB',
|
||||
date: '2025-08-22T00:00:00.000Z',
|
||||
date: '2025-08-22T00:00:03.000Z',
|
||||
description: 'Mock description',
|
||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||
@ -29,7 +27,6 @@ const expenseItem = {
|
||||
};
|
||||
|
||||
const renderButton = () => {
|
||||
server.use(getBankCardList, getCashList);
|
||||
render(
|
||||
<EditExpenseItemButton item={expenseItem} />,
|
||||
{ wrapper: createAppWrapper() },
|
||||
@ -52,7 +49,7 @@ const openModal = async () => {
|
||||
};
|
||||
};
|
||||
|
||||
const submitHandler = (requestSpy: jest.Mock, status = 200) => http.patch<
|
||||
const submitHandler = (requestSpy: jest.Mock) => http.patch<
|
||||
EditExpenseItemPath,
|
||||
EditExpenseItemBody,
|
||||
EditExpenseItemSuccessResponse
|
||||
@ -62,7 +59,7 @@ const submitHandler = (requestSpy: jest.Mock, status = 200) => http.patch<
|
||||
const body = await request.json();
|
||||
requestSpy(body);
|
||||
|
||||
return HttpResponse.json(expenseItem, { status });
|
||||
return HttpResponse.json(expenseItem);
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@ -1,6 +1,4 @@
|
||||
import { getBankCardList, getCashList } from '@mocks/browser/handlers';
|
||||
import { typeDateField, typeTextField } from '@mocks/jest/helpers';
|
||||
import { server } from '@mocks/jest/server';
|
||||
import { createAppWrapper } from '@mocks/jest/wrappers';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
@ -15,7 +13,7 @@ const expenseItem = {
|
||||
count: 12,
|
||||
createdAt: '2025-08-22T07:57:40.503Z',
|
||||
currency: 'RUB',
|
||||
date: '2025-08-22T00:00:00.000Z',
|
||||
date: '2025-08-22T00:00:03.000Z',
|
||||
description: 'Mock description',
|
||||
expenseListId: '10cc093c-3875-4d33-a40a-df526266e262',
|
||||
id: '51a44dc0-597b-49b9-85bb-e0bcd42db3f6',
|
||||
@ -25,7 +23,6 @@ const expenseItem = {
|
||||
};
|
||||
|
||||
const renderForm = (props?: Partial<EditExpenseItemFormProps>) => {
|
||||
server.use(getBankCardList, getCashList);
|
||||
const onSubmit = jest.fn();
|
||||
|
||||
render(
|
||||
@ -49,27 +46,14 @@ const getFormElements = () => {
|
||||
const dateInputElem = screen.getAllByLabelText(/Дата/i)[1];
|
||||
const amountInputElem = screen.getByLabelText(/Сумма/);
|
||||
const countInputElem = screen.getByLabelText(/Количество/);
|
||||
const submitButtonElem = screen.getByTestId(DataTestId.EDIT_EXPENSE_ITEM_SUBMIT_BUTTON);
|
||||
|
||||
const paymentTypeSelect = screen.getByTestId('payment-type-select');
|
||||
|
||||
const bankCardSelect = screen.queryByTestId('bank-card-select');
|
||||
const bankCardSelectInput = screen.queryByTestId('bank-card-select-input');
|
||||
|
||||
const cashSelect = screen.queryByTestId('cash-select');
|
||||
const cashSelectInput = screen.queryByTestId('cash-select-input');
|
||||
const submitButtonElem = screen.getByRole('button', { name: /Сохранить/i });
|
||||
|
||||
return {
|
||||
amountInputElem,
|
||||
bankCardSelect,
|
||||
bankCardSelectInput,
|
||||
cashSelect,
|
||||
cashSelectInput,
|
||||
countInputElem,
|
||||
dateInputElem,
|
||||
descriptionInputElem,
|
||||
formElem,
|
||||
paymentTypeSelect,
|
||||
submitButtonElem,
|
||||
titleInputElem,
|
||||
};
|
||||
@ -81,7 +65,7 @@ const submitForm = async () => {
|
||||
};
|
||||
|
||||
const fillForm = async (
|
||||
data: Omit<EditExpenseItemFormValues, 'currency' | 'date'> & { date: string },
|
||||
data: Omit<EditExpenseItemFormValues, 'currency'>,
|
||||
options: { autoSubmit?: boolean } = {},
|
||||
) => {
|
||||
const {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user