This commit is contained in:
Дмитрий Савенко 2023-03-13 22:26:59 +03:00
parent 2dd4559caf
commit c118fce973
18 changed files with 1382 additions and 177 deletions

1006
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -3,6 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"dependencies": { "dependencies": {
"@reduxjs/toolkit": "^1.9.3",
"@testing-library/jest-dom": "^5.16.5", "@testing-library/jest-dom": "^5.16.5",
"@testing-library/react": "^13.4.0", "@testing-library/react": "^13.4.0",
"@testing-library/user-event": "^13.5.0", "@testing-library/user-event": "^13.5.0",
@ -10,9 +11,15 @@
"@types/node": "^16.18.14", "@types/node": "^16.18.14",
"@types/react": "^18.0.28", "@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11", "@types/react-dom": "^18.0.11",
"@types/react-redux": "^7.1.25",
"@types/redux-form": "^8.3.5",
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-inlinesvg": "^3.0.2",
"react-redux": "^8.0.5",
"react-scripts": "5.0.1", "react-scripts": "5.0.1",
"redux-form": "^8.3.9",
"sass": "^1.58.3",
"typescript": "^4.9.5", "typescript": "^4.9.5",
"web-vitals": "^2.1.4" "web-vitals": "^2.1.4"
}, },
@ -39,5 +46,11 @@
"last 1 firefox version", "last 1 firefox version",
"last 1 safari 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"
} }
} }

View File

@ -1,24 +1,15 @@
import React from 'react'; import React from 'react';
import logo from './logo.svg';
import './App.css'; import {ControlReduxForm} from "./components/ControlForm/ControlForm";
import './App.scss';
function App() { function App() {
return ( return (
<div className="App"> <div>
<header className="App-header"> <ControlReduxForm/>
<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> </div>
); );
} }

View 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);

View 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;
}
}

View 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>
</>
};

View 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

View 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

View 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;
}
}
}

View 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>
)
};

View File

@ -0,0 +1,6 @@
.styledInput {
max-width: 150px;
border-radius: 50px;
display: flex;
}

View 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}
/>
;

View 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
View 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;

View File

@ -1,16 +1,25 @@
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom/client'; import {Provider} from "react-redux";
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals'; 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( const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement document.getElementById('root') as HTMLElement
); );
const store = setupStore();
root.render( root.render(
<React.StrictMode> <Provider store={store}>
<App /> <App/>
</React.StrictMode> </Provider>
); );
// If you want to start measuring performance in your app, pass a function // If you want to start measuring performance in your app, pass a function

24
src/store/store.ts Normal file
View 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']

View 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