3 Commits

Author SHA1 Message Date
gorbenko.pavel e34d290865 Мониторинг: логи, ошибки и запись экрана портала заявок (@tnsu/monitoring-web) (#29)
Tests / frontend-test (push) Successful in 1m24s
Логи, ошибки и запись экрана портала заявок в центральный мониторинг (monitoring.tnsu.kz).

**Что поменялось**
- `@tnsu/monitoring-web` 0.3.1 из реестра tansu.
- `RequestPortalWebPart.onInit`: `initMonitoring({ service: 'request-portal', getAccessToken: spfxTokenProvider(this.context.aadTokenProviderFactory) })` — до `installErrorLogging`. Один на страницу (экземпляров веб-части бывает несколько) — это делает сама библиотека.
- `config/package-solution.json`: запрос `Logs.Write` к регистрации мониторинга `9232d9e2-…` (рядом с уже запрошенным `Reports.Read` той же регистрации).
- Хэш коммита сборки (`git.commit` в логах, «Сборка» в плеере): `config/webpack-patch.json` ссылается на патч из пакета (`node_modules/@tnsu/monitoring-web/spfx/webpack-patch.cjs`), он подставляет `__TNSU_GIT_COMMIT__` из `GIT_COMMIT`, `GITHUB_SHA` или `git rev-parse`. Пайплайн менять не нужно.

**Что пишется**
- Необработанные ошибки и отказы промисов, `console.error` (в том числе вывод `installErrorLogging`), `log.*`.
- Запись экрана (rrweb) — **ничего не маскируется**, записывается вся страница SharePoint, не только веб-часть. Неудачные запросы — вкладка «Сеть» в плеере.
- В workbench / при `gulp serve` окружение `development` — в админке мониторинга такие записи по умолчанию скрыты.
- Нет связи с мониторингом час — тост «Ошибка №… Обратитесь к администратору» (не чаще 3 раз в сутки).

**Проверено:** продакшен-сборка (`heft build --production`) проходит, патч применяется, хэш коммита попадает в бандл веб-части.

**После выкатки .sppkg** администратор один раз одобряет запрос: SharePoint admin center → Advanced → API access → `Logs.Write` → Approve. Пока не одобрено, токена нет и данные копятся в браузере (до 50 МБ), портал работает как обычно.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: gorbenko.pavel <gorbenko.pavel@tansu.kz>
Reviewed-on: #29
2026-10-30 14:24:58 +00:00
Тимур Городецкий b1060433d0 feat: «Необходимое ПО» в рабочем месте — мультивыбор из списка
Tests / frontend-test (push) Successful in 1m28s
Вместо свободного текста — выбор из общего списка ПО (как в подборе) со
своим значением. В SP и в маршрут по-прежнему уходит текст через запятую в
колонке Software: условия «Software содержит …» и старые заявки работают
без изменений.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-30 09:58:32 +05:00
Тимур Городецкий cf039f4d08 refactor: общий список ПО в shared/dictionary
Список программ жил в модели «Заявки на подбор»; теперь он нужен и
«Организации рабочего места». Вынесен в shared/dictionary/software.ts, чтобы
обе формы брали его из одного места.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-30 09:58:31 +05:00
15 changed files with 336 additions and 51 deletions
+4
View File
@@ -30,6 +30,10 @@
"resource": "9232d9e2-9807-4753-808f-07250e5474b0",
"scope": "Reports.Read"
},
{
"resource": "9232d9e2-9807-4753-808f-07250e5474b0",
"scope": "Logs.Write"
},
{
"resource": "Microsoft Flow Service",
"scope": "Flows.Read.All"
+4
View File
@@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/webpack-patch.schema.json",
"patchFiles": ["./node_modules/@tnsu/monitoring-web/spfx/webpack-patch.cjs"]
}
+132 -24
View File
@@ -17,6 +17,8 @@
"@microsoft/sp-webpart-base": "1.22.1",
"@pnp/sp": "^4.20.0",
"@pnp/spfx-controls-react": "^3.25.0",
"@pnp/telemetry-js": "2.0.0",
"@tnsu/monitoring-web": "^0.3.4",
"@tnsu/ui-kit": "^0.15.0",
"jszip": "~3.8.0",
"react": "17.0.1",
@@ -341,7 +343,7 @@
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz",
"integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -351,7 +353,7 @@
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.7.tgz",
"integrity": "sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.7",
@@ -382,7 +384,7 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"devOptional": true,
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -408,7 +410,7 @@
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz",
"integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.29.7",
@@ -425,7 +427,7 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"devOptional": true,
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -457,7 +459,7 @@
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz",
"integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.29.7",
@@ -503,7 +505,7 @@
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz",
"integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -513,7 +515,7 @@
"version": "7.29.7",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz",
"integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.29.7",
@@ -3979,7 +3981,7 @@
"version": "2.3.5",
"resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz",
"integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@@ -8726,6 +8728,28 @@
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rrweb/rrweb-plugin-console-record": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@rrweb/rrweb-plugin-console-record/-/rrweb-plugin-console-record-2.2.0.tgz",
"integrity": "sha512-2c6LEsoH3+1NFcHckcwuErg7bkpFadt9++uKJh2x9BtWUDxoVKzLp+qLCq6f90MLTjSIABDkAiCswVskENZT3g==",
"license": "MIT",
"peerDependencies": {
"@rrweb/utils": "^2.1.1",
"rrweb": "^2.1.1"
}
},
"node_modules/@rrweb/types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@rrweb/types/-/types-2.2.0.tgz",
"integrity": "sha512-2gTA/pRpIbuET123oSdX5ZUDZfWCOIMYBvpSai3fr5pk9LcbccPqqaXflX+roP4oKenLjNtJPGpFK3BfYeBKnA==",
"license": "MIT"
},
"node_modules/@rrweb/utils": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/@rrweb/utils/-/utils-2.2.0.tgz",
"integrity": "sha512-e8FEp8wLswqgjDHpQNUUVfEXeDsrRMOVouvqIGKvnPxKYonjLlh0mU3O/17QNvCixFG5eUFR1B2jgTdiLlZxeQ==",
"license": "MIT"
},
"node_modules/@rushstack/debug-certificate-manager": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/@rushstack/debug-certificate-manager/-/debug-certificate-manager-1.6.2.tgz",
@@ -9885,6 +9909,28 @@
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/@tnsu/monitoring-web": {
"version": "0.3.4",
"resolved": "https://git.tnsu.kz/api/packages/tansu/npm/%40tnsu%2Fmonitoring-web/-/0.3.4/monitoring-web-0.3.4.tgz",
"integrity": "sha512-tQe0ZM1tBVO3XtbNFliRaaooqvCX3gb/Q/j2lV6+rYCXprodBZByQ6PLrCu/P0me/rW8KdQ02hu7avyLXXfkoQ==",
"license": "UNLICENSED",
"dependencies": {
"@rrweb/rrweb-plugin-console-record": "^2.2.0",
"rrweb": "^2.2.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"vue": "^3.0.0"
},
"peerDependenciesMeta": {
"react": {
"optional": true
},
"vue": {
"optional": true
}
}
},
"node_modules/@tnsu/ui-kit": {
"version": "0.15.0",
"resolved": "https://git.tnsu.kz/api/packages/tansu/npm/%40tnsu%2Fui-kit/-/0.15.0/ui-kit-0.15.0.tgz",
@@ -10022,6 +10068,12 @@
"@types/node": "*"
}
},
"node_modules/@types/css-font-loading-module": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/@types/css-font-loading-module/-/css-font-loading-module-0.0.7.tgz",
"integrity": "sha512-nl09VhutdjINdWyXxHWN/w9zlNCfr60JUqJbd24YXUuCwgeL0TpFSdElCwb6cxfB6ybE19Gjj4g0jsgkXxKv1Q==",
"license": "MIT"
},
"node_modules/@types/ejs": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/@types/ejs/-/ejs-3.1.5.tgz",
@@ -10250,7 +10302,7 @@
"version": "26.1.2",
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.2.tgz",
"integrity": "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"undici-types": "~8.3.0"
@@ -10279,6 +10331,7 @@
"version": "15.7.15",
"resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz",
"integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/qs": {
@@ -10308,6 +10361,7 @@
"version": "17.0.45",
"resolved": "https://registry.npmjs.org/@types/react/-/react-17.0.45.tgz",
"integrity": "sha512-YfhQ22Lah2e3CHPsb93tRwIGNiSwkuz1/blk4e6QrWS0jQzCSNbGLtOEYhPg02W0yGTTmpajp7dCTbBAMN3qsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
@@ -10319,6 +10373,7 @@
"version": "17.0.17",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.17.tgz",
"integrity": "sha512-VjnqEmqGnasQKV0CWLevqMTXBYG9GbwuE6x3VetERLh0cq2LTptFE73MrQi2S7GkKXCf2GgwItB/melLnxfnsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/react": "^17"
@@ -10341,6 +10396,7 @@
"version": "0.26.0",
"resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.26.0.tgz",
"integrity": "sha512-WFHp9YUJQ6CKshqoC37iOlHnQSmxNc795UhB26CyBBttrN9svdIrUjl/NjnNmfcwtncN0h/0PPAFWv9ovP8mLA==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/send": {
@@ -11627,6 +11683,12 @@
"node": ">=10.0.0"
}
},
"node_modules/@xstate/fsm": {
"version": "1.6.5",
"resolved": "https://registry.npmjs.org/@xstate/fsm/-/fsm-1.6.5.tgz",
"integrity": "sha512-b5o1I6aLNeYlU/3CPlj/Z91ybk1gUsKT+5NAJI+2W4UjvS5KLG28K9v5UvNoFVjHV8PajVZ00RH3vnjyQO7ZAw==",
"license": "MIT"
},
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz",
@@ -12513,6 +12575,15 @@
}
}
},
"node_modules/base64-arraybuffer": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-1.0.2.tgz",
"integrity": "sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==",
"license": "MIT",
"engines": {
"node": ">= 0.6.0"
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -12537,7 +12608,7 @@
"version": "2.11.7",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.7.tgz",
"integrity": "sha512-APw5YuIQAg6L9w4sHDI6j26DGFJI6RpYOhnkMPdC9lWbkKvsyPHzDsve1yd73lk21yz7Y09Kci8B2Pp9FonzWA==",
"devOptional": true,
"dev": true,
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.cjs"
@@ -12711,7 +12782,7 @@
"version": "4.28.7",
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.7.tgz",
"integrity": "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==",
"devOptional": true,
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -12920,7 +12991,7 @@
"version": "1.0.30001806",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001806.tgz",
"integrity": "sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==",
"devOptional": true,
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -13345,7 +13416,7 @@
"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==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/cookie": {
@@ -14234,7 +14305,7 @@
"version": "1.5.398",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.398.tgz",
"integrity": "sha512-AsvhAxopJGh6museTDMIjn6JpDYOfgu4RLlygomt87MUwBUqTfd/1EiPtx10/LZE8xpTvkP2E9Gafq7lkLtodQ==",
"devOptional": true,
"dev": true,
"license": "ISC"
},
"node_modules/embla-carousel": {
@@ -14564,7 +14635,7 @@
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@@ -15984,7 +16055,7 @@
"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==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -19133,7 +19204,7 @@
"version": "2.2.3",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz",
"integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"bin": {
"json5": "lib/cli.js"
@@ -19813,6 +19884,12 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
@@ -19935,7 +20012,6 @@
"version": "3.3.16",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
"integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
"dev": true,
"funding": [
{
"type": "github",
@@ -20077,7 +20153,7 @@
"version": "2.0.51",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.51.tgz",
"integrity": "sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=18"
@@ -20742,7 +20818,6 @@
"version": "8.5.25",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz",
"integrity": "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==",
"dev": true,
"funding": [
{
"type": "opencollective",
@@ -22018,6 +22093,40 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/rrdom": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/rrdom/-/rrdom-2.2.0.tgz",
"integrity": "sha512-gxcBJWVtS/Xz+Ys8xcjPoWqr+C7wz/MS8NgmKw136I47Xygy17FYOGZqoGwEyvBn724jRDK1ljS+8w9FsZStOg==",
"license": "MIT",
"dependencies": {
"rrweb-snapshot": "^2.2.0"
}
},
"node_modules/rrweb": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/rrweb/-/rrweb-2.2.0.tgz",
"integrity": "sha512-ofVzPq11/yIQ7WhVb++rJGI2LoPPrxdqu0QAvRA4KGeXJ7LH52jQ0VlRH4+VXn5PA/7pz85/rRkQyBUqT1LraQ==",
"license": "MIT",
"dependencies": {
"@rrweb/types": "^2.2.0",
"@rrweb/utils": "^2.2.0",
"@types/css-font-loading-module": "0.0.7",
"@xstate/fsm": "^1.4.0",
"base64-arraybuffer": "^1.0.1",
"mitt": "^3.0.0",
"rrdom": "^2.2.0",
"rrweb-snapshot": "^2.2.0"
}
},
"node_modules/rrweb-snapshot": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/rrweb-snapshot/-/rrweb-snapshot-2.2.0.tgz",
"integrity": "sha512-Tg/PiCLy+zV9C0Jfb4IAGABtt5l/UOvwW5xjFk41ZQhBfUjeqPP1rZ9HMDrFPc7cawCUEenWw54+g/4vluO5lg==",
"license": "MIT",
"dependencies": {
"postcss": "^8.4.38"
}
},
"node_modules/rtl-css-js": {
"version": "1.16.1",
"resolved": "https://registry.npmjs.org/rtl-css-js/-/rtl-css-js-1.16.1.tgz",
@@ -23127,7 +23236,6 @@
"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"
@@ -24407,7 +24515,7 @@
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
"integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/unicorn-magic": {
@@ -24461,7 +24569,7 @@
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
"integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==",
"devOptional": true,
"dev": true,
"funding": [
{
"type": "opencollective",
+2
View File
@@ -21,6 +21,8 @@
"@microsoft/sp-webpart-base": "1.22.1",
"@pnp/sp": "^4.20.0",
"@pnp/spfx-controls-react": "^3.25.0",
"@pnp/telemetry-js": "2.0.0",
"@tnsu/monitoring-web": "^0.3.4",
"@tnsu/ui-kit": "^0.15.0",
"jszip": "~3.8.0",
"react": "17.0.1",
@@ -9,6 +9,8 @@ import {
} from '@microsoft/sp-property-pane';
import { BaseClientSideWebPart } from '@microsoft/sp-webpart-base';
import { IReadonlyTheme } from '@microsoft/sp-component-base';
import PnPTelemetry from '@pnp/telemetry-js';
import { initMonitoring, spfxTokenProvider } from '@tnsu/monitoring-web';
import * as strings from 'RequestPortalWebPartStrings';
import RequestPortal from './components/RequestPortal';
@@ -98,7 +100,14 @@ export default class RequestPortalWebPart extends BaseClientSideWebPart<IRequest
}
protected onInit(): Promise<void> {
// Логи, ошибки, console.error и запись экрана в мониторинг (monitoring.tnsu.kz). Токен — молча через
// AAD веб-части (разрешение Logs.Write в package-solution.json); при отладке в workbench окружение — development.
// Один на страницу: повторный вызов (несколько веб-частей) библиотека игнорирует сама.
initMonitoring({ service: 'request-portal', getAccessToken: spfxTokenProvider(this.context.aadTokenProviderFactory) });
installErrorLogging(); // до всего остального — чтобы поймать и ранние отклонения
// Телеметрия PnP (её включает @pnp/spfx-controls-react) шлёт fetch на pnptelemetryproxy.azurewebsites.net
// без catch: где адрес закрыт прокси или блокировщиком, это необработанный отказ промиса в мониторинге.
PnPTelemetry.getInstance().optOut();
initSP(this.context); // единый клиент PnPjs для записи в списки
// Адрес approval-route должен быть выставлен ДО загрузки партиций — они
@@ -207,28 +207,6 @@ export const LANGUAGE_LEVEL_OPTIONS: ISelectOption[] = [
{ value: 'Свободный', label: 'Свободный' }
];
/**
* Программы (ПО) — ВРЕМЕННЫЙ список, пока нет справочника в API. Используется в
* мультивыборе «Необходимые знания в программах». Меняется здесь одним местом.
*/
export const SOFTWARE_OPTIONS: ISelectOption[] = [
{ value: '1С:Предприятие', label: '1С:Предприятие' },
{ value: 'MS Word', label: 'MS Word' },
{ value: 'MS Excel', label: 'MS Excel' },
{ value: 'MS PowerPoint', label: 'MS PowerPoint' },
{ value: 'MS Outlook', label: 'MS Outlook' },
{ value: 'MS Project', label: 'MS Project' },
{ value: 'AutoCAD', label: 'AutoCAD' },
{ value: 'Autodesk Forma', label: 'Autodesk Forma' },
{ value: 'BIM', label: 'BIM' },
{ value: 'Revit', label: 'Revit' },
{ value: 'ArchiCAD', label: 'ArchiCAD' },
{ value: 'Компас-3D', label: 'Компас-3D' },
{ value: 'Primavera', label: 'Primavera' },
{ value: 'SAP', label: 'SAP' },
{ value: 'Adobe Photoshop', label: 'Adobe Photoshop' }
];
/** Категории «Обеспечение рабочего места» (+ «Другое» через allowOther). */
export const WORKPLACE_PROVISION_OPTIONS: ISelectOption[] = [
{ value: 'Оргтехника', label: 'Оргтехника' },
@@ -1,6 +1,6 @@
import * as React from 'react';
import { CollapseSection, FormField, TextInput, TextArea, Select, MultiChoiceField } from '../../../shared/ui';
import { PositionPicker } from '../../../shared/dictionary';
import { PositionPicker, SOFTWARE_OPTIONS } from '../../../shared/dictionary';
import { LanguagesField } from './LanguagesField';
import {
ISectionProps,
@@ -8,7 +8,6 @@ import {
EDUCATION_OPTIONS,
WORK_EXPERIENCE_OPTIONS,
RESIDENCE_OPTIONS,
SOFTWARE_OPTIONS,
OTHER_VALUE
} from '../model';
@@ -28,6 +28,10 @@
**первого** шага согласования и это текущий пользователь; сохраняются кнопкой
«Сохранить» и автоматически при «Согласовать» (`updateReviewFields`,
`DecisionPanel.onBeforeDecide`). Остальная форма при этом read-only.
- **Необходимое ПО** — мультивыбор из общего списка `shared/dictionary/software.ts`
(тот же, что в подборе) + своё значение. В SP и в маршрут — по-прежнему ТЕКСТОМ
в колонке `Software` через запятую (`softwareList.ts`): условия «Software содержит
AutoCAD» и старые заявки со свободным текстом работают как раньше.
- **Мебель** — мультивыбор (`FURNITURE_OPTIONS`); при «Другое» появляется поле
своего значения. В SP — `WorkplaceFurnitureJson` (JSON-массив) и
`WorkplaceFurnitureOther`. Необязательная. Уходит в маршрут как
@@ -1,6 +1,8 @@
import * as React from 'react';
import { CollapseSection, FormField, TextArea, Toggle } from '../../../shared/ui';
import { CollapseSection, FormField, FormModeProvider, MultiChoiceField, TextArea, Toggle } from '../../../shared/ui';
import { SOFTWARE_OPTIONS } from '../../../shared/dictionary';
import { ISectionProps } from '../model';
import { formatSoftware, parseSoftware } from '../softwareList';
interface IEquipmentSectionProps extends ISectionProps {
/** Согласующий-руководитель на этапе согласования может править ПО и доступы,
@@ -13,13 +15,25 @@ export const EquipmentSection: React.FC<IEquipmentSectionProps> = ({ data, set,
// берёт значение из FormMode (в форме подачи редактируемо, в просмотре — нет).
const fieldReadOnly = approverEditable ? false : undefined;
const software = (
<MultiChoiceField
value={parseSoftware(data.Software)}
onChange={(items) => set('Software', formatSoftware(items))}
options={SOFTWARE_OPTIONS}
allowOther
placeholder="Выберите программу или введите свою"
/>
);
return (
<CollapseSection title="Оборудование и доступы" icon="monitor">
<FormField label="Нужна ли техника" fieldKey="NeedsEquipment">
<Toggle checked={data.NeedsEquipment} onChange={(v) => set('NeedsEquipment', v)} onLabel="Да" />
</FormField>
<FormField label="Необходимое ПО" htmlFor="Software" full>
<TextArea id="Software" value={data.Software} onChange={(v) => set('Software', v)} readOnly={fieldReadOnly} />
<FormField label="Необходимое ПО" fieldKey="Software" full>
{/* У MultiChoiceField нет своего readOnly — право руководителя даём вложенным
провайдером режима, как в FurnitureSection. */}
{approverEditable ? <FormModeProvider readOnly={false}>{software}</FormModeProvider> : software}
</FormField>
<FormField label="Доступы" htmlFor="AccessRights" full>
<TextArea id="AccessRights" value={data.AccessRights} onChange={(v) => set('AccessRights', v)} readOnly={fieldReadOnly} />
@@ -0,0 +1,31 @@
import { formatSoftware, parseSoftware } from './softwareList';
describe('parseSoftware', () => {
it('разбирает сохранённый список', () => {
expect(parseSoftware('AutoCAD, MS Excel, Revit')).toEqual(['AutoCAD', 'MS Excel', 'Revit']);
});
it('пустой текст — пустой список', () => {
expect(parseSoftware('')).toEqual([]);
expect(parseSoftware(' , \n ')).toEqual([]);
});
it('старый свободный текст с переносами строк — тоже чипами', () => {
expect(parseSoftware('AutoCAD\nКорпоративная почта')).toEqual(['AutoCAD', 'Корпоративная почта']);
});
it('убирает повторы', () => {
expect(parseSoftware('SAP, SAP, Revit')).toEqual(['SAP', 'Revit']);
});
});
describe('formatSoftware', () => {
it('склеивает через запятую — формат, который ждут условия маршрута', () => {
expect(formatSoftware(['AutoCAD', 'Revit'])).toBe('AutoCAD, Revit');
});
it('туда и обратно — без потерь', () => {
const items = ['1С:Предприятие', 'MS Word', 'Своя программа'];
expect(parseSoftware(formatSoftware(items))).toEqual(items);
});
});
@@ -0,0 +1,25 @@
/**
* «Необходимое ПО» — мультивыбор на экране, но в SP и в маршруте это ТЕКСТ в
* колонке Software (условия вида «Software содержит AutoCAD» заведены по нему).
* Поэтому модель хранит строку, а раздел переводит её в список и обратно.
*/
const SEPARATOR = ', ';
/**
* Текст колонки → список для мультивыбора. Делим по запятым и переносам строк:
* у старых заявок ПО написано свободным текстом — так он тоже ляжет чипами.
* Повторы убираем: чип с тем же значением был бы дублем.
*/
export function parseSoftware(text: string): string[] {
const items = text
.split(/[,\n]/)
.map((s) => s.trim())
.filter((s) => s.length > 0);
return items.filter((s, i) => items.indexOf(s) === i);
}
/** Список из мультивыбора → текст для колонки Software. */
export function formatSoftware(items: string[]): string {
return items.join(SEPARATOR);
}
@@ -1,3 +1,4 @@
import { log } from '@tnsu/monitoring-web';
import { getSP } from '../pnp/pnpjs';
import '@pnp/sp/webs';
import '@pnp/sp/lists';
@@ -25,6 +26,14 @@ export interface IPendingRow extends IApprovalRow {
isDelegator: boolean;
}
// Решение → текст события в логах мониторинга.
const DECISION_MESSAGE: Record<ApproverDecision, string> = {
approve: 'Заявка согласована',
return: 'Заявка возвращена на доработку',
reject: 'Заявка отклонена',
execute: 'Заявка выполнена исполнителем',
};
// Решение → статус его строки в списке согласования.
const ROW_STATUS: Record<ApproverDecision, string> = {
approve: REQUEST_STATUS.approved,
@@ -336,6 +345,7 @@ export class ApproverService {
const rows = await this.approval.getForRequest(requestId);
const requestStatus = this.computeRequestStatus(rows);
await this.updateRequestStatus(requestId, requestStatus);
await this.logDecision(requestId, rowId, decision, comment, requestStatus);
emitRequestsChanged();
return requestStatus;
}
@@ -364,6 +374,42 @@ export class ApproverService {
return REQUEST_STATUS.execution;
}
/**
* Решение — в логи мониторинга, с кодом и названием заявки, чтобы её было видно прямо в списке логов.
* Код и название дочитываются отдельно; не получилось — пишем без них: решение уже применено.
*/
private async logDecision(
requestId: number,
rowId: number,
decision: ApproverDecision,
comment: string,
requestStatus: string
): Promise<void> {
let code = '';
let title = '';
try {
const item = (await this.dataList().items.getById(requestId).select('Title', 'RequestUniqueCode')()) as {
Title?: string;
RequestUniqueCode?: string;
};
code = item.RequestUniqueCode || '';
title = item.Title || '';
} catch {
// без кода и названия — лог всё равно нужен
}
const what = [code, title].filter(Boolean).join(' · ') || `№${requestId}`;
log.info(`${DECISION_MESSAGE[decision]}: ${what}`, {
'request.id': requestId,
'request.code': code,
'request.title': title,
'request.list': this.dataListRef,
'request.status': requestStatus,
'approval.decision': decision,
'approval.rowId': rowId,
'approval.comment': comment.slice(0, 500),
});
}
private dataList(): IList {
return this.dataListRef.startsWith('/')
? getSP().web.getList(this.dataListRef)
@@ -2,10 +2,13 @@ import '@pnp/sp/webs';
import '@pnp/sp/lists';
import '@pnp/sp/items';
import { IList } from '@pnp/sp/lists';
import { log } from '@tnsu/monitoring-web';
import { getSP } from './pnp/pnpjs';
import { buildRequestUniqueCode } from './requestCode';
import { dataListTitle } from './listTitles';
import { RequestTypeId } from '../models/types';
import { REQUEST_TYPES } from '../data/requests';
import { isSubmittedStatus } from './approval/models/statuses';
/**
* Список данных, в который фактически пишем: listId (конкретная партиция —
@@ -50,6 +53,7 @@ export async function saveRequestItem(
if (existingId) {
await list.items.getById(existingId).update(item);
logSaved(formId, existingId, undefined, item, false);
return { id: existingId };
}
@@ -58,5 +62,34 @@ export async function saveRequestItem(
// Код известен только после создания элемента (нужен его Id).
const code = buildRequestUniqueCode(codePrefix, id, new Date());
await list.items.getById(id).update({ RequestUniqueCode: code });
logSaved(formId, id, code, item, true);
return { id, code };
}
/**
* События для мониторинга: какая заявка создана и отправлена на согласование (пользователь — из токена).
* В тексте — вид, код и название заявки, чтобы её было видно прямо в списке логов. Решения согласующих
* пишет approverDecision.service.ts.
*/
function logSaved(
formId: RequestTypeId,
id: number,
code: string | undefined,
item: Record<string, unknown>,
created: boolean
): void {
const typeTitle = REQUEST_TYPES.find((t) => t.id === formId)?.title ?? formId;
const title = typeof item.Title === 'string' ? item.Title : '';
const status = typeof item.RequestStatus === 'string' ? item.RequestStatus : '';
const what = [typeTitle, code, title].filter(Boolean).join(' · ');
const attributes = {
'request.type': formId,
'request.typeTitle': typeTitle,
'request.id': id,
'request.code': code ?? '',
'request.title': title,
'request.status': status,
};
if (created) log.info(`Создана заявка: ${what}`, attributes);
if (isSubmittedStatus(status)) log.info(`Заявка отправлена на согласование: ${what}`, attributes);
}
@@ -50,3 +50,4 @@ export {
parseOrganizationLine
} from './organizationLines';
export type { IOrganizationLine } from './organizationLines';
export { SOFTWARE_OPTIONS } from './software';
@@ -0,0 +1,27 @@
import { ISelectOption } from '@tnsu/ui-kit';
/**
* Программы (ПО) — ВРЕМЕННЫЙ список, пока нет справочника в API. Общий для форм:
* «Заявка на подбор» (3.8 Необходимые знания в программах) и «Организация
* рабочего места» (Необходимое ПО). Пункт добавляется здесь — появляется в обеих.
*
* Значения уходят в условия маршрута как текст («Software содержит AutoCAD»),
* поэтому существующие значения не переименовывать без правки условий в админке.
*/
export const SOFTWARE_OPTIONS: ISelectOption[] = [
{ value: '1С:Предприятие', label: '1С:Предприятие' },
{ value: 'MS Word', label: 'MS Word' },
{ value: 'MS Excel', label: 'MS Excel' },
{ value: 'MS PowerPoint', label: 'MS PowerPoint' },
{ value: 'MS Outlook', label: 'MS Outlook' },
{ value: 'MS Project', label: 'MS Project' },
{ value: 'AutoCAD', label: 'AutoCAD' },
{ value: 'Autodesk Forma', label: 'Autodesk Forma' },
{ value: 'BIM', label: 'BIM' },
{ value: 'Revit', label: 'Revit' },
{ value: 'ArchiCAD', label: 'ArchiCAD' },
{ value: 'Компас-3D', label: 'Компас-3D' },
{ value: 'Primavera', label: 'Primavera' },
{ value: 'SAP', label: 'SAP' },
{ value: 'Adobe Photoshop', label: 'Adobe Photoshop' }
];