Files
rmser/rmser-view/project_context.md
SERTY 51bc5bf8f0 0302-отрефакторил в нормальный вид на мобилу и десктоп
сразу выкинул пути в импортах и добавил алиас для корня
2026-02-03 12:49:20 +03:00

12452 lines
393 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# ===================================================================
# Полный контекст React Typescript проекта
# Сгенерировано: 2026-02-03 12:00:59
# ===================================================================
Это полный дамп исходного кода React Typescript (Vite) проекта.
Каждый файл предваряется заголовком с путём к нему.
# ===================================================================
# Файл: Dockerfile
# ===================================================================
```
# Этап 1: Сборка (Build)
FROM node:24-alpine as builder
WORKDIR /app
# Копируем файлы зависимостей
COPY package*.json ./
# Устанавливаем зависимости
RUN npm install
# Копируем исходный код
COPY . .
# Собираем проект (результат будет в папке dist)
# Важно: Vite подставит VITE_API_URL во время сборки.
# Мы будем использовать относительный путь /api, чтобы работал прокси Nginx.
ENV VITE_API_URL=/api
RUN npm run build
# Этап 2: Запуск (Serve via Nginx)
FROM nginx:alpine
# Копируем конфиг nginx (создадим его на след. шаге)
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Копируем собранные файлы из этапа сборки
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
```
# ===================================================================
# Файл: eslint.config.js
# ===================================================================
```
import js from '@eslint/js'
import globals from 'globals'
import reactHooks from 'eslint-plugin-react-hooks'
import reactRefresh from 'eslint-plugin-react-refresh'
import tseslint from 'typescript-eslint'
import { defineConfig, globalIgnores } from 'eslint/config'
export default defineConfig([
globalIgnores(['dist']),
{
files: ['**/*.{ts,tsx}'],
extends: [
js.configs.recommended,
tseslint.configs.recommended,
reactHooks.configs.flat.recommended,
reactRefresh.configs.vite,
],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
},
},
])
```
# ===================================================================
# Файл: index.html
# ===================================================================
```
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>RMSer App</title>
<script src="https://telegram.org/js/telegram-web-app.js"></script>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.tsx"></script>
</body>
</html>
```
# ===================================================================
# Файл: package-lock.json
# ===================================================================
```
{
"name": "rmser-view",
"version": "0.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "rmser-view",
"version": "0.0.0",
"dependencies": {
"@hello-pangea/dnd": "^18.0.1",
"@tanstack/react-query": "^5.90.12",
"@twa-dev/sdk": "^8.0.2",
"antd": "^6.1.0",
"axios": "^1.13.2",
"clsx": "^2.1.1",
"immer": "^11.1.3",
"lucide-react": "^0.563.0",
"qrcode.react": "^4.2.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-dropzone": "^14.3.8",
"react-router-dom": "^7.10.1",
"xlsx": "^0.18.5",
"zustand": "^5.0.9"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.2.4"
}
},
"node_modules/@ant-design/colors": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-8.0.0.tgz",
"integrity": "sha512-6YzkKCw30EI/E9kHOIXsQDHmMvTllT8STzjMb4K2qzit33RW2pqCJP0sk+hidBntXxE+Vz4n1+RvCTfBw6OErw==",
"license": "MIT",
"dependencies": {
"@ant-design/fast-color": "^3.0.0"
}
},
"node_modules/@ant-design/cssinjs": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@ant-design/cssinjs/-/cssinjs-2.0.1.tgz",
"integrity": "sha512-Lw1Z4cUQxdMmTNir67gU0HCpTl5TtkKCJPZ6UBvCqzcOTl/QmMFB6qAEoj8qFl0CuZDX9qQYa3m9+rEKfaBSbA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.11.1",
"@emotion/hash": "^0.8.0",
"@emotion/unitless": "^0.7.5",
"@rc-component/util": "^1.4.0",
"clsx": "^2.1.1",
"csstype": "^3.1.3",
"stylis": "^4.3.4"
},
"peerDependencies": {
"react": ">=16.0.0",
"react-dom": ">=16.0.0"
}
},
"node_modules/@ant-design/cssinjs-utils": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@ant-design/cssinjs-utils/-/cssinjs-utils-2.0.2.tgz",
"integrity": "sha512-Mq3Hm6fJuQeFNKSp3+yT4bjuhVbdrsyXE2RyfpJFL0xiYNZdaJ6oFaE3zFrzmHbmvTd2Wp3HCbRtkD4fU+v2ZA==",
"license": "MIT",
"dependencies": {
"@ant-design/cssinjs": "^2.0.1",
"@babel/runtime": "^7.23.2",
"@rc-component/util": "^1.4.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/@ant-design/fast-color": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@ant-design/fast-color/-/fast-color-3.0.0.tgz",
"integrity": "sha512-eqvpP7xEDm2S7dUzl5srEQCBTXZMmY3ekf97zI+M2DHOYyKdJGH0qua0JACHTqbkRnD/KHFQP9J1uMJ/XWVzzA==",
"license": "MIT",
"engines": {
"node": ">=8.x"
}
},
"node_modules/@ant-design/icons": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/@ant-design/icons/-/icons-6.1.0.tgz",
"integrity": "sha512-KrWMu1fIg3w/1F2zfn+JlfNDU8dDqILfA5Tg85iqs1lf8ooyGlbkA+TkwfOKKgqpUmAiRY1PTFpuOU2DAIgSUg==",
"license": "MIT",
"dependencies": {
"@ant-design/colors": "^8.0.0",
"@ant-design/icons-svg": "^4.4.0",
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
},
"engines": {
"node": ">=8"
},
"peerDependencies": {
"react": ">=16.0.0",
"react-dom": ">=16.0.0"
}
},
"node_modules/@ant-design/icons-svg": {
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz",
"integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==",
"license": "MIT"
},
"node_modules/@ant-design/react-slick": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/@ant-design/react-slick/-/react-slick-2.0.0.tgz",
"integrity": "sha512-HMS9sRoEmZey8LsE/Yo6+klhlzU12PisjrVcydW3So7RdklyEd2qehyU6a7Yp+OYN72mgsYs3NFCyP2lCPFVqg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.28.4",
"clsx": "^2.1.1",
"json2mq": "^0.2.0",
"throttle-debounce": "^5.0.0"
},
"peerDependencies": {
"react": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^0.14.0 || ^15.0.1 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/compat-data": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz",
"integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/core": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz",
"integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-module-transforms": "^7.28.3",
"@babel/helpers": "^7.28.4",
"@babel/parser": "^7.28.5",
"@babel/template": "^7.27.2",
"@babel/traverse": "^7.28.5",
"@babel/types": "^7.28.5",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
"gensync": "^1.0.0-beta.2",
"json5": "^2.2.3",
"semver": "^6.3.1"
},
"engines": {
"node": ">=6.9.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/babel"
}
},
"node_modules/@babel/generator": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz",
"integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.28.5",
"@babel/types": "^7.28.5",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-compilation-targets": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.27.2",
"@babel/helper-validator-option": "^7.27.1",
"browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
"semver": "^6.3.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-globals": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz",
"integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-imports": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.27.1",
"@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
"integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1",
"@babel/traverse": "^7.28.3"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0"
}
},
"node_modules/@babel/helper-plugin-utils": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz",
"integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-string-parser": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz",
"integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-validator-option": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz",
"integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helpers": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
"integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.27.2",
"@babel/types": "^7.28.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz",
"integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.5"
},
"bin": {
"parser": "bin/babel-parser.js"
},
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@babel/plugin-transform-react-jsx-self": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz",
"integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-react-jsx-source": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz",
"integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/runtime": {
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz",
"integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/template": {
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/parser": "^7.27.2",
"@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz",
"integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.5",
"@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.28.5",
"@babel/template": "^7.27.2",
"@babel/types": "^7.28.5",
"debug": "^4.3.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz",
"integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@emotion/hash": {
"version": "0.8.0",
"resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz",
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
"license": "MIT"
},
"node_modules/@emotion/unitless": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz",
"integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openharmony-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.9.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz",
"integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"eslint-visitor-keys": "^3.4.3"
},
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
},
"peerDependencies": {
"eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
}
},
"node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": {
"version": "3.4.3",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz",
"integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^12.22.0 || ^14.17.0 || >=16.0.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint-community/regexpp": {
"version": "4.12.2",
"resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz",
"integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.0.0 || ^14.0.0 || >=16.0.0"
}
},
"node_modules/@eslint/config-array": {
"version": "0.21.1",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.1.tgz",
"integrity": "sha512-aw1gNayWpdI/jSYVgzN5pL0cfzU02GT3NBpeT/DXbx1/1x7ZKxFPd9bwrzygx/qiwIQiJ1sw/zD8qY/kRvlGHA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/object-schema": "^2.1.7",
"debug": "^4.3.1",
"minimatch": "^3.1.2"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/config-helpers": {
"version": "0.4.2",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz",
"integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.17.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/core": {
"version": "0.17.0",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz",
"integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@types/json-schema": "^7.0.15"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/eslintrc": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.3.tgz",
"integrity": "sha512-Kr+LPIUVKz2qkx1HAMH8q1q6azbqBAsXJUxBl/ODDuVPX45Z9DfwB8tPjTi6nNZ8BuM3nbJxC5zCAg5elnBUTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ajv": "^6.12.4",
"debug": "^4.3.2",
"espree": "^10.0.1",
"globals": "^14.0.0",
"ignore": "^5.2.0",
"import-fresh": "^3.2.1",
"js-yaml": "^4.1.1",
"minimatch": "^3.1.2",
"strip-json-comments": "^3.1.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/@eslint/eslintrc/node_modules/globals": {
"version": "14.0.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz",
"integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@eslint/js": {
"version": "9.39.1",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.1.tgz",
"integrity": "sha512-S26Stp4zCy88tH94QbBv3XCuzRQiZ9yXofEILmglYTh/Ug/a9/umqvgFtYBAo3Lp0nsI/5/qH1CCrbdK3AP1Tw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://eslint.org/donate"
}
},
"node_modules/@eslint/object-schema": {
"version": "2.1.7",
"resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz",
"integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@eslint/plugin-kit": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz",
"integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@eslint/core": "^0.17.0",
"levn": "^0.4.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
}
},
"node_modules/@hello-pangea/dnd": {
"version": "18.0.1",
"resolved": "https://registry.npmjs.org/@hello-pangea/dnd/-/dnd-18.0.1.tgz",
"integrity": "sha512-xojVWG8s/TGrKT1fC8K2tIWeejJYTAeJuj36zM//yEm/ZrnZUSFGS15BpO+jGZT1ybWvyXmeDJwPYb4dhWlbZQ==",
"license": "Apache-2.0",
"dependencies": {
"@babel/runtime": "^7.26.7",
"css-box-model": "^1.2.1",
"raf-schd": "^4.0.3",
"react-redux": "^9.2.0",
"redux": "^5.0.1"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0",
"react-dom": "^18.0.0 || ^19.0.0"
}
},
"node_modules/@humanfs/core": {
"version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
"integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanfs/node": {
"version": "0.16.7",
"resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz",
"integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@humanfs/core": "^0.19.1",
"@humanwhocodes/retry": "^0.4.0"
},
"engines": {
"node": ">=18.18.0"
}
},
"node_modules/@humanwhocodes/module-importer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz",
"integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=12.22"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@humanwhocodes/retry": {
"version": "0.4.3",
"resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz",
"integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/nzakas"
}
},
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/sourcemap-codec": "^1.5.0",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/remapping": {
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.24"
}
},
"node_modules/@jridgewell/resolve-uri": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
"dev": true,
"license": "MIT"
},
"node_modules/@jridgewell/trace-mapping": {
"version": "0.3.31",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/resolve-uri": "^3.1.0",
"@jridgewell/sourcemap-codec": "^1.4.14"
}
},
"node_modules/@rc-component/async-validator": {
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@rc-component/async-validator/-/async-validator-5.0.4.tgz",
"integrity": "sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.24.4"
},
"engines": {
"node": ">=14.x"
}
},
"node_modules/@rc-component/cascader": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@rc-component/cascader/-/cascader-1.9.0.tgz",
"integrity": "sha512-2jbthe1QZrMBgtCvNKkJFjZYC3uKl4N/aYm5SsMvO3T+F+qRT1CGsSM9bXnh1rLj7jDk/GK0natShWF/jinhWQ==",
"license": "MIT",
"dependencies": {
"@rc-component/select": "~1.3.0",
"@rc-component/tree": "~1.1.0",
"@rc-component/util": "^1.4.0",
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
},
"node_modules/@rc-component/checkbox": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rc-component/checkbox/-/checkbox-1.0.1.tgz",
"integrity": "sha512-08yTH8m+bSm8TOqbybbJ9KiAuIATti6bDs2mVeSfu4QfEnyeF6X0enHVvD1NEAyuBWEAo56QtLe++MYs2D9XiQ==",
"license": "MIT",
"dependencies": {
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/collapse": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@rc-component/collapse/-/collapse-1.1.2.tgz",
"integrity": "sha512-ilBYk1dLLJHu5Q74dF28vwtKUYQ42ZXIIDmqTuVy4rD8JQVvkXOs+KixVNbweyuIEtJYJ7+t+9GVD9dPc6N02w==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.10.1",
"@rc-component/motion": "^1.1.4",
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
},
"node_modules/@rc-component/color-picker": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@rc-component/color-picker/-/color-picker-3.0.3.tgz",
"integrity": "sha512-V7gFF9O7o5XwIWafdbOtqI4BUUkEUkgdBwp6favy3xajMX/2dDqytFaiXlcwrpq6aRyPLp5dKLAG5RFKLXMeGA==",
"license": "MIT",
"dependencies": {
"@ant-design/fast-color": "^3.0.0",
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/context": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@rc-component/context/-/context-2.0.1.tgz",
"integrity": "sha512-HyZbYm47s/YqtP6pKXNMjPEMaukyg7P0qVfgMLzr7YiFNMHbK2fKTAGzms9ykfGHSfyf75nBbgWw+hHkp+VImw==",
"license": "MIT",
"dependencies": {
"@rc-component/util": "^1.3.0"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/dialog": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@rc-component/dialog/-/dialog-1.5.1.tgz",
"integrity": "sha512-by4Sf/a3azcb89WayWuwG19/Y312xtu8N81HoVQQtnsBDylfs+dog98fTAvLinnpeoWG52m/M7QLRW6fXR3l1g==",
"license": "MIT",
"dependencies": {
"@rc-component/motion": "^1.1.3",
"@rc-component/portal": "^2.0.0",
"@rc-component/util": "^1.0.1",
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
},
"node_modules/@rc-component/drawer": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@rc-component/drawer/-/drawer-1.3.0.tgz",
"integrity": "sha512-rE+sdXEmv2W25VBQ9daGbnb4J4hBIEKmdbj0b3xpY+K7TUmLXDIlSnoXraIbFZdGyek9WxxGKK887uRnFgI+pQ==",
"license": "MIT",
"dependencies": {
"@rc-component/motion": "^1.1.4",
"@rc-component/portal": "^2.0.0",
"@rc-component/util": "^1.2.1",
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
},
"node_modules/@rc-component/dropdown": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rc-component/dropdown/-/dropdown-1.0.2.tgz",
"integrity": "sha512-6PY2ecUSYhDPhkNHHb4wfeAya04WhpmUSKzdR60G+kMNVUCX2vjT/AgTS0Lz0I/K6xrPMJ3enQbwVpeN3sHCgg==",
"license": "MIT",
"dependencies": {
"@rc-component/trigger": "^3.0.0",
"@rc-component/util": "^1.2.1",
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": ">=16.11.0",
"react-dom": ">=16.11.0"
}
},
"node_modules/@rc-component/form": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@rc-component/form/-/form-1.4.0.tgz",
"integrity": "sha512-C8MN/2wIaW9hSrCCtJmcgCkWTQNIspN7ARXLFA4F8PGr8Qxk39U5pS3kRK51/bUJNhb/fEtdFnaViLlISGKI2A==",
"license": "MIT",
"dependencies": {
"@rc-component/async-validator": "^5.0.3",
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
},
"engines": {
"node": ">=8.x"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/image": {
"version": "1.5.3",
"resolved": "https://registry.npmjs.org/@rc-component/image/-/image-1.5.3.tgz",
"integrity": "sha512-/NR7QW9uCN8Ugar+xsHZOPvzPySfEhcW2/vLcr7VPRM+THZMrllMRv7LAUgW7ikR+Z67Ab67cgPp5K5YftpJsQ==",
"license": "MIT",
"dependencies": {
"@rc-component/motion": "^1.0.0",
"@rc-component/portal": "^2.0.0",
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/input": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@rc-component/input/-/input-1.1.2.tgz",
"integrity": "sha512-Q61IMR47piUBudgixJ30CciKIy9b1H95qe7GgEKOmSJVJXvFRWJllJfQry9tif+MX2cWFXWJf/RXz4kaCeq/Fg==",
"license": "MIT",
"dependencies": {
"@rc-component/util": "^1.4.0",
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": ">=16.0.0",
"react-dom": ">=16.0.0"
}
},
"node_modules/@rc-component/input-number": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@rc-component/input-number/-/input-number-1.6.2.tgz",
"integrity": "sha512-Gjcq7meZlCOiWN1t1xCC+7/s85humHVokTBI7PJgTfoyw5OWF74y3e6P8PHX104g9+b54jsodFIzyaj6p8LI9w==",
"license": "MIT",
"dependencies": {
"@rc-component/mini-decimal": "^1.0.1",
"@rc-component/util": "^1.4.0",
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/mentions": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@rc-component/mentions/-/mentions-1.6.0.tgz",
"integrity": "sha512-KIkQNP6habNuTsLhUv0UGEOwG67tlmE7KNIJoQZZNggEZl5lQJTytFDb69sl5CK3TDdISCTjKP3nGEBKgT61CQ==",
"license": "MIT",
"dependencies": {
"@rc-component/input": "~1.1.0",
"@rc-component/menu": "~1.2.0",
"@rc-component/textarea": "~1.1.0",
"@rc-component/trigger": "^3.0.0",
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/menu": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@rc-component/menu/-/menu-1.2.0.tgz",
"integrity": "sha512-VWwDuhvYHSnTGj4n6bV3ISrLACcPAzdPOq3d0BzkeiM5cve8BEYfvkEhNoM0PLzv51jpcejeyrLXeMVIJ+QJlg==",
"license": "MIT",
"dependencies": {
"@rc-component/motion": "^1.1.4",
"@rc-component/overflow": "^1.0.0",
"@rc-component/trigger": "^3.0.0",
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/mini-decimal": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz",
"integrity": "sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.18.0"
},
"engines": {
"node": ">=8.x"
}
},
"node_modules/@rc-component/motion": {
"version": "1.1.6",
"resolved": "https://registry.npmjs.org/@rc-component/motion/-/motion-1.1.6.tgz",
"integrity": "sha512-aEQobs/YA0kqRvHIPjQvOytdtdRVyhf/uXAal4chBjxDu6odHckExJzjn2D+Ju1aKK6hx3pAs6BXdV9+86xkgQ==",
"license": "MIT",
"dependencies": {
"@rc-component/util": "^1.2.0",
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/mutate-observer": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@rc-component/mutate-observer/-/mutate-observer-2.0.1.tgz",
"integrity": "sha512-AyarjoLU5YlxuValRi+w8JRH2Z84TBbFO2RoGWz9d8bSu0FqT8DtugH3xC3BV7mUwlmROFauyWuXFuq4IFbH+w==",
"license": "MIT",
"dependencies": {
"@rc-component/util": "^1.2.0"
},
"engines": {
"node": ">=8.x"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/notification": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@rc-component/notification/-/notification-1.2.0.tgz",
"integrity": "sha512-OX3J+zVU7rvoJCikjrfW7qOUp7zlDeFBK2eA3SFbGSkDqo63Sl4Ss8A04kFP+fxHSxMDIS9jYVEZtU1FNCFuBA==",
"license": "MIT",
"dependencies": {
"@rc-component/motion": "^1.1.4",
"@rc-component/util": "^1.2.1",
"clsx": "^2.1.1"
},
"engines": {
"node": ">=8.x"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/overflow": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@rc-component/overflow/-/overflow-1.0.0.tgz",
"integrity": "sha512-GSlBeoE0XTBi5cf3zl8Qh7Uqhn7v8RrlJ8ajeVpEkNe94HWy5l5BQ0Mwn2TVUq9gdgbfEMUmTX7tJFAg7mz0Rw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.11.1",
"@rc-component/resize-observer": "^1.0.1",
"@rc-component/util": "^1.4.0",
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/pagination": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@rc-component/pagination/-/pagination-1.2.0.tgz",
"integrity": "sha512-YcpUFE8dMLfSo6OARJlK6DbHHvrxz7pMGPGmC/caZSJJz6HRKHC1RPP001PRHCvG9Z/veD039uOQmazVuLJzlw==",
"license": "MIT",
"dependencies": {
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/picker": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@rc-component/picker/-/picker-1.8.0.tgz",
"integrity": "sha512-ek4efrIy+peC8WFJg6Lg7c+WNkykr+wUGQGBNoKmlF0K752aIJuaPcBj6p8CceT9vSJ9gOeeclQCBQIFWVDk1A==",
"license": "MIT",
"dependencies": {
"@rc-component/overflow": "^1.0.0",
"@rc-component/resize-observer": "^1.0.0",
"@rc-component/trigger": "^3.6.15",
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
},
"engines": {
"node": ">=12.x"
},
"peerDependencies": {
"date-fns": ">= 2.x",
"dayjs": ">= 1.x",
"luxon": ">= 3.x",
"moment": ">= 2.x",
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
},
"peerDependenciesMeta": {
"date-fns": {
"optional": true
},
"dayjs": {
"optional": true
},
"luxon": {
"optional": true
},
"moment": {
"optional": true
}
}
},
"node_modules/@rc-component/portal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@rc-component/portal/-/portal-2.0.1.tgz",
"integrity": "sha512-46KYuA7Udb1LAaLIdDrfmDz3wzyeEZxIURJCn+heoQVbhtW5PQkhBSQtRus+DUdsknmTFQulxSnqrbX3CI4yXw==",
"license": "MIT",
"dependencies": {
"@rc-component/util": "^1.2.1",
"clsx": "^2.1.1"
},
"engines": {
"node": ">=12.x"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
},
"node_modules/@rc-component/progress": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rc-component/progress/-/progress-1.0.2.tgz",
"integrity": "sha512-WZUnH9eGxH1+xodZKqdrHke59uyGZSWgj5HBM5Kwk5BrTMuAORO7VJ2IP5Qbm9aH3n9x3IcesqHHR0NWPBC7fQ==",
"license": "MIT",
"dependencies": {
"@rc-component/util": "^1.2.1",
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/qrcode": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/@rc-component/qrcode/-/qrcode-1.1.1.tgz",
"integrity": "sha512-LfLGNymzKdUPjXUbRP+xOhIWY4jQ+YMj5MmWAcgcAq1Ij8XP7tRmAXqyuv96XvLUBE/5cA8hLFl9eO1JQMujrA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.24.7"
},
"engines": {
"node": ">=8.x"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/rate": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rc-component/rate/-/rate-1.0.1.tgz",
"integrity": "sha512-bkXxeBqDpl5IOC7yL7GcSYjQx9G8H+6kLYQnNZWeBYq2OYIv1MONd6mqKTjnnJYpV0cQIU2z3atdW0j1kttpTw==",
"license": "MIT",
"dependencies": {
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
},
"engines": {
"node": ">=8.x"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/resize-observer": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rc-component/resize-observer/-/resize-observer-1.0.1.tgz",
"integrity": "sha512-r+w+Mz1EiueGk1IgjB3ptNXLYSLZ5vnEfKHH+gfgj7JMupftyzvUUl3fRcMZe5uMM04x0n8+G2o/c6nlO2+Wag==",
"license": "MIT",
"dependencies": {
"@rc-component/util": "^1.2.0"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/segmented": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/@rc-component/segmented/-/segmented-1.2.3.tgz",
"integrity": "sha512-L7G4S6zUpqHclOXK0wKKN2/VyqHa9tfDNxkoFjWOTPtQ0ROFaBwZhbf1+9sdZfIFkxJkpcShAmDOMEIBaFFqkw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.11.1",
"@rc-component/motion": "^1.1.4",
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": ">=16.0.0",
"react-dom": ">=16.0.0"
}
},
"node_modules/@rc-component/select": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/@rc-component/select/-/select-1.3.5.tgz",
"integrity": "sha512-A2QVOWDfRoLgHwPHrCGx1G42dYntOk+nsT6SX4ADCoagqu4bcxceJPbYvVKkfMYSIwgtfu+tDhPk3Z5gz8944g==",
"license": "MIT",
"dependencies": {
"@rc-component/overflow": "^1.0.0",
"@rc-component/trigger": "^3.0.0",
"@rc-component/util": "^1.3.0",
"@rc-component/virtual-list": "^1.0.1",
"clsx": "^2.1.1"
},
"engines": {
"node": ">=8.x"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
}
},
"node_modules/@rc-component/slider": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@rc-component/slider/-/slider-1.0.1.tgz",
"integrity": "sha512-uDhEPU1z3WDfCJhaL9jfd2ha/Eqpdfxsn0Zb0Xcq1NGQAman0TWaR37OWp2vVXEOdV2y0njSILTMpTfPV1454g==",
"license": "MIT",
"dependencies": {
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
},
"engines": {
"node": ">=8.x"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/steps": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/@rc-component/steps/-/steps-1.2.2.tgz",
"integrity": "sha512-/yVIZ00gDYYPHSY0JP+M+s3ZvuXLu2f9rEjQqiUDs7EcYsUYrpJ/1bLj9aI9R7MBR3fu/NGh6RM9u2qGfqp+Nw==",
"license": "MIT",
"dependencies": {
"@rc-component/util": "^1.2.1",
"clsx": "^2.1.1"
},
"engines": {
"node": ">=8.x"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/switch": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/@rc-component/switch/-/switch-1.0.3.tgz",
"integrity": "sha512-Jgi+EbOBquje/XNdofr7xbJQZPYJP+BlPfR0h+WN4zFkdtB2EWqEfvkXJWeipflwjWip0/17rNbxEAqs8hVHfw==",
"license": "MIT",
"dependencies": {
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/table": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@rc-component/table/-/table-1.9.0.tgz",
"integrity": "sha512-cq3P9FkD+F3eglkFYhBuNlHclg+r4jY8+ZIgK7zbEFo6IwpnA77YL/Gq4ensLw9oua3zFCTA6JDu6YgBei0TxA==",
"license": "MIT",
"dependencies": {
"@rc-component/context": "^2.0.1",
"@rc-component/resize-observer": "^1.0.0",
"@rc-component/util": "^1.1.0",
"@rc-component/virtual-list": "^1.0.1",
"clsx": "^2.1.1"
},
"engines": {
"node": ">=8.x"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
},
"node_modules/@rc-component/tabs": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@rc-component/tabs/-/tabs-1.7.0.tgz",
"integrity": "sha512-J48cs2iBi7Ho3nptBxxIqizEliUC+ExE23faspUQKGQ550vaBlv3aGF8Epv/UB1vFWeoJDTW/dNzgIU0Qj5i/w==",
"license": "MIT",
"dependencies": {
"@rc-component/dropdown": "~1.0.0",
"@rc-component/menu": "~1.2.0",
"@rc-component/motion": "^1.1.3",
"@rc-component/resize-observer": "^1.0.0",
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
},
"engines": {
"node": ">=8.x"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/textarea": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/@rc-component/textarea/-/textarea-1.1.2.tgz",
"integrity": "sha512-9rMUEODWZDMovfScIEHXWlVZuPljZ2pd1LKNjslJVitn4SldEzq5vO1CL3yy3Dnib6zZal2r2DPtjy84VVpF6A==",
"license": "MIT",
"dependencies": {
"@rc-component/input": "~1.1.0",
"@rc-component/resize-observer": "^1.0.0",
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/tooltip": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@rc-component/tooltip/-/tooltip-1.4.0.tgz",
"integrity": "sha512-8Rx5DCctIlLI4raR0I0xHjVTf1aF48+gKCNeAAo5bmF5VoR5YED+A/XEqzXv9KKqrJDRcd3Wndpxh2hyzrTtSg==",
"license": "MIT",
"dependencies": {
"@rc-component/trigger": "^3.7.1",
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
},
"node_modules/@rc-component/tour": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@rc-component/tour/-/tour-2.2.1.tgz",
"integrity": "sha512-BUCrVikGJsXli38qlJ+h2WyDD6dYxzDA9dV3o0ij6gYhAq6ooT08SUMWOikva9v4KZ2BEuluGl5bPcsjrSoBgQ==",
"license": "MIT",
"dependencies": {
"@rc-component/portal": "^2.0.0",
"@rc-component/trigger": "^3.0.0",
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
},
"engines": {
"node": ">=8.x"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/tree": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rc-component/tree/-/tree-1.1.0.tgz",
"integrity": "sha512-HZs3aOlvFgQdgrmURRc/f4IujiNBf4DdEeXUlkS0lPoLlx9RoqsZcF0caXIAMVb+NaWqKtGQDnrH8hqLCN5zlA==",
"license": "MIT",
"dependencies": {
"@rc-component/motion": "^1.0.0",
"@rc-component/util": "^1.2.1",
"@rc-component/virtual-list": "^1.0.1",
"clsx": "^2.1.1"
},
"engines": {
"node": ">=10.x"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
}
},
"node_modules/@rc-component/tree-select": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/@rc-component/tree-select/-/tree-select-1.4.0.tgz",
"integrity": "sha512-I3UAlO2hNqy9CSKc8EBaESgnmKk2QaRzuZ2XHZGFCgsSMkGl06mdF97sVfROM02YIb64ocgLKefsjE0Ch4ocwQ==",
"license": "MIT",
"dependencies": {
"@rc-component/select": "~1.3.0",
"@rc-component/tree": "~1.1.0",
"@rc-component/util": "^1.4.0",
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": "*",
"react-dom": "*"
}
},
"node_modules/@rc-component/trigger": {
"version": "3.7.1",
"resolved": "https://registry.npmjs.org/@rc-component/trigger/-/trigger-3.7.1.tgz",
"integrity": "sha512-+YNP8FywxKJpdqzlAp6TN8UbSK6YsQtIs3kI13mHfm87qi3qUd5Q9AGW8Unfv76kXFUSu7U7D0FygRsGH+6MiA==",
"license": "MIT",
"dependencies": {
"@rc-component/motion": "^1.1.4",
"@rc-component/portal": "^2.0.0",
"@rc-component/resize-observer": "^1.0.0",
"@rc-component/util": "^1.2.1",
"clsx": "^2.1.1"
},
"engines": {
"node": ">=8.x"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
},
"node_modules/@rc-component/upload": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@rc-component/upload/-/upload-1.1.0.tgz",
"integrity": "sha512-LIBV90mAnUE6VK5N4QvForoxZc4XqEYZimcp7fk+lkE4XwHHyJWxpIXQQwMU8hJM+YwBbsoZkGksL1sISWHQxw==",
"license": "MIT",
"dependencies": {
"@rc-component/util": "^1.3.0",
"clsx": "^2.1.1"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rc-component/util": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@rc-component/util/-/util-1.6.0.tgz",
"integrity": "sha512-YbjuIVAm8InCnXVoA4n6G+uh31yESTxQ6fSY2frZ2/oMSvktoB+bumFUfNN7RKh7YeOkZgOvN2suGtEDhJSX0A==",
"license": "MIT",
"dependencies": {
"is-mobile": "^5.0.0",
"react-is": "^18.2.0"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
},
"node_modules/@rc-component/virtual-list": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/@rc-component/virtual-list/-/virtual-list-1.0.2.tgz",
"integrity": "sha512-uvTol/mH74FYsn5loDGJxo+7kjkO4i+y4j87Re1pxJBs0FaeuMuLRzQRGaXwnMcV1CxpZLi2Z56Rerj2M00fjQ==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.20.0",
"@rc-component/resize-observer": "^1.0.1",
"@rc-component/util": "^1.4.0",
"clsx": "^2.1.1"
},
"engines": {
"node": ">=8.x"
},
"peerDependencies": {
"react": ">=16.9.0",
"react-dom": ">=16.9.0"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.53",
"resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz",
"integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==",
"dev": true,
"license": "MIT"
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz",
"integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz",
"integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz",
"integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz",
"integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz",
"integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz",
"integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz",
"integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz",
"integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz",
"integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz",
"integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-loong64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz",
"integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-ppc64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz",
"integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz",
"integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz",
"integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz",
"integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz",
"integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-musl": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz",
"integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-openharmony-arm64": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz",
"integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openharmony"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz",
"integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz",
"integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-gnu": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz",
"integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz",
"integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@tanstack/query-core": {
"version": "5.90.12",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.12.tgz",
"integrity": "sha512-T1/8t5DhV/SisWjDnaiU2drl6ySvsHj1bHBCWNXd+/T+Hh1cf6JodyEYMd5sgwm+b/mETT4EV3H+zCVczCU5hg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.90.12",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.12.tgz",
"integrity": "sha512-graRZspg7EoEaw0a8faiUASCyJrqjKPdqJ9EwuDRUF9mEYJ1YPczI9H+/agJ0mOJkPCJDk0lsz5QTrLZ/jQ2rg==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.90.12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^18 || ^19"
}
},
"node_modules/@twa-dev/sdk": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@twa-dev/sdk/-/sdk-8.0.2.tgz",
"integrity": "sha512-Pp5GxnxP2blboVZFiM9aWjs4cb8IpW3x2jP3kLOMvIqy0jzNUTuFHkwHtx+zEvh/UcF2F+wmS8G6ebIA0XPXcg==",
"license": "MIT",
"dependencies": {
"@twa-dev/types": "^8.0.1"
},
"peerDependencies": {
"react": "^18.0.0 || ^19.0.0"
}
},
"node_modules/@twa-dev/types": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@twa-dev/types/-/types-8.0.2.tgz",
"integrity": "sha512-ICQ6n4NaUPPzV3/GzflVQS6Nnu5QX2vr9OlOG8ZkFf3rSJXzRKazrLAbZlVhCPPWkIW3MMuELPsE6tByrA49qA==",
"license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
"resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
"integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.20.7",
"@babel/types": "^7.20.7",
"@types/babel__generator": "*",
"@types/babel__template": "*",
"@types/babel__traverse": "*"
}
},
"node_modules/@types/babel__generator": {
"version": "7.27.0",
"resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz",
"integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.0.0"
}
},
"node_modules/@types/babel__template": {
"version": "7.4.4",
"resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz",
"integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.1.0",
"@babel/types": "^7.0.0"
}
},
"node_modules/@types/babel__traverse": {
"version": "7.28.0",
"resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz",
"integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.28.2"
}
},
"node_modules/@types/estree": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz",
"integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
"integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/node": {
"version": "24.10.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.10.2.tgz",
"integrity": "sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.16.0"
}
},
"node_modules/@types/react": {
"version": "19.2.7",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz",
"integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
"version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"@types/react": "^19.2.0"
}
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.6.tgz",
"integrity": "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==",
"license": "MIT"
},
"node_modules/@typescript-eslint/eslint-plugin": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.49.0.tgz",
"integrity": "sha512-JXij0vzIaTtCwu6SxTh8qBc66kmf1xs7pI4UOiMDFVct6q86G0Zs7KRcEoJgY3Cav3x5Tq0MF5jwgpgLqgKG3A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/regexpp": "^4.10.0",
"@typescript-eslint/scope-manager": "8.49.0",
"@typescript-eslint/type-utils": "8.49.0",
"@typescript-eslint/utils": "8.49.0",
"@typescript-eslint/visitor-keys": "8.49.0",
"ignore": "^7.0.0",
"natural-compare": "^1.4.0",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"@typescript-eslint/parser": "^8.49.0",
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": {
"version": "7.0.5",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz",
"integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/@typescript-eslint/parser": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.49.0.tgz",
"integrity": "sha512-N9lBGA9o9aqb1hVMc9hzySbhKibHmB+N3IpoShyV6HyQYRGIhlrO5rQgttypi+yEeKsKI4idxC8Jw6gXKD4THA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.49.0",
"@typescript-eslint/types": "8.49.0",
"@typescript-eslint/typescript-estree": "8.49.0",
"@typescript-eslint/visitor-keys": "8.49.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/project-service": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.49.0.tgz",
"integrity": "sha512-/wJN0/DKkmRUMXjZUXYZpD1NEQzQAAn9QWfGwo+Ai8gnzqH7tvqS7oNVdTjKqOcPyVIdZdyCMoqN66Ia789e7g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/tsconfig-utils": "^8.49.0",
"@typescript-eslint/types": "^8.49.0",
"debug": "^4.3.4"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/scope-manager": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.49.0.tgz",
"integrity": "sha512-npgS3zi+/30KSOkXNs0LQXtsg9ekZ8OISAOLGWA/ZOEn0ZH74Ginfl7foziV8DT+D98WfQ5Kopwqb/PZOaIJGg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.49.0",
"@typescript-eslint/visitor-keys": "8.49.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/tsconfig-utils": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.49.0.tgz",
"integrity": "sha512-8prixNi1/6nawsRYxet4YOhnbW+W9FK/bQPxsGB1D3ZrDzbJ5FXw5XmzxZv82X3B+ZccuSxo/X8q9nQ+mFecWA==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/type-utils": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.49.0.tgz",
"integrity": "sha512-KTExJfQ+svY8I10P4HdxKzWsvtVnsuCifU5MvXrRwoP2KOlNZ9ADNEWWsQTJgMxLzS5VLQKDjkCT/YzgsnqmZg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.49.0",
"@typescript-eslint/typescript-estree": "8.49.0",
"@typescript-eslint/utils": "8.49.0",
"debug": "^4.3.4",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/types": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.49.0.tgz",
"integrity": "sha512-e9k/fneezorUo6WShlQpMxXh8/8wfyc+biu6tnAqA81oWrEic0k21RHzP9uqqpyBBeBKu4T+Bsjy9/b8u7obXQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typescript-eslint/typescript-estree": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.49.0.tgz",
"integrity": "sha512-jrLdRuAbPfPIdYNppHJ/D0wN+wwNfJ32YTAm10eJVsFmrVpXQnDWBn8niCSMlWjvml8jsce5E/O+86IQtTbJWA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/project-service": "8.49.0",
"@typescript-eslint/tsconfig-utils": "8.49.0",
"@typescript-eslint/types": "8.49.0",
"@typescript-eslint/visitor-keys": "8.49.0",
"debug": "^4.3.4",
"minimatch": "^9.0.4",
"semver": "^7.6.0",
"tinyglobby": "^0.2.15",
"ts-api-utils": "^2.1.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/semver": {
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/@typescript-eslint/utils": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.49.0.tgz",
"integrity": "sha512-N3W7rJw7Rw+z1tRsHZbK395TWSYvufBXumYtEGzypgMUthlg0/hmCImeA8hgO2d2G4pd7ftpxxul2J8OdtdaFA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@eslint-community/eslint-utils": "^4.7.0",
"@typescript-eslint/scope-manager": "8.49.0",
"@typescript-eslint/types": "8.49.0",
"@typescript-eslint/typescript-estree": "8.49.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/@typescript-eslint/visitor-keys": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.49.0.tgz",
"integrity": "sha512-LlKaciDe3GmZFphXIc79THF/YYBugZ7FS1pO581E/edlVVNbZKDy93evqmrfQ9/Y4uN0vVhX4iuchq26mK/iiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/types": "8.49.0",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@vitejs/plugin-react": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz",
"integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.28.5",
"@babel/plugin-transform-react-jsx-self": "^7.27.1",
"@babel/plugin-transform-react-jsx-source": "^7.27.1",
"@rolldown/pluginutils": "1.0.0-beta.53",
"@types/babel__core": "^7.20.5",
"react-refresh": "^0.18.0"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"peerDependencies": {
"vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
}
},
"node_modules/acorn": {
"version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
}
},
"node_modules/adler-32": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.3.1.tgz",
"integrity": "sha512-ynZ4w/nUUv5rrsR8UUGoe1VC9hZj6V5hU9Qw1HlMDJGEJw5S7TfTErWTjMys6M7vr0YWcPqs3qAr4ss0nDfP+A==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-convert": "^2.0.1"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/antd": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/antd/-/antd-6.1.0.tgz",
"integrity": "sha512-RIe4W5saaL9SWgvqCcvz6LZta/KwT50B0YF7xYiWVZh0Gqfw2rJAsOMcp202Hxgm+YiyoSp4QqqvexKhuGGarw==",
"license": "MIT",
"dependencies": {
"@ant-design/colors": "^8.0.0",
"@ant-design/cssinjs": "^2.0.1",
"@ant-design/cssinjs-utils": "^2.0.2",
"@ant-design/fast-color": "^3.0.0",
"@ant-design/icons": "^6.1.0",
"@ant-design/react-slick": "~2.0.0",
"@babel/runtime": "^7.28.4",
"@rc-component/cascader": "~1.9.0",
"@rc-component/checkbox": "~1.0.1",
"@rc-component/collapse": "~1.1.2",
"@rc-component/color-picker": "~3.0.3",
"@rc-component/dialog": "~1.5.1",
"@rc-component/drawer": "~1.3.0",
"@rc-component/dropdown": "~1.0.2",
"@rc-component/form": "~1.4.0",
"@rc-component/image": "~1.5.2",
"@rc-component/input": "~1.1.2",
"@rc-component/input-number": "~1.6.2",
"@rc-component/mentions": "~1.6.0",
"@rc-component/menu": "~1.2.0",
"@rc-component/motion": "~1.1.6",
"@rc-component/mutate-observer": "^2.0.1",
"@rc-component/notification": "~1.2.0",
"@rc-component/pagination": "~1.2.0",
"@rc-component/picker": "~1.8.0",
"@rc-component/progress": "~1.0.2",
"@rc-component/qrcode": "~1.1.1",
"@rc-component/rate": "~1.0.1",
"@rc-component/resize-observer": "^1.0.1",
"@rc-component/segmented": "~1.2.3",
"@rc-component/select": "~1.3.2",
"@rc-component/slider": "~1.0.1",
"@rc-component/steps": "~1.2.2",
"@rc-component/switch": "~1.0.3",
"@rc-component/table": "~1.9.0",
"@rc-component/tabs": "~1.7.0",
"@rc-component/textarea": "~1.1.2",
"@rc-component/tooltip": "~1.4.0",
"@rc-component/tour": "~2.2.1",
"@rc-component/tree": "~1.1.0",
"@rc-component/tree-select": "~1.4.0",
"@rc-component/trigger": "^3.7.1",
"@rc-component/upload": "~1.1.0",
"@rc-component/util": "^1.4.0",
"clsx": "^2.1.1",
"dayjs": "^1.11.11",
"scroll-into-view-if-needed": "^3.1.0",
"throttle-debounce": "^5.0.2"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ant-design"
},
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
},
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true,
"license": "Python-2.0"
},
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/attr-accept": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.5.tgz",
"integrity": "sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/axios": {
"version": "1.13.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz",
"integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==",
"license": "MIT",
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
"version": "2.9.6",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.6.tgz",
"integrity": "sha512-v9BVVpOTLB59C9E7aSnmIF8h7qRsFpx+A2nugVMTszEOMcfjlZMsXRm4LF23I3Z9AJxc8ANpIvzbzONoX9VJlg==",
"dev": true,
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
}
},
"node_modules/brace-expansion": {
"version": "1.1.12",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
"integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0",
"concat-map": "0.0.1"
}
},
"node_modules/browserslist": {
"version": "4.28.1",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz",
"integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
"electron-to-chromium": "^1.5.263",
"node-releases": "^2.0.27",
"update-browserslist-db": "^1.2.0"
},
"bin": {
"browserslist": "cli.js"
},
"engines": {
"node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
}
},
"node_modules/call-bind-apply-helpers": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
"integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/callsites": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
"integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001760",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001760.tgz",
"integrity": "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/caniuse-lite"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "CC-BY-4.0"
},
"node_modules/cfb": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.2.tgz",
"integrity": "sha512-KfdUZsSOw19/ObEWasvBP/Ac4reZvAGauZhs6S/gqNhXhI7cKwvlH7ulj+dOEYnca4bm4SGo8C1bTAQvnTjgQA==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"crc-32": "~1.2.0"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/chalk?sponsor=1"
}
},
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
"integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/codepage": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/codepage/-/codepage-1.15.0.tgz",
"integrity": "sha512-3g6NUTPd/YtuuGrhMnOMRjFc+LJw/bnMp3+0r/Wcz3IXUuCosKRJvMphm5+Q+bvTVGcJJuRvVLuYba+WojaFaA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"color-name": "~1.1.4"
},
"engines": {
"node": ">=7.0.0"
}
},
"node_modules/color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true,
"license": "MIT"
},
"node_modules/combined-stream": {
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"license": "MIT",
"dependencies": {
"delayed-stream": "~1.0.0"
},
"engines": {
"node": ">= 0.8"
}
},
"node_modules/compute-scroll-into-view": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz",
"integrity": "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==",
"license": "MIT"
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"dev": true,
"license": "MIT"
},
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
"integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==",
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz",
"integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==",
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/express"
}
},
"node_modules/crc-32": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.2.tgz",
"integrity": "sha512-ROmzCKrTnOwybPcJApAA6WBWij23HVfGVNKqqrZpuyZOHqK2CwHSvpGuyt/UNNvaIjEd8X5IFGp4Mh+Ie1IHJQ==",
"license": "Apache-2.0",
"bin": {
"crc32": "bin/crc32.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/cross-spawn": {
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
"shebang-command": "^2.0.0",
"which": "^2.0.1"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/css-box-model": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/css-box-model/-/css-box-model-1.2.1.tgz",
"integrity": "sha512-a7Vr4Q/kd/aw96bnJG332W9V9LkJO69JRcaCYDUqjp6/z0w6VcZjgAcTbgFxEPfBgdnAwlh3iwu+hLopa+flJw==",
"license": "MIT",
"dependencies": {
"tiny-invariant": "^1.0.6"
}
},
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
"integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
"license": "MIT"
},
"node_modules/dayjs": {
"version": "1.11.19",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz",
"integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==",
"license": "MIT",
"peer": true
},
"node_modules/debug": {
"version": "4.4.3",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz",
"integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
"integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==",
"dev": true,
"license": "MIT"
},
"node_modules/delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
"license": "MIT",
"engines": {
"node": ">=0.4.0"
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
"integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.1",
"es-errors": "^1.3.0",
"gopd": "^1.2.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/electron-to-chromium": {
"version": "1.5.267",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
"integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==",
"dev": true,
"license": "ISC"
},
"node_modules/es-define-property": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
"integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-errors": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-object-atoms": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz",
"integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/es-set-tostringtag": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz",
"integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==",
"license": "MIT",
"dependencies": {
"es-errors": "^1.3.0",
"get-intrinsic": "^1.2.6",
"has-tostringtag": "^1.0.2",
"hasown": "^2.0.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/esbuild": {
"version": "0.25.12",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.12",
"@esbuild/android-arm": "0.25.12",
"@esbuild/android-arm64": "0.25.12",
"@esbuild/android-x64": "0.25.12",
"@esbuild/darwin-arm64": "0.25.12",
"@esbuild/darwin-x64": "0.25.12",
"@esbuild/freebsd-arm64": "0.25.12",
"@esbuild/freebsd-x64": "0.25.12",
"@esbuild/linux-arm": "0.25.12",
"@esbuild/linux-arm64": "0.25.12",
"@esbuild/linux-ia32": "0.25.12",
"@esbuild/linux-loong64": "0.25.12",
"@esbuild/linux-mips64el": "0.25.12",
"@esbuild/linux-ppc64": "0.25.12",
"@esbuild/linux-riscv64": "0.25.12",
"@esbuild/linux-s390x": "0.25.12",
"@esbuild/linux-x64": "0.25.12",
"@esbuild/netbsd-arm64": "0.25.12",
"@esbuild/netbsd-x64": "0.25.12",
"@esbuild/openbsd-arm64": "0.25.12",
"@esbuild/openbsd-x64": "0.25.12",
"@esbuild/openharmony-arm64": "0.25.12",
"@esbuild/sunos-x64": "0.25.12",
"@esbuild/win32-arm64": "0.25.12",
"@esbuild/win32-ia32": "0.25.12",
"@esbuild/win32-x64": "0.25.12"
}
},
"node_modules/escalade": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
"integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/eslint": {
"version": "9.39.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.1.tgz",
"integrity": "sha512-BhHmn2yNOFA9H9JmmIVKJmd288g9hrVRDkdoIgRCRuSySRUHH7r/DI6aAXW9T1WwUuY3DFgrcaqB+deURBLR5g==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.21.1",
"@eslint/config-helpers": "^0.4.2",
"@eslint/core": "^0.17.0",
"@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.39.1",
"@eslint/plugin-kit": "^0.4.1",
"@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2",
"@types/estree": "^1.0.6",
"ajv": "^6.12.4",
"chalk": "^4.0.0",
"cross-spawn": "^7.0.6",
"debug": "^4.3.2",
"escape-string-regexp": "^4.0.0",
"eslint-scope": "^8.4.0",
"eslint-visitor-keys": "^4.2.1",
"espree": "^10.4.0",
"esquery": "^1.5.0",
"esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3",
"file-entry-cache": "^8.0.0",
"find-up": "^5.0.0",
"glob-parent": "^6.0.2",
"ignore": "^5.2.0",
"imurmurhash": "^0.1.4",
"is-glob": "^4.0.0",
"json-stable-stringify-without-jsonify": "^1.0.1",
"lodash.merge": "^4.6.2",
"minimatch": "^3.1.2",
"natural-compare": "^1.4.0",
"optionator": "^0.9.3"
},
"bin": {
"eslint": "bin/eslint.js"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://eslint.org/donate"
},
"peerDependencies": {
"jiti": "*"
},
"peerDependenciesMeta": {
"jiti": {
"optional": true
}
}
},
"node_modules/eslint-plugin-react-hooks": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-7.0.1.tgz",
"integrity": "sha512-O0d0m04evaNzEPoSW+59Mezf8Qt0InfgGIBJnpC0h3NH/WjUAR7BIKUfysC6todmtiZ/A0oUVS8Gce0WhBrHsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.24.4",
"@babel/parser": "^7.24.4",
"hermes-parser": "^0.25.1",
"zod": "^3.25.0 || ^4.0.0",
"zod-validation-error": "^3.5.0 || ^4.0.0"
},
"engines": {
"node": ">=18"
},
"peerDependencies": {
"eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0"
}
},
"node_modules/eslint-plugin-react-refresh": {
"version": "0.4.24",
"resolved": "https://registry.npmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz",
"integrity": "sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"eslint": ">=8.40"
}
},
"node_modules/eslint-scope": {
"version": "8.4.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
"integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
"estraverse": "^5.2.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint-visitor-keys": {
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true,
"license": "Apache-2.0",
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/espree": {
"version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
"integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"acorn": "^8.15.0",
"acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.2.1"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://opencollective.com/eslint"
}
},
"node_modules/esquery": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz",
"integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"estraverse": "^5.1.0"
},
"engines": {
"node": ">=0.10"
}
},
"node_modules/esrecurse": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz",
"integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"estraverse": "^5.2.0"
},
"engines": {
"node": ">=4.0"
}
},
"node_modules/estraverse": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
"integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
}
},
"node_modules/esutils": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz",
"integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true,
"license": "MIT"
},
"node_modules/fast-levenshtein": {
"version": "2.0.6",
"resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz",
"integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==",
"dev": true,
"license": "MIT"
},
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/file-entry-cache": {
"version": "8.0.0",
"resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz",
"integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"flat-cache": "^4.0.0"
},
"engines": {
"node": ">=16.0.0"
}
},
"node_modules/file-selector": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz",
"integrity": "sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==",
"license": "MIT",
"dependencies": {
"tslib": "^2.7.0"
},
"engines": {
"node": ">= 12"
}
},
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
"integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==",
"dev": true,
"license": "MIT",
"dependencies": {
"locate-path": "^6.0.0",
"path-exists": "^4.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/flat-cache": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz",
"integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==",
"dev": true,
"license": "MIT",
"dependencies": {
"flatted": "^3.2.9",
"keyv": "^4.5.4"
},
"engines": {
"node": ">=16"
}
},
"node_modules/flatted": {
"version": "3.3.3",
"resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz",
"integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==",
"dev": true,
"license": "ISC"
},
"node_modules/follow-redirects": {
"version": "1.15.11",
"resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz",
"integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/RubenVerborgh"
}
],
"license": "MIT",
"engines": {
"node": ">=4.0"
},
"peerDependenciesMeta": {
"debug": {
"optional": true
}
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
"node": ">= 6"
}
},
"node_modules/frac": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz",
"integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
"integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/get-intrinsic": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz",
"integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==",
"license": "MIT",
"dependencies": {
"call-bind-apply-helpers": "^1.0.2",
"es-define-property": "^1.0.1",
"es-errors": "^1.3.0",
"es-object-atoms": "^1.1.1",
"function-bind": "^1.1.2",
"get-proto": "^1.0.1",
"gopd": "^1.2.0",
"has-symbols": "^1.1.0",
"hasown": "^2.0.2",
"math-intrinsics": "^1.1.0"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/get-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz",
"integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==",
"license": "MIT",
"dependencies": {
"dunder-proto": "^1.0.1",
"es-object-atoms": "^1.0.0"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
"integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==",
"dev": true,
"license": "ISC",
"dependencies": {
"is-glob": "^4.0.3"
},
"engines": {
"node": ">=10.13.0"
}
},
"node_modules/globals": {
"version": "16.5.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-16.5.0.tgz",
"integrity": "sha512-c/c15i26VrJ4IRt5Z89DnIzCGDn9EcebibhAOjw5ibqEHsE1wLUgkPn9RDmNcUKyU87GeaL633nyJ+pplFR2ZQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
"integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/has-symbols": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz",
"integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/has-tostringtag": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz",
"integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==",
"license": "MIT",
"dependencies": {
"has-symbols": "^1.0.3"
},
"engines": {
"node": ">= 0.4"
},
"funding": {
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/hasown": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
"license": "MIT",
"dependencies": {
"function-bind": "^1.1.2"
},
"engines": {
"node": ">= 0.4"
}
},
"node_modules/hermes-estree": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/hermes-estree/-/hermes-estree-0.25.1.tgz",
"integrity": "sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==",
"dev": true,
"license": "MIT"
},
"node_modules/hermes-parser": {
"version": "0.25.1",
"resolved": "https://registry.npmjs.org/hermes-parser/-/hermes-parser-0.25.1.tgz",
"integrity": "sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==",
"dev": true,
"license": "MIT",
"dependencies": {
"hermes-estree": "0.25.1"
}
},
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
"integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 4"
}
},
"node_modules/immer": {
"version": "11.1.3",
"resolved": "https://registry.npmjs.org/immer/-/immer-11.1.3.tgz",
"integrity": "sha512-6jQTc5z0KJFtr1UgFpIL3N9XSC3saRaI9PwWtzM2pSqkNGtiNkYY2OSwkOGDK2XcTRcLb1pi/aNkKZz0nxVH4Q==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/immer"
}
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
"integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"parent-module": "^1.0.0",
"resolve-from": "^4.0.0"
},
"engines": {
"node": ">=6"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.8.19"
}
},
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
"dev": true,
"license": "MIT",
"dependencies": {
"is-extglob": "^2.1.1"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-mobile": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/is-mobile/-/is-mobile-5.0.0.tgz",
"integrity": "sha512-Tz/yndySvLAEXh+Uk8liFCxOwVH6YutuR74utvOcu7I9Di+DwM0mtdPVZNaVvvBUM2OXxne/NhOs1zAO7riusQ==",
"license": "MIT"
},
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
"dev": true,
"license": "ISC"
},
"node_modules/js-tokens": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
},
"bin": {
"js-yaml": "bin/js-yaml.js"
}
},
"node_modules/jsesc": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz",
"integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==",
"dev": true,
"license": "MIT",
"bin": {
"jsesc": "bin/jsesc"
},
"engines": {
"node": ">=6"
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
"integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==",
"dev": true,
"license": "MIT"
},
"node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/json-stable-stringify-without-jsonify": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz",
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true,
"license": "MIT"
},
"node_modules/json2mq": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/json2mq/-/json2mq-0.2.0.tgz",
"integrity": "sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==",
"license": "MIT",
"dependencies": {
"string-convert": "^0.2.0"
}
},
"node_modules/json5": {
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"dev": true,
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
},
"engines": {
"node": ">=6"
}
},
"node_modules/keyv": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
"integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==",
"dev": true,
"license": "MIT",
"dependencies": {
"json-buffer": "3.0.1"
}
},
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
"integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"prelude-ls": "^1.2.1",
"type-check": "~0.4.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",
"integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-locate": "^5.0.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/lodash.merge": {
"version": "4.6.2",
"resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
"integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==",
"dev": true,
"license": "MIT"
},
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lru-cache": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz",
"integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==",
"dev": true,
"license": "ISC",
"dependencies": {
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "0.563.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.563.0.tgz",
"integrity": "sha512-8dXPB2GI4dI8jV4MgUDGBeLdGk8ekfqVZ0BdLcrRzocGgG75ltNEmWS+gE7uokKF/0oSUuczNDT+g9hFJ23FkA==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
"integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==",
"license": "MIT",
"engines": {
"node": ">= 0.4"
}
},
"node_modules/mime-db": {
"version": "1.52.0",
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
"license": "MIT",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/mime-types": {
"version": "2.1.35",
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
},
"engines": {
"node": ">= 0.6"
}
},
"node_modules/minimatch": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
"integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^1.1.7"
},
"engines": {
"node": "*"
}
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"dev": true,
"license": "MIT"
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
"dev": true,
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"bin": {
"nanoid": "bin/nanoid.cjs"
},
"engines": {
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
"node_modules/natural-compare": {
"version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true,
"license": "MIT"
},
"node_modules/node-releases": {
"version": "2.0.27",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
"integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==",
"dev": true,
"license": "MIT"
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
"integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
"integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==",
"dev": true,
"license": "MIT",
"dependencies": {
"deep-is": "^0.1.3",
"fast-levenshtein": "^2.0.6",
"levn": "^0.4.1",
"prelude-ls": "^1.2.1",
"type-check": "^0.4.0",
"word-wrap": "^1.2.5"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
"integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"yocto-queue": "^0.1.0"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-locate": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz",
"integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"p-limit": "^3.0.2"
},
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
"integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==",
"dev": true,
"license": "MIT",
"dependencies": {
"callsites": "^3.0.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/path-exists": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
"integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/path-key": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
"node_modules/picomatch": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"dev": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz",
"integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/postcss/"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/postcss"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"nanoid": "^3.3.11",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
"engines": {
"node": "^10 || ^12 || >=14"
}
},
"node_modules/prelude-ls": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
"integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/prop-types": {
"version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
"react-is": "^16.13.1"
}
},
"node_modules/prop-types/node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/proxy-from-env": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
"license": "MIT"
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/qrcode.react": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/qrcode.react/-/qrcode.react-4.2.0.tgz",
"integrity": "sha512-QpgqWi8rD9DsS9EP3z7BT+5lY5SFhsqGjpgW5DY/i3mK4M9DTBNz3ErMi8BWYEfI3L0d8GIbGmcdFAS1uIRGjA==",
"license": "ISC",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/raf-schd": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/raf-schd/-/raf-schd-4.0.3.tgz",
"integrity": "sha512-tQkJl2GRWh83ui2DiPTJz9wEiMN20syf+5oKfB03yYP7ioZcJwsIK8FjrtLwH1m7C7e+Tt2yYBlrOpdT+dyeIQ==",
"license": "MIT"
},
"node_modules/react": {
"version": "19.2.1",
"resolved": "https://registry.npmjs.org/react/-/react-19.2.1.tgz",
"integrity": "sha512-DGrYcCWK7tvYMnWh79yrPHt+vdx9tY+1gPZa7nJQtO/p8bLTDaHp4dzwEhQB7pZ4Xe3ok4XKuEPrVuc+wlpkmw==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-dom": {
"version": "19.2.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.1.tgz",
"integrity": "sha512-ibrK8llX2a4eOskq1mXKu/TGZj9qzomO+sNfO98M6d9zIPOEhlBkMkBUBLd1vgS0gQsLDBzA+8jJBVXDnfHmJg==",
"license": "MIT",
"peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
"peerDependencies": {
"react": "^19.2.1"
}
},
"node_modules/react-dropzone": {
"version": "14.3.8",
"resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.3.8.tgz",
"integrity": "sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==",
"license": "MIT",
"dependencies": {
"attr-accept": "^2.2.4",
"file-selector": "^2.1.0",
"prop-types": "^15.8.1"
},
"engines": {
"node": ">= 10.13"
},
"peerDependencies": {
"react": ">= 16.8 || 18.0.0"
}
},
"node_modules/react-is": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
"license": "MIT"
},
"node_modules/react-redux": {
"version": "9.2.0",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-9.2.0.tgz",
"integrity": "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g==",
"license": "MIT",
"dependencies": {
"@types/use-sync-external-store": "^0.0.6",
"use-sync-external-store": "^1.4.0"
},
"peerDependencies": {
"@types/react": "^18.2.25 || ^19",
"react": "^18.0 || ^19",
"redux": "^5.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/react-refresh": {
"version": "0.18.0",
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz",
"integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react-router": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-7.13.0.tgz",
"integrity": "sha512-PZgus8ETambRT17BUm/LL8lX3Of+oiLaPuVTRH3l1eLvSPpKO3AvhAEb5N7ihAFZQrYDqkvvWfFh9p0z9VsjLw==",
"license": "MIT",
"dependencies": {
"cookie": "^1.0.1",
"set-cookie-parser": "^2.6.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
},
"peerDependenciesMeta": {
"react-dom": {
"optional": true
}
}
},
"node_modules/react-router-dom": {
"version": "7.13.0",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.13.0.tgz",
"integrity": "sha512-5CO/l5Yahi2SKC6rGZ+HDEjpjkGaG/ncEP7eWFTvFxbHP8yeeI0PxTDjimtpXYlR3b3i9/WIL4VJttPrESIf2g==",
"license": "MIT",
"dependencies": {
"react-router": "7.13.0"
},
"engines": {
"node": ">=20.0.0"
},
"peerDependencies": {
"react": ">=18",
"react-dom": ">=18"
}
},
"node_modules/redux": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT",
"peer": true
},
"node_modules/resolve-from": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz",
"integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/rollup": {
"version": "4.53.3",
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz",
"integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "1.0.8"
},
"bin": {
"rollup": "dist/bin/rollup"
},
"engines": {
"node": ">=18.0.0",
"npm": ">=8.0.0"
},
"optionalDependencies": {
"@rollup/rollup-android-arm-eabi": "4.53.3",
"@rollup/rollup-android-arm64": "4.53.3",
"@rollup/rollup-darwin-arm64": "4.53.3",
"@rollup/rollup-darwin-x64": "4.53.3",
"@rollup/rollup-freebsd-arm64": "4.53.3",
"@rollup/rollup-freebsd-x64": "4.53.3",
"@rollup/rollup-linux-arm-gnueabihf": "4.53.3",
"@rollup/rollup-linux-arm-musleabihf": "4.53.3",
"@rollup/rollup-linux-arm64-gnu": "4.53.3",
"@rollup/rollup-linux-arm64-musl": "4.53.3",
"@rollup/rollup-linux-loong64-gnu": "4.53.3",
"@rollup/rollup-linux-ppc64-gnu": "4.53.3",
"@rollup/rollup-linux-riscv64-gnu": "4.53.3",
"@rollup/rollup-linux-riscv64-musl": "4.53.3",
"@rollup/rollup-linux-s390x-gnu": "4.53.3",
"@rollup/rollup-linux-x64-gnu": "4.53.3",
"@rollup/rollup-linux-x64-musl": "4.53.3",
"@rollup/rollup-openharmony-arm64": "4.53.3",
"@rollup/rollup-win32-arm64-msvc": "4.53.3",
"@rollup/rollup-win32-ia32-msvc": "4.53.3",
"@rollup/rollup-win32-x64-gnu": "4.53.3",
"@rollup/rollup-win32-x64-msvc": "4.53.3",
"fsevents": "~2.3.2"
}
},
"node_modules/scheduler": {
"version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT"
},
"node_modules/scroll-into-view-if-needed": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz",
"integrity": "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==",
"license": "MIT",
"dependencies": {
"compute-scroll-into-view": "^3.0.2"
}
},
"node_modules/semver": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/set-cookie-parser": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz",
"integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==",
"license": "MIT"
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
"dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/shebang-regex": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/ssf": {
"version": "0.11.2",
"resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz",
"integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==",
"license": "Apache-2.0",
"dependencies": {
"frac": "~1.1.2"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/string-convert": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/string-convert/-/string-convert-0.2.1.tgz",
"integrity": "sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==",
"license": "MIT"
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/stylis": {
"version": "4.3.6",
"resolved": "https://registry.npmjs.org/stylis/-/stylis-4.3.6.tgz",
"integrity": "sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==",
"license": "MIT"
},
"node_modules/supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/throttle-debounce": {
"version": "5.0.2",
"resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
"integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==",
"license": "MIT",
"engines": {
"node": ">=12.22"
}
},
"node_modules/tiny-invariant": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz",
"integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==",
"license": "MIT"
},
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/ts-api-utils": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz",
"integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.12"
},
"peerDependencies": {
"typescript": ">=4.8.4"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
"integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==",
"dev": true,
"license": "MIT",
"dependencies": {
"prelude-ls": "^1.2.1"
},
"engines": {
"node": ">= 0.8.0"
}
},
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
},
"engines": {
"node": ">=14.17"
}
},
"node_modules/typescript-eslint": {
"version": "8.49.0",
"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.49.0.tgz",
"integrity": "sha512-zRSVH1WXD0uXczCXw+nsdjGPUdx4dfrs5VQoHnUWmv1U3oNlAKv4FUNdLDhVUg+gYn+a5hUESqch//Rv5wVhrg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@typescript-eslint/eslint-plugin": "8.49.0",
"@typescript-eslint/parser": "8.49.0",
"@typescript-eslint/typescript-estree": "8.49.0",
"@typescript-eslint/utils": "8.49.0"
},
"engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/typescript-eslint"
},
"peerDependencies": {
"eslint": "^8.57.0 || ^9.0.0",
"typescript": ">=4.8.4 <6.0.0"
}
},
"node_modules/undici-types": {
"version": "7.16.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
"integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==",
"dev": true,
"license": "MIT"
},
"node_modules/update-browserslist-db": {
"version": "1.2.2",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.2.tgz",
"integrity": "sha512-E85pfNzMQ9jpKkA7+TJAi4TJN+tBCuWh5rUcS/sv6cFi+1q9LYDwDI5dpUL0u/73EElyQ8d3TEaeW4sPedBqYA==",
"dev": true,
"funding": [
{
"type": "opencollective",
"url": "https://opencollective.com/browserslist"
},
{
"type": "tidelift",
"url": "https://tidelift.com/funding/github/npm/browserslist"
},
{
"type": "github",
"url": "https://github.com/sponsors/ai"
}
],
"license": "MIT",
"dependencies": {
"escalade": "^3.2.0",
"picocolors": "^1.1.1"
},
"bin": {
"update-browserslist-db": "cli.js"
},
"peerDependencies": {
"browserslist": ">= 4.21.0"
}
},
"node_modules/uri-js": {
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"punycode": "^2.1.0"
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/vite": {
"version": "7.2.7",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.2.7.tgz",
"integrity": "sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.5.0",
"picomatch": "^4.0.3",
"postcss": "^8.5.6",
"rollup": "^4.43.0",
"tinyglobby": "^0.2.15"
},
"bin": {
"vite": "bin/vite.js"
},
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"funding": {
"url": "https://github.com/vitejs/vite?sponsor=1"
},
"optionalDependencies": {
"fsevents": "~2.3.3"
},
"peerDependencies": {
"@types/node": "^20.19.0 || >=22.12.0",
"jiti": ">=1.21.0",
"less": "^4.0.0",
"lightningcss": "^1.21.0",
"sass": "^1.70.0",
"sass-embedded": "^1.70.0",
"stylus": ">=0.54.8",
"sugarss": "^5.0.0",
"terser": "^5.16.0",
"tsx": "^4.8.1",
"yaml": "^2.4.2"
},
"peerDependenciesMeta": {
"@types/node": {
"optional": true
},
"jiti": {
"optional": true
},
"less": {
"optional": true
},
"lightningcss": {
"optional": true
},
"sass": {
"optional": true
},
"sass-embedded": {
"optional": true
},
"stylus": {
"optional": true
},
"sugarss": {
"optional": true
},
"terser": {
"optional": true
},
"tsx": {
"optional": true
},
"yaml": {
"optional": true
}
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
"dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
},
"bin": {
"node-which": "bin/node-which"
},
"engines": {
"node": ">= 8"
}
},
"node_modules/wmf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz",
"integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word": {
"version": "0.3.0",
"resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz",
"integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==",
"license": "Apache-2.0",
"engines": {
"node": ">=0.8"
}
},
"node_modules/word-wrap": {
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz",
"integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/xlsx": {
"version": "0.18.5",
"resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.18.5.tgz",
"integrity": "sha512-dmg3LCjBPHZnQp5/F/+nnTa+miPJxUXB6vtk42YjBBKayDNagxGEeIdWApkYPOf3Z3pm3k62Knjzp7lMeTEtFQ==",
"license": "Apache-2.0",
"dependencies": {
"adler-32": "~1.3.0",
"cfb": "~1.2.1",
"codepage": "~1.15.0",
"crc-32": "~1.2.1",
"ssf": "~0.11.2",
"wmf": "~1.0.1",
"word": "~0.3.0"
},
"bin": {
"xlsx": "bin/xlsx.njs"
},
"engines": {
"node": ">=0.8"
}
},
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"dev": true,
"license": "ISC"
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
"integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/zod": {
"version": "4.1.13",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.1.13.tgz",
"integrity": "sha512-AvvthqfqrAhNH9dnfmrfKzX5upOdjUVJYFqNSlkmGf64gRaTzlPwz99IHYnVs28qYAybvAlBV+H7pn0saFY4Ig==",
"dev": true,
"license": "MIT",
"peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
},
"node_modules/zod-validation-error": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/zod-validation-error/-/zod-validation-error-4.0.2.tgz",
"integrity": "sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18.0.0"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
}
},
"node_modules/zustand": {
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.9.tgz",
"integrity": "sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==",
"license": "MIT",
"engines": {
"node": ">=12.20.0"
},
"peerDependencies": {
"@types/react": ">=18.0.0",
"immer": ">=9.0.6",
"react": ">=18.0.0",
"use-sync-external-store": ">=1.2.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"immer": {
"optional": true
},
"react": {
"optional": true
},
"use-sync-external-store": {
"optional": true
}
}
}
}
}
```
# ===================================================================
# Файл: package.json
# ===================================================================
```
{
"name": "rmser-view",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@hello-pangea/dnd": "^18.0.1",
"@tanstack/react-query": "^5.90.12",
"@twa-dev/sdk": "^8.0.2",
"antd": "^6.1.0",
"axios": "^1.13.2",
"clsx": "^2.1.1",
"immer": "^11.1.3",
"lucide-react": "^0.563.0",
"qrcode.react": "^4.2.0",
"react": "^19.2.0",
"react-dom": "^19.2.0",
"react-dropzone": "^14.3.8",
"react-router-dom": "^7.10.1",
"xlsx": "^0.18.5",
"zustand": "^5.0.9"
},
"devDependencies": {
"@eslint/js": "^9.39.1",
"@types/node": "^24.10.1",
"@types/react": "^19.2.5",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^5.1.1",
"eslint": "^9.39.1",
"eslint-plugin-react-hooks": "^7.0.1",
"eslint-plugin-react-refresh": "^0.4.24",
"globals": "^16.5.0",
"typescript": "~5.9.3",
"typescript-eslint": "^8.46.4",
"vite": "^7.2.4"
}
}
```
# ===================================================================
# Файл: src/App.tsx
# ===================================================================
```
import { useEffect, useState } from "react";
import { BrowserRouter, Routes, Route } from "react-router-dom";
import { Result, Button } from "antd";
import { Providers } from "@/app/Providers";
import { usePlatform } from "@/shared/hooks/usePlatform";
import { UNAUTHORIZED_EVENT, MAINTENANCE_EVENT } from "@/shared/api";
import { MobileBrowserStub } from "@/modules/desktop/pages/auth/MobileBrowserStub";
import { DesktopAuthScreen } from "@/modules/desktop/pages/auth/DesktopAuthScreen";
import { DesktopLayout } from "@/modules/desktop/layouts/DesktopLayout/DesktopLayout";
import { InvoicesDashboard } from "@/modules/desktop/pages/dashboard/InvoicesDashboard";
import { MobileApp } from "@/modules/mobile/MobileApp";
import MaintenancePage from "@/modules/mobile/pages/MaintenancePage";
const NotInTelegramScreen = () => (
<div
style={{
height: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "#fff",
padding: 20,
}}
>
<Result
status="warning"
title="Доступ ограничен"
subTitle="Пожалуйста, откройте это приложение через официального Telegram бота @RmserBot."
extra={
<Button type="primary" href="https://t.me/RmserBot" target="_blank">
Перейти в бота
</Button>
}
/>
</div>
);
const AppContent = () => {
const [isUnauthorized, setIsUnauthorized] = useState(false);
const [isMaintenance, setIsMaintenance] = useState(false);
const tg = window.Telegram?.WebApp;
const platform = usePlatform();
const isInTelegram = !!tg?.initData;
const isDesktopRoute = window.location.pathname.startsWith("/web");
useEffect(() => {
const handleUnauthorized = () => setIsUnauthorized(true);
const handleMaintenance = () => setIsMaintenance(true);
window.addEventListener(UNAUTHORIZED_EVENT, handleUnauthorized);
window.addEventListener(MAINTENANCE_EVENT, handleMaintenance);
if (tg) tg.expand();
return () => {
window.removeEventListener(UNAUTHORIZED_EVENT, handleUnauthorized);
window.removeEventListener(MAINTENANCE_EVENT, handleMaintenance);
};
}, [tg]);
if (!isInTelegram && !isDesktopRoute) {
return <NotInTelegramScreen />;
}
if (isDesktopRoute && platform === "MobileBrowser") {
return <MobileBrowserStub />;
}
if (isUnauthorized) {
return (
<div
style={{
height: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<Result
status="403"
title="Ошибка доступа"
subTitle="Не удалось подтвердить вашу личность."
/>
</div>
);
}
if (isMaintenance) {
return <MaintenancePage />;
}
return (
<Routes>
<Route path="/" element={<MobileApp />} />
<Route path="/web" element={<DesktopAuthScreen />} />
<Route path="/web" element={<DesktopLayout />}>
<Route path="dashboard" element={<InvoicesDashboard />} />
</Route>
</Routes>
);
};
function App() {
return (
<Providers>
<BrowserRouter>
<AppContent />
</BrowserRouter>
</Providers>
);
}
export default App;
```
# ===================================================================
# Файл: src/app/Providers.tsx
# ===================================================================
```
import React from "react";
import { useEffect } from "react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import WebApp from "@twa-dev/sdk";
// Настройка клиента React Query
const queryClient = new QueryClient({
defaultOptions: {
queries: {
refetchOnWindowFocus: false,
retry: 1,
},
},
});
interface ProvidersProps {
children: React.ReactNode;
}
export const Providers: React.FC<ProvidersProps> = ({ children }) => {
useEffect(() => {
WebApp.ready();
WebApp.expand();
WebApp.setHeaderColor("secondary_bg_color");
}, []);
return (
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
);
};
```
# ===================================================================
# Файл: src/main.tsx
# ===================================================================
```
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.tsx'
// Если есть глобальные стили, они подключаются тут.
// Если файла index.css нет, убери эту строку.
// import './index.css'
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)
```
# ===================================================================
# Файл: src/modules/desktop/DesktopApp.tsx
# ===================================================================
```
import React from "react";
import { Routes, Route, Navigate, Outlet } from "react-router-dom";
import { SessionGuard } from "./components/SessionGuard";
import { DesktopLayout } from "./layouts/DesktopLayout/DesktopLayout";
import { DesktopAuthScreen } from "./pages/auth/DesktopAuthScreen";
import { InvoicesDashboard } from "./pages/dashboard/InvoicesDashboard";
import { useAuthStore } from "@/shared/stores/authStore";
const ProtectedRoute = () => {
const { isAuthenticated } = useAuthStore();
if (!isAuthenticated) return <Navigate to="/web" replace />;
return <Outlet />;
};
export const DesktopApp: React.FC = () => {
return (
<SessionGuard>
<Routes>
<Route path="/" element={<DesktopAuthScreen />} />
<Route element={<ProtectedRoute />}>
<Route path="/dashboard" element={<DesktopLayout />}>
<Route index element={<InvoicesDashboard />} />
</Route>
</Route>
{/* Redirect unknown routes back to login or dashboard */}
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
</SessionGuard>
);
};
```
# ===================================================================
# Файл: src/modules/desktop/components/SessionGuard.tsx
# ===================================================================
```
import { useEffect, useState } from "react";
import { useNavigate } from "react-router-dom";
import { Spin } from "antd";
import { api } from "@/shared/api";
import { useAuthStore } from "@/shared/stores/authStore";
interface SessionGuardProps {
children: React.ReactNode;
}
/**
* Компонент для проверки сессии при загрузке десктопного приложения.
* Если пользователь авторизован через куки - редиректит на dashboard.
* В Telegram Mini App проверка не требуется - сразу отдаём children.
*/
export function SessionGuard({ children }: SessionGuardProps) {
// Проверяем, находимся ли мы в Telegram - если да, пропускаем без проверки
const isTelegram = !!window.Telegram?.WebApp?.initData;
const { isAuthenticated } = useAuthStore();
const [isChecking, setIsChecking] = useState(true);
const navigate = useNavigate();
useEffect(() => {
// В Telegram проверка сессии не требуется
if (isTelegram) {
return;
}
const checkSession = async () => {
// Если пользователь уже авторизован в store - пропускаем
if (isAuthenticated) {
setIsChecking(false);
return;
}
try {
// Проверяем сессию через API
const user = await api.getMe();
// Если успех - сохраняем в store и редиректим
useAuthStore.getState().setUser(user);
useAuthStore.getState().setToken("cookie");
navigate("/web/dashboard", { replace: true });
} catch {
// 401 - нет сессии, остаёмся на странице входа
// Другие ошибки - тоже остаёмся
} finally {
setIsChecking(false);
}
};
checkSession();
}, [isTelegram, isAuthenticated, navigate]);
// В Telegram сразу пропускаем
if (isTelegram) {
return <>{children}</>;
}
// Если проверяем - показываем лоадер
if (isChecking) {
return (
<div
style={{
height: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "#f5f5f5",
}}
>
<Spin size="large" tip="Проверка сессии..." />
</div>
);
}
return <>{children}</>;
}
```
# ===================================================================
# Файл: src/modules/desktop/layouts/DesktopLayout/DesktopHeader.tsx
# ===================================================================
```
import React, { useEffect } from "react";
import { Layout, Space, Avatar, Dropdown, Select } from "antd";
import { UserOutlined, LogoutOutlined } from "@ant-design/icons";
import { useAuthStore } from "@/shared/stores/authStore";
import { useServerStore } from "@/shared/stores/serverStore";
import { api } from "@/shared/api";
const { Header } = Layout;
/**
* Header для десктопной версии
* Содержит логотип, заглушку выбора сервера и аватар пользователя
*/
export const DesktopHeader: React.FC = () => {
const { user, logout } = useAuthStore();
const { servers, activeServer, isLoading, fetchServers, setActiveServer } =
useServerStore();
// Загружаем список серверов при маунте компонента
useEffect(() => {
fetchServers();
}, [fetchServers]);
const handleLogout = async () => {
await api.logout();
logout();
window.location.href = "/web";
};
const handleServerChange = (serverId: string) => {
setActiveServer(serverId);
};
const userMenuItems = [
{
key: "logout",
label: "Выйти",
icon: <LogoutOutlined />,
onClick: handleLogout,
},
];
return (
<Header
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
backgroundColor: "#ffffff",
boxShadow: "0 2px 8px rgba(0, 0, 0, 0.06)",
padding: "0 24px",
height: "64px",
position: "fixed",
top: 0,
left: 0,
right: 0,
zIndex: 1000,
}}
>
<div style={{ display: "flex", alignItems: "center", gap: "24px" }}>
{/* Логотип */}
<div
style={{
fontSize: "20px",
fontWeight: "bold",
color: "#1890ff",
display: "flex",
alignItems: "center",
gap: "8px",
}}
>
<span>RMSer</span>
</div>
{/* Выбор сервера */}
<Select
placeholder="Выберите сервер"
value={activeServer?.id || undefined}
onChange={handleServerChange}
loading={isLoading}
disabled={isLoading}
style={{ minWidth: "200px" }}
options={servers.map((server) => ({
label: server.name,
value: server.id,
}))}
/>
</div>
{/* Аватар пользователя */}
<Space>
<Dropdown menu={{ items: userMenuItems }} placement="bottomRight">
<div
style={{
cursor: "pointer",
display: "flex",
alignItems: "center",
gap: "8px",
}}
>
<Avatar size="default" icon={<UserOutlined />} />
<span style={{ color: "#262626" }}>
{user?.username || "Пользователь"}
</span>
</div>
</Dropdown>
</Space>
</Header>
);
};
```
# ===================================================================
# Файл: src/modules/desktop/layouts/DesktopLayout/DesktopLayout.tsx
# ===================================================================
```
import React from 'react';
import { Layout } from 'antd';
import { Outlet } from 'react-router-dom';
import { DesktopHeader } from './DesktopHeader.tsx';
const { Content } = Layout;
/**
* Основной layout для десктопной версии
* Использует Ant Design Layout с фиксированным Header
*/
export const DesktopLayout: React.FC = () => {
return (
<Layout style={{ minHeight: '100vh', backgroundColor: '#f0f2f5' }}>
<DesktopHeader />
<Content style={{ padding: '24px' }}>
<div
style={{
minHeight: 'calc(100vh - 64px - 48px)',
backgroundColor: '#ffffff',
borderRadius: '8px',
padding: '24px',
}}
>
<Outlet />
</div>
</Content>
</Layout>
);
};
```
# ===================================================================
# Файл: src/modules/desktop/pages/auth/DesktopAuthScreen.tsx
# ===================================================================
```
import React, { useEffect, useState, useCallback } from "react";
import { Card, Typography, Spin, Alert, message, Button } from "antd";
import { QRCodeSVG } from "qrcode.react";
import { SendOutlined, ReloadOutlined } from "@ant-design/icons";
import { useNavigate } from "react-router-dom";
import { useWebSocket } from "@/shared/hooks/useWebSocket";
import { useAuthStore } from "@/shared/stores/authStore";
import { api } from "@/shared/api";
const { Title, Paragraph } = Typography;
interface AuthSuccessData {
token: string;
user: {
id: string;
username: string;
email?: string;
role?: string;
};
}
/**
* Экран авторизации для десктопной версии
* Отображает QR код для авторизации через мобильное приложение
* Реализует самовосстановление при разрыве соединения
*/
export const DesktopAuthScreen: React.FC = () => {
const navigate = useNavigate();
const { setToken, setUser } = useAuthStore();
const [sessionId, setSessionId] = useState<string | null>(null);
const [qrLink, setQrLink] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const [isRefreshing, setIsRefreshing] = useState(false);
// Функция обновления сессии QR
const refreshSession = useCallback(async () => {
try {
setIsRefreshing(true);
setError(null);
const data = await api.initDesktopAuth();
setSessionId(data.session_id);
setQrLink(data.qr_url);
console.log("🔄 Session refreshed:", data.session_id);
} catch (err) {
const errorMessage =
err instanceof Error
? err.message
: "Неизвестная ошибка при обновлении QR";
setError(errorMessage);
console.error("❌ Refresh error:", err);
} finally {
setIsRefreshing(false);
}
}, []);
// Обработка разрыва соединения - автообновление QR
const handleDisconnect = useCallback(() => {
console.log("⚠️ WebSocket disconnected, refreshing QR...");
refreshSession();
}, [refreshSession]);
// Инициализация WebSocket с отключенным автореконнектом
const { isConnected, lastError, lastMessage } = useWebSocket(sessionId, {
autoReconnect: false,
onDisconnect: handleDisconnect,
});
// Инициализация сессии авторизации при маунте
useEffect(() => {
refreshSession();
}, [refreshSession]);
// Обработка события успешной авторизации через WebSocket
useEffect(() => {
if (lastMessage && lastMessage.event === "auth_success") {
const handleAuthSuccess = async () => {
const data = lastMessage.data as AuthSuccessData;
const { token, user } = data;
console.log("🎉 Auth Success:", user);
// Создаём сессию на сервере (установка HttpOnly куки)
await api.createSession();
// Устанавливаем токен и данные пользователя
setToken(token);
setUser(user);
message.success("Вход выполнен!");
navigate("/web/dashboard");
};
handleAuthSuccess();
}
}, [lastMessage, setToken, setUser, navigate]);
// Отображение лоадера при обновлении QR
if (isRefreshing) {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "100vh",
backgroundColor: "#f0f2f5",
}}
>
<Spin size="large" tip="Обновление QR..." />
</div>
);
}
if (error || lastError) {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "100vh",
backgroundColor: "#f0f2f5",
padding: "24px",
}}
>
<Alert
message="Ошибка"
description={error || lastError || "Произошла ошибка при подключении"}
type="error"
showIcon
style={{ maxWidth: "400px" }}
action={
<Button size="small" type="primary" danger onClick={refreshSession}>
Повторить
</Button>
}
/>
</div>
);
}
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "100vh",
backgroundColor: "#f0f2f5",
padding: "24px",
}}
>
<Card
style={{
width: "100%",
maxWidth: "400px",
textAlign: "center",
boxShadow: "0 4px 12px rgba(0, 0, 0, 0.1)",
}}
>
<Title level={3}>Авторизация</Title>
<Paragraph type="secondary">
Отсканируйте QR код для авторизации через Телеграмм
</Paragraph>
{qrLink && (
<div
style={{
margin: "24px 0",
display: "flex",
justifyContent: "center",
}}
>
<QRCodeSVG value={qrLink} size={200} />
</div>
)}
{qrLink && (
<Button
type="primary"
href={qrLink}
target="_blank"
icon={<SendOutlined />}
style={{ marginTop: 16 }}
>
Открыть в Telegram Desktop
</Button>
)}
<div
style={{
marginTop: 24,
fontSize: 12,
color: "#888",
textAlign: "left",
}}
>
<p>
Status:{" "}
{isConnected ? (
<span style={{ color: "green" }}>Connected</span>
) : (
<span style={{ color: "red" }}>Disconnected</span>
)}
</p>
<p>Session: {sessionId?.slice(0, 8)}...</p>
{lastError && <p style={{ color: "red" }}>Error: {lastError}</p>}
</div>
{/* Кнопка ручного обновления QR */}
<Button
type="link"
icon={<ReloadOutlined />}
onClick={refreshSession}
style={{ marginTop: 12 }}
>
Обновить QR
</Button>
</Card>
</div>
);
};
```
# ===================================================================
# Файл: src/modules/desktop/pages/auth/MobileBrowserStub.tsx
# ===================================================================
```
import React from "react";
import { Result, Button } from "antd";
import { MobileOutlined } from "@ant-design/icons";
/**
* Заглушка для мобильных браузеров
* Отображается когда пользователь пытается открыть десктопную версию на мобильном устройстве
*/
export const MobileBrowserStub: React.FC = () => {
const handleRedirect = () => {
window.location.href = "/";
};
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "100vh",
backgroundColor: "#f0f2f5",
padding: "24px",
}}
>
<Result
icon={<MobileOutlined style={{ fontSize: "72px", color: "#1890ff" }} />}
title="Десктопная версия недоступна"
subTitle="Пожалуйста, используйте мобильное приложение или откройте сайт на десктопном устройстве"
extra={[
<Button type="primary" key="mobile" onClick={handleRedirect}>
Перейти к мобильной версии
</Button>,
]}
/>
</div>
);
};
```
# ===================================================================
# Файл: src/modules/desktop/pages/dashboard/InvoicesDashboard.tsx
# ===================================================================
```
import React, { useState } from "react";
import { Typography, Card, List, Empty, Tag, Spin } from "antd";
import { UploadOutlined } from "@ant-design/icons";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { DraftVerificationModal } from "@/shared/features/invoices/DraftVerificationModal";
import { api } from "@/shared/api";
import { useServerStore } from "@/shared/stores/serverStore";
import type { UnifiedInvoice } from "@/shared/types";
const { Title } = Typography;
/**
* Дашборд черновиков для десктопной версии
* Содержит зону для загрузки файлов и список черновиков
*/
export const InvoicesDashboard: React.FC = () => {
const { activeServer } = useServerStore();
const queryClient = useQueryClient();
// Состояние для Drag-n-Drop
const [isDragOver, setIsDragOver] = useState(false);
const [isUploading, setIsUploading] = useState(false);
const [verificationDraftId, setVerificationDraftId] = useState<string | null>(
null
);
// Загружаем список черновиков через useQuery
const { data: drafts, isLoading } = useQuery({
queryKey: ["drafts", activeServer?.id],
queryFn: () => api.getDrafts(),
enabled: !!activeServer, // Запрос выполняется только если выбран сервер
});
// Обработчики Drag-n-Drop
const handleDragEnter = (e: React.DragEvent) => {
e.preventDefault();
// Проверяем, что перетаскивается файл
if (e.dataTransfer.types.includes("Files")) {
setIsDragOver(true);
}
};
const handleDragLeave = (e: React.DragEvent) => {
e.preventDefault();
// Проверяем, что мы действительно покидаем элемент, а не просто переходим к дочернему
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX;
const y = e.clientY;
if (x < rect.left || x >= rect.right || y < rect.top || y >= rect.bottom) {
setIsDragOver(false);
}
};
const handleDragOver = (e: React.DragEvent) => {
e.preventDefault();
// Разрешаем drop
};
const handleDrop = async (e: React.DragEvent) => {
e.preventDefault();
setIsDragOver(false);
const files = Array.from(e.dataTransfer.files);
if (files.length === 0) return;
// Берем только первый файл
const file = files[0];
try {
setIsUploading(true);
const draft = await api.uploadFile(file);
// Обновляем список черновиков
queryClient.invalidateQueries({ queryKey: ["drafts", activeServer?.id] });
// Открываем модальное окно проверки
setVerificationDraftId(draft.id);
} catch (error) {
console.error("Ошибка загрузки файла:", error);
} finally {
setIsUploading(false);
}
};
const handleCloseVerification = () => {
setVerificationDraftId(null);
};
// Если сервер не выбран, показываем сообщение
if (!activeServer) {
return (
<div>
<Title level={2}>Черновики</Title>
<Card>
<Empty
description="Выберите сервер сверху"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
</Card>
</div>
);
}
return (
<div
style={{ position: "relative" }}
onDragEnter={handleDragEnter}
onDragLeave={handleDragLeave}
onDragOver={handleDragOver}
onDrop={handleDrop}
>
<Title level={2}>Черновики</Title>
{/* Список черновиков */}
<Card title="Последние черновики" loading={isLoading}>
<List
dataSource={drafts || []}
renderItem={(draft: UnifiedInvoice) => (
<List.Item>
<List.Item.Meta
title={
<div
style={{
display: "flex",
alignItems: "center",
gap: "8px",
}}
>
<span>{draft.document_number}</span>
{draft.type === "DRAFT" && <Tag color="blue">Черновик</Tag>}
{draft.type === "SYNCED" && (
<Tag color="green">Синхронизировано</Tag>
)}
</div>
}
description={`${draft.date_incoming} • ${
draft.store_name || "Склад не указан"
} • ${draft.items_count} позиций`}
/>
</List.Item>
)}
locale={{
emptyText: (
<Empty
description="Нет черновиков"
image={Empty.PRESENTED_IMAGE_SIMPLE}
/>
),
}}
/>
</Card>
{/* Overlay для Drag-n-Drop */}
{isDragOver && (
<div
style={{
position: "absolute",
inset: 0,
background: "rgba(255, 255, 255, 0.9)",
zIndex: 100,
border: "3px dashed #1890ff",
borderRadius: "8px",
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
pointerEvents: "none",
}}
>
<UploadOutlined
style={{ fontSize: "64px", color: "#1890ff", marginBottom: "16px" }}
/>
<Typography.Text style={{ fontSize: "18px", color: "#1890ff" }}>
Отпустите файл для загрузки
</Typography.Text>
</div>
)}
{/* Лоадер загрузки файла */}
{isUploading && (
<div
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
background: "rgba(0, 0, 0, 0.5)",
zIndex: 1000,
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
}}
>
<Spin size="large" />
<Typography.Text style={{ color: "#fff", marginTop: "16px" }}>
Загрузка файла...
</Typography.Text>
</div>
)}
{/* Модальное окно проверки черновика */}
{verificationDraftId && (
<DraftVerificationModal
draftId={verificationDraftId}
visible={!!verificationDraftId}
onClose={handleCloseVerification}
/>
)}
</div>
);
};
```
# ===================================================================
# Файл: src/modules/mobile/MobileApp.tsx
# ===================================================================
```
import React, { useEffect, useState } from "react";
import { Routes, Route, Navigate } from "react-router-dom";
import { Result, Spin } from "antd";
import { AppLayout } from "./components/AppLayout";
import { DraftsList } from "./pages/DraftsList";
import { OcrLearning } from "./pages/OcrLearning";
import { SettingsPage } from "./pages/SettingsPage";
import { InvoiceDraftPage } from "./pages/InvoiceDraftPage";
import { InvoiceViewPage } from "./pages/InvoiceViewPage";
import MaintenancePage from "./pages/MaintenancePage";
import OperatorRestricted from "./components/OperatorRestricted";
import { UNAUTHORIZED_EVENT, MAINTENANCE_EVENT } from "../../shared/api";
import { useServerStore } from "../../shared/stores/serverStore";
import type { UserRole } from "../../shared/types";
export const MobileApp: React.FC = () => {
const [isUnauthorized, setIsUnauthorized] = useState(false);
const [isMaintenance, setIsMaintenance] = useState(false);
const [userRole, setUserRole] = useState<UserRole | null>(null);
const [isLoadingRole, setIsLoadingRole] = useState(true);
const { activeServer, fetchServers } = useServerStore();
const tg = window.Telegram?.WebApp;
useEffect(() => {
const handleUnauthorized = () => setIsUnauthorized(true);
const handleMaintenance = () => setIsMaintenance(true);
window.addEventListener(UNAUTHORIZED_EVENT, handleUnauthorized);
window.addEventListener(MAINTENANCE_EVENT, handleMaintenance);
if (tg) tg.expand();
const loadUserData = async () => {
try {
await fetchServers();
const currentServer = useServerStore.getState().activeServer;
if (currentServer) setUserRole(currentServer.role);
} catch (error) {
console.error("User load error", error);
} finally {
setIsLoadingRole(false);
}
};
loadUserData();
return () => {
window.removeEventListener(UNAUTHORIZED_EVENT, handleUnauthorized);
window.removeEventListener(MAINTENANCE_EVENT, handleMaintenance);
};
}, [fetchServers, tg]);
if (isLoadingRole)
return (
<div
style={{
height: "100vh",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<Spin size="large" />
</div>
);
if (isUnauthorized)
return (
<Result
status="403"
title="Ошибка доступа"
subTitle="Перезапустите бота."
/>
);
if (isMaintenance) return <MaintenancePage />;
if (userRole === "OPERATOR")
return <OperatorRestricted serverName={activeServer?.name} />;
return (
<Routes>
<Route path="/" element={<AppLayout />}>
<Route index element={<Navigate to="/invoices" replace />} />
<Route path="invoices" element={<DraftsList />} />
<Route path="ocr" element={<OcrLearning />} />
<Route path="settings" element={<SettingsPage />} />
</Route>
<Route path="/invoice/draft/:id" element={<InvoiceDraftPage />} />
<Route path="/invoice/view/:id" element={<InvoiceViewPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
);
};
```
# ===================================================================
# Файл: src/modules/mobile/components/AppLayout.tsx
# ===================================================================
```
import React from "react";
import { Layout, theme } from "antd";
import { Outlet, useNavigate, useLocation } from "react-router-dom";
import {
ScanOutlined,
FileTextOutlined,
SettingOutlined,
} from "@ant-design/icons";
const { Content } = Layout;
export const AppLayout: React.FC = () => {
const navigate = useNavigate();
const location = useLocation();
const {
token: { colorBgContainer, colorPrimary, colorTextSecondary },
} = theme.useToken();
const path = location.pathname;
let activeKey = "invoices";
if (path.startsWith("/ocr")) activeKey = "ocr";
else if (path.startsWith("/settings")) activeKey = "settings";
const menuItems = [
{
key: "invoices",
icon: <FileTextOutlined style={{ fontSize: 20 }} />,
label: "Накладные",
path: "/invoices",
},
{
key: "ocr",
icon: <ScanOutlined style={{ fontSize: 20 }} />,
label: "Обучение",
path: "/ocr",
},
{
key: "settings",
icon: <SettingOutlined style={{ fontSize: 20 }} />,
label: "Настройки",
path: "/settings",
},
];
return (
<Layout
style={{ minHeight: "100vh", display: "flex", flexDirection: "column" }}
>
<Content
style={{ padding: "0", flex: 1, overflowY: "auto", marginBottom: 60 }}
>
<div
style={{
background: colorBgContainer,
minHeight: "100%",
padding: "12px 12px 80px 12px",
borderRadius: 0,
}}
>
<Outlet />
</div>
</Content>
<div
style={{
position: "fixed",
bottom: 0,
width: "100%",
zIndex: 1000,
background: "#fff",
borderTop: "1px solid #f0f0f0",
display: "flex",
justifyContent: "space-around",
alignItems: "center",
padding: "8px 0",
height: 60,
boxShadow: "0 -2px 8px rgba(0,0,0,0.05)",
}}
>
{menuItems.map((item) => {
const isActive = activeKey === item.key;
return (
<div
key={item.key}
onClick={() => navigate(item.path)}
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
width: "33%",
cursor: "pointer",
color: isActive ? colorPrimary : colorTextSecondary,
}}
>
{item.icon}
<span
style={{
fontSize: 10,
marginTop: 2,
fontWeight: isActive ? 500 : 400,
}}
>
{item.label}
</span>
</div>
);
})}
</div>
</Layout>
);
};
```
# ===================================================================
# Файл: src/modules/mobile/components/OperatorRestricted.tsx
# ===================================================================
```
import React from "react";
import { Result, Button, Spin } from "antd";
import { StopOutlined, CameraOutlined } from "@ant-design/icons";
interface Props {
serverName?: string;
loading?: boolean;
}
const OperatorRestricted: React.FC<Props> = ({
serverName,
loading = false,
}) => {
if (loading) {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "100vh",
background: "#f5f5f5",
}}
>
<Spin size="large" tip="Загрузка..." />
</div>
);
}
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minHeight: "100vh",
padding: 24,
background: "#f5f5f5",
}}
>
<Result
icon={<StopOutlined style={{ color: "#faad14" }} />}
title="Доступ ограничен"
subTitle={
<div style={{ textAlign: "center" }}>
<p>
Вы вошли как <strong>Оператор</strong>
{serverName && (
<>
{" "}
на сервере <strong>{serverName}</strong>
</>
)}
.
</p>
<p>
Операторы могут загружать фото накладных только через
Telegram-бота.
</p>
<p style={{ marginTop: 16, color: "#666" }}>
Для доступа к полному интерфейсу обратитесь к администратору
сервера.
</p>
</div>
}
extra={
<Button
type="primary"
icon={<CameraOutlined />}
size="large"
onClick={() => {
window.location.href = "https://t.me/RmserBot";
}}
>
Открыть бота в Telegram
</Button>
}
/>
</div>
);
};
export default OperatorRestricted;
```
# ===================================================================
# Файл: src/modules/mobile/components/recommendations/RecommendationCard.tsx
# ===================================================================
```
import React from "react";
import { Card, Tag, Typography, Button } from "antd";
import { WarningOutlined, InfoCircleOutlined } from "@ant-design/icons";
import type { Recommendation } from "../../../../shared/types";
const { Text, Paragraph } = Typography;
interface Props {
item: Recommendation;
}
export const RecommendationCard: React.FC<Props> = ({ item }) => {
const getTagColor = (type: string) => {
switch (type) {
case "UNUSED_IN_RECIPES":
return "volcano";
case "NO_INCOMING":
return "gold";
default:
return "blue";
}
};
const getIcon = (type: string) => {
return type === "UNUSED_IN_RECIPES" ? (
<WarningOutlined />
) : (
<InfoCircleOutlined />
);
};
return (
<Card
size="small"
title={
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
{getIcon(item.Type)}
<Text strong ellipsis>
{item.ProductName}
</Text>
</div>
}
extra={<Tag color={getTagColor(item.Type)}>{item.Type}</Tag>}
style={{ marginBottom: 12, boxShadow: "0 2px 8px rgba(0,0,0,0.05)" }}
>
<Paragraph style={{ marginBottom: 8 }}>{item.Reason}</Paragraph>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Text type="secondary" style={{ fontSize: 12 }}>
{new Date(item.CreatedAt).toLocaleDateString()}
</Text>
<Button size="small" type="link">
Исправить
</Button>
</div>
</Card>
);
};
```
# ===================================================================
# Файл: src/modules/mobile/components/settings/PhotoStorageTab.tsx
# ===================================================================
```
import React, { useState } from "react";
import {
Card,
Image,
Button,
Popconfirm,
Tag,
Pagination,
Empty,
Spin,
message,
Tooltip,
} from "antd";
import {
DeleteOutlined,
ReloadOutlined,
FileImageOutlined,
CheckCircleOutlined,
FileTextOutlined,
} from "@ant-design/icons";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { api, getStaticUrl } from "../../../../shared/api";
import { AxiosError } from "axios";
import type { ReceiptPhoto, PhotoStatus } from "../../../../shared/types";
export const PhotoStorageTab: React.FC = () => {
const [page, setPage] = useState(1);
const queryClient = useQueryClient();
const { data, isLoading, isError } = useQuery({
queryKey: ["photos", page],
queryFn: () => api.getPhotos(page, 18),
});
const deleteMutation = useMutation({
mutationFn: ({ id, force }: { id: string; force: boolean }) =>
api.deletePhoto(id, force),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["photos"] });
message.success("Фото удалено");
},
onError: (error: AxiosError<{ error: string }>) => {
if (error.response?.status === 409) {
message.warning("Это фото связано с черновиком.");
} else {
message.error(error.response?.data?.error || "Ошибка удаления");
}
},
});
const regenerateMutation = useMutation({
mutationFn: (id: string) => api.regenerateDraftFromPhoto(id),
onSuccess: () => message.success("Черновик восстановлен"),
onError: () => message.error("Ошибка восстановления"),
});
const getStatusTag = (status: PhotoStatus) => {
switch (status) {
case "ORPHAN":
return <Tag color="default">Без привязки</Tag>;
case "HAS_DRAFT":
return (
<Tag icon={<FileTextOutlined />} color="processing">
Черновик
</Tag>
);
case "HAS_INVOICE":
return (
<Tag icon={<CheckCircleOutlined />} color="success">
В iiko
</Tag>
);
default:
return <Tag>{status}</Tag>;
}
};
if (isLoading) {
return (
<div style={{ textAlign: "center", padding: 40 }}>
<Spin size="large" />
</div>
);
}
if (isError) {
return <Empty description="Ошибка загрузки фото" />;
}
if (!data?.photos?.length) {
return <Empty description="Нет загруженных фото" />;
}
return (
<div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fill, minmax(160px, 1fr))",
gap: 16,
marginBottom: 24,
}}
>
{data.photos.map((photo: ReceiptPhoto) => (
<Card
key={photo.id}
hoverable
size="small"
cover={
<div
style={{
height: 160,
overflow: "hidden",
position: "relative",
}}
>
<Image
src={getStaticUrl(photo.file_url)}
alt={photo.file_name}
style={{ width: "100%", height: "100%", objectFit: "cover" }}
preview={{ mask: <FileImageOutlined /> }}
/>
</div>
}
actions={[
photo.can_regenerate ? (
<Tooltip title="Создать черновик заново">
<Button
type="text"
icon={<ReloadOutlined />}
onClick={() => regenerateMutation.mutate(photo.id)}
loading={regenerateMutation.isPending}
size="small"
/>
</Tooltip>
) : (
<span />
),
photo.can_delete ? (
<Popconfirm
title="Удалить фото?"
description={
photo.status === "HAS_DRAFT"
? "Черновик тоже будет удален."
: "Восстановить будет невозможно."
}
onConfirm={() =>
deleteMutation.mutate({
id: photo.id,
force: photo.status === "HAS_DRAFT",
})
}
okText="Удалить"
cancelText="Отмена"
okButtonProps={{ danger: true }}
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
size="small"
/>
</Popconfirm>
) : (
<Tooltip title="Нельзя удалить: накладная уже в iiko">
<Button
type="text"
disabled
icon={<DeleteOutlined />}
size="small"
/>
</Tooltip>
),
]}
>
<Card.Meta
title={
<div
style={{
fontSize: 12,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{new Date(photo.created_at).toLocaleDateString()}
</div>
}
description={getStatusTag(photo.status)}
/>
</Card>
))}
</div>
<Pagination
current={page}
total={data.total}
pageSize={data.limit}
onChange={setPage}
showSizeChanger={false}
style={{ textAlign: "center" }}
simple
/>
</div>
);
};
```
# ===================================================================
# Файл: src/modules/mobile/components/settings/SyncBlock.tsx
# ===================================================================
```
import React from "react";
import { Card, Button, Typography, Space, Tooltip } from "antd";
import { SyncOutlined } from "@ant-design/icons";
import dayjs from "dayjs";
import relativeTime from "dayjs/plugin/relativeTime";
import "dayjs/locale/ru";
import type { UserRole } from "../../../../shared/types";
dayjs.extend(relativeTime);
dayjs.locale("ru");
const { Text } = Typography;
interface SyncBlockProps {
lastSyncAt: string | null;
userRole: UserRole;
onSync: () => void;
isLoading?: boolean;
}
export const SyncBlock: React.FC<SyncBlockProps> = ({
lastSyncAt,
userRole,
onSync,
isLoading = false,
}) => {
const canSync = userRole === "OWNER" || userRole === "ADMIN";
const formatLastSync = (dateStr: string | null): string => {
if (!dateStr) return "Никогда";
const date = dayjs(dateStr);
return `${date.format("DD.MM.YYYY HH:mm")} (${date.fromNow()})`;
};
return (
<Card size="small" style={{ marginBottom: 16 }}>
<Space direction="vertical" style={{ width: "100%" }} size="middle">
<div>
<Text
strong
style={{ fontSize: 16, display: "block", marginBottom: 8 }}
>
Синхронизация данных
</Text>
<Text type="secondary">
Последняя синхронизация: {formatLastSync(lastSyncAt)}
</Text>
</div>
<Tooltip title={!canSync ? "Только для администраторов" : undefined}>
<Button
type="primary"
icon={<SyncOutlined spin={isLoading} />}
onClick={onSync}
loading={isLoading}
disabled={!canSync}
block
>
Синхронизировать
</Button>
</Tooltip>
<Text type="secondary" style={{ fontSize: 12 }}>
Загружает справочники, накладные и пересчитывает рекомендации
</Text>
</Space>
</Card>
);
};
```
# ===================================================================
# Файл: src/modules/mobile/components/settings/TeamList.tsx
# ===================================================================
```
import React from "react";
import {
List,
Avatar,
Tag,
Button,
Select,
Popconfirm,
message,
Spin,
Alert,
Typography,
} from "antd";
import { DeleteOutlined, UserOutlined } from "@ant-design/icons";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { api } from "../../../../shared/api";
import type { ServerUser, UserRole } from "../../../../shared/types";
const { Text } = Typography;
interface Props {
currentUserRole: UserRole;
}
export const TeamList: React.FC<Props> = ({ currentUserRole }) => {
const queryClient = useQueryClient();
const {
data: users,
isLoading,
isError,
} = useQuery({
queryKey: ["serverUsers"],
queryFn: api.getUsers,
});
const updateRoleMutation = useMutation({
mutationFn: ({ userId, newRole }: { userId: string; newRole: UserRole }) =>
api.updateUserRole(userId, newRole),
onSuccess: () => {
message.success("Роль пользователя обновлена");
queryClient.invalidateQueries({ queryKey: ["serverUsers"] });
},
onError: () => {
message.error("Не удалось изменить роль");
},
});
const removeUserMutation = useMutation({
mutationFn: (userId: string) => api.removeUser(userId),
onSuccess: () => {
message.success("Пользователь удален из команды");
queryClient.invalidateQueries({ queryKey: ["serverUsers"] });
},
onError: () => {
message.error("Не удалось удалить пользователя");
},
});
const getRoleColor = (role: UserRole) => {
switch (role) {
case "OWNER":
return "gold";
case "ADMIN":
return "blue";
case "OPERATOR":
return "default";
default:
return "default";
}
};
const getRoleName = (role: UserRole) => {
switch (role) {
case "OWNER":
return "Владелец";
case "ADMIN":
return "Админ";
case "OPERATOR":
return "Оператор";
default:
return role;
}
};
const canDelete = (targetUser: ServerUser) => {
if (targetUser.is_me) return false;
if (targetUser.role === "OWNER") return false;
if (currentUserRole === "ADMIN" && targetUser.role === "ADMIN")
return false;
return true;
};
if (isLoading) {
return (
<div style={{ textAlign: "center", padding: 20 }}>
<Spin />
</div>
);
}
if (isError) {
return <Alert type="error" message="Не удалось загрузить список команды" />;
}
return (
<>
<Alert
type="info"
showIcon
style={{ marginBottom: 16 }}
message="Приглашение сотрудников"
description="Чтобы добавить сотрудника, отправьте ему ссылку-приглашение."
/>
<List
itemLayout="horizontal"
dataSource={users || []}
renderItem={(user) => (
<List.Item
actions={[
currentUserRole === "OWNER" && !user.is_me ? (
<Select
key="role-select"
defaultValue={user.role}
size="small"
style={{ width: 110 }}
disabled={updateRoleMutation.isPending}
onChange={(val) =>
updateRoleMutation.mutate({
userId: user.user_id,
newRole: val,
})
}
options={[
{ value: "ADMIN", label: "Админ" },
{ value: "OPERATOR", label: "Оператор" },
]}
/>
) : (
<Tag key="role-tag" color={getRoleColor(user.role)}>
{getRoleName(user.role)}
</Tag>
),
<Popconfirm
key="delete"
title="Закрыть доступ?"
description={`Вы уверены, что хотите удалить ${user.first_name}?`}
onConfirm={() => removeUserMutation.mutate(user.user_id)}
disabled={!canDelete(user)}
okText="Да"
cancelText="Нет"
>
<Button
danger
type="text"
icon={<DeleteOutlined />}
disabled={!canDelete(user) || removeUserMutation.isPending}
/>
</Popconfirm>,
]}
>
<List.Item.Meta
avatar={
<Avatar src={user.photo_url} icon={<UserOutlined />}>
{user.first_name?.[0]}
</Avatar>
}
title={
<span>
{user.first_name} {user.last_name}{" "}
{user.is_me && <Text type="secondary">(Вы)</Text>}
</span>
}
description={
user.username ? (
<a
href={`https://t.me/${user.username}`}
target="_blank"
rel="noopener noreferrer"
style={{ fontSize: 12 }}
>
@{user.username}
</a>
) : (
<Text type="secondary" style={{ fontSize: 12 }}>
Нет username
</Text>
)
}
/>
</List.Item>
)}
/>
</>
);
};
```
# ===================================================================
# Файл: src/modules/mobile/pages/DraftsList.tsx
# ===================================================================
```
import React, { useState, useMemo } from "react";
import { useQuery } from "@tanstack/react-query";
import {
List,
Typography,
Tag,
Spin,
Empty,
Flex,
Button,
Select,
DatePicker,
} from "antd";
import { useNavigate } from "react-router-dom";
import {
CheckCircleOutlined,
DeleteOutlined,
PlusOutlined,
ExclamationCircleOutlined,
LoadingOutlined,
CloseCircleOutlined,
StopOutlined,
SyncOutlined,
CloudServerOutlined,
} from "@ant-design/icons";
import dayjs from "dayjs";
import "dayjs/locale/ru";
import { api } from "../../../shared/api";
import type { UnifiedInvoice } from "../../../shared/types";
const { Title, Text } = Typography;
type FilterType = "ALL" | "DRAFT" | "SYNCED";
dayjs.locale("ru");
const DayDivider: React.FC<{ date: string }> = ({ date }) => {
const d = dayjs(date);
const dayOfWeek = d.format("dddd");
const formattedDate = d.format("D MMMM YYYY");
return (
<div
style={{
display: "flex",
alignItems: "center",
gap: 12,
padding: "16px 0 8px",
borderBottom: "1px solid #f0f0f0",
marginBottom: 8,
}}
>
<Text strong style={{ fontSize: 14, color: "#1890ff" }}>
{formattedDate}
</Text>
<Text type="secondary" style={{ fontSize: 12 }}>
{dayOfWeek}
</Text>
</div>
);
};
export const DraftsList: React.FC = () => {
const navigate = useNavigate();
const [syncLoading, setSyncLoading] = useState(false);
const [filterType, setFilterType] = useState<FilterType>("ALL");
const [currentPage, setCurrentPage] = useState(1);
const [pageSize, setPageSize] = useState(20);
const [startDate, setStartDate] = useState<dayjs.Dayjs | null>(
dayjs().subtract(30, "day")
);
const [endDate, setEndDate] = useState<dayjs.Dayjs | null>(dayjs());
const {
data: invoices,
isLoading,
isError,
refetch,
} = useQuery({
queryKey: [
"drafts",
startDate?.format("YYYY-MM-DD"),
endDate?.format("YYYY-MM-DD"),
],
queryFn: () =>
api.getDrafts(
startDate?.format("YYYY-MM-DD"),
endDate?.format("YYYY-MM-DD")
),
staleTime: 0,
refetchOnMount: true,
refetchOnWindowFocus: true,
});
const handleSync = async () => {
setSyncLoading(true);
try {
await api.syncInvoices();
refetch();
} finally {
setSyncLoading(false);
}
};
const getStatusTag = (item: UnifiedInvoice) => {
switch (item.status) {
case "PROCESSING":
return (
<Tag icon={<LoadingOutlined />} color="blue">
Обработка
</Tag>
);
case "READY_TO_VERIFY":
return (
<Tag icon={<ExclamationCircleOutlined />} color="orange">
Проверка
</Tag>
);
case "COMPLETED":
return (
<Tag icon={<CheckCircleOutlined />} color="green">
Готово
</Tag>
);
case "ERROR":
return (
<Tag icon={<CloseCircleOutlined />} color="red">
Ошибка
</Tag>
);
case "CANCELED":
return (
<Tag icon={<StopOutlined />} color="default">
Отменен
</Tag>
);
case "NEW":
return (
<Tag icon={<PlusOutlined />} color="blue">
Новая
</Tag>
);
case "PROCESSED":
return (
<Tag icon={<CheckCircleOutlined />} color="green">
Проведена
</Tag>
);
case "DELETED":
return (
<Tag icon={<DeleteOutlined />} color="red">
Удалена
</Tag>
);
default:
return <Tag>{item.status}</Tag>;
}
};
const handleInvoiceClick = (item: UnifiedInvoice) => {
if (item.type === "DRAFT") {
navigate("/invoice/draft/" + item.id);
return;
}
if (item.type === "SYNCED") {
if (item.draft_id) {
navigate("/invoice/draft/" + item.draft_id);
} else {
navigate("/invoice/view/" + item.id);
}
}
};
const handleFilterChange = (value: FilterType) => {
setFilterType(value);
setCurrentPage(1);
};
const handlePageSizeChange = (value: number) => {
setPageSize(value);
setCurrentPage(1);
};
const getItemDate = (item: UnifiedInvoice) =>
item.type === "DRAFT" ? item.created_at : item.date_incoming;
const filteredAndSortedInvoices = useMemo(() => {
if (!invoices || invoices.length === 0) return [];
let result = [...invoices];
if (filterType !== "ALL") {
result = result.filter((item) => item.type === filterType);
}
result.sort((a, b) => {
const dateA = dayjs(getItemDate(a)).startOf("day");
const dateB = dayjs(getItemDate(b)).startOf("day");
if (!dateA.isSame(dateB)) {
return dateB.valueOf() - dateA.valueOf();
}
if (a.type !== b.type) {
return a.type === "DRAFT" ? -1 : 1;
}
return (b.document_number || "").localeCompare(
a.document_number || "",
"ru",
{ numeric: true }
);
});
return result;
}, [invoices, filterType]);
const paginatedInvoices = useMemo(() => {
const startIndex = (currentPage - 1) * pageSize;
return filteredAndSortedInvoices.slice(startIndex, startIndex + pageSize);
}, [filteredAndSortedInvoices, currentPage, pageSize]);
const groupedInvoices = useMemo(() => {
const groups: { [key: string]: UnifiedInvoice[] } = {};
paginatedInvoices.forEach((item) => {
const dateKey = dayjs(getItemDate(item)).format("YYYY-MM-DD");
if (!groups[dateKey]) {
groups[dateKey] = [];
}
groups[dateKey].push(item);
});
return groups;
}, [paginatedInvoices]);
const filterCounts = useMemo(() => {
if (!invoices) return { all: 0, draft: 0, synced: 0 };
return {
all: invoices.length,
draft: invoices.filter((item) => item.type === "DRAFT").length,
synced: invoices.filter((item) => item.type === "SYNCED").length,
};
}, [invoices]);
const totalPages = Math.ceil(
(filteredAndSortedInvoices.length || 0) / pageSize
);
if (isError) {
return (
<div style={{ padding: 20 }}>
<Text type="danger">Ошибка загрузки списка накладных</Text>
</div>
);
}
return (
<div style={{ padding: "0 4px 20px" }}>
<Flex
align="center"
justify="space-between"
style={{ marginTop: 16, marginBottom: 16 }}
>
<Flex align="center" gap={8}>
<Title level={4} style={{ margin: 0 }}>
Накладные
</Title>
<Button
icon={<SyncOutlined />}
loading={syncLoading}
onClick={handleSync}
/>
</Flex>
<Select
value={filterType}
onChange={handleFilterChange}
options={[
{ label: `Все (${filterCounts.all})`, value: "ALL" },
{ label: `Черновики (${filterCounts.draft})`, value: "DRAFT" },
{ label: `Накладные (${filterCounts.synced})`, value: "SYNCED" },
]}
size="small"
style={{ width: 140 }}
/>
</Flex>
<div
style={{
marginBottom: 8,
background: "#fff",
padding: 8,
borderRadius: 8,
}}
>
<Flex vertical gap={8}>
<Text style={{ fontSize: 13 }}>Период:</Text>
<Flex align="center" gap={8}>
<DatePicker
value={startDate}
onChange={setStartDate}
format="DD.MM.YYYY"
size="small"
placeholder="Начало"
style={{ width: 110 }}
/>
<Text type="secondary">—</Text>
<DatePicker
value={endDate}
onChange={setEndDate}
format="DD.MM.YYYY"
size="small"
placeholder="Конец"
style={{ width: 110 }}
/>
</Flex>
</Flex>
</div>
{isLoading ? (
<div style={{ textAlign: "center", padding: 40 }}>
<Spin size="large" />
</div>
) : !invoices || invoices.length === 0 ? (
<Empty description="Нет данных" />
) : (
<>
<div>
{Object.entries(groupedInvoices).map(([dateKey, items]) => (
<div key={dateKey}>
<DayDivider date={dateKey} />
{items.map((item) => {
const isSynced = item.type === "SYNCED";
const displayDate =
item.type === "DRAFT"
? item.created_at
: item.date_incoming;
return (
<List.Item
key={item.id}
style={{
background: isSynced ? "#fafafa" : "#fff",
padding: 12,
marginBottom: 10,
borderRadius: 12,
cursor: "pointer",
border: isSynced
? "1px solid #f0f0f0"
: "1px solid #e6f7ff",
boxShadow: "0 2px 4px rgba(0,0,0,0.02)",
display: "block",
position: "relative",
}}
onClick={() => handleInvoiceClick(item)}
>
<div
style={{
position: "absolute",
top: 12,
right: 12,
}}
>
{getStatusTag(item)}
</div>
<Flex vertical gap={4}>
<Flex align="center" gap={8}>
<Text strong style={{ fontSize: 16 }}>
{item.document_number || "Без номера"}
</Text>
{item.type === "SYNCED" && (
<CloudServerOutlined style={{ color: "gray" }} />
)}
{item.is_app_created && (
<span title="Создано в RMSer">📱</span>
)}
</Flex>
<Flex vertical gap={2}>
<Text type="secondary" style={{ fontSize: 13 }}>
{item.items_count} поз.
</Text>
<Text type="secondary" style={{ fontSize: 13 }}>
{dayjs(displayDate).format("DD.MM.YYYY")}
</Text>
</Flex>
{item.incoming_number && (
<Text type="secondary" style={{ fontSize: 12 }}>
Вх. № {item.incoming_number}
</Text>
)}
<Flex justify="space-between" align="center">
<div></div>
{item.store_name && (
<Tag
style={{
margin: 0,
maxWidth: 120,
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{item.store_name}
</Tag>
)}
</Flex>
<Flex
justify="space-between"
align="center"
style={{ marginTop: 8 }}
>
<Text
strong
style={{
fontSize: 17,
color: isSynced ? "#595959" : "#1890ff",
}}
>
{item.total_sum.toLocaleString("ru-RU", {
style: "currency",
currency: "RUB",
maximumFractionDigits: 0,
})}
</Text>
{item.items_preview && (
<div
style={{
textAlign: "right",
maxWidth: 150,
}}
>
{item.items_preview
.split(", ")
.slice(0, 3)
.map((previewItem, idx) => (
<div
key={idx}
style={{
fontSize: 12,
color: "#666",
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{previewItem}
</div>
))}
</div>
)}
</Flex>
</Flex>
</List.Item>
);
})}
</div>
))}
</div>
{totalPages > 1 && (
<Flex
justify="space-between"
align="center"
style={{
marginTop: 16,
padding: "8px 12px",
background: "#fff",
borderRadius: 8,
}}
>
<Flex align="center" gap={8}>
<Text style={{ fontSize: 12 }}>На странице:</Text>
<Select
value={pageSize}
onChange={handlePageSizeChange}
options={[
{ label: "10", value: 10 },
{ label: "20", value: 20 },
{ label: "50", value: 50 },
]}
size="small"
style={{ width: 70 }}
/>
</Flex>
<Flex align="center" gap={8}>
<Text style={{ fontSize: 13 }}>
Стр. {currentPage} из {totalPages}
</Text>
<Button
size="small"
disabled={currentPage === 1}
onClick={() => setCurrentPage((prev) => prev - 1)}
>
</Button>
<Button
size="small"
disabled={currentPage === totalPages}
onClick={() => setCurrentPage((prev) => prev + 1)}
>
</Button>
</Flex>
</Flex>
)}
</>
)}
</div>
);
};
```
# ===================================================================
# Файл: src/modules/mobile/pages/InvoiceDraftPage.tsx
# ===================================================================
```
import React from "react";
import { useParams, useNavigate } from "react-router-dom";
import { DraftEditor } from "../../../shared/features/invoices/DraftEditor";
export const InvoiceDraftPage: React.FC = () => {
const { id: draftId } = useParams<{ id: string }>();
const navigate = useNavigate();
if (!draftId) {
return null;
}
return (
<div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
<DraftEditor draftId={draftId} onBack={() => navigate("/invoices")} />
</div>
);
};
```
# ===================================================================
# Файл: src/modules/mobile/pages/InvoiceViewPage.tsx
# ===================================================================
```
import React from "react";
import { useParams, useNavigate } from "react-router-dom";
import { InvoiceViewer } from "../../../shared/features/invoices/InvoiceViewer";
export const InvoiceViewPage: React.FC = () => {
const { id: invoiceId } = useParams<{ id: string }>();
const navigate = useNavigate();
return (
<div style={{ height: "100vh", display: "flex", flexDirection: "column" }}>
{invoiceId && (
<InvoiceViewer
invoiceId={invoiceId}
onBack={() => navigate("/invoices")}
/>
)}
</div>
);
};
```
# ===================================================================
# Файл: src/modules/mobile/pages/MaintenancePage.tsx
# ===================================================================
```
import { Result, Button } from "antd";
const MaintenancePage = () => (
<div
style={{
height: "100vh",
display: "flex",
alignItems: "center",
justifyContent: "center",
background: "#fff",
padding: 20,
}}
>
<Result
status="warning"
title="Сервис на техническом обслуживании"
subTitle="Мы скоро вернемся с новыми функциями!"
extra={
<Button type="primary" onClick={() => window.location.reload()}>
Попробовать снова
</Button>
}
/>
</div>
);
export default MaintenancePage;
```
# ===================================================================
# Файл: src/modules/mobile/pages/OcrLearning.tsx
# ===================================================================
```
import React from "react";
import { Spin, Alert } from "antd";
import { useOcr } from "../../../shared/hooks/useOcr";
import { AddMatchForm } from "../../../shared/features/ocr/AddMatchForm";
import { MatchList } from "../../../shared/features/ocr/MatchList";
export const OcrLearning: React.FC = () => {
const {
catalog,
matches,
unmatched,
isLoading,
isError,
createMatch,
isCreating,
deleteMatch,
isDeletingMatch,
deleteUnmatched,
} = useOcr();
const [editingMatch, setEditingMatch] = React.useState<string | null>(null);
const currentEditingMatch = React.useMemo(() => {
if (!editingMatch) return undefined;
return matches.find((match) => match.raw_name === editingMatch);
}, [editingMatch, matches]);
if (isLoading && matches.length === 0) {
return (
<div
style={{
display: "flex",
justifyContent: "center",
alignItems: "center",
height: "50vh",
flexDirection: "column",
gap: 16,
}}
>
<Spin size="large" />
<span style={{ color: "#888" }}>Загрузка справочников...</span>
</div>
);
}
if (isError) {
return (
<Alert
message="Ошибка"
description="Не удалось загрузить данные."
type="error"
showIcon
style={{ margin: 16 }}
/>
);
}
return (
<div style={{ paddingBottom: 20 }}>
<AddMatchForm
catalog={catalog}
unmatched={unmatched}
onSave={(raw, prodId, qty, contId) => {
if (currentEditingMatch) {
createMatch({
raw_name: raw,
product_id: prodId,
quantity: qty,
container_id: contId,
});
setEditingMatch(null);
} else {
createMatch({
raw_name: raw,
product_id: prodId,
quantity: qty,
container_id: contId,
});
}
}}
onDeleteUnmatched={deleteUnmatched}
isLoading={isCreating}
initialValues={currentEditingMatch}
onCancelEdit={() => setEditingMatch(null)}
/>
<h3 style={{ marginLeft: 4, marginBottom: 12 }}>
Обученные позиции ({matches.length})
</h3>
<MatchList
matches={matches}
onDeleteMatch={deleteMatch}
onEditMatch={(match) => {
setEditingMatch(match.raw_name);
}}
isDeleting={isDeletingMatch}
/>
</div>
);
};
```
# ===================================================================
# Файл: src/modules/mobile/pages/SettingsPage.tsx
# ===================================================================
```
import React, { useEffect } from "react";
import {
Typography,
Card,
Form,
Select,
Switch,
Button,
Row,
Col,
Statistic,
TreeSelect,
Spin,
message,
Tabs,
Popconfirm,
} from "antd";
import {
SaveOutlined,
BarChartOutlined,
SettingOutlined,
FolderOpenOutlined,
TeamOutlined,
CameraOutlined,
} from "@ant-design/icons";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { api } from "../../../shared/api";
import type { UserSettings, Store } from "../../../shared/types";
import { TeamList } from "../components/settings/TeamList";
import { PhotoStorageTab } from "../components/settings/PhotoStorageTab";
import { SyncBlock } from "../components/settings/SyncBlock";
const { Title, Text } = Typography;
export const SettingsPage: React.FC = () => {
const queryClient = useQueryClient();
const [form] = Form.useForm();
const settingsQuery = useQuery({
queryKey: ["settings"],
queryFn: api.getSettings,
});
const statsQuery = useQuery({
queryKey: ["stats"],
queryFn: api.getStats,
});
const dictQuery = useQuery({
queryKey: ["dictionaries"],
queryFn: api.getDictionaries,
staleTime: 1000 * 60 * 5,
});
const groupsQuery = useQuery({
queryKey: ["productGroups"],
queryFn: api.getProductGroups,
staleTime: 1000 * 60 * 10,
});
const saveMutation = useMutation({
mutationFn: (vals: UserSettings) => api.updateSettings(vals),
onSuccess: () => {
message.success("Настройки сохранены");
queryClient.invalidateQueries({ queryKey: ["settings"] });
},
onError: () => {
message.error("Не удалось сохранить настройки");
},
});
const deleteAllDraftsMutation = useMutation({
mutationFn: () => api.deleteAllDrafts(),
onSuccess: (data) => {
message.success(`Удалено черновиков: ${data.count}`);
queryClient.invalidateQueries({ queryKey: ["stats"] });
},
onError: () => {
message.error("Не удалось удалить черновики");
},
});
const syncMutation = useMutation({
mutationFn: () => api.syncAll(true),
onSuccess: () => {
message.success("Синхронизация запущена в фоне");
queryClient.invalidateQueries({ queryKey: ["settings"] });
},
onError: () => {
message.error("Ошибка запуска синхронизации");
},
});
useEffect(() => {
if (settingsQuery.data) {
form.setFieldsValue(settingsQuery.data);
}
}, [settingsQuery.data, form]);
const handleSave = async () => {
try {
const values = await form.validateFields();
saveMutation.mutate({
...values,
auto_conduct: !!values.auto_conduct,
});
} catch {
// validation errors
}
};
if (settingsQuery.isLoading) {
return (
<div style={{ textAlign: "center", padding: 50 }}>
<Spin size="large" />
</div>
);
}
const currentUserRole = settingsQuery.data?.role || "OPERATOR";
const showTeamSettings =
currentUserRole === "ADMIN" || currentUserRole === "OWNER";
const tabsItems = [
{
key: "general",
label: "Общие",
icon: <SettingOutlined />,
children: (
<Form form={form} layout="vertical">
<SyncBlock
lastSyncAt={settingsQuery.data?.last_sync_at || null}
userRole={currentUserRole}
onSync={() => syncMutation.mutate()}
isLoading={syncMutation.isPending}
/>
<Card size="small" style={{ marginBottom: 16 }}>
<Form.Item
label="Склад по умолчанию"
name="default_store_id"
tooltip="Этот склад будет выбираться автоматически при создании новой накладной"
>
<Select
placeholder="Не выбрано"
allowClear
loading={dictQuery.isLoading}
options={dictQuery.data?.stores.map((s: Store) => ({
label: s.name,
value: s.id,
}))}
/>
</Form.Item>
<Form.Item
label="Корневая группа товаров"
name="root_group_id"
tooltip="Товары для распознавания будут искаться только внутри этой группы (и её подгрупп)."
>
<TreeSelect
showSearch
style={{ width: "100%" }}
dropdownStyle={{ maxHeight: 400, overflow: "auto" }}
placeholder="Выберите папку"
allowClear
treeDefaultExpandAll={false}
treeData={groupsQuery.data}
fieldNames={{
label: "title",
value: "value",
children: "children",
}}
treeNodeFilterProp="title"
suffixIcon={<FolderOpenOutlined />}
loading={groupsQuery.isLoading}
/>
</Form.Item>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: 24,
}}
>
<div>
<Text>Проводить накладные автоматически</Text>
<br />
<Text type="secondary" style={{ fontSize: 12 }}>
Если выключено, накладные в iiko будут создаваться как
"Непроведенные"
</Text>
</div>
<Form.Item name="auto_conduct" valuePropName="checked" noStyle>
<Switch />
</Form.Item>
</div>
</Card>
<Button
type="primary"
icon={<SaveOutlined />}
block
size="large"
onClick={handleSave}
loading={saveMutation.isPending}
>
Сохранить настройки
</Button>
{currentUserRole === "OWNER" && (
<Card
size="small"
style={{
marginTop: 24,
borderColor: "#ff4d4f",
borderWidth: 2,
}}
>
<Title level={5} style={{ color: "#ff4d4f", marginBottom: 16 }}>
Опасная зона
</Title>
<Popconfirm
title="Вы уверены?"
description="Это удалит ВСЕ черновики, которые еще не были отправлены в iiko. Это действие необратимо."
onConfirm={() => deleteAllDraftsMutation.mutate()}
okText="Удалить"
cancelText="Отмена"
okButtonProps={{ danger: true }}
>
<Button
danger
block
loading={deleteAllDraftsMutation.isPending}
>
Удалить все черновики
</Button>
</Popconfirm>
</Card>
)}
</Form>
),
},
];
if (showTeamSettings) {
tabsItems.push({
key: "team",
label: "Команда",
icon: <TeamOutlined />,
children: (
<Card size="small">
<TeamList currentUserRole={currentUserRole} />
</Card>
),
});
}
if (currentUserRole === "OWNER") {
tabsItems.push({
key: "photos",
label: "Архив фото",
icon: <CameraOutlined />,
children: <PhotoStorageTab />,
});
}
return (
<div style={{ padding: "0 16px 80px" }}>
<Title level={4} style={{ marginTop: 16 }}>
<SettingOutlined /> Настройки
</Title>
<Card
size="small"
style={{
marginBottom: 16,
background: "#f0f5ff",
borderColor: "#d6e4ff",
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
marginBottom: 12,
}}
>
<BarChartOutlined style={{ color: "#1890ff" }} />
<Text strong>Статистика накладных</Text>
</div>
<Row gutter={16}>
<Col span={8}>
<Statistic
title="За 24ч"
value={statsQuery.data?.last_24h || 0}
valueStyle={{ fontSize: 18 }}
/>
</Col>
<Col span={8}>
<Statistic
title="Месяц"
value={statsQuery.data?.last_month || 0}
valueStyle={{ fontSize: 18 }}
/>
</Col>
<Col span={8}>
<Statistic
title="Всего"
value={statsQuery.data?.total || 0}
valueStyle={{ fontSize: 18 }}
/>
</Col>
</Row>
</Card>
<Tabs defaultActiveKey="general" items={tabsItems} />
</div>
);
};
```
# ===================================================================
# Файл: src/shared/api/index.ts
# ===================================================================
```
import axios from 'axios';
import { notification } from 'antd';
import { useAuthStore } from '../stores/authStore';
// Тип пользователя для сессионной авторизации
export interface User {
id: string;
username: string;
email?: string;
role?: string;
}
import type {
CatalogItem,
CreateInvoiceRequest,
MatchRequest,
HealthResponse,
InvoiceResponse,
ProductMatch,
Recommendation,
UnmatchedItem,
UserSettings,
InvoiceStats,
ProductGroup,
Store,
Supplier,
DraftInvoice,
DraftItem,
UpdateDraftItemRequest,
UpdateDraftRequest,
CommitDraftRequest,
ReorderDraftItemsRequest,
ProductSearchResult,
AddContainerRequest,
AddContainerResponse,
DictionariesResponse,
UnifiedInvoice,
ServerUser,
UserRole,
InvoiceDetails,
GetPhotosResponse,
ServerShort
} from '../types';
// Интерфейс для ответа метода инициализации десктопной авторизации
export interface InitDesktopAuthResponse {
session_id: string;
qr_url: string;
}
// Базовый URL
export const API_BASE_URL = import.meta.env.VITE_API_URL || 'http://localhost:8080/api';
// Хелпер для получения полного URL картинки (убирает /api если путь статики идет от корня, или добавляет как есть)
// В данном ТЗ сказано просто склеивать.
export const getStaticUrl = (path: string | null | undefined): string => {
if (!path) return '';
if (path.startsWith('http')) return path;
return `${API_BASE_URL}${path}`;
};
// Телеграм объект
const tg = window.Telegram?.WebApp;
// Событие для глобальной обработки 401
export const UNAUTHORIZED_EVENT = 'rms_unauthorized';
// Событие для режима технического обслуживания (503)
export const MAINTENANCE_EVENT = 'rms_maintenance';
export const apiClient = axios.create({
baseURL: API_BASE_URL,
headers: {
'Content-Type': 'application/json',
},
withCredentials: true,
});
// --- Request Interceptor (Авторизация через initData или JWT) ---
apiClient.interceptors.request.use((config) => {
// Шаг 1: Whitelist - пропускаем запросы к инициализации десктопной авторизации
if (config.url?.endsWith('/auth/init-desktop')) {
return config;
}
// Шаг 2: Mobile Auth (Telegram Mini App) - проверяем Telegram initData ПЕРВЫМ (высший приоритет)
const initData = tg?.initData;
if (initData) {
config.headers['Authorization'] = `Bearer ${initData}`;
return config;
}
// Шаг 3: Desktop Auth - проверяем JWT токен из authStore
const jwtToken = useAuthStore.getState().token;
if (jwtToken) {
config.headers['Authorization'] = `Bearer ${jwtToken}`;
return config;
}
// Шаг 4: Block - если нет ни initData, ни JWT, отклоняем запрос
console.error('Запрос заблокирован: отсутствуют данные авторизации.');
return Promise.reject(new Error('MISSING_AUTH'));
});
// --- Response Interceptor (Обработка ошибок и уведомления) ---
apiClient.interceptors.response.use(
(response) => response,
(error) => {
if (error.response && error.response.status === 401) {
// Глобальное уведомление об ошибке авторизации
notification.error({
message: 'Ошибка авторизации',
description: 'Ваша сессия в Telegram истекла или данные неверны. Попробуйте перезапустить бота.',
placement: 'top',
});
window.dispatchEvent(new Event(UNAUTHORIZED_EVENT));
}
if (error.response && error.response.status === 503) {
// Режим технического обслуживания
window.dispatchEvent(new Event(MAINTENANCE_EVENT));
}
// Если запрос был отменен нами (нет авторизации), не выводим стандартную ошибку API
if (error.message === 'MISSING_AUTH') {
return Promise.reject(error);
}
console.error('API Error:', error);
return Promise.reject(error);
}
);
export const api = {
checkHealth: async (): Promise<HealthResponse> => {
const { data } = await apiClient.get<HealthResponse>('/health');
return data;
},
getRecommendations: async (): Promise<Recommendation[]> => {
const { data } = await apiClient.get<Recommendation[]>('/recommendations');
return data;
},
getCatalogItems: async (): Promise<CatalogItem[]> => {
const { data } = await apiClient.get<CatalogItem[]>('/ocr/catalog');
return data;
},
searchProducts: async (query: string): Promise<ProductSearchResult[]> => {
const { data } = await apiClient.get<ProductSearchResult[]>('/ocr/search', {
params: { q: query }
});
return data;
},
createContainer: async (payload: AddContainerRequest): Promise<AddContainerResponse> => {
const { data } = await apiClient.post<AddContainerResponse>('/drafts/container', payload);
return data;
},
getMatches: async (): Promise<ProductMatch[]> => {
const { data } = await apiClient.get<ProductMatch[]>('/ocr/matches');
return data;
},
getUnmatched: async (): Promise<UnmatchedItem[]> => {
const { data } = await apiClient.get<UnmatchedItem[]>('/ocr/unmatched');
return data;
},
createMatch: async (payload: MatchRequest): Promise<{ status: string }> => {
const { data } = await apiClient.post<{ status: string }>('/ocr/match', payload);
return data;
},
deleteMatch: async (rawName: string): Promise<{ status: string }> => {
const { data } = await apiClient.delete<{ status: string }>('/ocr/match', {
params: { raw_name: rawName }
});
return data;
},
deleteUnmatched: async (rawName: string): Promise<{ status: string }> => {
const { data } = await apiClient.delete<{ status: string }>('/ocr/unmatched', {
params: { raw_name: rawName }
});
return data;
},
createInvoice: async (payload: CreateInvoiceRequest): Promise<InvoiceResponse> => {
const { data } = await apiClient.post<InvoiceResponse>('/invoices/send', payload);
return data;
},
// --- НОВЫЙ МЕТОД: Получение всех справочников ---
getDictionaries: async (): Promise<DictionariesResponse> => {
const { data } = await apiClient.get<DictionariesResponse>('/dictionaries');
return data;
},
// Старые методы оставляем для совместимости, но они могут вызывать getDictionaries внутри или deprecated endpoint
getStores: async (): Promise<Store[]> => {
// Можно использовать новый эндпоинт и возвращать часть данных
const { data } = await apiClient.get<DictionariesResponse>('/dictionaries');
return data.stores;
},
getSuppliers: async (): Promise<Supplier[]> => {
// Реальный запрос вместо мока
const { data } = await apiClient.get<DictionariesResponse>('/dictionaries');
return data.suppliers;
},
// Обновленный метод получения списка накладных с фильтрацией
getDrafts: async (from?: string, to?: string): Promise<UnifiedInvoice[]> => {
const { data } = await apiClient.get<UnifiedInvoice[]>('/drafts', {
params: { from, to }
});
return data;
},
getDraft: async (id: string): Promise<DraftInvoice> => {
const { data } = await apiClient.get<DraftInvoice>(`/drafts/${id}`);
return data;
},
updateDraft: async (id: string, payload: UpdateDraftRequest): Promise<DraftInvoice> => {
const { data } = await apiClient.patch<DraftInvoice>(`/drafts/${id}`, payload);
return data;
},
updateDraftItem: async (draftId: string, itemId: string, payload: UpdateDraftItemRequest): Promise<DraftInvoice> => {
const { data } = await apiClient.patch<DraftInvoice>(`/drafts/${draftId}/items/${itemId}`, payload);
return data;
},
addDraftItem: async (draftId: string): Promise<DraftItem> => {
const { data } = await apiClient.post<DraftItem>(`/drafts/${draftId}/items`, {});
return data;
},
deleteDraftItem: async (draftId: string, itemId: string): Promise<void> => {
await apiClient.delete(`/drafts/${draftId}/items/${itemId}`);
},
reorderDraftItems: async (draftId: string, payload: ReorderDraftItemsRequest): Promise<void> => {
await apiClient.post(`/drafts/${draftId}/reorder`, payload);
},
commitDraft: async (draftId: string, payload: CommitDraftRequest): Promise<{ document_number: string }> => {
const { data } = await apiClient.post<{ document_number: string }>(`/drafts/${draftId}/commit`, payload);
return data;
},
deleteDraft: async (id: string): Promise<void> => {
await apiClient.delete(`/drafts/${id}`);
},
deleteAllDrafts: async (): Promise<{ count: number }> => {
const { data } = await apiClient.delete<{ count: number }>('/drafts');
return data;
},
// --- Настройки и Статистика ---
getSettings: async (): Promise<UserSettings> => {
const { data } = await apiClient.get<UserSettings>('/settings');
return data;
},
updateSettings: async (payload: UserSettings): Promise<UserSettings> => {
const { data } = await apiClient.post<UserSettings>('/settings', payload);
return data;
},
getStats: async (): Promise<InvoiceStats> => {
const { data } = await apiClient.get<InvoiceStats>('/invoices/stats');
return data;
},
getProductGroups: async (): Promise<ProductGroup[]> => {
const { data } = await apiClient.get<ProductGroup[]>('/dictionaries/groups');
return data;
},
// --- Управление командой ---
getUsers: async (): Promise<ServerUser[]> => {
const { data } = await apiClient.get<ServerUser[]>('/settings/users');
return data;
},
updateUserRole: async (userId: string, newRole: UserRole): Promise<{ status: string }> => {
const { data } = await apiClient.patch<{ status: string }>(`/settings/users/${userId}`, { new_role: newRole });
return data;
},
removeUser: async (userId: string): Promise<{ status: string }> => {
const { data } = await apiClient.delete<{ status: string }>(`/settings/users/${userId}`);
return data;
},
getInvoice: async (id: string): Promise<InvoiceDetails> => {
const { data } = await apiClient.get<InvoiceDetails>(`/invoices/${id}`);
return data;
},
syncInvoices: async (): Promise<void> => {
await apiClient.post('/invoices/sync');
},
syncAll: async (force = true): Promise<void> => {
await apiClient.post('/sync/all', null, {
params: { force }
});
},
getPhotos: async (page = 1, limit = 20): Promise<GetPhotosResponse> => {
const { data } = await apiClient.get<GetPhotosResponse>('/photos', {
params: { page, limit }
});
return data;
},
deletePhoto: async (id: string, force = false): Promise<void> => {
await apiClient.delete(`/photos/${id}`, {
params: { force }
});
},
regenerateDraftFromPhoto: async (id: string): Promise<void> => {
await apiClient.post(`/photos/${id}/regenerate`);
},
// --- Десктопная авторизация ---
initDesktopAuth: async (): Promise<InitDesktopAuthResponse> => {
const { data } = await apiClient.post<InitDesktopAuthResponse>('/auth/init-desktop');
return data;
},
// Сессионная авторизация (Desktop через куки)
createSession: async (): Promise<void> => {
await apiClient.post('/auth/session');
},
getMe: async (): Promise<User> => {
const { data } = await apiClient.get<User>('/auth/me');
return data;
},
logout: async (): Promise<void> => {
await apiClient.post('/auth/logout');
},
// --- Управление серверами ---
getUserServers: async (): Promise<ServerShort[]> => {
const { data } = await apiClient.get<ServerShort[]>('/user/servers');
return data;
},
switchServer: async (serverId: string): Promise<{ status: string }> => {
const { data } = await apiClient.post<{ status: string }>(`/user/servers/active`, { server_id: serverId });
return data;
},
// --- Загрузка файлов для создания черновика ---
uploadFile: async (file: File): Promise<DraftInvoice> => {
const formData = new FormData();
formData.append('file', file);
const { data } = await apiClient.post<DraftInvoice>('/drafts/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
return data;
},
};
```
# ===================================================================
# Файл: src/shared/features/invoices/CreateContainerModal.tsx
# ===================================================================
```
import React, { useState } from "react";
import { Modal, Form, Input, InputNumber, Button, message } from "antd";
import { api } from "../../api";
import type { ProductContainer } from "../../types";
interface Props {
visible: boolean;
onCancel: () => void;
productId: string;
productBaseUnit: string;
// Callback возвращает уже полный объект с ID от сервера
onSuccess: (container: ProductContainer) => void;
}
export const CreateContainerModal: React.FC<Props> = ({
visible,
onCancel,
productId,
productBaseUnit,
onSuccess,
}) => {
const [loading, setLoading] = useState(false);
const [form] = Form.useForm();
const handleOk = async () => {
try {
const values = await form.validateFields();
setLoading(true);
// 1. Отправляем запрос на БЭКЕНД
const res = await api.createContainer({
product_id: productId,
name: values.name,
count: values.count,
});
message.success("Фасовка создана");
// 2. БЭКЕНД вернул ID. Теперь мы собираем объект для UI
// Мы не придумываем ID сами, мы берем res.container_id
const newContainer: ProductContainer = {
id: res.container_id, // <--- ID от сервера
name: values.name,
count: values.count,
};
// 3. Возвращаем полный объект родителю
onSuccess(newContainer);
form.resetFields();
} catch {
message.error("Ошибка создания фасовки");
} finally {
setLoading(false);
}
};
return (
<Modal
title="Новая фасовка"
open={visible}
onCancel={onCancel}
footer={[
<Button key="back" onClick={onCancel}>
Отмена
</Button>,
<Button
key="submit"
type="primary"
loading={loading}
onClick={handleOk}
>
Создать
</Button>,
]}
>
<Form form={form} layout="vertical">
<Form.Item
name="name"
label="Название"
rules={[
{ required: true, message: "Введите название, например 0.5" },
]}
>
<Input placeholder="Например: Бутылка 0.5" />
</Form.Item>
<Form.Item
name="count"
label={`Количество в базовых ед. (${productBaseUnit})`}
rules={[{ required: true, message: "Введите коэффициент" }]}
>
<InputNumber
style={{ width: "100%" }}
step={0.001}
placeholder="0.5"
/>
</Form.Item>
</Form>
</Modal>
);
};
```
# ===================================================================
# Файл: src/shared/features/invoices/DraftEditor.tsx
# ===================================================================
```
import React, { useEffect, useMemo, useState, useRef } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import {
Spin,
Alert,
Button,
Form,
Select,
DatePicker,
Input,
Typography,
message,
Row,
Col,
Modal,
Tag,
Image,
Checkbox,
} from "antd";
import {
// CheckOutlined,
DeleteOutlined,
ExclamationCircleFilled,
StopOutlined,
ArrowLeftOutlined,
PlusOutlined,
FileImageOutlined,
FileExcelOutlined,
SwapOutlined,
} from "@ant-design/icons";
import dayjs from "dayjs";
import { api, getStaticUrl } from "../../api";
import { DraftItemRow } from "./DraftItemRow";
import ExcelPreviewModal from "../../ui/ExcelPreviewModal";
import { useActiveDraftStore } from "../../stores/activeDraftStore";
import type {
DraftItem,
UpdateDraftRequest,
CommitDraftRequest,
ReorderDraftItemsRequest,
Store,
Supplier,
Recommendation,
DictionariesResponse,
} from "../../types";
import { DragDropContext, Droppable, type DropResult } from "@hello-pangea/dnd";
const { Text } = Typography;
const { TextArea } = Input;
const { confirm } = Modal;
interface DraftEditorProps {
draftId: string;
onBack?: () => void;
}
export const DraftEditor: React.FC<DraftEditorProps> = ({
draftId,
onBack,
}) => {
const queryClient = useQueryClient();
const [form] = Form.useForm();
// Zustand стор для локального управления элементами черновика
const {
items,
isDirty,
setItems,
updateItem,
deleteItem,
addItem,
reorderItems,
resetDirty,
markAsDirty,
} = useActiveDraftStore();
// Отслеживаем текущий draftId для инициализации стора
const currentDraftIdRef = useRef<string | null>(null);
const [isDragging, setIsDragging] = useState(false);
const [isReordering, setIsReordering] = useState(false);
const [isSaving, setIsSaving] = useState(false);
// Состояние для просмотра фото чека
const [previewVisible, setPreviewVisible] = useState(false);
// Состояние для просмотра Excel файла
const [excelPreviewVisible, setExcelPreviewVisible] = useState(false);
// Состояние для чекбокса "Проведено"
const [isProcessed, setIsProcessed] = useState(true); // По умолчанию true для MVP
// --- ЗАПРОСЫ ---
const dictQuery = useQuery({
queryKey: ["dictionaries"],
queryFn: api.getDictionaries,
staleTime: 1000 * 60 * 5,
});
const recommendationsQuery = useQuery({
queryKey: ["recommendations"],
queryFn: api.getRecommendations,
});
const draftQuery = useQuery({
queryKey: ["draft", draftId],
queryFn: () => api.getDraft(draftId),
enabled: !!draftId,
refetchInterval: (query) => {
if (isDragging) return false;
const status = query.state.data?.status;
return status === "PROCESSING" ? 3000 : false;
},
});
const draft = draftQuery.data;
const stores =
(dictQuery.data as DictionariesResponse | undefined)?.stores || [];
const suppliers =
(dictQuery.data as DictionariesResponse | undefined)?.suppliers || [];
// Определение типа файла по расширению
const isExcelFile = draft?.photo_url?.toLowerCase().match(/\.(xls|xlsx)$/);
// --- МУТАЦИИ ---
const addItemMutation = useMutation({
mutationFn: () => api.addDraftItem(draftId),
onSuccess: (newItem) => {
message.success("Строка добавлена");
// Добавляем новый элемент в стор
addItem(newItem);
},
onError: () => {
message.error("Ошибка создания строки");
},
});
const commitMutation = useMutation({
mutationFn: (payload: CommitDraftRequest) =>
api.commitDraft(draftId, payload),
onSuccess: (data: { document_number: string }) => {
message.success(`Накладная ${data.document_number} создана!`);
queryClient.invalidateQueries({ queryKey: ["drafts"] });
},
onError: () => {
message.error("Ошибка при создании накладной");
},
});
const deleteDraftMutation = useMutation({
mutationFn: () => api.deleteDraft(draftId),
onSuccess: () => {
if (draft?.status === "CANCELED") {
message.info("Черновик удален окончательно");
} else {
message.warning("Черновик отменен");
queryClient.invalidateQueries({ queryKey: ["draft", draftId] });
}
},
onError: () => {
message.error("Ошибка при удалении");
},
});
const reorderItemsMutation = useMutation({
mutationFn: ({
draftId: id,
payload,
}: {
draftId: string;
payload: ReorderDraftItemsRequest;
}) => api.reorderDraftItems(id, payload),
onError: (error) => {
message.error("Не удалось изменить порядок элементов");
console.error("Reorder error:", error);
},
});
// --- ЭФФЕКТЫ ---
// Инициализация данных при загрузке черновика
useEffect(() => {
if (draft) {
// Инициализируем только если изменился draftId или стор пуст
if (currentDraftIdRef.current !== draft.id || items.length === 0) {
// 1. Инициализация строк (Store)
setItems(draft.items || []);
// 2. Инициализация шапки (Form)
form.setFieldsValue({
store_id: draft.store_id,
supplier_id: draft.supplier_id,
comment: draft.comment,
incoming_document_number: draft.incoming_document_number,
date_incoming: draft.date_incoming
? dayjs(draft.date_incoming)
: dayjs(),
});
currentDraftIdRef.current = draft.id;
}
}
}, [draft, items.length, setItems, form]);
// --- ХЕЛПЕРЫ ---
const totalSum = useMemo(() => {
return (
items.reduce(
(acc: number, item: DraftItem) =>
acc + Number(item.quantity) * Number(item.price),
0
) || 0
);
}, [items]);
const invalidItemsCount = useMemo(() => {
return items.filter((i: DraftItem) => !i.product_id).length || 0;
}, [items]);
// Функция сохранения изменений на сервер
const saveChanges = async () => {
if (!isDirty) return;
setIsSaving(true);
try {
// Собираем значения формы для обновления шапки черновика
const formValues = form.getFieldsValue();
// Формируем единый payload для пакетного обновления (шапка + элементы)
const payload: UpdateDraftRequest = {
store_id: formValues.store_id,
supplier_id: formValues.supplier_id,
comment: formValues.comment || "",
incoming_document_number: formValues.incoming_document_number || "",
date_incoming: formValues.date_incoming
? formValues.date_incoming.format("YYYY-MM-DD")
: undefined,
items: items.map((item: DraftItem) => ({
id: item.id,
product_id: item.product_id ?? "",
container_id: item.container_id ?? "",
quantity: Number(item.quantity),
price: Number(item.price),
sum: Number(item.sum),
})),
};
// Отправляем единый запрос на сервер
await api.updateDraft(draftId, payload);
// После успешного сохранения обновляем данные с сервера
await queryClient.invalidateQueries({ queryKey: ["draft", draftId] });
// Сбрасываем флаг isDirty
resetDirty();
message.success("Изменения сохранены");
} catch (error) {
console.error("Ошибка сохранения:", error);
message.error("Не удалось сохранить изменения");
throw error;
} finally {
setIsSaving(false);
}
};
const handleItemUpdate = (id: string, changes: Partial<DraftItem>) => {
// Обновляем локально через стор
updateItem(id, changes);
};
const handleCommit = async () => {
try {
const values = await form.validateFields();
if (invalidItemsCount > 0) {
message.warning(
`Осталось ${invalidItemsCount} нераспознанных товаров!`
);
return;
}
// Сначала сохраняем изменения, если есть
if (isDirty) {
await saveChanges();
}
commitMutation.mutate({
date_incoming: values.date_incoming.format("YYYY-MM-DD"),
store_id: values.store_id,
supplier_id: values.supplier_id,
comment: values.comment || "",
incoming_document_number: values.incoming_document_number || "",
is_processed: isProcessed,
});
} catch {
message.error("Заполните обязательные поля (Склад, Поставщик)");
}
};
const isCanceled = draft?.status === "CANCELED";
const isCompleted = draft?.status === "COMPLETED";
const handleBack = () => {
if (isDirty) {
confirm({
title: "Сохранить изменения?",
icon: <ExclamationCircleFilled style={{ color: "#1890ff" }} />,
content:
"У вас есть несохраненные изменения. Сохранить их перед выходом?",
okText: "Сохранить и выйти",
cancelText: "Выйти без сохранения",
onOk: async () => {
try {
await saveChanges();
onBack?.();
} catch {
// Ошибка уже обработана в saveChanges
}
},
onCancel: () => {
onBack?.();
},
});
} else {
onBack?.();
}
};
const handleDelete = () => {
confirm({
title: isCanceled ? "Удалить окончательно?" : "Отменить черновик?",
icon: <ExclamationCircleFilled style={{ color: "red" }} />,
content: isCanceled
? "Черновик пропадет из списка навсегда."
: 'Черновик получит статус "Отменен", но останется в списке.',
okText: isCanceled ? "Удалить навсегда" : "Отменить",
okType: "danger",
cancelText: "Назад",
onOk() {
deleteDraftMutation.mutate();
},
});
};
const handleDragStart = () => {
setIsDragging(true);
};
const handleDragEnd = async (result: DropResult) => {
setIsDragging(false);
const { source, destination } = result;
// Если нет назначения или позиция не изменилась
if (
!destination ||
(source.droppableId === destination.droppableId &&
source.index === destination.index)
) {
return;
}
if (!draft) return;
// Обновляем локальное состояние через стор
reorderItems(source.index, destination.index);
// Подготавливаем payload для API
const reorderPayload: ReorderDraftItemsRequest = {
items: items.map((item: DraftItem, index: number) => ({
id: item.id,
order: index,
})),
};
// Отправляем запрос на сервер
try {
await reorderItemsMutation.mutateAsync({
draftId: draft.id,
payload: reorderPayload,
});
} catch {
// При ошибке откатываем локальное состояние через стор
reorderItems(destination.index, source.index);
message.error("Не удалось изменить порядок элементов");
}
};
// --- RENDER ---
const showSpinner =
draftQuery.isLoading ||
(draft?.status === "PROCESSING" && items.length === 0);
if (showSpinner) {
return (
<div style={{ textAlign: "center", padding: 50 }}>
<Spin size="large" />
</div>
);
}
if (draftQuery.isError || !draft) {
return <Alert type="error" message="Ошибка загрузки черновика" />;
}
return (
<div
style={{
height: "100%",
display: "flex",
flexDirection: "column",
fontFamily:
"-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif",
backgroundColor: "#f5f5f5",
}}
>
{/* Единый хедер */}
<div
style={{
padding: "8px 12px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 8,
flexShrink: 0,
background: "#fff",
borderBottom: "1px solid #f0f0f0",
}}
>
{/* Левая часть: Кнопка назад + Номер + Статус */}
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
flex: 1,
minWidth: 0,
}}
>
{onBack && (
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={handleBack}
size="small"
style={{ flexShrink: 0 }}
/>
)}
<div
style={{
display: "flex",
alignItems: "center",
gap: 6,
flexWrap: "wrap",
minWidth: 0,
}}
>
<Text
style={{ fontSize: 16, fontWeight: "bold", whiteSpace: "nowrap" }}
>
{draft.document_number ? `№${draft.document_number}` : "Черновик"}
</Text>
{draft.status === "PROCESSING" && <Spin size="small" />}
{isCanceled && (
<Tag color="red" style={{ margin: 0, fontSize: 11 }}>
ОТМЕНЕН
</Tag>
)}
</div>
</div>
{/* Правая часть: Кнопки действий */}
<div style={{ display: "flex", gap: 6, flexShrink: 0 }}>
{/* Кнопка сохранения (показывается только если есть несохраненные изменения) */}
{isDirty && (
<Button
type="primary"
onClick={saveChanges}
loading={isSaving}
size="small"
>
Сохранить
</Button>
)}
{/* Кнопка просмотра чека (только если есть URL) */}
{draft.photo_url && (
<Button
icon={isExcelFile ? <FileExcelOutlined /> : <FileImageOutlined />}
onClick={() =>
isExcelFile
? setExcelPreviewVisible(true)
: setPreviewVisible(true)
}
size="small"
type="text"
/>
)}
{/* Кнопка переключения режима перетаскивания */}
<Button
type={isReordering ? "primary" : "default"}
icon={<SwapOutlined rotate={90} />}
onClick={() => setIsReordering(!isReordering)}
size="small"
/>
{/* Кнопка удаления/отмены */}
<Button
danger={isCanceled}
type={isCanceled ? "primary" : "default"}
icon={isCanceled ? <DeleteOutlined /> : <StopOutlined />}
onClick={handleDelete}
loading={deleteDraftMutation.isPending}
size="small"
/>
</div>
</div>
{/* Основная часть с прокруткой */}
<div style={{ flex: 1, overflowY: "auto", padding: "8px 12px" }}>
{/* Form: Склады и Поставщики */}
<div
style={{
background: "#fff",
padding: 6,
borderRadius: 8,
marginBottom: 12,
opacity: isCanceled ? 0.6 : 1,
}}
>
<Form
form={form}
layout="vertical"
onValuesChange={() => markAsDirty()}
>
<Row gutter={[4, 4]}>
<Col span={12}>
<Form.Item
name="date_incoming"
label="Дата"
rules={[{ required: true }]}
style={{ marginBottom: 0 }}
>
<DatePicker
style={{ width: "100%" }}
format="DD.MM.YYYY"
placeholder=""
size="small"
/>
</Form.Item>
</Col>
<Col span={12}>
<Form.Item
name="incoming_document_number"
label="№ входящего"
style={{ marginBottom: 0 }}
>
<Input placeholder="" size="small" />
</Form.Item>
</Col>
</Row>
<Row gutter={[4, 4]}>
<Col span={24}>
<Form.Item
name="store_id"
rules={[{ required: true, message: "Выберите склад" }]}
style={{ marginBottom: 0 }}
>
<Select
placeholder="Выберите склад..."
loading={dictQuery.isLoading}
options={stores.map((s: Store) => ({
label: s.name,
value: s.id,
}))}
size="small"
/>
</Form.Item>
</Col>
</Row>
<Form.Item
name="supplier_id"
rules={[{ required: true, message: "Выберите поставщика" }]}
style={{ marginBottom: 8 }}
>
<Select
placeholder="Поставщик..."
loading={dictQuery.isLoading}
options={suppliers.map((s: Supplier) => ({
label: s.name,
value: s.id,
}))}
size="small"
showSearch
filterOption={(input, option) =>
String(option?.label ?? "")
.toLowerCase()
.includes(String(input).toLowerCase())
}
/>
</Form.Item>
<Form.Item name="comment" style={{ marginBottom: 0 }}>
<TextArea
rows={1}
placeholder="Комментарий..."
style={{ fontSize: 13 }}
size="small"
/>
</Form.Item>
</Form>
</div>
{/* Items Header */}
<div
style={{
marginBottom: 8,
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "0 4px",
}}
>
<Text strong style={{ fontSize: 14 }}>
Позиции ({items.length})
</Text>
{invalidItemsCount > 0 && (
<Text type="danger" style={{ fontSize: 12 }}>
{invalidItemsCount} нераспознано
</Text>
)}
</div>
{/* Items List */}
<DragDropContext
onDragStart={handleDragStart}
onDragEnd={handleDragEnd}
>
<Droppable droppableId="draft-items">
{(provided, snapshot) => (
<div
{...provided.droppableProps}
ref={provided.innerRef}
style={{
display: "flex",
flexDirection: "column",
gap: 8,
backgroundColor: snapshot.isDraggingOver
? "#f0f0f0"
: "transparent",
borderRadius: "4px",
padding: snapshot.isDraggingOver ? "8px" : "0",
transition: "background-color 0.2s ease",
}}
>
{items.map((item: DraftItem, index: number) => (
<DraftItemRow
key={item.id}
item={item}
index={index}
onLocalUpdate={handleItemUpdate}
onDelete={(itemId) => deleteItem(itemId)}
isUpdating={false}
recommendations={
(recommendationsQuery.data as
| Recommendation[]
| undefined) || []
}
isReordering={isReordering}
/>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
{/* Кнопка добавления позиции */}
<Button
type="dashed"
block
icon={<PlusOutlined />}
style={{ marginTop: 12, marginBottom: 80, height: 40 }}
onClick={() => addItemMutation.mutate()}
loading={addItemMutation.isPending}
disabled={isCanceled}
size="small"
>
Добавить товар
</Button>
</div>
{/* Footer Actions - прижат к низу контейнера */}
<div
style={{
flexShrink: 0,
background: "#fff",
padding: "8px 12px",
borderTop: "1px solid #f0f0f0",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<div style={{ display: "flex", flexDirection: "column" }}>
<Text style={{ fontSize: 11, color: "#888", lineHeight: 1 }}>
Итого:
</Text>
<Text
style={{
fontSize: 16,
fontWeight: "bold",
color: "#1890ff",
lineHeight: 1.2,
}}
>
{totalSum.toLocaleString("ru-RU", {
style: "currency",
currency: "RUB",
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}
</Text>
</div>
<Button
type="primary"
// icon={<CheckOutlined />}
onClick={handleCommit}
loading={commitMutation.isPending}
disabled={invalidItemsCount > 0 || isCanceled}
style={{
position: "relative",
paddingLeft: 40,
height: 36,
}}
size="small"
>
<Checkbox
checked={isProcessed}
onChange={(e) => setIsProcessed(e.target.checked)}
onClick={(e) => e.stopPropagation()}
disabled={invalidItemsCount > 0 || isCanceled}
style={{
position: "absolute",
left: 10,
top: "50%",
transform: "translateY(-50%)",
pointerEvents: "auto",
}}
/>
<span style={{ marginLeft: 8 }}>
{isCanceled
? "Восстановить"
: isCompleted
? "Обновить в iiko"
: isProcessed
? "Провести и отправить"
: "Сохранить (без проведения)"}
</span>
</Button>
</div>
{/* Скрытый компонент для просмотра изображения */}
{draft.photo_url && (
<div style={{ display: "none" }}>
<Image.PreviewGroup
preview={{
visible: previewVisible,
onVisibleChange: (vis) => setPreviewVisible(vis),
movable: true,
scaleStep: 0.5,
}}
>
<Image src={getStaticUrl(draft.photo_url)} />
</Image.PreviewGroup>
</div>
)}
{/* Модальное окно для просмотра Excel файлов */}
<ExcelPreviewModal
visible={excelPreviewVisible}
onCancel={() => setExcelPreviewVisible(false)}
fileUrl={draft.photo_url || ""}
/>
</div>
);
};
```
# ===================================================================
# Файл: src/shared/features/invoices/DraftItemRow.tsx
# ===================================================================
```
import React, { useMemo, useState } from "react";
import { Draggable } from "@hello-pangea/dnd";
import {
Card,
Flex,
InputNumber,
Typography,
Select,
Tag,
Button,
Divider,
Modal,
Popconfirm,
} from "antd";
import {
SyncOutlined,
PlusOutlined,
WarningFilled,
DeleteOutlined,
} from "@ant-design/icons";
import { GripVertical } from "lucide-react";
import { CatalogSelect } from "../ocr/CatalogSelect";
import { CreateContainerModal } from "./CreateContainerModal";
import type {
DraftItem,
ProductSearchResult,
ProductContainer,
Recommendation,
} from "../../types";
const { Text } = Typography;
interface DraftItemRowProps {
item: DraftItem;
index: number;
onLocalUpdate: (id: string, changes: Partial<DraftItem>) => void;
onDelete: (itemId: string) => void;
isUpdating: boolean;
recommendations?: Recommendation[];
isReordering: boolean;
}
export const DraftItemRow: React.FC<DraftItemRowProps> = ({
item,
index,
onLocalUpdate,
onDelete,
isUpdating,
recommendations = [],
isReordering,
}) => {
const [isModalOpen, setIsModalOpen] = useState(false);
const activeProduct = useMemo(() => {
return item.product as unknown as ProductSearchResult | undefined;
}, [item.product]);
const containers = useMemo(() => {
if (!activeProduct) return [];
return activeProduct.containers || [];
}, [activeProduct]);
const baseUom =
activeProduct?.main_unit?.name || activeProduct?.measure_unit || "ед.";
const containerOptions = useMemo(() => {
if (!activeProduct) return [];
const opts = [
{ value: "BASE_UNIT", label: `Базовая (${baseUom})` },
...containers.map((c) => ({
value: c.id,
label: `${c.name} (=${Number(c.count)} ${baseUom})`,
})),
];
if (
item.container_id &&
item.container &&
!containers.find((c) => c.id === item.container_id)
) {
opts.push({
value: item.container.id,
label: `${item.container.name} (=${Number(
item.container.count
)} ${baseUom})`,
});
}
return opts;
}, [activeProduct, containers, baseUom, item.container_id, item.container]);
// --- WARNING LOGIC ---
const activeWarning = useMemo(() => {
if (!item.product_id) return null;
return recommendations.find((r) => r.ProductID === item.product_id);
}, [item.product_id, recommendations]);
const showWarningModal = () => {
if (!activeWarning) return;
Modal.warning({
title: "Внимание: проблемный товар",
content: (
<div>
<p>
<b>{activeWarning.ProductName}</b>
</p>
<p>{activeWarning.Reason}</p>
<p>
<Tag color="orange">{activeWarning.Type}</Tag>
</p>
</div>
),
okText: "Понятно",
maskClosable: true,
});
};
// --- Handlers ---
const handleProductChange = (
prodId: string | null,
productObj?: ProductSearchResult
) => {
onLocalUpdate(item.id, {
product_id: prodId,
product: prodId ? productObj : null,
container_id: null,
});
};
const handleContainerChange = (val: string) => {
const newVal = val === "BASE_UNIT" ? "" : val;
onLocalUpdate(item.id, {
container_id: newVal,
});
};
const handleContainerCreated = (newContainer: ProductContainer) => {
setIsModalOpen(false);
onLocalUpdate(item.id, { container_id: newContainer.id });
};
const handleValueChange = (
field: "quantity" | "price" | "sum",
val: number | null
) => {
if (val !== null) {
onLocalUpdate(item.id, {
[field]: Number(val),
});
}
};
const handleBlur = () => {
// Изменения уже отправлены через onLocalUpdate в handleValueChange
};
const cardBorderColor = !item.product_id
? "#ffa39e"
: item.is_matched
? "#b7eb8f"
: "#d9d9d9";
return (
<>
<Draggable
draggableId={item.id}
index={index}
isDragDisabled={!isReordering}
>
{(provided, snapshot) => {
const style = {
marginBottom: "8px",
backgroundColor: snapshot.isDragging ? "#e6f7ff" : "transparent",
boxShadow: snapshot.isDragging
? "0 4px 12px rgba(0, 0, 0, 0.15)"
: "none",
borderRadius: "4px",
transition: "background-color 0.2s ease, box-shadow 0.2s ease",
...provided.draggableProps.style,
};
return (
<div
ref={provided.innerRef}
{...provided.draggableProps}
style={style}
>
<Card
size="small"
style={{
display: "flex",
alignItems: "center",
padding: "12px 16px",
borderLeft: `4px solid ${cardBorderColor}`,
border: snapshot.isDragging
? "2px solid #1890ff"
: "1px solid #d9d9d9",
background: item.product_id ? "#fff" : "#fff1f0",
borderRadius: "4px",
}}
bodyStyle={{ padding: 0 }}
>
{/* Drag handle - иконка для перетаскивания (показываем только в режиме перетаскивания) */}
{isReordering && (
<div
{...provided.dragHandleProps}
style={{
cursor: "grab",
padding: "4px 8px 4px 0",
color: "#8c8c8c",
display: "flex",
alignItems: "center",
transition: "color 0.2s ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.color = "#1890ff";
}}
onMouseLeave={(e) => {
e.currentTarget.style.color = "#8c8c8c";
}}
>
<GripVertical size={20} />
</div>
)}
<Flex vertical gap={10} style={{ flex: 1 }}>
<Flex justify="space-between" align="start">
<div style={{ flex: 1 }}>
<Text
type="secondary"
style={{
fontSize: 12,
lineHeight: 1.2,
display: "block",
}}
>
{item.raw_name || "Новая позиция"}
</Text>
{item.raw_amount > 0 && (
<Text
type="secondary"
style={{ fontSize: 10, display: "block" }}
>
(чек: {item.raw_amount} x {item.raw_price})
</Text>
)}
</div>
<div
style={{
marginLeft: 8,
display: "flex",
alignItems: "center",
gap: 6,
}}
>
{isUpdating && (
<SyncOutlined spin style={{ color: "#1890ff" }} />
)}
{activeWarning && (
<WarningFilled
style={{
color: "#faad14",
fontSize: 16,
cursor: "pointer",
}}
onClick={showWarningModal}
/>
)}
{!item.product_id && (
<Tag color="error" style={{ margin: 0 }}>
?
</Tag>
)}
<Popconfirm
title="Удалить строку?"
onConfirm={() => onDelete(item.id)}
okText="Да"
cancelText="Нет"
placement="left"
>
<Button
type="text"
size="small"
icon={<DeleteOutlined />}
danger
style={{ marginLeft: 4 }}
/>
</Popconfirm>
</div>
</Flex>
<CatalogSelect
value={item.product_id || null}
onChange={handleProductChange}
initialProduct={activeProduct}
/>
{activeProduct && (
<Select
style={{ width: "100%" }}
placeholder="Выберите единицу измерения"
options={containerOptions}
value={item.container_id || "BASE_UNIT"}
onChange={handleContainerChange}
dropdownRender={(menu) => (
<>
{menu}
<Divider style={{ margin: "4px 0" }} />
<Button
type="text"
block
icon={<PlusOutlined />}
onClick={() => setIsModalOpen(true)}
style={{ textAlign: "left" }}
>
Добавить фасовку...
</Button>
</>
)}
/>
)}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
background: "#fafafa",
margin: "0 -12px -12px -12px",
padding: "8px 12px",
borderTop: "1px solid #f0f0f0",
borderBottomLeftRadius: 8,
borderBottomRightRadius: 8,
}}
>
<div
style={{
display: "flex",
gap: 8,
alignItems: "center",
flex: 1,
}}
>
<InputNumber
style={{ width: 70 }}
controls={false}
placeholder="Кол"
min={0}
value={item.quantity}
onChange={(val) => handleValueChange("quantity", val)}
onBlur={() => handleBlur()}
precision={3}
parser={(value) =>
value?.replace(",", ".") as unknown as number
}
/>
<Text type="secondary">x</Text>
<InputNumber
style={{ width: 80 }}
controls={false}
placeholder="Цена"
min={0}
value={item.price}
onChange={(val) => handleValueChange("price", val)}
onBlur={() => handleBlur()}
precision={2}
parser={(value) =>
value?.replace(",", ".") as unknown as number
}
/>
</div>
<div
style={{ display: "flex", alignItems: "center", gap: 4 }}
>
<Text type="secondary">=</Text>
<InputNumber
style={{ width: 90, fontWeight: "bold" }}
controls={false}
placeholder="Сумма"
min={0}
value={item.sum}
onChange={(val) => handleValueChange("sum", val)}
onBlur={() => handleBlur()}
precision={2}
parser={(value) =>
value?.replace(",", ".") as unknown as number
}
/>
</div>
</div>
</Flex>
</Card>
</div>
);
}}
</Draggable>
{activeProduct && (
<CreateContainerModal
visible={isModalOpen}
onCancel={() => setIsModalOpen(false)}
productId={activeProduct.id}
productBaseUnit={baseUom}
onSuccess={handleContainerCreated}
/>
)}
</>
);
};
```
# ===================================================================
# Файл: src/shared/features/invoices/DraftVerificationModal.tsx
# ===================================================================
```
import React, { useState } from "react";
import { Modal, Spin, Button, Typography, Alert, message } from "antd";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import { DragDropContext, Droppable } from "@hello-pangea/dnd";
import { api } from "../../api";
import { DraftItemRow } from "./DraftItemRow";
import type { UpdateDraftItemRequest, DraftItem } from "../../types";
const { Text } = Typography;
interface DraftVerificationModalProps {
draftId: string;
visible: boolean;
onClose: () => void;
}
/**
* Модальное окно для быстрой проверки черновика после загрузки файла
* Позволяет просмотреть и отредактировать позиции перед переходом к полному редактированию
*/
export const DraftVerificationModal: React.FC<DraftVerificationModalProps> = ({
draftId,
visible,
onClose,
}) => {
const navigate = useNavigate();
const queryClient = useQueryClient();
const [updatingItems, setUpdatingItems] = useState<Set<string>>(new Set());
// Получаем данные черновика
const draftQuery = useQuery({
queryKey: ["draft", draftId],
queryFn: () => api.getDraft(draftId),
enabled: visible && !!draftId,
refetchInterval: (query) => {
const status = query.state.data?.status;
return status === "PROCESSING" ? 3000 : false;
},
});
// Получаем рекомендации
const recommendationsQuery = useQuery({
queryKey: ["recommendations"],
queryFn: api.getRecommendations,
enabled: visible,
});
// Мутация для обновления строки
const updateItemMutation = useMutation({
mutationFn: (vars: { itemId: string; payload: UpdateDraftItemRequest }) =>
api.updateDraftItem(draftId, vars.itemId, vars.payload),
onMutate: async ({ itemId }) => {
setUpdatingItems((prev) => new Set(prev).add(itemId));
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["draft", draftId] });
},
onError: () => {
message.error("Не удалось сохранить строку");
},
onSettled: (_data, _err, vars) => {
setUpdatingItems((prev) => {
const next = new Set(prev);
next.delete(vars.itemId);
return next;
});
},
});
const draft = draftQuery.data;
const recommendations = recommendationsQuery.data || [];
const handleItemUpdate = (itemId: string, changes: Partial<DraftItem>) => {
// Преобразуем Partial<DraftItem> в UpdateDraftItemRequest
const payload: UpdateDraftItemRequest = {};
if (changes.product_id !== undefined) {
// product_id может быть null, но UpdateDraftItemRequest ожидает только UUID
// Если null, не включаем поле в payload
if (changes.product_id !== null) {
payload.product_id = changes.product_id;
}
}
if (changes.container_id !== undefined) {
payload.container_id = changes.container_id;
}
if (changes.quantity !== undefined) {
payload.quantity = changes.quantity;
}
if (changes.price !== undefined) {
payload.price = changes.price;
}
if (changes.sum !== undefined) {
payload.sum = changes.sum;
}
updateItemMutation.mutate({ itemId, payload });
};
const handleDeleteItem = () => {
// В модальном окне не реализуем удаление для упрощения
// Пользователь может перейти к полному редактированию
message.info("Для удаления позиции перейдите к полному редактированию");
};
const handleGoToFullEdit = () => {
onClose();
navigate(`/web/invoice/draft/${draftId}`);
};
const invalidItemsCount =
draft?.items.filter((i: DraftItem) => !i.product_id).length || 0;
return (
<Modal
title="Проверка черновика"
open={visible}
onCancel={onClose}
width={800}
footer={[
<Button key="close" onClick={onClose}>
Закрыть
</Button>,
<Button
key="edit"
type="primary"
onClick={handleGoToFullEdit}
disabled={draftQuery.isLoading}
>
Перейти к полному редактированию
</Button>,
]}
destroyOnClose
>
{draftQuery.isLoading ? (
<div style={{ textAlign: "center", padding: "40px 0" }}>
<Spin size="large" />
<div style={{ marginTop: "16px" }}>
<Text type="secondary">Обработка файла...</Text>
</div>
</div>
) : draftQuery.isError ? (
<Alert
message="Ошибка загрузки черновика"
description="Не удалось загрузить данные черновика. Попробуйте закрыть модальное окно и загрузить файл заново."
type="error"
showIcon
/>
) : draft ? (
<div>
{/* Информация о черновике */}
<div style={{ marginBottom: "16px" }}>
<Text strong>Номер: </Text>
<Text>{draft.document_number || "Не указан"}</Text>
<br />
<Text strong>Статус: </Text>
<Text>{draft.status}</Text>
{draft.photo_url && (
<>
<br />
<Text strong>Фото чека: </Text>
<Text type="secondary">Загружено</Text>
</>
)}
</div>
{/* Предупреждение о нераспознанных товарах */}
{invalidItemsCount > 0 && (
<Alert
message={`${invalidItemsCount} позиций не распознано`}
description="Пожалуйста, сопоставьте товары с каталогом перед отправкой"
type="warning"
showIcon
style={{ marginBottom: "16px" }}
/>
)}
{/* Список позиций */}
<div style={{ maxHeight: "400px", overflowY: "auto" }}>
{draft.items.length === 0 ? (
<div style={{ textAlign: "center", padding: "20px" }}>
<Text type="secondary">Нет позиций</Text>
</div>
) : (
<DragDropContext onDragEnd={() => {}}>
<Droppable droppableId="modal-verification-list">
{(provided) => (
<div ref={provided.innerRef} {...provided.droppableProps}>
{draft.items.map((item: DraftItem, index: number) => (
<DraftItemRow
key={item.id}
item={item}
index={index}
onLocalUpdate={handleItemUpdate}
onDelete={handleDeleteItem}
isUpdating={updatingItems.has(item.id)}
recommendations={recommendations}
isReordering={false}
/>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
)}
</div>
</div>
) : null}
</Modal>
);
};
```
# ===================================================================
# Файл: src/shared/features/invoices/InvoiceViewer.tsx
# ===================================================================
```
import React, { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { Spin, Alert, Button, Table, Typography, Tag, Image } from "antd";
import {
FileImageOutlined,
FileExcelOutlined,
HistoryOutlined,
ArrowLeftOutlined,
} from "@ant-design/icons";
import { api, getStaticUrl } from "../../api";
import type { DraftStatus } from "../../types";
import ExcelPreviewModal from "../../ui/ExcelPreviewModal";
const { Title, Text } = Typography;
interface InvoiceViewerProps {
invoiceId: string;
onBack?: () => void;
}
export const InvoiceViewer: React.FC<InvoiceViewerProps> = ({
invoiceId,
onBack,
}) => {
// Состояние для просмотра фото чека
const [previewVisible, setPreviewVisible] = useState(false);
// Состояние для просмотра Excel файла
const [excelPreviewVisible, setExcelPreviewVisible] = useState(false);
// Запрос данных накладной
const {
data: invoice,
isLoading,
isError,
} = useQuery({
queryKey: ["invoice", invoiceId],
queryFn: () => api.getInvoice(invoiceId),
enabled: !!invoiceId,
});
// Определение типа файла по расширению
const isExcelFile = invoice?.photo_url?.toLowerCase().match(/\.(xls|xlsx)$/);
const getStatusTag = (status: DraftStatus) => {
switch (status) {
case "PROCESSING":
return <Tag color="blue">Обработка</Tag>;
case "READY_TO_VERIFY":
return <Tag color="orange">Проверка</Tag>;
case "COMPLETED":
return (
<Tag icon={<HistoryOutlined />} color="success">
Синхронизировано
</Tag>
);
case "ERROR":
return <Tag color="red">Ошибка</Tag>;
case "CANCELED":
return <Tag color="default">Отменен</Tag>;
default:
return <Tag>{status}</Tag>;
}
};
if (isLoading) {
return (
<div style={{ textAlign: "center", padding: 50 }}>
<Spin size="large" />
</div>
);
}
if (isError || !invoice) {
return <Alert type="error" message="Ошибка загрузки накладной" />;
}
const columns = [
{
title: "Товар",
dataIndex: "name",
key: "name",
},
{
title: "Кол-во",
dataIndex: "quantity",
key: "quantity",
align: "right" as const,
},
{
title: "Сумма",
dataIndex: "total",
key: "total",
align: "right" as const,
render: (total: number) =>
total.toLocaleString("ru-RU", {
style: "currency",
currency: "RUB",
}),
},
];
const totalSum = (invoice.items || []).reduce(
(acc: number, item: { total: number }) => acc + item.total,
0
);
return (
<div
style={{
height: "100%",
display: "flex",
flexDirection: "column",
backgroundColor: "#f5f5f5",
}}
>
{/* Единый хедер */}
<div
style={{
padding: "8px 12px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 8,
flexShrink: 0,
background: "#fff",
borderBottom: "1px solid #f0f0f0",
}}
>
{/* Левая часть: Кнопка назад + Номер + Информация */}
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
flex: 1,
minWidth: 0,
}}
>
{onBack && (
<Button
type="text"
icon={<ArrowLeftOutlined />}
onClick={onBack}
size="small"
style={{ flexShrink: 0 }}
/>
)}
<div
style={{
display: "flex",
flexDirection: "column",
gap: 2,
minWidth: 0,
}}
>
<Title
level={5}
style={{ margin: 0, fontSize: 16, whiteSpace: "nowrap" }}
>
№{invoice.number}
</Title>
<Text
type="secondary"
style={{ fontSize: 11, whiteSpace: "nowrap" }}
>
{invoice.date} • {invoice.supplier.name}
</Text>
</div>
</div>
{/* Правая часть: Статус + Кнопка фото */}
<div
style={{
display: "flex",
gap: 6,
alignItems: "center",
flexShrink: 0,
}}
>
{getStatusTag(invoice.status)}
{/* Кнопка просмотра чека (только если есть URL) */}
{invoice.photo_url && (
<Button
icon={isExcelFile ? <FileExcelOutlined /> : <FileImageOutlined />}
onClick={() =>
isExcelFile
? setExcelPreviewVisible(true)
: setPreviewVisible(true)
}
size="small"
type="text"
/>
)}
</div>
</div>
{/* Основная часть с прокруткой */}
<div style={{ flex: 1, overflowY: "auto", padding: "8px 12px" }}>
{/* Таблица товаров */}
<div
style={{
background: "#fff",
padding: 12,
borderRadius: 8,
marginBottom: 16,
}}
>
<Title level={5} style={{ marginBottom: 12, fontSize: 14 }}>
Товары ({(invoice.items || []).length} поз.)
</Title>
<Table
columns={columns}
dataSource={invoice.items || []}
pagination={false}
rowKey="name"
size="small"
summary={() => (
<Table.Summary.Row>
<Table.Summary.Cell index={0} colSpan={2}>
<Text strong>Итого:</Text>
</Table.Summary.Cell>
<Table.Summary.Cell index={2} align="right">
<Text strong>
{totalSum.toLocaleString("ru-RU", {
style: "currency",
currency: "RUB",
})}
</Text>
</Table.Summary.Cell>
</Table.Summary.Row>
)}
/>
</div>
</div>
{/* Скрытый компонент для просмотра изображения */}
{invoice.photo_url && (
<div style={{ display: "none" }}>
<Image.PreviewGroup
preview={{
visible: previewVisible,
onVisibleChange: (vis) => setPreviewVisible(vis),
movable: true,
scaleStep: 0.5,
}}
>
<Image src={getStaticUrl(invoice.photo_url)} />
</Image.PreviewGroup>
</div>
)}
{/* Модальное окно для просмотра Excel файлов */}
<ExcelPreviewModal
visible={excelPreviewVisible}
onCancel={() => setExcelPreviewVisible(false)}
fileUrl={invoice.photo_url || ""}
/>
</div>
);
};
```
# ===================================================================
# Файл: src/shared/features/ocr/AddMatchForm.tsx
# ===================================================================
```
import React, { useState, useMemo, useEffect } from "react";
import {
Card,
Button,
Flex,
AutoComplete,
Input,
InputNumber,
Typography,
Select,
Divider,
Popconfirm,
} from "antd";
import {
PlusOutlined,
DeleteOutlined,
EditOutlined,
CloseOutlined,
} from "@ant-design/icons";
import { CatalogSelect } from "./CatalogSelect";
import { CreateContainerModal } from "../invoices/CreateContainerModal";
import type {
CatalogItem,
UnmatchedItem,
ProductSearchResult,
ProductContainer,
ProductMatch,
} from "../../types";
const { Text } = Typography;
interface Props {
catalog: CatalogItem[];
unmatched?: UnmatchedItem[];
onSave: (
rawName: string,
productId: string,
quantity: number,
containerId?: string
) => void;
onDeleteUnmatched?: (rawName: string) => void;
isLoading: boolean;
initialValues?: ProductMatch; // Для редактирования
onCancelEdit?: () => void; // Для сброса режима редактирования
}
export const AddMatchForm: React.FC<Props> = ({
catalog,
unmatched = [],
onSave,
onDeleteUnmatched,
isLoading,
initialValues,
onCancelEdit,
}) => {
// --- Состояния ---
const [rawName, setRawName] = useState("");
const [selectedProduct, setSelectedProduct] = useState<string | undefined>(
undefined
);
// Храним полный объект товара, чтобы достать из него фасовки и имя для отображения
const [selectedProductData, setSelectedProductData] = useState<
ProductSearchResult | undefined
>(undefined);
const [quantity, setQuantity] = useState<number | null>(1);
const [selectedContainer, setSelectedContainer] = useState<string | null>(
null
);
const [isModalOpen, setIsModalOpen] = useState(false);
// --- Эффект для инициализации полей при редактировании ---
useEffect(() => {
if (initialValues) {
// eslint-disable-next-line
setRawName(initialValues.raw_name || "");
const prodId = initialValues.product?.id;
setSelectedProduct(prodId);
// Важно: восстанавливаем объект продукта из initialValues
// Приводим тип, так как DTO могут немного отличаться, но нам нужны containers и name
const prodData = initialValues.product as unknown as ProductSearchResult;
setSelectedProductData(prodData);
setQuantity(Number(initialValues.quantity) || 1);
setSelectedContainer(initialValues.container?.id || null);
} else {
// РЕЖИМ СОЗДАНИЯ (Сброс)
setRawName("");
setSelectedProduct(undefined);
setSelectedProductData(undefined);
setQuantity(1);
setSelectedContainer(null);
}
}, [initialValues]);
// --- Вычисляемые значения ---
const unmatchedOptions = useMemo(() => {
return unmatched.map((item) => ({
value: item.raw_name,
label: item.count ? `${item.raw_name} (${item.count} шт)` : item.raw_name,
}));
}, [unmatched]);
// Активный продукт: либо то, что выбрали в поиске, либо то, что пришло из редактирования
const activeProduct = useMemo(() => {
if (selectedProductData) return selectedProductData;
// Фоллбэк: пытаемся найти в общем каталоге (если он загружен полностью, что редко)
if (selectedProduct && catalog.length > 0) {
return catalog.find(
(item) => item.id === selectedProduct
) as unknown as ProductSearchResult;
}
return undefined;
}, [selectedProduct, selectedProductData, catalog]);
// Список контейнеров текущего товара
const containers = useMemo(() => {
return activeProduct?.containers || [];
}, [activeProduct]);
// Базовая единица
const baseUom =
activeProduct?.main_unit?.name || activeProduct?.measure_unit || "ед.";
// Текстовое отображение текущей единицы (для инпута количества)
const currentUomName = useMemo(() => {
if (selectedContainer) {
const cont = containers.find((c) => c.id === selectedContainer);
return cont ? cont.name : baseUom;
}
return baseUom;
}, [selectedContainer, containers, baseUom]);
const isButtonDisabled =
!rawName.trim() ||
!selectedProduct ||
quantity === null ||
quantity <= 0 ||
isLoading;
// --- Хендлеры ---
const handleProductChange = (
val: string | null,
productObj?: ProductSearchResult
) => {
setSelectedProduct(val || undefined);
if (productObj) {
setSelectedProductData(productObj);
}
// При смене товара сбрасываем фасовку
setSelectedContainer(null);
};
const handleSubmit = () => {
let quantityValue = quantity;
// Защита от null/строк
if (quantityValue === null || quantityValue === undefined) {
quantityValue = 1;
} else if (typeof quantityValue === "string") {
quantityValue = parseFloat(quantityValue);
}
if (isNaN(quantityValue) || quantityValue <= 0) {
quantityValue = 1;
}
if (rawName.trim() && selectedProduct) {
onSave(
rawName,
selectedProduct,
quantityValue,
selectedContainer || undefined
);
// Если это не редактирование, очищаем форму
if (!initialValues) {
setRawName("");
setSelectedProduct(undefined);
setSelectedProductData(undefined);
setQuantity(1);
setSelectedContainer(null);
}
}
};
const handleContainerCreated = (newContainer: ProductContainer) => {
setIsModalOpen(false);
// Добавляем созданную фасовку в локальный стейт продукта
if (selectedProductData) {
setSelectedProductData({
...selectedProductData,
containers: [...(selectedProductData.containers || []), newContainer],
});
} else if (activeProduct) {
setSelectedProductData({
...activeProduct,
containers: [...(activeProduct.containers || []), newContainer],
});
}
// Выбираем новую фасовку
setSelectedContainer(newContainer.id);
};
const handleDeleteUnmatched = () => {
if (onDeleteUnmatched && rawName.trim()) {
onDeleteUnmatched(rawName);
setRawName("");
}
};
// Кнопка "Сбросить" вызывает внешний обработчик отмены редактирования
const handleCancel = () => {
if (onCancelEdit) {
onCancelEdit();
}
};
return (
<Card
title={
initialValues ? "✏️ Редактирование связи" : " Добавить новую связь"
}
size="small"
style={{
marginBottom: 16,
borderColor: initialValues ? "#1890ff" : undefined, // Подсветка при редактировании
background: initialValues ? "#f0f5ff" : undefined,
}}
>
<Flex vertical gap="middle">
{/* Поле: Текст из чека */}
<div>
<div style={{ marginBottom: 4, fontSize: 12, color: "#888" }}>
Текст из чека (Raw Name):
</div>
<div style={{ display: "flex", gap: 8 }}>
<AutoComplete
options={unmatchedOptions}
value={rawName}
onChange={setRawName}
filterOption={(inputValue, option) =>
!inputValue ||
(option?.value as string)
.toLowerCase()
.includes(inputValue.toLowerCase())
}
style={{ flex: 1 }}
>
<Input.TextArea
placeholder="Например: Масло слив. коробка"
autoSize={{ minRows: 1, maxRows: 4 }}
/>
</AutoComplete>
{onDeleteUnmatched && !initialValues && (
<Popconfirm
title="Удалить строку?"
description="Удалить из списка нераспознанных?"
onConfirm={handleDeleteUnmatched}
okText="Да"
cancelText="Нет"
>
<Button
danger
icon={<DeleteOutlined />}
disabled={!rawName.trim()}
title="Удалить мусорную строку"
/>
</Popconfirm>
)}
</div>
</div>
{/* Поле: Товар */}
<div>
<div style={{ marginBottom: 4, fontSize: 12, color: "#888" }}>
Товар в iiko:
</div>
<CatalogSelect
value={selectedProduct || null}
onChange={handleProductChange}
disabled={isLoading}
initialProduct={activeProduct} // Передаем полный объект для правильного отображения!
/>
</div>
{/* Поле: Фасовка */}
{activeProduct && (
<div>
<div style={{ marginBottom: 4, fontSize: 12, color: "#888" }}>
Единица измерения / Фасовка:
</div>
<Select
style={{ width: "100%" }}
value={selectedContainer}
onChange={setSelectedContainer}
placeholder="Выберите ед. измерения"
options={[
{ value: null, label: `Базовая единица (${baseUom})` },
...containers.map((c) => ({
value: c.id,
label: `${c.name} (=${Number(c.count)} ${baseUom})`,
})),
]}
dropdownRender={(menu) => (
<>
{menu}
<Divider style={{ margin: "4px 0" }} />
<Button
type="text"
block
icon={<PlusOutlined />}
onClick={() => setIsModalOpen(true)}
style={{ textAlign: "left" }}
>
Добавить фасовку
</Button>
</>
)}
/>
</div>
)}
{/* Поле: Количество */}
<div>
<div style={{ marginBottom: 4, fontSize: 12, color: "#888" }}>
Коэффициент (сколько товара в одной позиции чека):
</div>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<InputNumber
min={0.001}
step={selectedContainer ? 1 : 0.1}
value={quantity}
onChange={(val) => setQuantity(Number(val))}
style={{ flex: 1 }}
placeholder="1"
/>
<Text strong>{currentUomName}</Text>
</div>
</div>
{/* Кнопки действий */}
<div style={{ display: "flex", gap: 8, marginTop: 4 }}>
<Button
type="primary"
icon={initialValues ? <EditOutlined /> : <PlusOutlined />}
onClick={handleSubmit}
loading={isLoading}
disabled={isButtonDisabled}
block
>
{initialValues ? "Сохранить изменения" : "Добавить связь"}
</Button>
{initialValues && (
<Button
onClick={handleCancel}
icon={<CloseOutlined />}
title="Отменить редактирование"
>
Отмена
</Button>
)}
</div>
</Flex>
{/* Модалка создания фасовки */}
{activeProduct && (
<CreateContainerModal
visible={isModalOpen}
onCancel={() => setIsModalOpen(false)}
productId={activeProduct.id}
productBaseUnit={baseUom}
onSuccess={handleContainerCreated}
/>
)}
</Card>
);
};
```
# ===================================================================
# Файл: src/shared/features/ocr/CatalogSelect.tsx
# ===================================================================
```
import React, { useState, useEffect, useRef } from "react";
import { Select, Spin } from "antd";
import { api } from "../../api";
import type { CatalogItem, ProductSearchResult } from "../../types";
interface Props {
value: string | null;
onChange: (value: string | null, productObj?: ProductSearchResult) => void;
disabled?: boolean;
initialProduct?: CatalogItem | ProductSearchResult;
}
// Интерфейс для элемента выпадающего списка
interface SelectOption {
label: string;
value: string;
data: ProductSearchResult;
}
export const CatalogSelect: React.FC<Props> = ({
value,
onChange,
disabled,
initialProduct,
}) => {
const [options, setOptions] = useState<SelectOption[]>([]);
const [fetching, setFetching] = useState(false);
const [notFound, setNotFound] = useState(false);
const fetchRef = useRef<number | null>(null);
useEffect(() => {
if (initialProduct && initialProduct.id === value) {
const name = initialProduct.name;
const code = initialProduct.code;
setOptions([
{
label: code ? `${name} [${code}]` : name,
value: initialProduct.id,
data: initialProduct as ProductSearchResult,
},
]);
}
}, [initialProduct, value]);
const fetchProducts = async (search: string) => {
if (!search) {
setOptions([]);
setNotFound(false);
return;
}
setFetching(true);
// Не сбрасываем options сразу, чтобы не моргало
try {
const results = await api.searchProducts(search);
const newOptions = results.map((item) => ({
label: item.code ? `${item.name} [${item.code}]` : item.name,
value: item.id,
data: item,
}));
setOptions(newOptions);
// Показываем "Не найдено" если результатов нет
setNotFound(results.length === 0);
} catch (e) {
console.error(e);
setNotFound(true);
} finally {
setFetching(false);
}
};
const handleSearch = (val: string) => {
if (fetchRef.current !== null) {
window.clearTimeout(fetchRef.current);
}
// Сбрасываем notFound при новом поиске
setNotFound(false);
// Запускаем поиск только если введено хотя бы 2 символа
if (val.length < 2) {
return;
}
fetchRef.current = window.setTimeout(() => {
fetchProducts(val);
}, 500);
};
const handleChange = (
val: string | undefined,
option: SelectOption | SelectOption[] | undefined
) => {
if (onChange) {
const opt = Array.isArray(option) ? option[0] : option;
onChange(val ?? null, opt?.data);
}
};
return (
<Select
showSearch
placeholder="Начните вводить название товара..."
filterOption={false}
onSearch={handleSearch}
notFoundContent={
fetching ? (
<Spin size="small" />
) : notFound ? (
<div style={{ padding: "8px", color: "#999" }}>Товар не найден</div>
) : null
}
options={options}
value={value || undefined}
onChange={handleChange}
disabled={disabled}
style={{ width: "100%" }}
listHeight={256}
allowClear
// При очистке сбрасываем опции и notFound, чтобы при следующем клике не вылезал старый товар
onClear={() => {
setOptions([]);
setNotFound(false);
onChange(null);
}}
// При клике (фокусе), если поле пустое - можно показать дефолтные опции или оставить пустым
onFocus={() => {
if (!value) setOptions([]);
}}
/>
);
};
```
# ===================================================================
# Файл: src/shared/features/ocr/MatchList.tsx
# ===================================================================
```
import React from "react";
import { List, Typography, Tag, Input, Empty, Button, Popconfirm } from "antd";
import {
ArrowRightOutlined,
SearchOutlined,
DeleteOutlined,
EditOutlined,
} from "@ant-design/icons";
import type { ProductMatch } from "../../types";
const { Text } = Typography;
interface Props {
matches: ProductMatch[];
onDeleteMatch?: (rawName: string) => void;
onEditMatch?: (match: ProductMatch) => void;
isDeleting?: boolean;
}
export const MatchList: React.FC<Props> = ({
matches,
onDeleteMatch,
onEditMatch,
isDeleting = false,
}) => {
const [searchText, setSearchText] = React.useState("");
const filteredData = matches.filter((item) => {
const raw = (item.raw_name || "").toLowerCase();
const prod = item.product;
const prodName = (prod?.name || "").toLowerCase();
const search = searchText.toLowerCase();
return raw.includes(search) || prodName.includes(search);
});
return (
<div>
<Input
placeholder="Поиск по связям..."
prefix={<SearchOutlined style={{ color: "#ccc" }} />}
style={{ marginBottom: 12 }}
value={searchText}
onChange={(e) => setSearchText(e.target.value)}
allowClear
/>
<List
itemLayout="vertical"
dataSource={filteredData}
locale={{ emptyText: <Empty description="Нет данных" /> }}
pagination={{ pageSize: 10, size: "small", simple: true }}
renderItem={(item) => {
// Унификация полей (только snake_case)
const rawName = item.raw_name || "Без названия";
const product = item.product;
const productName = product?.name || "Товар не найден";
const qty = item.quantity || 1;
// Логика отображения Единицы или Фасовки
const container = item.container;
let displayUnit = "";
if (container) {
// Если есть фасовка: "Пачка 180г"
displayUnit = container.name;
} else {
// Иначе базовая ед.: "кг"
displayUnit = product?.measure_unit || "ед.";
}
return (
<List.Item
style={{
background: "#fff",
padding: 12,
marginBottom: 8,
borderRadius: 8,
display: "flex",
flexDirection: "column",
}}
>
<div style={{ flex: 1 }}>
<div style={{ marginBottom: 4 }}>
<Tag color="geekblue">Чек</Tag>
<Text strong>{rawName}</Text>
</div>
<div
style={{
display: "flex",
alignItems: "center",
gap: 8,
color: "#888",
}}
>
<ArrowRightOutlined />
<Text>
{productName}
<Text strong style={{ color: "#555", marginLeft: 6 }}>
x {qty} {displayUnit}
</Text>
</Text>
</div>
</div>
{(onDeleteMatch || onEditMatch) && (
<div
style={{ display: "flex", justifyContent: "space-between" }}
>
{onEditMatch && (
<Button
type="text"
icon={<EditOutlined />}
onClick={() => onEditMatch(item)}
size="small"
>
Редактировать
</Button>
)}
{onDeleteMatch && (
<Popconfirm
title="Удалить связь?"
description="Это действие нельзя отменить"
onConfirm={() => onDeleteMatch(rawName)}
okText="Да"
cancelText="Нет"
>
<Button
type="text"
danger
icon={<DeleteOutlined />}
loading={isDeleting}
size="small"
>
Удалить
</Button>
</Popconfirm>
)}
</div>
)}
</List.Item>
);
}}
/>
</div>
);
};
```
# ===================================================================
# Файл: src/shared/hooks/useOcr.ts
# ===================================================================
```
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { api } from '../api';
import type { MatchRequest, ProductMatch, CatalogItem, UnmatchedItem } from '../types';
import { message } from 'antd';
export const useOcr = () => {
const queryClient = useQueryClient();
const catalogQuery = useQuery<CatalogItem[], Error>({
queryKey: ['catalog'],
queryFn: api.getCatalogItems,
staleTime: 1000 * 60 * 5,
});
const matchesQuery = useQuery<ProductMatch[], Error>({
queryKey: ['matches'],
queryFn: api.getMatches,
});
const unmatchedQuery = useQuery<UnmatchedItem[], Error>({
queryKey: ['unmatched'],
queryFn: api.getUnmatched,
staleTime: 0,
});
const createMatchMutation = useMutation({
// Теперь типы совпадают, any не нужен
mutationFn: (newMatch: MatchRequest) => api.createMatch(newMatch),
onSuccess: () => {
message.success('Связь сохранена');
queryClient.invalidateQueries({ queryKey: ['matches'] });
queryClient.invalidateQueries({ queryKey: ['unmatched'] });
},
onError: () => {
message.error('Ошибка при сохранении');
},
});
const deleteMatchMutation = useMutation({
mutationFn: (rawName: string) => api.deleteMatch(rawName),
onSuccess: () => {
message.success('Связь удалена');
queryClient.invalidateQueries({ queryKey: ['matches'] });
queryClient.invalidateQueries({ queryKey: ['unmatched'] });
},
onError: () => {
message.error('Ошибка при удалении связи');
},
});
const deleteUnmatchedMutation = useMutation({
mutationFn: (rawName: string) => api.deleteUnmatched(rawName),
onSuccess: () => {
message.success('Нераспознанная строка удалена');
queryClient.invalidateQueries({ queryKey: ['unmatched'] });
},
onError: () => {
message.error('Ошибка при удалении нераспознанной строки');
},
});
return {
catalog: catalogQuery.data || [],
matches: matchesQuery.data || [],
unmatched: unmatchedQuery.data || [],
isLoading: catalogQuery.isPending || matchesQuery.isPending,
isError: catalogQuery.isError || matchesQuery.isError,
createMatch: createMatchMutation.mutate,
isCreating: createMatchMutation.isPending,
deleteMatch: deleteMatchMutation.mutate,
isDeletingMatch: deleteMatchMutation.isPending,
deleteUnmatched: deleteUnmatchedMutation.mutate,
isDeletingUnmatched: deleteUnmatchedMutation.isPending,
};
};
```
# ===================================================================
# Файл: src/shared/hooks/usePlatform.ts
# ===================================================================
```
import { useMemo } from 'react';
export type Platform = 'MobileApp' | 'Desktop' | 'MobileBrowser';
/**
* Хук для определения текущей платформы
* MobileApp - если есть специфические признаки мобильного приложения
* Desktop - если это десктопный браузер
* MobileBrowser - если это мобильный браузер
*/
export const usePlatform = (): Platform => {
return useMemo(() => {
const userAgent = navigator.userAgent;
// Проверка на мобильное приложение (специфические признаки)
// Можно добавить дополнительные проверки для конкретных приложений
const isMobileApp = /rmser-app|mobile-app|cordova|phonegap/i.test(userAgent);
if (isMobileApp) {
return 'MobileApp';
}
// Проверка на мобильный браузер
const isMobileBrowser = /android|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(userAgent);
if (isMobileBrowser) {
return 'MobileBrowser';
}
// По умолчанию - десктоп
return 'Desktop';
}, []);
};
```
# ===================================================================
# Файл: src/shared/hooks/useRecommendations.ts
# ===================================================================
```
import { useQuery } from '@tanstack/react-query';
import { api } from '../api';
import type { Recommendation } from '../types';
export const useRecommendations = () => {
return useQuery<Recommendation[], Error>({
queryKey: ['recommendations'],
queryFn: api.getRecommendations,
// Обновлять данные каждые 30 секунд, если вкладка активна
refetchInterval: 30000,
});
};
```
# ===================================================================
# Файл: src/shared/hooks/useUserRole.ts
# ===================================================================
```
import { useEffect, useState, useCallback } from 'react';
import { api } from '../api';
import type { UserSettings, UserRole } from '../types';
interface UseUserRoleResult {
/** Роль текущего пользователя или null если не загружено */
role: UserRole | null;
/** Полные настройки пользователя */
settings: UserSettings | null;
/** Состояние загрузки */
loading: boolean;
/** Ошибка загрузки */
error: string | null;
/** Функция для повторной загрузки настроек */
refetch: () => Promise<void>;
/** Является ли пользователь оператором */
isOperator: boolean;
/** Является ли пользователь админом или владельцем */
isAdminOrOwner: boolean;
}
/**
* Хук для получения роли пользователя и настроек.
* Автоматически загружает настройки при монтировании компонента.
*/
export const useUserRole = (): UseUserRoleResult => {
const [settings, setSettings] = useState<UserSettings | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchSettings = useCallback(async () => {
setLoading(true);
setError(null);
try {
const data = await api.getSettings();
setSettings(data);
} catch (err) {
console.error('Ошибка при загрузке настроек:', err);
setError(err instanceof Error ? err.message : 'Не удалось загрузить настройки');
} finally {
setLoading(false);
}
}, []);
// Загружаем настройки при монтировании
useEffect(() => {
fetchSettings();
}, [fetchSettings]);
const role = settings?.role ?? null;
const isOperator = role === 'OPERATOR';
const isAdminOrOwner = role === 'ADMIN' || role === 'OWNER';
return {
role,
settings,
loading,
error,
refetch: fetchSettings,
isOperator,
isAdminOrOwner,
};
};
```
# ===================================================================
# Файл: src/shared/hooks/useWebSocket.ts
# ===================================================================
```
import { useEffect, useState, useRef, useCallback } from 'react';
const apiUrl = import.meta.env.VITE_API_URL || '';
// Определяем базовый URL для WS (меняем http->ws, https->wss)
const getWsUrl = () => {
let baseUrl = apiUrl;
if (baseUrl.startsWith('/')) {
baseUrl = window.location.origin;
} else if (!baseUrl) {
baseUrl = 'http://localhost:8080';
}
// Заменяем протокол
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
const host = baseUrl.replace(/^http(s)?:\/\//, '');
// Важно: путь /socket.io/ оставлен для совместимости с Nginx конфигом
return `${protocol}//${host}/socket.io/`;
};
interface WsEvent {
event: string;
data: unknown;
}
interface UseWebSocketParams {
autoReconnect?: boolean;
onDisconnect?: () => void;
}
export const useWebSocket = (sessionId: string | null, params: UseWebSocketParams = {}) => {
const { autoReconnect = false, onDisconnect } = params;
const [isConnected, setIsConnected] = useState(false);
const [lastError, setLastError] = useState<string | null>(null);
const [lastMessage, setLastMessage] = useState<WsEvent | null>(null);
const wsRef = useRef<WebSocket | null>(null);
const reconnectTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
const reconnectAttemptsRef = useRef(0);
const currentSessionIdRef = useRef<string | null>(null);
// Используем ref для хранения функции reconnect, чтобы она могла вызывать сама себя
const reconnectRef = useRef<(() => void) | null>(null);
// Функция для переподключения WebSocket
const reconnect = useCallback(() => {
if (!currentSessionIdRef.current) return;
const delay = Math.min(1000 + reconnectAttemptsRef.current * 1000, 5000);
reconnectAttemptsRef.current++;
console.log(`🔄 WS Reconnect attempt ${reconnectAttemptsRef.current} in ${delay}ms`);
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}
reconnectTimeoutRef.current = setTimeout(() => {
const url = `${getWsUrl()}?session_id=${currentSessionIdRef.current}`;
console.log('🔌 Reconnecting WS:', url);
const ws = new WebSocket(url);
wsRef.current = ws;
ws.onopen = () => {
console.log('✅ WS Reconnected');
setIsConnected(true);
setLastError(null);
reconnectAttemptsRef.current = 0;
};
ws.onclose = () => {
// Используем ref для вызова reconnect
reconnectRef.current?.();
};
ws.onerror = (error) => {
console.error('❌ WS Error', error);
setLastError('Connection error');
setIsConnected(false);
};
ws.onmessage = (event) => {
try {
const parsed = JSON.parse(event.data);
console.log('📨 WS Message:', parsed);
setLastMessage(parsed);
} catch {
console.error('Failed to parse WS message', event.data);
}
};
}, delay);
}, []);
const handleDisconnect = useCallback(() => {
if (onDisconnect) {
onDisconnect();
}
if (autoReconnect) {
reconnect();
}
}, [onDisconnect, reconnect, autoReconnect]);
useEffect(() => {
// Сохраняем reconnect в ref после его создания
reconnectRef.current = reconnect;
}, [reconnect]);
useEffect(() => {
if (!sessionId) return;
currentSessionIdRef.current = sessionId;
reconnectAttemptsRef.current = 0;
const url = `${getWsUrl()}?session_id=${sessionId}`;
console.log('🔌 Connecting WS:', url);
const ws = new WebSocket(url);
wsRef.current = ws;
ws.onopen = () => {
console.log('✅ WS Connected');
setIsConnected(true);
setLastError(null);
};
ws.onclose = (event) => {
console.log('⚠️ WS Closed', event.code, event.reason);
setIsConnected(false);
handleDisconnect();
};
ws.onerror = (error) => {
console.error('❌ WS Error', error);
setLastError('Connection error');
setIsConnected(false);
};
ws.onmessage = (event) => {
try {
const parsed = JSON.parse(event.data);
console.log('📨 WS Message:', parsed);
setLastMessage(parsed);
} catch {
console.error('Failed to parse WS message', event.data);
}
};
return () => {
console.log('🧹 WS Cleanup');
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
reconnectTimeoutRef.current = null;
}
ws.close();
};
}, [sessionId, handleDisconnect]);
return { isConnected, lastError, lastMessage };
};
```
# ===================================================================
# Файл: src/shared/stores/activeDraftStore.ts
# ===================================================================
```
import { create } from 'zustand';
import { immer } from 'zustand/middleware/immer';
import type { DraftItem } from '../types';
import { recalculateItem } from '../utils/calculations';
/**
* Интерфейс хранилища активного черновика
*
* Управляет локальным состоянием элементов черновика накладной.
* Отслеживает изменения через флаг isDirty.
*/
interface ActiveDraftStore {
/** Массив элементов черновика */
items: DraftItem[];
/** Флаг, указывающий на наличие несохраненных изменений */
isDirty: boolean;
/**
* Заменяет весь массив элементов черновика
*
* @param items - Новый массив элементов
*/
setItems: (items: DraftItem[]) => void;
/**
* Обновляет элемент черновика по идентификатору
*
* При изменении числовых полей (quantity, price, sum) автоматически
* пересчитывает зависимые значения с помощью recalculateItem.
*
* @param id - Идентификатор элемента для обновления
* @param changes - Частичные изменения элемента
*
* @remarks
* Если меняется product_id, quantity и price НЕ сбрасываются в 0.
* При изменении любого поля устанавливает isDirty = true.
*/
updateItem: (id: string, changes: Partial<DraftItem>) => void;
/**
* Удаляет элемент черновика по идентификатору
*
* @param id - Идентификатор элемента для удаления
*/
deleteItem: (id: string) => void;
/**
* Добавляет новый элемент в черновик
*
* @param item - Новый элемент для добавления
*/
addItem: (item: DraftItem) => void;
/**
* Перемещает элемент с одной позиции на другую
*
* @param startIndex - Исходная позиция элемента
* @param endIndex - Новая позиция элемента
*/
reorderItems: (startIndex: number, endIndex: number) => void;
/**
* Сбрасывает флаг isDirty в false
* Используется после успешного сохранения изменений на сервер
*/
resetDirty: () => void;
/**
* Устанавливает флаг isDirty в true
* Используется для пометки черновика как измененного
*/
markAsDirty: () => void;
}
/**
* Zustand стор для управления состоянием активного черновика
*
* Использует immer middleware для иммутабельных обновлений состояния.
* Все изменения элементов автоматически устанавливают флаг isDirty = true,
* кроме метода setItems, который сбрасывает флаг в false.
*/
export const useActiveDraftStore = create<ActiveDraftStore>()(
immer((set) => ({
// Начальное состояние
items: [],
isDirty: false,
/**
* Заменяет весь массив элементов черновика
* Сбрасывает флаг isDirty в false
*/
setItems: (items: DraftItem[]) =>
set((state) => {
state.items = items;
state.isDirty = false;
}),
/**
* Обновляет элемент черновика по идентификатору
*
* Логика:
* 1. Находит элемент по id
* 2. Если меняются числовые поля (quantity, price, sum), использует recalculateItem
* 3. Устанавливает isDirty = true
* 4. При изменении product_id НЕ сбрасывает quantity и price
*/
updateItem: (id: string, changes: Partial<DraftItem>) =>
set((state) => {
const itemIndex = state.items.findIndex((item: DraftItem) => item.id === id);
if (itemIndex === -1) {
return;
}
const item = state.items[itemIndex];
// Проверяем, изменились ли числовые поля
const numericFields: Array<keyof DraftItem> = ['quantity', 'price', 'sum'];
const changedNumericField = numericFields.find(
(field) =>
changes[field] !== undefined &&
changes[field] !== item[field]
);
if (changedNumericField) {
// Используем recalculateItem для пересчета зависимых полей
const newValue = changes[changedNumericField] as number;
const recalculated = recalculateItem(
item,
changedNumericField as 'quantity' | 'price' | 'sum',
newValue
);
// Применяем пересчитанные значения и остальные изменения
state.items[itemIndex] = {
...recalculated,
...changes,
};
} else {
// Просто применяем изменения без пересчета
state.items[itemIndex] = {
...item,
...changes,
};
}
state.isDirty = true;
}),
/**
* Удаляет элемент черновика по идентификатору
* Устанавливает isDirty = true
*/
deleteItem: (id: string) =>
set((state) => {
state.items = state.items.filter((item: DraftItem) => item.id !== id);
state.isDirty = true;
}),
/**
* Добавляет новый элемент в черновик
* Устанавливает isDirty = true
*/
addItem: (item: DraftItem) =>
set((state) => {
state.items.push(item);
state.isDirty = true;
}),
/**
* Перемещает элемент с позиции startIndex на endIndex
* Устанавливает isDirty = true
*/
reorderItems: (startIndex: number, endIndex: number) =>
set((state) => {
const [removed] = state.items.splice(startIndex, 1);
state.items.splice(endIndex, 0, removed);
state.isDirty = true;
}),
/**
* Сбрасывает флаг isDirty в false
* Используется после успешного сохранения изменений на сервер
*/
resetDirty: () =>
set((state) => {
state.isDirty = false;
}),
/**
* Устанавливает флаг isDirty в true
* Используется для пометки черновика как измененного
*/
markAsDirty: () =>
set((state) => {
state.isDirty = true;
}),
}))
);
```
# ===================================================================
# Файл: src/shared/stores/authStore.ts
# ===================================================================
```
import { create } from 'zustand';
import { persist } from 'zustand/middleware';
interface User {
id: string;
username: string;
email?: string;
role?: string;
}
interface AuthState {
token: string | null;
isAuthenticated: boolean;
user: User | null;
setToken: (token: string) => void;
setUser: (user: User) => void;
logout: () => void;
}
/**
* Хранилище состояния авторизации
* Сохраняет токен и данные пользователя в localStorage
*/
export const useAuthStore = create<AuthState>()(
persist(
(set) => ({
token: null,
isAuthenticated: false,
user: null,
setToken: (token: string) => {
set({ token, isAuthenticated: true });
},
setUser: (user: User) => {
set({ user });
},
logout: () => {
set({ token: null, isAuthenticated: false, user: null });
},
}),
{
name: 'auth-storage',
partialize: (state) => ({
token: state.token,
isAuthenticated: state.isAuthenticated,
user: state.user,
}),
}
)
);
```
# ===================================================================
# Файл: src/shared/stores/serverStore.ts
# ===================================================================
```
import { create } from 'zustand';
import type { ServerShort } from '../types';
import { api } from '../api';
interface ServerState {
servers: ServerShort[];
activeServer: ServerShort | null;
isLoading: boolean;
error: string | null;
fetchServers: () => Promise<void>;
setActiveServer: (id: string) => Promise<void>;
}
/**
* Хранилище состояния серверов
* Управляет списком доступных серверов и текущим активным сервером
*/
export const useServerStore = create<ServerState>((set, get) => ({
servers: [],
activeServer: null,
isLoading: false,
error: null,
fetchServers: async () => {
set({ isLoading: true, error: null });
try {
const servers = await api.getUserServers();
const activeServer = servers.find(s => s.is_active) || null;
set({ servers, activeServer, isLoading: false });
} catch (error) {
console.error('Ошибка при загрузке списка серверов:', error);
set({
error: error instanceof Error ? error.message : 'Не удалось загрузить список серверов',
isLoading: false
});
}
},
setActiveServer: async (id: string) => {
set({ isLoading: true, error: null });
try {
await api.switchServer(id);
// После успешного переключения перезагружаем список серверов
// чтобы обновить флаг is_active
await get().fetchServers();
} catch (error) {
console.error('Ошибка при переключении сервера:', error);
set({
error: error instanceof Error ? error.message : 'Не удалось переключить сервер',
isLoading: false
});
}
},
}));
```
# ===================================================================
# Файл: src/shared/stores/uiStore.ts
# ===================================================================
```
import { create } from 'zustand';
interface UIState {
// Выбранный сервер (заглушка для будущего функционала)
selectedServer: string | null;
sidebarCollapsed: boolean;
setSelectedServer: (server: string | null) => void;
toggleSidebar: () => void;
setSidebarCollapsed: (collapsed: boolean) => void;
}
/**
* Хранилище UI состояния десктопной версии
*/
export const useUIStore = create<UIState>((set) => ({
selectedServer: null,
sidebarCollapsed: false,
setSelectedServer: (server: string | null) => {
set({ selectedServer: server });
},
toggleSidebar: () => {
set((state) => ({ sidebarCollapsed: !state.sidebarCollapsed }));
},
setSidebarCollapsed: (collapsed: boolean) => {
set({ sidebarCollapsed: collapsed });
},
}));
```
# ===================================================================
# Файл: src/shared/types/index.ts
# ===================================================================
```
// --- Общие типы ---
export type UUID = string;
// Добавляем типы ролей
export type UserRole = 'OWNER' | 'ADMIN' | 'OPERATOR';
// Краткая информация о сервере
export interface ServerShort {
id: string;
name: string;
role: UserRole;
is_active: boolean;
}
// Интерфейс пользователя сервера
export interface ServerUser {
user_id: string;
username: string; // @username или пустая строка
first_name: string;
last_name: string;
photo_url: string; // URL картинки или пустая строка
role: UserRole;
is_me: boolean; // Флаг, является ли этот юзер текущим пользователем
}
// --- Каталог и Фасовки (API v2.0) ---
export interface ProductContainer {
id: UUID;
name: string; // "Пачка 180г"
count: number; // 0.180
}
// Запрос на создание фасовки
export interface AddContainerRequest {
product_id: UUID;
name: string; // "Бутылка 0.75"
count: number; // 0.75
}
// Ответ на создание фасовки
export interface AddContainerResponse {
status: string;
container_id: UUID;
}
// Результат поиска товара
export interface ProductSearchResult {
id: UUID;
name: string;
code: string;
num?: string;
// Обновляем структуру единицы измерения
main_unit?: MainUnit;
measure_unit?: string; // Оставим для совместимости, но брать будем из main_unit.name
containers: ProductContainer[];
}
// Совместимость с CatalogItem (чтобы не ломать старый код, если он где-то используется)
export interface CatalogItem extends ProductSearchResult {
// Fallback поля
ID?: UUID;
Name?: string;
Code?: string;
MeasureUnit?: string;
Containers?: ProductContainer[];
}
// --- Матчинг (Обучение) ---
export interface MatchRequest {
raw_name: string;
product_id: UUID;
quantity: number;
container_id?: UUID;
}
export interface ProductMatch {
raw_name: string;
product_id: UUID;
product?: CatalogItem;
quantity: number;
container_id?: UUID;
container?: ProductContainer;
updated_at: string;
}
// --- Нераспознанное ---
export interface UnmatchedItem {
raw_name: string;
count: number;
last_seen: string;
}
// --- Остальные типы ---
export interface Recommendation {
ID: UUID;
Type: string;
ProductID: UUID;
ProductName: string;
Reason: string;
CreatedAt: string;
}
export interface InvoiceItemRequest {
product_id: UUID;
amount: number;
price: number;
}
export interface CreateInvoiceRequest {
document_number: string;
date_incoming: string;
supplier_id: UUID;
store_id: UUID;
items: InvoiceItemRequest[];
}
export interface InvoiceResponse {
status: string;
created_number: string;
}
export interface HealthResponse {
status: string;
time: string;
}
// --- Справочники ---
export interface Store {
id: UUID;
name: string;
}
export interface Supplier {
id: UUID;
name: string;
}
export interface DictionariesResponse {
stores: Store[];
suppliers: Supplier[];
// product_groups?: ProductGroup[]; // пока не реализовано
}
// --- Настройки и Статистика ---
export interface UserSettings {
root_group_id: UUID | null;
default_store_id: UUID | null;
auto_conduct: boolean;
role: UserRole; // Добавляем поле роли в настройки текущего пользователя
last_sync_at: string | null; // Время последней синхронизации с iiko
last_activity_at: string | null; // Время последней активности
sync_interval: number; // Интервал синхронизации в минутах
}
export interface InvoiceStats {
last_month: number;
last_24h: number;
total: number;
}
// Интерфейс группы товаров (рекурсивный)
export interface ProductGroup {
key: string;
value: string;
title: string;
children?: ProductGroup[];
}
// --- Черновик Накладной (Draft) ---
export type DraftStatus = 'PROCESSING' | 'READY_TO_VERIFY' | 'COMPLETED' | 'ERROR' | 'CANCELED' | 'NEW' | 'PROCESSED' | 'DELETED';
export interface DraftItem {
id: UUID;
// Данные из OCR (Read-only)
raw_name: string;
raw_amount: number;
raw_price: number;
// Редактируемые данные
product_id: UUID | null;
container_id: UUID | null; // Фасовка
quantity: number;
price: number;
sum: number;
// Мета-данные
is_matched: boolean;
product?: CatalogItem | null;
container?: ProductContainer;
// Поля для синхронизации состояния (опционально, если бэкенд их отдает)
last_edited_field_1?: string;
last_edited_field_2?: string;
}
// --- Список Черновиков (Summary) ---
export interface DraftSummary {
id: UUID;
document_number: string;
date_incoming: string;
status: DraftStatus; // Используем существующий тип статуса
items_count: number;
total_sum: number;
store_name?: string;
created_at: string;
}
export interface DraftInvoice {
id: UUID;
status: DraftStatus;
document_number: string;
incoming_document_number?: string
date_incoming: string | null; // YYYY-MM-DD
store_id: UUID | null;
supplier_id: UUID | null;
comment: string;
items: DraftItem[];
created_at?: string;
photo_url?: string; // Добавлено поле фото чека
}
// DTO для обновления строки
export interface UpdateDraftItemRequest {
id?: UUID; // ID элемента для идентификации при пакетном обновлении
product_id?: UUID | null;
container_id?: UUID | null;
quantity?: number;
price?: number;
sum?: number;
edited_field?: string; // ('quantity' | 'price' | 'sum')
}
// DTO для пакетного обновления черновика (шапка + элементы)
export interface UpdateDraftRequest {
date_incoming?: string;
store_id?: UUID;
supplier_id?: UUID;
comment?: string;
incoming_document_number?: string;
items?: UpdateDraftItemRequest[];
}
// DTO для коммита
export interface CommitDraftRequest {
date_incoming: string;
store_id: UUID;
supplier_id: UUID;
comment: string;
incoming_document_number?: string;
is_processed: boolean;
}
export interface ReorderDraftItemsRequest {
items: Array<{
id: UUID;
order: number;
}>;
}
export interface MainUnit {
id: UUID;
name: string; // "кг"
code: string;
}
export type InvoiceType = 'DRAFT' | 'SYNCED'; // Тип записи: Черновик или Синхронизировано из iiko
export interface UnifiedInvoice {
id: UUID;
type: InvoiceType; // Новый признак типа
document_number: string; // Внутренний номер iiko или ID черновика
incoming_number: string; // Входящий номер накладной от поставщика
date_incoming: string;
status: DraftStatus;
items_count: number;
total_sum: number;
store_name?: string;
created_at: string;
is_app_created: boolean; // Создано ли через наше приложение
items_preview: string; // Краткое содержание товаров
photo_url: string | null; // Ссылка на фото чека
draft_id?: string; // ID черновика для SYNCED накладных, созданных в приложении
}
export interface InvoiceDetails {
id: UUID;
number: string;
date: string;
status: DraftStatus;
supplier: Supplier;
items: {
name: string;
quantity: number;
price: number;
total: number;
}[];
photo_url: string | null;
}
export type PhotoStatus = 'ORPHAN' | 'HAS_DRAFT' | 'HAS_INVOICE';
export interface ReceiptPhoto {
id: string;
rms_server_id: string;
uploaded_by: string;
file_url: string;
file_name: string;
file_size: number;
draft_id?: string;
invoice_id?: string;
created_at: string;
status: PhotoStatus;
can_delete: boolean;
can_regenerate: boolean;
}
export interface GetPhotosResponse {
photos: ReceiptPhoto[];
total: number;
page: number;
limit: number;
}
```
# ===================================================================
# Файл: src/shared/ui/DragDropZone.tsx
# ===================================================================
```
import React from 'react';
import { useDropzone } from 'react-dropzone';
import { InboxOutlined } from '@ant-design/icons';
import { Typography } from 'antd';
const { Text } = Typography;
interface DragDropZoneProps {
onDrop: (files: File[]) => void;
accept?: Record<string, string[]>;
maxSize?: number;
maxFiles?: number;
disabled?: boolean;
className?: string;
children?: React.ReactNode;
}
/**
* Компонент зоны перетаскивания файлов
* Обертка над react-dropzone с Ant Design стилизацией
*/
export const DragDropZone: React.FC<DragDropZoneProps> = ({
onDrop,
accept = {
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'],
'application/vnd.ms-excel': ['.xls'],
'image/*': ['.png', '.jpg', '.jpeg', '.gif', '.webp'],
},
maxSize = 10 * 1024 * 1024, // 10MB по умолчанию
maxFiles = 10,
disabled = false,
className = '',
children,
}) => {
const { getRootProps, getInputProps, isDragActive, isDragReject } = useDropzone({
onDrop,
accept,
maxSize,
maxFiles,
disabled,
});
const getBorderColor = () => {
if (isDragReject) return '#ff4d4f';
if (isDragActive) return '#1890ff';
return '#d9d9d9';
};
const getBackgroundColor = () => {
if (isDragActive) return '#e6f7ff';
if (disabled) return '#f5f5f5';
return '#fafafa';
};
return (
<div
{...getRootProps()}
className={className}
style={{
border: `2px dashed ${getBorderColor()}`,
borderRadius: '8px',
padding: '40px 20px',
textAlign: 'center',
cursor: disabled ? 'not-allowed' : 'pointer',
backgroundColor: getBackgroundColor(),
transition: 'all 0.3s ease',
}}
>
<input {...getInputProps()} />
{children || (
<div>
<InboxOutlined
style={{
fontSize: '48px',
color: isDragActive ? '#1890ff' : '#bfbfbf',
marginBottom: '16px',
}}
/>
<div>
{isDragActive ? (
<Text type="secondary">Отпустите файлы здесь</Text>
) : (
<div>
<Text>Перетащите файлы сюда или нажмите для выбора</Text>
<br />
<Text type="secondary" style={{ fontSize: '12px' }}>
Поддерживаются: .xlsx, .xls, изображения (макс. {maxSize / 1024 / 1024}MB)
</Text>
</div>
)}
</div>
</div>
)}
</div>
);
};
```
# ===================================================================
# Файл: src/shared/ui/ExcelPreviewModal.tsx
# ===================================================================
```
import React, { useState, useEffect } from "react";
import { Modal, Button, message } from "antd";
import {
ZoomInOutlined,
ZoomOutOutlined,
UndoOutlined,
} from "@ant-design/icons";
import * as XLSX from "xlsx";
import { apiClient } from "../api";
interface ExcelPreviewModalProps {
visible: boolean;
onCancel: () => void;
fileUrl: string;
}
/**
* Компонент для предпросмотра Excel файлов
* Позволяет просматривать содержимое Excel файлов с возможностью масштабирования
*/
const ExcelPreviewModal: React.FC<ExcelPreviewModalProps> = ({
visible,
onCancel,
fileUrl,
}) => {
// Данные таблицы из Excel файла
const [data, setData] = useState<
(string | number | boolean | null | undefined)[][]
>([]);
// Масштаб отображения таблицы
const [scale, setScale] = useState<number>(1);
/**
* Загрузка и парсинг Excel файла при изменении видимости или URL файла
*/
useEffect(() => {
const loadExcelFile = async () => {
if (!visible || !fileUrl) {
return;
}
console.log("ExcelPreviewModal: Start loading", fileUrl);
try {
// Загрузка файла через apiClient с авторизацией
const response = await apiClient.get(fileUrl, {
responseType: "arraybuffer",
});
console.log(
"ExcelPreviewModal: Got response",
response.status,
response.data.byteLength
);
const arrayBuffer = response.data;
// Чтение Excel файла
const workbook = XLSX.read(arrayBuffer, { type: "array" });
console.log("ExcelPreviewModal: Workbook parsed", workbook.SheetNames);
// Получение первого листа
const firstSheetName = workbook.SheetNames[0];
const sheet = workbook.Sheets[firstSheetName];
// Преобразование листа в JSON-массив массивов
const jsonData = XLSX.utils.sheet_to_json(sheet, {
header: 1,
}) as (string | number | boolean | null | undefined)[][];
setData(jsonData);
console.log("ExcelPreviewModal: Data set, rows:", jsonData.length);
// Сброс масштаба при загрузке нового файла
setScale(1);
} catch (error) {
console.error("ExcelPreviewModal Error:", error);
// Обработка ошибок авторизации (401) обрабатывается в интерсепторе apiClient
if (error && typeof error === "object" && "response" in error) {
const axiosError = error as { response?: { status?: number } };
if (axiosError.response?.status === 401) {
message.error(
"Ошибка авторизации. Необходима повторная авторизация."
);
} else {
message.error("Не удалось загрузить Excel файл");
}
} else {
message.error("Не удалось загрузить Excel файл");
}
setData([]);
}
};
loadExcelFile();
}, [visible, fileUrl]);
/**
* Увеличение масштаба
*/
const handleZoomIn = () => {
setScale((prev) => Math.min(prev + 0.1, 3));
};
/**
* Уменьшение масштаба
*/
const handleZoomOut = () => {
setScale((prev) => Math.max(prev - 0.1, 0.5));
};
/**
* Сброс масштаба до исходного значения
*/
const handleReset = () => {
setScale(1);
};
/**
* Обработчик закрытия модалки
*/
const handleCancel = () => {
setData([]);
onCancel();
};
return (
<Modal
open={visible}
onCancel={handleCancel}
width="90%"
footer={null}
title="Предпросмотр Excel"
style={{ top: 20, zIndex: 10000 }}
>
{/* Панель инструментов для управления масштабом */}
<div style={{ marginBottom: 16, display: "flex", gap: 8 }}>
<Button
icon={<ZoomInOutlined />}
onClick={handleZoomIn}
disabled={scale >= 3}
>
Увеличить (+)
</Button>
<Button
icon={<ZoomOutOutlined />}
onClick={handleZoomOut}
disabled={scale <= 0.5}
>
Уменьшить (-)
</Button>
<Button
icon={<UndoOutlined />}
onClick={handleReset}
disabled={scale === 1}
>
Сброс
</Button>
<span style={{ marginLeft: "auto", lineHeight: "32px" }}>
Масштаб: {Math.round(scale * 100)}%
</span>
</div>
{/* Контейнер с прокруткой для таблицы */}
<div
style={{
height: 600,
overflow: "auto",
border: "1px solid #d9d9d9",
borderRadius: 4,
}}
>
{/* Таблица с данными Excel */}
<table
style={{
borderCollapse: "collapse",
transform: `scale(${scale})`,
transformOrigin: "top left",
width: "100%",
}}
>
<tbody>
{data.map((row, rowIndex) => (
<tr key={rowIndex}>
{row.map((cell, cellIndex) => (
<td
key={cellIndex}
style={{
border: "1px solid #ccc",
padding: "8px",
minWidth: 100,
whiteSpace: "nowrap",
}}
>
{cell !== undefined && cell !== null ? String(cell) : ""}
</td>
))}
</tr>
))}
</tbody>
</table>
</div>
</Modal>
);
};
export default ExcelPreviewModal;
```
# ===================================================================
# Файл: src/shared/utils/calculations.ts
# ===================================================================
```
import type { DraftItem } from '../types';
/**
* Пересчитывает значения полей элемента черновика на основе измененного поля.
*
* Логика "Треугольник": Q (Quantity) -> P (Price) -> S (Sum) -> Q...
* Правило: "Пересчитываем значение, следующее за редактируемым.
* Оставляем значение, предшествующее редактируемому."
*
* - Если меняем Quantity (Q): Previous=Sum (Keep), Next=Price (Recalc). Price = Sum / Quantity
* - Если меняем Price (P): Previous=Quantity (Keep), Next=Sum (Recalc). Sum = Quantity * Price
* - Если меняем Sum (S): Previous=Price (Keep), Next=Quantity (Recalc). Quantity = Sum / Price
*
* @param item - Исходный элемент черновика (содержит "предыдущие/сохраняемые" значения)
* @param changedField - Измененное поле ('quantity' | 'price' | 'sum')
* @param newValue - Новое значение измененного поля
* @returns Новый объект DraftItem с пересчитанными значениями
*
* @example
* // При изменении количества (пересчитываем цену, сумма сохраняется)
* const updated = recalculateItem(item, 'quantity', 5);
*
* @example
* // При изменении суммы (пересчитываем количество, цена сохраняется)
* const updated = recalculateItem(item, 'sum', 100);
*/
export function recalculateItem(
item: DraftItem,
changedField: 'quantity' | 'price' | 'sum',
newValue: number
): DraftItem {
switch (changedField) {
case 'quantity': {
// Меняем Quantity (Q): Previous=Sum (Keep), Next=Price (Recalc)
// Price = Sum / Quantity
// Обрабатываем деление на ноль
if (newValue === 0) {
return {
...item,
quantity: newValue,
price: 0,
};
}
const newPrice = item.sum / newValue;
return {
...item,
quantity: newValue,
price: newPrice,
};
}
case 'price': {
// Меняем Price (P): Previous=Quantity (Keep), Next=Sum (Recalc)
// Sum = Quantity * Price
const newSum = item.quantity * newValue;
return {
...item,
price: newValue,
sum: newSum,
};
}
case 'sum': {
// Меняем Sum (S): Previous=Price (Keep), Next=Quantity (Recalc)
// Quantity = Sum / Price
// Обрабатываем деление на ноль
if (item.price === 0) {
return {
...item,
sum: newValue,
quantity: 0,
};
}
const newQuantity = newValue / item.price;
return {
...item,
sum: newValue,
quantity: newQuantity,
};
}
default: {
// Для неизвестных полей возвращаем исходный объект
return { ...item };
}
}
}
```
# ===================================================================
# Файл: src/vite-env.d.ts
# ===================================================================
```
/// <reference types="vite/client" />
interface TelegramWebApp {
initData: string; // Сырая строка с параметрами и хешем
initDataUnsafe: {
user?: {
id: number;
first_name: string;
last_name?: string;
username?: string;
language_code?: string;
};
};
close: () => void;
expand: () => void;
}
interface Window {
Telegram?: {
WebApp: TelegramWebApp;
};
}
```
# ===================================================================
# Файл: tsconfig.app.json
# ===================================================================
```
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
"target": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"types": ["vite/client"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
"jsx": "react-jsx",
/* Path aliases */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
},
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src"]
}
```
# ===================================================================
# Файл: tsconfig.json
# ===================================================================
```
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
}
```
# ===================================================================
# Файл: tsconfig.node.json
# ===================================================================
```
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"types": ["node"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["vite.config.ts"]
}
```