diff --git a/apps/web/Components/Calculation/Form/Insurance/FinGAPTable/index.jsx b/apps/web/Components/Calculation/Form/Insurance/FinGAPTable/index.jsx index 5ce9cb8..3547673 100644 --- a/apps/web/Components/Calculation/Form/Insurance/FinGAPTable/index.jsx +++ b/apps/web/Components/Calculation/Form/Insurance/FinGAPTable/index.jsx @@ -14,10 +14,10 @@ const Grid = styled(Flex)` const Validation = observer(() => { const store = useStore(); - const messages = store.$tables.fingap.validation.getMessages(); + const errors = store.$tables.fingap.validation.getErrors(); - if (messages?.length) { - return ; + if (errors?.length) { + return ; } return null; diff --git a/apps/web/Components/Calculation/Form/Insurance/InsuranceTable/index.jsx b/apps/web/Components/Calculation/Form/Insurance/InsuranceTable/index.jsx index 6cc6fde..24ef8af 100644 --- a/apps/web/Components/Calculation/Form/Insurance/InsuranceTable/index.jsx +++ b/apps/web/Components/Calculation/Form/Insurance/InsuranceTable/index.jsx @@ -19,10 +19,10 @@ const TableWrapper = styled.div` const Validation = observer(() => { const store = useStore(); - const messages = store.$tables.insurance.validation.getMessages(); + const errors = store.$tables.insurance.validation.getErrors(); - if (messages?.length) { - return ; + if (errors?.length) { + return ; } return null; diff --git a/apps/web/Components/Calculation/Form/Payments/PaymentsTable/index.jsx b/apps/web/Components/Calculation/Form/Payments/PaymentsTable/index.jsx index 804ecbd..a8fd83b 100644 --- a/apps/web/Components/Calculation/Form/Payments/PaymentsTable/index.jsx +++ b/apps/web/Components/Calculation/Form/Payments/PaymentsTable/index.jsx @@ -33,10 +33,10 @@ const Validation = observer(() => { const store = useStore(); const { payments } = store.$tables; - const messages = payments.validation.getMessages(); + const errors = payments.validation.getErrors(); - if (messages?.length) { - return ; + if (errors?.length) { + return ; } return null; diff --git a/apps/web/Components/Output/Validation.jsx b/apps/web/Components/Output/Validation.jsx index 6435e2b..c66cff2 100644 --- a/apps/web/Components/Output/Validation.jsx +++ b/apps/web/Components/Output/Validation.jsx @@ -25,12 +25,12 @@ const AlertWrapper = styled(Box)` function getElementsErrors($calculation) { return Object.values($calculation.$validation).map((validation) => { - const elementErrors = validation.getMessages(); + const elementErrors = validation.getErrors(); const elementTitle = validation.params.err_title; - return elementErrors.map((error) => ( + return elementErrors.map(({ key, message }) => ( - + )); }); @@ -38,18 +38,22 @@ function getElementsErrors($calculation) { function getPaymentsTableErrors($tables) { const { payments } = $tables; - const messages = payments.validation.getMessages(); + const errors = payments.validation.getErrors(); const title = payments.validation.params.err_title; - return messages.map((text) => ); + return errors.map(({ key, message }) => ( + + )); } function getInsuranceTableErrors($tables) { const { insurance } = $tables; - const messages = insurance.validation.getMessages(); + const errors = insurance.validation.getErrors(); const title = insurance.validation.params.err_title; - return messages.map((text) => ); + return errors.map(({ key, message }) => ( + + )); } const Errors = observer(() => { diff --git a/apps/web/constants/values.js b/apps/web/constants/values.js index ae2d4ca..979db7d 100644 --- a/apps/web/constants/values.js +++ b/apps/web/constants/values.js @@ -7,3 +7,4 @@ export const MAX_LEASING_PERIOD = 60; export const MIN_LASTPAYMENT_NSIB = 3500; export const MIN_PAYMENT = 3; export const VAT = 0.2; +export const MAX_MASS = 3500; diff --git a/apps/web/process/add-product/reactions.ts b/apps/web/process/add-product/reactions.ts index 17dbd24..6930279 100644 --- a/apps/web/process/add-product/reactions.ts +++ b/apps/web/process/add-product/reactions.ts @@ -1,10 +1,10 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type { ProcessContext } from '../types'; -import { normalizeOptions } from '@/../../packages/tools'; import * as CRMTypes from '@/graphql/crm.types'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import { reaction } from 'mobx'; +import { normalizeOptions } from 'tools'; dayjs.extend(utc); diff --git a/apps/web/process/bonuses/reactions/lib/helper.ts b/apps/web/process/bonuses/lib/helper.ts similarity index 92% rename from apps/web/process/bonuses/reactions/lib/helper.ts rename to apps/web/process/bonuses/lib/helper.ts index 4b4ae16..8273620 100644 --- a/apps/web/process/bonuses/reactions/lib/helper.ts +++ b/apps/web/process/bonuses/lib/helper.ts @@ -1,5 +1,5 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import type { ProcessContext } from '../../../types'; +import type { ValidationContext } from '../../types'; import { getUser } from '@/api/user/query'; import type { ElementsTypes } from '@/Components/Calculation/config/map/values'; import { STALE_TIME } from '@/constants/request'; @@ -8,7 +8,7 @@ import dayjs from 'dayjs'; export type ProductId = ElementsTypes['selectProduct']; -export default function helper({ apolloClient, queryClient }: ProcessContext) { +export default function helper({ apolloClient, queryClient }: ValidationContext) { return { async getCoefficient(productId: ProductId) { if (!productId) { diff --git a/apps/web/process/bonuses/reactions/common.ts b/apps/web/process/bonuses/reactions/common.ts index 93a4fdc..27160b8 100644 --- a/apps/web/process/bonuses/reactions/common.ts +++ b/apps/web/process/bonuses/reactions/common.ts @@ -1,11 +1,11 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type { ProcessContext } from '../../types'; -import helper from './lib/helper'; -import { makeDisposable } from '@/../../packages/tools'; +import helper from '../lib/helper'; import * as CRMTypes from '@/graphql/crm.types'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import { reaction } from 'mobx'; +import { makeDisposable } from 'tools'; dayjs.extend(utc); diff --git a/apps/web/process/bonuses/reactions/validation.ts b/apps/web/process/bonuses/reactions/validation.ts index 961c169..d8ff572 100644 --- a/apps/web/process/bonuses/reactions/validation.ts +++ b/apps/web/process/bonuses/reactions/validation.ts @@ -1,23 +1,35 @@ -import helper from './lib/helper'; +import { createValidationSchema } from '../validation'; +import type { Elements } from '@/Components/Calculation/config/map/values'; import type { ProcessContext } from '@/process/types'; +import ValidationHelper from '@/stores/validation/helper'; import { reaction } from 'mobx'; -import { round } from 'tools'; +import { uid } from 'radash'; + +const key = uid(7); export default function reactions(context: ProcessContext) { const { store } = context; const { $calculation } = store; - const { getCoefficient } = helper(context); + const validationSchema = createValidationSchema(context); + const helper = new ValidationHelper(); reaction( () => $calculation.$values.getValues(['product', 'saleBonus']), - async ({ product: productId, saleBonus }) => { - const coefficient = await getCoefficient(productId); - const maxBonus = (coefficient?.evo_sot_coefficient || 0) * 100; + async (values) => { + helper.removeErrors(); + const validationResult = await validationSchema.safeParseAsync(values); - $calculation.element('tbxSaleBonus').validate({ - invalid: round(saleBonus, 2) > round(maxBonus, 2), - message: 'Размер бонуса МПЛ не может быть выше установленного по СОТ', - }); + if (!validationResult.success) { + validationResult.error.errors.forEach(({ path, message }) => { + (path as Elements[]).forEach((elementName) => { + const removeError = $calculation.element(elementName).setError({ key, message }); + if (removeError) helper.add(removeError); + }); + }); + } + }, + { + delay: 100, } ); } diff --git a/apps/web/process/bonuses/validation.ts b/apps/web/process/bonuses/validation.ts new file mode 100644 index 0000000..6727bda --- /dev/null +++ b/apps/web/process/bonuses/validation.ts @@ -0,0 +1,24 @@ +import type { ValidationContext } from '../types'; +import helper from './lib/helper'; +import ValuesSchema from '@/config/schema/values'; +import { round } from 'tools'; +import { z } from 'zod'; + +export function createValidationSchema(context: ValidationContext) { + const { getCoefficient } = helper(context); + + return ValuesSchema.pick({ product: true, saleBonus: true }).superRefine( + async ({ product, saleBonus }, ctx) => { + const coefficient = await getCoefficient(product); + const maxBonus = (coefficient?.evo_sot_coefficient || 0) * 100; + + if (round(saleBonus, 2) > round(maxBonus, 2)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Размер бонуса МПЛ не может быть выше установленного по СОТ', + path: ['tbxSaleBonus'], + }); + } + } + ); +} diff --git a/apps/web/process/calculate/reactions/validation.ts b/apps/web/process/calculate/reactions/validation.ts index 1ace7fb..18c95ae 100644 --- a/apps/web/process/calculate/reactions/validation.ts +++ b/apps/web/process/calculate/reactions/validation.ts @@ -2,7 +2,9 @@ import types from '@/Components/Calculation/config/elements-types'; import type * as Values from '@/Components/Calculation/config/map/values'; import type * as Insurance from '@/Components/Calculation/Form/Insurance/InsuranceTable/types'; import type { ProcessContext } from '@/process/types'; +import ValidationHelper from '@/stores/validation/helper'; import { reaction } from 'mobx'; +import { uid } from 'radash'; import type { BaseOption } from 'ui/elements/types'; function hasInvalidValueOrOptions(value: unknown, options: Array>) { @@ -21,6 +23,7 @@ export default function reactions({ store }: ProcessContext) { const hasElementsErrors = Object.values($calculation.$validation).some( (validation) => validation.hasErrors ); + const hasPaymentsErrors = $tables.payments.validation.hasErrors; const hasInsuranceErrors = $tables.insurance.validation.hasErrors; const hasFingapErrors = $tables.fingap.validation.hasErrors; @@ -44,6 +47,10 @@ export default function reactions({ store }: ProcessContext) { /** * Проверяем, что выбранное значение элемента есть в списке */ + + const key = uid(7); + const message = 'Выбранное значение отсутствует в списке'; + function validateOptionsElement(elementName: Values.Elements) { const type = types[elementName]; if (type().typeName !== 'Options') { @@ -52,6 +59,8 @@ export default function reactions({ store }: ProcessContext) { const element = $calculation.element(elementName); + const validationHelper = new ValidationHelper(); + reaction( () => { const options = element.getOptions(); @@ -63,11 +72,16 @@ export default function reactions({ store }: ProcessContext) { }; }, ({ value, options }) => { - element.validate({ - invalid: hasInvalidValueOrOptions(value, options), - message: 'Выбранное значение отсутствует в списке', - silent: true, - }); + if (hasInvalidValueOrOptions(value, options)) { + const removeError = element.setError({ + key, + message, + silent: true, + }); + if (removeError) validationHelper.add(removeError); + } else { + validationHelper.removeErrors(); + } }, { delay: 100, @@ -95,6 +109,8 @@ export default function reactions({ store }: ProcessContext) { function validateInsuranceCompany(rowKey: Insurance.Keys) { const row = $tables.insurance.row(rowKey); + const validationHelper = new ValidationHelper(); + reaction( () => { const options = row.getOptions('insuranceCompany'); @@ -106,11 +122,16 @@ export default function reactions({ store }: ProcessContext) { }; }, ({ value, options }) => { - $tables.insurance.validate({ - invalid: hasInvalidValueOrOptions(value, options), - message: 'Выбранное значение отсутствует в списке', - silent: true, - }); + if (hasInvalidValueOrOptions(value, options)) { + const removeError = $tables.insurance.setError({ + key, + message, + silent: true, + }); + validationHelper.add(removeError); + } else { + validationHelper.removeErrors(); + } }, { delay: 100, diff --git a/apps/web/process/configurator/reactions/validation.ts b/apps/web/process/configurator/reactions/validation.ts index 9e24896..86a8dda 100644 --- a/apps/web/process/configurator/reactions/validation.ts +++ b/apps/web/process/configurator/reactions/validation.ts @@ -1,44 +1,35 @@ -/* eslint-disable @typescript-eslint/naming-convention */ -import * as CRMTypes from '@/graphql/crm.types'; +import { createValidationSchema } from '../validation'; +import type { Elements } from '@/Components/Calculation/config/map/values'; import type { ProcessContext } from '@/process/types'; +import ValidationHelper from '@/stores/validation/helper'; import { reaction } from 'mobx'; +import { uid } from 'radash'; -export default function reactions({ store, apolloClient }: ProcessContext) { +const key = uid(7); + +export default function reactions(context: ProcessContext) { + const { store } = context; const { $calculation } = store; - /** - * На изменение поля Процет убывания платежей tbxParmentsDecreasePercent необходимо заложить проверку: - * Если значение поля меньше значения в поле "Минимальный % убывания платежей" evo_min_decreasing_perc из записи, - * указанной в поле ТарифselectTarif , то поле Процет убывания платежей tbxParmentsDecreasePercent должно обводиться красной рамкой - * и выводиться сообщение "Процент убывания не может быть меньше минимального значения по данному тарифу - * - <указывается значение из поля "Минимальный % убывания платежей">, иначе красная рамка снимается. - * При красной рамке в данном поле нельзя осуществить расчет графика. - */ + const validationSchema = createValidationSchema(context); + + const helper = new ValidationHelper(); reaction( - () => ({ - parmentsDecreasePercent: $calculation.element('tbxParmentsDecreasePercent').getValue(), - tarifId: $calculation.element('selectTarif').getValue(), - }), - async ({ parmentsDecreasePercent, tarifId }) => { - let evo_tarif: CRMTypes.GetTarifQuery['evo_tarif'] = null; + () => $calculation.$values.getValues(['parmentsDecreasePercent', 'tarif']), + async (values) => { + helper.removeErrors(); + const validationResult = await validationSchema.safeParseAsync(values); - if (tarifId) { - const { data } = await apolloClient.query({ - query: CRMTypes.GetTarifDocument, - variables: { - tarifId, - }, + if (!validationResult.success) { + validationResult.error.errors.forEach(({ path, message }) => { + (path as Elements[]).forEach((elementName) => { + const removeError = $calculation.element(elementName).setError({ key, message }); + if (removeError) helper.add(removeError); + }); }); - - ({ evo_tarif } = data); } - - $calculation.element('tbxParmentsDecreasePercent').validate({ - invalid: Boolean( - evo_tarif?.evo_min_decreasing_perc && - parmentsDecreasePercent < evo_tarif?.evo_min_decreasing_perc - ), - message: `Процент убывания не может быть меньше минимального значения по данному тарифу - ${evo_tarif?.evo_min_decreasing_perc}`, - }); + }, + { + delay: 100, } ); } diff --git a/apps/web/process/configurator/validation.ts b/apps/web/process/configurator/validation.ts new file mode 100644 index 0000000..c25abc4 --- /dev/null +++ b/apps/web/process/configurator/validation.ts @@ -0,0 +1,41 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { ValidationContext } from '../types'; +import ValuesSchema from '@/config/schema/values'; +import * as CRMTypes from '@/graphql/crm.types'; +import { z } from 'zod'; + +export function createValidationSchema({ apolloClient }: ValidationContext) { + return ValuesSchema.pick({ parmentsDecreasePercent: true, tarif: true }).superRefine( + async ({ parmentsDecreasePercent, tarif: tarifId }, ctx) => { + /** + * На изменение поля Процет убывания платежей tbxParmentsDecreasePercent необходимо заложить проверку: + * Если значение поля меньше значения в поле "Минимальный % убывания платежей" evo_min_decreasing_perc из записи, + * указанной в поле ТарифselectTarif , то поле Процет убывания платежей tbxParmentsDecreasePercent должно обводиться красной рамкой + * и выводиться сообщение "Процент убывания не может быть меньше минимального значения по данному тарифу + * - <указывается значение из поля "Минимальный % убывания платежей">, иначе красная рамка снимается. + * При красной рамке в данном поле нельзя осуществить расчет графика. + */ + if (tarifId) { + const { + data: { evo_tarif }, + } = await apolloClient.query({ + query: CRMTypes.GetTarifDocument, + variables: { + tarifId, + }, + }); + + if ( + evo_tarif?.evo_min_decreasing_perc && + parmentsDecreasePercent < evo_tarif?.evo_min_decreasing_perc + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Процент убывания не может быть меньше минимального значения по данному тарифу - ${evo_tarif?.evo_min_decreasing_perc}`, + path: ['tbxParmentsDecreasePercent'], + }); + } + } + } + ); +} diff --git a/apps/web/process/fingap/reactions/validation.ts b/apps/web/process/fingap/reactions/validation.ts index 3f8df5f..580208e 100644 --- a/apps/web/process/fingap/reactions/validation.ts +++ b/apps/web/process/fingap/reactions/validation.ts @@ -1,9 +1,14 @@ import type { ProcessContext } from '@/process/types'; +import ValidationHelper from '@/stores/validation/helper'; import { reaction } from 'mobx'; +import { uid } from 'radash'; + +const key = uid(7); export default function reactions({ store }: ProcessContext) { const { $tables } = store; + const helper = new ValidationHelper(); reaction( () => { const hasPaymentsErrors = $tables.payments.validation.hasErrors; @@ -15,10 +20,15 @@ export default function reactions({ store }: ProcessContext) { }; }, ({ hasPaymentsErrors, finGAPInsuranceCompany }) => { - $tables.fingap.validate({ - invalid: finGAPInsuranceCompany !== null && hasPaymentsErrors, - message: 'Неверно заполнены платежи', - }); + if (finGAPInsuranceCompany !== null && hasPaymentsErrors) { + const removeError = $tables.fingap.setError({ + key, + message: 'Неверно заполнены платежи', + }); + helper.add(removeError); + } else { + helper.removeErrors(); + } if (hasPaymentsErrors) { $tables.fingap.clear(); diff --git a/apps/web/process/gibdd/reactions.ts b/apps/web/process/gibdd/reactions.ts index b9326c0..a2f65ed 100644 --- a/apps/web/process/gibdd/reactions.ts +++ b/apps/web/process/gibdd/reactions.ts @@ -1,7 +1,9 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type { ProcessContext } from '../types'; import helper from './lib/helper'; +import { createValidationSchema } from './validation'; import { getTransTax } from '@/api/1c/query'; +import type { Elements } from '@/Components/Calculation/config/map/values'; import { selectObjectCategoryTax } from '@/config/default-options'; import { STALE_TIME } from '@/constants/request'; import * as CRMTypes from '@/graphql/crm.types'; @@ -10,6 +12,7 @@ import type { QueryFunctionContext } from '@tanstack/react-query'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; import { reaction } from 'mobx'; +import { uid } from 'radash'; import { makeDisposable, normalizeOptions } from 'tools'; dayjs.extend(utc); @@ -43,36 +46,6 @@ export function common({ store, apolloClient, queryClient }: ProcessContext) { } ); - reaction( - () => $calculation.$values.getValues(['objectRegistration', 'vehicleTaxInYear']), - ({ objectRegistration, vehicleTaxInYear }) => { - if (objectRegistration === 100_000_001) { - $calculation.element('tbxVehicleTaxInYear').unblock(); - } else { - $calculation.element('tbxVehicleTaxInYear').resetValue().block(); - } - $calculation.element('tbxVehicleTaxInYear').validate({ - invalid: objectRegistration === 100_000_001 && !(vehicleTaxInYear > 0), - message: 'Значение должно быть больше 0', - }); - } - ); - - reaction( - () => $calculation.$values.getValues(['objectRegistration', 'typePTS']), - ({ objectRegistration, typePTS }) => { - if (objectRegistration === 100_000_001) { - $calculation.element('radioTypePTS').unblock(); - } else { - $calculation.element('radioTypePTS').resetValue().block(); - } - $calculation.element('radioTypePTS').validate({ - invalid: objectRegistration === 100_000_001 && !typePTS, - message: 'Не заполнено поле', - }); - } - ); - makeDisposable( () => reaction( @@ -503,89 +476,41 @@ export function common({ store, apolloClient, queryClient }: ProcessContext) { ); } -export function validation({ store, apolloClient }: ProcessContext) { - const { $calculation } = store; +const key = uid(7); +export function validation(context: ProcessContext) { + const { store } = context; + const { $calculation } = store; + const validationSchema = createValidationSchema(context); + + const validationHelper = new ValidationHelper(); reaction( - () => $calculation.$values.getValues(['leaseObjectCategory', 'maxMass']), - ({ leaseObjectCategory, maxMass }) => { - $calculation.element('tbxMaxMass').validate({ - invalid: leaseObjectCategory === 100_000_001 && maxMass > 3500, - message: 'При категории ТС = В Разрешенная макс.масса не может быть больше 3500 кг', - }); - $calculation.element('tbxMaxMass').validate({ - invalid: leaseObjectCategory === 100_000_002 && maxMass <= 3500, - message: 'При категории ТС = С Разрешенная макс.масса не может быть меньше 3500 кг', - }); + () => + $calculation.$values.getValues([ + 'leaseObjectCategory', + 'maxMass', + 'leaseObjectType', + 'typePTS', + 'objectRegistration', + 'objectCategoryTax', + 'insNSIB', + 'vehicleTaxInYear', + ]), + async (values) => { + validationHelper.removeErrors(); + const validationResult = await validationSchema.safeParseAsync(values); + + if (!validationResult.success) { + validationResult.error.errors.forEach(({ path, message }) => { + (path as Elements[]).forEach((elementName) => { + const removeError = $calculation.element(elementName).setError({ key, message }); + if (removeError) validationHelper.add(removeError); + }); + }); + } + }, + { + delay: 100, } ); - - { - const validationHelper = new ValidationHelper(); - reaction( - () => - $calculation.$values.getValues([ - 'typePTS', - 'objectRegistration', - 'objectCategoryTax', - 'leaseObjectType', - ]), - async ({ - leaseObjectType: leaseObjectTypeId, - typePTS, - objectRegistration, - objectCategoryTax, - }) => { - if (!leaseObjectTypeId) { - validationHelper.removeErrors(); - - return; - } - - const { - data: { evo_leasingobject_type }, - } = await apolloClient.query({ - query: CRMTypes.GetLeaseObjectTypeDocument, - variables: { leaseObjectTypeId }, - }); - - $calculation.element('selectObjectCategoryTax').validate({ - helper: validationHelper, - invalid: - objectRegistration === 100_000_001 && - typePTS === 100_000_001 && - objectCategoryTax === null && - Boolean(evo_leasingobject_type?.evo_category_tr?.length), - message: 'Необходимо из ЭПТС указать Категорию в соответствии с ТР ТС 018/2011', - }); - } - ); - } - - { - const validationHelper = new ValidationHelper(); - reaction( - () => $calculation.$values.getValues(['leaseObjectType', 'insNSIB']), - async ({ insNSIB, leaseObjectType: leaseObjectTypeId }) => { - if (!leaseObjectTypeId) { - validationHelper.removeErrors(); - - return; - } - - const { - data: { evo_leasingobject_type }, - } = await apolloClient.query({ - query: CRMTypes.GetLeaseObjectTypeDocument, - variables: { leaseObjectTypeId }, - }); - - $calculation.element('selectInsNSIB').validate({ - helper: validationHelper, - invalid: evo_leasingobject_type?.evo_id === '11' && !insNSIB, - message: 'Страхование НСИБ обязательно для мотоциклистов', - }); - } - ); - } } diff --git a/apps/web/process/gibdd/validation.ts b/apps/web/process/gibdd/validation.ts new file mode 100644 index 0000000..e1c90c6 --- /dev/null +++ b/apps/web/process/gibdd/validation.ts @@ -0,0 +1,95 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { ValidationContext } from '../types'; +import ValuesSchema from '@/config/schema/values'; +import { MAX_MASS } from '@/constants/values'; +import * as CRMTypes from '@/graphql/crm.types'; +import { z } from 'zod'; + +export function createValidationSchema({ apolloClient }: ValidationContext) { + return ValuesSchema.pick({ + insNSIB: true, + leaseObjectCategory: true, + leaseObjectType: true, + maxMass: true, + objectCategoryTax: true, + objectRegistration: true, + typePTS: true, + vehicleTaxInYear: true, + }).superRefine( + async ( + { + leaseObjectCategory, + maxMass, + leaseObjectType: leaseObjectTypeId, + typePTS, + objectRegistration, + objectCategoryTax, + insNSIB, + vehicleTaxInYear, + }, + ctx + ) => { + if (leaseObjectCategory === 100_000_001 && maxMass > MAX_MASS) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `При категории ТС = В Разрешенная макс.масса не может быть больше ${MAX_MASS} кг`, + path: ['tbxMaxMass'], + }); + } + + if (leaseObjectCategory === 100_000_002 && maxMass <= MAX_MASS) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `При категории ТС = С Разрешенная макс.масса не может быть меньше ${MAX_MASS} кг`, + path: ['tbxMaxMass'], + }); + } + + if (leaseObjectTypeId) { + const { + data: { evo_leasingobject_type }, + } = await apolloClient.query({ + query: CRMTypes.GetLeaseObjectTypeDocument, + variables: { leaseObjectTypeId }, + }); + + if ( + objectRegistration === 100_000_001 && + typePTS === 100_000_001 && + objectCategoryTax === null && + Boolean(evo_leasingobject_type?.evo_category_tr?.length) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Не заполнено поле', + path: ['selectObjectCategoryTax'], + }); + } + + if (evo_leasingobject_type?.evo_id === '11' && !insNSIB) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Страхование НСИБ обязательно для мотоциклистов', + path: ['selectInsNSIB'], + }); + } + } + + if (objectRegistration === 100_000_001 && !(vehicleTaxInYear > 0)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Значение должно быть больше 0', + path: ['tbxVehicleTaxInYear'], + }); + } + + if (objectRegistration === 100_000_001 && !typePTS) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Не заполнено поле', + path: ['radioTypePTS'], + }); + } + } + ); +} diff --git a/apps/web/process/insurance/reactions.ts b/apps/web/process/insurance/reactions.ts index 7f4a971..7acfad4 100644 --- a/apps/web/process/insurance/reactions.ts +++ b/apps/web/process/insurance/reactions.ts @@ -1,14 +1,16 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type { ProcessContext } from '../types'; -import { normalizeOptions } from '@/../../packages/tools'; -import type * as Insurance from '@/Components/Calculation/Form/Insurance/InsuranceTable/types'; +import { createValidationSchema } from './validation'; +import type { Elements } from '@/Components/Calculation/config/map/values'; import { selectLeaseObjectUseFor } from '@/config/default-options'; import * as CRMTypes from '@/graphql/crm.types'; import ValidationHelper from '@/stores/validation/helper'; -import { reaction } from 'mobx'; +import { comparer, reaction, toJS } from 'mobx'; +import { uid } from 'radash'; +import { normalizeOptions } from 'tools'; export function common({ store, apolloClient }: ProcessContext) { - const { $calculation, $tables } = store; + const { $calculation } = store; reaction( () => $calculation.element('selectGPSBrand').getValue(), @@ -113,75 +115,52 @@ export function common({ store, apolloClient }: ProcessContext) { ); } -export function validation({ store, apolloClient }: ProcessContext) { - const { $calculation, $tables } = store; +const key = uid(7); - { - const validationHelper = new ValidationHelper(); - reaction( - () => ({ - insTerm: $tables.insurance.row('kasko').getValue('insTerm'), - ...$calculation.$values.getValues(['leasingPeriod', 'recalcWithRevision', 'quote']), - }), - async ({ leasingPeriod, insTerm, recalcWithRevision, quote: quoteId }) => { - if (!quoteId) { - validationHelper.removeErrors(); +export function validation(context: ProcessContext) { + const { $calculation, $tables } = context.store; - return; - } + const validationSchema = createValidationSchema(context); + const helper = new ValidationHelper(); - const { - data: { quote }, - } = await apolloClient.query({ - query: CRMTypes.GetQuoteDocument, - variables: { quoteId }, - }); + reaction( + () => { + const values = $calculation.$values.getValues([ + 'leasingPeriod', + 'quote', + 'recalcWithRevision', + ]); - $tables.insurance.validate({ - helper: validationHelper, - invalid: - recalcWithRevision && - quote?.evo_one_year_insurance === true && - leasingPeriod > 15 && - insTerm === 100_000_001, - message: - 'Срок страхования КАСКО должен быть 12 месяцев, т.к. оформляется Однолетний полис', + return { + insurance: { + fingap: toJS($tables.insurance.row('fingap').getValues()), + kasko: toJS($tables.insurance.row('kasko').getValues()), + osago: toJS($tables.insurance.row('osago').getValues()), + }, + ...values, + }; + }, + async (values) => { + helper.removeErrors(); + const validationResult = await validationSchema.safeParseAsync(values); + + if (!validationResult.success) { + validationResult.error.errors.forEach(({ path, message }) => { + (path as Array).forEach((elementName) => { + if (elementName === 'insurance') { + const removeError = $tables.insurance.setError({ key, message }); + if (removeError) helper.add(removeError); + } else { + const removeError = $calculation.element(elementName).setError({ key, message }); + if (removeError) helper.add(removeError); + } + }); }); } - ); - } - - (['osago', 'kasko'] as Insurance.Keys[]).forEach((key) => { - const validationHelper = new ValidationHelper(); - reaction( - () => $tables.insurance.row(key).getValues(), - ({ insCost, insured, policyType, insuranceCompany, insTerm }) => { - validationHelper.removeErrors(); - - $tables.insurance.validate({ - helper: validationHelper, - invalid: insCost === 0 && insured === 100_000_001, - message: `Укажите стоимость ${policyType}, включаемую в график`, - }); - - $tables.insurance.validate({ - helper: validationHelper, - invalid: insCost > 0 && !insuranceCompany, - message: `Укажите страховую компанию ${policyType}`, - }); - - $tables.insurance.validate({ - helper: validationHelper, - invalid: insCost > 0 && !insTerm, - message: `Укажите срок страхования ${policyType}`, - }); - - $tables.insurance.validate({ - helper: validationHelper, - invalid: insCost > 0 && !insured, - message: `Укажите плательщика ${policyType}`, - }); - } - ); - }); + }, + { + delay: 100, + equals: comparer.structural, + } + ); } diff --git a/apps/web/process/insurance/validation.ts b/apps/web/process/insurance/validation.ts new file mode 100644 index 0000000..61f59ac --- /dev/null +++ b/apps/web/process/insurance/validation.ts @@ -0,0 +1,82 @@ +import type { ValidationContext } from '../types'; +import type * as Insurance from '@/Components/Calculation/Form/Insurance/InsuranceTable/types'; +import { RowSchema } from '@/config/schema/insurance'; +import ValuesSchema from '@/config/schema/values'; +import * as CRMTypes from '@/graphql/crm.types'; +import { z } from 'zod'; + +export function createValidationSchema({ apolloClient }: ValidationContext) { + return ValuesSchema.pick({ + leasingPeriod: true, + quote: true, + recalcWithRevision: true, + }) + .extend({ + insurance: z + .object({ + fingap: RowSchema, + kasko: RowSchema, + osago: RowSchema, + }) + .strict(), + }) + .superRefine(async ({ leasingPeriod, recalcWithRevision, quote: quoteId, insurance }, ctx) => { + if (quoteId) { + const { + data: { quote }, + } = await apolloClient.query({ + query: CRMTypes.GetQuoteDocument, + variables: { quoteId }, + }); + if ( + recalcWithRevision && + quote?.evo_one_year_insurance === true && + leasingPeriod > 15 && + insurance.kasko.insTerm === 100_000_001 + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + 'Срок страхования КАСКО должен быть 12 месяцев, т.к. оформляется Однолетний полис', + path: ['insurance'], + }); + } + } + + (['osago', 'kasko'] as Insurance.Keys[]).forEach((key) => { + const { insCost, insured, policyType, insuranceCompany, insTerm } = insurance[key]; + + if (insCost === 0 && insured === 100_000_001) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Укажите стоимость ${policyType}, включаемую в график`, + path: ['insurance'], + }); + } + + if (insCost > 0 && !insuranceCompany) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Укажите страховую компанию ${policyType}`, + path: ['insurance'], + }); + } + + if (insCost > 0 && !insTerm) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Укажите срок страхования ${policyType}`, + path: ['insurance'], + }); + } + + if (insCost > 0 && !insured) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Укажите плательщика ${policyType}`, + path: ['insurance'], + }); + } + }); + }); +} diff --git a/apps/web/process/leasing-object/reactions/common.ts b/apps/web/process/leasing-object/reactions/common.ts index 7e0ec09..0842641 100644 --- a/apps/web/process/leasing-object/reactions/common.ts +++ b/apps/web/process/leasing-object/reactions/common.ts @@ -405,4 +405,42 @@ export default function reactions({ store, apolloClient }: ProcessContext) { } } ); + + reaction( + () => + $calculation.$values.getValues([ + 'leaseObjectType', + 'engineVolume', + 'engineType', + 'leaseObjectMotorPower', + ]), + async ({ leaseObjectType: leaseObjectTypeId }) => { + if (!leaseObjectTypeId) { + $calculation.element('selectEngineType').unblock(); + $calculation.element('tbxEngineVolume').unblock(); + $calculation.element('tbxLeaseObjectMotorPower').unblock(); + + return; + } + + const { + data: { evo_leasingobject_type }, + } = await apolloClient.query({ + query: CRMTypes.GetLeaseObjectTypeDocument, + variables: { + leaseObjectTypeId, + }, + }); + + if (evo_leasingobject_type?.evo_id === '8') { + $calculation.element('selectEngineType').resetValue().block(); + $calculation.element('tbxEngineVolume').resetValue().block(); + $calculation.element('tbxLeaseObjectMotorPower').resetValue().block(); + } else { + $calculation.element('selectEngineType').unblock(); + $calculation.element('tbxEngineVolume').unblock(); + $calculation.element('tbxLeaseObjectMotorPower').unblock(); + } + } + ); } diff --git a/apps/web/process/leasing-object/reactions/validation.ts b/apps/web/process/leasing-object/reactions/validation.ts index a981034..417ff38 100644 --- a/apps/web/process/leasing-object/reactions/validation.ts +++ b/apps/web/process/leasing-object/reactions/validation.ts @@ -1,11 +1,15 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import * as CRMTypes from '@/graphql/crm.types'; +import { createValidationSchema } from '../validation'; +import type { Elements } from '@/Components/Calculation/config/map/values'; import type { ProcessContext } from '@/process/types'; import ValidationHelper from '@/stores/validation/helper'; import { autorun, reaction } from 'mobx'; +import { uid } from 'radash'; -export default function reactions({ store, apolloClient }: ProcessContext) { - const { $calculation } = store; +const key = uid(7); + +export default function reactions(context: ProcessContext) { + const { $calculation } = context.store; /** * Если model содержит данные и по связи Модель-Комплектация в CRM у данной модели есть связанные активные записи Комплектаций, @@ -14,16 +18,23 @@ export default function reactions({ store, apolloClient }: ProcessContext) { autorun( () => { const selectConfiguration = $calculation.element('selectConfiguration'); - selectConfiguration.validate({ - invalid: selectConfiguration.getOptions()?.length > 0 && !selectConfiguration.getValue(), - message: 'Не заполнено поле', - }); + if (selectConfiguration.getOptions()?.length > 0 && !selectConfiguration.getValue()) { + selectConfiguration.setError({ + key, + message: 'Не заполнено поле', + }); + } else { + selectConfiguration.removeError({ key }); + } }, { delay: 10, } ); + const validationSchema = createValidationSchema(context); + const helper = new ValidationHelper(); + reaction( () => $calculation.$values.getValues([ @@ -31,134 +42,25 @@ export default function reactions({ store, apolloClient }: ProcessContext) { 'engineVolume', 'engineType', 'leaseObjectMotorPower', + 'countSeats', + 'maxMass', + 'leaseObjectCategory', ]), - async ({ - engineType, - engineVolume, - leaseObjectType: leaseObjectTypeId, - leaseObjectMotorPower, - }) => { - if (!leaseObjectTypeId) { - $calculation.element('selectEngineType').unblock(); - $calculation.element('tbxEngineVolume').unblock(); - $calculation.element('tbxLeaseObjectMotorPower').unblock(); + async (values) => { + helper.removeErrors(); + const validationResult = await validationSchema.safeParseAsync(values); - return; + if (!validationResult.success) { + validationResult.error.errors.forEach(({ path, message }) => { + (path as Elements[]).forEach((elementName) => { + const removeError = $calculation.element(elementName).setError({ key, message }); + if (removeError) helper.add(removeError); + }); + }); } - - const { - data: { evo_leasingobject_type }, - } = await apolloClient.query({ - query: CRMTypes.GetLeaseObjectTypeDocument, - variables: { - leaseObjectTypeId, - }, - }); - - if (evo_leasingobject_type?.evo_id === '8') { - $calculation.element('selectEngineType').resetValue().block(); - $calculation.element('tbxEngineVolume').resetValue().block(); - $calculation.element('tbxLeaseObjectMotorPower').resetValue().block(); - } else { - $calculation.element('selectEngineType').unblock(); - $calculation.element('tbxEngineVolume').unblock(); - $calculation.element('tbxLeaseObjectMotorPower').unblock(); - } - - const isNotTrailer = - evo_leasingobject_type?.evo_id !== null && evo_leasingobject_type?.evo_id !== '8'; - - $calculation.element('tbxEngineVolume').validate({ - invalid: isNotTrailer && engineVolume <= 0, - message: 'Не заполнено поле', - }); - $calculation.element('selectEngineType').validate({ - invalid: isNotTrailer && !engineType, - message: 'Не заполнено поле', - }); - $calculation.element('tbxLeaseObjectMotorPower').validate({ - invalid: isNotTrailer && leaseObjectMotorPower <= 0, - message: 'Не заполнено поле', - }); + }, + { + delay: 100, } ); - - { - const validationHelper = new ValidationHelper(); - - reaction( - () => $calculation.$values.getValues(['leaseObjectType', 'countSeats', 'maxMass']), - async ({ countSeats, leaseObjectType: leaseObjectTypeId, maxMass }) => { - if (!leaseObjectTypeId) { - validationHelper.removeErrors(); - - return; - } - - const { - data: { evo_leasingobject_type }, - } = await apolloClient.query({ - query: CRMTypes.GetLeaseObjectTypeDocument, - variables: { - leaseObjectTypeId, - }, - }); - - $calculation.element('tbxCountSeats').validate({ - helper: validationHelper, - invalid: evo_leasingobject_type?.evo_id === '1' && countSeats >= 9, - message: 'Количество мест должно быть меньше 9', - }); - - $calculation.element('tbxCountSeats').validate({ - helper: validationHelper, - invalid: - (evo_leasingobject_type?.evo_id === '4' || evo_leasingobject_type?.evo_id === '5') && - countSeats <= 8, - message: 'Количество мест должно быть больше 8', - }); - - $calculation.element('tbxMaxMass').validate({ - helper: validationHelper, - invalid: evo_leasingobject_type?.evo_id === '2' && maxMass <= 0, - message: 'Не заполнено поле', - }); - } - ); - } - - { - const validationHelper = new ValidationHelper(); - - reaction( - () => $calculation.$values.getValues(['leaseObjectType', 'leaseObjectCategory']), - async ({ leaseObjectCategory, leaseObjectType: leaseObjectTypeId }) => { - if (!leaseObjectTypeId) { - validationHelper.removeErrors(); - - return; - } - - const { - data: { evo_leasingobject_type }, - } = await apolloClient.query({ - query: CRMTypes.GetLeaseObjectTypeDocument, - variables: { - leaseObjectTypeId, - }, - }); - - $calculation.element('selectLeaseObjectCategory').validate({ - helper: validationHelper, - invalid: - !leaseObjectCategory && - Boolean( - evo_leasingobject_type?.evo_id && - !['6', '9', '10'].includes(evo_leasingobject_type?.evo_id) - ), - message: 'Не заполнено поле', - }); - } - ); - } } diff --git a/apps/web/process/leasing-object/validation.ts b/apps/web/process/leasing-object/validation.ts new file mode 100644 index 0000000..72d4437 --- /dev/null +++ b/apps/web/process/leasing-object/validation.ts @@ -0,0 +1,109 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { ValidationContext } from '../types'; +import ValuesSchema from '@/config/schema/values'; +import * as CRMTypes from '@/graphql/crm.types'; +import { z } from 'zod'; + +export function createValidationSchema({ apolloClient }: ValidationContext) { + return ValuesSchema.pick({ + countSeats: true, + engineType: true, + engineVolume: true, + leaseObjectCategory: true, + leaseObjectMotorPower: true, + leaseObjectType: true, + maxMass: true, + }).superRefine( + async ( + { + leaseObjectType: leaseObjectTypeId, + engineVolume, + engineType, + leaseObjectMotorPower, + countSeats, + maxMass, + leaseObjectCategory, + }, + ctx + ) => { + if (leaseObjectTypeId) { + const { + data: { evo_leasingobject_type }, + } = await apolloClient.query({ + query: CRMTypes.GetLeaseObjectTypeDocument, + variables: { + leaseObjectTypeId, + }, + }); + + const isNotTrailer = + evo_leasingobject_type?.evo_id !== null && evo_leasingobject_type?.evo_id !== '8'; + + if (isNotTrailer && engineVolume <= 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Не заполнено поле', + path: ['tbxEngineVolume'], + }); + } + + if (isNotTrailer && !engineType) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Не заполнено поле', + path: ['selectEngineType'], + }); + } + + if (isNotTrailer && leaseObjectMotorPower <= 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Не заполнено поле', + path: ['tbxLeaseObjectMotorPower'], + }); + } + + if (evo_leasingobject_type?.evo_id === '1' && countSeats >= 9) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Количество мест должно быть меньше 9', + path: ['tbxCountSeats'], + }); + } + + if ( + (evo_leasingobject_type?.evo_id === '4' || evo_leasingobject_type?.evo_id === '5') && + countSeats <= 8 + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Количество мест должно быть больше 8', + path: ['tbxCountSeats'], + }); + } + + if (evo_leasingobject_type?.evo_id === '2' && maxMass <= 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Не заполнено поле', + path: ['tbxMaxMass'], + }); + } + + if ( + !leaseObjectCategory && + Boolean( + evo_leasingobject_type?.evo_id && + !['6', '9', '10'].includes(evo_leasingobject_type?.evo_id) + ) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Не заполнено поле', + path: ['selectLeaseObjectCategory'], + }); + } + } + } + ); +} diff --git a/apps/web/process/payments/lib/validation.ts b/apps/web/process/payments/lib/validation.ts deleted file mode 100644 index ae38795..0000000 --- a/apps/web/process/payments/lib/validation.ts +++ /dev/null @@ -1,138 +0,0 @@ -import { SEASONS_PERIOD_NUMBER, SEASONS_PERIODS } from './seasons-constants'; -import { MIN_PAYMENT } from '@/constants/values'; -import type RootStore from '@/stores/root'; -import { counting, max, min, shift, sort } from 'radash'; -import { areEqual, isSorted } from 'tools/array'; - -// eslint-disable-next-line sonarjs/cognitive-complexity -export default function validatePaymentsTable({ $calculation, $tables }: RootStore) { - /** - * в таблице платежей в столбце Соотношение платежей - * для строк с 2 до "Срок лизинга-1" минимальное значение должно быть равно 3 - */ - { - const leasingPeriod = $calculation.element('tbxLeasingPeriod').getValue(); - const targetPayments = $tables.payments.values.slice(1, leasingPeriod - 1); - - if (!targetPayments.every((payment) => payment >= MIN_PAYMENT)) { - return `Минимальное значение платежа должно быть равно ${MIN_PAYMENT}`; - } - } - - switch ($calculation.element('radioGraphType').getValue()) { - // Дегрессия - case 100_000_001: { - if (!$calculation.element('selectSeasonType').getValue()) { - return 'Не выбран тип дегрессии'; - } - - /** - * при Дегрессии все значения не должны быть равны друг другу - * + что при Легком старте 2,3 и 4 платежи не должны быть равны 100 - */ - { - const leasingPeriod = $calculation.element('tbxLeasingPeriod').getValue(); - const targetPayments = $tables.payments.values.slice(1, leasingPeriod - 1); - - if (new Set(targetPayments).size === 1) { - return 'Платежи не должны быть равны друг другу'; - } - } - - /** - * Проверка на возрастание - */ - { - const leasingPeriod = $calculation.element('tbxLeasingPeriod').getValue(); - 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) || 0) - (min(targetPayments) || 0) > 10) { - return 'Указана очень жесткая дегрессия. На 2-4 платежах Соотношение платежа должен отличаться не более чем на 10%'; - } - } - - /** - * Если вид графика = Дегрессия И значения в "Соотношении платежей" для строк с 2 До "Срок лизинга-1" как минимум 2 раза по 2 платежа должны между собой быть равны - * (т.е. берем значения "Соотношения платежей" для строк с 2 до "Срок лизинга-1" и делаем сводную таблицу - если кол-во одинаковых значение больше 2 встречаются 2 и более раза), - * то осуществлять расчет, - * иначе не осуществлять Расчет графика и выводить ошибку "Нельзя осуществить расчет - указана очень жетская дегрессия. Не менее чем у 4х платежей "Соотношение платежа" должно не отличаться между самой", - */ - { - const leasingPeriod = $calculation.element('tbxLeasingPeriod').getValue(); - 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.element('tbxLeasingPeriod').getValue(); - if (leasingPeriod < 14) { - return 'При сезонном виде графика срок лизинга должен быть больше 14 месяцев'; - } - - const seasonType = $calculation.element('selectSeasonType').getValue(); - if (!seasonType) { - return 'Не выбран тип сезонности'; - } - - const highSeasonStartOption = $calculation.element('selectHighSeasonStart').getOption(); - 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; -} diff --git a/apps/web/process/payments/reactions/validation.ts b/apps/web/process/payments/reactions/validation.ts index 7d6eefb..7338325 100644 --- a/apps/web/process/payments/reactions/validation.ts +++ b/apps/web/process/payments/reactions/validation.ts @@ -1,64 +1,56 @@ -import validatePaymentsTable from '../lib/validation'; -import { MIN_LASTPAYMENT_NSIB } from '@/constants/values'; +import { createValidationSchema } from '../validation'; +import type { Elements } from '@/Components/Calculation/config/map/values'; import type { ProcessContext } from '@/process/types'; import ValidationHelper from '@/stores/validation/helper'; import { comparer, reaction, toJS } from 'mobx'; +import { uid } from 'radash'; -export default function reactions({ store }: ProcessContext) { - const { $calculation, $tables } = store; +const key = uid(7); - const validationHelper = new ValidationHelper(); +export default function reactions(context: ProcessContext) { + const { $calculation, $tables } = context.store; + + const validationSchema = createValidationSchema(); + const helper = new ValidationHelper(); reaction( () => { const payments = toJS($tables.payments.values); - const graphType = $calculation.element('radioGraphType').getValue(); - const seasonType = $calculation.element('selectSeasonType').getValue(); - const highSeasonStart = $calculation.element('selectHighSeasonStart').getValue(); - const leasingPeriod = $calculation.element('tbxLeasingPeriod').getValue(); + const values = $calculation.$values.getValues([ + 'graphType', + 'highSeasonStart', + 'leasingPeriod', + 'seasonType', + 'insNSIB', + 'lastPaymentRub', + ]); return { - graphType, - highSeasonStart, - leasingPeriod, - payments, - seasonType, + payments: { values: payments }, + ...values, }; }, - () => { - validationHelper.removeErrors(); + async (values) => { + helper.removeErrors(); + const validationResult = await validationSchema.safeParseAsync(values); - const errorText = validatePaymentsTable(store); - - if (errorText) { - $tables.payments.validate({ - helper: validationHelper, - invalid: errorText !== null, - message: errorText, + if (!validationResult.success) { + validationResult.error.errors.forEach(({ path, message }) => { + (path as Array).forEach((elementName) => { + if (elementName === 'payments') { + const removeError = $tables.payments.setError({ key, message }); + if (removeError) helper.add(removeError); + } else { + const removeError = $calculation.element(elementName).setError({ key, message }); + if (removeError) helper.add(removeError); + } + }); }); } }, { - delay: 50, + delay: 100, equals: comparer.structural, } ); - - reaction( - () => { - const lastPaymentRub = $calculation.element('tbxLastPaymentRub').getValue(); - const insNSIB = $calculation.element('selectInsNSIB').getValue(); - - return { - insNSIB, - lastPaymentRub, - }; - }, - ({ lastPaymentRub, insNSIB }) => { - $calculation.element('tbxLastPaymentRub').validate({ - invalid: Boolean(insNSIB) && lastPaymentRub < MIN_LASTPAYMENT_NSIB, - message: `Последний платеж меньше ${MIN_LASTPAYMENT_NSIB} руб. не может быть при наличии НСИБ, укажите большее значение`, - }); - } - ); } diff --git a/apps/web/process/payments/validation.ts b/apps/web/process/payments/validation.ts new file mode 100644 index 0000000..5a5fc1f --- /dev/null +++ b/apps/web/process/payments/validation.ts @@ -0,0 +1,215 @@ +/* eslint-disable sonarjs/cognitive-complexity */ +/* eslint-disable complexity */ +import { SEASONS_PERIOD_NUMBER, SEASONS_PERIODS } from './lib/seasons-constants'; +import { selectHighSeasonStart } from '@/config/default-options'; +import PaymentsSchema from '@/config/schema/payments'; +import ValuesSchema from '@/config/schema/values'; +import { MIN_LASTPAYMENT_NSIB, MIN_PAYMENT } from '@/constants/values'; +import { counting, max, min, shift, sort } from 'radash'; +import { areEqual, isSorted } from 'tools/array'; +import { z } from 'zod'; + +export function createValidationSchema() { + return ValuesSchema.pick({ + graphType: true, + highSeasonStart: true, + insNSIB: true, + lastPaymentRub: true, + leasingPeriod: true, + seasonType: true, + }) + .extend({ + payments: PaymentsSchema, + }) + .superRefine( + async ( + { + graphType, + highSeasonStart, + leasingPeriod, + payments, + seasonType, + insNSIB, + lastPaymentRub, + }, + ctx + ) => { + if (Boolean(insNSIB) && lastPaymentRub < MIN_LASTPAYMENT_NSIB) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Последний платеж не может быть меньше ${MIN_LASTPAYMENT_NSIB} руб. при наличии НСИБ, укажите большее значение`, + path: ['tbxLastPaymentRub'], + }); + } + + { + const targetPayments = payments.values.slice(1, leasingPeriod - 1); + + if (!targetPayments.every((payment) => payment >= MIN_PAYMENT)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Минимальное значение платежа должно быть равно ${MIN_PAYMENT}`, + path: ['payments'], + }); + } + } + + switch (graphType) { + // Дегрессия + case 100_000_001: { + if (!seasonType) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Не выбран тип дегрессии', + path: ['payments', 'selectSeasonType'], + }); + break; + } + + /** + * при Дегрессии все значения не должны быть равны друг другу + * + что при Легком старте 2,3 и 4 платежи не должны быть равны 100 + */ + { + const targetPayments = payments.values.slice(1, leasingPeriod - 1); + if (new Set(targetPayments).size === 1) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Платежи не должны быть равны друг другу', + path: ['payments'], + }); + } + } + + /** + * Проверка на возрастание + */ + { + const targetPayments = payments.values.slice(1, leasingPeriod - 1); + for (let i = 2; i < targetPayments.length - 1; i += 1) { + if (targetPayments[i] > targetPayments[i - 1]) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Платежи должны убывать', + path: ['payments'], + }); + } + } + } + + /** + * Если вид графика = Дегрессия И значения в "Соотношении платежей" у 2, 3 и 4 платежа отличаются друг от друга не более чем на 10 + * (т.е. берем значения в этих полях, определяем максимальное и минимальное значение и смотрим на их разницу) + * то не осуществлять Расчет графика и выводить ошибку "Нельзя осуществить расчет - указана очень жетская дегрессия. + * На 2-4 платежах Соотношение платежа должен отличаться не более чем на 10%", + * иначе осуществлять расчет + */ + { + const targetPayments = payments.values.slice(1, 4); + + if ((max(targetPayments) || 0) - (min(targetPayments) || 0) > 10) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + 'Указана очень жесткая дегрессия. На 2-4 платежах Соотношение платежа должен отличаться не более чем на 10%', + path: ['payments'], + }); + } + } + + /** + * Если вид графика = Дегрессия И значения в "Соотношении платежей" для строк с 2 До "Срок лизинга-1" как минимум 2 раза по 2 платежа должны между собой быть равны + * (т.е. берем значения "Соотношения платежей" для строк с 2 до "Срок лизинга-1" и делаем сводную таблицу - если кол-во одинаковых значение больше 2 встречаются 2 и более раза), + * то осуществлять расчет, + * иначе не осуществлять Расчет графика и выводить ошибку "Нельзя осуществить расчет - указана очень жетская дегрессия. Не менее чем у 4х платежей "Соотношение платежа" должно не отличаться между самой", + */ + { + const targetPayments = payments.values.slice(1, leasingPeriod - 1); + const counts = counting(targetPayments, (v) => v); + if (Object.values(counts).filter((count) => count > 1).length < 2) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + 'Указана очень жесткая дегрессия. Не менее чем у 4х платежей соотношение должно не отличаться между собой', + path: ['payments'], + }); + } + } + + break; + } + + case 100_000_003: { + if (leasingPeriod < 14) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'При сезонном виде графика срок лизинга должен быть больше 14 месяцев', + path: ['payments', 'tbxLeasingPeriod'], + }); + } + + if (!seasonType) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Не выбран тип сезонности', + path: ['payments', 'selectSeasonType'], + }); + break; + } + + if (!highSeasonStart) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Не выбрано смещение сезонности', + path: ['payments', 'selectHighSeasonStart'], + }); + break; + } + + const seasons = payments.values.slice(1, SEASONS_PERIOD_NUMBER + 1); + const highSeasonStartOption = selectHighSeasonStart.find( + (x) => x.value === highSeasonStart + ); + + if (highSeasonStartOption) { + 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)) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Сезонные платежи должны убывать', + path: ['payments'], + }); + } + } + + break; + } + + // Легкий старт + case 100_000_004: { + const targetPayments = payments.values.slice(1, 4); + const sortedPayments = sort(targetPayments, (x) => x); + const areEqualPayments = new Set(targetPayments).size === 1; + + if (!areEqual(targetPayments, sortedPayments) || areEqualPayments) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: '2, 3, 4 платежи должны возрастать', + path: ['payments'], + }); + } + + break; + } + default: { + break; + } + } + } + ); +} diff --git a/apps/web/process/price/reactions/validation.ts b/apps/web/process/price/reactions/validation.ts index 8860c17..32343d9 100644 --- a/apps/web/process/price/reactions/validation.ts +++ b/apps/web/process/price/reactions/validation.ts @@ -1,74 +1,45 @@ /* eslint-disable @typescript-eslint/naming-convention */ -import { VAT } from '@/constants/values'; -import * as CRMTypes from '@/graphql/crm.types'; +import { createValidationSchema } from '../validation'; +import type { Elements } from '@/Components/Calculation/config/map/values'; import type { ProcessContext } from '@/process/types'; +import ValidationHelper from '@/stores/validation/helper'; import { reaction } from 'mobx'; -import { round } from 'tools'; +import { uid } from 'radash'; -export default function reactions({ store, apolloClient }: ProcessContext) { +const key = uid(7); + +export default function reactions(context: ProcessContext) { + const { store } = context; const { $calculation } = store; + const validationSchema = createValidationSchema(context); + const helper = new ValidationHelper(); reaction( () => $calculation.$values.getValues([ 'VATInLeaseObjectPrice', 'leaseObjectPriceWthtVAT', 'product', + 'supplierDiscountRub', + 'plPriceRub', + 'firstPaymentRub', + 'subsidySum', ]), - async ({ VATInLeaseObjectPrice, leaseObjectPriceWthtVAT, product: productId }) => { - let evo_sale_without_nds = false; + async (values) => { + helper.removeErrors(); + const validationResult = await validationSchema.safeParseAsync(values); - if (productId) { - const { - data: { evo_baseproduct }, - } = await apolloClient.query({ - query: CRMTypes.GetProductDocument, - variables: { - productId, - }, + if (!validationResult.success) { + validationResult.error.errors.forEach(({ path, message }) => { + (path as Elements[]).forEach((elementName) => { + const removeError = $calculation.element(elementName).setError({ key, message }); + if (removeError) helper.add(removeError); + }); }); - if (evo_baseproduct?.evo_sale_without_nds) { - evo_sale_without_nds = evo_baseproduct.evo_sale_without_nds; - } } - - $calculation.element('tbxVATInLeaseObjectPrice').validate({ - invalid: - evo_sale_without_nds && round(VATInLeaseObjectPrice / leaseObjectPriceWthtVAT, 2) >= VAT, - message: - 'При продаже ПЛ после ФЛ размер НДС в стоимости ПЛ не может составлять 20% и более от стоимости с НДС. Проверьте корректность НДС, либо измените Продукт', - }); - } - ); - - reaction( - () => $calculation.$values.getValues(['supplierDiscountRub', 'plPriceRub']), - ({ supplierDiscountRub, plPriceRub }) => { - $calculation.element('tbxSupplierDiscountRub').validate({ - invalid: supplierDiscountRub >= plPriceRub, - message: 'Скидка от поставщика не может быть больше или равна стоимости ПЛ', - }); - } - ); - - reaction( - () => $calculation.$values.getValues(['firstPaymentRub', 'plPriceRub']), - ({ firstPaymentRub, plPriceRub }) => { - $calculation.element('tbxFirstPaymentRub').validate({ - invalid: firstPaymentRub >= plPriceRub, - message: 'Первый платеж не может быть больше или равен стоимости ПЛ', - }); - } - ); - - reaction( - () => $calculation.$values.getValues(['firstPaymentRub', 'subsidySum']), - ({ firstPaymentRub, subsidySum }) => { - $calculation.element('tbxFirstPaymentRub').validate({ - invalid: firstPaymentRub - subsidySum < 0, - message: - 'Первый платеж с учетом субсидии получается отрицательный, увеличьте первый платеж', - }); + }, + { + delay: 100, } ); } diff --git a/apps/web/process/price/validation.ts b/apps/web/process/price/validation.ts new file mode 100644 index 0000000..427a09e --- /dev/null +++ b/apps/web/process/price/validation.ts @@ -0,0 +1,80 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { ValidationContext } from '../types'; +import ValuesSchema from '@/config/schema/values'; +import { VAT } from '@/constants/values'; +import * as CRMTypes from '@/graphql/crm.types'; +import { round } from 'tools'; +import { z } from 'zod'; + +export function createValidationSchema({ apolloClient }: ValidationContext) { + return ValuesSchema.pick({ + VATInLeaseObjectPrice: true, + firstPaymentRub: true, + leaseObjectPriceWthtVAT: true, + plPriceRub: true, + product: true, + subsidySum: true, + supplierDiscountRub: true, + }).superRefine( + async ( + { + VATInLeaseObjectPrice, + leaseObjectPriceWthtVAT, + product: productId, + supplierDiscountRub, + plPriceRub, + firstPaymentRub, + subsidySum, + }, + ctx + ) => { + if (productId) { + const { + data: { evo_baseproduct }, + } = await apolloClient.query({ + query: CRMTypes.GetProductDocument, + variables: { + productId, + }, + }); + + if ( + evo_baseproduct?.evo_sale_without_nds && + round(VATInLeaseObjectPrice / leaseObjectPriceWthtVAT, 2) >= VAT + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + 'При продаже ПЛ после ФЛ размер НДС в стоимости ПЛ не может составлять 20% и более от стоимости с НДС. Проверьте корректность НДС, либо измените Продукт', + path: ['tbxVATInLeaseObjectPrice'], + }); + } + } + + if (supplierDiscountRub >= plPriceRub) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Скидка не может быть больше или равна стоимости ПЛ', + path: ['tbxSupplierDiscountRub'], + }); + } + + if (firstPaymentRub >= plPriceRub) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Первый платеж не может быть больше или равен стоимости ПЛ', + path: ['tbxFirstPaymentRub'], + }); + } + + if (firstPaymentRub - subsidySum < 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: + 'Первый платеж с учетом субсидии получается отрицательный, увеличьте первый платеж', + path: ['tbxFirstPaymentRub'], + }); + } + } + ); +} diff --git a/apps/web/process/supplier-agent/lib/create-reactions.ts b/apps/web/process/supplier-agent/lib/create-reactions.ts index 9022c5d..5108e68 100644 --- a/apps/web/process/supplier-agent/lib/create-reactions.ts +++ b/apps/web/process/supplier-agent/lib/create-reactions.ts @@ -2,11 +2,10 @@ import type { AgentsFields, AgentsRewardConditionsFields, AgentsSumFields } from './types'; import * as CRMTypes from '@/graphql/crm.types'; import type RootStore from '@/stores/root'; -import ValidationHelper from '@/stores/validation/helper'; import type { ApolloClient } from '@apollo/client'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; -import { autorun, reaction } from 'mobx'; +import { reaction } from 'mobx'; import { normalizeOptions } from 'tools/entity'; import { makeDisposable } from 'tools/mobx'; @@ -104,108 +103,3 @@ export function fillAgentRewardSummReaction( } ); } - -export function validateAgentRewardSumm( - store: RootStore, - apolloClient: ApolloClient, - agentParams: { - rewardConditionField: AgentsRewardConditionsFields; - rewardSummField: AgentsSumFields; - } -) { - const { $calculation } = store; - const { rewardConditionField, rewardSummField } = agentParams; - - const validationHelper = new ValidationHelper(); - - reaction( - () => $calculation.element(rewardSummField).getValue(), - async (rewardSumm) => { - const conditionId = $calculation.element(rewardConditionField).getValue(); - if (!conditionId) { - validationHelper.removeErrors(); - - return; - } - - const { - data: { evo_reward_condition }, - } = await apolloClient.query< - CRMTypes.GetRewardConditionQuery, - CRMTypes.GetRewardConditionQueryVariables - >({ - query: CRMTypes.GetRewardConditionDocument, - variables: { - conditionId, - }, - }); - - if (!evo_reward_condition) { - validationHelper.removeErrors(); - - return; - } - - if (evo_reward_condition.evo_reward_summ) { - $calculation.element(rewardSummField).validate({ - helper: validationHelper, - invalid: rewardSumm > evo_reward_condition?.evo_reward_summ, - message: 'Вознаграждение указано больше условия по агентскому договору!', - }); - } - - if ( - evo_reward_condition?.evo_reduce_reward !== null && - evo_reward_condition.evo_reward_summ - ) { - $calculation.element(rewardSummField).validate({ - helper: validationHelper, - invalid: - evo_reward_condition.evo_reduce_reward === false && - rewardSumm < evo_reward_condition.evo_reward_summ, - message: 'Вознаграждение указано меньше условия по агентскому договору!', - }); - } - - if (evo_reward_condition?.evo_min_reward_summ !== null) { - $calculation.element(rewardSummField).validate({ - helper: validationHelper, - invalid: rewardSumm < evo_reward_condition?.evo_min_reward_summ, - message: 'Вознаграждение указано меньше условия по агентскому договору!', - }); - } - } - ); - - autorun(() => { - const rewardSumm = $calculation.element(rewardSummField).getValue(); - const rewardConditionOptions = $calculation.$options.getOptions(rewardConditionField); - - const conditionIds = rewardConditionOptions.map((x) => x.value); - const requests = conditionIds.map(async (conditionId) => { - const { - data: { evo_reward_condition }, - } = await apolloClient.query< - CRMTypes.GetRewardConditionQuery, - CRMTypes.GetRewardConditionQueryVariables - >({ - query: CRMTypes.GetRewardConditionDocument, - variables: { - conditionId, - }, - }); - - return evo_reward_condition; - }); - - Promise.all(requests).then((results) => { - const required_reward = results.some( - (x) => x?.evo_agency_agreementidData?.evo_required_reward === true - ); - $calculation.element(rewardSummField).validate({ - invalid: rewardSumm === 0 && required_reward, - message: 'Согласно агентскому договору обязательна выплата АВ. Заложите АВ в расчет', - }); - }); - }); -} diff --git a/apps/web/process/supplier-agent/reactions/agents.ts b/apps/web/process/supplier-agent/reactions/agents.ts index 000750d..c4e94be 100644 --- a/apps/web/process/supplier-agent/reactions/agents.ts +++ b/apps/web/process/supplier-agent/reactions/agents.ts @@ -6,12 +6,11 @@ import * as CRMTypes from '@/graphql/crm.types'; import type { ProcessContext } from '@/process/types'; import dayjs from 'dayjs'; import utc from 'dayjs/plugin/utc'; -import { autorun, reaction } from 'mobx'; +import { reaction } from 'mobx'; import { makeDisposable } from 'tools/mobx'; const { fillIndAgent, fillCalcBroker, fillCalcDoubleAgent, fillFinDepartment } = fillAgentsFromLead; -const { fillAgentRewardReaction, fillAgentRewardSummReaction, validateAgentRewardSumm } = - createReactions; +const { fillAgentRewardReaction, fillAgentRewardSummReaction } = createReactions; dayjs.extend(utc); @@ -521,165 +520,3 @@ export function common({ store, apolloClient }: ProcessContext) { ); } } - -export function validation({ store, apolloClient }: ProcessContext) { - const { $calculation } = store; - - validateAgentRewardSumm(store, apolloClient, { - rewardConditionField: 'selectIndAgentRewardCondition', - rewardSummField: 'tbxIndAgentRewardSumm', - }); - - validateAgentRewardSumm(store, apolloClient, { - rewardConditionField: 'selectCalcDoubleAgentRewardCondition', - rewardSummField: 'tbxCalcDoubleAgentRewardSumm', - }); - - validateAgentRewardSumm(store, apolloClient, { - rewardConditionField: 'selectCalcBrokerRewardCondition', - rewardSummField: 'tbxCalcBrokerRewardSum', - }); - - validateAgentRewardSumm(store, apolloClient, { - rewardConditionField: 'selectFinDepartmentRewardCondtion', - rewardSummField: 'tbxFinDepartmentRewardSumm', - }); - - /** - * Добавить валидацию на кнопку Рассчитать: - * если tbxDealerRewardSumm > 0 и - * если selectDealerPerson = selectDealerBroker и tbxDealerBrokerRewardSumm > 0, то ругаться на selectDealerPerson - * если selectDealerPerson = selectIndAgent и tbxIndAgentRewardSumm > 0, то ругаться на selectDealerPerson - * если selectDealerPerson = selectCalcDoubleAgent и tbxCalcDoubleAgentRewardSumm > 0, то ругаться на selectDealerPerson - * если selectDealerPerson = selectCalcBroker tbxCalcBrokerRewardSum > 0, то ругаться на selectDealerPerson - * если selectDealerPerson = selectFinDepartment и tbxFinDepartmentRewardSumm > 0, то ругаться на selectDealerPerson - * 2.если tbxDealerBrokerRewardSumm > 0 и - * - * если selectDealerBroker = selectDealerPerson и tbxDealerRewardSumm > 0, то ругаться на selectDealerBroker - * если selectDealerBroker = selectIndAgent и tbxIndAgentRewardSumm > 0, то ругаться на selectDealerBroker - * если selectDealerBroker = selectCalcDoubleAgent и tbxCalcDoubleAgentRewardSumm > 0, то ругаться на selectDealerBroker - * если selectDealerBroker = selectCalcBroker tbxCalcBrokerRewardSum > 0, то ругаться на selectDealerBroker - * если selectDealerBroker = selectFinDepartment и tbxFinDepartmentRewardSumm > 0, то ругаться на selectDealerBroker - * 3. если tbxIndAgentRewardSumm > 0 и - * - * если selectIndAgent = selectDealerPerson и tbxDealerRewardSumm > 0, то ругаться на selectIndAgent - * если selectIndAgent = selectDealerBroker и tbxDealerBrokerRewardSumm > 0, то ругаться на selectIndAgent - * если selectIndAgent = selectCalcDoubleAgent и tbxCalcDoubleAgentRewardSumm > 0, то ругаться на selectIndAgent - * если selectIndAgent = selectCalcBroker tbxCalcBrokerRewardSum > 0, то ругаться на selectIndAgent - * если selectIndAgent = selectFinDepartment и tbxFinDepartmentRewardSumm > 0, то ругаться на selectIndAgent - * 4. если tbxCalcDoubleAgentRewardSumm > 0 и - * - * если selectCalcDoubleAgent = selectDealerPerson и tbxDealerRewardSumm > 0, то ругаться на selectCalcDoubleAgent - * если selectCalcDoubleAgent = selectDealerBroker и tbxDealerBrokerRewardSumm > 0, то ругаться на selectCalcDoubleAgent - * если selectCalcDoubleAgent = sselectIndAgent и tbxIndAgentRewardSumm > 0, то ругаться на selectCalcDoubleAgent - * если selectCalcDoubleAgent = selectCalcBroker tbxCalcBrokerRewardSum > 0, то ругаться на selectCalcDoubleAgent - * если selectCalcDoubleAgent = selectFinDepartment и tbxFinDepartmentRewardSumm > 0, то ругаться на selectCalcDoubleAgent - * 5. если tbxCalcBrokerRewardSum > 0 и - * - * если selectCalcBroker = selectDealerPerson и tbxDealerRewardSumm > 0, то ругаться на selectCalcBroker - * если selectCalcBroker = selectDealerBroker и tbxDealerBrokerRewardSumm > 0, то ругаться на selectCalcBroker - * если selectCalcBroker = sselectIndAgent и tbxIndAgentRewardSumm > 0, то ругаться на selectCalcBroker - * если selectCalcBroker = selectCalcDoubleAgent и tbxCalcDoubleAgentRewardSumm > 0, то ругаться на selectCalcBroker - * если selectCalcBroker = selectFinDepartment и tbxFinDepartmentRewardSumm > 0, то ругаться на selectCalcBroker - * 6. если tbxFinDepartmentRewardSumm > 0 и - * - * если selectFinDepartment = selectDealerPerson и tbxDealerRewardSumm > 0, то ругаться на selectFinDepartment - * если selectFinDepartment = selectDealerBroker и tbxDealerBrokerRewardSumm > 0, то ругаться на selectFinDepartment - * если selectFinDepartment = sselectIndAgent и tbxIndAgentRewardSumm > 0, то ругаться на selectFinDepartment - * если selectFinDepartment = selectCalcDoubleAgent и tbxCalcDoubleAgentRewardSumm > 0, то ругаться на selectFinDepartment - * если selectFinDepartment = selectCalcBroker tbxCalcBrokerRewardSum > 0, то ругаться на selectFinDepartment - */ - - // eslint-disable-next-line complexity, sonarjs/cognitive-complexity - autorun(() => { - const dealerRewardSumm = $calculation.element('tbxDealerRewardSumm').getValue(); - const dealerBrokerRewardSumm = $calculation.element('tbxDealerBrokerRewardSumm').getValue(); - const indAgentRewardSumm = $calculation.element('tbxIndAgentRewardSumm').getValue(); - const calcDoubleAgentRewardSumm = $calculation - .element('tbxCalcDoubleAgentRewardSumm') - .getValue(); - const calcBrokerRewardSum = $calculation.element('tbxCalcBrokerRewardSum').getValue(); - const finDepartmentRewardSumm = $calculation.element('tbxFinDepartmentRewardSumm').getValue(); - - const dealerPerson = $calculation.element('selectDealerPerson').getValue(); - const dealerBroker = $calculation.element('selectDealerBroker').getValue(); - const indAgent = $calculation.element('selectIndAgent').getValue(); - const calcDoubleAgent = $calculation.element('selectCalcDoubleAgent').getValue(); - const calcBroker = $calculation.element('selectCalcBroker').getValue(); - const calcFinDepartment = $calculation.element('selectCalcFinDepartment').getValue(); - - const message = 'Вы закладываете вознаграждение одному и тому же агенту дважды'; - - $calculation.element('selectDealerPerson').validate({ - invalid: - dealerRewardSumm > 0 && - Boolean(dealerPerson) && - ((dealerPerson === dealerBroker && dealerBrokerRewardSumm > 0) || - (dealerPerson === indAgent && indAgentRewardSumm > 0) || - (dealerPerson === calcDoubleAgent && calcDoubleAgentRewardSumm > 0) || - (dealerPerson === calcBroker && calcBrokerRewardSum > 0) || - (dealerPerson === calcFinDepartment && finDepartmentRewardSumm > 0)), - message, - }); - - $calculation.element('selectDealerBroker').validate({ - invalid: - dealerBrokerRewardSumm > 0 && - Boolean(dealerBroker) && - ((dealerBroker === dealerPerson && dealerRewardSumm > 0) || - (dealerBroker === indAgent && indAgentRewardSumm > 0) || - (dealerBroker === calcDoubleAgent && calcDoubleAgentRewardSumm > 0) || - (dealerBroker === calcBroker && calcBrokerRewardSum > 0) || - (dealerBroker === calcFinDepartment && finDepartmentRewardSumm > 0)), - message, - }); - - $calculation.element('selectIndAgent').validate({ - invalid: - indAgentRewardSumm > 0 && - Boolean(indAgent) && - ((indAgent === dealerPerson && dealerRewardSumm > 0) || - (indAgent === dealerBroker && dealerBrokerRewardSumm > 0) || - (indAgent === calcDoubleAgent && calcDoubleAgentRewardSumm > 0) || - (indAgent === calcBroker && calcBrokerRewardSum > 0) || - (indAgent === calcFinDepartment && finDepartmentRewardSumm > 0)), - message, - }); - - $calculation.element('selectCalcDoubleAgent').validate({ - invalid: - calcDoubleAgentRewardSumm > 0 && - Boolean(calcDoubleAgent) && - ((calcDoubleAgent === dealerPerson && dealerRewardSumm > 0) || - (calcDoubleAgent === dealerBroker && dealerBrokerRewardSumm > 0) || - (calcDoubleAgent === indAgent && indAgentRewardSumm > 0) || - (calcDoubleAgent === calcBroker && calcBrokerRewardSum > 0) || - (calcDoubleAgent === calcFinDepartment && finDepartmentRewardSumm > 0)), - message, - }); - - $calculation.element('selectCalcBroker').validate({ - invalid: - calcBrokerRewardSum > 0 && - Boolean(calcBroker) && - ((calcBroker === dealerPerson && dealerRewardSumm > 0) || - (calcBroker === dealerBroker && dealerBrokerRewardSumm > 0) || - (calcBroker === indAgent && indAgentRewardSumm > 0) || - (calcBroker === calcDoubleAgent && calcDoubleAgentRewardSumm > 0) || - (calcBroker === calcFinDepartment && finDepartmentRewardSumm > 0)), - message, - }); - - $calculation.element('selectCalcFinDepartment').validate({ - invalid: - finDepartmentRewardSumm > 0 && - Boolean(calcFinDepartment) && - ((calcFinDepartment === dealerPerson && dealerRewardSumm > 0) || - (calcFinDepartment === dealerBroker && dealerBrokerRewardSumm > 0) || - (calcFinDepartment === indAgent && indAgentRewardSumm > 0) || - (calcFinDepartment === calcDoubleAgent && calcDoubleAgentRewardSumm > 0) || - (calcFinDepartment === calcBroker && calcBrokerRewardSum > 0)), - message, - }); - }); -} diff --git a/apps/web/process/supplier-agent/reactions/index.ts b/apps/web/process/supplier-agent/reactions/index.ts index 52e9725..9a46f6f 100644 --- a/apps/web/process/supplier-agent/reactions/index.ts +++ b/apps/web/process/supplier-agent/reactions/index.ts @@ -9,9 +9,6 @@ function common(context: ProcessContext) { supplier.common(context); } -function validation(context: ProcessContext) { - agents.validation(context); - supplier.validation(context); -} +export { common }; -export { common, validation }; +export { default as validation } from './validation'; diff --git a/apps/web/process/supplier-agent/reactions/leaseback.ts b/apps/web/process/supplier-agent/reactions/leaseback.ts index ce8ff98..0340a81 100644 --- a/apps/web/process/supplier-agent/reactions/leaseback.ts +++ b/apps/web/process/supplier-agent/reactions/leaseback.ts @@ -1,6 +1,6 @@ import * as CRMTypes from '@/graphql/crm.types'; import type { ProcessContext } from '@/process/types'; -import { autorun, reaction } from 'mobx'; +import { reaction } from 'mobx'; export function common({ store, apolloClient }: ProcessContext) { const { $calculation, $tables } = store; @@ -73,37 +73,4 @@ export function common({ store, apolloClient }: ProcessContext) { } } ); - - /** - * В валидацию на кнопку Рассчитать внести изменение: - * 1) поле selectDealerPerson убрать из списка обязательных для расчета полей - * 2) добавить валидацию на поле selectDealerPerson : - * Если в поле selectDealer указан account, у которого evo_return_leasing_dealer = False (или null) - * и поле selectDealerPerson = null, то выводить ошибку и поле selectDealerPerson обводить красной рамкой, - * иначе все ок - */ - - autorun(async () => { - const dealerId = $calculation.element('selectDealer').getValue(); - const dealerPersonId = $calculation.element('selectDealerPerson').getValue(); - - let returnLeasing: boolean | null | undefined; - - if (dealerId) { - const { - data: { dealer }, - } = await apolloClient.query({ - query: CRMTypes.GetDealerDocument, - variables: { - dealerId, - }, - }); - returnLeasing = dealer?.evo_return_leasing_dealer; - } - - $calculation.element('selectDealerPerson').validate({ - invalid: Boolean(dealerId) && !dealerPersonId && !returnLeasing, - message: 'Не заполнено поле', - }); - }); } diff --git a/apps/web/process/supplier-agent/reactions/supplier.ts b/apps/web/process/supplier-agent/reactions/supplier.ts index bbb50f7..1709be1 100644 --- a/apps/web/process/supplier-agent/reactions/supplier.ts +++ b/apps/web/process/supplier-agent/reactions/supplier.ts @@ -157,15 +157,3 @@ export function common({ store, apolloClient }: ProcessContext) { } ); } - -export function validation({ store, apolloClient }: ProcessContext) { - createReactions.validateAgentRewardSumm(store, apolloClient, { - rewardConditionField: 'selectDealerRewardCondition', - rewardSummField: 'tbxDealerRewardSumm', - }); - - createReactions.validateAgentRewardSumm(store, apolloClient, { - rewardConditionField: 'selectDealerBrokerRewardCondition', - rewardSummField: 'tbxDealerBrokerRewardSumm', - }); -} diff --git a/apps/web/process/supplier-agent/reactions/validation.ts b/apps/web/process/supplier-agent/reactions/validation.ts new file mode 100644 index 0000000..234660c --- /dev/null +++ b/apps/web/process/supplier-agent/reactions/validation.ts @@ -0,0 +1,70 @@ +import { createValidationSchema } from '../validation'; +import type { Elements } from '@/Components/Calculation/config/map/values'; +import type { ProcessContext } from '@/process/types'; +import ValidationHelper from '@/stores/validation/helper'; +import { reaction } from 'mobx'; +import { uid } from 'radash'; + +const key = uid(7); + +export default function reactions(context: ProcessContext) { + const { store } = context; + const { $calculation } = store; + const validationSchema = createValidationSchema(context); + + const helper = new ValidationHelper(); + reaction( + () => { + const values = $calculation.$values.getValues([ + 'calcBrokerRewardCondition', + 'calcBrokerRewardSum', + 'calcDoubleAgentRewardCondition', + 'calcDoubleAgentRewardSumm', + 'dealer', + 'dealerBroker', + 'calcFinDepartment', + 'dealerBrokerRewardCondition', + 'dealerBrokerRewardSumm', + 'dealerPerson', + 'dealerRewardCondition', + 'dealerRewardSumm', + 'finDepartmentRewardCondtion', + 'finDepartmentRewardSumm', + 'indAgent', + 'indAgentRewardCondition', + 'indAgentRewardSumm', + 'calcDoubleAgent', + 'calcBroker', + ]); + + const options = ( + [ + 'selectCalcBrokerRewardCondition', + 'selectCalcDoubleAgentRewardCondition', + 'selectDealerBrokerRewardCondition', + 'selectDealerRewardCondition', + 'selectFinDepartmentRewardCondtion', + 'selectIndAgentRewardCondition', + ] as Elements[] + ).map((elementName) => $calculation.element(elementName).getOptions()); + + return { options, values }; + }, + async ({ values }) => { + helper.removeErrors(); + const validationResult = await validationSchema.safeParseAsync(values); + + if (!validationResult.success) { + validationResult.error.errors.forEach(({ path, message }) => { + (path as Elements[]).forEach((elementName) => { + const removeError = $calculation.element(elementName).setError({ key, message }); + if (removeError) helper.add(removeError); + }); + }); + } + }, + { + delay: 100, + } + ); +} diff --git a/apps/web/process/supplier-agent/validation.ts b/apps/web/process/supplier-agent/validation.ts new file mode 100644 index 0000000..47a2a4b --- /dev/null +++ b/apps/web/process/supplier-agent/validation.ts @@ -0,0 +1,388 @@ +/* eslint-disable zod/require-strict */ +/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable complexity */ +import type { ValidationContext } from '../types'; +import type { Elements } from '@/Components/Calculation/config/map/values'; +import ValuesSchema from '@/config/schema/values'; +import * as CRMTypes from '@/graphql/crm.types'; +import dayjs from 'dayjs'; +import utc from 'dayjs/plugin/utc'; +import { normalizeOptions } from 'tools'; +import type { RefinementCtx } from 'zod'; +import { z } from 'zod'; + +dayjs.extend(utc); + +function helper({ apolloClient, ctx }: ValidationContext & { ctx: RefinementCtx }) { + return { + async validateRewardSum({ + agentid, + conditionId, + sumFieldName, + sum, + }: { + agentid: string | null; + conditionId: string | null; + sum: number; + sumFieldName: Elements; + }) { + if (agentid) { + const { + data: { evo_reward_conditions }, + } = await apolloClient.query({ + query: CRMTypes.GetRewardConditionsDocument, + variables: { + agentid, + currentDate: dayjs().utc(false).format('YYYY-MM-DD'), + }, + }); + + const requests = normalizeOptions(evo_reward_conditions)?.map(async ({ value }) => { + const { + data: { evo_reward_condition }, + } = await apolloClient.query< + CRMTypes.GetRewardConditionQuery, + CRMTypes.GetRewardConditionQueryVariables + >({ + query: CRMTypes.GetRewardConditionDocument, + variables: { + conditionId: value, + }, + }); + + return evo_reward_condition; + }); + + const requiredReward = (await Promise.all(requests)).some( + (x) => x?.evo_agency_agreementidData?.evo_required_reward === true + ); + + if (sum === 0 && requiredReward) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Согласно агентскому договору обязательна выплата АВ. Заложите АВ в расчет', + path: [sumFieldName], + }); + } + } + + if (conditionId) { + const { + data: { evo_reward_condition }, + } = await apolloClient.query< + CRMTypes.GetRewardConditionQuery, + CRMTypes.GetRewardConditionQueryVariables + >({ + query: CRMTypes.GetRewardConditionDocument, + variables: { + conditionId, + }, + }); + + if (evo_reward_condition?.evo_reward_summ && sum > evo_reward_condition?.evo_reward_summ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Вознаграждение указано больше условия по агентскому договору!', + path: [sumFieldName], + }); + } + + if ( + evo_reward_condition?.evo_reduce_reward === false && + evo_reward_condition?.evo_reward_summ && + sum < evo_reward_condition?.evo_reward_summ + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Вознаграждение указано меньше условия по агентскому договору!', + path: [sumFieldName], + }); + } + + if ( + evo_reward_condition?.evo_min_reward_summ && + sum < evo_reward_condition?.evo_min_reward_summ + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Вознаграждение указано меньше условия по агентскому договору!', + path: [sumFieldName], + }); + } + } + }, + }; +} + +const ERR_DOUBLE_REWARD = 'Вы закладываете вознаграждение одному и тому же агенту дважды'; + +const AgentsSchema = ValuesSchema.pick({ + calcBroker: true, + calcDoubleAgent: true, + calcFinDepartment: true, + dealer: true, + dealerBroker: true, + dealerPerson: true, + indAgent: true, +}); + +const ConditionsSchema = ValuesSchema.pick({ + calcBrokerRewardCondition: true, + calcDoubleAgentRewardCondition: true, + dealerBrokerRewardCondition: true, + dealerRewardCondition: true, + finDepartmentRewardCondtion: true, + indAgentRewardCondition: true, +}); + +const SumsSchema = ValuesSchema.pick({ + calcBrokerRewardSum: true, + calcDoubleAgentRewardSumm: true, + dealerBrokerRewardSumm: true, + dealerRewardSumm: true, + finDepartmentRewardSumm: true, + indAgentRewardSumm: true, +}); + +export function createValidationSchema(context: ValidationContext) { + const { apolloClient } = context; + + return z + .object({}) + .merge(AgentsSchema) + .merge(ConditionsSchema) + .merge(SumsSchema) + .superRefine(async (values, ctx) => { + const { + calcBrokerRewardCondition, + calcBrokerRewardSum, + calcDoubleAgentRewardCondition, + calcDoubleAgentRewardSumm, + dealer: dealerId, + dealerBroker, + calcFinDepartment, + dealerBrokerRewardCondition, + dealerBrokerRewardSumm, + dealerPerson, + dealerRewardCondition, + dealerRewardSumm, + finDepartmentRewardCondtion, + finDepartmentRewardSumm, + indAgent, + indAgentRewardCondition, + indAgentRewardSumm, + calcDoubleAgent, + calcBroker, + } = values; + /** + * Добавить валидацию на кнопку Рассчитать: + * если tbxDealerRewardSumm > 0 и + * если selectDealerPerson = selectDealerBroker и tbxDealerBrokerRewardSumm > 0, то ругаться на selectDealerPerson + * если selectDealerPerson = selectIndAgent и tbxIndAgentRewardSumm > 0, то ругаться на selectDealerPerson + * если selectDealerPerson = selectCalcDoubleAgent и tbxCalcDoubleAgentRewardSumm > 0, то ругаться на selectDealerPerson + * если selectDealerPerson = selectCalcBroker tbxCalcBrokerRewardSum > 0, то ругаться на selectDealerPerson + * если selectDealerPerson = selectFinDepartment и tbxFinDepartmentRewardSumm > 0, то ругаться на selectDealerPerson + * 2.если tbxDealerBrokerRewardSumm > 0 и + * + * если selectDealerBroker = selectDealerPerson и tbxDealerRewardSumm > 0, то ругаться на selectDealerBroker + * если selectDealerBroker = selectIndAgent и tbxIndAgentRewardSumm > 0, то ругаться на selectDealerBroker + * если selectDealerBroker = selectCalcDoubleAgent и tbxCalcDoubleAgentRewardSumm > 0, то ругаться на selectDealerBroker + * если selectDealerBroker = selectCalcBroker tbxCalcBrokerRewardSum > 0, то ругаться на selectDealerBroker + * если selectDealerBroker = selectFinDepartment и tbxFinDepartmentRewardSumm > 0, то ругаться на selectDealerBroker + * 3. если tbxIndAgentRewardSumm > 0 и + * + * если selectIndAgent = selectDealerPerson и tbxDealerRewardSumm > 0, то ругаться на selectIndAgent + * если selectIndAgent = selectDealerBroker и tbxDealerBrokerRewardSumm > 0, то ругаться на selectIndAgent + * если selectIndAgent = selectCalcDoubleAgent и tbxCalcDoubleAgentRewardSumm > 0, то ругаться на selectIndAgent + * если selectIndAgent = selectCalcBroker tbxCalcBrokerRewardSum > 0, то ругаться на selectIndAgent + * если selectIndAgent = selectFinDepartment и tbxFinDepartmentRewardSumm > 0, то ругаться на selectIndAgent + * 4. если tbxCalcDoubleAgentRewardSumm > 0 и + * + * если selectCalcDoubleAgent = selectDealerPerson и tbxDealerRewardSumm > 0, то ругаться на selectCalcDoubleAgent + * если selectCalcDoubleAgent = selectDealerBroker и tbxDealerBrokerRewardSumm > 0, то ругаться на selectCalcDoubleAgent + * если selectCalcDoubleAgent = sselectIndAgent и tbxIndAgentRewardSumm > 0, то ругаться на selectCalcDoubleAgent + * если selectCalcDoubleAgent = selectCalcBroker tbxCalcBrokerRewardSum > 0, то ругаться на selectCalcDoubleAgent + * если selectCalcDoubleAgent = selectFinDepartment и tbxFinDepartmentRewardSumm > 0, то ругаться на selectCalcDoubleAgent + * 5. если tbxCalcBrokerRewardSum > 0 и + * + * если selectCalcBroker = selectDealerPerson и tbxDealerRewardSumm > 0, то ругаться на selectCalcBroker + * если selectCalcBroker = selectDealerBroker и tbxDealerBrokerRewardSumm > 0, то ругаться на selectCalcBroker + * если selectCalcBroker = sselectIndAgent и tbxIndAgentRewardSumm > 0, то ругаться на selectCalcBroker + * если selectCalcBroker = selectCalcDoubleAgent и tbxCalcDoubleAgentRewardSumm > 0, то ругаться на selectCalcBroker + * если selectCalcBroker = selectFinDepartment и tbxFinDepartmentRewardSumm > 0, то ругаться на selectCalcBroker + * 6. если tbxFinDepartmentRewardSumm > 0 и + * + * если selectFinDepartment = selectDealerPerson и tbxDealerRewardSumm > 0, то ругаться на selectFinDepartment + * если selectFinDepartment = selectDealerBroker и tbxDealerBrokerRewardSumm > 0, то ругаться на selectFinDepartment + * если selectFinDepartment = sselectIndAgent и tbxIndAgentRewardSumm > 0, то ругаться на selectFinDepartment + * если selectFinDepartment = selectCalcDoubleAgent и tbxCalcDoubleAgentRewardSumm > 0, то ругаться на selectFinDepartment + * если selectFinDepartment = selectCalcBroker tbxCalcBrokerRewardSum > 0, то ругаться на selectFinDepartment + */ + + if ( + dealerRewardSumm > 0 && + Boolean(dealerPerson) && + ((dealerPerson === dealerBroker && dealerBrokerRewardSumm > 0) || + (dealerPerson === indAgent && indAgentRewardSumm > 0) || + (dealerPerson === calcDoubleAgent && calcDoubleAgentRewardSumm > 0) || + (dealerPerson === calcBroker && calcBrokerRewardSum > 0) || + (dealerPerson === calcFinDepartment && finDepartmentRewardSumm > 0)) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: ERR_DOUBLE_REWARD, + path: ['selectDealerPerson'], + }); + } + + if ( + dealerBrokerRewardSumm > 0 && + Boolean(dealerBroker) && + ((dealerBroker === dealerPerson && dealerRewardSumm > 0) || + (dealerBroker === indAgent && indAgentRewardSumm > 0) || + (dealerBroker === calcDoubleAgent && calcDoubleAgentRewardSumm > 0) || + (dealerBroker === calcBroker && calcBrokerRewardSum > 0) || + (dealerBroker === calcFinDepartment && finDepartmentRewardSumm > 0)) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: ERR_DOUBLE_REWARD, + path: ['selectDealerBroker'], + }); + } + + if ( + indAgentRewardSumm > 0 && + Boolean(indAgent) && + ((indAgent === dealerPerson && dealerRewardSumm > 0) || + (indAgent === dealerBroker && dealerBrokerRewardSumm > 0) || + (indAgent === calcDoubleAgent && calcDoubleAgentRewardSumm > 0) || + (indAgent === calcBroker && calcBrokerRewardSum > 0) || + (indAgent === calcFinDepartment && finDepartmentRewardSumm > 0)) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: ERR_DOUBLE_REWARD, + path: ['selectIndAgent'], + }); + } + + if ( + calcDoubleAgentRewardSumm > 0 && + Boolean(calcDoubleAgent) && + ((calcDoubleAgent === dealerPerson && dealerRewardSumm > 0) || + (calcDoubleAgent === dealerBroker && dealerBrokerRewardSumm > 0) || + (calcDoubleAgent === indAgent && indAgentRewardSumm > 0) || + (calcDoubleAgent === calcBroker && calcBrokerRewardSum > 0) || + (calcDoubleAgent === calcFinDepartment && finDepartmentRewardSumm > 0)) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: ERR_DOUBLE_REWARD, + path: ['selectCalcDoubleAgent'], + }); + } + + if ( + calcBrokerRewardSum > 0 && + Boolean(calcBroker) && + ((calcBroker === dealerPerson && dealerRewardSumm > 0) || + (calcBroker === dealerBroker && dealerBrokerRewardSumm > 0) || + (calcBroker === indAgent && indAgentRewardSumm > 0) || + (calcBroker === calcDoubleAgent && calcDoubleAgentRewardSumm > 0) || + (calcBroker === calcFinDepartment && finDepartmentRewardSumm > 0)) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: ERR_DOUBLE_REWARD, + path: ['selectCalcBroker'], + }); + } + + if ( + finDepartmentRewardSumm > 0 && + Boolean(calcFinDepartment) && + ((calcFinDepartment === dealerPerson && dealerRewardSumm > 0) || + (calcFinDepartment === dealerBroker && dealerBrokerRewardSumm > 0) || + (calcFinDepartment === indAgent && indAgentRewardSumm > 0) || + (calcFinDepartment === calcDoubleAgent && calcDoubleAgentRewardSumm > 0) || + (calcFinDepartment === calcBroker && calcBrokerRewardSum > 0)) + ) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: ERR_DOUBLE_REWARD, + path: ['selectCalcFinDepartment'], + }); + } + + /** + * В валидацию на кнопку Рассчитать внести изменение: + * 1) поле selectDealerPerson убрать из списка обязательных для расчета полей + * 2) добавить валидацию на поле selectDealerPerson : + * Если в поле selectDealer указан account, у которого evo_return_leasing_dealer = False (или null) + * и поле selectDealerPerson = null, то выводить ошибку и поле selectDealerPerson обводить красной рамкой, + * иначе все ок + */ + if (dealerId) { + const { + data: { dealer }, + } = await apolloClient.query({ + query: CRMTypes.GetDealerDocument, + variables: { + dealerId, + }, + }); + + if (!dealerPerson && !dealer?.evo_return_leasing_dealer) + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Не заполнено поле', + path: ['selectDealerPerson'], + }); + } + + const { validateRewardSum } = helper({ ...context, ctx }); + + await validateRewardSum({ + agentid: dealerPerson, + conditionId: dealerRewardCondition, + sum: dealerRewardSumm, + sumFieldName: 'tbxDealerRewardSumm', + }); + + await validateRewardSum({ + agentid: dealerBroker, + conditionId: dealerBrokerRewardCondition, + sum: dealerBrokerRewardSumm, + sumFieldName: 'tbxDealerBrokerRewardSumm', + }); + + await validateRewardSum({ + agentid: indAgent, + conditionId: indAgentRewardCondition, + sum: indAgentRewardSumm, + sumFieldName: 'tbxIndAgentRewardSumm', + }); + + await validateRewardSum({ + agentid: calcDoubleAgent, + conditionId: calcDoubleAgentRewardCondition, + sum: calcDoubleAgentRewardSumm, + sumFieldName: 'tbxCalcDoubleAgentRewardSumm', + }); + + await validateRewardSum({ + agentid: calcBroker, + conditionId: calcBrokerRewardCondition, + sum: calcBrokerRewardSum, + sumFieldName: 'tbxCalcBrokerRewardSum', + }); + + await validateRewardSum({ + agentid: calcFinDepartment, + conditionId: finDepartmentRewardCondtion, + sum: finDepartmentRewardSumm, + sumFieldName: 'tbxFinDepartmentRewardSumm', + }); + }); +} diff --git a/apps/web/process/types.ts b/apps/web/process/types.ts index 6ec8db7..da05177 100644 --- a/apps/web/process/types.ts +++ b/apps/web/process/types.ts @@ -13,3 +13,5 @@ export type ProcessContext = { export type Process = { reactions: Record void>; }; + +export type ValidationContext = Omit; diff --git a/apps/web/process/used-pl/reactions.ts b/apps/web/process/used-pl/reactions.ts index 50c88da..8375a75 100644 --- a/apps/web/process/used-pl/reactions.ts +++ b/apps/web/process/used-pl/reactions.ts @@ -1,8 +1,11 @@ /* eslint-disable @typescript-eslint/naming-convention */ import type { ProcessContext } from '../types'; +import { createValidationSchema } from './validation'; +import type { Elements } from '@/Components/Calculation/config/map/values'; import * as CRMTypes from '@/graphql/crm.types'; import ValidationHelper from '@/stores/validation/helper'; import { reaction } from 'mobx'; +import { uid } from 'radash'; export function common({ store, apolloClient }: ProcessContext) { const { $calculation } = store; @@ -150,41 +153,6 @@ export function common({ store, apolloClient }: ProcessContext) { } ); - /** - * Добавить реакцию на изменение Типа ПЛ selectLeaseObjectType , ПЛ БУ cbxLeaseObjectUsed и Моточасы tbxEngineHours: - * Если ПЛ БУ cbxLeaseObjectUsed = True и Тип ПЛ selectLeaseObjectType = Спецтехника (id=9) и Моточасы = 0, - * то поле Моточасы tbxEngineHours должно обводиться красной рамкой и выводиться сообщение - * "Укажите Моточасы, иначе красная рамка снимается. - * При красной рамке в данном поле нельзя осуществить расчет графика. - */ - { - const validationHelper = new ValidationHelper(); - - reaction( - () => $calculation.$values.getValues(['leaseObjectUsed', 'engineHours', 'leaseObjectType']), - async ({ leaseObjectType: leaseObjectTypeId, leaseObjectUsed, engineHours }) => { - if (!leaseObjectTypeId) { - validationHelper.removeErrors(); - - return; - } - - const { - data: { evo_leasingobject_type }, - } = await apolloClient.query({ - query: CRMTypes.GetLeaseObjectTypeDocument, - variables: { leaseObjectTypeId }, - }); - - $calculation.element('tbxEngineHours').validate({ - helper: validationHelper, - invalid: leaseObjectUsed && evo_leasingobject_type?.evo_id === '9' && !engineHours, - message: 'Не заполнено поле', - }); - } - ); - } - reaction( () => $calculation.element('cbxLeaseObjectUsed').getValue(), (leaseObjectUsed) => { @@ -196,16 +164,6 @@ export function common({ store, apolloClient }: ProcessContext) { } ); - reaction( - () => $calculation.$values.getValues(['mileage', 'leaseObjectUsed']), - ({ mileage, leaseObjectUsed }) => { - $calculation.element('tbxMileage').validate({ - invalid: leaseObjectUsed && !mileage, - message: 'Не заполнено поле', - }); - } - ); - reaction( () => $calculation.element('cbxLeaseObjectUsed').getValue(), (leaseObjectUsed) => { @@ -273,38 +231,6 @@ export function common({ store, apolloClient }: ProcessContext) { } ); - /** - * Если "Категория" содержит данные, то должны быть доступными для набора только арабские цифры и буквы латинского алфавита за исключением I, O, Q, так как они сходны по начертанию с цифрами 1, 0, 9. Можно использовать регулярное выражение: "^[A-HJ-NPR-Za-hj-npr-z0-9]{17}$". - * Иначе (если Категория = пусто) то требуется аналогичная первому условию маска, но без проверки 17ти символов (допускать и больше и меньше символов, мб так: "^[A-HJ-NPR-Za-hj-npr-z0-9]{99}$". - * Вот так: /^[A-HJ-NPR-Za-hj-npr-z0-9]+$/ - */ - { - const vinRegex = /^[\dA-HJ-NPR-Za-hj-npr-z]+$/u; - const validationHelper = new ValidationHelper(); - - reaction( - () => $calculation.$values.getValues(['vin', 'leaseObjectCategory', 'leaseObjectUsed']), - ({ vin, leaseObjectCategory }) => { - if (!vin) { - validationHelper.removeErrors(); - - return; - } - - let invalid = vinRegex.test(vin) === false; - if (leaseObjectCategory && vin?.length !== 17) { - invalid = true; - } - - $calculation.element('tbxVIN').validate({ - helper: validationHelper, - invalid, - message: 'Неверно заполнено поле', - }); - } - ); - } - reaction( () => $calculation.element('cbxLeaseObjectUsed').getValue(), (leaseObjectUsed) => { @@ -316,3 +242,40 @@ export function common({ store, apolloClient }: ProcessContext) { } ); } + +const key = uid(7); + +export function validation(context: ProcessContext) { + const { store } = context; + const { $calculation } = store; + const validationSchema = createValidationSchema(context); + + const helper = new ValidationHelper(); + reaction( + () => + $calculation.$values.getValues([ + 'engineHours', + 'leaseObjectCategory', + 'leaseObjectType', + 'leaseObjectUsed', + 'mileage', + 'vin', + ]), + async (values) => { + helper.removeErrors(); + const validationResult = await validationSchema.safeParseAsync(values); + + if (!validationResult.success) { + validationResult.error.errors.forEach(({ path, message }) => { + (path as Elements[]).forEach((elementName) => { + const removeError = $calculation.element(elementName).setError({ key, message }); + if (removeError) helper.add(removeError); + }); + }); + } + }, + { + delay: 100, + } + ); +} diff --git a/apps/web/process/used-pl/validation.ts b/apps/web/process/used-pl/validation.ts new file mode 100644 index 0000000..b759cbd --- /dev/null +++ b/apps/web/process/used-pl/validation.ts @@ -0,0 +1,81 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { ValidationContext } from '../types'; +import ValuesSchema from '@/config/schema/values'; +import * as CRMTypes from '@/graphql/crm.types'; +import { z } from 'zod'; + +const vinRegex = /^[\dA-HJ-NPR-Za-hj-npr-z]+$/u; + +export function createValidationSchema({ apolloClient }: ValidationContext) { + return ValuesSchema.pick({ + engineHours: true, + leaseObjectCategory: true, + leaseObjectType: true, + leaseObjectUsed: true, + mileage: true, + vin: true, + }).superRefine( + async ( + { + engineHours, + leaseObjectCategory, + leaseObjectType: leaseObjectTypeId, + leaseObjectUsed, + mileage, + vin, + }, + ctx + ) => { + /** + * Добавить реакцию на изменение Типа ПЛ selectLeaseObjectType , ПЛ БУ cbxLeaseObjectUsed и Моточасы tbxEngineHours: + * Если ПЛ БУ cbxLeaseObjectUsed = True и Тип ПЛ selectLeaseObjectType = Спецтехника (id=9) и Моточасы = 0, + * то поле Моточасы tbxEngineHours должно обводиться красной рамкой и выводиться сообщение + * "Укажите Моточасы, иначе красная рамка снимается. + * При красной рамке в данном поле нельзя осуществить расчет графика. + */ + if (leaseObjectTypeId) { + const { + data: { evo_leasingobject_type }, + } = await apolloClient.query({ + query: CRMTypes.GetLeaseObjectTypeDocument, + variables: { leaseObjectTypeId }, + }); + if (leaseObjectUsed && evo_leasingobject_type?.evo_id === '9' && !engineHours) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Не заполнено поле', + path: ['tbxEngineHours'], + }); + } + } + + if (leaseObjectUsed && !mileage) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Не заполнено поле', + path: ['tbxMileage'], + }); + } + + /** + * Если "Категория" содержит данные, то должны быть доступными для набора только арабские цифры и буквы латинского алфавита за исключением I, O, Q, так как они сходны по начертанию с цифрами 1, 0, 9. Можно использовать регулярное выражение: "^[A-HJ-NPR-Za-hj-npr-z0-9]{17}$". + * Иначе (если Категория = пусто) то требуется аналогичная первому условию маска, но без проверки 17ти символов (допускать и больше и меньше символов, мб так: "^[A-HJ-NPR-Za-hj-npr-z0-9]{99}$". + * Вот так: /^[A-HJ-NPR-Za-hj-npr-z0-9]+$/ + */ + if (vin) { + let invalid = vinRegex.test(vin) === false; + if (leaseObjectCategory && vin?.length !== 17) { + invalid = true; + } + + if (invalid) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: 'Неверно заполнено поле', + path: ['tbxVIN'], + }); + } + } + } + ); +} diff --git a/apps/web/stores/calculation/index.ts b/apps/web/stores/calculation/index.ts index 42816cb..ab662c5 100644 --- a/apps/web/stores/calculation/index.ts +++ b/apps/web/stores/calculation/index.ts @@ -1,4 +1,4 @@ -import type { RemoveError, ValidationParams } from '../validation/types'; +import type { ValidationParams } from '../validation/types'; import OptionsStore from './options'; import StatusStore from './statuses'; import ValuesStore from './values'; @@ -56,6 +56,10 @@ export default class CalculationStore { return this.$values.getValue(valueName) as Values.ElementsTypes[E]; }, + removeError: (params: Pick) => { + this.$validation[elementName]?.removeError(params); + }, + reset: () => { const valueName = getValueName(elementName); this.$values.resetValue(valueName); @@ -80,6 +84,12 @@ export default class CalculationStore { return this.element(elementName); }, + setError: (params: ValidationParams) => { + if (!this.$validation[elementName]) this.createElementValidation(elementName); + + return this.$validation[elementName]?.setError(params); + }, + setOptions: (options: Array>) => { this.$options.setOptions(elementName, options); @@ -98,20 +108,5 @@ export default class CalculationStore { return this.element(elementName); }, - - validate: ({ invalid, message, silent, helper }: ValidationParams) => { - if (!this.$validation[elementName]) this.createElementValidation(elementName); - - let removeError: RemoveError | undefined; - - if (invalid) { - removeError = this.$validation[elementName]?.addError(message, silent); - if (helper && removeError) helper.add(removeError); - } else { - this.$validation[elementName]?.removeError(message); - } - - return removeError; - }, }); } diff --git a/apps/web/stores/calculation/validation/hooks.js b/apps/web/stores/calculation/validation/hooks.js index 9ae1faa..7a8a06e 100644 --- a/apps/web/stores/calculation/validation/hooks.js +++ b/apps/web/stores/calculation/validation/hooks.js @@ -2,9 +2,8 @@ import { useStore } from '@/stores/hooks'; export function useValidation(elementName) { const { $calculation } = useStore(); - const messages = $calculation.$validation[elementName]?.getMessages(); - - if (messages?.length) { + const hasErrors = $calculation.$validation?.[elementName]?.hasErrors; + if (hasErrors) { return { help: 'Некорректные данные', isValid: false, diff --git a/apps/web/stores/tables/fingap/index.ts b/apps/web/stores/tables/fingap/index.ts index 9df4da9..82d5f6d 100644 --- a/apps/web/stores/tables/fingap/index.ts +++ b/apps/web/stores/tables/fingap/index.ts @@ -1,5 +1,5 @@ import Validation from '../../validation'; -import type { RemoveError, ValidationParams } from '../../validation/types'; +import type { ValidationParams } from '../../validation/types'; import type * as FinGAP from '@/Components/Calculation/Form/Insurance/FinGAPTable/types'; import type RootStore from '@/stores/root'; import type { IObservableArray } from 'mobx'; @@ -38,17 +38,10 @@ export default class FinGAPTable { .reduce((sum, risk) => sum + risk.premium, 0); } - public validate = ({ invalid, message, helper }: ValidationParams) => { - let removeError: RemoveError | undefined; + public setError = (params: ValidationParams) => this.validation.setError(params); - if (invalid) { - removeError = this.validation?.addError(message); - if (helper && removeError) helper.add(removeError); - } else { - this.validation?.removeError(message); - } - - return removeError; + public removeError = (params: Pick) => { + this.validation.removeError(params); }; public clear = () => { diff --git a/apps/web/stores/tables/insurance/index.ts b/apps/web/stores/tables/insurance/index.ts index f66df2d..af7faaa 100644 --- a/apps/web/stores/tables/insurance/index.ts +++ b/apps/web/stores/tables/insurance/index.ts @@ -1,5 +1,5 @@ import Validation from '../../validation'; -import type { RemoveError, ValidationParams } from '../../validation/types'; +import type { ValidationParams } from '../../validation/types'; import type * as Insurance from '@/Components/Calculation/Form/Insurance/InsuranceTable/types'; import * as insuranceTableConfig from '@/config/tables/insurance-table'; import type RootStore from '@/stores/root'; @@ -40,17 +40,10 @@ export default class InsuranceTable { if (initialStatuses) this.statuses = initialStatuses; }; - public validate = ({ invalid, message, helper }: ValidationParams) => { - let removeError: RemoveError | undefined; + public setError = (params: ValidationParams) => this.validation.setError(params); - if (invalid) { - removeError = this.validation?.addError(message); - if (helper && removeError) helper.add(removeError); - } else { - this.validation?.removeError(message); - } - - return removeError; + public removeError = (params: Pick) => { + this.validation.removeError(params); }; public reset = () => { diff --git a/apps/web/stores/tables/payments/index.ts b/apps/web/stores/tables/payments/index.ts index cbe63eb..ce9b135 100644 --- a/apps/web/stores/tables/payments/index.ts +++ b/apps/web/stores/tables/payments/index.ts @@ -1,5 +1,5 @@ import Validation from '../../validation'; -import type { RemoveError, ValidationParams } from '../../validation/types'; +import type { ValidationParams } from '../../validation/types'; import type { Row } from './types'; import type RootStore from '@/stores/root'; import type { IObservableArray } from 'mobx'; @@ -72,17 +72,10 @@ export default class PaymentsTable { this.setStatuses(statuses); }; - public validate = ({ invalid, message, helper }: ValidationParams) => { - let removeError: RemoveError | undefined; + public setError = (params: ValidationParams) => this.validation.setError(params); - if (invalid) { - removeError = this.validation?.addError(message); - if (helper && removeError) helper.add(removeError); - } else { - this.validation?.removeError(message); - } - - return removeError; + public removeError = (params: Pick) => { + this.validation.removeError(params); }; public reset = () => { diff --git a/apps/web/stores/validation/index.ts b/apps/web/stores/validation/index.ts index e9c86b7..5b9ca5a 100644 --- a/apps/web/stores/validation/index.ts +++ b/apps/web/stores/validation/index.ts @@ -1,32 +1,38 @@ -import type { RemoveError, ValidationConfig } from './types'; +import type { RemoveError, ValidationConfig, ValidationError, ValidationParams } from './types'; import { makeAutoObservable } from 'mobx'; import notification from 'ui/elements/notification'; export default class Validation { private params: ValidationConfig; - private messages: Set; + private errors: Set; constructor(config: ValidationConfig) { this.params = config; - this.messages = new Set(); + this.errors = new Set(); makeAutoObservable(this); } public get hasErrors() { - return this.messages.size > 0; + return this.errors.size > 0; } - public getMessages() { - return [...this.messages]; + public getErrors() { + return [...this.errors]; } - public removeError = (message: string) => { - this.messages.delete(message); - if (this.messages.size === 0) notification.close(this.params.err_key); + public removeError = ({ key }: Pick) => { + const error = [...this.errors].find((x) => x.key === key); + if (error) this.errors.delete(error); + if (this.errors.size === 0) notification.close(this.params.err_key); }; - public addError = (message: string, silent?: boolean) => { - if (!silent && !this.messages.has(message)) { + public setError = ({ key, message, silent }: ValidationParams) => { + const error = [...this.errors].find((x) => x.key === key); + if (error) this.removeError({ key }); + + this.errors.add({ key, message }); + + if (!silent) { notification.error({ description: message, key: this.params.err_key, @@ -34,13 +40,11 @@ export default class Validation { }); } - this.messages.add(message); - - return (() => this.removeError(message)) as RemoveError; + return (() => this.removeError({ key })) as RemoveError; }; public clearErrors = () => { - this.messages.clear(); + this.errors.clear(); notification.close(this.params.err_key); }; } diff --git a/apps/web/stores/validation/types.ts b/apps/web/stores/validation/types.ts index 4c27464..888cd7e 100644 --- a/apps/web/stores/validation/types.ts +++ b/apps/web/stores/validation/types.ts @@ -1,15 +1,10 @@ -import type ValidationHelper from './helper'; - export type ValidationConfig = { err_key: string; err_title: string; }; -export type ValidationParams = { - helper?: ValidationHelper; - invalid: boolean; - message: string; - silent?: boolean; -}; +export type ValidationError = { key: string; message: string }; + +export type ValidationParams = ValidationError & { silent?: boolean }; export type RemoveError = () => void;