Compare commits

..

41 Commits

Author SHA1 Message Date
vchikalkin
2884ccf3e0 add options/value validation for insurance table 2023-02-02 20:38:58 +03:00
vchikalkin
2a4203914b move options elements validation to process/calculate 2023-02-02 20:00:08 +03:00
vchikalkin
6172833fdb process/price: add common reactions 2023-02-02 13:26:49 +03:00
vchikalkin
d3c805adb6 process/price: load data from kp 2023-02-02 13:14:14 +03:00
vchikalkin
6bba6113a7 fix build 2023-02-02 12:22:50 +03:00
vchikalkin
626b71c363 validation: disable notification for options elements validation 2023-02-02 12:09:29 +03:00
vchikalkin
f53f4b8d66 elements: check value in options 2023-02-02 11:28:21 +03:00
vchikalkin
a5f6805115 process/configurator: get quote data 2023-02-01 21:13:18 +03:00
vchikalkin
1aa557cd39 packages: upgrade turbo 2023-02-01 20:24:47 +03:00
vchikalkin
5fd2d4b9d8 turbo: minor config changes 2023-02-01 20:22:14 +03:00
vchikalkin
3d4d8198da turbo: enable cache for build 2023-02-01 20:13:00 +03:00
vchikalkin
3bc6f43551 graphql: specify fetchPolicy to some queries 2023-02-01 20:02:04 +03:00
vchikalkin
30b6947314 graphql: rename GetQuotesByLead query 2023-02-01 19:57:03 +03:00
vchikalkin
9e33e1f70a graphql: share more queries 2023-02-01 17:53:04 +03:00
vchikalkin
68a6b8d205 graphql: move shared queries to .graphql file 2023-02-01 16:22:30 +03:00
vchikalkin
922244d33a graphql: share processes queries 2023-02-01 16:05:48 +03:00
vchikalkin
079446181a graphql: share common queries
packages: add @graphql-codegen/typed-document-node
2023-02-01 15:18:55 +03:00
vchikalkin
6ffc830469 fix build 2023-02-01 12:41:52 +03:00
vchikalkin
7b6d8d00b4 config/options: add default selectLeasingWithoutKasko 2023-01-30 17:27:57 +03:00
vchikalkin
81a10296a9 bdc360f: поправили 2023-01-30 17:25:45 +03:00
vchikalkin
aab9045460 ff465ae: поправили 2023-01-30 17:09:32 +03:00
vchikalkin
d4b4fc2246 ee53815: поправили 2023-01-30 16:32:56 +03:00
vchikalkin
888a4badf1 50a1fb0: добавили описание 2023-01-30 16:09:56 +03:00
vchikalkin
1863d866b6 5ea1fe3: поправили [2] 2023-01-30 15:41:33 +03:00
vchikalkin
51171b7cce 5ea1fe3: поправили и добавили описание 2023-01-30 15:14:31 +03:00
vchikalkin
fe43a92c4d На изменение поля Процет убывания платежей tbxParmentsDecreasePercent необходимо заложить проверку:
* Если значение поля меньше значения в поле "Минимальный % убывания платежей" evo_min_decreasing_perc из записи,
     * указанной в поле ТарифselectTarif , то поле Процет убывания платежей tbxParmentsDecreasePercent должно обводиться красной рамкой
     * и выводиться сообщение "Процент убывания не может быть меньше минимального значения по данному тарифу
      - <указывается значение из поля "Минимальный % убывания платежей">, иначе красная рамка снимается.
     * При красной рамке в данном поле нельзя осуществить расчет графика.
2023-01-30 13:52:26 +03:00
vchikalkin
fd116f89bb Добавить фильтр для поля selectProduct (работает на загрузку КП)
* если  recalcWithRevision=True, то в списке поля selectProduct должны отображаться
   * записи quote.evo_baseproductid из поля Предложение selectQuote + дать возможность выбирать продукт,
   * связанный с оquote.evo_baseproductid из поля Предложение selectQuote по связи evo_evo_baseproduct_evo_baseproduct
2023-01-30 13:38:49 +03:00
vchikalkin
aa711a3e37 При изменении Продукта selectProduct необходимо в поле "Расчет от"
* radioCalcType фильтровать значения согласно списку
   * в поле "Доступные Методы расчета в калькуляторе" evo_calculation_method в selectProduct
2023-01-30 11:45:24 +03:00
vchikalkin
ff465ae65e в поле Тип дегрессии/сезонности selectSeasonType должны отображаться только те значения,
* которых нет в массиве поля "Недопустимые Типы дегрессий/сезонности" evo_seasons_type_exception
   * (добавить в текущую фильтрацию поля)
2023-01-30 11:37:40 +03:00
vchikalkin
bdc360fec0 На изменение поля Тариф selectTarif добавляем фильрацию в полях:
в поле Вид графика radioGraphType должны закрываться значения для выбора,
    которые указаны в массиве поля "Недопустимые виды графиков" evo_graphtype_exception.
    Если в массиве пусто, значит допустимы все значения
2023-01-30 11:24:36 +03:00
vchikalkin
ee538152b4 process/configurator: подбор тарифа 2023-01-26 20:07:47 +03:00
vchikalkin
156d3a131a add configurator process 2023-01-25 18:28:42 +03:00
vchikalkin
6ac00c17d6 fix mobx errors 2023-01-25 17:43:23 +03:00
vchikalkin
50a1fb07cd leasing-object: загрузка списка selectConfiguration
+ валидация
2023-01-25 15:19:35 +03:00
vchikalkin
93d4c31d0c load-kp: minor improvements 2023-01-25 14:57:09 +03:00
vchikalkin
34c4c130be load-kp: get leaseObjectType, brand, model from quote 2023-01-25 14:33:02 +03:00
vchikalkin
ce4a8ad2c1 selectBrand: загрузка списка и фильтрация 2023-01-25 13:58:05 +03:00
vchikalkin
b8539c4cc5 selectModel: загрузка списка и фильтрация 2023-01-24 14:25:59 +03:00
vchikalkin
2782df536c process/leasing-without-kasko: filter selectLeasingWithoutKasko 2023-01-19 10:50:55 +03:00
vchikalkin
5ea1fe3925 process: add leasing-without-kasko
add subsidy (calculate importProgramSum)
2023-01-18 18:34:47 +03:00
vchikalkin
8a6a537590 добавили новое поле selectLeasingWithoutKasko 2023-01-18 17:15:17 +03:00
484 changed files with 18373 additions and 49125 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

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,6 +1,6 @@
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:
@ -16,7 +16,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) { () => {
debouncedSetStoreValue(value); if (storeValue !== 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,13 @@
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']],
[['selectLeasingWithoutKasko', 'selectInsNSIB']],
]; ];
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) { () => {
debouncedSetStoreValue(value); if (storeValue !== value) {
} debouncedSetStoreValue(value);
}, [value]); }
},
useEffect(() => { // eslint-disable-next-line react-hooks/exhaustive-deps
setValue(storeValue); [value]
}, [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 (
<TableWrapper>
return useMemo( <Table size="small" columns={columns} dataSource={dataSource} pagination={false} />
() => ( </TableWrapper>
<TableWrapper>
<Table
size="small"
columns={columns.filter((x) => ['num', mode].includes(x.key))}
dataSource={dataSource}
pagination={false}
/>
</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 /> <TablesGroup />
<Mode setMode={debouncedSetMode} />
<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,7 +1,7 @@
import * as CRMTypes from '@/graphql/crm.types';
import { useStore } from '@/stores/hooks';
import { useQuery } from '@apollo/client'; import { useQuery } from '@apollo/client';
import * as CRMTypes from 'graphql/crm.types';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import { useStore } from 'stores/hooks';
const CurrencyAddon = observer(() => { const CurrencyAddon = observer(() => {
const { $calculation } = useStore(); const { $calculation } = useStore();
@ -9,11 +9,10 @@ const CurrencyAddon = observer(() => {
const currencyid = $calculation.element('selectSupplierCurrency').getValue(); const currencyid = $calculation.element('selectSupplierCurrency').getValue();
const { data } = useQuery(CRMTypes.GetTransactionCurrencyDocument, { const { data } = useQuery(CRMTypes.GetTransactionCurrencyDocument, {
skip: !currencyid,
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}
/>
);
});
}

View File

@ -1,39 +1,37 @@
import type { Elements } from '../config/map/values';
import { useStoreValue } from './hooks';
import { useOptions } from '@/stores/calculation/options/hooks';
import { useStatus } from '@/stores/calculation/statuses/hooks';
import { useValidation } from '@/stores/calculation/validation/hooks';
import type { Values } from '@/stores/calculation/values/types';
import { observer } from 'mobx-react-lite'; import { observer } from 'mobx-react-lite';
import type { ComponentType } from 'react'; import type { ComponentType } from 'react';
import { Form } from 'ui/elements'; import { useOptions } from 'stores/calculation/options/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 { Elements } from '../config/map/values';
import { useStoreValue } from './hooks';
type BuilderProps = { type BuilderProps = {
elementName: Elements; elementName: Elements;
valueName: Values; valueName: Values;
}; };
export function buildOptions<T>( export default function buildOptions<T>(
Component: ComponentType<T>, Component: ComponentType<T>,
{ elementName, valueName }: BuilderProps { elementName, valueName }: BuilderProps
) { ) {
return observer((props: T) => { return observer((props: T) => {
const [value, setValue] = useStoreValue(valueName); const [value, setValue] = useStoreValue(valueName);
const status = useStatus(elementName); const status = useStatus(elementName);
const { validateStatus, help } = useValidation(elementName); const { isValid, help } = useValidation(elementName);
const options = useOptions(elementName); const options = useOptions(elementName);
return ( return (
<Form.Item help={help} validateStatus={validateStatus}> <Component
<Component value={value}
disabled={status === 'Disabled'} setValue={setValue}
loading={status === 'Loading'} options={options}
options={options} status={status}
onChange={setValue} isValid={isValid}
value={value} help={help}
{...props} {...props}
/> />
</Form.Item>
); );
}); });
} }

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