Merge branch 'dev' into copilot/add-ai-settings-page

This commit is contained in:
dawnsystem 2025-11-15 18:05:36 +01:00 committed by GitHub
commit 7f615d578e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
66 changed files with 10792 additions and 370 deletions

214
src-ui/package-lock.json generated
View file

@ -1026,6 +1026,13 @@
"license": "MIT",
"dependencies": {
"@angular-devkit/core": "20.3.9",
"version": "0.2003.10",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2003.10.tgz",
"integrity": "sha512-2SWetxJzS8gRX6OKQstkWx37VRvZVgcEBDLsDSaeTjpnwh81A+niZQjAVRdwL0NEt1Wixk/RxfeUuCmdyyHvhQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@angular-devkit/core": "20.3.10",
"rxjs": "7.8.2"
},
"engines": {
@ -1038,6 +1045,9 @@
"version": "20.3.9",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-20.3.9.tgz",
"integrity": "sha512-DCzHY+EQ98u0h1n8s9add1KVSNWco1RW/Rl8TRkEuGmRQ43MpOfTIZQvlnnqaeMcNH0fZ4zkybVBDj7korJbZg==",
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-20.3.10.tgz",
"integrity": "sha512-SWGh1ASXEXtzFv/OSlmYGsYlIWHNeZRWkwkBe6mPfxZMX4JZ4HKbxmMtKV9hifvFdITU393IxPH5JXlFZJpZhQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1046,6 +1056,10 @@
"@angular-devkit/build-webpack": "0.2003.9",
"@angular-devkit/core": "20.3.9",
"@angular/build": "20.3.9",
"@angular-devkit/architect": "0.2003.10",
"@angular-devkit/build-webpack": "0.2003.10",
"@angular-devkit/core": "20.3.10",
"@angular/build": "20.3.10",
"@babel/core": "7.28.3",
"@babel/generator": "7.28.3",
"@babel/helper-annotate-as-pure": "7.27.3",
@ -1057,6 +1071,7 @@
"@babel/runtime": "7.28.3",
"@discoveryjs/json-ext": "0.6.3",
"@ngtools/webpack": "20.3.9",
"@ngtools/webpack": "20.3.10",
"ansi-colors": "4.1.3",
"autoprefixer": "10.4.21",
"babel-loader": "10.0.0",
@ -1112,6 +1127,7 @@
"@angular/platform-server": "^20.0.0",
"@angular/service-worker": "^20.0.0",
"@angular/ssr": "^20.3.9",
"@angular/ssr": "^20.3.10",
"@web/test-runner": "^0.20.0",
"browser-sync": "^3.0.2",
"jest": "^29.5.0 || ^30.2.0",
@ -1278,6 +1294,13 @@
"license": "MIT",
"dependencies": {
"@angular-devkit/architect": "0.2003.9",
"version": "0.2003.10",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.2003.10.tgz",
"integrity": "sha512-/e76O5MnoAplV+LW6XAWyd8e1KR1HqRTCSTngLMO+VMADbcQkD4i01ouridlxVLKkGDg83hvASUz2M6x0duZ9w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@angular-devkit/architect": "0.2003.10",
"rxjs": "7.8.2"
},
"engines": {
@ -1294,6 +1317,9 @@
"version": "20.3.9",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.9.tgz",
"integrity": "sha512-bXsAGIUb4p60x548YmvnMvjwd3FwWz6re1uTM7dV0XH8nQn3XMhOQ3Q3sAckzJHxkDuaRhB3K/a4kupoOmVfTQ==",
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.10.tgz",
"integrity": "sha512-COOT2eVebDwHhwENk12VR6m0wjL8D7p0dncEHF15zaBt1IXEnVhGESjSrs5klnPnt5T55qCBKyCTaeK7i/cS8Q==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -1326,6 +1352,13 @@
"license": "MIT",
"dependencies": {
"@angular-devkit/core": "20.3.9",
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.3.10.tgz",
"integrity": "sha512-2N2WF9lj+kr3uCG4+vFadYCL5hAT4dxMgzwScSdOqSd0O+GZD0CzKbDzlfvWIWC/ZealC5Sh4dFEQaRfmy72xA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@angular-devkit/core": "20.3.10",
"jsonc-parser": "3.3.1",
"magic-string": "0.30.17",
"ora": "8.2.0",
@ -1446,11 +1479,15 @@
"version": "20.3.9",
"resolved": "https://registry.npmjs.org/@angular/build/-/build-20.3.9.tgz",
"integrity": "sha512-Ulimvg6twPSCraaZECEmENfKBlD4M1yqeHlg6dCzFNM4xcwaGUnuG6O3cIQD59DaEvaG73ceM2y8ftYdxAwFow==",
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular/build/-/build-20.3.10.tgz",
"integrity": "sha512-nQrj1nMNZygYDilThc7hPrD6/NIWF/BOSgMfE4VkXQp8d0QronP3HFJ/h77MeoughMRFRhix0pqQSlXJQ2SGTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@ampproject/remapping": "2.3.0",
"@angular-devkit/architect": "0.2003.9",
"@angular-devkit/architect": "0.2003.10",
"@babel/core": "7.28.3",
"@babel/helper-annotate-as-pure": "7.27.3",
"@babel/helper-split-export-declaration": "7.24.7",
@ -1493,6 +1530,7 @@
"@angular/platform-server": "^20.0.0",
"@angular/service-worker": "^20.0.0",
"@angular/ssr": "^20.3.9",
"@angular/ssr": "^20.3.10",
"karma": "^6.4.0",
"less": "^4.2.0",
"ng-packagr": "^20.0.0",
@ -1545,6 +1583,9 @@
"version": "20.2.12",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.12.tgz",
"integrity": "sha512-hz8GtiMy3N9/e8407ZfrByHD5GEC4SkWtxyUknWuTM9P88AOie0jDZ6CfQg9gQ0OJX+6BAbJV3RpYZA1uzNUqA==",
"version": "20.2.13",
"resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.2.13.tgz",
"integrity": "sha512-h1jTkCmJ/rEQQMkxgKFMCBOrMfjZEnppgdekNmSTerwdVp4vdosTDTzFH/kwiOGFeRClffmvqQ2XLG8mQOKOtA==",
"license": "MIT",
"dependencies": {
"parse5": "^8.0.0",
@ -1570,6 +1611,19 @@
"@listr2/prompt-adapter-inquirer": "3.0.1",
"@modelcontextprotocol/sdk": "1.17.3",
"@schematics/angular": "20.3.9",
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.3.10.tgz",
"integrity": "sha512-CQzXScurBXSuMMn0jf6UYDItdggaM3bHYERKL4cUG1z5JqSozVFin1+TB1EjWYkddwdgC10R5xQurdMb+ahRNw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@angular-devkit/architect": "0.2003.10",
"@angular-devkit/core": "20.3.10",
"@angular-devkit/schematics": "20.3.10",
"@inquirer/prompts": "7.8.2",
"@listr2/prompt-adapter-inquirer": "3.0.1",
"@modelcontextprotocol/sdk": "1.17.3",
"@schematics/angular": "20.3.10",
"@yarnpkg/lockfile": "1.1.0",
"algoliasearch": "5.35.0",
"ini": "5.0.0",
@ -1595,6 +1649,9 @@
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.10.tgz",
"integrity": "sha512-12fEzvKbEqjqy1fSk9DMYlJz6dF1MJVXuC5BB+oWWJpd+2lfh4xJ62pkvvLGAICI89hfM5n9Cy5kWnXwnqPZsA==",
"version": "20.3.12",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-20.3.12.tgz",
"integrity": "sha512-rFcDfe67ffrb435C6t2lc27WGbizeOcgce30tUhH0iezwEvU+kHHWezXXX6Ylx3TFgqGkhcxL0fliuFYrpM1Vw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -1604,6 +1661,7 @@
},
"peerDependencies": {
"@angular/core": "20.3.10",
"@angular/core": "20.3.12",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
@ -1611,6 +1669,9 @@
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.10.tgz",
"integrity": "sha512-cW939Lr8GZjPSYfbQKIDNrUaHWmn2M+zBbERThfq5skLuY+xM60bJFv4NqBekfX6YqKLCY62ilUZlnImYIXaqA==",
"version": "20.3.12",
"resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.3.12.tgz",
"integrity": "sha512-bGESKz97nWiEQ/sydTq/Lzv3zlLvDb8t0msLG5Xti7Ch1EdLddXS8d2D/zFsjiGbAUKVsT6RgPCLHYoi4ocbhA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -1623,6 +1684,9 @@
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.3.10.tgz",
"integrity": "sha512-9BemvpFxA26yIVdu8ROffadMkEdlk/AQQ2Jb486w7RPkrvUQ0pbEJukhv9aryJvhbMopT66S5H/j4ipOUMzmzQ==",
"version": "20.3.12",
"resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.3.12.tgz",
"integrity": "sha512-3SJkexqsydYjIs0iLiJr5AdwkvumpzvjJM6s76iaxXHkRll5k/vM0wqkXLlSIwieBrecO9D4J73lDLWDevXl5A==",
"license": "MIT",
"dependencies": {
"@babel/core": "7.28.3",
@ -1643,6 +1707,7 @@
},
"peerDependencies": {
"@angular/compiler": "20.3.10",
"@angular/compiler": "20.3.12",
"typescript": ">=5.8 <6.0"
},
"peerDependenciesMeta": {
@ -1655,6 +1720,9 @@
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.10.tgz",
"integrity": "sha512-g99Qe+NOVo72OLxowVF9NjCckswWYHmvO7MgeiZTDJbTjF9tXH96dMx7AWq76/GUinV10sNzDysVW16NoAbCRQ==",
"version": "20.3.12",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-20.3.12.tgz",
"integrity": "sha512-K7vibMr55a7+EsuDhkg4Pk+ELuMm12olllwqL/CiQUcHXZ9Zgc4KYGTUuxWB69qJCG90gdSZS7tm5Dx0wDcyjg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -1664,6 +1732,7 @@
},
"peerDependencies": {
"@angular/compiler": "20.3.10",
"@angular/compiler": "20.3.12",
"rxjs": "^6.5.3 || ^7.4.0",
"zone.js": "~0.15.0"
},
@ -1680,6 +1749,9 @@
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.10.tgz",
"integrity": "sha512-9yWr51EUauTEINB745AaHwZNTHLpXIm4uxuykxzOg+g2QskEgVfH26uS8G2ogdNuwYpB8wnsXWr34qhM3qgOWw==",
"version": "20.3.12",
"resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.3.12.tgz",
"integrity": "sha512-O0Jy8ScaN3qVipDfR4s0SIxGrz/+MbCdmR05ZYVWf1W5P3dvETKt9WNjX9fYYV47GdgSveyFjuCR2NvWlv94zA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -1691,6 +1763,9 @@
"@angular/common": "20.3.10",
"@angular/core": "20.3.10",
"@angular/platform-browser": "20.3.10",
"@angular/common": "20.3.12",
"@angular/core": "20.3.12",
"@angular/platform-browser": "20.3.12",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
@ -1698,6 +1773,9 @@
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-20.3.10.tgz",
"integrity": "sha512-kw9yypjUdZP2uEknpNJq8Dryj4xAjwK0aIun0Wz2ZlnP8J6yH0U56qqKRQaqusKjt7fe1OFmJ2XbFEb0LrNlMw==",
"version": "20.3.12",
"resolved": "https://registry.npmjs.org/@angular/localize/-/localize-20.3.12.tgz",
"integrity": "sha512-wolRAeaWCh6kLNZitrlnQYm9nPaGQ2OwO04I10p1dEY2gC/nCMdJvh3umaOHTD2lN64ebZUxS5gJS8+PPTOcmg==",
"license": "MIT",
"dependencies": {
"@babel/core": "7.28.3",
@ -1722,6 +1800,14 @@
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.10.tgz",
"integrity": "sha512-UV8CGoB5P3FmJciI3/I/n3L7C3NVgGh7bIlZ1BaB/qJDtv0Wq0rRAGwmT/Z3gwmrRtfHZWme7/CeQ2CYJmMyUQ==",
"@angular/compiler": "20.3.12",
"@angular/compiler-cli": "20.3.12"
}
},
"node_modules/@angular/platform-browser": {
"version": "20.3.12",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.3.12.tgz",
"integrity": "sha512-14KQsXZyaQhbRwFz1W58CtbXQc9L+mfuHBgwQjQo99422Yk0ye5WVMb6DHH7dH671qFVqL0XL7zdOPBebaAnJQ==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -1733,6 +1819,9 @@
"@angular/animations": "20.3.10",
"@angular/common": "20.3.10",
"@angular/core": "20.3.10"
"@angular/animations": "20.3.12",
"@angular/common": "20.3.12",
"@angular/core": "20.3.12"
},
"peerDependenciesMeta": {
"@angular/animations": {
@ -1744,6 +1833,9 @@
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.3.10.tgz",
"integrity": "sha512-gtZPCuxfxxkMzHYBdTU9tJeTiHj+Aty3C408DJGtGU+7rZgKt9hDC14vQN9OVzB9Ly9Jwj2yr8u7AH80TxxCJw==",
"version": "20.3.12",
"resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.3.12.tgz",
"integrity": "sha512-VviTUCpcbwErQjWd+EZklQf1Fw1FtXui6ey4rEb9g9mCEJ/o08LkM7mWV5IoE6QNCfbgkfgNjEJSJvWe409Mow==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -1762,6 +1854,16 @@
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.10.tgz",
"integrity": "sha512-Z03cfH1jgQ7XMDJj4R8qAGqivcvhdG3wYBwaiN1K1ODBgPhbFKNeD4stKqYp7xBNtswmM2O2jMxrL/Djwju4Gg==",
"@angular/common": "20.3.12",
"@angular/compiler": "20.3.12",
"@angular/core": "20.3.12",
"@angular/platform-browser": "20.3.12"
}
},
"node_modules/@angular/router": {
"version": "20.3.12",
"resolved": "https://registry.npmjs.org/@angular/router/-/router-20.3.12.tgz",
"integrity": "sha512-hUipb9JI/Euy3bdlhzkcWlw3cTyssPTVTDwSvyGxWO4i+UKATQYmxh8EDOrDYzFp6Aexiy0Hff/H8umdsn6ZdA==",
"license": "MIT",
"dependencies": {
"tslib": "^2.3.0"
@ -1773,6 +1875,9 @@
"@angular/common": "20.3.10",
"@angular/core": "20.3.10",
"@angular/platform-browser": "20.3.10",
"@angular/common": "20.3.12",
"@angular/core": "20.3.12",
"@angular/platform-browser": "20.3.12",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
@ -3906,6 +4011,9 @@
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.0.tgz",
"integrity": "sha512-pJdKGq/1iquWYtv1RRSljZklxHCOCAJFJrImO5ZLKPJVJlVUcs8yFwNQlqS0Lo8xT1VAXXTCZocF9n26FWEKsw==",
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz",
"integrity": "sha512-o1uhUASyo921r2XtHYOHy7gdkGLge8ghBEQHMWmyJFoXlpU58kIrhhN3w26lpQb6dspetweapMn2CSNwQ8I4wg==",
"dev": true,
"license": "MIT",
"optional": true,
@ -3918,6 +4026,9 @@
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.0.tgz",
"integrity": "sha512-oAYoQnCYaQZKVS53Fq23ceWMRxq5EhQsE0x0RdQ55jT7wagMu5k+fS39v1fiSLrtrLQlXwVINenqhLMtTrV/1Q==",
"version": "1.7.1",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.1.tgz",
"integrity": "sha512-PVtJr5CmLwYAU9PZDMITZoR5iAOShYREoR45EyyLrbntV50mdePTgUn4AmOw90Ifcj+x2kRjdzr1HP3RrNiHGA==",
"dev": true,
"license": "MIT",
"optional": true,
@ -4667,11 +4778,15 @@
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.1.tgz",
"integrity": "sha512-rOcLotrptYIy59SGQhKlU0xBg1vvcVl2FdPIEclUvKHh0wo12OfGkId/01PIMJ/V+EimJ77t085YabgnQHBa5A==",
"version": "4.3.2",
"resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.3.2.tgz",
"integrity": "sha512-VXukHf0RR1doGe6Sm4F0Em7SWYLTHSsbGfJdS9Ja2bX5/D5uwVOEjr07cncLROdBvmnvCATYEWlHqYmXv2IlQA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/ansi": "^1.0.2",
"@inquirer/core": "^10.3.1",
"@inquirer/core": "^10.3.2",
"@inquirer/figures": "^1.0.15",
"@inquirer/type": "^3.0.10",
"yoctocolors-cjs": "^2.1.3"
@ -4714,6 +4829,9 @@
"version": "10.3.1",
"resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.1.tgz",
"integrity": "sha512-hzGKIkfomGFPgxKmnKEKeA+uCYBqC+TKtRx5LgyHRCrF6S2MliwRIjp3sUaWwVzMp7ZXVs8elB0Tfe682Rpg4w==",
"version": "10.3.2",
"resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.3.2.tgz",
"integrity": "sha512-43RTuEbfP8MbKzedNqBrlhhNKVwoK//vUFNW3Q3vZ88BLcrs4kYpGg+B2mm5p2K/HfygoCxuKwJJiv8PbGmE0A==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -4722,6 +4840,7 @@
"@inquirer/type": "^3.0.10",
"cli-width": "^4.1.0",
"mute-stream": "^3.0.0",
"mute-stream": "^2.0.0",
"signal-exit": "^4.1.0",
"wrap-ansi": "^6.2.0",
"yoctocolors-cjs": "^2.1.3"
@ -4746,6 +4865,13 @@
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.1",
"version": "4.2.23",
"resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.23.tgz",
"integrity": "sha512-aLSROkEwirotxZ1pBaP8tugXRFCxW94gwrQLxXfrZsKkfjOYC1aRvAZuhpJOb5cu4IBTJdsCigUlf2iCOu4ZDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.2",
"@inquirer/external-editor": "^1.0.3",
"@inquirer/type": "^3.0.10"
},
@ -4769,6 +4895,13 @@
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.1",
"version": "4.0.23",
"resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.23.tgz",
"integrity": "sha512-nRzdOyFYnpeYTTR2qFwEVmIWypzdAx/sIkCMeTNTcflFOovfqUk+HcFhQQVBftAh9gmGrpFj6QcGEqrDMDOiew==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.2",
"@inquirer/type": "^3.0.10",
"yoctocolors-cjs": "^2.1.3"
},
@ -4824,6 +4957,13 @@
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.1",
"version": "4.3.1",
"resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz",
"integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.2",
"@inquirer/type": "^3.0.10"
},
"engines": {
@ -4846,6 +4986,13 @@
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.1",
"version": "3.0.23",
"resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.23.tgz",
"integrity": "sha512-5Smv0OK7K0KUzUfYUXDXQc9jrf8OHo4ktlEayFlelCjwMXz0299Y8OrI+lj7i4gCBY15UObk76q0QtxjzFcFcg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.2",
"@inquirer/type": "^3.0.10"
},
"engines": {
@ -4864,11 +5011,15 @@
"version": "4.0.22",
"resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.22.tgz",
"integrity": "sha512-CbdqK1ioIr0Y3akx03k/+Twf+KSlHjn05hBL+rmubMll7PsDTGH0R4vfFkr+XrkB0FOHrjIwVP9crt49dgt+1g==",
"version": "4.0.23",
"resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.23.tgz",
"integrity": "sha512-zREJHjhT5vJBMZX/IUbyI9zVtVfOLiTO66MrF/3GFZYZ7T4YILW5MSkEYHceSii/KtRk+4i3RE7E1CUXA2jHcA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/ansi": "^1.0.2",
"@inquirer/core": "^10.3.1",
"@inquirer/core": "^10.3.2",
"@inquirer/type": "^3.0.10"
},
"engines": {
@ -4921,6 +5072,13 @@
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.1",
"version": "4.1.11",
"resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.11.tgz",
"integrity": "sha512-+LLQB8XGr3I5LZN/GuAHo+GpDJegQwuPARLChlMICNdwW7OwV2izlCSCxN6cqpL0sMXmbKbFcItJgdQq5EBXTw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.2",
"@inquirer/type": "^3.0.10",
"yoctocolors-cjs": "^2.1.3"
},
@ -4944,6 +5102,13 @@
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.1",
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.2.2.tgz",
"integrity": "sha512-p2bvRfENXCZdWF/U2BXvnSI9h+tuA8iNqtUKb9UWbmLYCRQxd8WkvwWvYn+3NgYaNwdUkHytJMGG4MMLucI1kA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/core": "^10.3.2",
"@inquirer/figures": "^1.0.15",
"@inquirer/type": "^3.0.10",
"yoctocolors-cjs": "^2.1.3"
@ -4964,11 +5129,15 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.1.tgz",
"integrity": "sha512-E9hbLU4XsNe2SAOSsFrtYtYQDVi1mfbqJrPDvXKnGlnRiApBdWMJz7r3J2Ff38AqULkPUD3XjQMD4492TymD7Q==",
"version": "4.4.2",
"resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz",
"integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==",
"dev": true,
"license": "MIT",
"dependencies": {
"@inquirer/ansi": "^1.0.2",
"@inquirer/core": "^10.3.1",
"@inquirer/core": "^10.3.2",
"@inquirer/figures": "^1.0.15",
"@inquirer/type": "^3.0.10",
"yoctocolors-cjs": "^2.1.3"
@ -5133,6 +5302,9 @@
"version": "3.14.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz",
"integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==",
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz",
"integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -6503,6 +6675,9 @@
"version": "20.3.9",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-20.3.9.tgz",
"integrity": "sha512-3h5laY9+kP7Tzociy3Lg5sMfpTTKMU+XbLQAHxnIvywHLD6r/fgVkwRli8GZf5JFMTwAkul0AQPKom9SCSWJLg==",
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-20.3.10.tgz",
"integrity": "sha512-W/+CGQFhmYEMJ/YgkC5p9khkxu2ocrvM0Pe0GxcUldrpBpdm1GCphEH1kTo7MeCupUK4/6rXGUt+GoA6PYchOg==",
"dev": true,
"license": "MIT",
"engines": {
@ -7674,6 +7849,14 @@
"dependencies": {
"@angular-devkit/core": "20.3.9",
"@angular-devkit/schematics": "20.3.9",
"version": "20.3.10",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.3.10.tgz",
"integrity": "sha512-F9ntS2CElpoWlENf4b03nwdTcN9Ri0Nb4SAE/pfRw3In09h2UHxYyf1ex9jqQt70xltDg4wvyuc3mMs+JlSx9A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@angular-devkit/core": "20.3.10",
"@angular-devkit/schematics": "20.3.10",
"jsonc-parser": "3.3.1"
},
"engines": {
@ -7803,6 +7986,9 @@
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz",
"integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==",
"version": "1.0.12",
"resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz",
"integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==",
"dev": true,
"license": "MIT"
},
@ -8229,6 +8415,9 @@
"version": "17.0.34",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.34.tgz",
"integrity": "sha512-KExbHVa92aJpw9WDQvzBaGVE2/Pz+pLZQloT2hjL8IqsZnV62rlPOYvNnLmf/L2dyllfVUOVBj64M0z/46eR2A==",
"version": "17.0.35",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz",
"integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -9559,6 +9748,9 @@
"version": "2.8.27",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.27.tgz",
"integrity": "sha512-2CXFpkjVnY2FT+B6GrSYxzYf65BJWEqz5tIRHCvNsZZ2F3CmsCB37h8SpYgKG7y9C4YAeTipIPWG7EmFmhAeXA==",
"version": "2.8.28",
"resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.28.tgz",
"integrity": "sha512-gYjt7OIqdM0PcttNYP2aVrr2G0bMALkBaoehD4BuRGjAOtipg0b6wHg1yNL+s5zSnLZZrGHOw4IrND8CD+3oIQ==",
"license": "Apache-2.0",
"bin": {
"baseline-browser-mapping": "dist/cli.js"
@ -10830,6 +11022,9 @@
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.3.0.tgz",
"integrity": "sha512-Qq68+VkJlc8tjnPV1i7HtbIn7ohmjZa88qUvHMIK0ZKUXMCuV45cT7cEXALPUmeXCe0q1DWQkQTemHVaLIFSrg==",
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.4.0.tgz",
"integrity": "sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==",
"dev": true,
"license": "MIT",
"dependencies": {
@ -10847,6 +11042,9 @@
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz",
"integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==",
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.1.tgz",
"integrity": "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==",
"dev": true,
"license": "MIT",
"engines": {
@ -11092,6 +11290,9 @@
"version": "1.5.250",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.250.tgz",
"integrity": "sha512-/5UMj9IiGDMOFBnN4i7/Ry5onJrAGSbOGo3s9FEKmwobGq6xw832ccET0CE3CkkMBZ8GJSlUIesZofpyurqDXw==",
"version": "1.5.253",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.253.tgz",
"integrity": "sha512-O0tpQ/35rrgdiGQ0/OFWhy1itmd9A6TY9uQzlqj3hKSu/aYpe7UIn5d7CU2N9myH6biZiWF3VMZVuup8pw5U9w==",
"license": "ISC"
},
"node_modules/emittery": {
@ -14545,6 +14746,9 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"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": {
@ -15782,6 +15986,13 @@
"license": "ISC",
"engines": {
"node": "^20.17.0 || >=22.9.0"
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
"integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==",
"dev": true,
"license": "ISC",
"engines": {
"node": "^18.17.0 || >=20.5.0"
}
},
"node_modules/nan": {
@ -15993,6 +16204,9 @@
"version": "3.80.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.80.0.tgz",
"integrity": "sha512-LyPuZJcI9HVwzXK1GPxWNzrr+vr8Hp/3UqlmWxxh8p54U1ZbclOqbSog9lWHaCX+dBaiGi6n/hIX+mKu74GmPA==",
"version": "3.85.0",
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.85.0.tgz",
"integrity": "sha512-zsFhmbkAzwhTft6nd3VxcG0cvJsT70rL+BIGHWVq5fi6MwGrHwzqKaxXE+Hl2GmnGItnDKPPkO5/LQqjVkIdFg==",
"license": "MIT",
"optional": true,
"dependencies": {

View file

@ -11,6 +11,7 @@
},
"private": true,
"dependencies": {
"@angular/animations": "~20.3.12",
"@angular/cdk": "^20.2.6",
"@angular/common": "~20.3.2",
"@angular/compiler": "~20.3.2",

120
src-ui/pnpm-lock.yaml generated
View file

@ -8,6 +8,9 @@ importers:
.:
dependencies:
'@angular/animations':
specifier: ~20.3.12
version: 20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))
'@angular/cdk':
specifier: ^20.2.6
version: 20.2.6(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
@ -22,28 +25,28 @@ importers:
version: 20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)
'@angular/forms':
specifier: ~20.3.2
version: 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
version: 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
'@angular/localize':
specifier: ~20.3.2
version: 20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)
'@angular/platform-browser':
specifier: ~20.3.2
version: 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))
version: 20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))
'@angular/platform-browser-dynamic':
specifier: ~20.3.2
version: 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))
version: 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))
'@angular/router':
specifier: ~20.3.2
version: 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
version: 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
'@ng-bootstrap/ng-bootstrap':
specifier: ^19.0.1
version: 19.0.1(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/forms@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@popperjs/core@2.11.8)(rxjs@7.8.2)
version: 19.0.1(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/forms@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@popperjs/core@2.11.8)(rxjs@7.8.2)
'@ng-select/ng-select':
specifier: ^20.6.3
version: 20.6.3(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/forms@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))
version: 20.6.3(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/forms@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))
'@ngneat/dirty-check-forms':
specifier: ^3.0.3
version: 3.0.3(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/forms@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))(@angular/router@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))(lodash-es@4.17.21)(rxjs@7.8.2)
version: 3.0.3(291c247a225ddc29ee470ed21e444e55)
'@popperjs/core':
specifier: ^2.11.8
version: 2.11.8
@ -73,7 +76,7 @@ importers:
version: 10.1.0(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))
ngx-ui-tour-ng-bootstrap:
specifier: ^17.0.1
version: 17.0.1(a51ec0d773a3e93ac3d51d20ca771021)
version: 17.0.1(f8db16ccbb0d6be45bab4b8410cc9846)
rxjs:
specifier: ^7.8.2
version: 7.8.2
@ -92,10 +95,10 @@ importers:
devDependencies:
'@angular-builders/custom-webpack':
specifier: ^20.0.0
version: 20.0.0(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.6.1)(chokidar@4.0.3)(jest-environment-jsdom@30.2.0(canvas@3.0.0))(jest@30.2.0(@types/node@24.6.1)(ts-node@10.9.2(@types/node@24.6.1)(typescript@5.8.3)))(jiti@1.21.7)(less@4.3.0)(postcss@8.5.3)(terser@5.39.1)(tslib@2.8.1)(typescript@5.8.3)(vite@7.1.5(@types/node@24.6.1)(jiti@1.21.7)(less@4.3.0)(sass@1.90.0)(terser@5.39.1)(yaml@2.7.0))(yaml@2.7.0)
version: 20.0.0(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.6.1)(chokidar@4.0.3)(jest-environment-jsdom@30.2.0(canvas@3.0.0))(jest@30.2.0(@types/node@24.6.1)(ts-node@10.9.2(@types/node@24.6.1)(typescript@5.8.3)))(jiti@1.21.7)(less@4.3.0)(postcss@8.5.3)(terser@5.39.1)(tslib@2.8.1)(typescript@5.8.3)(vite@7.1.5(@types/node@24.6.1)(jiti@1.21.7)(less@4.3.0)(sass@1.90.0)(terser@5.39.1)(yaml@2.7.0))(yaml@2.7.0)
'@angular-builders/jest':
specifier: ^20.0.0
version: 20.0.0(617e23274585616dcf62fd78c9140eac)
version: 20.0.0(496b29fc4599be2dae83ff2679fdbd16)
'@angular-devkit/core':
specifier: ^20.3.3
version: 20.3.3(chokidar@4.0.3)
@ -119,7 +122,7 @@ importers:
version: 20.3.0(eslint@9.36.0(jiti@1.21.7))(typescript@5.8.3)
'@angular/build':
specifier: ^20.3.3
version: 20.3.3(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.6.1)(chokidar@4.0.3)(jiti@1.21.7)(less@4.3.0)(postcss@8.5.3)(terser@5.39.1)(tslib@2.8.1)(typescript@5.8.3)(yaml@2.7.0)
version: 20.3.3(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.6.1)(chokidar@4.0.3)(jiti@1.21.7)(less@4.3.0)(postcss@8.5.3)(terser@5.39.1)(tslib@2.8.1)(typescript@5.8.3)(yaml@2.7.0)
'@angular/cli':
specifier: ~20.3.3
version: 20.3.3(@types/node@24.6.1)(chokidar@4.0.3)
@ -161,7 +164,7 @@ importers:
version: 16.0.0
jest-preset-angular:
specifier: ^15.0.2
version: 15.0.2(ccefccc315e3e4bd30d78eb49c90d46a)
version: 15.0.2(83827844341020d1e6edc9d0e74e3f3d)
jest-websocket-mock:
specifier: ^2.5.0
version: 2.5.0
@ -403,6 +406,12 @@ packages:
eslint: ^8.57.0 || ^9.0.0
typescript: '*'
'@angular/animations@20.3.12':
resolution: {integrity: sha512-tkzruF0pbcOrC2lwsPKjkp5btazs6vcX4At7kyVFjjuPbgI6RNG+MoFXHpN9ypenscYtTAhDcPSmjBnzoDaXhQ==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0}
peerDependencies:
'@angular/core': 20.3.12
'@angular/build@20.0.4':
resolution: {integrity: sha512-SIYLg2st05Q5hgFrxwj6L4i9j2j2JNWYoYgacXp+mw9YVhFiC02Ymbakc9fq+3+sWlm0XTX5JgrupV2ac1ytNQ==}
engines: {node: ^20.19.0 || ^22.12.0 || >=24.0.0, npm: ^6.11.0 || ^7.5.6 || >=8.0.0, yarn: '>= 1.13.0'}
@ -7096,13 +7105,13 @@ snapshots:
- chokidar
- typescript
'@angular-builders/custom-webpack@20.0.0(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.6.1)(chokidar@4.0.3)(jest-environment-jsdom@30.2.0(canvas@3.0.0))(jest@30.2.0(@types/node@24.6.1)(ts-node@10.9.2(@types/node@24.6.1)(typescript@5.8.3)))(jiti@1.21.7)(less@4.3.0)(postcss@8.5.3)(terser@5.39.1)(tslib@2.8.1)(typescript@5.8.3)(vite@7.1.5(@types/node@24.6.1)(jiti@1.21.7)(less@4.3.0)(sass@1.90.0)(terser@5.39.1)(yaml@2.7.0))(yaml@2.7.0)':
dependencies:
? '@angular-builders/custom-webpack@20.0.0(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.6.1)(chokidar@4.0.3)(jest-environment-jsdom@30.2.0(canvas@3.0.0))(jest@30.2.0(@types/node@24.6.1)(ts-node@10.9.2(@types/node@24.6.1)(typescript@5.8.3)))(jiti@1.21.7)(less@4.3.0)(postcss@8.5.3)(terser@5.39.1)(tslib@2.8.1)(typescript@5.8.3)(vite@7.1.5(@types/node@24.6.1)(jiti@1.21.7)(less@4.3.0)(sass@1.90.0)(terser@5.39.1)(yaml@2.7.0))(yaml@2.7.0)'
: dependencies:
'@angular-builders/common': 4.0.0(@types/node@24.6.1)(chokidar@4.0.3)(typescript@5.8.3)
'@angular-devkit/architect': 0.2000.4(chokidar@4.0.3)
'@angular-devkit/build-angular': 20.0.4(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.6.1)(chokidar@4.0.3)(jest-environment-jsdom@30.2.0(canvas@3.0.0))(jest@30.2.0(@types/node@24.6.1)(ts-node@10.9.2(@types/node@24.6.1)(typescript@5.8.3)))(jiti@1.21.7)(typescript@5.8.3)(vite@7.1.5(@types/node@24.6.1)(jiti@1.21.7)(less@4.3.0)(sass@1.90.0)(terser@5.39.1)(yaml@2.7.0))(yaml@2.7.0)
'@angular-devkit/build-angular': 20.0.4(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.6.1)(chokidar@4.0.3)(jest-environment-jsdom@30.2.0(canvas@3.0.0))(jest@30.2.0(@types/node@24.6.1)(ts-node@10.9.2(@types/node@24.6.1)(typescript@5.8.3)))(jiti@1.21.7)(typescript@5.8.3)(vite@7.1.5(@types/node@24.6.1)(jiti@1.21.7)(less@4.3.0)(sass@1.90.0)(terser@5.39.1)(yaml@2.7.0))(yaml@2.7.0)
'@angular-devkit/core': 20.3.3(chokidar@4.0.3)
'@angular/build': 20.3.3(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.6.1)(chokidar@4.0.3)(jiti@1.21.7)(less@4.3.0)(postcss@8.5.3)(terser@5.39.1)(tslib@2.8.1)(typescript@5.8.3)(yaml@2.7.0)
'@angular/build': 20.3.3(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.6.1)(chokidar@4.0.3)(jiti@1.21.7)(less@4.3.0)(postcss@8.5.3)(terser@5.39.1)(tslib@2.8.1)(typescript@5.8.3)(yaml@2.7.0)
'@angular/compiler-cli': 20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3)
lodash: 4.17.21
webpack-merge: 6.0.1
@ -7150,17 +7159,17 @@ snapshots:
- webpack-cli
- yaml
'@angular-builders/jest@20.0.0(617e23274585616dcf62fd78c9140eac)':
'@angular-builders/jest@20.0.0(496b29fc4599be2dae83ff2679fdbd16)':
dependencies:
'@angular-builders/common': 4.0.0(@types/node@24.6.1)(chokidar@4.0.3)(typescript@5.8.3)
'@angular-devkit/architect': 0.2000.4(chokidar@4.0.3)
'@angular-devkit/build-angular': 20.0.4(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.6.1)(chokidar@4.0.3)(jest-environment-jsdom@30.2.0(canvas@3.0.0))(jest@30.2.0(@types/node@24.6.1)(ts-node@10.9.2(@types/node@24.6.1)(typescript@5.8.3)))(jiti@1.21.7)(typescript@5.8.3)(vite@7.1.5(@types/node@24.6.1)(jiti@1.21.7)(less@4.3.0)(sass@1.90.0)(terser@5.39.1)(yaml@2.7.0))(yaml@2.7.0)
'@angular-devkit/build-angular': 20.0.4(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.6.1)(chokidar@4.0.3)(jest-environment-jsdom@30.2.0(canvas@3.0.0))(jest@30.2.0(@types/node@24.6.1)(ts-node@10.9.2(@types/node@24.6.1)(typescript@5.8.3)))(jiti@1.21.7)(typescript@5.8.3)(vite@7.1.5(@types/node@24.6.1)(jiti@1.21.7)(less@4.3.0)(sass@1.90.0)(terser@5.39.1)(yaml@2.7.0))(yaml@2.7.0)
'@angular-devkit/core': 20.3.3(chokidar@4.0.3)
'@angular/compiler-cli': 20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3)
'@angular/core': 20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)
'@angular/platform-browser-dynamic': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))
'@angular/platform-browser-dynamic': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))
jest: 30.2.0(@types/node@24.6.1)(ts-node@10.9.2(@types/node@24.6.1)(typescript@5.8.3))
jest-preset-angular: 14.6.0(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser-dynamic@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))))(@babel/core@7.28.4)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.4))(canvas@3.0.0)(jest@30.2.0(@types/node@24.6.1)(ts-node@10.9.2(@types/node@24.6.1)(typescript@5.8.3)))(jsdom@26.1.0(canvas@3.0.0))(typescript@5.8.3)
jest-preset-angular: 14.6.0(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser-dynamic@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))))(@babel/core@7.28.4)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.4))(canvas@3.0.0)(jest@30.2.0(@types/node@24.6.1)(ts-node@10.9.2(@types/node@24.6.1)(typescript@5.8.3)))(jsdom@26.1.0(canvas@3.0.0))(typescript@5.8.3)
lodash: 4.17.21
transitivePeerDependencies:
- '@babel/core'
@ -7192,13 +7201,13 @@ snapshots:
transitivePeerDependencies:
- chokidar
'@angular-devkit/build-angular@20.0.4(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.6.1)(chokidar@4.0.3)(jest-environment-jsdom@30.2.0(canvas@3.0.0))(jest@30.2.0(@types/node@24.6.1)(ts-node@10.9.2(@types/node@24.6.1)(typescript@5.8.3)))(jiti@1.21.7)(typescript@5.8.3)(vite@7.1.5(@types/node@24.6.1)(jiti@1.21.7)(less@4.3.0)(sass@1.90.0)(terser@5.39.1)(yaml@2.7.0))(yaml@2.7.0)':
'@angular-devkit/build-angular@20.0.4(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.6.1)(chokidar@4.0.3)(jest-environment-jsdom@30.2.0(canvas@3.0.0))(jest@30.2.0(@types/node@24.6.1)(ts-node@10.9.2(@types/node@24.6.1)(typescript@5.8.3)))(jiti@1.21.7)(typescript@5.8.3)(vite@7.1.5(@types/node@24.6.1)(jiti@1.21.7)(less@4.3.0)(sass@1.90.0)(terser@5.39.1)(yaml@2.7.0))(yaml@2.7.0)':
dependencies:
'@ampproject/remapping': 2.3.0
'@angular-devkit/architect': 0.2000.4(chokidar@4.0.3)
'@angular-devkit/build-webpack': 0.2000.4(chokidar@4.0.3)(webpack-dev-server@5.2.1(webpack@5.102.0))(webpack@5.99.8(esbuild@0.25.5))
'@angular-devkit/core': 20.0.4(chokidar@4.0.3)
'@angular/build': 20.0.4(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.6.1)(chokidar@4.0.3)(jiti@1.21.7)(less@4.3.0)(postcss@8.5.3)(terser@5.39.1)(tslib@2.8.1)(typescript@5.8.3)(yaml@2.7.0)
'@angular/build': 20.0.4(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.6.1)(chokidar@4.0.3)(jiti@1.21.7)(less@4.3.0)(postcss@8.5.3)(terser@5.39.1)(tslib@2.8.1)(typescript@5.8.3)(yaml@2.7.0)
'@angular/compiler-cli': 20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3)
'@babel/core': 7.27.1
'@babel/generator': 7.27.1
@ -7254,7 +7263,7 @@ snapshots:
optionalDependencies:
'@angular/core': 20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)
'@angular/localize': 20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)
'@angular/platform-browser': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))
'@angular/platform-browser': 20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))
esbuild: 0.25.5
jest: 30.2.0(@types/node@24.6.1)(ts-node@10.9.2(@types/node@24.6.1)(typescript@5.8.3))
jest-environment-jsdom: 30.2.0(canvas@3.0.0)
@ -7386,7 +7395,12 @@ snapshots:
eslint: 9.36.0(jiti@1.21.7)
typescript: 5.8.3
'@angular/build@20.0.4(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.6.1)(chokidar@4.0.3)(jiti@1.21.7)(less@4.3.0)(postcss@8.5.3)(terser@5.39.1)(tslib@2.8.1)(typescript@5.8.3)(yaml@2.7.0)':
'@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))':
dependencies:
'@angular/core': 20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)
tslib: 2.8.1
'@angular/build@20.0.4(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.6.1)(chokidar@4.0.3)(jiti@1.21.7)(less@4.3.0)(postcss@8.5.3)(terser@5.39.1)(tslib@2.8.1)(typescript@5.8.3)(yaml@2.7.0)':
dependencies:
'@ampproject/remapping': 2.3.0
'@angular-devkit/architect': 0.2000.4(chokidar@4.0.3)
@ -7421,7 +7435,7 @@ snapshots:
optionalDependencies:
'@angular/core': 20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)
'@angular/localize': 20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)
'@angular/platform-browser': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))
'@angular/platform-browser': 20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))
less: 4.3.0
lmdb: 3.3.0
postcss: 8.5.3
@ -7438,7 +7452,7 @@ snapshots:
- tsx
- yaml
'@angular/build@20.3.3(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.6.1)(chokidar@4.0.3)(jiti@1.21.7)(less@4.3.0)(postcss@8.5.3)(terser@5.39.1)(tslib@2.8.1)(typescript@5.8.3)(yaml@2.7.0)':
'@angular/build@20.3.3(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@types/node@24.6.1)(chokidar@4.0.3)(jiti@1.21.7)(less@4.3.0)(postcss@8.5.3)(terser@5.39.1)(tslib@2.8.1)(typescript@5.8.3)(yaml@2.7.0)':
dependencies:
'@ampproject/remapping': 2.3.0
'@angular-devkit/architect': 0.2003.3(chokidar@4.0.3)
@ -7473,7 +7487,7 @@ snapshots:
optionalDependencies:
'@angular/core': 20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)
'@angular/localize': 20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)
'@angular/platform-browser': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))
'@angular/platform-browser': 20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))
less: 4.3.0
lmdb: 3.4.2
postcss: 8.5.3
@ -7557,11 +7571,11 @@ snapshots:
'@angular/compiler': 20.3.2
zone.js: 0.15.1
'@angular/forms@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)':
'@angular/forms@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)':
dependencies:
'@angular/common': 20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
'@angular/core': 20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)
'@angular/platform-browser': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))
'@angular/platform-browser': 20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))
rxjs: 7.8.2
tslib: 2.8.1
@ -7576,25 +7590,27 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@angular/platform-browser-dynamic@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))':
'@angular/platform-browser-dynamic@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))':
dependencies:
'@angular/common': 20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
'@angular/compiler': 20.3.2
'@angular/core': 20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)
'@angular/platform-browser': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))
'@angular/platform-browser': 20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))
tslib: 2.8.1
'@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))':
'@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))':
dependencies:
'@angular/common': 20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
'@angular/core': 20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)
tslib: 2.8.1
optionalDependencies:
'@angular/animations': 20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))
'@angular/router@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)':
'@angular/router@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)':
dependencies:
'@angular/common': 20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
'@angular/core': 20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)
'@angular/platform-browser': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))
'@angular/platform-browser': 20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))
rxjs: 7.8.2
tslib: 2.8.1
@ -9403,28 +9419,28 @@ snapshots:
'@tybys/wasm-util': 0.10.1
optional: true
'@ng-bootstrap/ng-bootstrap@19.0.1(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/forms@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@popperjs/core@2.11.8)(rxjs@7.8.2)':
'@ng-bootstrap/ng-bootstrap@19.0.1(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/forms@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@popperjs/core@2.11.8)(rxjs@7.8.2)':
dependencies:
'@angular/common': 20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
'@angular/core': 20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)
'@angular/forms': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
'@angular/forms': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
'@angular/localize': 20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2)
'@popperjs/core': 2.11.8
rxjs: 7.8.2
tslib: 2.8.1
'@ng-select/ng-select@20.6.3(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/forms@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))':
'@ng-select/ng-select@20.6.3(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/forms@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))':
dependencies:
'@angular/common': 20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
'@angular/core': 20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)
'@angular/forms': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
'@angular/forms': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
tslib: 2.8.1
? '@ngneat/dirty-check-forms@3.0.3(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/forms@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))(@angular/router@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))(lodash-es@4.17.21)(rxjs@7.8.2)'
: dependencies:
'@ngneat/dirty-check-forms@3.0.3(291c247a225ddc29ee470ed21e444e55)':
dependencies:
'@angular/core': 20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)
'@angular/forms': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
'@angular/router': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
'@angular/forms': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
'@angular/router': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
lodash-es: 4.17.21
rxjs: 7.8.2
tslib: 2.8.1
@ -12158,11 +12174,11 @@ snapshots:
optionalDependencies:
jest-resolve: 30.2.0
jest-preset-angular@14.6.0(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser-dynamic@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))))(@babel/core@7.28.4)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.4))(canvas@3.0.0)(jest@30.2.0(@types/node@24.6.1)(ts-node@10.9.2(@types/node@24.6.1)(typescript@5.8.3)))(jsdom@26.1.0(canvas@3.0.0))(typescript@5.8.3):
jest-preset-angular@14.6.0(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser-dynamic@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))))(@babel/core@7.28.4)(@jest/transform@30.2.0)(@jest/types@30.2.0)(babel-jest@30.2.0(@babel/core@7.28.4))(canvas@3.0.0)(jest@30.2.0(@types/node@24.6.1)(ts-node@10.9.2(@types/node@24.6.1)(typescript@5.8.3)))(jsdom@26.1.0(canvas@3.0.0))(typescript@5.8.3):
dependencies:
'@angular/compiler-cli': 20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3)
'@angular/core': 20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)
'@angular/platform-browser-dynamic': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))
'@angular/platform-browser-dynamic': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))
bs-logger: 0.2.6
esbuild-wasm: 0.25.10
jest: 30.2.0(@types/node@24.6.1)(ts-node@10.9.2(@types/node@24.6.1)(typescript@5.8.3))
@ -12184,12 +12200,12 @@ snapshots:
- supports-color
- utf-8-validate
jest-preset-angular@15.0.2(ccefccc315e3e4bd30d78eb49c90d46a):
jest-preset-angular@15.0.2(83827844341020d1e6edc9d0e74e3f3d):
dependencies:
'@angular/compiler-cli': 20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3)
'@angular/core': 20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)
'@angular/platform-browser': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))
'@angular/platform-browser-dynamic': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))
'@angular/platform-browser': 20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))
'@angular/platform-browser-dynamic': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/compiler@20.3.2)(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))
'@jest/environment-jsdom-abstract': 30.2.0(canvas@3.0.0)(jsdom@26.1.0(canvas@3.0.0))
bs-logger: 0.2.6
esbuild-wasm: 0.25.10
@ -12883,20 +12899,20 @@ snapshots:
'@angular/core': 20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)
tslib: 2.8.1
ngx-ui-tour-core@15.0.0(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/router@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))(rxjs@7.8.2):
ngx-ui-tour-core@15.0.0(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/router@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))(rxjs@7.8.2):
dependencies:
'@angular/common': 20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
'@angular/core': 20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)
'@angular/router': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
'@angular/router': 20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2)
rxjs: 7.8.2
tslib: 2.8.1
ngx-ui-tour-ng-bootstrap@17.0.1(a51ec0d773a3e93ac3d51d20ca771021):
ngx-ui-tour-ng-bootstrap@17.0.1(f8db16ccbb0d6be45bab4b8410cc9846):
dependencies:
'@angular/common': 20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2)
'@angular/core': 20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)
'@ng-bootstrap/ng-bootstrap': 19.0.1(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/forms@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@popperjs/core@2.11.8)(rxjs@7.8.2)
ngx-ui-tour-core: 15.0.0(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/router@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))(rxjs@7.8.2)
'@ng-bootstrap/ng-bootstrap': 19.0.1(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/forms@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))(@angular/localize@20.3.2(@angular/compiler-cli@20.3.2(@angular/compiler@20.3.2)(typescript@5.8.3))(@angular/compiler@20.3.2))(@popperjs/core@2.11.8)(rxjs@7.8.2)
ngx-ui-tour-core: 15.0.0(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/router@20.3.2(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(@angular/platform-browser@20.3.2(@angular/animations@20.3.12(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(@angular/common@20.3.2(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1))(rxjs@7.8.2))(@angular/core@20.3.2(@angular/compiler@20.3.2)(rxjs@7.8.2)(zone.js@0.15.1)))(rxjs@7.8.2))(rxjs@7.8.2)
tslib: 2.8.1
transitivePeerDependencies:
- '@angular/router'

View file

@ -8,6 +8,7 @@ import { TrashComponent } from './components/admin/trash/trash.component'
import { UsersAndGroupsComponent } from './components/admin/users-groups/users-groups.component'
import { AppFrameComponent } from './components/app-frame/app-frame.component'
import { DashboardComponent } from './components/dashboard/dashboard.component'
import { DeletionRequestsComponent } from './components/deletion-requests/deletion-requests.component'
import { DocumentAsnComponent } from './components/document-asn/document-asn.component'
import { DocumentDetailComponent } from './components/document-detail/document-detail.component'
import { DocumentListComponent } from './components/document-list/document-list.component'
@ -174,6 +175,18 @@ export const routes: Routes = [
componentName: 'TrashComponent',
},
},
{
path: 'deletion-requests',
component: DeletionRequestsComponent,
canActivate: [PermissionsGuard],
data: {
requiredPermission: {
action: PermissionAction.View,
type: PermissionType.Document,
},
componentName: 'DeletionRequestsComponent',
},
},
// redirect old paths
{
path: 'settings/mail',

View file

@ -0,0 +1,126 @@
@if (hasSuggestions) {
<div class="ai-suggestions-panel card shadow-sm mb-3" [@slideIn]>
<div class="card-header bg-primary text-white d-flex align-items-center justify-content-between" role="button" (click)="toggleCollapse()">
<div class="d-flex align-items-center">
<i-bs name="magic" width="1.2em" height="1.2em" class="me-2"></i-bs>
<strong i18n>AI Suggestions</strong>
<span class="badge bg-light text-primary ms-2">{{ pendingSuggestions.length }}</span>
</div>
<div class="d-flex align-items-center gap-2">
@if (appliedCount > 0) {
<span class="badge bg-success" i18n>{{ appliedCount }} applied</span>
}
@if (rejectedCount > 0) {
<span class="badge bg-danger" i18n>{{ rejectedCount }} rejected</span>
}
<i-bs [name]="isCollapsed ? 'chevron-down' : 'chevron-up'" width="1em" height="1em"></i-bs>
</div>
</div>
<div [ngbCollapse]="isCollapsed" class="card-body">
<div class="mb-3 pb-2 border-bottom">
<p class="text-muted small mb-2" i18n>
<i-bs name="info-circle" width="0.9em" height="0.9em" class="me-1"></i-bs>
AI has analyzed this document and suggests the following metadata. Review and apply or reject each suggestion.
</p>
<div class="d-flex gap-2 flex-wrap">
<button
type="button"
class="btn btn-sm btn-success"
[disabled]="disabled || pendingSuggestions.length === 0"
(click)="applyAll()"
i18n>
<i-bs name="check-all" width="1em" height="1em" class="me-1"></i-bs>
Apply All
</button>
<button
type="button"
class="btn btn-sm btn-outline-danger"
[disabled]="disabled || pendingSuggestions.length === 0"
(click)="rejectAll()"
i18n>
<i-bs name="x-circle" width="1em" height="1em" class="me-1"></i-bs>
Reject All
</button>
</div>
</div>
<div class="suggestions-container">
@for (type of suggestionTypes; track type) {
<div class="suggestion-group mb-3">
<div class="suggestion-group-header d-flex align-items-center mb-2">
<i-bs [name]="getTypeIcon(type)" width="1.1em" height="1.1em" class="me-2 text-primary"></i-bs>
<strong class="text-secondary">{{ getTypeLabel(type) }}</strong>
<span class="badge bg-secondary ms-2">{{ groupedSuggestions.get(type)?.length }}</span>
</div>
<div class="suggestion-items">
@for (suggestion of groupedSuggestions.get(type); track suggestion.id) {
<div
class="suggestion-item card mb-2"
[@fadeInOut]
[class.suggestion-applying]="suggestion.status === AISuggestionStatus.Applied"
[class.suggestion-rejecting]="suggestion.status === AISuggestionStatus.Rejected">
<div class="card-body p-2">
<div class="d-flex align-items-start justify-content-between gap-2">
<div class="flex-grow-1">
<div class="suggestion-value fw-medium mb-1">
@if (suggestion.type === AISuggestionType.CustomField && suggestion.field_name) {
<span class="text-muted small me-1">{{ suggestion.field_name }}:</span>
}
{{ getLabel(suggestion) }}
</div>
<div class="d-flex align-items-center gap-2 flex-wrap">
<span
class="confidence-badge badge"
[ngClass]="getConfidenceClass(suggestion.confidence)">
<i-bs [name]="getConfidenceIcon(suggestion.confidence)" width="0.8em" height="0.8em" class="me-1"></i-bs>
{{ getConfidenceLabel(suggestion.confidence) }}
</span>
@if (suggestion.created_at) {
<span class="text-muted small">
<i-bs name="clock" width="0.8em" height="0.8em" class="me-1"></i-bs>
{{ suggestion.created_at | date:'short' }}
</span>
}
</div>
</div>
<div class="suggestion-actions d-flex gap-1 flex-shrink-0">
<button
type="button"
class="btn btn-sm btn-success"
[disabled]="disabled"
(click)="applySuggestion(suggestion)"
i18n-title
title="Apply this suggestion">
<i-bs name="check-lg" width="1em" height="1em"></i-bs>
</button>
<button
type="button"
class="btn btn-sm btn-outline-danger"
[disabled]="disabled"
(click)="rejectSuggestion(suggestion)"
i18n-title
title="Reject this suggestion">
<i-bs name="x-lg" width="1em" height="1em"></i-bs>
</button>
</div>
</div>
</div>
</div>
}
</div>
</div>
}
</div>
@if (pendingSuggestions.length === 0) {
<div class="text-center text-muted py-3">
<i-bs name="check-circle" width="2em" height="2em" class="mb-2"></i-bs>
<p i18n>All suggestions have been processed</p>
</div>
}
</div>
</div>
}

View file

@ -0,0 +1,241 @@
.ai-suggestions-panel {
border: 2px solid var(--bs-primary);
border-radius: 0.5rem;
overflow: hidden;
transition: all 0.3s ease;
&:hover {
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15) !important;
}
.card-header {
cursor: pointer;
user-select: none;
transition: background-color 0.2s ease;
padding: 0.75rem 1rem;
&:hover {
background-color: var(--bs-primary) !important;
filter: brightness(1.1);
}
.badge {
font-size: 0.75rem;
padding: 0.25rem 0.5rem;
}
}
.card-body {
padding: 1rem;
}
}
.suggestions-container {
max-height: 600px;
overflow-y: auto;
overflow-x: hidden;
// Custom scrollbar styles
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
&::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
&:hover {
background: #555;
}
}
}
.suggestion-group {
.suggestion-group-header {
padding-bottom: 0.5rem;
border-bottom: 1px solid #dee2e6;
strong {
font-size: 0.95rem;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.badge {
font-size: 0.7rem;
}
}
.suggestion-items {
padding-left: 1.5rem;
}
}
.suggestion-item {
border-left: 3px solid var(--bs-primary);
transition: all 0.3s ease;
position: relative;
&:hover {
border-left-color: var(--bs-success);
box-shadow: 0 0.25rem 0.5rem rgba(0, 0, 0, 0.1);
transform: translateX(2px);
}
&.suggestion-applying {
animation: applyAnimation 0.5s ease;
border-left-color: var(--bs-success);
background-color: rgba(25, 135, 84, 0.1);
}
&.suggestion-rejecting {
animation: rejectAnimation 0.5s ease;
border-left-color: var(--bs-danger);
background-color: rgba(220, 53, 69, 0.1);
}
.suggestion-value {
color: #333;
font-size: 0.95rem;
word-break: break-word;
}
.confidence-badge {
font-size: 0.75rem;
font-weight: 500;
padding: 0.25rem 0.5rem;
border-radius: 0.25rem;
display: inline-flex;
align-items: center;
&.confidence-high {
background-color: #28a745;
color: white;
}
&.confidence-medium {
background-color: #ffc107;
color: #333;
}
&.confidence-low {
background-color: #dc3545;
color: white;
}
}
.suggestion-actions {
.btn {
min-width: 36px;
padding: 0.25rem 0.5rem;
transition: all 0.2s ease;
&:hover {
transform: scale(1.1);
}
&:active {
transform: scale(0.95);
}
}
}
}
// Animations
@keyframes applyAnimation {
0% {
opacity: 1;
transform: translateX(0);
}
50% {
opacity: 0.5;
transform: translateX(20px);
}
100% {
opacity: 0;
transform: translateX(40px);
}
}
@keyframes rejectAnimation {
0% {
opacity: 1;
transform: translateX(0) rotate(0deg);
}
50% {
opacity: 0.5;
transform: translateX(-20px) rotate(-5deg);
}
100% {
opacity: 0;
transform: translateX(-40px) rotate(-10deg);
}
}
// Responsive design
@media (max-width: 768px) {
.ai-suggestions-panel {
.card-header {
padding: 0.5rem 0.75rem;
flex-wrap: wrap;
.badge {
font-size: 0.65rem;
padding: 0.2rem 0.4rem;
}
}
.card-body {
padding: 0.75rem;
}
}
.suggestions-container {
max-height: 400px;
}
.suggestion-group {
.suggestion-items {
padding-left: 0.5rem;
}
}
.suggestion-item {
.d-flex {
flex-direction: column;
gap: 0.5rem !important;
}
.suggestion-actions {
width: 100%;
justify-content: flex-end;
}
}
}
@media (max-width: 576px) {
.ai-suggestions-panel {
.card-header {
.d-flex {
flex-direction: column;
align-items: flex-start !important;
gap: 0.5rem;
}
}
}
.suggestion-item {
.suggestion-value {
font-size: 0.875rem;
}
.confidence-badge {
font-size: 0.7rem;
}
}
}

View file

@ -0,0 +1,331 @@
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'
import { provideHttpClientTesting } from '@angular/common/http/testing'
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { provideAnimations } from '@angular/platform-browser/animations'
import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule, allIcons } from 'ngx-bootstrap-icons'
import { of } from 'rxjs'
import {
AISuggestion,
AISuggestionStatus,
AISuggestionType,
} from 'src/app/data/ai-suggestion'
import { Correspondent } from 'src/app/data/correspondent'
import { DocumentType } from 'src/app/data/document-type'
import { StoragePath } from 'src/app/data/storage-path'
import { Tag } from 'src/app/data/tag'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { TagService } from 'src/app/services/rest/tag.service'
import { ToastService } from 'src/app/services/toast.service'
import { AiSuggestionsPanelComponent } from './ai-suggestions-panel.component'
const mockTags: Tag[] = [
{ id: 1, name: 'Invoice', colour: '#ff0000', text_colour: '#ffffff' },
{ id: 2, name: 'Receipt', colour: '#00ff00', text_colour: '#000000' },
]
const mockCorrespondents: Correspondent[] = [
{ id: 1, name: 'Acme Corp' },
{ id: 2, name: 'TechStart LLC' },
]
const mockDocumentTypes: DocumentType[] = [
{ id: 1, name: 'Invoice' },
{ id: 2, name: 'Contract' },
]
const mockStoragePaths: StoragePath[] = [
{ id: 1, name: '/invoices', path: '/invoices' },
{ id: 2, name: '/contracts', path: '/contracts' },
]
const mockSuggestions: AISuggestion[] = [
{
id: '1',
type: AISuggestionType.Tag,
value: 1,
confidence: 0.85,
status: AISuggestionStatus.Pending,
},
{
id: '2',
type: AISuggestionType.Correspondent,
value: 1,
confidence: 0.75,
status: AISuggestionStatus.Pending,
},
{
id: '3',
type: AISuggestionType.DocumentType,
value: 1,
confidence: 0.90,
status: AISuggestionStatus.Pending,
},
]
describe('AiSuggestionsPanelComponent', () => {
let component: AiSuggestionsPanelComponent
let fixture: ComponentFixture<AiSuggestionsPanelComponent>
let tagService: TagService
let correspondentService: CorrespondentService
let documentTypeService: DocumentTypeService
let storagePathService: StoragePathService
let customFieldsService: CustomFieldsService
let toastService: ToastService
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [
AiSuggestionsPanelComponent,
NgbCollapseModule,
NgxBootstrapIconsModule.pick(allIcons),
],
providers: [
provideHttpClient(withInterceptorsFromDi()),
provideHttpClientTesting(),
provideAnimations(),
],
}).compileComponents()
tagService = TestBed.inject(TagService)
correspondentService = TestBed.inject(CorrespondentService)
documentTypeService = TestBed.inject(DocumentTypeService)
storagePathService = TestBed.inject(StoragePathService)
customFieldsService = TestBed.inject(CustomFieldsService)
toastService = TestBed.inject(ToastService)
jest.spyOn(tagService, 'listAll').mockReturnValue(
of({
all: mockTags.map((t) => t.id),
count: mockTags.length,
results: mockTags,
})
)
jest.spyOn(correspondentService, 'listAll').mockReturnValue(
of({
all: mockCorrespondents.map((c) => c.id),
count: mockCorrespondents.length,
results: mockCorrespondents,
})
)
jest.spyOn(documentTypeService, 'listAll').mockReturnValue(
of({
all: mockDocumentTypes.map((dt) => dt.id),
count: mockDocumentTypes.length,
results: mockDocumentTypes,
})
)
jest.spyOn(storagePathService, 'listAll').mockReturnValue(
of({
all: mockStoragePaths.map((sp) => sp.id),
count: mockStoragePaths.length,
results: mockStoragePaths,
})
)
jest.spyOn(customFieldsService, 'listAll').mockReturnValue(
of({
all: [],
count: 0,
results: [],
})
)
fixture = TestBed.createComponent(AiSuggestionsPanelComponent)
component = fixture.componentInstance
})
it('should create', () => {
expect(component).toBeTruthy()
})
it('should process suggestions on input change', () => {
component.suggestions = mockSuggestions
component.ngOnChanges({
suggestions: {
currentValue: mockSuggestions,
previousValue: [],
firstChange: true,
isFirstChange: () => true,
},
})
expect(component.pendingSuggestions.length).toBe(3)
expect(component.appliedCount).toBe(0)
expect(component.rejectedCount).toBe(0)
})
it('should group suggestions by type', () => {
component.suggestions = mockSuggestions
component.ngOnChanges({
suggestions: {
currentValue: mockSuggestions,
previousValue: [],
firstChange: true,
isFirstChange: () => true,
},
})
expect(component.groupedSuggestions.size).toBe(3)
expect(component.groupedSuggestions.get(AISuggestionType.Tag)?.length).toBe(
1
)
expect(
component.groupedSuggestions.get(AISuggestionType.Correspondent)?.length
).toBe(1)
expect(
component.groupedSuggestions.get(AISuggestionType.DocumentType)?.length
).toBe(1)
})
it('should apply a suggestion', () => {
component.suggestions = mockSuggestions
component.ngOnChanges({
suggestions: {
currentValue: mockSuggestions,
previousValue: [],
firstChange: true,
isFirstChange: () => true,
},
})
const toastSpy = jest.spyOn(toastService, 'showInfo')
const applySpy = jest.spyOn(component.apply, 'emit')
const suggestion = component.pendingSuggestions[0]
component.applySuggestion(suggestion)
expect(suggestion.status).toBe(AISuggestionStatus.Applied)
expect(applySpy).toHaveBeenCalledWith(suggestion)
expect(toastSpy).toHaveBeenCalled()
})
it('should reject a suggestion', () => {
component.suggestions = mockSuggestions
component.ngOnChanges({
suggestions: {
currentValue: mockSuggestions,
previousValue: [],
firstChange: true,
isFirstChange: () => true,
},
})
const toastSpy = jest.spyOn(toastService, 'showInfo')
const rejectSpy = jest.spyOn(component.reject, 'emit')
const suggestion = component.pendingSuggestions[0]
component.rejectSuggestion(suggestion)
expect(suggestion.status).toBe(AISuggestionStatus.Rejected)
expect(rejectSpy).toHaveBeenCalledWith(suggestion)
expect(toastSpy).toHaveBeenCalled()
})
it('should apply all suggestions', () => {
component.suggestions = mockSuggestions
component.ngOnChanges({
suggestions: {
currentValue: mockSuggestions,
previousValue: [],
firstChange: true,
isFirstChange: () => true,
},
})
const toastSpy = jest.spyOn(toastService, 'showInfo')
const applySpy = jest.spyOn(component.apply, 'emit')
component.applyAll()
expect(applySpy).toHaveBeenCalledTimes(3)
expect(toastSpy).toHaveBeenCalled()
})
it('should reject all suggestions', () => {
component.suggestions = mockSuggestions
component.ngOnChanges({
suggestions: {
currentValue: mockSuggestions,
previousValue: [],
firstChange: true,
isFirstChange: () => true,
},
})
const toastSpy = jest.spyOn(toastService, 'showInfo')
const rejectSpy = jest.spyOn(component.reject, 'emit')
component.rejectAll()
expect(rejectSpy).toHaveBeenCalledTimes(3)
expect(toastSpy).toHaveBeenCalled()
})
it('should return correct confidence class', () => {
expect(component.getConfidenceClass(0.9)).toBe('confidence-high')
expect(component.getConfidenceClass(0.7)).toBe('confidence-medium')
expect(component.getConfidenceClass(0.5)).toBe('confidence-low')
})
it('should return correct confidence label', () => {
expect(component.getConfidenceLabel(0.85)).toContain('85%')
expect(component.getConfidenceLabel(0.65)).toContain('65%')
expect(component.getConfidenceLabel(0.45)).toContain('45%')
})
it('should toggle collapse', () => {
expect(component.isCollapsed).toBe(false)
component.toggleCollapse()
expect(component.isCollapsed).toBe(true)
component.toggleCollapse()
expect(component.isCollapsed).toBe(false)
})
it('should respect disabled state', () => {
component.suggestions = mockSuggestions
component.disabled = true
component.ngOnChanges({
suggestions: {
currentValue: mockSuggestions,
previousValue: [],
firstChange: true,
isFirstChange: () => true,
},
})
const applySpy = jest.spyOn(component.apply, 'emit')
const suggestion = component.pendingSuggestions[0]
component.applySuggestion(suggestion)
expect(applySpy).not.toHaveBeenCalled()
})
it('should not render panel when there are no suggestions', () => {
component.suggestions = []
fixture.detectChanges()
expect(component.hasSuggestions).toBe(false)
})
it('should render panel when there are suggestions', () => {
component.suggestions = mockSuggestions
component.ngOnChanges({
suggestions: {
currentValue: mockSuggestions,
previousValue: [],
firstChange: true,
isFirstChange: () => true,
},
})
fixture.detectChanges()
expect(component.hasSuggestions).toBe(true)
})
})

View file

@ -0,0 +1,381 @@
import { CommonModule } from '@angular/common'
import {
trigger,
state,
style,
transition,
animate,
} from '@angular/animations'
import {
Component,
EventEmitter,
Input,
OnChanges,
Output,
SimpleChanges,
inject,
} from '@angular/core'
import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import {
AISuggestion,
AISuggestionStatus,
AISuggestionType,
} from 'src/app/data/ai-suggestion'
import { Correspondent } from 'src/app/data/correspondent'
import { CustomField } from 'src/app/data/custom-field'
import { DocumentType } from 'src/app/data/document-type'
import { StoragePath } from 'src/app/data/storage-path'
import { Tag } from 'src/app/data/tag'
import { CorrespondentService } from 'src/app/services/rest/correspondent.service'
import { CustomFieldsService } from 'src/app/services/rest/custom-fields.service'
import { DocumentTypeService } from 'src/app/services/rest/document-type.service'
import { StoragePathService } from 'src/app/services/rest/storage-path.service'
import { TagService } from 'src/app/services/rest/tag.service'
import { ToastService } from 'src/app/services/toast.service'
@Component({
selector: 'pngx-ai-suggestions-panel',
templateUrl: './ai-suggestions-panel.component.html',
styleUrls: ['./ai-suggestions-panel.component.scss'],
imports: [
CommonModule,
NgbCollapseModule,
NgxBootstrapIconsModule,
],
animations: [
trigger('slideIn', [
transition(':enter', [
style({ transform: 'translateY(-20px)', opacity: 0 }),
animate('300ms ease-out', style({ transform: 'translateY(0)', opacity: 1 })),
]),
]),
trigger('fadeInOut', [
transition(':enter', [
style({ opacity: 0, transform: 'scale(0.95)' }),
animate('200ms ease-out', style({ opacity: 1, transform: 'scale(1)' })),
]),
transition(':leave', [
animate('200ms ease-in', style({ opacity: 0, transform: 'scale(0.95)' })),
]),
]),
],
})
export class AiSuggestionsPanelComponent implements OnChanges {
private tagService = inject(TagService)
private correspondentService = inject(CorrespondentService)
private documentTypeService = inject(DocumentTypeService)
private storagePathService = inject(StoragePathService)
private customFieldsService = inject(CustomFieldsService)
private toastService = inject(ToastService)
@Input()
suggestions: AISuggestion[] = []
@Input()
disabled: boolean = false
@Output()
apply = new EventEmitter<AISuggestion>()
@Output()
reject = new EventEmitter<AISuggestion>()
public isCollapsed = false
public pendingSuggestions: AISuggestion[] = []
public groupedSuggestions: Map<AISuggestionType, AISuggestion[]> = new Map()
public appliedCount = 0
public rejectedCount = 0
private tags: Tag[] = []
private correspondents: Correspondent[] = []
private documentTypes: DocumentType[] = []
private storagePaths: StoragePath[] = []
private customFields: CustomField[] = []
public AISuggestionType = AISuggestionType
public AISuggestionStatus = AISuggestionStatus
ngOnChanges(changes: SimpleChanges): void {
if (changes['suggestions']) {
this.processSuggestions()
this.loadMetadata()
}
}
private processSuggestions(): void {
this.pendingSuggestions = this.suggestions.filter(
(s) => s.status === AISuggestionStatus.Pending
)
this.appliedCount = this.suggestions.filter(
(s) => s.status === AISuggestionStatus.Applied
).length
this.rejectedCount = this.suggestions.filter(
(s) => s.status === AISuggestionStatus.Rejected
).length
// Group suggestions by type
this.groupedSuggestions.clear()
this.pendingSuggestions.forEach((suggestion) => {
const group = this.groupedSuggestions.get(suggestion.type) || []
group.push(suggestion)
this.groupedSuggestions.set(suggestion.type, group)
})
}
private loadMetadata(): void {
// Load tags if needed
const tagSuggestions = this.pendingSuggestions.filter(
(s) => s.type === AISuggestionType.Tag
)
if (tagSuggestions.length > 0) {
this.tagService.listAll().subscribe((tags) => {
this.tags = tags.results
this.updateSuggestionLabels()
})
}
// Load correspondents if needed
const correspondentSuggestions = this.pendingSuggestions.filter(
(s) => s.type === AISuggestionType.Correspondent
)
if (correspondentSuggestions.length > 0) {
this.correspondentService.listAll().subscribe((correspondents) => {
this.correspondents = correspondents.results
this.updateSuggestionLabels()
})
}
// Load document types if needed
const documentTypeSuggestions = this.pendingSuggestions.filter(
(s) => s.type === AISuggestionType.DocumentType
)
if (documentTypeSuggestions.length > 0) {
this.documentTypeService.listAll().subscribe((documentTypes) => {
this.documentTypes = documentTypes.results
this.updateSuggestionLabels()
})
}
// Load storage paths if needed
const storagePathSuggestions = this.pendingSuggestions.filter(
(s) => s.type === AISuggestionType.StoragePath
)
if (storagePathSuggestions.length > 0) {
this.storagePathService.listAll().subscribe((storagePaths) => {
this.storagePaths = storagePaths.results
this.updateSuggestionLabels()
})
}
// Load custom fields if needed
const customFieldSuggestions = this.pendingSuggestions.filter(
(s) => s.type === AISuggestionType.CustomField
)
if (customFieldSuggestions.length > 0) {
this.customFieldsService.listAll().subscribe((customFields) => {
this.customFields = customFields.results
this.updateSuggestionLabels()
})
}
}
private updateSuggestionLabels(): void {
this.pendingSuggestions.forEach((suggestion) => {
if (!suggestion.label) {
suggestion.label = this.getLabel(suggestion)
}
})
}
public getLabel(suggestion: AISuggestion): string {
if (suggestion.label) {
return suggestion.label
}
switch (suggestion.type) {
case AISuggestionType.Tag:
const tag = this.tags.find((t) => t.id === suggestion.value)
return tag ? tag.name : `Tag #${suggestion.value}`
case AISuggestionType.Correspondent:
const correspondent = this.correspondents.find(
(c) => c.id === suggestion.value
)
return correspondent
? correspondent.name
: `Correspondent #${suggestion.value}`
case AISuggestionType.DocumentType:
const docType = this.documentTypes.find(
(dt) => dt.id === suggestion.value
)
return docType ? docType.name : `Document Type #${suggestion.value}`
case AISuggestionType.StoragePath:
const storagePath = this.storagePaths.find(
(sp) => sp.id === suggestion.value
)
return storagePath ? storagePath.name : `Storage Path #${suggestion.value}`
case AISuggestionType.CustomField:
return suggestion.field_name || 'Custom Field'
case AISuggestionType.Date:
return new Date(suggestion.value).toLocaleDateString()
case AISuggestionType.Title:
return suggestion.value
default:
return String(suggestion.value)
}
}
public getTypeLabel(type: AISuggestionType): string {
switch (type) {
case AISuggestionType.Tag:
return $localize`Tags`
case AISuggestionType.Correspondent:
return $localize`Correspondent`
case AISuggestionType.DocumentType:
return $localize`Document Type`
case AISuggestionType.StoragePath:
return $localize`Storage Path`
case AISuggestionType.CustomField:
return $localize`Custom Field`
case AISuggestionType.Date:
return $localize`Date`
case AISuggestionType.Title:
return $localize`Title`
default:
return String(type)
}
}
public getTypeIcon(type: AISuggestionType): string {
switch (type) {
case AISuggestionType.Tag:
return 'tag'
case AISuggestionType.Correspondent:
return 'person'
case AISuggestionType.DocumentType:
return 'file-earmark-text'
case AISuggestionType.StoragePath:
return 'folder'
case AISuggestionType.CustomField:
return 'input-cursor-text'
case AISuggestionType.Date:
return 'calendar'
case AISuggestionType.Title:
return 'pencil'
default:
return 'lightbulb'
}
}
public getConfidenceClass(confidence: number): string {
if (confidence >= 0.8) {
return 'confidence-high'
} else if (confidence >= 0.6) {
return 'confidence-medium'
} else {
return 'confidence-low'
}
}
public getConfidenceLabel(confidence: number): string {
const percentage = Math.round(confidence * 100)
if (confidence >= 0.8) {
return $localize`High (${percentage}%)`
} else if (confidence >= 0.6) {
return $localize`Medium (${percentage}%)`
} else {
return $localize`Low (${percentage}%)`
}
}
public getConfidenceIcon(confidence: number): string {
if (confidence >= 0.8) {
return 'check-circle-fill'
} else if (confidence >= 0.6) {
return 'exclamation-circle'
} else {
return 'question-circle'
}
}
public applySuggestion(suggestion: AISuggestion): void {
if (this.disabled) {
return
}
suggestion.status = AISuggestionStatus.Applied
this.apply.emit(suggestion)
this.processSuggestions()
this.toastService.showInfo(
$localize`Applied AI suggestion: ${this.getLabel(suggestion)}`
)
}
public rejectSuggestion(suggestion: AISuggestion): void {
if (this.disabled) {
return
}
suggestion.status = AISuggestionStatus.Rejected
this.reject.emit(suggestion)
this.processSuggestions()
this.toastService.showInfo(
$localize`Rejected AI suggestion: ${this.getLabel(suggestion)}`
)
}
public applyAll(): void {
if (this.disabled) {
return
}
const count = this.pendingSuggestions.length
this.pendingSuggestions.forEach((suggestion) => {
suggestion.status = AISuggestionStatus.Applied
this.apply.emit(suggestion)
})
this.processSuggestions()
this.toastService.showInfo(
$localize`Applied ${count} AI suggestions`
)
}
public rejectAll(): void {
if (this.disabled) {
return
}
const count = this.pendingSuggestions.length
this.pendingSuggestions.forEach((suggestion) => {
suggestion.status = AISuggestionStatus.Rejected
this.reject.emit(suggestion)
})
this.processSuggestions()
this.toastService.showInfo(
$localize`Rejected ${count} AI suggestions`
)
}
public toggleCollapse(): void {
this.isCollapsed = !this.isCollapsed
}
public get hasSuggestions(): boolean {
return this.pendingSuggestions.length > 0
}
public get suggestionTypes(): AISuggestionType[] {
return Array.from(this.groupedSuggestions.keys())
}
}

View file

@ -0,0 +1,77 @@
<li class="nav-item ai-status-container">
<button
class="btn border-0 position-relative"
[ngbPopover]="aiStatusPopover"
popoverClass="ai-status-popover"
placement="bottom"
container="body"
triggers="mouseenter:mouseleave">
<i-bs
width="1.3em"
height="1.3em"
[name]="iconName"
[class]="iconClass">
</i-bs>
@if (hasAlerts) {
<span class="badge rounded-pill bg-danger position-absolute top-0 end-0 translate-middle-y">
{{ aiStatus.pending_deletion_requests }}
</span>
}
</button>
</li>
<ng-template #aiStatusPopover>
<div class="ai-status-tooltip">
<h6 class="mb-2 border-bottom pb-2">
<i-bs name="robot" width="1em" height="1em" class="me-1"></i-bs>
<span i18n>AI Scanner Status</span>
</h6>
<div class="status-info">
<div class="status-row mb-2">
<span class="status-label" i18n>Status:</span>
<span [class]="aiStatus.active ? 'text-success' : 'text-muted'">
{{ aiStatus.active ? 'Active' : 'Inactive' }}
@if (aiStatus.processing) {
<span class="badge bg-primary ms-1" i18n>Processing</span>
}
</span>
</div>
@if (aiStatus.active) {
<div class="status-row mb-2">
<span class="status-label" i18n>Scanned Today:</span>
<span class="fw-bold">{{ aiStatus.documents_scanned_today }}</span>
</div>
<div class="status-row mb-2">
<span class="status-label" i18n>Suggestions Applied:</span>
<span class="fw-bold">{{ aiStatus.suggestions_applied }}</span>
</div>
@if (aiStatus.pending_deletion_requests > 0) {
<div class="status-row mb-2">
<span class="status-label text-danger" i18n>Pending Deletions:</span>
<span class="fw-bold text-danger">{{ aiStatus.pending_deletion_requests }}</span>
</div>
}
@if (aiStatus.last_scan) {
<div class="status-row mb-2 small text-muted">
<span class="status-label" i18n>Last Scan:</span>
<span>{{ aiStatus.last_scan | date: 'short' }}</span>
</div>
}
}
</div>
<div class="mt-3 pt-2 border-top">
<button
class="btn btn-sm btn-outline-primary w-100"
(click)="navigateToSettings()"
i18n>
Configure AI Scanner
</button>
</div>
</div>
</ng-template>

View file

@ -0,0 +1,55 @@
.ai-status-container {
display: flex;
align-items: center;
}
.ai-status-icon {
transition: all 0.3s ease;
&.inactive {
opacity: 0.4;
color: var(--bs-secondary);
}
&.active {
color: var(--bs-success);
}
&.processing {
color: var(--bs-primary);
animation: pulse 1.5s ease-in-out infinite;
}
}
@keyframes pulse {
0%, 100% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.7;
transform: scale(1.1);
}
}
.ai-status-tooltip {
min-width: 250px;
.status-row {
display: flex;
justify-content: space-between;
align-items: center;
.status-label {
font-weight: 500;
margin-right: 0.5rem;
}
}
}
// Badge positioning
.badge.rounded-pill {
font-size: 0.65rem;
padding: 0.15rem 0.35rem;
min-width: 1.2rem;
}

View file

@ -0,0 +1,104 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { Router } from '@angular/router'
import { of } from 'rxjs'
import { AIStatus } from 'src/app/data/ai-status'
import { AIStatusService } from 'src/app/services/ai-status.service'
import { AIStatusIndicatorComponent } from './ai-status-indicator.component'
describe('AIStatusIndicatorComponent', () => {
let component: AIStatusIndicatorComponent
let fixture: ComponentFixture<AIStatusIndicatorComponent>
let aiStatusService: jasmine.SpyObj<AIStatusService>
let router: jasmine.SpyObj<Router>
const mockAIStatus: AIStatus = {
active: true,
processing: false,
documents_scanned_today: 42,
suggestions_applied: 15,
pending_deletion_requests: 2,
last_scan: '2025-11-15T12:00:00Z',
version: '1.0.0',
}
beforeEach(async () => {
const aiStatusServiceSpy = jasmine.createSpyObj('AIStatusService', [
'getStatus',
'getCurrentStatus',
'refresh',
])
const routerSpy = jasmine.createSpyObj('Router', ['navigate'])
aiStatusServiceSpy.getStatus.and.returnValue(of(mockAIStatus))
aiStatusServiceSpy.getCurrentStatus.and.returnValue(mockAIStatus)
await TestBed.configureTestingModule({
imports: [AIStatusIndicatorComponent],
providers: [
{ provide: AIStatusService, useValue: aiStatusServiceSpy },
{ provide: Router, useValue: routerSpy },
],
}).compileComponents()
aiStatusService = TestBed.inject(
AIStatusService
) as jasmine.SpyObj<AIStatusService>
router = TestBed.inject(Router) as jasmine.SpyObj<Router>
fixture = TestBed.createComponent(AIStatusIndicatorComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should create', () => {
expect(component).toBeTruthy()
})
it('should subscribe to AI status on init', () => {
expect(aiStatusService.getStatus).toHaveBeenCalled()
expect(component.aiStatus).toEqual(mockAIStatus)
})
it('should show robot icon', () => {
expect(component.iconName).toBe('robot')
})
it('should have active class when AI is active', () => {
component.aiStatus = { ...mockAIStatus, active: true, processing: false }
expect(component.iconClass).toContain('active')
})
it('should have inactive class when AI is inactive', () => {
component.aiStatus = { ...mockAIStatus, active: false }
expect(component.iconClass).toContain('inactive')
})
it('should have processing class when AI is processing', () => {
component.aiStatus = { ...mockAIStatus, active: true, processing: true }
expect(component.iconClass).toContain('processing')
})
it('should show alerts when there are pending deletion requests', () => {
component.aiStatus = { ...mockAIStatus, pending_deletion_requests: 2 }
expect(component.hasAlerts).toBe(true)
})
it('should not show alerts when there are no pending deletion requests', () => {
component.aiStatus = { ...mockAIStatus, pending_deletion_requests: 0 }
expect(component.hasAlerts).toBe(false)
})
it('should navigate to settings when navigateToSettings is called', () => {
component.navigateToSettings()
expect(router.navigate).toHaveBeenCalledWith(['/settings'], {
fragment: 'ai-scanner',
})
})
it('should unsubscribe on destroy', () => {
const subscription = component['subscription']
spyOn(subscription, 'unsubscribe')
component.ngOnDestroy()
expect(subscription.unsubscribe).toHaveBeenCalled()
})
})

View file

@ -0,0 +1,94 @@
import { DatePipe } from '@angular/common'
import { Component, OnDestroy, OnInit, inject } from '@angular/core'
import { Router, RouterModule } from '@angular/router'
import {
NgbPopoverModule,
NgbTooltipModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subscription } from 'rxjs'
import { AIStatus } from 'src/app/data/ai-status'
import { AIStatusService } from 'src/app/services/ai-status.service'
@Component({
selector: 'pngx-ai-status-indicator',
templateUrl: './ai-status-indicator.component.html',
styleUrls: ['./ai-status-indicator.component.scss'],
imports: [
DatePipe,
NgbPopoverModule,
NgbTooltipModule,
NgxBootstrapIconsModule,
RouterModule,
],
})
export class AIStatusIndicatorComponent implements OnInit, OnDestroy {
private aiStatusService = inject(AIStatusService)
private router = inject(Router)
private subscription: Subscription
public aiStatus: AIStatus = {
active: false,
processing: false,
documents_scanned_today: 0,
suggestions_applied: 0,
pending_deletion_requests: 0,
}
ngOnInit(): void {
this.subscription = this.aiStatusService
.getStatus()
.subscribe((status) => {
this.aiStatus = status
})
}
ngOnDestroy(): void {
this.subscription?.unsubscribe()
}
/**
* Get the appropriate icon name based on AI status
*/
get iconName(): string {
if (!this.aiStatus.active) {
return 'robot' // Inactive
}
if (this.aiStatus.processing) {
return 'robot' // Processing (will add animation via CSS)
}
return 'robot' // Active
}
/**
* Get the CSS class for the icon based on status
*/
get iconClass(): string {
const classes = ['ai-status-icon']
if (!this.aiStatus.active) {
classes.push('inactive')
} else if (this.aiStatus.processing) {
classes.push('processing')
} else {
classes.push('active')
}
return classes.join(' ')
}
/**
* Navigate to AI configuration settings
*/
navigateToSettings(): void {
this.router.navigate(['/settings'], { fragment: 'ai-scanner' })
}
/**
* Check if there are any alerts (pending deletion requests)
*/
get hasAlerts(): boolean {
return this.aiStatus.pending_deletion_requests > 0
}
}

View file

@ -31,6 +31,7 @@
</div>
<ul ngbNav class="order-sm-3">
<pngx-toasts-dropdown></pngx-toasts-dropdown>
<pngx-ai-status-indicator></pngx-ai-status-indicator>
<li ngbDropdown class="nav-item dropdown">
<button class="btn ps-1 border-0" id="userDropdown" ngbDropdownToggle>
<i-bs width="1.3em" height="1.3em" name="person-circle"></i-bs>

View file

@ -47,6 +47,7 @@ import { environment } from 'src/environments/environment'
import { ProfileEditDialogComponent } from '../common/profile-edit-dialog/profile-edit-dialog.component'
import { DocumentDetailComponent } from '../document-detail/document-detail.component'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
import { AIStatusIndicatorComponent } from './ai-status-indicator/ai-status-indicator.component'
import { GlobalSearchComponent } from './global-search/global-search.component'
import { ToastsDropdownComponent } from './toasts-dropdown/toasts-dropdown.component'
@ -59,6 +60,7 @@ import { ToastsDropdownComponent } from './toasts-dropdown/toasts-dropdown.compo
DocumentTitlePipe,
IfPermissionsDirective,
ToastsDropdownComponent,
AIStatusIndicatorComponent,
RouterModule,
NgClass,
NgbDropdownModule,

View file

@ -0,0 +1,244 @@
<div class="modal-header">
<h4 class="modal-title" i18n>Deletion Request Details</h4>
<button
type="button"
class="btn-close"
aria-label="Close"
(click)="activeModal.dismiss()"
></button>
</div>
<div class="modal-body">
<!-- Request Information -->
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0" i18n>Request Information</h5>
</div>
<div class="card-body">
<div class="row mb-2">
<div class="col-md-3"><strong i18n>ID:</strong></div>
<div class="col-md-9">{{ deletionRequest.id }}</div>
</div>
<div class="row mb-2">
<div class="col-md-3"><strong i18n>Status:</strong></div>
<div class="col-md-9">
<span
class="badge"
[ngClass]="{
'bg-warning text-dark': deletionRequest.status === DeletionRequestStatus.Pending,
'bg-success': deletionRequest.status === DeletionRequestStatus.Approved,
'bg-danger': deletionRequest.status === DeletionRequestStatus.Rejected,
'bg-info': deletionRequest.status === DeletionRequestStatus.Completed,
'bg-secondary': deletionRequest.status === DeletionRequestStatus.Cancelled
}"
>
{{ deletionRequest.status }}
</span>
</div>
</div>
<div class="row mb-2">
<div class="col-md-3"><strong i18n>Created:</strong></div>
<div class="col-md-9">
{{ deletionRequest.created_at | customDate:'medium' }}
</div>
</div>
<div class="row mb-2">
<div class="col-md-3"><strong i18n>User:</strong></div>
<div class="col-md-9">{{ deletionRequest.user_username }}</div>
</div>
@if (deletionRequest.reviewed_by_username) {
<div class="row mb-2">
<div class="col-md-3"><strong i18n>Reviewed By:</strong></div>
<div class="col-md-9">{{ deletionRequest.reviewed_by_username }}</div>
</div>
}
@if (deletionRequest.reviewed_at) {
<div class="row mb-2">
<div class="col-md-3"><strong i18n>Reviewed At:</strong></div>
<div class="col-md-9">
{{ deletionRequest.reviewed_at | customDate:'medium' }}
</div>
</div>
}
</div>
</div>
<!-- AI Reason -->
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0" i18n>AI Reason</h5>
</div>
<div class="card-body">
<p>{{ deletionRequest.ai_reason }}</p>
</div>
</div>
<!-- Impact Analysis -->
@if (deletionRequest.impact_summary) {
<div class="card mb-3">
<div class="card-header bg-warning text-dark">
<h5 class="mb-0" i18n>
<i-bs name="exclamation-triangle"></i-bs> Impact Analysis
</h5>
</div>
<div class="card-body">
<div class="row mb-3">
<div class="col-md-4">
<div class="text-center p-3 bg-light rounded">
<h3 class="mb-0">{{ deletionRequest.impact_summary.document_count }}</h3>
<small class="text-muted" i18n>Documents</small>
</div>
</div>
@if (deletionRequest.impact_summary.affected_tags?.length > 0) {
<div class="col-md-4">
<div class="text-center p-3 bg-light rounded">
<h3 class="mb-0">{{ deletionRequest.impact_summary.affected_tags.length }}</h3>
<small class="text-muted" i18n>Affected Tags</small>
</div>
</div>
}
@if (deletionRequest.impact_summary.affected_correspondents?.length > 0) {
<div class="col-md-4">
<div class="text-center p-3 bg-light rounded">
<h3 class="mb-0">{{ deletionRequest.impact_summary.affected_correspondents.length }}</h3>
<small class="text-muted" i18n>Affected Correspondents</small>
</div>
</div>
}
</div>
@if (deletionRequest.impact_summary.affected_tags?.length > 0) {
<div class="mb-3">
<strong i18n>Affected Tags:</strong>
<div class="mt-1">
@for (tag of deletionRequest.impact_summary.affected_tags; track tag) {
<span class="badge bg-secondary me-1">{{ tag }}</span>
}
</div>
</div>
}
@if (deletionRequest.impact_summary.affected_correspondents?.length > 0) {
<div class="mb-3">
<strong i18n>Affected Correspondents:</strong>
<div class="mt-1">
@for (correspondent of deletionRequest.impact_summary.affected_correspondents; track correspondent) {
<span class="badge bg-info me-1">{{ correspondent }}</span>
}
</div>
</div>
}
@if (deletionRequest.impact_summary.affected_types?.length > 0) {
<div class="mb-3">
<strong i18n>Affected Document Types:</strong>
<div class="mt-1">
@for (type of deletionRequest.impact_summary.affected_types; track type) {
<span class="badge bg-primary me-1">{{ type }}</span>
}
</div>
</div>
}
</div>
</div>
}
<!-- Documents List -->
@if (deletionRequest.documents_detail && deletionRequest.documents_detail.length > 0) {
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0" i18n>Documents to be Deleted</h5>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-sm table-hover">
<thead>
<tr>
<th scope="col" i18n>ID</th>
<th scope="col" i18n>Title</th>
<th scope="col" i18n>Correspondent</th>
<th scope="col" i18n>Type</th>
<th scope="col" i18n>Tags</th>
</tr>
</thead>
<tbody>
@for (doc of deletionRequest.documents_detail; track doc.id) {
<tr>
<td>{{ doc.id }}</td>
<td>{{ doc.title }}</td>
<td>{{ doc.correspondent || '-' }}</td>
<td>{{ doc.document_type || '-' }}</td>
<td>
@for (tag of doc.tags; track tag) {
<span class="badge bg-secondary me-1">{{ tag }}</span>
}
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
</div>
}
<!-- Review Comment (if exists or editable) -->
@if (deletionRequest.review_comment || canModify()) {
<div class="card mb-3">
<div class="card-header">
<h5 class="mb-0" i18n>Review Comment</h5>
</div>
<div class="card-body">
@if (deletionRequest.review_comment && !canModify()) {
<p>{{ deletionRequest.review_comment }}</p>
} @else if (canModify()) {
<textarea
class="form-control"
rows="3"
[(ngModel)]="reviewComment"
placeholder="Add a comment (optional)"
i18n-placeholder
></textarea>
}
</div>
</div>
}
</div>
<div class="modal-footer">
@if (canModify()) {
<button
type="button"
class="btn btn-danger"
(click)="reject()"
[disabled]="isProcessing"
>
@if (isProcessing) {
<span class="spinner-border spinner-border-sm me-1"></span>
}
<i-bs name="x-circle"></i-bs>
<ng-container i18n>Reject</ng-container>
</button>
<button
type="button"
class="btn btn-success"
(click)="approve()"
[disabled]="isProcessing"
>
@if (isProcessing) {
<span class="spinner-border spinner-border-sm me-1"></span>
}
<i-bs name="check-circle"></i-bs>
<ng-container i18n>Approve</ng-container>
</button>
}
<button
type="button"
class="btn btn-secondary"
(click)="activeModal.dismiss()"
[disabled]="isProcessing"
i18n
>
Close
</button>
</div>

View file

@ -0,0 +1,46 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { DeletionRequestDetailComponent } from './deletion-request-detail.component'
import { DeletionRequestService } from 'src/app/services/rest/deletion-request.service'
import { ToastService } from 'src/app/services/toast.service'
describe('DeletionRequestDetailComponent', () => {
let component: DeletionRequestDetailComponent
let fixture: ComponentFixture<DeletionRequestDetailComponent>
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DeletionRequestDetailComponent, HttpClientTestingModule],
providers: [NgbActiveModal, DeletionRequestService, ToastService],
}).compileComponents()
fixture = TestBed.createComponent(DeletionRequestDetailComponent)
component = fixture.componentInstance
component.deletionRequest = {
id: 1,
created_at: '2024-01-01',
updated_at: '2024-01-01',
requested_by_ai: true,
ai_reason: 'Test reason',
user: 1,
user_username: 'testuser',
status: 'pending' as any,
documents: [1, 2],
documents_detail: [],
document_count: 2,
impact_summary: {
document_count: 2,
documents: [],
affected_tags: [],
affected_correspondents: [],
affected_types: [],
},
}
fixture.detectChanges()
})
it('should create', () => {
expect(component).toBeTruthy()
})
})

View file

@ -0,0 +1,88 @@
import { CommonModule } from '@angular/common'
import { Component, inject, Input } from '@angular/core'
import { FormsModule } from '@angular/forms'
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import {
DeletionRequest,
DeletionRequestStatus,
} from 'src/app/data/deletion-request'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { DeletionRequestService } from 'src/app/services/rest/deletion-request.service'
import { ToastService } from 'src/app/services/toast.service'
@Component({
selector: 'pngx-deletion-request-detail',
standalone: true,
imports: [
CommonModule,
FormsModule,
NgxBootstrapIconsModule,
CustomDatePipe,
],
templateUrl: './deletion-request-detail.component.html',
styleUrls: ['./deletion-request-detail.component.scss'],
})
export class DeletionRequestDetailComponent {
@Input() deletionRequest: DeletionRequest
public DeletionRequestStatus = DeletionRequestStatus
public activeModal = inject(NgbActiveModal)
private deletionRequestService = inject(DeletionRequestService)
private toastService = inject(ToastService)
public reviewComment: string = ''
public isProcessing: boolean = false
approve(): void {
if (this.isProcessing) return
this.isProcessing = true
this.deletionRequestService
.approve(this.deletionRequest.id, this.reviewComment)
.subscribe({
next: (result) => {
this.toastService.showInfo(
$localize`Deletion request approved successfully`
)
this.isProcessing = false
this.activeModal.close('approved')
},
error: (error) => {
this.toastService.showError(
$localize`Error approving deletion request`,
error
)
this.isProcessing = false
},
})
}
reject(): void {
if (this.isProcessing) return
this.isProcessing = true
this.deletionRequestService
.reject(this.deletionRequest.id, this.reviewComment)
.subscribe({
next: (result) => {
this.toastService.showInfo(
$localize`Deletion request rejected successfully`
)
this.isProcessing = false
this.activeModal.close('rejected')
},
error: (error) => {
this.toastService.showError(
$localize`Error rejecting deletion request`,
error
)
this.isProcessing = false
},
})
}
canModify(): boolean {
return this.deletionRequest.status === DeletionRequestStatus.Pending
}
}

View file

@ -0,0 +1,114 @@
<pngx-page-header
title="Deletion Requests"
i18n-title
info="Manage AI-initiated deletion requests. Review and approve or reject document deletions recommended by the AI system."
i18n-info
>
@if (getPendingCount() > 0) {
<div class="badge bg-warning text-dark ms-2">
{{ getPendingCount() }} <ng-container i18n>pending</ng-container>
</div>
}
</pngx-page-header>
<ul ngbNav #nav="ngbNav" [(activeId)]="activeTab" class="nav-tabs mb-3">
<li [ngbNavItem]="DeletionRequestStatus.Pending">
<button ngbNavLink (click)="onTabChange(DeletionRequestStatus.Pending)">
<ng-container i18n>Pending</ng-container>
@if (getStatusCount(DeletionRequestStatus.Pending) > 0) {
<span class="badge bg-warning text-dark ms-1">
{{ getStatusCount(DeletionRequestStatus.Pending) }}
</span>
}
</button>
</li>
<li [ngbNavItem]="DeletionRequestStatus.Approved">
<button ngbNavLink (click)="onTabChange(DeletionRequestStatus.Approved)">
<ng-container i18n>Approved</ng-container>
</button>
</li>
<li [ngbNavItem]="DeletionRequestStatus.Rejected">
<button ngbNavLink (click)="onTabChange(DeletionRequestStatus.Rejected)">
<ng-container i18n>Rejected</ng-container>
</button>
</li>
<li [ngbNavItem]="DeletionRequestStatus.Completed">
<button ngbNavLink (click)="onTabChange(DeletionRequestStatus.Completed)">
<ng-container i18n>Completed</ng-container>
</button>
</li>
</ul>
<div [ngbNavOutlet]="nav"></div>
@if (deletionRequestService.loading) {
<div class="text-center my-5">
<div class="spinner-border" role="status">
<span class="visually-hidden" i18n>Loading...</span>
</div>
</div>
} @else if (filteredRequests.length === 0) {
<div class="alert alert-info" i18n>
No deletion requests found with status: {{ activeTab }}
</div>
} @else {
<div class="table-responsive">
<table class="table table-hover table-striped align-middle">
<thead>
<tr>
<th scope="col" i18n>ID</th>
<th scope="col" i18n>Created</th>
<th scope="col" i18n>Documents</th>
<th scope="col" i18n>AI Reason</th>
<th scope="col" i18n>Status</th>
<th scope="col" i18n>Actions</th>
</tr>
</thead>
<tbody>
@for (request of filteredRequests | slice: (page-1) * pageSize : page * pageSize; track request.id) {
<tr (click)="viewDetails(request)" style="cursor: pointer;">
<td>{{ request.id }}</td>
<td>{{ request.created_at | customDate:'short' }}</td>
<td>
<span class="badge bg-primary">
{{ request.document_count }} <ng-container i18n>docs</ng-container>
</span>
</td>
<td>
<div class="text-truncate" style="max-width: 300px;" [ngbTooltip]="request.ai_reason">
{{ request.ai_reason }}
</div>
</td>
<td>
<span class="badge" [ngClass]="getStatusBadgeClass(request.status)">
{{ request.status }}
</span>
</td>
<td>
<button
class="btn btn-sm btn-outline-primary"
(click)="viewDetails(request); $event.stopPropagation()"
i18n
>
<i-bs name="eye"></i-bs> View Details
</button>
</td>
</tr>
}
</tbody>
</table>
</div>
@if (collectionSize > pageSize) {
<div class="d-flex justify-content-center mt-3">
<ngb-pagination
[(page)]="page"
[pageSize]="pageSize"
[collectionSize]="collectionSize"
[maxSize]="5"
[rotate]="true"
[boundaryLinks]="true"
></ngb-pagination>
</div>
}
}

View file

@ -0,0 +1,6 @@
// Component-specific styles for deletion requests
.text-truncate {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}

View file

@ -0,0 +1,25 @@
import { ComponentFixture, TestBed } from '@angular/core/testing'
import { HttpClientTestingModule } from '@angular/common/http/testing'
import { DeletionRequestsComponent } from './deletion-requests.component'
import { DeletionRequestService } from 'src/app/services/rest/deletion-request.service'
import { ToastService } from 'src/app/services/toast.service'
describe('DeletionRequestsComponent', () => {
let component: DeletionRequestsComponent
let fixture: ComponentFixture<DeletionRequestsComponent>
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DeletionRequestsComponent, HttpClientTestingModule],
providers: [DeletionRequestService, ToastService],
}).compileComponents()
fixture = TestBed.createComponent(DeletionRequestsComponent)
component = fixture.componentInstance
fixture.detectChanges()
})
it('should create', () => {
expect(component).toBeTruthy()
})
})

View file

@ -0,0 +1,141 @@
import { CommonModule } from '@angular/common'
import { Component, OnInit, OnDestroy, inject } from '@angular/core'
import { FormsModule } from '@angular/forms'
import {
NgbModal,
NgbNavModule,
NgbPaginationModule,
NgbTooltipModule,
} from '@ng-bootstrap/ng-bootstrap'
import { NgxBootstrapIconsModule } from 'ngx-bootstrap-icons'
import { Subject, takeUntil } from 'rxjs'
import {
DeletionRequest,
DeletionRequestStatus,
} from 'src/app/data/deletion-request'
import { CustomDatePipe } from 'src/app/pipes/custom-date.pipe'
import { DeletionRequestService } from 'src/app/services/rest/deletion-request.service'
import { ToastService } from 'src/app/services/toast.service'
import { PageHeaderComponent } from '../common/page-header/page-header.component'
import { LoadingComponentWithPermissions } from '../loading-component/loading.component'
import { DeletionRequestDetailComponent } from './deletion-request-detail/deletion-request-detail.component'
@Component({
selector: 'pngx-deletion-requests',
standalone: true,
imports: [
CommonModule,
FormsModule,
NgbNavModule,
NgbPaginationModule,
NgbTooltipModule,
NgxBootstrapIconsModule,
PageHeaderComponent,
CustomDatePipe,
],
templateUrl: './deletion-requests.component.html',
styleUrls: ['./deletion-requests.component.scss'],
})
export class DeletionRequestsComponent
extends LoadingComponentWithPermissions
implements OnInit, OnDestroy
{
public DeletionRequestStatus = DeletionRequestStatus
public deletionRequestService = inject(DeletionRequestService)
private modalService = inject(NgbModal)
private toastService = inject(ToastService)
protected unsubscribeNotifier: Subject<void> = new Subject()
public deletionRequests: DeletionRequest[] = []
public filteredRequests: DeletionRequest[] = []
public activeTab: DeletionRequestStatus = DeletionRequestStatus.Pending
public page: number = 1
public pageSize: number = 25
public collectionSize: number = 0
ngOnInit(): void {
this.loadDeletionRequests()
}
ngOnDestroy(): void {
this.unsubscribeNotifier.next()
this.unsubscribeNotifier.complete()
}
loadDeletionRequests(): void {
this.deletionRequestService
.listAll()
.pipe(takeUntil(this.unsubscribeNotifier))
.subscribe({
next: (result) => {
this.deletionRequests = result.results
this.filterByStatus()
},
error: (error) => {
this.toastService.showError(
$localize`Error loading deletion requests`,
error
)
},
})
}
filterByStatus(): void {
this.filteredRequests = this.deletionRequests.filter(
(req) => req.status === this.activeTab
)
this.collectionSize = this.filteredRequests.length
this.page = 1
}
onTabChange(status: DeletionRequestStatus): void {
this.activeTab = status
this.filterByStatus()
}
viewDetails(request: DeletionRequest): void {
const modalRef = this.modalService.open(DeletionRequestDetailComponent, {
size: 'xl',
backdrop: 'static',
})
modalRef.componentInstance.deletionRequest = request
modalRef.result.then(
(result) => {
if (result === 'approved' || result === 'rejected') {
this.loadDeletionRequests()
}
},
() => {
// Modal dismissed
}
)
}
getStatusBadgeClass(status: DeletionRequestStatus): string {
switch (status) {
case DeletionRequestStatus.Pending:
return 'bg-warning text-dark'
case DeletionRequestStatus.Approved:
return 'bg-success'
case DeletionRequestStatus.Rejected:
return 'bg-danger'
case DeletionRequestStatus.Completed:
return 'bg-info'
case DeletionRequestStatus.Cancelled:
return 'bg-secondary'
default:
return 'bg-secondary'
}
}
getPendingCount(): number {
return this.deletionRequests.filter(
(req) => req.status === DeletionRequestStatus.Pending
).length
}
getStatusCount(status: DeletionRequestStatus): number {
return this.deletionRequests.filter((req) => req.status === status).length
}
}

View file

@ -118,6 +118,13 @@
<ng-container *ngTemplateOutlet="saveButtons"></ng-container>
</div>
<pngx-ai-suggestions-panel
[suggestions]="aiSuggestions"
[disabled]="!userCanEdit"
(apply)="onApplySuggestion($event)"
(reject)="onRejectSuggestion($event)">
</pngx-ai-suggestions-panel>
<ul ngbNav #nav="ngbNav" class="nav-underline flex-nowrap flex-md-wrap overflow-auto" (navChange)="onNavChange($event)" [(activeId)]="activeNavID">
<li [ngbNavItem]="DocumentDetailNavIDs.Details">
<a ngbNavLink i18n>Details</a>

View file

@ -32,6 +32,11 @@ import {
switchMap,
takeUntil,
} from 'rxjs/operators'
import {
AISuggestion,
AISuggestionStatus,
AISuggestionType,
} from 'src/app/data/ai-suggestion'
import { Correspondent } from 'src/app/data/correspondent'
import { CustomField, CustomFieldDataType } from 'src/app/data/custom-field'
import { CustomFieldInstance } from 'src/app/data/custom-field-instance'
@ -109,6 +114,7 @@ import { ShareLinksDialogComponent } from '../common/share-links-dialog/share-li
import { DocumentHistoryComponent } from '../document-history/document-history.component'
import { DocumentNotesComponent } from '../document-notes/document-notes.component'
import { ComponentWithPermissions } from '../with-permissions/with-permissions.component'
import { AiSuggestionsPanelComponent } from '../ai-suggestions-panel/ai-suggestions-panel.component'
import { MetadataCollapseComponent } from './metadata-collapse/metadata-collapse.component'
enum DocumentDetailNavIDs {
@ -151,6 +157,7 @@ export enum ZoomSetting {
CustomFieldsDropdownComponent,
DocumentNotesComponent,
DocumentHistoryComponent,
AiSuggestionsPanelComponent,
CheckComponent,
DateComponent,
DocumentLinkComponent,
@ -216,6 +223,7 @@ export class DocumentDetailComponent
document: Document
metadata: DocumentMetadata
suggestions: DocumentSuggestions
aiSuggestions: AISuggestion[] = []
users: User[]
title: string
@ -437,6 +445,7 @@ export class DocumentDetailComponent
}
this.documentId = doc.id
this.suggestions = null
this.aiSuggestions = []
const openDocument = this.openDocumentService.getOpenDocument(
this.documentId
)
@ -691,9 +700,11 @@ export class DocumentDetailComponent
.subscribe({
next: (result) => {
this.suggestions = result
this.aiSuggestions = this.convertSuggestionsToAI(result)
},
error: (error) => {
this.suggestions = null
this.aiSuggestions = []
this.toastService.showError(
$localize`Error retrieving suggestions.`,
error
@ -1542,4 +1553,124 @@ export class DocumentDetailComponent
},
})
}
private convertSuggestionsToAI(suggestions: DocumentSuggestions): AISuggestion[] {
if (!suggestions) {
return []
}
const aiSuggestions: AISuggestion[] = []
let id = 1
// Convert tag suggestions
if (suggestions.tags && suggestions.tags.length > 0) {
suggestions.tags.forEach((tagId) => {
aiSuggestions.push({
id: `tag-${id++}`,
type: AISuggestionType.Tag,
value: tagId,
confidence: 0.75, // Default confidence for legacy suggestions
status: AISuggestionStatus.Pending,
})
})
}
// Convert correspondent suggestions
if (suggestions.correspondents && suggestions.correspondents.length > 0) {
suggestions.correspondents.forEach((corrId) => {
aiSuggestions.push({
id: `correspondent-${id++}`,
type: AISuggestionType.Correspondent,
value: corrId,
confidence: 0.75,
status: AISuggestionStatus.Pending,
})
})
}
// Convert document type suggestions
if (suggestions.document_types && suggestions.document_types.length > 0) {
suggestions.document_types.forEach((docTypeId) => {
aiSuggestions.push({
id: `doctype-${id++}`,
type: AISuggestionType.DocumentType,
value: docTypeId,
confidence: 0.75,
status: AISuggestionStatus.Pending,
})
})
}
// Convert storage path suggestions
if (suggestions.storage_paths && suggestions.storage_paths.length > 0) {
suggestions.storage_paths.forEach((storagePathId) => {
aiSuggestions.push({
id: `storage-${id++}`,
type: AISuggestionType.StoragePath,
value: storagePathId,
confidence: 0.75,
status: AISuggestionStatus.Pending,
})
})
}
// Convert date suggestions
if (suggestions.dates && suggestions.dates.length > 0) {
suggestions.dates.forEach((date) => {
aiSuggestions.push({
id: `date-${id++}`,
type: AISuggestionType.Date,
value: date,
confidence: 0.75,
status: AISuggestionStatus.Pending,
})
})
}
return aiSuggestions
}
onApplySuggestion(suggestion: AISuggestion): void {
switch (suggestion.type) {
case AISuggestionType.Tag:
const currentTags = this.documentForm.get('tags').value || []
if (!currentTags.includes(suggestion.value)) {
this.documentForm.get('tags').setValue([...currentTags, suggestion.value])
this.documentForm.get('tags').markAsDirty()
}
break
case AISuggestionType.Correspondent:
this.documentForm.get('correspondent').setValue(suggestion.value)
this.documentForm.get('correspondent').markAsDirty()
break
case AISuggestionType.DocumentType:
this.documentForm.get('document_type').setValue(suggestion.value)
this.documentForm.get('document_type').markAsDirty()
break
case AISuggestionType.StoragePath:
this.documentForm.get('storage_path').setValue(suggestion.value)
this.documentForm.get('storage_path').markAsDirty()
break
case AISuggestionType.Date:
const dateAdapter = new ISODateAdapter()
const dateValue = dateAdapter.fromModel(suggestion.value)
this.documentForm.get('created').setValue(dateValue)
this.documentForm.get('created').markAsDirty()
break
case AISuggestionType.Title:
this.documentForm.get('title').setValue(suggestion.value)
this.documentForm.get('title').markAsDirty()
break
}
}
onRejectSuggestion(suggestion: AISuggestion): void {
// Just remove it from the list (handled by the panel component)
// No additional action needed here
}
}

View file

@ -0,0 +1,63 @@
/**
* Represents the AI scanner status and statistics
*/
export interface AIStatus {
/**
* Whether the AI scanner is currently active/enabled
*/
active: boolean
/**
* Whether the AI scanner is currently processing documents
*/
processing: boolean
/**
* Number of documents scanned today
*/
documents_scanned_today: number
/**
* Number of AI suggestions applied
*/
suggestions_applied: number
/**
* Number of pending deletion requests awaiting user approval
*/
pending_deletion_requests: number
/**
* Last scan timestamp (ISO format)
*/
last_scan?: string
/**
* AI scanner version or configuration info
*/
version?: string
}
/**
* Represents a pending deletion request initiated by AI
*/
export interface DeletionRequest {
id: number
document_id: number
document_title: string
reason: string
confidence: number
created_at: string
status: DeletionRequestStatus
}
/**
* Status of a deletion request
*/
export enum DeletionRequestStatus {
Pending = 'pending',
Approved = 'approved',
Rejected = 'rejected',
Cancelled = 'cancelled',
Completed = 'completed',
}

View file

@ -0,0 +1,32 @@
export enum AISuggestionType {
Tag = 'tag',
Correspondent = 'correspondent',
DocumentType = 'document_type',
StoragePath = 'storage_path',
CustomField = 'custom_field',
Date = 'date',
Title = 'title',
}
export enum AISuggestionStatus {
Pending = 'pending',
Applied = 'applied',
Rejected = 'rejected',
}
export interface AISuggestion {
id: string
type: AISuggestionType
value: any
confidence: number
status: AISuggestionStatus
label?: string
field_name?: string // For custom fields
created_at?: Date
}
export interface AIDocumentSuggestions {
document_id: number
suggestions: AISuggestion[]
generated_at: Date
}

View file

@ -0,0 +1,50 @@
import { ObjectWithId } from './object-with-id'
export interface DeletionRequestDocument {
id: number
title: string
created: string
correspondent?: string
document_type?: string
tags: string[]
}
export interface DeletionRequestImpactSummary {
document_count: number
documents: DeletionRequestDocument[]
affected_tags: string[]
affected_correspondents: string[]
affected_types: string[]
date_range?: {
earliest: string
latest: string
}
}
export enum DeletionRequestStatus {
Pending = 'pending',
Approved = 'approved',
Rejected = 'rejected',
Cancelled = 'cancelled',
Completed = 'completed',
}
export interface DeletionRequest extends ObjectWithId {
created_at: string
updated_at: string
requested_by_ai: boolean
ai_reason: string
user: number
user_username: string
status: DeletionRequestStatus
documents: number[]
documents_detail: DeletionRequestDocument[]
document_count: number
impact_summary: DeletionRequestImpactSummary
reviewed_at?: string
reviewed_by?: number
reviewed_by_username?: string
review_comment?: string
completed_at?: string
completion_details?: any
}

View file

@ -0,0 +1,87 @@
import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing'
import { TestBed } from '@angular/core/testing'
import { AIStatus } from 'src/app/data/ai-status'
import { environment } from 'src/environments/environment'
import { AIStatusService } from './ai-status.service'
describe('AIStatusService', () => {
let service: AIStatusService
let httpMock: HttpTestingController
const mockAIStatus: AIStatus = {
active: true,
processing: false,
documents_scanned_today: 42,
suggestions_applied: 15,
pending_deletion_requests: 2,
last_scan: '2025-11-15T12:00:00Z',
version: '1.0.0',
}
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [AIStatusService],
})
service = TestBed.inject(AIStatusService)
httpMock = TestBed.inject(HttpTestingController)
})
afterEach(() => {
httpMock.verify()
})
it('should be created', () => {
expect(service).toBeTruthy()
})
it('should return AI status as observable', (done) => {
service.getStatus().subscribe((status) => {
expect(status).toBeDefined()
expect(status.active).toBeDefined()
expect(status.processing).toBeDefined()
done()
})
})
it('should return current status value', () => {
const status = service.getCurrentStatus()
expect(status).toBeDefined()
expect(status.active).toBeDefined()
})
it('should fetch AI status from backend', (done) => {
service['fetchAIStatus']().subscribe((status) => {
expect(status).toEqual(mockAIStatus)
expect(service.loading).toBe(false)
done()
})
const req = httpMock.expectOne(`${environment.apiBaseUrl}ai/status/`)
expect(req.request.method).toBe('GET')
req.flush(mockAIStatus)
})
it('should handle error and return mock data', (done) => {
service['fetchAIStatus']().subscribe((status) => {
expect(status).toBeDefined()
expect(status.active).toBeDefined()
expect(service.loading).toBe(false)
done()
})
const req = httpMock.expectOne(`${environment.apiBaseUrl}ai/status/`)
req.error(new ProgressEvent('error'))
})
it('should manually refresh status', () => {
service.refresh()
const req = httpMock.expectOne(`${environment.apiBaseUrl}ai/status/`)
expect(req.request.method).toBe('GET')
req.flush(mockAIStatus)
})
})

View file

@ -0,0 +1,100 @@
import { HttpClient } from '@angular/common/http'
import { Injectable, inject } from '@angular/core'
import { BehaviorSubject, Observable, interval } from 'rxjs'
import { catchError, map, startWith, switchMap } from 'rxjs/operators'
import { AIStatus } from 'src/app/data/ai-status'
import { environment } from 'src/environments/environment'
@Injectable({
providedIn: 'root',
})
export class AIStatusService {
private http = inject(HttpClient)
private baseUrl: string = environment.apiBaseUrl
private aiStatusSubject = new BehaviorSubject<AIStatus>({
active: false,
processing: false,
documents_scanned_today: 0,
suggestions_applied: 0,
pending_deletion_requests: 0,
})
public loading: boolean = false
// Poll every 30 seconds for AI status updates
private readonly POLL_INTERVAL = 30000
constructor() {
this.startPolling()
}
/**
* Get the current AI status as an observable
*/
public getStatus(): Observable<AIStatus> {
return this.aiStatusSubject.asObservable()
}
/**
* Get the current AI status value
*/
public getCurrentStatus(): AIStatus {
return this.aiStatusSubject.value
}
/**
* Start polling for AI status updates
*/
private startPolling(): void {
interval(this.POLL_INTERVAL)
.pipe(
startWith(0), // Emit immediately on subscription
switchMap(() => this.fetchAIStatus())
)
.subscribe((status) => {
this.aiStatusSubject.next(status)
})
}
/**
* Fetch AI status from the backend
*/
private fetchAIStatus(): Observable<AIStatus> {
this.loading = true
return this.http
.get<AIStatus>(`${this.baseUrl}ai/status/`)
.pipe(
map((status) => {
this.loading = false
return status
}),
catchError((error) => {
this.loading = false
console.warn('Failed to fetch AI status, using mock data:', error)
// Return mock data if endpoint doesn't exist yet
return [
{
active: true,
processing: false,
documents_scanned_today: 42,
suggestions_applied: 15,
pending_deletion_requests: 2,
last_scan: new Date().toISOString(),
version: '1.0.0',
},
]
})
)
}
/**
* Manually refresh the AI status
*/
public refresh(): void {
this.fetchAIStatus().subscribe((status) => {
this.aiStatusSubject.next(status)
})
}
}

View file

@ -0,0 +1,79 @@
import { TestBed } from '@angular/core/testing'
import {
HttpClientTestingModule,
HttpTestingController,
} from '@angular/common/http/testing'
import { DeletionRequestService } from './deletion-request.service'
import { environment } from 'src/environments/environment'
describe('DeletionRequestService', () => {
let service: DeletionRequestService
let httpMock: HttpTestingController
beforeEach(() => {
TestBed.configureTestingModule({
imports: [HttpClientTestingModule],
providers: [DeletionRequestService],
})
service = TestBed.inject(DeletionRequestService)
httpMock = TestBed.inject(HttpTestingController)
})
afterEach(() => {
httpMock.verify()
})
it('should be created', () => {
expect(service).toBeTruthy()
})
it('should get pending count', () => {
const mockResponse = { count: 5 }
service.getPendingCount().subscribe((response) => {
expect(response.count).toBe(5)
})
const req = httpMock.expectOne(
`${environment.apiBaseUrl}deletion_requests/pending_count/`
)
expect(req.request.method).toBe('GET')
req.flush(mockResponse)
})
it('should approve a deletion request', () => {
const mockRequest = {
id: 1,
status: 'approved',
}
service.approve(1, 'Approved').subscribe((response) => {
expect(response.status).toBe('approved')
})
const req = httpMock.expectOne(
`${environment.apiBaseUrl}deletion_requests/1/approve/`
)
expect(req.request.method).toBe('POST')
expect(req.request.body).toEqual({ review_comment: 'Approved' })
req.flush(mockRequest)
})
it('should reject a deletion request', () => {
const mockRequest = {
id: 1,
status: 'rejected',
}
service.reject(1, 'Rejected').subscribe((response) => {
expect(response.status).toBe('rejected')
})
const req = httpMock.expectOne(
`${environment.apiBaseUrl}deletion_requests/1/reject/`
)
expect(req.request.method).toBe('POST')
expect(req.request.body).toEqual({ review_comment: 'Rejected' })
req.flush(mockRequest)
})
})

View file

@ -0,0 +1,62 @@
import { HttpClient } from '@angular/common/http'
import { Injectable } from '@angular/core'
import { Observable } from 'rxjs'
import { tap } from 'rxjs/operators'
import { DeletionRequest } from 'src/app/data/deletion-request'
import { AbstractPaperlessService } from './abstract-paperless-service'
@Injectable({
providedIn: 'root',
})
export class DeletionRequestService extends AbstractPaperlessService<DeletionRequest> {
constructor() {
super()
this.resourceName = 'deletion_requests'
}
/**
* Approve a deletion request
* @param id The ID of the deletion request
* @param reviewComment Optional comment for the approval
* @returns Observable of the updated deletion request
*/
approve(id: number, reviewComment?: string): Observable<DeletionRequest> {
this._loading = true
const body = reviewComment ? { review_comment: reviewComment } : {}
return this.http
.post<DeletionRequest>(this.getResourceUrl(id, 'approve'), body)
.pipe(
tap(() => {
this._loading = false
})
)
}
/**
* Reject a deletion request
* @param id The ID of the deletion request
* @param reviewComment Optional comment for the rejection
* @returns Observable of the updated deletion request
*/
reject(id: number, reviewComment?: string): Observable<DeletionRequest> {
this._loading = true
const body = reviewComment ? { review_comment: reviewComment } : {}
return this.http
.post<DeletionRequest>(this.getResourceUrl(id, 'reject'), body)
.pipe(
tap(() => {
this._loading = false
})
)
}
/**
* Get the count of pending deletion requests
* @returns Observable with the count
*/
getPendingCount(): Observable<{ count: number }> {
return this.http.get<{ count: number }>(
this.getResourceUrl(null, 'pending_count')
)
}
}

View file

@ -13,6 +13,7 @@ import {
} from '@angular/common/http'
import { FormsModule, ReactiveFormsModule } from '@angular/forms'
import { BrowserModule, bootstrapApplication } from '@angular/platform-browser'
import { provideAnimations } from '@angular/platform-browser/animations'
import {
NgbDateAdapter,
NgbDateParserFormatter,
@ -56,11 +57,14 @@ import {
checkLg,
chevronDoubleLeft,
chevronDoubleRight,
chevronDown,
chevronRight,
chevronUp,
clipboard,
clipboardCheck,
clipboardCheckFill,
clipboardFill,
clock,
clockHistory,
dash,
dashCircle,
@ -71,6 +75,7 @@ import {
envelope,
envelopeAt,
envelopeAtFill,
exclamationCircle,
exclamationCircleFill,
exclamationTriangle,
exclamationTriangleFill,
@ -81,6 +86,7 @@ import {
fileEarmarkLock,
fileEarmarkMinus,
fileEarmarkRichtext,
fileEarmarkText,
fileText,
files,
filter,
@ -95,11 +101,14 @@ import {
hddStack,
house,
infoCircle,
inputCursorText,
journals,
lightbulb,
link,
listNested,
listTask,
listUl,
magic,
microsoft,
nodePlus,
pencil,
@ -270,11 +279,14 @@ const icons = {
checkLg,
chevronDoubleLeft,
chevronDoubleRight,
chevronDown,
chevronRight,
chevronUp,
clipboard,
clipboardCheck,
clipboardCheckFill,
clipboardFill,
clock,
clockHistory,
dash,
dashCircle,
@ -285,6 +297,7 @@ const icons = {
envelope,
envelopeAt,
envelopeAtFill,
exclamationCircle,
exclamationCircleFill,
exclamationTriangle,
exclamationTriangleFill,
@ -295,6 +308,7 @@ const icons = {
fileEarmarkLock,
fileEarmarkMinus,
fileEarmarkRichtext,
fileEarmarkText,
files,
fileText,
filter,
@ -309,11 +323,14 @@ const icons = {
hddStack,
house,
infoCircle,
inputCursorText,
journals,
lightbulb,
link,
listNested,
listTask,
listUl,
magic,
microsoft,
nodePlus,
pencil,
@ -402,5 +419,6 @@ bootstrapApplication(AppComponent, {
DocumentTypeNamePipe,
StoragePathNamePipe,
provideHttpClient(withInterceptorsFromDi()),
provideAnimations(),
],
}).catch((err) => console.error(err))