Compare commits
27 Commits
dev
...
migration/
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cf7189123e | ||
|
|
e0de887c45 | ||
|
|
9b9e27bd74 | ||
|
|
4749c2167f | ||
|
|
de26e39eff | ||
|
|
600bb1a730 | ||
|
|
073dd1ac01 | ||
|
|
310736f050 | ||
|
|
333fcbd8b4 | ||
|
|
ca8d060851 | ||
|
|
9cac288dcc | ||
|
|
4acbcde1cc | ||
|
|
5a25b342f6 | ||
|
|
ab89ca3475 | ||
|
|
3cb0a6a73c | ||
|
|
2d2d5d6cc3 | ||
|
|
adf186d8da | ||
|
|
6107e0c15f | ||
|
|
8f47cc44f8 | ||
|
|
c8afffe5b2 | ||
|
|
2b6960de45 | ||
|
|
326257cd7c | ||
|
|
375e59fb6a | ||
|
|
9bedfe8fab | ||
|
|
7c40e2cf4f | ||
|
|
048da7947a | ||
|
|
eb7dc997e4 |
45
Components/Calculation/Form/Payments/PaymentsParams.jsx
Normal file
45
Components/Calculation/Form/Payments/PaymentsParams.jsx
Normal file
@ -0,0 +1,45 @@
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import { useStore } from 'stores/hooks';
|
||||
import { Flex } from 'UIKit/grid';
|
||||
import elementsRender from '../../config/elements-render';
|
||||
import { elements } from './config';
|
||||
|
||||
function PaymentsParams() {
|
||||
const renderedElements = elements.map((elementName) => {
|
||||
const render = elementsRender[elementName]?.render;
|
||||
|
||||
return render();
|
||||
});
|
||||
|
||||
const [selectSeasonType, tbxParmentsDecreasePercent, selectHighSeasonStart] = renderedElements;
|
||||
|
||||
const { $calculation } = useStore();
|
||||
const graphType = $calculation.getElementValue('radioGraphType');
|
||||
|
||||
switch (graphType) {
|
||||
case 100_000_000:
|
||||
return null;
|
||||
case 100_000_001: {
|
||||
return <Flex flexDirection="column">{selectSeasonType}</Flex>;
|
||||
}
|
||||
case 100_000_002: {
|
||||
return <Flex flexDirection="column">{tbxParmentsDecreasePercent}</Flex>;
|
||||
}
|
||||
case 100_000_003: {
|
||||
return (
|
||||
<Flex flexDirection="column">
|
||||
{selectSeasonType}
|
||||
{selectHighSeasonStart}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
case 100_000_004: {
|
||||
return null;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export default observer(PaymentsParams);
|
||||
@ -4,7 +4,6 @@ export const id = 'payments';
|
||||
export const title = 'Платежи';
|
||||
|
||||
export const elements: ElementsRow['0'] = [
|
||||
'radioGraphType',
|
||||
'selectSeasonType',
|
||||
'tbxParmentsDecreasePercent',
|
||||
'selectHighSeasonStart',
|
||||
|
||||
@ -1,18 +1,11 @@
|
||||
import { Box, Flex } from 'UIKit/grid';
|
||||
import elementsRender from '../../config/elements-render';
|
||||
import { elements, id, title } from './config';
|
||||
import { id, title } from './config';
|
||||
import PaymentsParams from './PaymentsParams';
|
||||
import PaymentsTable from './PaymentsTable';
|
||||
|
||||
function Payments() {
|
||||
const renderedElements = elements.map((elementName) => {
|
||||
const render = elementsRender[elementName]?.render;
|
||||
|
||||
return render();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line operator-linebreak
|
||||
const [radioGraphType, selectSeasonType, tbxParmentsDecreasePercent, selectHighSeasonStart] =
|
||||
renderedElements;
|
||||
const radioGraphType = elementsRender.radioGraphType.render();
|
||||
|
||||
return (
|
||||
<Flex flexDirection="column">
|
||||
@ -24,11 +17,7 @@ function Payments() {
|
||||
}}
|
||||
>
|
||||
{radioGraphType}
|
||||
<Flex flexDirection="column">
|
||||
{selectSeasonType}
|
||||
{tbxParmentsDecreasePercent}
|
||||
{selectHighSeasonStart}
|
||||
</Flex>
|
||||
<PaymentsParams />
|
||||
</Box>
|
||||
<PaymentsTable />
|
||||
</Flex>
|
||||
|
||||
@ -2,7 +2,9 @@
|
||||
import { Container, Head } from 'Components/Layout/Element';
|
||||
import Link from 'Elements/Link';
|
||||
import Tooltip from 'Elements/Tooltip';
|
||||
import { observer } from 'mobx-react-lite';
|
||||
import type { ComponentProps } from 'react';
|
||||
import { useStore } from 'stores/hooks';
|
||||
import buildReadonly from '../../builders/build-readonly';
|
||||
import builders from '../elements-builders';
|
||||
import components from '../elements-components';
|
||||
@ -162,6 +164,42 @@ const overrideRender: Partial<Record<keyof typeof map, RenderProps>> = {
|
||||
);
|
||||
},
|
||||
},
|
||||
|
||||
selectSeasonType: {
|
||||
render: () => {
|
||||
const elementName = 'selectSeasonType';
|
||||
const valueName = map.selectSeasonType;
|
||||
const Component = components.selectSeasonType;
|
||||
const props = elementsProps.selectSeasonType;
|
||||
const builder = builders.selectSeasonType;
|
||||
|
||||
const Element = builder(Component, {
|
||||
elementName,
|
||||
valueName,
|
||||
});
|
||||
|
||||
const Title = observer(() => {
|
||||
const { $calculation } = useStore();
|
||||
const graphType = $calculation.getElementValue('radioGraphType');
|
||||
|
||||
switch (graphType) {
|
||||
case 100_000_001:
|
||||
return <span>Тип дегрессии</span>;
|
||||
case 100_000_003:
|
||||
return <span>Тип сезонности</span>;
|
||||
default:
|
||||
return <span>{titles.selectSeasonType}</span>;
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<Head htmlFor={elementName} title={<Title />} />
|
||||
<Element {...props} id={elementName} />
|
||||
</Container>
|
||||
);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default overrideRender;
|
||||
|
||||
@ -23,7 +23,7 @@ export function Head({
|
||||
addon,
|
||||
htmlFor,
|
||||
}: {
|
||||
title: string;
|
||||
title: ReactNode;
|
||||
addon?: ReactNode;
|
||||
htmlFor: string;
|
||||
}) {
|
||||
|
||||
@ -1,6 +1,51 @@
|
||||
import { alphabetical } from 'radash/dist/array';
|
||||
import type { CalculationOptions } from 'stores/calculation/options/types';
|
||||
|
||||
export const selectSeasonType = [
|
||||
{
|
||||
label: '6/6',
|
||||
value: 100_000_000,
|
||||
},
|
||||
{
|
||||
label: '8/4',
|
||||
value: 100_000_001,
|
||||
},
|
||||
{
|
||||
label: '4/4/4',
|
||||
value: 100_000_002,
|
||||
},
|
||||
{
|
||||
label: 'Гибкая дегрессия',
|
||||
value: 100_000_007,
|
||||
},
|
||||
{
|
||||
label: '100.50.25',
|
||||
value: 100_000_003,
|
||||
},
|
||||
{
|
||||
label: '100.30.10',
|
||||
value: 100_000_004,
|
||||
},
|
||||
{
|
||||
label: '100.70.40',
|
||||
value: 100_000_005,
|
||||
},
|
||||
{
|
||||
label: '100.7.3',
|
||||
value: 100_000_006,
|
||||
},
|
||||
];
|
||||
|
||||
export const selectHighSeasonStart = Array.from(
|
||||
{
|
||||
length: 12,
|
||||
},
|
||||
(_, i) => ({
|
||||
label: `${i + 2}`,
|
||||
value: 100_000_000 + i,
|
||||
})
|
||||
);
|
||||
|
||||
const defaultOptions: CalculationOptions = {
|
||||
radioLastPaymentRule: [
|
||||
{
|
||||
@ -51,50 +96,9 @@ const defaultOptions: CalculationOptions = {
|
||||
},
|
||||
],
|
||||
|
||||
selectSeasonType: [
|
||||
{
|
||||
label: '6/6',
|
||||
value: 100_000_000,
|
||||
},
|
||||
{
|
||||
label: '8/4',
|
||||
value: 100_000_001,
|
||||
},
|
||||
{
|
||||
label: '4/4/4',
|
||||
value: 100_000_002,
|
||||
},
|
||||
{
|
||||
label: 'Гибкая дегрессия',
|
||||
value: 100_000_007,
|
||||
},
|
||||
{
|
||||
label: '100.50.25',
|
||||
value: 100_000_003,
|
||||
},
|
||||
{
|
||||
label: '100.30.10',
|
||||
value: 100_000_004,
|
||||
},
|
||||
{
|
||||
label: '100.70.40',
|
||||
value: 100_000_005,
|
||||
},
|
||||
{
|
||||
label: '100.7.3',
|
||||
value: 100_000_006,
|
||||
},
|
||||
],
|
||||
selectSeasonType,
|
||||
|
||||
selectHighSeasonStart: Array.from(
|
||||
{
|
||||
length: 12,
|
||||
},
|
||||
(_, i) => ({
|
||||
label: `${i + 2}`,
|
||||
value: 100_000_000 + i,
|
||||
})
|
||||
),
|
||||
selectHighSeasonStart,
|
||||
|
||||
radioDeliveryTime: [
|
||||
{
|
||||
|
||||
@ -23,8 +23,8 @@ const defaultValues: CalculationValues = {
|
||||
balanceHolder: 100_000_001,
|
||||
graphType: 100_000_000,
|
||||
parmentsDecreasePercent: 94,
|
||||
seasonType: 100_000_000,
|
||||
highSeasonStart: 100_000_000,
|
||||
seasonType: null,
|
||||
highSeasonStart: null,
|
||||
comissionPerc: 0,
|
||||
comissionRub: 0,
|
||||
saleBonus: 1.3,
|
||||
|
||||
@ -26,9 +26,7 @@ export default function validationReactions(
|
||||
({ hasPaymentsErrors, finGAPInsuranceCompany }) => {
|
||||
if (finGAPInsuranceCompany && hasPaymentsErrors) {
|
||||
removeError = $tables.fingap.validation.addError(errorText);
|
||||
} else {
|
||||
removeError();
|
||||
}
|
||||
} else if (removeError) removeError();
|
||||
},
|
||||
{
|
||||
fireImmediately: true,
|
||||
|
||||
25
process/payments/lib/__tests__/seasons.test.js
Normal file
25
process/payments/lib/__tests__/seasons.test.js
Normal file
@ -0,0 +1,25 @@
|
||||
import { getPositionIndex } from '../seasons-tools';
|
||||
|
||||
describe('process/payments/lib/seasons-tools', () => {
|
||||
describe('[function] getPositionIndex', () => {
|
||||
test('should return 0', () => {
|
||||
const result = getPositionIndex(100_000_001, 0);
|
||||
expect(result).toEqual(0);
|
||||
});
|
||||
|
||||
test('should return 0', () => {
|
||||
const result = getPositionIndex(100_000_001, 7);
|
||||
expect(result).toEqual(0);
|
||||
});
|
||||
|
||||
test('should return 1', () => {
|
||||
const result = getPositionIndex(100_000_001, 8);
|
||||
expect(result).toEqual(1);
|
||||
});
|
||||
|
||||
test('should return 1', () => {
|
||||
const result = getPositionIndex(100_000_001, 11);
|
||||
expect(result).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
15
process/payments/lib/seasons-constants.ts
Normal file
15
process/payments/lib/seasons-constants.ts
Normal file
@ -0,0 +1,15 @@
|
||||
export const SEASONS_PERIODS: Record<number, Array<number>> = {
|
||||
100_000_000: [0, 6],
|
||||
100_000_001: [0, 8],
|
||||
100_000_002: [0, 4, 8],
|
||||
};
|
||||
|
||||
export const FORBIDDEN_HIGH_SEASON_START: Record<number, Array<string>> = {
|
||||
100_000_000: ['9', '10'],
|
||||
100_000_001: ['7', '8', '9'],
|
||||
100_000_002: ['11'],
|
||||
};
|
||||
|
||||
export const SEASONS_PERIOD_NUMBER = 12;
|
||||
|
||||
export const DEFAULT_SEASONS_VALUES = [100, 75, 50];
|
||||
55
process/payments/lib/seasons-tools.ts
Normal file
55
process/payments/lib/seasons-tools.ts
Normal file
@ -0,0 +1,55 @@
|
||||
/* eslint-disable implicit-arrow-linebreak */
|
||||
import type { CalculationValues } from 'stores/calculation/values/types';
|
||||
import { SEASONS_PERIODS, SEASONS_PERIOD_NUMBER } from './seasons-constants';
|
||||
|
||||
type SeasonType = NonNullable<CalculationValues['seasonType']>;
|
||||
type LeasingPeriod = CalculationValues['leasingPeriod'];
|
||||
|
||||
export function getPositionIndex(seasonType: SeasonType, seasonsIndex: number) {
|
||||
let positionIndex = 0;
|
||||
|
||||
SEASONS_PERIODS[seasonType].forEach((position, i) => {
|
||||
if (seasonsIndex >= position) {
|
||||
positionIndex = i;
|
||||
}
|
||||
});
|
||||
|
||||
return positionIndex;
|
||||
}
|
||||
|
||||
export function getSeasonsValues(seasonType: SeasonType, seasons: Array<number>) {
|
||||
return SEASONS_PERIODS[seasonType].map((index) => seasons[index]);
|
||||
}
|
||||
|
||||
export function generateSeasons(seasonType: SeasonType, values: Array<number>) {
|
||||
const positions = SEASONS_PERIODS[seasonType];
|
||||
|
||||
const seasons: Array<number> = [];
|
||||
|
||||
[...positions, SEASONS_PERIOD_NUMBER].forEach((position, i, arr) => {
|
||||
if (position < SEASONS_PERIOD_NUMBER) {
|
||||
const start = arr[i];
|
||||
const end = arr[i + 1];
|
||||
const value = values[i];
|
||||
|
||||
// eslint-disable-next-line unicorn/new-for-builtins
|
||||
seasons.push(...Array(end - start).fill(value));
|
||||
}
|
||||
});
|
||||
|
||||
return seasons;
|
||||
}
|
||||
|
||||
export function generateSeasonsPayments(leasingPeriod: LeasingPeriod, seasons: Array<number>) {
|
||||
return (
|
||||
Array.from(
|
||||
{
|
||||
length: Math.floor((leasingPeriod - 2) / SEASONS_PERIOD_NUMBER),
|
||||
},
|
||||
() => seasons
|
||||
)
|
||||
.flat()
|
||||
// eslint-disable-next-line unicorn/prefer-spread
|
||||
.concat(seasons.slice(0, (leasingPeriod - 2) % SEASONS_PERIOD_NUMBER))
|
||||
);
|
||||
}
|
||||
@ -1,25 +1,78 @@
|
||||
/* eslint-disable function-paren-newline */
|
||||
/* eslint-disable implicit-arrow-linebreak */
|
||||
import type { ApolloClient } from '@apollo/client';
|
||||
import { reaction, toJS } from 'mobx';
|
||||
import type { QueryClient } from '@tanstack/react-query';
|
||||
import { selectHighSeasonStart, selectSeasonType } from 'config/default-options';
|
||||
import { comparer, reaction, toJS } from 'mobx';
|
||||
import type { CalculationOptions } from 'stores/calculation/options/types';
|
||||
import type RootStore from 'stores/root';
|
||||
import type { Row } from 'stores/tables/payments/types';
|
||||
import { difference, shift } from 'tools/array';
|
||||
import * as seasonsConstants from './lib/seasons-constants';
|
||||
import * as seasonsTools from './lib/seasons-tools';
|
||||
import validatePaymentsTable from './validation';
|
||||
|
||||
export default function paymentsReactions(store: RootStore, apolloClient: ApolloClient<object>) {
|
||||
export default function paymentsReactions(
|
||||
store: RootStore,
|
||||
apolloClient: ApolloClient<object>,
|
||||
queryClient: QueryClient
|
||||
) {
|
||||
const { $calculation, $tables } = store;
|
||||
|
||||
reaction(
|
||||
() => $calculation.getElementValue('tbxFirstPaymentPerc'),
|
||||
(firstPaymentPerc) => {
|
||||
$tables.payments.setValue(0, firstPaymentPerc);
|
||||
}
|
||||
);
|
||||
|
||||
reaction(
|
||||
() => $calculation.getElementValue('tbxLastPaymentPerc'),
|
||||
(lastPaymentPerc) => {
|
||||
const paymentsLength = $tables.payments.values.length;
|
||||
$tables.payments.setValue(paymentsLength - 1, lastPaymentPerc);
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* При изменении срока лизинга регулируем длину таблицы платежей
|
||||
* Аннуитет
|
||||
*/
|
||||
reaction(
|
||||
() => $calculation.$values.getValue('leasingPeriod'),
|
||||
(leasingPeriod) => {
|
||||
if (leasingPeriod) {
|
||||
$tables.payments.setValues(
|
||||
Array.from(
|
||||
{
|
||||
length: leasingPeriod,
|
||||
},
|
||||
() => 0
|
||||
)
|
||||
() => {
|
||||
const graphType = $calculation.getElementValue('radioGraphType');
|
||||
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
|
||||
|
||||
return {
|
||||
graphType,
|
||||
leasingPeriod,
|
||||
};
|
||||
},
|
||||
({ graphType, leasingPeriod }) => {
|
||||
if (graphType === 100_000_000) {
|
||||
const middlePayments: Row[] = Array.from(
|
||||
{
|
||||
length: leasingPeriod - 2,
|
||||
},
|
||||
() => ({
|
||||
value: 100,
|
||||
status: 'Disabled',
|
||||
})
|
||||
);
|
||||
|
||||
const firstPaymentPerc = $calculation.getElementValue('tbxFirstPaymentPerc');
|
||||
const lastPaymentPerc = $calculation.getElementValue('tbxLastPaymentPerc');
|
||||
|
||||
$tables.payments.setRows([
|
||||
{
|
||||
value: firstPaymentPerc,
|
||||
status: 'Disabled',
|
||||
},
|
||||
...middlePayments,
|
||||
{
|
||||
value: lastPaymentPerc,
|
||||
status: 'Disabled',
|
||||
},
|
||||
]);
|
||||
}
|
||||
},
|
||||
{
|
||||
@ -28,22 +81,479 @@ export default function paymentsReactions(store: RootStore, apolloClient: Apollo
|
||||
);
|
||||
|
||||
/**
|
||||
* Проверяем платежи на 0
|
||||
* Равноубывающий
|
||||
*/
|
||||
reaction(
|
||||
() => {
|
||||
const graphType = $calculation.getElementValue('radioGraphType');
|
||||
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
|
||||
const parmentsDecreasePercent = $calculation.getElementValue('tbxParmentsDecreasePercent');
|
||||
|
||||
return {
|
||||
graphType,
|
||||
leasingPeriod,
|
||||
parmentsDecreasePercent,
|
||||
};
|
||||
},
|
||||
({ graphType, leasingPeriod, parmentsDecreasePercent }) => {
|
||||
if (graphType === 100_000_002) {
|
||||
const middlePayments: Row[] = Array.from(
|
||||
{
|
||||
length: leasingPeriod - 2,
|
||||
},
|
||||
(_, k) => {
|
||||
const payment = 100 * (parmentsDecreasePercent / 100) ** k;
|
||||
|
||||
return {
|
||||
value: Number(payment.toFixed(2)),
|
||||
status: 'Disabled',
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const firstPaymentPerc = $calculation.getElementValue('tbxFirstPaymentPerc');
|
||||
const lastPaymentPerc = $calculation.getElementValue('tbxLastPaymentPerc');
|
||||
|
||||
$tables.payments.setRows([
|
||||
{
|
||||
value: firstPaymentPerc,
|
||||
status: 'Disabled',
|
||||
},
|
||||
...middlePayments,
|
||||
{
|
||||
value: lastPaymentPerc,
|
||||
status: 'Disabled',
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Легкий старт
|
||||
*/
|
||||
reaction(
|
||||
() => {
|
||||
const graphType = $calculation.getElementValue('radioGraphType');
|
||||
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
|
||||
|
||||
return {
|
||||
graphType,
|
||||
leasingPeriod,
|
||||
};
|
||||
},
|
||||
({ graphType, leasingPeriod }) => {
|
||||
if (graphType === 100_000_004) {
|
||||
const editablePayments: Row[] = [
|
||||
{
|
||||
value: 25,
|
||||
status: 'Default',
|
||||
},
|
||||
{
|
||||
value: 50,
|
||||
status: 'Default',
|
||||
},
|
||||
{
|
||||
value: 75,
|
||||
status: 'Default',
|
||||
},
|
||||
];
|
||||
|
||||
const payments: Row[] = Array.from(
|
||||
{
|
||||
length: leasingPeriod - 5,
|
||||
},
|
||||
() => ({
|
||||
value: 100,
|
||||
status: 'Disabled',
|
||||
})
|
||||
);
|
||||
|
||||
const firstPaymentPerc = $calculation.getElementValue('tbxFirstPaymentPerc');
|
||||
const lastPaymentPerc = $calculation.getElementValue('tbxLastPaymentPerc');
|
||||
|
||||
$tables.payments.setRows([
|
||||
{
|
||||
value: firstPaymentPerc,
|
||||
status: 'Disabled',
|
||||
},
|
||||
...editablePayments,
|
||||
...payments,
|
||||
{
|
||||
value: lastPaymentPerc,
|
||||
status: 'Disabled',
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Дегрессия
|
||||
*/
|
||||
// TODO: const exeption = tarif?.evo_seasons_type_exception
|
||||
const degressionSeasonTypes = new Set([
|
||||
100_000_003, 100_000_004, 100_000_005, 100_000_006, 100_000_007,
|
||||
]);
|
||||
const seasonSeasonTypes = new Set([100_000_000, 100_000_001, 100_000_002]);
|
||||
|
||||
reaction(
|
||||
() => {
|
||||
const graphType = $calculation.getElementValue('radioGraphType');
|
||||
|
||||
return graphType;
|
||||
},
|
||||
(graphType) => {
|
||||
let selectSeasonTypeOptions: CalculationOptions['selectSeasonType'] = [];
|
||||
|
||||
if (graphType === 100_000_001) {
|
||||
selectSeasonTypeOptions = selectSeasonType.filter((option) =>
|
||||
degressionSeasonTypes.has(option.value)
|
||||
);
|
||||
} else if (graphType === 100_000_003) {
|
||||
selectSeasonTypeOptions = selectSeasonType.filter((option) =>
|
||||
seasonSeasonTypes.has(option.value)
|
||||
);
|
||||
}
|
||||
$calculation.setElementOptions('selectSeasonType', selectSeasonTypeOptions);
|
||||
}
|
||||
);
|
||||
|
||||
const degressionSteps: { [key: number]: Array<number> } = {
|
||||
100_000_003: [100, 50, 25],
|
||||
100_000_004: [100, 30, 10],
|
||||
100_000_005: [100, 70, 40],
|
||||
100_000_006: [100, 7, 3],
|
||||
};
|
||||
|
||||
reaction(
|
||||
() => {
|
||||
const degressionType = $calculation.getElementValue('selectSeasonType');
|
||||
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
|
||||
const graphType = $calculation.getElementValue('radioGraphType');
|
||||
|
||||
return {
|
||||
degressionType,
|
||||
leasingPeriod,
|
||||
graphType,
|
||||
};
|
||||
},
|
||||
({ degressionType, leasingPeriod, graphType }) => {
|
||||
if (graphType === 100_000_001) {
|
||||
let payments: Row[] = [];
|
||||
|
||||
switch (degressionType) {
|
||||
case 100_000_007: {
|
||||
const editablePayments: Row[] = Array.from(
|
||||
{
|
||||
length: leasingPeriod - 3,
|
||||
},
|
||||
() => ({
|
||||
value: 100,
|
||||
status: 'Default',
|
||||
})
|
||||
);
|
||||
|
||||
payments = [
|
||||
{
|
||||
value: 100,
|
||||
status: 'Disabled',
|
||||
},
|
||||
...editablePayments,
|
||||
];
|
||||
|
||||
break;
|
||||
}
|
||||
case 100_000_003:
|
||||
case 100_000_004:
|
||||
case 100_000_005:
|
||||
case 100_000_006: {
|
||||
const [step1, step2, step3] = degressionSteps[degressionType];
|
||||
const paymentsInStep = Math.ceil((leasingPeriod - 2) / 3);
|
||||
|
||||
payments = Array.from(
|
||||
{
|
||||
length: leasingPeriod - 2,
|
||||
},
|
||||
(_v, i) => {
|
||||
let value = step3;
|
||||
|
||||
if (i <= paymentsInStep * 2 - 1) {
|
||||
value = step2;
|
||||
}
|
||||
|
||||
if (i <= paymentsInStep - 1) {
|
||||
value = step1;
|
||||
}
|
||||
|
||||
return {
|
||||
value,
|
||||
status: 'Disabled',
|
||||
};
|
||||
}
|
||||
);
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const firstPaymentPerc = $calculation.getElementValue('tbxFirstPaymentPerc');
|
||||
const lastPaymentPerc = $calculation.getElementValue('tbxLastPaymentPerc');
|
||||
|
||||
$tables.payments.setRows([
|
||||
{
|
||||
value: firstPaymentPerc,
|
||||
status: 'Disabled',
|
||||
},
|
||||
...payments,
|
||||
{
|
||||
value: lastPaymentPerc,
|
||||
status: 'Disabled',
|
||||
},
|
||||
]);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
reaction(
|
||||
() => {
|
||||
const graphType = $calculation.getElementValue('radioGraphType');
|
||||
const payments = toJS($tables.payments.values);
|
||||
const degressionType = $calculation.getElementValue('selectSeasonType');
|
||||
|
||||
return {
|
||||
graphType,
|
||||
payments,
|
||||
degressionType,
|
||||
};
|
||||
},
|
||||
(nextParams, prevParams) => {
|
||||
if (nextParams.graphType === 100_000_001 && nextParams.degressionType === 100_000_007) {
|
||||
const changes = difference(nextParams.payments, prevParams.payments);
|
||||
|
||||
if (changes === null || changes.length > 1) return;
|
||||
|
||||
const [changeIndex] = changes;
|
||||
const value = nextParams.payments[changeIndex];
|
||||
const payments = nextParams.payments.slice(1, -1).map((payment, i) => {
|
||||
if (i <= changeIndex - 2) return payment;
|
||||
|
||||
return value;
|
||||
});
|
||||
|
||||
const firstPaymentPerc = $calculation.getElementValue('tbxFirstPaymentPerc');
|
||||
const lastPaymentPerc = $calculation.getElementValue('tbxLastPaymentPerc');
|
||||
|
||||
$tables.payments.setValues([firstPaymentPerc, ...payments, lastPaymentPerc]);
|
||||
}
|
||||
},
|
||||
{
|
||||
delay: 50,
|
||||
equals: comparer.structural,
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Сезонный
|
||||
*/
|
||||
|
||||
reaction(
|
||||
() => {
|
||||
const graphType = $calculation.getElementValue('radioGraphType');
|
||||
|
||||
return {
|
||||
graphType,
|
||||
};
|
||||
},
|
||||
({ graphType }) => {
|
||||
if (graphType !== 100_000_003) return;
|
||||
|
||||
const seasonType = $calculation.getElementValue('selectSeasonType');
|
||||
const highSeasonStart = $calculation.getElementValue('selectHighSeasonStart');
|
||||
|
||||
if (!seasonType || !highSeasonStart) {
|
||||
$tables.payments.setValues([]);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
reaction(
|
||||
() => {
|
||||
const seasonType = $calculation.getElementValue('selectSeasonType');
|
||||
|
||||
return seasonType;
|
||||
},
|
||||
(seasonType) => {
|
||||
const graphType = $calculation.getElementValue('radioGraphType');
|
||||
if (graphType !== 100_000_003) return;
|
||||
|
||||
if (!seasonType) {
|
||||
$calculation.resetElement('selectHighSeasonStart');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const highSeasonStartOptions = selectHighSeasonStart.filter(
|
||||
(option) => !seasonsConstants.FORBIDDEN_HIGH_SEASON_START[seasonType].includes(option.label)
|
||||
);
|
||||
|
||||
$calculation.setElementOptions('selectHighSeasonStart', highSeasonStartOptions);
|
||||
}
|
||||
);
|
||||
|
||||
function generateSeasonPaymentsRows(seasonType: number, shiftNumber: number, payments: number[]) {
|
||||
const shiftedPeriods = new Set(
|
||||
seasonsConstants.SEASONS_PERIODS[seasonType].map(
|
||||
(position) => (position + shiftNumber) % seasonsConstants.SEASONS_PERIOD_NUMBER
|
||||
)
|
||||
);
|
||||
const rows: Row[] = payments.map((value, i) => ({
|
||||
value,
|
||||
status: shiftedPeriods.has(i) ? 'Default' : 'Disabled',
|
||||
}));
|
||||
|
||||
return rows;
|
||||
}
|
||||
|
||||
reaction(
|
||||
() => {
|
||||
const seasonType = $calculation.getElementValue('selectSeasonType');
|
||||
const highSeasonStartOption = $calculation.getElementOption('selectHighSeasonStart');
|
||||
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
|
||||
|
||||
return {
|
||||
seasonType,
|
||||
highSeasonStartOption,
|
||||
leasingPeriod,
|
||||
};
|
||||
},
|
||||
({ seasonType, highSeasonStartOption, leasingPeriod }) => {
|
||||
const graphType = $calculation.getElementValue('radioGraphType');
|
||||
if (graphType !== 100_000_003) return;
|
||||
|
||||
if (!seasonType || !highSeasonStartOption) {
|
||||
$tables.payments.setValues([]);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
const seasons = seasonsTools.generateSeasons(
|
||||
seasonType,
|
||||
seasonsConstants.DEFAULT_SEASONS_VALUES
|
||||
);
|
||||
const shiftNumber = Number.parseInt(highSeasonStartOption.label, 10) - 2;
|
||||
const payments = seasonsTools.generateSeasonsPayments(
|
||||
leasingPeriod,
|
||||
shift(seasons, shiftNumber)
|
||||
);
|
||||
const rows: Row[] = generateSeasonPaymentsRows(seasonType, shiftNumber, payments);
|
||||
const firstPaymentPerc = $calculation.getElementValue('tbxFirstPaymentPerc');
|
||||
const lastPaymentPerc = $calculation.getElementValue('tbxLastPaymentPerc');
|
||||
|
||||
$tables.payments.setRows([
|
||||
{
|
||||
value: firstPaymentPerc,
|
||||
status: 'Disabled',
|
||||
},
|
||||
...rows,
|
||||
{
|
||||
value: lastPaymentPerc,
|
||||
status: 'Disabled',
|
||||
},
|
||||
]);
|
||||
}
|
||||
);
|
||||
|
||||
reaction(
|
||||
() => {
|
||||
const payments = toJS($tables.payments.values);
|
||||
const seasons = payments.slice(1, seasonsConstants.SEASONS_PERIOD_NUMBER + 1);
|
||||
|
||||
return seasons;
|
||||
},
|
||||
(nextSeasons, prevSeasons) => {
|
||||
const graphType = $calculation.getElementValue('radioGraphType');
|
||||
if (graphType !== 100_000_003) return;
|
||||
|
||||
const seasonType = $calculation.getElementValue('selectSeasonType');
|
||||
const highSeasonStartOption = $calculation.getElementOption('selectHighSeasonStart');
|
||||
if (!seasonType || !highSeasonStartOption) return;
|
||||
|
||||
const shiftNumber = Number.parseInt(highSeasonStartOption.label, 10) - 2;
|
||||
const unshiftedNextSeasons = shift(nextSeasons, -shiftNumber);
|
||||
const unshiftedPrevSeasons = shift(prevSeasons, -shiftNumber);
|
||||
|
||||
const changes = difference(unshiftedNextSeasons, unshiftedPrevSeasons);
|
||||
if (changes === null || changes.length > 1) return;
|
||||
|
||||
const [changeIndex] = changes;
|
||||
const positionIndex = seasonsTools.getPositionIndex(seasonType, changeIndex);
|
||||
|
||||
const values = seasonsTools.getSeasonsValues(seasonType, unshiftedNextSeasons);
|
||||
values[positionIndex] = unshiftedNextSeasons[changeIndex];
|
||||
|
||||
const seasons = seasonsTools.generateSeasons(seasonType, values);
|
||||
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
|
||||
const payments = seasonsTools.generateSeasonsPayments(
|
||||
leasingPeriod,
|
||||
shift(seasons, shiftNumber)
|
||||
);
|
||||
const rows: Row[] = generateSeasonPaymentsRows(seasonType, shiftNumber, payments);
|
||||
const firstPaymentPerc = $calculation.getElementValue('tbxFirstPaymentPerc');
|
||||
const lastPaymentPerc = $calculation.getElementValue('tbxLastPaymentPerc');
|
||||
|
||||
$tables.payments.setRows([
|
||||
{
|
||||
value: firstPaymentPerc,
|
||||
status: 'Disabled',
|
||||
},
|
||||
...rows,
|
||||
{
|
||||
value: lastPaymentPerc,
|
||||
status: 'Disabled',
|
||||
},
|
||||
]);
|
||||
},
|
||||
{
|
||||
delay: 50,
|
||||
equals: comparer.structural,
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Валидация
|
||||
*/
|
||||
const errorText = 'Значения должны быть больше 0';
|
||||
let removeError: () => void;
|
||||
|
||||
reaction(
|
||||
() => toJS($tables.payments.values),
|
||||
(values) => {
|
||||
if (values.includes(0)) {
|
||||
() => {
|
||||
const payments = toJS($tables.payments.values);
|
||||
const graphType = $calculation.getElementValue('radioGraphType');
|
||||
const seasonType = $calculation.getElementValue('selectSeasonType');
|
||||
const highSeasonStart = $calculation.getElementValue('selectHighSeasonStart');
|
||||
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
|
||||
|
||||
return {
|
||||
payments,
|
||||
graphType,
|
||||
seasonType,
|
||||
highSeasonStart,
|
||||
leasingPeriod,
|
||||
};
|
||||
},
|
||||
() => {
|
||||
if (removeError) removeError();
|
||||
const errorText = validatePaymentsTable(store);
|
||||
|
||||
if (errorText) {
|
||||
removeError = $tables.payments.validation.addError(errorText);
|
||||
} else {
|
||||
removeError();
|
||||
}
|
||||
},
|
||||
{
|
||||
fireImmediately: true,
|
||||
delay: 50,
|
||||
equals: comparer.structural,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
134
process/payments/validation.ts
Normal file
134
process/payments/validation.ts
Normal file
@ -0,0 +1,134 @@
|
||||
/* eslint-disable max-len */
|
||||
/* eslint-disable no-case-declarations */
|
||||
import { counting, max, min, sort } from 'radash/dist/array';
|
||||
import type RootStore from 'stores/root';
|
||||
import { areEqual, isSorted, shift } from 'tools/array';
|
||||
import { SEASONS_PERIODS, SEASONS_PERIOD_NUMBER } from './lib/seasons-constants';
|
||||
|
||||
export default function validatePaymentsTable({ $calculation, $tables }: RootStore) {
|
||||
switch ($calculation.getElementValue('radioGraphType')) {
|
||||
// Дегрессия
|
||||
case 100_000_001:
|
||||
if (!$calculation.getElementValue('selectSeasonType')) {
|
||||
return 'Не выбран тип дегрессии';
|
||||
}
|
||||
/**
|
||||
* в таблице платежей в столбце Соотношение платежей для строк с 2 до "Срок лизинга-1" минимальное значение должно быть равно 3
|
||||
*/
|
||||
{
|
||||
const MIN_PAYMENT = 3;
|
||||
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
|
||||
const targetPayments = $tables.payments.values.slice(1, leasingPeriod - 1);
|
||||
|
||||
if (!targetPayments.every((payment) => payment >= MIN_PAYMENT)) {
|
||||
return `Минимальное значение платежа должно быть равно ${MIN_PAYMENT}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* при Дегрессии все значения не должны быть равны друг другу + что при Легком старте 2,3 и 4 платежи не должны быть равны 100
|
||||
*/
|
||||
{
|
||||
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
|
||||
const targetPayments = $tables.payments.values.slice(1, leasingPeriod - 1);
|
||||
|
||||
if (new Set(targetPayments).size === 1) {
|
||||
return 'Платежи не должны быть равны друг другу';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверка на возрастание
|
||||
*/
|
||||
{
|
||||
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
|
||||
const targetPayments = $tables.payments.values.slice(1, leasingPeriod - 1);
|
||||
|
||||
for (let i = 2; i < targetPayments.length - 1; i += 1) {
|
||||
if (targetPayments[i] > targetPayments[i - 1]) {
|
||||
return 'Платежи должны убывать';
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Если вид графика = Дегрессия И значения в "Соотношении платежей" у 2, 3 и 4 платежа отличаются друг от друга не более чем на 10
|
||||
* (т.е. берем значения в этих полях, определяем максимальное и минимальное значение и смотрим на их разницу)
|
||||
* то не осуществлять Расчет графика и выводить ошибку "Нельзя осуществить расчет - указана очень жетская дегрессия.
|
||||
* На 2-4 платежах Соотношение платежа должен отличаться не более чем на 10%",
|
||||
* иначе осуществлять расчет
|
||||
*/
|
||||
{
|
||||
const targetPayments = $tables.payments.values.slice(1, 4);
|
||||
|
||||
if (max(targetPayments) - min(targetPayments) > 10) {
|
||||
return 'Указана очень жесткая дегрессия';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Если вид графика = Дегрессия И значения в "Соотношении платежей" для строк с 2 До "Срок лизинга-1" как минимум 2 раза по 2 платежа должны между собой быть равны
|
||||
* (т.е. берем значения "Соотношения платежей" для строк с 2 до "Срок лизинга-1" и делаем сводную таблицу - если кол-во одинаковых значение больше 2 встречаются 2 и более раза),
|
||||
* то осуществлять расчет,
|
||||
* иначе не осуществлять Расчет графика и выводить ошибку "Нельзя осуществить расчет - указана очень жетская дегрессия. Не менее чем у 4х платежей "Соотношение платежа" должно не отличаться между самой",
|
||||
*/
|
||||
{
|
||||
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
|
||||
const targetPayments = $tables.payments.values.slice(1, leasingPeriod - 1);
|
||||
const counts = counting(targetPayments, (v) => v);
|
||||
if (Object.values(counts).filter((count) => count > 1).length < 2) {
|
||||
return 'Указана очень жесткая дегрессия. Не менее чем у 4х платежей соотношение должно не отличаться между собой';
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case 100_000_003: {
|
||||
const leasingPeriod = $calculation.getElementValue('tbxLeasingPeriod');
|
||||
if (leasingPeriod < 14) {
|
||||
return 'При сезонном виде графика срок лизинга должен быть больше 14 месяцев';
|
||||
}
|
||||
|
||||
const seasonType = $calculation.getElementValue('selectSeasonType');
|
||||
if (!seasonType) {
|
||||
return 'Не выбран тип сезонности';
|
||||
}
|
||||
|
||||
const highSeasonStartOption = $calculation.getElementOption('selectHighSeasonStart');
|
||||
if (!highSeasonStartOption) {
|
||||
return 'Не выбрано смещение сезонности';
|
||||
}
|
||||
|
||||
{
|
||||
const seasons = $tables.payments.values.slice(1, SEASONS_PERIOD_NUMBER + 1);
|
||||
const shiftNumber = Number.parseInt(highSeasonStartOption.label, 10) - 2;
|
||||
const unshiftedSeasons = shift(seasons, -shiftNumber);
|
||||
|
||||
const positions = SEASONS_PERIODS[seasonType];
|
||||
const seasonsValues = positions.map((position) => unshiftedSeasons[position]);
|
||||
|
||||
if (isSorted(seasonsValues)) {
|
||||
return 'Сезонные платежи должны убывать';
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Легкий старт
|
||||
case 100_000_004: {
|
||||
const targetPayments = $tables.payments.values.slice(1, 4);
|
||||
const sortedPayments = sort(targetPayments, (x) => x);
|
||||
const areEqualPayments = new Set(targetPayments).size === 1;
|
||||
|
||||
if (!areEqual(targetPayments, sortedPayments) || areEqualPayments) {
|
||||
return '2, 3, 4 платежи должны возрастать';
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
@ -34,7 +34,7 @@ export default class OptionsStore {
|
||||
if (
|
||||
// eslint-disable-next-line operator-linebreak
|
||||
!this.options[elementName]?.length ||
|
||||
this.options[elementName].some((x) => x.value === value)
|
||||
!this.options[elementName].some((x) => x.value === value)
|
||||
) {
|
||||
this.root.$calculation.resetElementValue(elementName);
|
||||
}
|
||||
|
||||
@ -4,11 +4,7 @@ import { makeAutoObservable, observable, reaction } from 'mobx';
|
||||
|
||||
import type RootStore from 'stores/root';
|
||||
import Validation from '../validation';
|
||||
|
||||
type Row = {
|
||||
value: number;
|
||||
status: Status;
|
||||
};
|
||||
import type { Row } from './types';
|
||||
|
||||
export default class PaymentsTable {
|
||||
root: RootStore;
|
||||
|
||||
6
stores/tables/payments/types.ts
Normal file
6
stores/tables/payments/types.ts
Normal file
@ -0,0 +1,6 @@
|
||||
import type { Status } from 'Elements/types';
|
||||
|
||||
export type Row = {
|
||||
value: number;
|
||||
status: Status;
|
||||
};
|
||||
@ -29,6 +29,7 @@ export default class Validation {
|
||||
|
||||
const removeError = () => {
|
||||
this.messages.delete(message);
|
||||
notification.close(this.params.err_key);
|
||||
};
|
||||
|
||||
notification.error({
|
||||
|
||||
39
tools/__tests__/array.test.js
Normal file
39
tools/__tests__/array.test.js
Normal file
@ -0,0 +1,39 @@
|
||||
import { shift } from 'tools/array';
|
||||
|
||||
describe('tools/array', () => {
|
||||
describe('[function] shift', () => {
|
||||
const arr = [1, 2, 3, 4, 5, 6, 7, 8, 9];
|
||||
test('should shift array right 3 positions', () => {
|
||||
const result = shift(arr, 3);
|
||||
expect(result).toEqual([7, 8, 9, 1, 2, 3, 4, 5, 6]);
|
||||
});
|
||||
test('should shift array left 3 positions', () => {
|
||||
const result = shift(arr, -3);
|
||||
expect(result).toEqual([4, 5, 6, 7, 8, 9, 1, 2, 3]);
|
||||
});
|
||||
test('should shift array right 6 positions', () => {
|
||||
const result = shift(arr, 15);
|
||||
expect(result).toEqual([4, 5, 6, 7, 8, 9, 1, 2, 3]);
|
||||
});
|
||||
test('should shift array left 6 positions', () => {
|
||||
const result = shift(arr, -15);
|
||||
expect(result).toEqual([7, 8, 9, 1, 2, 3, 4, 5, 6]);
|
||||
});
|
||||
test('should keep array as is', () => {
|
||||
const result = shift(arr, 0);
|
||||
expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
});
|
||||
test('should keep array as is', () => {
|
||||
const result = shift(arr, 9);
|
||||
expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||
});
|
||||
test('should return empty array', () => {
|
||||
const result = shift([], 0);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
test('should return empty array', () => {
|
||||
const result = shift([], 0);
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
31
tools/array.ts
Normal file
31
tools/array.ts
Normal file
@ -0,0 +1,31 @@
|
||||
export function areEqual<T>(arr1: ReadonlyArray<T>, arr2: ReadonlyArray<T>) {
|
||||
return arr1.length === arr2.length && arr1.every((value, index) => value === arr2[index]);
|
||||
}
|
||||
|
||||
export function difference<T>(arr1: ReadonlyArray<T>, arr2: ReadonlyArray<T>) {
|
||||
if (arr1.length !== arr2.length) return null;
|
||||
|
||||
const changes = [];
|
||||
// eslint-disable-next-line unicorn/no-for-loop
|
||||
for (let i = 0; i < arr1.length; i += 1) {
|
||||
if (arr1[i] !== arr2[i]) {
|
||||
changes.push(i);
|
||||
}
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
export function shift<T>(arr: Array<T>, n: number) {
|
||||
if (arr.length === 0) return arr;
|
||||
|
||||
const shiftNumber = n % arr.length;
|
||||
|
||||
if (shiftNumber === 0) return arr;
|
||||
|
||||
return [...arr.slice(-shiftNumber, arr.length), ...arr.slice(0, -shiftNumber)];
|
||||
}
|
||||
|
||||
export function isSorted(arr: Array<number>) {
|
||||
return arr.every((value, index, array) => !index || array[index - 1] <= value);
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user