init
This commit is contained in:
parent
2dd4559caf
commit
c118fce973
1006
package-lock.json
generated
1006
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@ -3,6 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@reduxjs/toolkit": "^1.9.3",
|
||||
"@testing-library/jest-dom": "^5.16.5",
|
||||
"@testing-library/react": "^13.4.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
@ -10,9 +11,15 @@
|
||||
"@types/node": "^16.18.14",
|
||||
"@types/react": "^18.0.28",
|
||||
"@types/react-dom": "^18.0.11",
|
||||
"@types/react-redux": "^7.1.25",
|
||||
"@types/redux-form": "^8.3.5",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-inlinesvg": "^3.0.2",
|
||||
"react-redux": "^8.0.5",
|
||||
"react-scripts": "5.0.1",
|
||||
"redux-form": "^8.3.9",
|
||||
"sass": "^1.58.3",
|
||||
"typescript": "^4.9.5",
|
||||
"web-vitals": "^2.1.4"
|
||||
},
|
||||
@ -39,5 +46,11 @@
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/bootstrap": "^5.2.6",
|
||||
"@types/react-bootstrap": "^0.32.32",
|
||||
"bootstrap": "^5.2.3",
|
||||
"react-bootstrap": "^2.7.2"
|
||||
}
|
||||
}
|
||||
|
25
src/App.tsx
25
src/App.tsx
@ -1,24 +1,15 @@
|
||||
import React from 'react';
|
||||
import logo from './logo.svg';
|
||||
import './App.css';
|
||||
|
||||
import {ControlReduxForm} from "./components/ControlForm/ControlForm";
|
||||
|
||||
import './App.scss';
|
||||
|
||||
|
||||
function App() {
|
||||
|
||||
return (
|
||||
<div className="App">
|
||||
<header className="App-header">
|
||||
<img src={logo} className="App-logo" alt="logo" />
|
||||
<p>
|
||||
Edit <code>src/App.tsx</code> and save to reload.
|
||||
</p>
|
||||
<a
|
||||
className="App-link"
|
||||
href="https://reactjs.org"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Learn React
|
||||
</a>
|
||||
</header>
|
||||
<div>
|
||||
<ControlReduxForm/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
213
src/components/ControlForm/ControlForm.tsx
Normal file
213
src/components/ControlForm/ControlForm.tsx
Normal file
@ -0,0 +1,213 @@
|
||||
import React, {FC, FormEvent, useEffect, useState} from "react";
|
||||
import {Field, InjectedFormProps, reduxForm} from "redux-form";
|
||||
|
||||
import {useAppSelector} from "../../hooks/redux";
|
||||
|
||||
import {StyledSwitch} from "../StyledSwitch/StyledSwitch";
|
||||
import {StyledInput} from "../StyledInput/StyledInput";
|
||||
import {CustomTooltip} from "../CustomTooltip/CustomTooltip";
|
||||
|
||||
import {number, required} from "../../validators/validators";
|
||||
|
||||
import './controlForm.scss'
|
||||
|
||||
interface FormProps {
|
||||
sumType: string
|
||||
salary: number
|
||||
ndfl: boolean
|
||||
}
|
||||
|
||||
type DataType = Record<string, {
|
||||
value: string;
|
||||
label: string;
|
||||
tooltip: boolean;
|
||||
calculateFunction: {
|
||||
multiplication: (a: number, b: number) => number;
|
||||
division: (a: number, b: number) => number;
|
||||
}
|
||||
}>
|
||||
|
||||
|
||||
const dataArr: DataType = {
|
||||
'1': {
|
||||
value: '1',
|
||||
label: 'Оклад за месяц',
|
||||
tooltip: false,
|
||||
|
||||
calculateFunction: {
|
||||
multiplication: () => {
|
||||
return 0
|
||||
},
|
||||
division: (salary, divider) => {
|
||||
return divider === 3 ? salary / 160 : salary / 20
|
||||
}
|
||||
}
|
||||
},
|
||||
'2': {
|
||||
value: '2',
|
||||
label: 'МРОТ',
|
||||
tooltip: true,
|
||||
calculateFunction: {
|
||||
multiplication: () => {
|
||||
return 0
|
||||
},
|
||||
division: () => {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
},
|
||||
'3': {
|
||||
value: '3',
|
||||
label: 'Оплата за день',
|
||||
tooltip: false,
|
||||
calculateFunction: {
|
||||
multiplication: (salary) => {
|
||||
return salary * 20
|
||||
},
|
||||
division: (salary) => {
|
||||
return salary / 8
|
||||
}
|
||||
}
|
||||
},
|
||||
'4': {
|
||||
value: '4',
|
||||
label: 'Оплата за час',
|
||||
tooltip: false,
|
||||
calculateFunction: {
|
||||
multiplication: (salary, multiplier) => {
|
||||
return multiplier === 3 ? salary * 8 * 20 : salary * 8
|
||||
},
|
||||
division: () => {
|
||||
return 0
|
||||
}
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
let ControlForm: FC<InjectedFormProps<FormProps>> = (props) => {
|
||||
|
||||
const {control} = useAppSelector(state => state.form);
|
||||
const [, setFormValue] = useState<string>(control?.values?.sumType);
|
||||
|
||||
const handlerSubmit = (e: FormEvent) => {
|
||||
e.preventDefault()
|
||||
};
|
||||
|
||||
|
||||
|
||||
useEffect(() => {
|
||||
setFormValue((prevSumType) => {
|
||||
|
||||
const sumType = control?.values?.sumType;
|
||||
|
||||
if (control?.values?.sumType === '2')
|
||||
return prevSumType;
|
||||
|
||||
if (prevSumType < control?.values?.sumType) {
|
||||
|
||||
const currentSallary = dataArr[prevSumType].calculateFunction.division(control?.values?.salary, sumType - Number(prevSumType));
|
||||
props.change('salary', currentSallary);
|
||||
return sumType
|
||||
} else if (prevSumType > control?.values?.sumType) {
|
||||
|
||||
const currentSallary = dataArr[prevSumType].calculateFunction.multiplication(control?.values?.salary, Number(prevSumType) - sumType);
|
||||
props.change('salary', currentSallary);
|
||||
return sumType
|
||||
}
|
||||
|
||||
return prevSumType
|
||||
});
|
||||
|
||||
|
||||
}, [control?.values?.sumType]);
|
||||
|
||||
|
||||
function getSalary(salary: number | string, ndfl: boolean): number {
|
||||
return ndfl ? Math.round(Number(salary)) : Math.round(Number(salary) * 0.87)
|
||||
}
|
||||
|
||||
function getNdflPercent(salary: number | string, ndfl: boolean): number {
|
||||
return ndfl ? Math.round(Number(salary) * 100 / 87 - Number(salary)) : Math.round(Number(salary) * 0.13)
|
||||
}
|
||||
|
||||
function getFullSalary(salary: number | string, ndfl: boolean): number {
|
||||
return ndfl ? Math.round(Number(salary) * 100 / 87) : Math.round(Number(salary))
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<>
|
||||
<form onSubmit={handlerSubmit} className='controlForm'>
|
||||
<h5 className='controlForm__title'>Сумма</h5>
|
||||
{Object.entries(dataArr).map(([key, i],) =>
|
||||
<label key={`controlForm__radioButton${key}`} className='controlForm__radioButton'>
|
||||
<Field
|
||||
component={'input'}
|
||||
type='radio'
|
||||
value={i.value}
|
||||
name='sumType'
|
||||
/>
|
||||
{i.label}
|
||||
{i.tooltip && <CustomTooltip/>}
|
||||
|
||||
</label>
|
||||
)}
|
||||
|
||||
|
||||
<label className='controlForm__inputWrapper'>
|
||||
<span className='title'>Указать с НДФЛ</span>
|
||||
<Field
|
||||
style={{margin: '10px'}}
|
||||
component={StyledSwitch}
|
||||
defaultChecked={true}
|
||||
name='ndfl'
|
||||
label={'Без НДФЛ'}
|
||||
/>
|
||||
|
||||
</label>
|
||||
<label className='controlForm__inputWrapper'>
|
||||
|
||||
<Field
|
||||
validate={[required, number]}
|
||||
component={StyledInput}
|
||||
name='salary'
|
||||
type={'number'}
|
||||
/>
|
||||
<span className='currency'>₽</span>
|
||||
|
||||
</label>
|
||||
|
||||
</form>
|
||||
{control?.values?.sumType == 1 &&
|
||||
<div className='calculateHint'>
|
||||
<p>
|
||||
<span
|
||||
className='calculateHint__value'>{getSalary(control.values.salary, control.values.ndfl)}
|
||||
</span>
|
||||
сотрудник будет получать на руки</p>
|
||||
<p>
|
||||
<span
|
||||
className='calculateHint__value'>{getNdflPercent(control.values.salary, control.values.ndfl)}
|
||||
</span>
|
||||
НДФЛ 13 % от оклада</p>
|
||||
<p>
|
||||
<span
|
||||
className='calculateHint__value'>{getFullSalary(control.values.salary, control.values.ndfl)}
|
||||
</span>
|
||||
сотрудник будет получать на руки</p>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
)
|
||||
};
|
||||
|
||||
|
||||
export const ControlReduxForm = reduxForm({
|
||||
initialValues: {
|
||||
sumType: '1',
|
||||
salary: 40000,
|
||||
ndfl: true
|
||||
},
|
||||
form: 'control'
|
||||
})(ControlForm);
|
60
src/components/ControlForm/controlForm.scss
Normal file
60
src/components/ControlForm/controlForm.scss
Normal file
@ -0,0 +1,60 @@
|
||||
.controlForm {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-weight: 600;
|
||||
padding: 20px;
|
||||
max-width: 300px;
|
||||
|
||||
&__title {
|
||||
font-size: 12px;
|
||||
color: #7e7f81;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
&__radioButton {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
input {
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.tooltip__button {
|
||||
margin-left: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
&__inputWrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: #7e7f81;
|
||||
font-size: 12px;
|
||||
margin-left: 35px;
|
||||
|
||||
.title {
|
||||
margin-bottom: -10px;
|
||||
}
|
||||
|
||||
.currency {
|
||||
color: black;
|
||||
font-size: 17px;
|
||||
}
|
||||
|
||||
.styledInput {
|
||||
|
||||
margin-right: 10px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.calculateHint {
|
||||
background-color: rgb(226 203 173 / 58%);
|
||||
max-width: 400px;
|
||||
padding: 20px;
|
||||
margin: 20px;
|
||||
|
||||
&__value {
|
||||
margin-right: 5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
52
src/components/CustomTooltip/CustomTooltip.tsx
Normal file
52
src/components/CustomTooltip/CustomTooltip.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import React, {useRef, useState} from "react";
|
||||
import Tooltip from 'react-bootstrap/Tooltip';
|
||||
import Overlay from "react-bootstrap/Overlay";
|
||||
import OverlayTrigger from "react-bootstrap/cjs/OverlayTrigger";
|
||||
import SVG from 'react-inlinesvg';
|
||||
|
||||
import {TooltipProps} from "react-bootstrap";
|
||||
|
||||
import './tooltip.scss'
|
||||
|
||||
import info from './info.svg'
|
||||
import cross from './cross.svg'
|
||||
|
||||
|
||||
export const CustomTooltip = () => {
|
||||
const [show, setShow] = useState(false);
|
||||
const target = useRef(null);
|
||||
|
||||
const renderTooltip = (props: TooltipProps) => {
|
||||
return (
|
||||
<Tooltip className='tooltip' id="button-tooltip" {...props}>
|
||||
МРОТ - минимальный размер оплаты труда. Разный для разных регионов
|
||||
</Tooltip>
|
||||
)
|
||||
};
|
||||
|
||||
const clickHandler = () => {
|
||||
setShow(!show);
|
||||
};
|
||||
|
||||
return <>
|
||||
|
||||
<Overlay target={target.current} show={show} placement="auto-start">
|
||||
{(props) => renderTooltip(props)}
|
||||
</Overlay>
|
||||
<div ref={target}>
|
||||
<OverlayTrigger
|
||||
trigger={['hover', 'focus']}
|
||||
placement="auto-start"
|
||||
delay={{show: 0, hide: 0}}
|
||||
overlay={show ? <></> : (renderTooltip)}>
|
||||
|
||||
<div className='tooltip__button' onClick={clickHandler}>
|
||||
|
||||
<SVG width={20} height={20} src={show ? cross : info}/>
|
||||
|
||||
</div>
|
||||
|
||||
</OverlayTrigger>
|
||||
</div>
|
||||
</>
|
||||
};
|
4
src/components/CustomTooltip/cross.svg
Normal file
4
src/components/CustomTooltip/cross.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M16.9498 8.46447C17.3404 8.07394 17.3404 7.44078 16.9498 7.05025C16.5593 6.65973 15.9261 6.65973 15.5356 7.05025L12.0001 10.5858L8.46455 7.05025C8.07402 6.65973 7.44086 6.65973 7.05033 7.05025C6.65981 7.44078 6.65981 8.07394 7.05033 8.46447L10.5859 12L7.05033 15.5355C6.65981 15.9261 6.65981 16.5592 7.05033 16.9497C7.44086 17.3403 8.07402 17.3403 8.46455 16.9497L12.0001 13.4142L15.5356 16.9497C15.9261 17.3403 16.5593 17.3403 16.9498 16.9497C17.3404 16.5592 17.3404 15.9261 16.9498 15.5355L13.4143 12L16.9498 8.46447Z" fill="#000000"/>
|
||||
</svg>
|
After Width: | Height: | Size: 814 B |
4
src/components/CustomTooltip/info.svg
Normal file
4
src/components/CustomTooltip/info.svg
Normal file
@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M12 7.01002L12 7.00003M12 17L12 10" stroke="#000000" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
After Width: | Height: | Size: 357 B |
30
src/components/CustomTooltip/tooltip.scss
Normal file
30
src/components/CustomTooltip/tooltip.scss
Normal file
@ -0,0 +1,30 @@
|
||||
.tooltip {
|
||||
|
||||
background-color: #2b4ea5!important;
|
||||
|
||||
&-arrow, &-inner {
|
||||
|
||||
background-color: #2b4ea5!important;
|
||||
}
|
||||
|
||||
&-inner {
|
||||
|
||||
text-align: left;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
&__button {
|
||||
cursor: pointer;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50px;
|
||||
border: 1px solid #b0b4a7;
|
||||
position: relative;
|
||||
|
||||
svg {
|
||||
position: absolute;
|
||||
top: -1px;
|
||||
left: -1px;
|
||||
}
|
||||
}
|
||||
}
|
26
src/components/StyledInput/StyledInput.tsx
Normal file
26
src/components/StyledInput/StyledInput.tsx
Normal file
@ -0,0 +1,26 @@
|
||||
import {Form} from "react-bootstrap"
|
||||
import React, {FC} from "react";
|
||||
import './styledInput.scss'
|
||||
|
||||
|
||||
interface InputProps {
|
||||
meta: {
|
||||
touched: boolean | undefined
|
||||
error: string | undefined
|
||||
warning: string | undefined
|
||||
}
|
||||
input: {
|
||||
onChange: () => void
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const StyledInput: FC<InputProps> = ({input, meta: {touched, error, warning, }, ...props}) => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Form.Control {...props} {...input} className='styledInput' size="sm"/>
|
||||
{touched && ((error && <span>{error}</span>) || (warning && <span>{warning}</span>))}
|
||||
</div>
|
||||
)
|
||||
};
|
6
src/components/StyledInput/styledInput.scss
Normal file
6
src/components/StyledInput/styledInput.scss
Normal file
@ -0,0 +1,6 @@
|
||||
.styledInput {
|
||||
max-width: 150px;
|
||||
border-radius: 50px;
|
||||
display: flex;
|
||||
|
||||
}
|
33
src/components/StyledSwitch/StyledSwitch.tsx
Normal file
33
src/components/StyledSwitch/StyledSwitch.tsx
Normal file
@ -0,0 +1,33 @@
|
||||
import React, {CSSProperties, FC, ReactNode} from 'react';
|
||||
|
||||
import {Form} from 'react-bootstrap';
|
||||
|
||||
import './styledSwitch.scss'
|
||||
|
||||
interface Props {
|
||||
label?: string
|
||||
value: string
|
||||
name: string
|
||||
defaultChecked?: boolean
|
||||
disabled?: boolean
|
||||
children?: ReactNode
|
||||
style?: CSSProperties
|
||||
meta?: {}
|
||||
input: {
|
||||
onChange: () => void
|
||||
}
|
||||
}
|
||||
|
||||
export const StyledSwitch: FC<Props> =
|
||||
({
|
||||
input,
|
||||
meta,
|
||||
...props
|
||||
}) =>
|
||||
|
||||
<Form.Switch
|
||||
{...input}
|
||||
{...props}
|
||||
|
||||
/>
|
||||
;
|
27
src/components/StyledSwitch/styledSwitch.scss
Normal file
27
src/components/StyledSwitch/styledSwitch.scss
Normal file
@ -0,0 +1,27 @@
|
||||
.form-check-input {
|
||||
border: 0 solid rgba(0,0,0,.25)!important; ;
|
||||
}
|
||||
.form-check-input {
|
||||
background-color: #e8e3e3!important;;
|
||||
border-color: #e8e3e3!important;;
|
||||
}
|
||||
.form-check-input:checked {
|
||||
background-color: #fd650c!important;
|
||||
border-color: #fd650c!important;;
|
||||
}
|
||||
.form-switch .form-check-input {
|
||||
width: 30px!important;;
|
||||
height: 19px!important;;
|
||||
}
|
||||
.form-check-input:focus{
|
||||
box-shadow: 0 0 0 0.25rem rgb(219 253 13 / 25%)!important;
|
||||
}
|
||||
.form-switch {
|
||||
display: flex!important;;
|
||||
align-items: end!important;;
|
||||
label{
|
||||
color: black!important;;
|
||||
margin-bottom: -2px!important;;
|
||||
margin-left: 10px!important;;
|
||||
}
|
||||
}
|
5
src/hooks/redux.ts
Normal file
5
src/hooks/redux.ts
Normal file
@ -0,0 +1,5 @@
|
||||
import {AppDispatch, RootState} from "../store/store";
|
||||
import {TypedUseSelectorHook, useDispatch, useSelector} from "react-redux";
|
||||
|
||||
export const useAppDispatch = () => useDispatch<AppDispatch>();
|
||||
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
|
@ -1,16 +1,25 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './App';
|
||||
import {Provider} from "react-redux";
|
||||
import reportWebVitals from './reportWebVitals';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
|
||||
import App from './App';
|
||||
|
||||
import {setupStore} from "./store/store";
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import './index.css';
|
||||
|
||||
const root = ReactDOM.createRoot(
|
||||
document.getElementById('root') as HTMLElement
|
||||
);
|
||||
|
||||
const store = setupStore();
|
||||
|
||||
root.render(
|
||||
<React.StrictMode>
|
||||
<Provider store={store}>
|
||||
<App/>
|
||||
</React.StrictMode>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
// If you want to start measuring performance in your app, pass a function
|
||||
|
24
src/store/store.ts
Normal file
24
src/store/store.ts
Normal file
@ -0,0 +1,24 @@
|
||||
import {configureStore, combineReducers} from "@reduxjs/toolkit";
|
||||
|
||||
import { reducer as formReducer } from 'redux-form'
|
||||
// import formSlice from "./reducers/formSlice";
|
||||
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
form: formReducer,
|
||||
// control: formSlice,
|
||||
|
||||
});
|
||||
|
||||
|
||||
export const setupStore = () => {
|
||||
return configureStore(
|
||||
{
|
||||
reducer: rootReducer,
|
||||
})
|
||||
};
|
||||
|
||||
|
||||
export type RootState = ReturnType<typeof rootReducer>
|
||||
export type AppStore = ReturnType<typeof setupStore>
|
||||
export type AppDispatch = AppStore['dispatch']
|
2
src/validators/validators.ts
Normal file
2
src/validators/validators.ts
Normal file
@ -0,0 +1,2 @@
|
||||
export const required = (value:any) => value ? undefined : 'Required';
|
||||
export const number = (value:any) => value && isNaN(Number(value)) ? 'Must be a number' : undefined
|
Loading…
Reference in New Issue
Block a user