Compare commits

..

10 Commits

Author SHA1 Message Date
vchikalkin
74401307fc eslint: max-len fixes 2023-01-17 14:58:13 +03:00
vchikalkin
57614ecddc fix: Согласно агентскому договору обязательна выплата АВ. Заложите АВ в расчет 2023-01-16 17:18:53 +03:00
vchikalkin
88aa991f0f В валидацию на кнопку Рассчитать внести изменение:
1) поле selectDealerPerson убрать из списка обязательных для расчета полей
    2) добавить валидацию на поле selectDealerPerson :
    Если в поле selectDealer указан account, у которого evo_return_leasing_dealer = False (или null)
    и поле selectDealerPerson = null, то выводить ошибку и поле selectDealerPerson обводить красной рамкой,
    иначе все ок
2023-01-16 15:38:28 +03:00
vchikalkin
5a6db6c837 Добавить валидацию на кнопку Рассчитать:
если 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
2023-01-16 14:55:40 +03:00
vchikalkin
66f60bbb91 add for prev commit 2023-01-11 19:50:06 +03:00
vchikalkin
670978cb0b на изменение списка в поле selectDealerRewardCondition :
Если  selectDealerRewardCondition пусто, то DealerRewardSumm обнуляется и закрывается для редактирования
Если в списке selectDealerRewardCondition есть запись, у которой evo_reward_condition.evo_agency_agreementid. Выплата без других агентов (evo_reward_without_other_agent) = True, то сбрасывают значение и закрываются для выбора поля: тест15.11 - указываю Авилон - сбрасывает другие АВ, но при этом я после этого могу их выбрать заново

selectDealerBroker
selectIndAgent
selectCalcDoubleAgent
selectCalcBroker
selectCalcFinDepartment
иначе

selectDealerBroker открываем для редактирования
selectIndAgent заполняется значением из Интереса
selectCalcDoubleAgent заполняется значением из Интереса
selectCalcBroker заполняется значением из Интереса
selectCalcFinDepartment заполняется значением из Интереса
2023-01-11 19:37:53 +03:00
vchikalkin
4f23fc964c поправили сброс списка rewardCondition при загрузке списка 2023-01-11 16:17:26 +03:00
vchikalkin
0a413f4eb7 реакция на изменение selectDealerBroker:
если selectDealerBroker пустое, то поле selectDealerBrokerRewardCondition сбрасывает значение, список и закрывается для редактирования
иначе:
обнуляем и закрываем поля selectDealerRewardCondition (каскадом должно обнулиться и закрыться tbxDealerRewardSumm) тест08.11 - не закрывается, а если указать Условие ЮЛ поставщика, а потом его убрать, то поле Брокер поставщика остается без списка см. Салон ООО Арктик-Сити
2023-01-11 15:59:16 +03:00
vchikalkin
5e977ec649 на выбор ЮЛ поставщика selectDealerPerson
если dealerPerson.evo_broker_accountid содержит данные, то в поле selectDealerBroker формируем список из контрагента из поля evo_broker_accountid и его же тут указываем, иначе selectDealerBroker пустое тест08.11 - 1) если обнуляю вручную dealerPerson, то selectDealerBroker не сбратывается, а вот если меняю dealer и обновляется dealerPerson, то selectDealerBroker перезаписывается или сбрасывается как надо
2023-01-11 15:41:13 +03:00
vchikalkin
0d3aecbcd4 на выбор Салона приобретения selectDealer - На изменение Салон приобретения selectDealer формируем список в поле ЮЛ поставщика selectDealerPerson-
если в поле selectDealer указан account, у которого evo_return_leasing_dealer = true, тест08.11 - не отработал на evo_return_leasing_dealer = true, заполнил как и на false

то  поле selectDealerPerson обнулять и закрывать для редактирования, иначе формировать список связанных значений - записи Контрагент, у которых статус = активный И Поставщик = Да И Тип поставщика = Юридическое лицо И связаны с карточкой Контрагент из поля "Салон приобретения" по связи Салон-ЮЛ (salon_providers)
2023-01-11 15:25:08 +03:00
486 changed files with 17587 additions and 50896 deletions

3
.dockerignore Normal file
View File

@ -0,0 +1,3 @@
.git
README.md
node_modules

11
.env Normal file
View File

@ -0,0 +1,11 @@
####### COMMON #######
USE_DEV_COLORS=
BASE_PATH=
####### USERS ########
USERS_SUPER=["akalinina","vchikalkin"]
####### URLS ########
URL_GET_USER_DIRECT=
URL_CRM_GRAPHQL_DIRECT=
URL_CORE_FINGAP_DIRECT=

View File

@ -8,5 +8,3 @@ coverage
.eslintrc.js .eslintrc.js
**/*.config.js **/*.config.js
**/scripts **/scripts
**/package.json
turbo.json

View File

@ -1,11 +1,10 @@
module.exports = { module.exports = {
root: true, root: true,
// This tells ESLint to load the config from the package `eslint-config-custom`
extends: ['custom'],
settings: { settings: {
next: { next: {
rootDir: ['apps/*/'], rootDir: ['apps/*/', 'packages/*/'],
},
react: {
version: 'detect',
}, },
}, },
}; };

54
.gitignore vendored
View File

@ -1,30 +1,5 @@
# Created by https://www.toptal.com/developers/gitignore/api/visualstudiocode # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Edit at https://www.toptal.com/developers/gitignore?templates=visualstudiocode
### VisualStudioCode ###
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
### VisualStudioCode Patch ###
# Ignore all local history of files
.history
.ionide
# End of https://www.toptal.com/developers/gitignore/api/visualstudiocode
# Created by https://www.toptal.com/developers/gitignore/api/nextjs
# Edit at https://www.toptal.com/developers/gitignore?templates=nextjs
### NextJS ###
# dependencies # dependencies
node_modules node_modules
.pnp .pnp
@ -36,10 +11,11 @@ coverage
# next.js # next.js
.next/ .next/
out/ out/
# production
build build
# other
dist/
# misc # misc
.DS_Store .DS_Store
*.pem *.pem
@ -51,22 +27,10 @@ yarn-error.log*
.pnpm-debug.log* .pnpm-debug.log*
# local env files # local env files
.env*.local .env.local
.env.development.local
.env.test.local
.env.production.local
# vercel # turbo
.vercel
# typescript
*.tsbuildinfo
# End of https://www.toptal.com/developers/gitignore/api/nextjs
# Created by https://www.toptal.com/developers/gitignore/api/turbo
# Edit at https://www.toptal.com/developers/gitignore?templates=turbo
### Turbo ###
# Turborepo task cache
.turbo .turbo
# End of https://www.toptal.com/developers/gitignore/api/turbo
.pnpm

View File

@ -1,4 +1,4 @@
#!/usr/bin/env sh #!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh" . "$(dirname -- "$0")/_/husky.sh"
npx lint-staged --concurrent false yarn precommit

View File

@ -1,5 +1,4 @@
.next .next
public public
**/graphql/*.types.ts graphql
**/graphql/*.schema.graphql
node_modules node_modules

10
.vscode/launch.json vendored
View File

@ -9,15 +9,9 @@
}, },
{ {
"name": "Next.js: debug client-side", "name": "Next.js: debug client-side",
"type": "chrome", "type": "pwa-chrome",
"request": "launch", "request": "launch",
"url": "http://localhost:3000", "url": "http://localhost:3000"
"webRoot": "${workspaceFolder}/apps/web",
"sourceMaps": true,
"sourceMapPathOverrides": {
"webpack://_N_E/*": "${webRoot}/*"
},
"skipFiles": ["**/<node_internals>/**", "**/node_modules/**"]
}, },
{ {
"name": "Next.js: debug full stack", "name": "Next.js: debug full stack",

17
.vscode/settings.json vendored
View File

@ -13,19 +13,10 @@
"editor.defaultFormatter": "esbenp.prettier-vscode", "editor.defaultFormatter": "esbenp.prettier-vscode",
"editor.formatOnSave": true, "editor.formatOnSave": true,
"editor.codeActionsOnSave": { "editor.codeActionsOnSave": {
"source.fixAll": "explicit", "source.organizeImports": true,
"source.fixAll.eslint": "explicit", "source.fixAll.eslint": true,
"source.removeUnusedImports": "explicit" "source.fixAll.format": true
}, },
"workbench.editor.labelFormat": "short", "workbench.editor.labelFormat": "short",
"eslint.workingDirectories": [{ "directory": "apps/web", "changeProcessCWD": true }], "eslint.workingDirectories": [{ "directory": "apps/web", "changeProcessCWD": true }]
"eslint.validate": [
"javascript",
"javascriptreact",
"json",
"typescript",
"typescriptreact",
"yaml"
],
"eslint.lintTask.enable": true
} }

104
README.md
View File

@ -1,28 +1,59 @@
# Turborepo starter # Turborepo Docker starter
This is an official starter Turborepo. This is an official Docker starter Turborepo.
## What's inside?
This turborepo uses [Yarn](https://classic.yarnpkg.com/lang/en/) as a package manager. It includes the following packages/apps:
### Apps and Packages
- `web`: a [Next.js](https://nextjs.org/) app
- `api`: an [Express](https://expressjs.com/) server
- `ui`: ui: a React component library
- `eslint-config-custom`: `eslint` configurations for client side applications (includes `eslint-config-next` and `eslint-config-prettier`)
- `eslint-config-custom-server`: `eslint` configurations for server side applications (includes `eslint-config-next` and `eslint-config-prettier`)
- `scripts`: Jest configurations
- `logger`: Isomorphic logger (a small wrapper around console.log)
- `tsconfig`: tsconfig.json;s used throughout the monorepo
Each package/app is 100% [TypeScript](https://www.typescriptlang.org/).
## Using this example ## Using this example
Run the following command: Run the following command:
```sh ```sh
npx create-turbo@latest npx degit vercel/turbo/examples/with-docker with-docker
cd with-docker
yarn install
git init . && git add . && git commit -m "Init"
``` ```
## What's inside? ### Docker
This Turborepo includes the following packages/apps: This repo is configured to be built with Docker, and Docker compose. To build all apps in this repo:
### Apps and Packages ```
# Create a network, which allows containers to communicate
# with each other, by using their container name as a hostname
docker network create app_network
- `docs`: a [Next.js](https://nextjs.org/) app # Build prod using new BuildKit engine
- `web`: another [Next.js](https://nextjs.org/) app COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker-compose -f docker-compose.yml build --parallel
- `@repo/ui`: a stub React component library shared by both `web` and `docs` applications
- `@repo/eslint-config`: `eslint` configurations (includes `eslint-config-next` and `eslint-config-prettier`)
- `@repo/tsconfig`: `tsconfig.json`s used throughout the monorepo
Each package/app is 100% [TypeScript](https://www.typescriptlang.org/). # Start prod in detached mode
docker-compose -f docker-compose.yml up -d
```
Open http://localhost:3000.
To shutdown all running containers:
```
# Stop all running containers
docker kill $(docker ps -q) && docker rm $(docker ps -a -q)
```
### Utilities ### Utilities
@ -30,52 +61,5 @@ This Turborepo has some additional tools already setup for you:
- [TypeScript](https://www.typescriptlang.org/) for static type checking - [TypeScript](https://www.typescriptlang.org/) for static type checking
- [ESLint](https://eslint.org/) for code linting - [ESLint](https://eslint.org/) for code linting
- [Jest](https://jestjs.io) test runner for all things JavaScript
- [Prettier](https://prettier.io) for code formatting - [Prettier](https://prettier.io) for code formatting
### Build
To build all apps and packages, run the following command:
```
cd my-turborepo
pnpm build
```
### Develop
To develop all apps and packages, run the following command:
```
cd my-turborepo
pnpm dev
```
### Remote Caching
Turborepo can use a technique known as [Remote Caching](https://turbo.build/repo/docs/core-concepts/remote-caching) to share cache artifacts across machines, enabling you to share build caches with your team and CI/CD pipelines.
By default, Turborepo will cache locally. To enable Remote Caching you will need an account with Vercel. If you don't have an account you can [create one](https://vercel.com/signup), then enter the following commands:
```
cd my-turborepo
npx turbo login
```
This will authenticate the Turborepo CLI with your [Vercel account](https://vercel.com/docs/concepts/personal-accounts/overview).
Next, you can link your Turborepo to your Remote Cache by running the following command from the root of your Turborepo:
```
npx turbo link
```
## Useful Links
Learn more about the power of Turborepo:
- [Tasks](https://turbo.build/repo/docs/core-concepts/monorepos/running-tasks)
- [Caching](https://turbo.build/repo/docs/core-concepts/caching)
- [Remote Caching](https://turbo.build/repo/docs/core-concepts/remote-caching)
- [Filtering](https://turbo.build/repo/docs/core-concepts/monorepos/filtering)
- [Configuration Options](https://turbo.build/repo/docs/reference/configuration)
- [CLI Usage](https://turbo.build/repo/docs/reference/command-line-reference)

View File

@ -1,7 +0,0 @@
.git
Dockerfile
.dockerignore
node_modules
*.log
dist
README.md

View File

@ -1,13 +0,0 @@
const { createConfig } = require('@vchikalkin/eslint-config-awesome');
module.exports = createConfig('typescript', {
parserOptions: {
project: './tsconfig.json',
tsconfigRootDir: __dirname,
},
ignorePatterns: ['*.config.js', '.eslintrc.js'],
rules: {
'import/no-duplicates': 'off',
'import/consistent-type-specifier-style': 'off',
},
});

56
apps/api/.gitignore vendored
View File

@ -1,56 +0,0 @@
# compiled output
/dist
/node_modules
/build
# Logs
logs
*.log
npm-debug.log*
pnpm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# OS
.DS_Store
# Tests
/coverage
/.nyc_output
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# temp directory
.temp
.tmp
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json

View File

@ -1,4 +0,0 @@
{
"singleQuote": true,
"trailingComma": "all"
}

View File

@ -1,45 +0,0 @@
# This Dockerfile is copy-pasted into our main docs at /docs/handbook/deploying-with-docker.
# Make sure you update both files!
FROM node:alpine AS builder
RUN corepack enable && corepack prepare pnpm@8.9.0 --activate
ENV PNPM_HOME=/usr/local/bin
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
RUN apk update
# Set working directory
WORKDIR /app
RUN pnpm add -g turbo@1.12.4 dotenv-cli
COPY . .
RUN turbo prune --scope=api --docker
# Add lockfile and package.json's of isolated subworkspace
FROM node:alpine AS installer
RUN corepack enable && corepack prepare pnpm@latest --activate
ENV PNPM_HOME=/usr/local/bin
RUN apk add --no-cache libc6-compat
RUN apk update
WORKDIR /app
# First install dependencies (as they change less often)
COPY .gitignore .gitignore
COPY --from=builder /app/out/json/ .
COPY --from=builder /app/out/pnpm-lock.yaml ./pnpm-lock.yaml
COPY --from=builder /app/out/pnpm-workspace.yaml ./pnpm-workspace.yaml
RUN pnpm install
# Build the project and its dependencies
COPY --from=builder /app/out/full/ .
COPY turbo.json turbo.json
RUN pnpm dotenv -e .env turbo run build --filter=api...
FROM node:alpine AS runner
WORKDIR /app
# Don't run production as root
RUN addgroup --system --gid 1001 nestjs
RUN adduser --system --uid 1001 nestjs
USER nestjs
COPY --from=installer /app .
CMD node apps/api/dist/main.js

View File

@ -1,73 +0,0 @@
<p align="center">
<a href="http://nestjs.com/" target="blank"><img src="https://nestjs.com/img/logo-small.svg" width="200" alt="Nest Logo" /></a>
</p>
[circleci-image]: https://img.shields.io/circleci/build/github/nestjs/nest/master?token=abc123def456
[circleci-url]: https://circleci.com/gh/nestjs/nest
<p align="center">A progressive <a href="http://nodejs.org" target="_blank">Node.js</a> framework for building efficient and scalable server-side applications.</p>
<p align="center">
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/v/@nestjs/core.svg" alt="NPM Version" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/l/@nestjs/core.svg" alt="Package License" /></a>
<a href="https://www.npmjs.com/~nestjscore" target="_blank"><img src="https://img.shields.io/npm/dm/@nestjs/common.svg" alt="NPM Downloads" /></a>
<a href="https://circleci.com/gh/nestjs/nest" target="_blank"><img src="https://img.shields.io/circleci/build/github/nestjs/nest/master" alt="CircleCI" /></a>
<a href="https://coveralls.io/github/nestjs/nest?branch=master" target="_blank"><img src="https://coveralls.io/repos/github/nestjs/nest/badge.svg?branch=master#9" alt="Coverage" /></a>
<a href="https://discord.gg/G7Qnnhy" target="_blank"><img src="https://img.shields.io/badge/discord-online-brightgreen.svg" alt="Discord"/></a>
<a href="https://opencollective.com/nest#backer" target="_blank"><img src="https://opencollective.com/nest/backers/badge.svg" alt="Backers on Open Collective" /></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://opencollective.com/nest/sponsors/badge.svg" alt="Sponsors on Open Collective" /></a>
<a href="https://paypal.me/kamilmysliwiec" target="_blank"><img src="https://img.shields.io/badge/Donate-PayPal-ff3f59.svg"/></a>
<a href="https://opencollective.com/nest#sponsor" target="_blank"><img src="https://img.shields.io/badge/Support%20us-Open%20Collective-41B883.svg" alt="Support us"></a>
<a href="https://twitter.com/nestframework" target="_blank"><img src="https://img.shields.io/twitter/follow/nestframework.svg?style=social&label=Follow"></a>
</p>
<!--[![Backers on Open Collective](https://opencollective.com/nest/backers/badge.svg)](https://opencollective.com/nest#backer)
[![Sponsors on Open Collective](https://opencollective.com/nest/sponsors/badge.svg)](https://opencollective.com/nest#sponsor)-->
## Description
[Nest](https://github.com/nestjs/nest) framework TypeScript starter repository.
## Installation
```bash
$ pnpm install
```
## Running the app
```bash
# development
$ pnpm run start
# watch mode
$ pnpm run start:dev
# production mode
$ pnpm run start:prod
```
## Test
```bash
# unit tests
$ pnpm run test
# e2e tests
$ pnpm run test:e2e
# test coverage
$ pnpm run test:cov
```
## Support
Nest is an MIT-licensed open source project. It can grow thanks to the sponsors and support by the amazing backers. If you'd like to join them, please [read more here](https://docs.nestjs.com/support).
## Stay in touch
- Author - [Kamil Myśliwiec](https://kamilmysliwiec.com)
- Website - [https://nestjs.com](https://nestjs.com/)
- Twitter - [@nestframework](https://twitter.com/nestframework)
## License
Nest is [MIT licensed](LICENSE).

View File

@ -1,8 +0,0 @@
{
"$schema": "https://json.schemastore.org/nest-cli",
"collection": "@nestjs/schematics",
"sourceRoot": "src",
"compilerOptions": {
"deleteOutDir": true
}
}

View File

@ -1,74 +0,0 @@
{
"name": "api",
"version": "0.0.1",
"description": "",
"author": "",
"private": true,
"license": "UNLICENSED",
"scripts": {
"build": "nest build",
"format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
"start": "nest start",
"dev": "nest start --watch",
"start:debug": "nest start --debug --watch",
"start:prod": "node dist/main",
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
"test": "jest",
"test:watch": "jest --watch",
"test:cov": "jest --coverage",
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
"test:e2e": "jest --config ./test/jest-e2e.json"
},
"dependencies": {
"@nestjs/cache-manager": "^2.2.1",
"@nestjs/common": "^10.0.0",
"@nestjs/config": "^3.2.0",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-fastify": "^10.3.3",
"cache-manager": "^5.4.0",
"cache-manager-ioredis": "^2.1.0",
"ioredis": "^5.3.2",
"reflect-metadata": "^0.2.0",
"rxjs": "^7.8.1",
"zod": "^3.22.4"
},
"devDependencies": {
"@nestjs/cli": "^10.0.0",
"@nestjs/schematics": "^10.0.0",
"@nestjs/testing": "^10.0.0",
"@types/jest": "^29.5.2",
"@types/node": "^20.3.1",
"@types/supertest": "^6.0.0",
"@vchikalkin/eslint-config-awesome": "^1.1.6",
"eslint": "^8.52.0",
"fastify": "^4.26.1",
"jest": "^29.5.0",
"prettier": "^3.0.0",
"shared": "workspace:*",
"source-map-support": "^0.5.21",
"supertest": "^6.3.3",
"ts-jest": "29.1.1",
"ts-loader": "^9.4.3",
"ts-node": "^10.9.1",
"tsconfig-paths": "^4.2.0",
"typescript": "^5.3.3"
},
"jest": {
"moduleFileExtensions": [
"js",
"json",
"ts"
],
"rootDir": "src",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
},
"collectCoverageFrom": [
"**/*.(t|j)s"
],
"coverageDirectory": "../coverage",
"testEnvironment": "node"
}
}

View File

@ -1,16 +0,0 @@
import { ProxyModule } from './proxy/proxy.module';
import { Global, Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
@Global()
@Module({
imports: [
ConfigModule.forRoot({
isGlobal: true,
}),
ProxyModule,
],
providers: [],
})
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class AppModule {}

View File

@ -1,3 +0,0 @@
import { seconds } from 'src/utils/time';
export const DEFAULT_CACHE_TTL = seconds().fromMinutes(15);

View File

@ -1,3 +0,0 @@
import envSchema from './schema/env';
export const env = envSchema.parse(process.env);

View File

@ -1,21 +0,0 @@
import { DEFAULT_CACHE_TTL } from '../constants';
import { z } from 'zod';
const envSchema = z.object({
CACHE_TTL: z
.string()
.transform((val) => Number.parseInt(val, 10))
.default(DEFAULT_CACHE_TTL.toString()),
PORT: z
.string()
.transform((val) => Number.parseInt(val, 10))
.default('3001'),
REDIS_HOST: z.string(),
REDIS_PORT: z
.string()
.transform((val) => Number.parseInt(val, 10))
.default('6379'),
URL_CRM_GRAPHQL_DIRECT: z.string(),
});
export default envSchema;

View File

@ -1,15 +0,0 @@
import { AppModule } from './app.module';
import { env } from './config/env';
import { NestFactory } from '@nestjs/core';
import type { NestFastifyApplication } from '@nestjs/platform-fastify';
import { FastifyAdapter } from '@nestjs/platform-fastify';
async function bootstrap() {
const app = await NestFactory.create<NestFastifyApplication>(
AppModule,
new FastifyAdapter(),
);
await app.listen(env.PORT, '0.0.0.0');
}
bootstrap();

View File

@ -1,64 +0,0 @@
import { seconds } from 'src/utils/time';
export const queryTTL: Record<string, number | false> = {
GetAddProductType: seconds().fromHours(12),
GetAddproductTypes: seconds().fromHours(12),
GetAgent: seconds().fromHours(12),
GetBrand: seconds().fromHours(3),
GetBrands: seconds().fromHours(3),
GetCoefficients: seconds().fromHours(12),
GetConfiguration: seconds().fromHours(3),
GetConfigurations: seconds().fromMinutes(15),
GetCurrencyChanges: seconds().fromHours(1),
GetDealer: seconds().fromHours(1),
GetDealerPerson: seconds().fromHours(1),
GetDealerPersons: seconds().fromHours(1),
GetDealers: seconds().fromMinutes(15),
GetEltInsuranceRules: seconds().fromHours(12),
GetFuelCards: seconds().fromHours(12),
GetGPSBrands: seconds().fromHours(24),
GetGPSModels: seconds().fromHours(24),
GetImportProgram: seconds().fromHours(12),
GetInsNSIBTypes: seconds().fromHours(12),
GetInsuranceCompanies: seconds().fromHours(12),
GetInsuranceCompany: seconds().fromHours(12),
GetLead: false,
GetLeadUrl: seconds().fromHours(12),
GetLeads: false,
GetLeaseObjectType: seconds().fromHours(24),
GetLeaseObjectTypes: seconds().fromHours(24),
GetLeasingWithoutKaskoTypes: seconds().fromHours(12),
GetModel: seconds().fromHours(3),
GetModels: seconds().fromMinutes(15),
GetOpportunities: false,
GetOpportunity: false,
GetOpportunityUrl: seconds().fromHours(12),
GetOsagoAddproductTypes: seconds().fromHours(12),
GetProduct: seconds().fromHours(12),
GetProducts: seconds().fromHours(12),
GetQuote: false,
GetQuoteData: false,
GetQuoteUrl: seconds().fromHours(12),
GetQuotes: false,
GetRate: seconds().fromHours(12),
GetRates: seconds().fromHours(12),
GetRegion: seconds().fromHours(24),
GetRegions: seconds().fromHours(24),
GetRegistrationTypes: seconds().fromHours(12),
GetRewardCondition: seconds().fromHours(1),
GetRewardConditions: seconds().fromHours(1),
GetRoles: seconds().fromHours(12),
GetSotCoefficientType: seconds().fromHours(12),
GetSubsidies: seconds().fromHours(12),
GetSubsidy: seconds().fromHours(12),
GetSystemUser: seconds().fromHours(12),
GetTarif: seconds().fromHours(12),
GetTarifs: seconds().fromHours(12),
GetTechnicalCards: seconds().fromHours(12),
GetTelematicTypes: seconds().fromHours(12),
GetTown: seconds().fromHours(24),
GetTowns: seconds().fromHours(24),
GetTrackerTypes: seconds().fromHours(12),
GetTransactionCurrencies: seconds().fromHours(12),
GetTransactionCurrency: seconds().fromHours(12),
};

View File

@ -1,127 +0,0 @@
import { queryTTL } from './lib/config';
import type { GQLRequest, GQLResponse } from './types';
import { CACHE_MANAGER } from '@nestjs/cache-manager';
import {
All,
Controller,
Delete,
Get,
HttpException,
HttpStatus,
Inject,
Query,
Req,
Res,
} from '@nestjs/common';
import type { Cache } from 'cache-manager';
import { FastifyReply, FastifyRequest } from 'fastify';
import type { QueryItem } from 'shared/types/cache';
import { env } from 'src/config/env';
type RedisStore = Omit<Cache, 'set'> & {
set: (key: string, value: unknown, { ttl }: { ttl: number }) => Promise<void>;
};
@Controller('proxy')
export class ProxyController {
constructor(
@Inject(CACHE_MANAGER) private readonly cacheManager: RedisStore,
) {}
@All('/graphql')
public async graphql(@Req() req: FastifyRequest, @Res() reply: FastifyReply) {
const { operationName, query, variables } = req.body as GQLRequest;
const key = `${operationName} ${JSON.stringify(variables)}`;
const cached = await this.cacheManager.get(key);
if (cached) return reply.send(cached);
const response = await fetch(env.URL_CRM_GRAPHQL_DIRECT, {
body: JSON.stringify({ operationName, query, variables }),
headers: {
Authorization: req.headers.authorization,
'Content-Type': 'application/json',
Cookie: req.headers.cookie,
},
method: req.method,
});
const data = (await response.json()) as GQLResponse;
if (!response.ok || data?.error || data?.errors?.length)
throw new HttpException(
response.statusText,
response.status || HttpStatus.INTERNAL_SERVER_ERROR,
);
const ttl = queryTTL[operationName];
if (data && ttl !== false)
await this.cacheManager.set(key, data, { ttl: ttl || env.CACHE_TTL });
return reply.send(data);
}
@Get('/get-queries')
public async getQueriesList(@Res() reply: FastifyReply) {
const res = await this.getAllQueries();
return reply.send(res);
}
private async getAllQueries() {
const list = await this.cacheManager.store.keys('*');
return (Object.keys(queryTTL) as Array<keyof typeof queryTTL>).reduce(
(acc, queryName) => {
const queries = list.filter((x) => x.split(' ').at(0) === queryName);
if (queries.length) {
const ttl = queryTTL[queryName];
acc[queryName] = { queries, ttl };
}
return acc;
},
{} as Record<string, QueryItem>,
);
}
@Delete('/delete-query')
public async deleteQuery(
@Query('queryKey') queryKey: string,
@Res() reply: FastifyReply,
) {
try {
await this.cacheManager.del(queryKey);
return reply.send('ok');
} catch (error) {
throw new HttpException(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Delete('/reset')
public async reset(@Res() reply: FastifyReply) {
try {
await this.cacheManager.reset();
return reply.send('ok');
} catch (error) {
throw new HttpException(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Get('/get-query')
public async getQueryValue(
@Query('queryKey') queryKey: string,
@Res() reply: FastifyReply,
) {
try {
const value = await this.cacheManager.get(queryKey);
return reply.send(value);
} catch (error) {
throw new HttpException(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}

View File

@ -1,20 +0,0 @@
import { ProxyController } from './proxy.controller';
import { CacheModule } from '@nestjs/cache-manager';
import { Module } from '@nestjs/common';
import * as redisStore from 'cache-manager-ioredis';
import type { RedisOptions } from 'ioredis';
import { env } from 'src/config/env';
@Module({
controllers: [ProxyController],
imports: [
CacheModule.register<RedisOptions>({
host: env.REDIS_HOST,
port: env.REDIS_PORT,
store: redisStore,
ttl: env.CACHE_TTL,
}),
],
})
// eslint-disable-next-line @typescript-eslint/no-extraneous-class
export class ProxyModule {}

View File

@ -1,11 +0,0 @@
export type GQLRequest = {
operationName: string;
query: string;
variables: string;
};
export type GQLResponse = {
data: unknown;
error?: unknown;
errors?: unknown[];
};

View File

@ -1,13 +0,0 @@
export function seconds() {
return {
fromDays(days: number) {
return days * 24 * 60 * 60;
},
fromHours(hours: number) {
return hours * 60 * 60;
},
fromMinutes(minutes: number) {
return minutes * 60;
},
};
}

View File

@ -1,4 +0,0 @@
{
"extends": "./tsconfig.json",
"exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
}

View File

@ -1,22 +0,0 @@
{
"compilerOptions": {
"module": "commonjs",
"declaration": true,
"removeComments": true,
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"allowSyntheticDefaultImports": true,
"target": "ES2021",
"sourceMap": true,
"outDir": "./dist",
"baseUrl": "./",
"incremental": true,
"skipLibCheck": true,
"strictNullChecks": false,
"noImplicitAny": false,
"strictBindCallApply": false,
"forceConsistentCasingInFileNames": false,
"noFallthroughCasesInSwitch": false
},
"exclude": ["node_modules"]
}

View File

@ -1,8 +0,0 @@
.git
Dockerfile
.dockerignore
node_modules
*.log
dist
.next
README.md

View File

@ -1,8 +1,5 @@
.next .next
public public
apollo.config.js *.config.js
mocks mocks
graphql/crm.schema.graphql graphql
graphql/crm.types.ts
package.json
next-env.d.ts

View File

@ -1,14 +1,8 @@
const { createConfig } = require('@vchikalkin/eslint-config-awesome'); module.exports = {
root: true,
module.exports = createConfig('next-typescript', { extends: ['custom'],
parserOptions: { parserOptions: {
project: './tsconfig.json', project: './tsconfig.json',
tsconfigRootDir: __dirname, tsconfigRootDir: __dirname,
}, },
rules: { };
'import/no-duplicates': 'off',
'react/forbid-component-props': 'off',
'import/consistent-type-specifier-style': 'off',
},
ignorePatterns: ['*.config.js', '.eslintrc.js'],
});

4
apps/web/.gitignore vendored
View File

@ -1,4 +0,0 @@
# Sentry Config File
.sentryclirc
/styles/antd.min.css

View File

@ -1,12 +1,11 @@
overwrite: true overwrite: true
schema: './graphql/crm.schema.graphql' schema: './graphql/crm.schema.graphql'
documents: [./**/!(*.schema).graphql] documents: '**/*.{graphql,js,ts,jsx,tsx}'
generates: generates:
./graphql/crm.types.ts: ./graphql/crm.types.ts:
plugins: plugins:
- typescript - typescript
- typescript-operations - typescript-operations
- typed-document-node
config: config:
onlyOperationTypes: true onlyOperationTypes: true
useTypeImports: true useTypeImports: true
@ -16,7 +15,7 @@ generates:
object: true object: true
defaultValue: true defaultValue: true
scalars: scalars:
UUID: string Uuid: string
Decimal: number Decimal: number
DateTime: string DateTime: string
# exclude: './graphql/crm.schema.graphql' # exclude: './graphql/crm.schema.graphql'

View File

@ -1,19 +0,0 @@
import elementsToValues from '@/Components/Calculation/config/map/values';
export const ERR_ELT_KASKO = 'ERR_ELT_KASKO';
export const ERR_ELT_OSAGO = 'ERR_ELT_OSAGO';
export const ERR_FINGAP_TABLE = 'ERR_FINGAP_TABLE';
export const ERR_INSURANCE_TABLE = 'ERR_INSURANCE_TABLE';
export const ERR_PAYMENTS_TABLE = 'ERR_PAYMENTS_TABLE';
export const ERROR_TABLE_KEYS = [
ERR_ELT_KASKO,
ERR_ELT_OSAGO,
ERR_FINGAP_TABLE,
ERR_INSURANCE_TABLE,
ERR_PAYMENTS_TABLE,
];
export const ERROR_ELEMENTS_KEYS = Object.keys(elementsToValues);
export const ERROR_KEYS = [...ERROR_ELEMENTS_KEYS, ...ERROR_TABLE_KEYS];

View File

@ -1,77 +0,0 @@
import * as cacheApi from '@/api/cache/query';
import { min } from '@/styles/mq';
import { useQuery } from '@tanstack/react-query';
import { memo, useState } from 'react';
import styled from 'styled-components';
import { Button, Collapse } from 'ui/elements';
import { Flex } from 'ui/grid';
type QueryProps = {
readonly onDeleteQuery: () => Promise<void>;
readonly queryKey: string;
};
const StyledPre = styled.pre`
max-height: 300px;
overflow-y: auto;
${min('desktop')} {
max-height: 800px;
}
`;
export const Query = memo(({ onDeleteQuery, queryKey }: QueryProps) => {
const { data, refetch } = useQuery({
enabled: false,
queryFn: ({ signal }) => signal && cacheApi.getQueryValue(queryKey, { signal }),
queryKey: ['admin', 'cache', 'query', queryKey],
refetchOnWindowFocus: false,
});
const [activeKey, setActiveKey] = useState<string | undefined>(undefined);
const [deletePending, setDeletePending] = useState(false);
const content = (
<>
<StyledPre>{JSON.stringify(data, null, 2)}</StyledPre>
<Flex justifyContent="flex-end">
<Button
type="primary"
danger
disabled={deletePending}
onClick={() => {
setDeletePending(true);
onDeleteQuery().finally(() => {
setDeletePending(false);
});
}}
>
Удалить
</Button>
</Flex>
</>
);
return (
<Collapse
bordered={false}
activeKey={activeKey}
items={[
{
children: data ? content : 'Загрузка...',
key: queryKey,
label: queryKey,
},
]}
onChange={() => {
if (activeKey) {
setActiveKey(undefined);
} else {
setActiveKey(queryKey);
refetch();
}
}}
/>
);
});

View File

@ -1,22 +0,0 @@
import { Query } from './Query';
import * as cacheApi from '@/api/cache/query';
import { useState } from 'react';
import type { QueryItem } from 'shared/types/cache';
type QueryListProps = QueryItem;
export const QueryList = ({ queries }: QueryListProps) => {
const [deletedQueries, setDeletedQueries] = useState<QueryItem['queries']>([]);
function handleDeleteQuery(queryKey: string) {
return cacheApi
.deleteQuery(queryKey)
.then(() => setDeletedQueries([...deletedQueries, queryKey]));
}
const activeQueries = queries.filter((queryKey) => !deletedQueries.includes(queryKey));
return activeQueries.map((queryKey) => (
<Query key={queryKey} queryKey={queryKey} onDeleteQuery={() => handleDeleteQuery(queryKey)} />
));
};

View File

@ -1,24 +0,0 @@
import { useState } from 'react';
import { Button } from 'ui/elements';
import { ReloadOutlined } from 'ui/elements/icons';
export function ReloadButton({ onClick }: { readonly onClick: () => Promise<unknown> }) {
const [pending, setPending] = useState(false);
return (
<Button
loading={pending}
onClick={() => {
setPending(true);
onClick().finally(() => {
setTimeout(() => {
setPending(false);
}, 1000);
});
}}
icon={<ReloadOutlined rev="" />}
>
Обновить
</Button>
);
}

View File

@ -1,70 +0,0 @@
import Background from '../../Layout/Background';
import { useFilteredQueries } from './lib/hooks';
import { QueryList } from './QueryList';
import { reset } from '@/api/cache/query';
import { min } from '@/styles/mq';
import styled from 'styled-components';
import { Button, Collapse, Divider, Input } from 'ui/elements';
const Wrapper = styled(Background)`
padding: 4px 6px;
width: 100vw;
${min('tablet')} {
min-height: 790px;
}
${min('laptop')} {
padding: 4px 18px 10px;
width: 1280px;
}
`;
const Flex = styled.div`
display: flex;
flex-direction: column;
gap: 10px;
`;
const ButtonWrapper = styled.div`
display: flex;
justify-content: flex-end;
`;
export function Cache() {
const { filteredQueries, refetch, setFilterString } = useFilteredQueries();
function handleDeleteQuery() {
return reset().then(() => refetch());
}
if (!filteredQueries) {
return <div>Загрузка...</div>;
}
return (
<Wrapper>
<Divider>Управление кэшем</Divider>
<Flex>
<Input
placeholder="Поиск по запросу"
allowClear
onChange={(e) => setFilterString(e.target.value)}
/>
<Collapse
accordion
items={Object.keys(filteredQueries).map((queryGroupName) => ({
children: <QueryList {...filteredQueries[queryGroupName]} />,
key: queryGroupName,
label: queryGroupName,
}))}
/>
<ButtonWrapper>
<Button type="primary" danger disabled={false} onClick={() => handleDeleteQuery()}>
Очистить кэш
</Button>
</ButtonWrapper>
</Flex>
</Wrapper>
);
}

View File

@ -1,30 +0,0 @@
import { filterQueries } from './utils';
import * as cacheApi from '@/api/cache/query';
import type { ResponseQueries } from '@/api/cache/types';
import { useQuery } from '@tanstack/react-query';
import { useEffect, useState } from 'react';
import { useDebounce } from 'use-debounce';
export function useFilteredQueries() {
const { data: queries, refetch } = useQuery({
enabled: false,
queryFn: ({ signal }) => signal && cacheApi.getQueries({ signal }),
queryKey: ['admin', 'cache', 'queries'],
refetchOnWindowFocus: false,
});
const [filteredQueries, setFilteredQueries] = useState<ResponseQueries | undefined>(queries);
const [filterString, setFilterString] = useState('');
const [debouncedFilterString] = useDebounce(filterString, 350);
useEffect(() => {
if (!debouncedFilterString) {
setFilteredQueries(queries);
}
if (queries && debouncedFilterString) {
setFilteredQueries(filterQueries(queries, debouncedFilterString));
}
}, [debouncedFilterString, queries]);
return { filteredQueries, queries, refetch, setFilterString };
}

View File

@ -1,23 +0,0 @@
import type { ResponseQueries } from '@/api/cache/types';
export function filterQueries(queriesObj: ResponseQueries, searchStr: string): ResponseQueries {
const filteredObj: ResponseQueries = {};
for (const key in queriesObj) {
if (key.includes(searchStr)) {
filteredObj[key] = queriesObj[key];
} else {
const queries: string[] = [];
queriesObj[key].queries.forEach((queryKey) => {
if (queryKey.toLowerCase().includes(searchStr.toLowerCase())) {
queries.push(queryKey);
}
});
if (queries.length) {
filteredObj[key] = { ...queriesObj[key], queries };
}
}
}
return filteredObj;
}

View File

@ -1,17 +0,0 @@
import { min } from '@/styles/mq';
import type { PropsWithChildren } from 'react';
import styled from 'styled-components';
const Flex = styled.div`
display: flex;
flex-direction: column;
${min('laptop')} {
flex-direction: row;
justify-content: center;
}
`;
export function Layout({ children }: PropsWithChildren) {
return <Flex>{children}</Flex>;
}

View File

@ -1,2 +0,0 @@
export * from './Cache';
export * from './Layout';

View File

@ -1,3 +1,4 @@
/* eslint-disable object-curly-newline */
import type { FormTabRows } from '../../lib/render-rows'; import type { FormTabRows } from '../../lib/render-rows';
export const id = 'add-product'; export const id = 'add-product';
@ -7,7 +8,7 @@ export const rows: FormTabRows = [
{ {
title: 'Регистрация', title: 'Регистрация',
}, },
[['radioObjectRegistration', 'radioTypePTS'], { gridTemplateColumns: ['1fr', '1fr 1fr'] }], [['radioObjectRegistration', 'radioTypePTS'], { gridTemplateColumns: ['1fr 1fr'] }],
[['selectRegionRegistration', 'selectTownRegistration', 'selectObjectRegionRegistration']], [['selectRegionRegistration', 'selectTownRegistration', 'selectObjectRegionRegistration']],
[['selectObjectCategoryTax', 'selectObjectTypeTax', 'tbxVehicleTaxInYear']], [['selectObjectCategoryTax', 'selectObjectTypeTax', 'tbxVehicleTaxInYear']],
[['tbxLeaseObjectYear', 'tbxLeaseObjectMotorPower', 'tbxVehicleTaxInLeasingPeriod']], [['tbxLeaseObjectYear', 'tbxLeaseObjectMotorPower', 'tbxVehicleTaxInLeasingPeriod']],
@ -26,6 +27,6 @@ export const rows: FormTabRows = [
{ {
title: 'Доп. продукты', title: 'Доп. продукты',
}, },
[['selectTechnicalCard', 'selectInsNSIB'], { gridTemplateColumns: ['1fr', '1fr 2fr'] }], [['selectTechnicalCard', 'selectInsNSIB'], { gridTemplateColumns: '1fr 2fr' }],
[['selectRequirementTelematic', 'selectTracker', 'selectTelematic']], [['selectRequirementTelematic', 'selectTracker', 'selectTelematic']],
]; ];

View File

@ -6,7 +6,7 @@ function Insurance() {
} }
export default { export default {
Component: Insurance,
id, id,
title, title,
Component: Insurance,
}; };

View File

@ -1,17 +1,13 @@
/* eslint-disable object-curly-newline */
import type { FormTabRows } from '../../lib/render-rows'; import type { FormTabRows } from '../../lib/render-rows';
export const id = 'create-kp'; export const id = 'create-kp';
export const title = 'Создание КП'; export const title = 'Создание КП';
export const rows: FormTabRows = [ export const rows: FormTabRows = [
[ [['cbxPriceWithDiscount', 'cbxFullPriceWithDiscount', 'cbxCostIncrease']],
['cbxPriceWithDiscount', 'cbxFullPriceWithDiscount'], [['cbxInsurance', 'cbxRegistrationQuote', 'cbxTechnicalCardQuote']],
{ gridTemplateColumns: ['1fr', '1fr 1fr'] }, [['cbxNSIB', 'cbxQuoteRedemptionGraph', 'cbxShowFinGAP']],
], [['tbxQuoteName', 'radioQuoteContactGender'], { gridTemplateColumns: '1fr 1fr' }],
[['cbxQuotePriceWithFullVAT', 'cbxCostIncrease'], { gridTemplateColumns: ['1fr', '1fr 1fr'] }], [['btnCreateKP', 'linkDownloadKp'], { gridTemplateColumns: '1fr 1fr' }],
[['cbxInsurance', 'cbxRegistrationQuote'], { gridTemplateColumns: ['1fr', '1fr 1fr'] }],
[['cbxTechnicalCardQuote', 'cbxNSIB'], { gridTemplateColumns: ['1fr', '1fr 1fr'] }],
[['cbxQuoteRedemptionGraph', 'cbxShowFinGAP'], { gridTemplateColumns: ['1fr', '1fr 1fr'] }],
[['cbxQuoteShowAcceptLimit'], { gridTemplateColumns: ['1fr', '1fr 1fr'] }],
[['tbxQuoteName', 'radioQuoteContactGender'], { gridTemplateColumns: ['1fr', '2fr 1fr'] }],
]; ];

View File

@ -6,7 +6,7 @@ function CreateKP() {
} }
export default { export default {
Component: CreateKP,
id, id,
title, title,
Component: CreateKP,
}; };

View File

@ -1,60 +0,0 @@
import type { columns } from '../lib/config';
import type { Row, StoreSelector } from '../types';
import { message } from '@/Components/Common/Notification';
import { useStore } from '@/stores/hooks';
import { observer } from 'mobx-react-lite';
import { Table } from 'ui/elements';
export const PolicyTable = observer(
({
onSelectRow,
storeSelector,
...props
}: {
columns: typeof columns;
onSelectRow: (row: Row) => void;
storeSelector: StoreSelector;
}) => {
const { $process, $tables } = useStore();
const { getRows, getSelectedRow, setSelectedKey } = storeSelector($tables.elt);
return (
<Table
size="small"
pagination={false}
dataSource={getRows}
scroll={{
x: true,
}}
rowSelection={{
getCheckboxProps: (record) => ({
disabled:
!record.sum || record.status !== null || getRows.some((x) => x.status === 'fetching'),
}),
hideSelectAll: true,
onSelect: (record) => {
if (record.sum > 0) {
$process.add('ELT');
setSelectedKey(record.key);
onSelectRow(record);
message.success({
content: 'Выбранный расчет ЭЛТ применен',
duration: 1,
key: record.key,
onClick: () => message.destroy(record.key),
});
$process.delete('ELT');
}
},
selectedRowKeys: getSelectedRow ? [getSelectedRow.key] : [],
type: 'radio',
}}
expandable={{
expandedRowRender: (record) => record.message,
rowExpandable: (record) => Boolean(record.message),
}}
{...props}
/>
);
}
);

View File

@ -1,32 +0,0 @@
import type { StoreSelector } from '../types';
import { useStore } from '@/stores/hooks';
import { observer } from 'mobx-react-lite';
import { Button } from 'ui/elements';
import { ReloadOutlined } from 'ui/elements/icons';
import { Flex } from 'ui/grid';
export const ReloadButton = observer(
({ storeSelector, onClick }: { onClick: () => void; storeSelector: StoreSelector }) => {
const { $tables, $process } = useStore();
const { validation, getRows: rows } = storeSelector($tables.elt);
const hasErrors = validation.hasErrors;
return (
<Flex justifyContent="center">
<Button
onClick={onClick}
disabled={
hasErrors ||
$process.has('LoadKP') ||
$process.has('Calculate') ||
$process.has('CreateKP')
}
loading={rows.some((x) => x.status === 'fetching')}
shape="circle"
icon={<ReloadOutlined rev="" />}
/>
</Flex>
);
}
);

View File

@ -1,23 +0,0 @@
import type { StoreSelector } from '../types';
import { useStore } from '@/stores/hooks';
import { observer } from 'mobx-react-lite';
import { Alert } from 'ui/elements';
export const Validation = observer(({ storeSelector }: { storeSelector: StoreSelector }) => {
const { $tables, $process } = useStore();
const { validation } = storeSelector($tables.elt);
const errors = validation.getErrors();
if (errors?.length) {
return (
<Alert
type={$process.has('Unlimited') ? 'warning' : 'error'}
banner
message={errors[0].message}
/>
);
}
return null;
});

View File

@ -1,3 +0,0 @@
export * from './PolicyTable';
export * from './ReloadButton';
export * from './Validation';

View File

@ -1,75 +0,0 @@
/* eslint-disable sonarjs/cognitive-complexity */
import { PolicyTable, ReloadButton, Validation } from './Components';
import { columns } from './lib/config';
import { resetRow } from './lib/tools';
import type { Row, StoreSelector } from './types';
import { useStore } from '@/stores/hooks';
import { trpcClient } from '@/trpc/client';
import { observer } from 'mobx-react-lite';
import { Flex } from 'ui/grid';
const storeSelector: StoreSelector = ({ kasko }) => kasko;
export const Kasko = observer(() => {
const store = useStore();
const { $calculation, $tables } = store;
const calculateKasko = trpcClient.eltKasko.useMutation({
onError() {
$tables.elt.kasko.setRows(
$tables.elt.kasko.getRows.map((row) => ({ ...row, status: 'error' }))
);
},
onMutate: () => {
const rows = $tables.elt.kasko.getRows;
$tables.elt.kasko.setRows(rows.map((row) => ({ ...resetRow(row), status: 'fetching' })));
},
onSuccess: ({ rows }) => {
$tables.elt.kasko.setRows(rows);
},
});
function handleOnClick() {
calculateKasko.mutate({
calculation: {
values: store.$calculation.$values.getValues(),
},
});
}
function handleOnSelectRow(row: Row) {
$tables.insurance.row('kasko').column('insuranceCompany').setValue(row.key);
$tables.insurance.row('kasko').column('insCost').setValue(row.sum);
$calculation.element('tbxInsFranchise').setValue(row.totalFranchise);
}
type Column = (typeof columns)[number];
const kaskoColumns = columns.map((column: Column) => {
if (column.key === 'name') {
return {
...column,
title: 'Страховая компания КАСКО',
};
}
if (column.key === 'status') {
return {
...column,
title: <ReloadButton storeSelector={storeSelector} onClick={() => handleOnClick()} />,
};
}
return column;
});
return (
<Flex flexDirection="column">
<Validation storeSelector={storeSelector} />
<PolicyTable
storeSelector={storeSelector}
columns={kaskoColumns}
onSelectRow={(row) => handleOnSelectRow(row)}
/>
</Flex>
);
});

View File

@ -1,80 +0,0 @@
/* eslint-disable no-negated-condition */
/* eslint-disable sonarjs/cognitive-complexity */
import { PolicyTable, ReloadButton, Validation } from './Components';
import { columns } from './lib/config';
import { resetRow } from './lib/tools';
import type { Row, StoreSelector } from './types';
import { useStore } from '@/stores/hooks';
import { trpcClient } from '@/trpc/client';
import { observer } from 'mobx-react-lite';
import { Flex } from 'ui/grid';
const storeSelector: StoreSelector = ({ osago }) => osago;
export const Osago = observer(() => {
const store = useStore();
const { $tables } = store;
const calculateOsago = trpcClient.eltOsago.useMutation({
onError() {
$tables.elt.osago.setRows(
$tables.elt.osago.getRows.map((row) => ({ ...row, status: 'error' }))
);
},
onMutate: () => {
const rows = $tables.elt.osago.getRows;
$tables.elt.osago.setRows(rows.map((row) => ({ ...resetRow(row), status: 'fetching' })));
},
onSuccess: ({ rows }) => {
$tables.elt.osago.setRows(rows);
},
});
async function handleOnClick() {
calculateOsago.mutate({
calculation: {
values: store.$calculation.$values.getValues(),
},
});
}
function handleOnSelectRow(row: Row) {
$tables.insurance.row('osago').column('insuranceCompany').setValue(row.key);
$tables.insurance.row('osago').column('insCost').setValue(row.sum);
}
type Column = (typeof columns)[number];
const osagoColumns = columns.map((column: Column) => {
if (column.key === 'name') {
return {
...column,
title: 'Страховая компания ОСАГО',
};
}
if (column.key === 'status') {
return {
...column,
title: <ReloadButton storeSelector={storeSelector} onClick={() => handleOnClick()} />,
};
}
if (column.key === 'totalFranchise') {
return {
...column,
render: () => 'Не требуется',
};
}
return column;
});
return (
<Flex flexDirection="column">
<Validation storeSelector={storeSelector} />
<PolicyTable
storeSelector={storeSelector}
columns={osagoColumns}
onSelectRow={(row) => handleOnSelectRow(row)}
/>
</Flex>
);
});

View File

@ -1,20 +0,0 @@
import { Kasko } from './Kasko';
import { Osago } from './Osago';
const id = 'elt';
const title = 'ЭЛТ';
function ELT() {
return (
<>
<Osago />
<Kasko />
</>
);
}
export default {
Component: ELT,
id,
title,
};

View File

@ -1,56 +0,0 @@
import type { Row } from '../types';
import type { ColumnsType } from 'antd/lib/table';
import { CloseOutlined, LoadingOutlined } from 'ui/elements/icons';
import { Flex } from 'ui/grid';
const formatter = Intl.NumberFormat('ru', {
currency: 'RUB',
style: 'currency',
}).format;
export const columns: ColumnsType<Row> = [
{
dataIndex: 'name',
key: 'name',
title: 'Страховая компания',
width: '50%',
},
{
dataIndex: 'sum',
key: 'sum',
render: formatter,
sortDirections: ['descend', 'ascend'],
sorter: (a, b) => a.sum - b.sum,
title: 'Сумма',
width: '20%',
},
{
dataIndex: 'totalFranchise',
key: 'totalFranchise',
render: formatter,
title: 'Франшиза',
width: '20%',
},
{
dataIndex: 'status',
key: 'status',
render: (_, record) => {
if (record.status === 'fetching')
return (
<Flex justifyContent="center">
<LoadingOutlined spin rev="" />
</Flex>
);
if (record.status === 'error')
return (
<Flex justifyContent="center">
<CloseOutlined rev="" />
</Flex>
);
return false;
},
title: undefined,
width: '5%',
},
];

View File

@ -1,12 +0,0 @@
import type { Row } from '@/Components/Calculation/Form/ELT/types';
import { defaultRow } from '@/stores/tables/elt/default-values';
export function resetRow(row: Row): Row {
return {
...defaultRow,
id: row.id,
key: row.key,
metodCalc: row.metodCalc,
name: row.name,
};
}

View File

@ -1,7 +0,0 @@
import type { RowSchema } from '@/config/schema/elt';
import type ELTStore from '@/stores/tables/elt';
import type PolicyStore from '@/stores/tables/elt/policy';
import type { z } from 'zod';
export type Row = z.infer<typeof RowSchema>;
export type StoreSelector = (eltStore: ELTStore) => PolicyStore;

View File

@ -1,6 +1,6 @@
/* eslint-disable canonical/sort-keys */ /* eslint-disable import/prefer-default-export */
import type { Risk } from './types';
import type { ColumnsType } from 'antd/lib/table'; import type { ColumnsType } from 'antd/lib/table';
import type { Risk } from './types';
export const columns: ColumnsType<Risk> = [ export const columns: ColumnsType<Risk> = [
{ {

View File

@ -1,48 +1,41 @@
import { columns } from './config';
import { useStore } from '@/stores/hooks';
import { toJS } from 'mobx'; import { toJS } from 'mobx';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useStore } from 'stores/hooks';
import styled from 'styled-components'; import styled from 'styled-components';
import { Alert, Table } from 'ui/elements'; import { Flex } from 'ui';
import { Flex } from 'ui/grid'; import Alert from 'ui/elements/Alert';
import Table from 'ui/elements/Table';
import { columns } from './config';
const Grid = styled(Flex)` const Grid = styled(Flex)`
flex-direction: column; flex-direction: column;
`; `;
const Validation = observer(() => { const Validation = observer(() => {
const { $tables, $process } = useStore(); const store = useStore();
const errors = $tables.fingap.validation.getErrors(); const messages = store.$tables.fingap.validation.getMessages();
if (errors?.length) { if (messages?.length) {
return ( return <Alert type="error" banner message={messages[0]} />;
<Alert
type={$process.has('Unlimited') ? 'warning' : 'error'}
banner
message={errors[0].message}
/>
);
} }
return null; return null;
}); });
const FinGAP = observer(() => { const FinGAPTable = observer(() => {
const { $tables, $process } = useStore(); const { $tables } = useStore();
const { fingap } = $tables; const { fingap } = $tables;
const dataSource = toJS(fingap.risks); const dataSource = toJS(fingap.risks);
const selectedRowKeys = [...toJS(fingap.selectedKeys)]; const selectedRowKeys = [...toJS(fingap.selectedKeys)];
const disabled = $process.has('LoadKP') || $process.has('Calculate') || $process.has('CreateKP');
return ( return (
<Table <Table
columns={columns} columns={columns}
dataSource={dataSource} dataSource={dataSource}
rowSelection={{ rowSelection={{
getCheckboxProps: () => ({ disabled }), type: 'checkbox',
onChange: (_, selectedRows) => { onChange: (_, selectedRows) => {
const selectedKeys = selectedRows.reduce((acc, row) => { const selectedKeys = selectedRows.reduce((acc, row) => {
acc.push(row.key); acc.push(row.key);
@ -54,7 +47,6 @@ const FinGAP = observer(() => {
fingap.setSelectedKeys(selectedKeys); fingap.setSelectedKeys(selectedKeys);
}, },
selectedRowKeys, selectedRowKeys,
type: 'checkbox',
}} }}
pagination={false} pagination={false}
size="small" size="small"
@ -65,11 +57,11 @@ const FinGAP = observer(() => {
); );
}); });
export default function FinGAPTable() { export default function () {
return ( return (
<Grid> <Grid>
<Validation /> <Validation />
<FinGAP /> <FinGAPTable />
</Grid> </Grid>
); );
} }

View File

@ -1,4 +1,4 @@
import type { RiskSchema } from '@/config/schema/fingap'; import type { RiskSchema } from 'config/schema/fingap';
import type { z } from 'zod'; import type { z } from 'zod';
export type Risk = z.infer<typeof RiskSchema>; export type Risk = z.infer<typeof RiskSchema>;

View File

@ -1,8 +1,8 @@
import { useInsuranceValue } from './hooks';
import type { Values } from './types';
import { useRow } from '@/stores/tables/insurance/hooks';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import type { ComponentType } from 'react'; import type { ComponentType } from 'react';
import { useRow } from 'stores/tables/insurance/hooks';
import { useInsuranceValue } from './hooks';
import type { Values } from './types';
export function buildOptionComponent<T>( export function buildOptionComponent<T>(
key: string, key: string,
@ -13,17 +13,10 @@ export function buildOptionComponent<T>(
const [value, setValue] = useInsuranceValue(key, valueName); const [value, setValue] = useInsuranceValue(key, valueName);
const { getOptions, getStatus } = useRow(key); const { getOptions, getStatus } = useRow(key);
const options = getOptions(valueName); const options = getOptions(valueName);
const status = getStatus(valueName); const statuses = getStatus(valueName);
return ( return (
<Component <Component value={value} options={options} setValue={setValue} status={statuses} {...props} />
options={options}
onChange={setValue}
disabled={status === 'Disabled'}
loading={status === 'Loading'}
value={value}
{...props}
/>
); );
}); });
} }
@ -36,10 +29,8 @@ export function buildValueComponent<T>(
return observer((props: T) => { return observer((props: T) => {
const [value, setValue] = useInsuranceValue(key, valueName); const [value, setValue] = useInsuranceValue(key, valueName);
const { getStatus } = useRow(key); const { getStatus } = useRow(key);
const status = getStatus(valueName); const statuses = getStatus(valueName);
return ( return <Component value={value} setValue={setValue} status={statuses} {...props} />;
<Component onChange={setValue} disabled={status === 'Disabled'} value={value} {...props} />
);
}); });
} }

View File

@ -1,60 +1,27 @@
/* eslint-disable react/forbid-component-props */ /* eslint-disable import/prefer-default-export */
/* eslint-disable canonical/sort-keys */ import type { ColumnsType } from 'antd/lib/table';
import { MAX_INSURANCE } from 'constants/values';
import { formatter, parser } from 'tools/number';
import InputNumber from 'ui/elements/InputNumber';
import Select from 'ui/elements/Select';
import { buildOptionComponent, buildValueComponent } from './builders'; import { buildOptionComponent, buildValueComponent } from './builders';
import type * as Insurance from './types'; import type * as Insurance from './types';
import { MAX_INSURANCE } from '@/constants/values';
import { useStore } from '@/stores/hooks';
import type { ColumnsType } from 'antd/lib/table';
import { observer } from 'mobx-react-lite';
import { parser } from 'tools/number';
import { InputNumber, Select } from 'ui/elements';
import { CheckOutlined } from 'ui/elements/icons';
import { createFormatter } from 'ui/elements/InputNumber';
export const columns: ColumnsType<Insurance.RowValues> = [ export const columns: ColumnsType<Insurance.RowValues> = [
{
key: 'elt',
dataIndex: 'elt',
render: (_, record) => {
const Check = observer(() => {
const { $tables } = useStore();
if (
(record.key === 'osago' && $tables.elt.osago.getSelectedRow?.key) ||
(record.key === 'kasko' && $tables.elt.kasko.getSelectedRow?.key)
) {
return <CheckOutlined rev="" />;
}
return null;
});
return <Check />;
},
title: 'ЭЛТ',
width: '1%',
},
{ {
key: 'policyType', key: 'policyType',
dataIndex: 'policyType', dataIndex: 'policyType',
title: 'Тип полиса', title: 'Тип полиса',
width: '11%',
}, },
{ {
key: 'insuranceCompany', key: 'insuranceCompany',
dataIndex: 'insuranceCompany', dataIndex: 'insuranceCompany',
title: 'Страховая компания', title: 'Страховая компания',
width: 300,
render: (_, record) => { render: (_, record) => {
const Component = buildOptionComponent(record.key, Select, 'insuranceCompany'); const Component = buildOptionComponent(record.key, Select, 'insuranceCompany');
return ( return <Component showSearch optionFilterProp="label" />;
<Component
optionFilterProp="label"
showSearch
style={{
width: '100%',
}}
/>
);
}, },
}, },
{ {
@ -64,40 +31,28 @@ export const columns: ColumnsType<Insurance.RowValues> = [
render: (_, record) => { render: (_, record) => {
const Component = buildOptionComponent(record.key, Select, 'insured'); const Component = buildOptionComponent(record.key, Select, 'insured');
return ( return <Component />;
<Component
style={{
width: '100%',
maxWidth: '100px',
}}
/>
);
}, },
width: '100px',
}, },
{ {
key: 'insCost', key: 'insCost',
dataIndex: 'insCost', dataIndex: 'insCost',
title: 'Сумма за 1-й период', title: 'Стоимость за 1-й период',
render: (_, record) => { render: (_, record) => {
const Component = buildValueComponent(record.key, InputNumber, 'insCost'); const Component = buildValueComponent(record.key, InputNumber, 'insCost');
return ( return (
<Component <Component
addonAfter="₽"
formatter={createFormatter({ minimumFractionDigits: 2, maximumFractionDigits: 2 })}
max={MAX_INSURANCE}
min={0} min={0}
parser={parser} max={MAX_INSURANCE}
precision={2}
step={1000} step={1000}
style={{ precision={2}
width: '150px', parser={parser}
}} formatter={formatter}
addonAfter="₽"
/> />
); );
}, },
width: '150px',
}, },
{ {
key: 'insTerm', key: 'insTerm',
@ -106,15 +61,7 @@ export const columns: ColumnsType<Insurance.RowValues> = [
render: (_, record) => { render: (_, record) => {
const Component = buildOptionComponent(record.key, Select, 'insTerm'); const Component = buildOptionComponent(record.key, Select, 'insTerm');
return ( return <Component />;
<Component
style={{
width: '150px',
maxWidth: '150px',
}}
/>
);
}, },
width: '150px',
}, },
]; ];

View File

@ -1,5 +1,6 @@
import { useRow } from '@/stores/tables/insurance/hooks'; /* eslint-disable import/prefer-default-export */
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRow } from 'stores/tables/insurance/hooks';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
export function useInsuranceValue(key, valueName) { export function useInsuranceValue(key, valueName) {
@ -7,19 +8,24 @@ export function useInsuranceValue(key, valueName) {
const storeValue = row.getValue(valueName); const storeValue = row.getValue(valueName);
function setStoreValue(val) { function setStoreValue(value) {
return row.setValue(valueName, val); return row.setValue(valueName, value);
} }
const [value, setValue] = useState(storeValue); const [value, setValue] = useState(storeValue);
// eslint-disable-next-line object-curly-newline
const debouncedSetStoreValue = useDebouncedCallback(setStoreValue, 350, { maxWait: 1000 }); const debouncedSetStoreValue = useDebouncedCallback(setStoreValue, 350, { maxWait: 1000 });
useEffect(() => { useEffect(
() => {
if (storeValue !== value) { if (storeValue !== value) {
debouncedSetStoreValue(value); debouncedSetStoreValue(value);
} }
}, [value]); },
// eslint-disable-next-line react-hooks/exhaustive-deps
[value]
);
useEffect(() => { useEffect(() => {
setValue(storeValue); setValue(storeValue);

View File

@ -1,9 +1,10 @@
import { columns } from './config';
import { useStore } from '@/stores/hooks';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useStore } from 'stores/hooks';
import styled from 'styled-components'; import styled from 'styled-components';
import { Alert, Table } from 'ui/elements'; import { Flex } from 'ui';
import { Flex } from 'ui/grid'; import Alert from 'ui/elements/Alert';
import Table from 'ui/elements/Table';
import { columns } from './config';
const Grid = styled(Flex)` const Grid = styled(Flex)`
flex-direction: column; flex-direction: column;
@ -16,24 +17,18 @@ const TableWrapper = styled.div`
`; `;
const Validation = observer(() => { const Validation = observer(() => {
const { $tables, $process } = useStore(); const store = useStore();
const errors = $tables.insurance.validation.getErrors(); const messages = store.$tables.insurance.validation.getMessages();
if (errors?.length) { if (messages?.length) {
return ( return <Alert type="error" banner message={messages[0]} />;
<Alert
type={$process.has('Unlimited') ? 'warning' : 'error'}
banner
message={errors[0].message}
/>
);
} }
return null; return null;
}); });
const Insurance = observer(() => { const InsuranceTable = observer(() => {
const store = useStore(); const store = useStore();
const { values } = store.$tables.insurance; const { values } = store.$tables.insurance;
@ -53,11 +48,11 @@ const Insurance = observer(() => {
); );
}); });
export default function InsuranceTable() { export default function () {
return ( return (
<Grid> <Grid>
<Validation /> <Validation />
<Insurance /> <InsuranceTable />
</Grid> </Grid>
); );
} }

View File

@ -1,6 +1,5 @@
import type { KeysSchema, RowSchema } from '@/config/schema/insurance'; import type { KeysSchema, RowSchema } from 'config/schema/insurance';
import type { Status } from '@/stores/calculation/statuses/types'; import type { BaseOption, Status } from 'ui/elements/types';
import type { BaseOption } from 'ui/elements/types';
import type { z } from 'zod'; import type { z } from 'zod';
export type Keys = z.infer<typeof KeysSchema>; export type Keys = z.infer<typeof KeysSchema>;
@ -10,7 +9,7 @@ export type RowValues = z.infer<typeof RowSchema>;
export type Values = Exclude<keyof RowValues, 'key'>; export type Values = Exclude<keyof RowValues, 'key'>;
export type RowOptions = { export type RowOptions = {
[ValueName in Values]: Array<BaseOption<RowValues[ValueName]>>; [ValueName in Values]?: BaseOption<RowValues[ValueName]>[];
}; };
export type RowStatuses = Record<Values, Status>; export type RowStatuses = Record<Values, Status>;

View File

@ -1,14 +1,12 @@
import type { FormTabRows } from '../../lib/render-rows'; import type { FormTabRows } from '../../lib/render-rows';
import { transformRowsForMobile } from '../lib/utils';
export const id = 'insurance'; export const id = 'insurance';
export const title = 'Страхование'; export const title = 'Страхование';
export const rows: FormTabRows = [ export const rows: FormTabRows = [
[['tbxLeaseObjectYear', 'selectLeaseObjectUseFor', 'selectLegalClientRegion']], [['tbxLeaseObjectYear', 'selectLeaseObjectUseFor', 'selectLegalClientRegion']],
[['tbxMileage', 'tbxInsFranchise', 'selectLegalClientTown']], [['selectEngineType', 'tbxInsFranchise', 'selectLegalClientTown']],
[['selectGPSBrand', 'cbxWithTrailer', 'selectInsNSIB']], [['selectLeaseObjectCategory', 'tbxMileage', 'tbxINNForCalc']],
[['selectGPSModel', 'cbxInsDecentral', 'selectLeasingWithoutKasko']], [['tbxLeaseObjectMotorPower', 'cbxWithTrailer', 'selectGPSBrand']],
[['tbxEngineVolume', 'cbxInsDecentral', 'selectGPSModel']],
]; ];
export const mobileRows = transformRowsForMobile(rows);

View File

@ -1,15 +1,15 @@
import { Flex } from 'ui';
import renderFormRows from '../../lib/render-rows'; import renderFormRows from '../../lib/render-rows';
import { id, mobileRows, rows, title } from './config'; import { id, rows, title } from './config';
import FinGAPTable from './FinGAPTable'; import FinGAPTable from './FinGAPTable';
import InsuranceTable from './InsuranceTable'; import InsuranceTable from './InsuranceTable';
import { Media } from '@/styles/media';
import { Flex } from 'ui/grid';
function Insurance() { function Insurance() {
const renderedRows = renderFormRows(rows);
return ( return (
<Flex flexDirection="column"> <Flex flexDirection="column">
<Media lessThan="laptop">{renderFormRows(mobileRows)}</Media> {renderedRows}
<Media greaterThanOrEqual="laptop">{renderFormRows(rows)}</Media>
<InsuranceTable /> <InsuranceTable />
<FinGAPTable /> <FinGAPTable />
</Flex> </Flex>
@ -17,7 +17,7 @@ function Insurance() {
} }
export default { export default {
Component: Insurance,
id, id,
title, title,
Component: Insurance,
}; };

View File

@ -1,3 +1,4 @@
/* eslint-disable object-curly-newline */
import type { FormTabRows } from '../../lib/render-rows'; import type { FormTabRows } from '../../lib/render-rows';
export const id = 'leasing'; export const id = 'leasing';
@ -8,8 +9,8 @@ export const rows: FormTabRows = [
[['tbxLeaseObjectPrice', 'tbxVATInLeaseObjectPrice', 'tbxLeaseObjectPriceWthtVAT']], [['tbxLeaseObjectPrice', 'tbxVATInLeaseObjectPrice', 'tbxLeaseObjectPriceWthtVAT']],
[['selectSupplierCurrency', 'tbxSupplierDiscountRub', 'tbxSupplierDiscountPerc']], [['selectSupplierCurrency', 'tbxSupplierDiscountRub', 'tbxSupplierDiscountPerc']],
[['tbxFirstPaymentPerc', 'tbxFirstPaymentRub']], [['tbxFirstPaymentPerc', 'tbxFirstPaymentRub']],
[['tbxLeasingPeriod', 'tbxSaleBonus']], [['tbxLeasingPeriod', 'tbxSaleBonus', 'tbxRedemptionPaymentSum']],
[['selectSubsidy', 'tbxSubsidySum'], { gridTemplateColumns: ['1fr', '2fr 1fr'] }], [['selectSubsidy', 'tbxSubsidySum'], { gridTemplateColumns: '2fr 1fr' }],
[['selectImportProgram', 'tbxImportProgramSum', 'tbxAddEquipmentPrice']], [['selectImportProgram', 'tbxImportProgramSum', 'tbxAddEquipmentPrice']],
[['radioLastPaymentRule'], { gridTemplateColumns: '1fr' }], [['radioLastPaymentRule'], { gridTemplateColumns: '1fr' }],
[['tbxLastPaymentPerc', 'tbxLastPaymentRub']], [['tbxLastPaymentPerc', 'tbxLastPaymentRub']],

View File

@ -6,7 +6,7 @@ function Leasing() {
} }
export default { export default {
Component: Leasing,
id, id,
title, title,
Component: Leasing,
}; };

View File

@ -1,5 +1,5 @@
/* eslint-disable object-curly-newline */
import type { FormTabRows } from '../../lib/render-rows'; import type { FormTabRows } from '../../lib/render-rows';
import { transformRowsForMobile } from '../lib/utils';
export const id = 'leasing-object'; export const id = 'leasing-object';
export const title = 'ПЛ'; export const title = 'ПЛ';
@ -15,7 +15,5 @@ export const rows: FormTabRows = [
[['tbxLeaseObjectCount', 'selectEngineType', 'tbxMaxSpeed']], [['tbxLeaseObjectCount', 'selectEngineType', 'tbxMaxSpeed']],
[['tbxLeaseObjectYear', 'tbxLeaseObjectMotorPower', 'tbxCountSeats']], [['tbxLeaseObjectYear', 'tbxLeaseObjectMotorPower', 'tbxCountSeats']],
[['selectLeaseObjectCategory', 'tbxEngineVolume', 'tbxMileage']], [['selectLeaseObjectCategory', 'tbxEngineVolume', 'tbxMileage']],
[['tbxMaxMass', 'tbxEngineHours', 'tbxVIN']], [['tbxMaxMass', 'tbxEngineHours']],
]; ];
export const mobileRows = transformRowsForMobile(rows);

View File

@ -1,18 +1,12 @@
import renderFormRows from '../../lib/render-rows'; import renderFormRows from '../../lib/render-rows';
import { id, mobileRows, rows, title } from './config'; import { id, rows, title } from './config';
import { Media } from '@/styles/media';
function LeasingObject() { function LeasingObject() {
return ( return renderFormRows(rows);
<>
<Media lessThan="laptop">{renderFormRows(mobileRows)}</Media>
<Media greaterThanOrEqual="laptop">{renderFormRows(rows)}</Media>
</>
);
} }
export default { export default {
Component: LeasingObject,
id, id,
title, title,
Component: LeasingObject,
}; };

View File

@ -1,8 +1,8 @@
import { observer } from 'mobx-react-lite';
import { useStore } from 'stores/hooks';
import { Flex } from 'ui';
import elementsRender from '../../config/elements-render'; import elementsRender from '../../config/elements-render';
import { elements } from './config'; import { elements } from './config';
import { useStore } from '@/stores/hooks';
import { observer } from 'mobx-react-lite';
import { Flex } from 'ui/grid';
function PaymentsParams() { function PaymentsParams() {
const renderedElements = elements.map((elementName) => { const renderedElements = elements.map((elementName) => {

View File

@ -1,26 +1,14 @@
import { usePaymentSum, usePaymentValue } from './hooks'; /* eslint-disable import/prefer-default-export */
import { useRowStatus } from '@/stores/tables/payments/hooks';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import type { ComponentType } from 'react'; import type { ComponentType } from 'react';
import { useRowStatus } from 'stores/tables/payments/hooks';
import { usePaymentValue } from './hooks';
export function buildValueComponent<T>(index: number, Component: ComponentType<T>) { export function buildValueComponent<T>(index: number, Component: ComponentType<T>) {
return observer((props: T) => { return observer((props: T) => {
const [value, setValue] = usePaymentValue(index); const [value, setValue] = usePaymentValue(index);
const status = useRowStatus(index); const status = useRowStatus(index);
return ( return <Component value={value} setValue={setValue} status={status} {...props} />;
<Component onChange={setValue} disabled={status === 'Disabled'} value={value} {...props} />
);
});
}
export function buildSumComponent<T>(index: number, Component: ComponentType<T>) {
return observer((props: T) => {
const [value, setValue] = usePaymentSum(index);
const status = useRowStatus(index);
return (
<Component onChange={setValue} disabled={status === 'Disabled'} value={value} {...props} />
);
}); });
} }

View File

@ -1,23 +1,15 @@
/* eslint-disable react/forbid-component-props */ /* eslint-disable import/prefer-default-export */
/* eslint-disable canonical/sort-keys */
import { buildSumComponent, buildValueComponent } from './builders';
import type { ColumnsType } from 'antd/lib/table'; import type { ColumnsType } from 'antd/lib/table';
import { parser } from 'tools/number'; import InputNumber from 'ui/elements/InputNumber';
import { InputNumber } from 'ui/elements';
import { createFormatter } from 'ui/elements/InputNumber'; import { buildValueComponent } from './builders';
type Payment = { type Payment = {
key: number; key: number;
num: number; num: number;
paymentRelation: number; paymentRelation: number;
paymentSum: number;
}; };
const formatter = createFormatter({
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
export const columns: ColumnsType<Payment> = [ export const columns: ColumnsType<Payment> = [
{ {
key: 'num', key: 'num',
@ -35,34 +27,10 @@ export const columns: ColumnsType<Payment> = [
return ( return (
<Component <Component
min={payment.num === 0 ? 0 : 0.01}
max={100} max={100}
min={0}
precision={payment.num === 0 ? 4 : 2}
step={1} step={1}
style={{ precision={payment.num === 0 ? 4 : 2}
width: '100%',
}}
/>
);
},
},
{
key: 'paymentSum',
dataIndex: 'paymentSum',
title: 'Сумма',
render: (_value, payment) => {
const Component = buildSumComponent(payment.num, InputNumber);
return (
<Component
min={0}
precision={2}
step={1000}
formatter={formatter}
parser={parser}
style={{
width: '100%',
}}
/> />
); );
}, },

View File

@ -1,37 +1,24 @@
import { useRowSum, useRowValue } from '@/stores/tables/payments/hooks'; /* eslint-disable import/prefer-default-export */
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
import { useRowValue } from 'stores/tables/payments/hooks';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
export function usePaymentValue(index) { export function usePaymentValue(index) {
const [storeValue, setStoreValue] = useRowValue(index); const [storeValue, setStoreValue] = useRowValue(index);
const [value, setValue] = useState(storeValue); const [value, setValue] = useState(storeValue);
// eslint-disable-next-line object-curly-newline
const debouncedSetStoreValue = useDebouncedCallback(setStoreValue, 350, { maxWait: 1000 }); const debouncedSetStoreValue = useDebouncedCallback(setStoreValue, 350, { maxWait: 1000 });
useEffect(() => { useEffect(
() => {
if (storeValue !== value) { if (storeValue !== value) {
debouncedSetStoreValue(value); debouncedSetStoreValue(value);
} }
}, [value]); },
// eslint-disable-next-line react-hooks/exhaustive-deps
useEffect(() => { [value]
setValue(storeValue); );
}, [storeValue]);
return [value, setValue];
}
export function usePaymentSum(index) {
const [storeValue, setStoreValue] = useRowSum(index);
const [value, setValue] = useState(storeValue);
const debouncedSetStoreValue = useDebouncedCallback(setStoreValue, 350, { maxWait: 1000 });
useEffect(() => {
if (storeValue !== value) {
debouncedSetStoreValue(value);
}
}, [value]);
useEffect(() => { useEffect(() => {
setValue(storeValue); setValue(storeValue);

View File

@ -1,13 +1,12 @@
import { columns } from './config';
import { useStore } from '@/stores/hooks';
import { min } from '@/styles/mq';
import { computed } from 'mobx'; import { computed } from 'mobx';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { createContext, useContext, useMemo, useState } from 'react'; import { useStore } from 'stores/hooks';
import styled from 'styled-components'; import styled from 'styled-components';
import { Alert, Segmented, Table } from 'ui/elements'; import { min } from 'styles/mq';
import Alert from 'ui/elements/Alert';
import Table from 'ui/elements/Table';
import { Box, Flex } from 'ui/grid'; import { Box, Flex } from 'ui/grid';
import { useDebouncedCallback } from 'use-debounce'; import { columns } from './config';
const Grid = styled(Flex)` const Grid = styled(Flex)`
flex-direction: column; flex-direction: column;
@ -31,26 +30,18 @@ const TablesGroupGrid = styled(Box)`
`; `;
const Validation = observer(() => { const Validation = observer(() => {
const { $tables, $process } = useStore(); const store = useStore();
const { payments } = $tables; const { payments } = store.$tables;
const errors = payments.validation.getErrors(); const messages = payments.validation.getMessages();
if (errors?.length) { if (messages?.length) {
return ( return <Alert type="error" banner message={messages[0]} />;
<Alert
type={$process.has('Unlimited') ? 'warning' : 'error'}
banner
message={errors[0].message}
/>
);
} }
return null; return null;
}); });
export const ModeContext = createContext('paymentRelation');
const SPLIT_NUMBER = 12; const SPLIT_NUMBER = 12;
function TablePart({ num }) { function TablePart({ num }) {
@ -59,25 +50,16 @@ function TablePart({ num }) {
const values = payments.values.slice(num * SPLIT_NUMBER, num * SPLIT_NUMBER + SPLIT_NUMBER); const values = payments.values.slice(num * SPLIT_NUMBER, num * SPLIT_NUMBER + SPLIT_NUMBER);
const dataSource = values.map((_, index) => ({ const dataSource = values.map((value, index) => ({
key: index + num * SPLIT_NUMBER, key: index + num * SPLIT_NUMBER,
num: index + num * SPLIT_NUMBER, num: index + num * SPLIT_NUMBER,
paymentRelation: value,
})); }));
const mode = useContext(ModeContext); return (
return useMemo(
() => (
<TableWrapper> <TableWrapper>
<Table <Table size="small" columns={columns} dataSource={dataSource} pagination={false} />
size="small"
columns={columns.filter((x) => ['num', mode].includes(x.key))}
dataSource={dataSource}
pagination={false}
/>
</TableWrapper> </TableWrapper>
),
[dataSource, mode]
); );
} }
@ -97,36 +79,11 @@ const TablesGroup = observer(() => {
return <TablesGroupGrid>{tables}</TablesGroupGrid>; return <TablesGroupGrid>{tables}</TablesGroupGrid>;
}); });
function Mode({ setMode }) {
const { $process } = useStore();
if (!$process.has('Unlimited')) {
return false;
}
return (
<Segmented
block
options={[
{ label: 'Процент', value: 'paymentRelation' },
{ label: 'Сумма', value: 'paymentSum' },
]}
onChange={(value) => setMode(value)}
/>
);
}
export default function TablePayments() { export default function TablePayments() {
const [mode, setMode] = useState('paymentRelation');
const debouncedSetMode = useDebouncedCallback(setMode, 300);
return ( return (
<Grid> <Grid>
<ModeContext.Provider value={mode}>
<Validation /> <Validation />
<Mode setMode={debouncedSetMode} />
<TablesGroup /> <TablesGroup />
</ModeContext.Provider>
</Grid> </Grid>
); );
} }

View File

@ -1,8 +1,8 @@
import { Box, Flex } from 'ui/grid';
import elementsRender from '../../config/elements-render'; import elementsRender from '../../config/elements-render';
import { id, title } from './config'; import { id, title } from './config';
import PaymentsParams from './PaymentsParams'; import PaymentsParams from './PaymentsParams';
import PaymentsTable from './PaymentsTable'; import PaymentsTable from './PaymentsTable';
import { Box, Flex } from 'ui/grid';
function Payments() { function Payments() {
const radioGraphType = elementsRender.radioGraphType.render(); const radioGraphType = elementsRender.radioGraphType.render();
@ -12,8 +12,8 @@ function Payments() {
<Box <Box
sx={{ sx={{
display: 'grid', display: 'grid',
gap: '10px',
gridTemplateColumns: ['1fr', '1fr', '1fr 1fr'], gridTemplateColumns: ['1fr', '1fr', '1fr 1fr'],
gap: '10px',
}} }}
> >
{radioGraphType} {radioGraphType}
@ -25,7 +25,7 @@ function Payments() {
} }
export default { export default {
Component: Payments,
id, id,
title, title,
Component: Payments,
}; };

View File

@ -1,5 +1,5 @@
/* eslint-disable object-curly-newline */
import type { FormTabRows } from '../../lib/render-rows'; import type { FormTabRows } from '../../lib/render-rows';
import { transformRowsForMobile } from '../lib/utils';
export const id = 'supplier-agent'; export const id = 'supplier-agent';
export const title = 'Поставщик/агент'; export const title = 'Поставщик/агент';
@ -21,5 +21,3 @@ export const rows: FormTabRows = [
[['selectCalcBrokerRewardCondition', 'selectFinDepartmentRewardCondtion'], defaultRowStyle], [['selectCalcBrokerRewardCondition', 'selectFinDepartmentRewardCondtion'], defaultRowStyle],
[['tbxCalcBrokerRewardSum', 'tbxFinDepartmentRewardSumm'], defaultRowStyle], [['tbxCalcBrokerRewardSum', 'tbxFinDepartmentRewardSumm'], defaultRowStyle],
]; ];
export const mobileRows = transformRowsForMobile(rows);

View File

@ -1,18 +1,12 @@
import renderFormRows from '../../lib/render-rows'; import renderFormRows from '../../lib/render-rows';
import { id, mobileRows, rows, title } from './config'; import { id, rows, title } from './config';
import { Media } from '@/styles/media';
function Leasing() { function Leasing() {
return ( return renderFormRows(rows);
<>
<Media lessThan="laptop">{renderFormRows(mobileRows)}</Media>
<Media greaterThanOrEqual="laptop">{renderFormRows(rows)}</Media>
</>
);
} }
export default { export default {
Component: Leasing,
id, id,
title, title,
Component: Leasing,
}; };

View File

@ -1,14 +0,0 @@
import type { FormTabRows } from '../../lib/render-rows';
export const id = 'unlimited';
export const title = 'Без ограничений';
export const rows: FormTabRows = [
[['cbxDisableChecks', 'selectUser']],
[['selectTarif', 'tbxCreditRate', 'selectRate']],
[['tbxMinPriceChange', 'tbxMaxPriceChange']],
[['tbxImporterRewardPerc', 'tbxImporterRewardRub']],
[['tbxBonusCoefficient', 'tbxComissionRub', 'tbxComissionPerc']],
[['cbxSupplierFinancing', 'cbxPartialVAT', 'cbxFloatingRate']],
[['cbxQuotePriceWithFullVAT']],
];

View File

@ -1,12 +0,0 @@
import renderFormRows from '../../lib/render-rows';
import { id, rows, title } from './config';
function Unlimited() {
return renderFormRows(rows);
}
export default {
Component: Unlimited,
id,
title,
};

View File

@ -1,30 +1,16 @@
import Background from 'Components/Layout/Background';
import styled from 'styled-components';
import { min } from 'styles/mq';
import Tabs from 'ui/elements/layout/Tabs';
import AddProduct from './AddProduct'; import AddProduct from './AddProduct';
import CreateKP from './CreateKP'; import CreateKP from './CreateKP';
import ELT from './ELT';
import Insurance from './Insurance'; import Insurance from './Insurance';
import Leasing from './Leasing'; import Leasing from './Leasing';
import LeasingObject from './LeasingObject'; import LeasingObject from './LeasingObject';
import Payments from './Payments'; import Payments from './Payments';
import SupplierAgent from './SupplierAgent'; import SupplierAgent from './SupplierAgent';
import Unlimited from './Unlimited';
import Background from '@/Components/Layout/Background';
import { useStore } from '@/stores/hooks';
import { min } from '@/styles/mq';
import { memo } from 'react';
import styled from 'styled-components';
import { Tabs } from 'ui/elements';
const formTabs = [ const formTabs = [Leasing, Payments, LeasingObject, SupplierAgent, Insurance, AddProduct, CreateKP];
Leasing,
Payments,
LeasingObject,
SupplierAgent,
Insurance,
ELT,
AddProduct,
CreateKP,
Unlimited,
];
const Wrapper = styled(Background)` const Wrapper = styled(Background)`
padding: 4px 6px; padding: 4px 6px;
@ -46,16 +32,11 @@ const ComponentWrapper = styled.div`
} }
`; `;
export const Form = memo(() => { function Form() {
const { $process } = useStore();
const filteredTabs =
$process.has('Unlimited') === false ? formTabs.filter((x) => x.id !== 'unlimited') : formTabs;
return ( return (
<Wrapper> <Wrapper>
<Tabs type="card" tabBarGutter="5px"> <Tabs type="card" tabBarGutter="5px">
{filteredTabs.map(({ Component, id, title }) => ( {formTabs.map(({ id, title, Component }) => (
<Tabs.TabPane tab={title} key={id}> <Tabs.TabPane tab={title} key={id}>
<ComponentWrapper> <ComponentWrapper>
<Component /> <Component />
@ -65,4 +46,6 @@ export const Form = memo(() => {
</Tabs> </Tabs>
</Wrapper> </Wrapper>
); );
}); }
export default Form;

View File

@ -1,32 +0,0 @@
/**
*
* @param {import('../../lib/render-rows').FormTabRows} rows
* @returns {import('../../lib/render-rows').FormTabRows}
*/
export function transformRowsForMobile(rows) {
const mobileRows = [];
let columnGroups = {};
rows.forEach((row) => {
if (Array.isArray(row)) {
row[0].forEach((item, index) => {
if (!columnGroups[index]) {
columnGroups[index] = [];
}
columnGroups[index].push(item);
});
} else {
Object.values(columnGroups).forEach((group) => {
mobileRows.push([group, { gridTemplateColumns: '1fr' }]);
});
columnGroups = {};
mobileRows.push(row);
}
});
Object.values(columnGroups).forEach((group) => {
mobileRows.push([group, { gridTemplateColumns: '1fr' }]);
});
return mobileRows;
}

View File

@ -1,20 +0,0 @@
import { min } from '@/styles/mq';
import styled from 'styled-components';
import { Box } from 'ui/grid';
export const Layout = styled(Box)`
display: flex;
flex-direction: column;
gap: 10px;
${min('laptop')} {
display: grid;
align-items: flex-start;
grid-template-columns: 2fr 1fr;
}
${min('desktop')} {
grid-template-columns: 2fr 1fr 1.5fr;
/* margin: 8px 5%; */
}
`;

View File

@ -1,84 +0,0 @@
/* eslint-disable canonical/sort-keys */
import type { ResultPayment } from '@/stores/results/types';
import type { ColumnsType } from 'antd/lib/table';
export const columns: ColumnsType<ResultPayment> = [
{
key: 'num',
dataIndex: 'num',
title: '#',
width: '10%',
},
{
key: 'paymentSum',
dataIndex: 'paymentSum',
title: 'Сумма платежа',
render: Intl.NumberFormat('ru', {
style: 'currency',
currency: 'RUB',
}).format,
},
{
key: 'ndsCompensation',
dataIndex: 'ndsCompensation',
title: 'НДС к возмещению',
render: Intl.NumberFormat('ru', {
style: 'currency',
currency: 'RUB',
}).format,
},
{
key: 'redemptionAmount',
dataIndex: 'redemptionAmount',
title: 'Сумма досрочного выкупа',
render: Intl.NumberFormat('ru', {
style: 'currency',
currency: 'RUB',
}).format,
},
{
key: '_piColumn',
dataIndex: '_piColumn',
title: 'PI Column',
render: Intl.NumberFormat('ru', {
style: 'currency',
currency: 'RUB',
}).format,
},
{
key: '_cashflowMsfoWithCfColumn',
dataIndex: '_cashflowMsfoWithCfColumn',
title: 'CashflowMSFOWithCF Column',
render: Intl.NumberFormat('ru', {
style: 'currency',
currency: 'RUB',
}).format,
},
{
key: '_creditPaymentColumn',
dataIndex: '_creditPaymentColumn',
title: 'CreditPayment Column',
render: Intl.NumberFormat('ru', {
style: 'currency',
currency: 'RUB',
}).format,
},
{
key: '_cashflowMsfoColumn',
dataIndex: '_cashflowMsfoColumn',
title: 'CashflowMSFO Column',
render: Intl.NumberFormat('ru', {
style: 'currency',
currency: 'RUB',
}).format,
},
{
key: '_interestColumn',
dataIndex: '_interestColumn',
title: 'Interest Column',
render: Intl.NumberFormat('ru', {
style: 'currency',
currency: 'RUB',
}).format,
},
];

View File

@ -1,41 +0,0 @@
/* eslint-disable no-negated-condition */
import { columns } from './config';
import { MAX_LEASING_PERIOD } from '@/constants/values';
import { useStore } from '@/stores/hooks';
import { toJS } from 'mobx';
import { observer } from 'mobx-react-lite';
import { Table } from 'ui/elements';
const PaymentsTable = observer(() => {
const { $process, $results } = useStore();
const unlimited = $process.has('Unlimited');
const dataSource = toJS($results.payments);
const dataColumns = !unlimited ? columns.filter((x) => !x.key.includes('_')) : columns;
return (
<Table
columns={dataColumns}
dataSource={dataSource}
size="small"
pagination={{
defaultPageSize: MAX_LEASING_PERIOD,
hideOnSinglePage: true,
responsive: true,
// showLessItems: true,
showSizeChanger: false,
}}
scroll={{
x: dataColumns.length > 5 ? 1000 : undefined,
y: dataSource.length > 16 ? 630 : undefined,
}}
/>
);
});
export default {
Component: PaymentsTable,
id: 'payments-table',
title: 'Таблица платежей',
};

View File

@ -1,102 +0,0 @@
import type { ResultValues } from '@/stores/results/types';
export const id = 'output';
export const title = 'Результаты';
export const titles: Record<keyof ResultValues, string> = {
_resultContractEconomy: 'Экономика',
_resultContractEconomyWithVAT: 'Экономика, с НДС',
_resultPi: 'PI',
_resultPiRepayment: 'PI для досрочки',
_resultSumCredit: 'Сумма кредита',
_resultSumCreditPayment: 'Сумма платежей по кредиту',
_resultVatRecoverable: 'НДС к возмещению',
resultAB_FL: 'АВ ФЛ, без НДФЛ.',
resultAB_UL: 'АВ ЮЛ, с НДС.',
resultBonusDopProd: 'Бонус МПЛ за доп.продукты, без НДФЛ',
resultBonusMPL: 'Бонус МПЛ за лизинг, без НДФЛ',
resultBonusSafeFinance: 'Бонус за Safe Finance без НДФЛ',
resultDopMPLLeasing: 'Доп.бонус МПЛ за лизинг, без НДФЛ',
resultDopProdSum: 'Общая сумма доп.продуктов',
resultFirstPayment: 'Первый платеж',
resultFirstPaymentRiskPolicy: 'Первый платеж по риск политике, %',
resultIRRGraphPerc: 'IRR по графику клиента, %',
resultIRRNominalPerc: 'IRR (номинал), %',
resultInsKasko: 'КАСКО, НС, ДГО в графике',
resultInsOsago: 'ОСАГО в графике',
resultLastPayment: 'Последний платеж',
resultParticipationAmount: 'Сумма участия (для принятия решения)',
resultPlPrice: 'Стоимость ПЛ с НДС',
resultPriceUpPr: 'Удорожание, год',
resultTerm: 'Срок, мес.',
resultTotalGraphwithNDS: 'Итого по графику, с НДС',
};
const moneyFormatter = Intl.NumberFormat('ru', {
currency: 'RUB',
style: 'currency',
}).format;
const percentFormatter = Intl.NumberFormat('ru', {
maximumFractionDigits: 2,
minimumFractionDigits: 2,
style: 'percent',
}).format;
export const formatters = {
_resultContractEconomy: moneyFormatter,
_resultContractEconomyWithVAT: moneyFormatter,
_resultPi: percentFormatter,
_resultPiRepayment: percentFormatter,
_resultSumCredit: moneyFormatter,
_resultSumCreditPayment: moneyFormatter,
_resultVatRecoverable: moneyFormatter,
resultAB_FL: moneyFormatter,
resultAB_UL: moneyFormatter,
resultBonusDopProd: moneyFormatter,
resultBonusMPL: moneyFormatter,
resultBonusSafeFinance: moneyFormatter,
resultDopMPLLeasing: moneyFormatter,
resultDopProdSum: moneyFormatter,
resultFirstPayment: moneyFormatter,
resultFirstPaymentRiskPolicy: percentFormatter,
resultIRRGraphPerc: percentFormatter,
resultIRRNominalPerc: percentFormatter,
resultInsKasko: moneyFormatter,
resultInsOsago: moneyFormatter,
resultLastPayment: moneyFormatter,
resultParticipationAmount: moneyFormatter,
resultPlPrice: moneyFormatter,
resultPriceUpPr: percentFormatter,
resultTerm: Intl.NumberFormat('ru').format,
resultTotalGraphwithNDS: moneyFormatter,
};
export const elements: Array<keyof ResultValues> = [
'_resultContractEconomy',
'_resultContractEconomyWithVAT',
'_resultPi',
'_resultPiRepayment',
'_resultSumCredit',
'_resultSumCreditPayment',
'_resultVatRecoverable',
'resultTotalGraphwithNDS',
'resultPlPrice',
'resultPriceUpPr',
'resultIRRGraphPerc',
'resultIRRNominalPerc',
'resultInsKasko',
'resultInsOsago',
'resultDopProdSum',
'resultFirstPayment',
'resultLastPayment',
'resultFirstPaymentRiskPolicy',
'resultTerm',
'resultAB_FL',
'resultAB_UL',
'resultBonusMPL',
'resultDopMPLLeasing',
'resultBonusDopProd',
'resultBonusSafeFinance',
'resultParticipationAmount',
];

View File

@ -1,55 +0,0 @@
import { elements, formatters, id, title, titles } from './config';
import { Container, Head } from '@/Components/Layout/Element';
import { useStore } from '@/stores/hooks';
import { min } from '@/styles/mq';
import { toJS } from 'mobx';
import { observer } from 'mobx-react-lite';
import styled from 'styled-components';
import { Text } from 'ui/elements';
import { Box } from 'ui/grid';
const Grid = styled(Box)`
display: grid;
grid-template-columns: 1fr;
${min('tablet')} {
grid-template-columns: 1fr 1fr;
}
`;
const Wrapper = styled.div`
margin-bottom: 18px;
`;
const Results = observer(() => {
const { $process, $results } = useStore();
const resultsValues = toJS($results.values);
// eslint-disable-next-line no-negated-condition
const values = !$process.has('Unlimited') ? elements.filter((x) => !x.startsWith('_')) : elements;
return (
<Grid>
{values.map((valueName) => {
const formatter = formatters[valueName];
const storeValue = resultsValues[valueName];
const value = formatter(storeValue);
return (
<Wrapper key={valueName}>
<Container key={valueName}>
<Head title={titles[valueName]} />
<Text>{value}</Text>
</Container>
</Wrapper>
);
})}
</Grid>
);
});
export default {
Component: Results,
id,
title,
};

View File

@ -1,93 +0,0 @@
/* eslint-disable react/jsx-key */
import { useStore } from '@/stores/hooks';
import { observer } from 'mobx-react-lite';
import styled from 'styled-components';
import { Alert } from 'ui/elements';
import { Box, Flex } from 'ui/grid';
const Bold = styled.span`
font-weight: bold;
`;
function Message(title, text) {
return (
<>
<Bold>{title}</Bold>
{': '}
{text}
</>
);
}
const AlertWrapper = styled(Box)`
margin: 0 0 5px 0;
`;
function getAlerts(errors, title, $process) {
return errors.map(({ key, message }) => (
<AlertWrapper>
<Alert
key={key}
type={$process.has('Unlimited') ? 'warning' : 'error'}
showIcon
message={Message(title, message)}
/>
</AlertWrapper>
));
}
function getElementsErrors({ $calculation, $process }) {
return Object.values($calculation.$validation).map((validation) => {
const elementErrors = validation.getErrors();
const elementTitle = validation.params.err_title;
return getAlerts(elementErrors, elementTitle, $process);
});
}
function getTableErrors(tableName, { $process, $tables }) {
const table = $tables[tableName];
const errors = table.validation.getErrors();
const title = table.validation.params.err_title;
return getAlerts(errors, title, $process);
}
const Errors = observer(() => {
const store = useStore();
const { $calculation, $tables } = store;
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;
if (!hasElementsErrors && !hasPaymentsErrors && !hasInsuranceErrors && !hasFingapErrors) {
return <Alert type="success" showIcon message="Ошибок нет 🙂" />;
}
const elementsErrors = getElementsErrors(store);
const paymentsErrors = getTableErrors('payments', store);
const insuranceErrors = getTableErrors('insurance', store);
const fingapErrors = getTableErrors('fingap', store);
const errors = [...elementsErrors, ...paymentsErrors, ...insuranceErrors, ...fingapErrors];
return <Flex flexDirection="column">{errors}</Flex>;
});
function Validation() {
return (
<Box>
<Errors />
</Box>
);
}
export default {
Component: Validation,
id: 'validation',
title: 'Ошибки',
};

View File

@ -1,71 +0,0 @@
import PaymentsTable from './PaymentsTable';
import Results from './Results';
import Validation from './Validation';
import Background from '@/Components/Layout/Background';
import { useErrors, useStore } from '@/stores/hooks';
import { min } from '@/styles/mq';
import { observer } from 'mobx-react-lite';
import { useEffect, useState } from 'react';
import styled from 'styled-components';
import { Badge, Tabs } from 'ui/elements';
const outputTabs = [PaymentsTable, Results, Validation];
const items = outputTabs.map(({ Component, id, title }) => {
let Label = () => title;
if (id === 'validation') {
Label = observer(() => {
const { hasErrors } = useErrors();
return (
<Badge offset={[5, 0]} dot={hasErrors}>
{title}
</Badge>
);
});
}
return {
children: <Component />,
key: id,
label: <Label />,
};
});
const Wrapper = styled(Background)`
padding: 4px 10px;
min-height: 200px;
${min('laptop')} {
padding: 4px 18px;
min-height: 790px;
}
`;
export const Output = observer(({ tabs }) => {
const { $results } = useStore();
const [activeKey, setActiveKey] = useState(undefined);
const { hasErrors } = useErrors();
useEffect(() => {
if ($results.payments.length > 0) {
setActiveKey('payments-table');
}
if (!tabs && hasErrors) {
setActiveKey('validation');
}
}, [$results.payments.length, hasErrors, tabs]);
return (
<Wrapper>
<Tabs
items={tabs ? items.filter((x) => x.key !== 'validation') : items}
activeKey={activeKey}
onChange={(key) => {
setActiveKey(key);
}}
/>
</Wrapper>
);
});

View File

@ -1,62 +1,20 @@
/* eslint-disable import/prefer-default-export */
/* eslint-disable object-curly-newline */
import type { FormTabRows } from '../lib/render-rows'; import type { FormTabRows } from '../lib/render-rows';
const defaultRowStyle = { gridTemplateColumns: '1fr' }; const defaultRowStyle = { gridTemplateColumns: '1fr' };
export const mainRows: FormTabRows = [ export const rows: FormTabRows = [
{ title: 'Выбор Интереса/ЛС' }, { title: 'Выбор Интереса/ЛС' },
[['selectLead'], defaultRowStyle], [['selectLead'], defaultRowStyle],
[['selectOpportunity'], defaultRowStyle], [['selectOpportunity'], defaultRowStyle],
[['cbxRecalcWithRevision'], defaultRowStyle], [['cbxRecalcWithRevision'], defaultRowStyle],
[['selectQuote'], defaultRowStyle], [['selectQuote'], defaultRowStyle],
[ [['btnCalculate'], defaultRowStyle],
['btnCalculate'],
{
gridTemplateColumns: '1fr',
marginBottom: ['5px'],
},
],
[
['btnCreateKP', 'linkDownloadKp'],
{
gap: ['10px'],
gridTemplateColumns: ['1fr 1fr'],
},
],
];
export const unlimitedMainRows: FormTabRows = [
{ title: 'Выбор Интереса/ЛС' },
[['selectUser'], defaultRowStyle],
[['selectLead'], defaultRowStyle],
[['selectOpportunity'], defaultRowStyle],
[['cbxRecalcWithRevision'], defaultRowStyle],
[['selectQuote'], defaultRowStyle],
[
['btnCalculate'],
{
gridTemplateColumns: '1fr',
marginBottom: ['5px'],
},
],
[
['btnCreateKP', 'linkDownloadKp'],
{
gap: ['10px'],
gridTemplateColumns: ['1fr 1fr'],
},
],
];
export const paramsRows: FormTabRows = [
{ title: 'Параметры расчета' }, { title: 'Параметры расчета' },
[['labelIrrInfo'], defaultRowStyle],
[['radioCalcType'], defaultRowStyle], [['radioCalcType'], defaultRowStyle],
[['tbxIRR_Perc'], defaultRowStyle], [['tbxIRR_Perc'], defaultRowStyle],
[['tbxTotalPayments'], defaultRowStyle], [['tbxTotalPayments'], defaultRowStyle],
]; ];
export const unlimitedParamsRows: FormTabRows = [
{ title: 'Параметры расчета' },
[['radioCalcType'], defaultRowStyle],
[['tbxIRR_Perc', 'tbxPi'], { gridTemplateColumns: '1fr 1fr' }],
[['tbxTotalPayments'], defaultRowStyle],
];

View File

@ -1,10 +1,8 @@
import renderFormRows from '../lib/render-rows'; import Background from 'Components/Layout/Background';
import * as config from './config';
import Background from '@/Components/Layout/Background';
import { useStore } from '@/stores/hooks';
import { min } from '@/styles/mq';
import { memo } from 'react';
import styled from 'styled-components'; import styled from 'styled-components';
import { min } from 'styles/mq';
import renderFormRows from '../lib/render-rows';
import { rows } from './config';
const Wrapper = styled(Background)` const Wrapper = styled(Background)`
padding: 4px 10px; padding: 4px 10px;
@ -18,20 +16,6 @@ const Wrapper = styled(Background)`
} }
`; `;
export const Settings = memo(() => { export default function Settings() {
const { $process } = useStore(); return <Wrapper>{renderFormRows(rows)}</Wrapper>;
}
const mainRows = $process.has('Unlimited')
? renderFormRows(config.unlimitedMainRows)
: renderFormRows(config.mainRows);
const paramsRows = $process.has('Unlimited')
? renderFormRows(config.unlimitedParamsRows)
: renderFormRows(config.paramsRows);
return (
<Wrapper>
{mainRows}
{paramsRows}
</Wrapper>
);
});

View File

@ -1,19 +0,0 @@
import Validation from '../Output/Validation';
import Background from '@/Components/Layout/Background';
import { min } from '@/styles/mq';
import { memo } from 'react';
import styled from 'styled-components';
const Wrapper = styled(Background)`
padding: 4px 10px;
${min('laptop')} {
padding: 4px 18px;
}
`;
export const Component = memo(() => (
<Wrapper>
<Validation.Component />
</Wrapper>
));

View File

@ -1,19 +1,29 @@
import * as CRMTypes from '@/graphql/crm.types'; import { gql, useQuery } from '@apollo/client';
import { useStore } from '@/stores/hooks'; import type * as CRMTypes from 'graphql/crm.types';
import { useQuery } from '@apollo/client';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useStore } from 'stores/hooks';
const QUERY_GET_CURRENCY_SYMBOL = gql`
query GetCurrencySymbol($currencyid: Uuid!) {
transactioncurrency(transactioncurrencyid: $currencyid) {
currencysymbol
}
}
`;
const CurrencyAddon = observer(() => { const CurrencyAddon = observer(() => {
const { $calculation } = useStore(); const { $calculation } = useStore();
const currencyid = $calculation.element('selectSupplierCurrency').getValue(); const currencyid = $calculation.element('selectSupplierCurrency').getValue();
const { data } = useQuery(CRMTypes.GetTransactionCurrencyDocument, { const { data } = useQuery<
skip: !currencyid, CRMTypes.GetCurrencySymbolQuery,
CRMTypes.GetCurrencySymbolQueryVariables
>(QUERY_GET_CURRENCY_SYMBOL, {
variables: { variables: {
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
currencyid: currencyid!, currencyid: currencyid!,
}, },
skip: !currencyid,
}); });
return <span>{data?.transactioncurrency?.currencysymbol}</span>; return <span>{data?.transactioncurrency?.currencysymbol}</span>;

View File

@ -1,31 +0,0 @@
import { useStore } from '@/stores/hooks';
import { observer } from 'mobx-react-lite';
import styled from 'styled-components';
import { LoadingOutlined } from 'ui/elements/icons';
const TextAddon = styled.span`
font-size: 14px;
`;
const formatter = Intl.NumberFormat('ru', {
minimumFractionDigits: 2,
}).format;
export const IRRAddon = observer(() => {
const { $calculation, $process } = useStore();
if ($process.has('Tarif')) {
return (
<TextAddon>
<LoadingOutlined rev="" />
{' Подбирается тариф...'}
</TextAddon>
);
}
const tarif = $calculation.element('selectTarif').getValue();
if (!tarif) return <TextAddon>Тариф не найден</TextAddon>;
const { min, max } = $calculation.$values.getValue('irrInfo');
return <TextAddon>{`${formatter(min)}% - ${formatter(max)}%`}</TextAddon>;
});

View File

@ -1,53 +0,0 @@
/* eslint-disable react/forbid-component-props */
import titles from '../config/elements-titles';
import { useStore } from '@/stores/hooks';
import { observer } from 'mobx-react-lite';
import { pick } from 'radash';
import styled from 'styled-components';
import { Tag } from 'ui/elements';
const Container = styled.div`
display: flex;
flex-direction: row;
gap: 5px;
`;
const TagWrapper = styled.div<{ disabled: boolean }>`
> span {
pointer-events: ${(props) => (props.disabled ? 'none' : 'auto')};
opacity: ${(props) => (props.disabled ? '50%' : '')};
}
`;
const tagsData = pick(titles, ['cbxPartialVAT', 'cbxFloatingRate']);
const { CheckableTag } = Tag;
export const ProductAddon = observer(() => {
const { $calculation } = useStore();
function handleChange(elementName: keyof typeof tagsData, checked: boolean) {
$calculation.element(elementName).setValue(checked);
}
return (
<Container>
{(Object.keys(tagsData) as Array<keyof typeof tagsData>).map((elementName) => {
const visible = $calculation.$status.getStatus(elementName);
return (
<TagWrapper key={elementName} disabled={visible === 'Disabled'}>
<CheckableTag
checked={$calculation.element(elementName).getValue()}
onChange={(checked) => handleChange(elementName, checked)}
key={elementName}
style={{ marginInlineEnd: 0 }}
>
{tagsData[elementName]}
</CheckableTag>
</TagWrapper>
);
})}
</Container>
);
});

View File

@ -1,39 +1,25 @@
import type { Elements } from '../config/map/actions'; /* eslint-disable react/jsx-no-bind */
import { useProcessContext } from '@/process/hooks/common';
import { useStatus } from '@/stores/calculation/statuses/hooks';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import type { ComponentType } from 'react'; import type { ComponentType } from 'react';
import { useThrottledCallback } from 'use-debounce'; import { useStatus } from 'stores/calculation/statuses/hooks';
import type { Elements } from '../config/map/actions';
type BuilderProps = { type BuilderProps = {
elementName: Elements; elementName: Elements;
valueName: string; valueName: string;
}; };
export function buildAction<T>( export default function buildAction<T>(
Component: ComponentType<T>, Component: ComponentType<T>,
{ elementName, valueName: processName }: BuilderProps { elementName, valueName: actionName }: BuilderProps
) { ) {
return observer((props: T) => { return observer((props: T) => {
const status = useStatus(elementName); const status = useStatus(elementName);
const context = useProcessContext();
const throttledAction = useThrottledCallback(
() => {
import(`process/${processName}/action`).then((module) => module.action(context));
},
1200,
{
trailing: false,
}
);
return ( return (
<Component <Component
onClick={throttledAction}
status={status} status={status}
disabled={status === 'Disabled'} action={() => import(`process/${actionName}`).then((m) => m.default())}
loading={status === 'Loading'}
{...props} {...props}
/> />
); );

View File

@ -1,38 +0,0 @@
import type { Elements } from '../config/map/values';
import { useStoreValue } from './hooks';
import { useStatus } from '@/stores/calculation/statuses/hooks';
import { useValidation } from '@/stores/calculation/validation/hooks';
import type { Values } from '@/stores/calculation/values/types';
import type { CheckboxChangeEvent } from 'antd/lib/checkbox';
import { observer } from 'mobx-react-lite';
import type { ComponentType } from 'react';
import { Form } from 'ui/elements';
type BuilderProps = {
elementName: Elements;
valueName: Values;
};
export function buildCheck<T>(
Component: ComponentType<T>,
{ elementName, valueName }: BuilderProps
) {
return observer((props: T) => {
const [value, setValue] = useStoreValue(valueName);
const status = useStatus(elementName);
const { validateStatus, help } = useValidation(elementName);
return (
<Form.Item help={help} validateStatus={validateStatus}>
<Component
onChange={(event: CheckboxChangeEvent) => {
setValue(event.target.checked);
}}
disabled={status === 'Disabled'}
checked={value}
{...props}
/>
</Form.Item>
);
});
}

View File

@ -1,30 +0,0 @@
import type { Elements } from '../config/map/values';
import { useStoreValue } from './hooks';
import { useStatus } from '@/stores/calculation/statuses/hooks';
import type { Values } from '@/stores/calculation/values/types';
import { observer } from 'mobx-react-lite';
import type { ComponentType } from 'react';
export type BuilderProps = {
elementName: Elements;
valueName: Values;
};
export function buildLink<T>(
Component: ComponentType<T>,
{ elementName, valueName }: BuilderProps
) {
return observer((props: T) => {
const [value] = useStoreValue(valueName);
const status = useStatus(elementName);
return (
<Component
href={status === 'Disabled' ? undefined : value || undefined}
disabled={!value || status === 'Disabled'}
loading={status === 'Loading'}
{...props}
/>
);
});
}

Some files were not shown because too many files have changed in this diff Show More