diff --git a/package-lock.json b/package-lock.json index 592a9c0cf..b3f19e3ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -319,6 +319,7 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", "integrity": "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==", + "dev": true, "license": "MIT", "engines": { "node": ">=10" @@ -429,218 +430,6 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/build-angular": { - "version": "20.3.15", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-20.3.15.tgz", - "integrity": "sha512-md5ikmdH0rM4jkVFwNQhN++onUUZPUDUfKijnb1XZArQisT9hPiNqTPbO+OZTBK/hQYYV/GfTOhDH30whG32xQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2003.15", - "@angular-devkit/build-webpack": "0.2003.15", - "@angular-devkit/core": "20.3.15", - "@angular/build": "20.3.15", - "@babel/core": "7.28.3", - "@babel/generator": "7.28.3", - "@babel/helper-annotate-as-pure": "7.27.3", - "@babel/helper-split-export-declaration": "7.24.7", - "@babel/plugin-transform-async-generator-functions": "7.28.0", - "@babel/plugin-transform-async-to-generator": "7.27.1", - "@babel/plugin-transform-runtime": "7.28.3", - "@babel/preset-env": "7.28.3", - "@babel/runtime": "7.28.3", - "@discoveryjs/json-ext": "0.6.3", - "@ngtools/webpack": "20.3.15", - "ansi-colors": "4.1.3", - "autoprefixer": "10.4.21", - "babel-loader": "10.0.0", - "browserslist": "^4.21.5", - "copy-webpack-plugin": "13.0.1", - "css-loader": "7.1.2", - "esbuild-wasm": "0.25.9", - "fast-glob": "3.3.3", - "http-proxy-middleware": "3.0.5", - "istanbul-lib-instrument": "6.0.3", - "jsonc-parser": "3.3.1", - "karma-source-map-support": "1.4.0", - "less": "4.4.0", - "less-loader": "12.3.0", - "license-webpack-plugin": "4.0.2", - "loader-utils": "3.3.1", - "mini-css-extract-plugin": "2.9.4", - "open": "10.2.0", - "ora": "8.2.0", - "picomatch": "4.0.3", - "piscina": "5.1.3", - "postcss": "8.5.6", - "postcss-loader": "8.1.1", - "resolve-url-loader": "5.0.0", - "rxjs": "7.8.2", - "sass": "1.90.0", - "sass-loader": "16.0.5", - "semver": "7.7.2", - "source-map-loader": "5.0.0", - "source-map-support": "0.5.21", - "terser": "5.43.1", - "tree-kill": "1.2.2", - "tslib": "2.8.1", - "webpack": "5.104.1", - "webpack-dev-middleware": "7.4.2", - "webpack-dev-server": "5.2.2", - "webpack-merge": "6.0.1", - "webpack-subresource-integrity": "5.1.0" - }, - "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" - }, - "optionalDependencies": { - "esbuild": "0.25.9" - }, - "peerDependencies": { - "@angular/compiler-cli": "^20.0.0", - "@angular/core": "^20.0.0", - "@angular/localize": "^20.0.0", - "@angular/platform-browser": "^20.0.0", - "@angular/platform-server": "^20.0.0", - "@angular/service-worker": "^20.0.0", - "@angular/ssr": "^20.3.15", - "@web/test-runner": "^0.20.0", - "browser-sync": "^3.0.2", - "jest": "^29.5.0 || ^30.2.0", - "jest-environment-jsdom": "^29.5.0 || ^30.2.0", - "karma": "^6.3.0", - "ng-packagr": "^20.0.0", - "protractor": "^7.0.0", - "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", - "typescript": ">=5.8 <6.0" - }, - "peerDependenciesMeta": { - "@angular/core": { - "optional": true - }, - "@angular/localize": { - "optional": true - }, - "@angular/platform-browser": { - "optional": true - }, - "@angular/platform-server": { - "optional": true - }, - "@angular/service-worker": { - "optional": true - }, - "@angular/ssr": { - "optional": true - }, - "@web/test-runner": { - "optional": true - }, - "browser-sync": { - "optional": true - }, - "jest": { - "optional": true - }, - "jest-environment-jsdom": { - "optional": true - }, - "karma": { - "optional": true - }, - "ng-packagr": { - "optional": true - }, - "protractor": { - "optional": true - }, - "tailwindcss": { - "optional": true - } - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/@babel/runtime": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz", - "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/esbuild-wasm": { - "version": "0.25.9", - "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.25.9.tgz", - "integrity": "sha512-Jpv5tCSwQg18aCqCRD3oHIX/prBhXMDapIoG//A+6+dV0e7KQMGFg85ihJ5T1EeMjbZjON3TqFy0VrGAnIHLDA==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@angular-devkit/build-angular/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "peer": true, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@angular-devkit/build-webpack": { - "version": "0.2003.15", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.2003.15.tgz", - "integrity": "sha512-u1gjLH0T+s4PiwbSUhzYZUKCsIF9CvTGJoN+Ki+CQqZAlTGLOp2y55VXmdrRX4e3tsKkS+chpQ8sN2b5vbzv9w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@angular-devkit/architect": "0.2003.15", - "rxjs": "7.8.2" - }, - "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" - }, - "peerDependencies": { - "webpack": "^5.30.0", - "webpack-dev-server": "^5.0.2" - } - }, "node_modules/@angular-devkit/core": { "version": "20.3.15", "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.3.15.tgz", @@ -1029,6 +818,7 @@ "version": "20.3.16", "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.3.16.tgz", "integrity": "sha512-l3xF/fXfJAl/UrNnH9Ufkr79myjMgXdHq1mmmph2UnpeqilRB1b8lC9sLBV9MipQHVn3dwocxMIvtrcryfOaXw==", + "dev": true, "license": "MIT", "dependencies": { "@babel/core": "7.28.3", @@ -1446,110 +1236,6 @@ "semver": "bin/semver.js" } }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.6.tgz", - "integrity": "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.6", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "peer": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.28.5.tgz", - "integrity": "sha512-N1EhvLtHzOvj7QQOUCCS3NrPJP8c5W6ZXCHDn7Yialuy1iu4r5EmIYkXlKNqT99Ciw+W0mDqWoR6HWMZlFP3hw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "regexpu-core": "^6.3.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "peer": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.6.tgz", - "integrity": "sha512-mOAsxeeKkUKayvZR3HeTYD/fICpCPLJrU5ZjelT/PA6WHtNDBOE436YiaEUvHN454bRM3CebhDsIpieCc4texA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "debug": "^4.4.3", - "lodash.debounce": "^4.0.8", - "resolve": "^1.22.11" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider/node_modules/resolve": { - "version": "1.22.11", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", - "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "is-core-module": "^2.16.1", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/@babel/helper-globals": { "version": "7.28.0", "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", @@ -1559,21 +1245,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.28.5.tgz", - "integrity": "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/traverse": "^7.28.5", - "@babel/types": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-module-imports": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", @@ -1604,20 +1275,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-plugin-utils": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", @@ -1628,59 +1285,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", - "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-wrap-function": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.28.6.tgz", - "integrity": "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.28.5", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-split-export-declaration": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", @@ -1721,22 +1325,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.6.tgz", - "integrity": "sha512-z+PwLziMNBeSQJonizz2AGnndLsP2DeGHIxDAn+wdHOGuo4Fo1x1HBPPXeE9TAOPHNNWQKCSlA2VZyYyyibDnQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/template": "^7.28.6", - "@babel/traverse": "^7.28.6", - "@babel/types": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helpers": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", @@ -1765,109 +1353,6 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.28.5.tgz", - "integrity": "sha512-87GDMS3tsmMSi/3bWOte1UblL+YUTFMV8SZPZ2eSEL17s74Cw/l63rR6NmGVKMYW2GYi85nE+/d6Hw5N0bEk2Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", - "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", - "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", - "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.6.tgz", - "integrity": "sha512-a0aBScVTlNaiUe35UtfxAN7A/tehvvG4/ByO6+46VPKTRSlfnAFsgKy0FUh+qAkQrDTmhDkT+IBOKlOoMUxQ0g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", @@ -1923,23 +1408,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.28.6.tgz", - "integrity": "sha512-pSJUpFHdx9z5nqTSirOCMtYVP2wFgoWhP0p3g8ONK/4IHhLIBd0B9NYqAvIUAhq+OkhO4VM1tENCt0cjlsNShw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-syntax-import-attributes": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", @@ -2124,1077 +1592,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", - "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.28.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", - "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-remap-async-to-generator": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", - "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.6.tgz", - "integrity": "sha512-tt/7wOtBmwHPNMPu7ax4pdPz6shjFrmHDghvNC+FG9Qvj7D6mJcoRQIF5dy4njmxR941l6rgtvfSB2zX3VlUIw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.28.6.tgz", - "integrity": "sha512-dY2wS3I2G7D697VHndN91TJr8/AAfXQNt5ynCTI/MpxMsSzHp+52uNivYT5wCPax3whc47DR8Ba7cmlQMg24bw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.6.tgz", - "integrity": "sha512-rfQ++ghVwTWTqQ7w8qyDxL1XGihjBss4CmTgGRCTAC9RIbhVpyp4fOeZtta0Lbf+dTNIVJer6ych2ibHwkZqsQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.6.tgz", - "integrity": "sha512-EF5KONAqC5zAqT783iMGuM2ZtmEBy+mJMOKl2BCvPZ2lVrwvXnB6o+OBWCS+CoeCCpVRF2sA2RBKUxvT8tQT5Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-replace-supers": "^7.28.6", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.28.6.tgz", - "integrity": "sha512-bcc3k0ijhHbc2lEfpFHgx7eYw9KNXqOerKWfzbxEHUGKnS3sz9C4CNL9OiFN1297bDNfUiSO7DaLzbvHQQQ1BQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/template": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.5.tgz", - "integrity": "sha512-Kl9Bc6D0zTUcFUvkNuQh4eGXPKKNDOJQXVyyM4ZAQPMveniJdxi8XMJwLo+xSoW3MIq81bD33lcUe9kZpl0MCw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.28.6.tgz", - "integrity": "sha512-SljjowuNKB7q5Oayv4FoPzeB74g3QgLt8IVJw9ADvWy3QnUb/01aw8I4AVv8wYnPvQz2GDDZ/g3GhcNyDBI4Bg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", - "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.29.0.tgz", - "integrity": "sha512-zBPcW2lFGxdiD8PUnPwJjag2J9otbcLQzvbiOzDxpYXyCuYX9agOwMPGn1prVH0a4qzhCKu24rlH4c1f7yA8rw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", - "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-explicit-resource-management": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.6.tgz", - "integrity": "sha512-Iao5Konzx2b6g7EPqTy40UZbcdXE126tTxVFr/nAIj+WItNxjKSYTEw3RC+A2/ZetmdJsgueL1KhaMCQHkLPIg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.28.6.tgz", - "integrity": "sha512-WitabqiGjV/vJ0aPOLSFfNY1u9U3R7W36B03r5I2KoNix+a3sOhJ3pKFB3R5It9/UiK78NiO0KE9P21cMhlPkw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", - "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.28.6.tgz", - "integrity": "sha512-Nr+hEN+0geQkzhbdgQVPoqr47lZbm+5fCUmO70722xJZd0Mvb59+33QLImGj6F+DkK3xgDi1YVysP8whD6FQAw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.28.6.tgz", - "integrity": "sha512-+anKKair6gpi8VsM/95kmomGNMD0eLz1NQ8+Pfw5sAwWH9fGYXT50E55ZpV0pHUHWf6IUTWPM+f/7AAff+wr9A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", - "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", - "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.28.6.tgz", - "integrity": "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.29.0.tgz", - "integrity": "sha512-PrujnVFbOdUpw4UHiVwKvKRLMMic8+eC0CuNlxjsyZUiBjhFdPsewdXCkveh2KqBA9/waD0W1b4hXSOBQJezpQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-validator-identifier": "^7.28.5", - "@babel/traverse": "^7.29.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", - "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.29.0.tgz", - "integrity": "sha512-1CZQA5KNAD6ZYQLPw7oi5ewtDNxH/2vuCh+6SmvgDfhumForvs8a1o9n0UrEoBD8HU4djO2yWngTQlXl1NDVEQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", - "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.28.6.tgz", - "integrity": "sha512-3wKbRgmzYbw24mDJXT7N+ADXw8BC/imU9yo9c9X9NKaLF1fW+e5H1U5QjMUBe4Qo4Ox/o++IyUkl1sVCLgevKg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.28.6.tgz", - "integrity": "sha512-SJR8hPynj8outz+SlStQSwvziMN4+Bq99it4tMIf5/Caq+3iOc0JtKyse8puvyXkk3eFRIA5ID/XfunGgO5i6w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.6.tgz", - "integrity": "sha512-5rh+JR4JBC4pGkXLAcYdLHZjXudVxWMXbB6u6+E9lRL5TrGVbHt1TjxGbZ8CkmYw9zjkB7jutzOROArsqtncEA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/plugin-transform-destructuring": "^7.28.5", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", - "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.28.6.tgz", - "integrity": "sha512-R8ja/Pyrv0OGAvAXQhSTmWyPJPml+0TMqXlO5w+AsMEiwb2fg3WkOvob7UxFSL3OIttFSGSRFKQsOhJ/X6HQdQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.28.6.tgz", - "integrity": "sha512-A4zobikRGJTsX9uqVFdafzGkqD30t26ck2LmOzAuLL8b2x6k3TIqRiT2xVvA9fNmFeTX484VpsdgmKNA0bS23w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.28.6.tgz", - "integrity": "sha512-piiuapX9CRv7+0st8lmuUlRSmX6mBcVeNQ1b4AYzJxfCMuBfB0vBXDiGSmm03pKJw1v6cZ8KSeM+oUnM6yAExg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.28.6.tgz", - "integrity": "sha512-b97jvNSOb5+ehyQmBpmhOCiUC5oVK4PMnpRvO7+ymFBoqYjeDHIU9jnrNUuwHOiL9RpGDoKBpSViarV+BU+eVA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.28.6", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", - "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.29.0.tgz", - "integrity": "sha512-FijqlqMA7DmRdg/aINBSs04y8XNTYw/lr1gJ2WsmBnnaNw1iS43EPkJW+zK7z65auG3AWRFXWj+NcTQwYptUog==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.28.6.tgz", - "integrity": "sha512-QGWAepm9qxpaIs7UM9FvUSnCGlb8Ua1RhyM4/veAxLwt3gMat/LSGrZixyuj4I6+Kn9iwvqCyPTtbdxanYoWYg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", - "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.3.tgz", - "integrity": "sha512-Y6ab1kGqZ0u42Zv/4a7l0l72n9DKP/MKoKWaUSBylrhNZO2prYuqFOLbn5aW5SIFXwSH93yfjbgllL8lxuGKLg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "babel-plugin-polyfill-corejs2": "^0.4.14", - "babel-plugin-polyfill-corejs3": "^0.13.0", - "babel-plugin-polyfill-regenerator": "^0.6.5", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "peer": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.28.6.tgz", - "integrity": "sha512-9U4QObUC0FtJl05AsUcodau/RWDytrU6uKgkxu09mLR9HLDAtUMoPuuskm5huQsoktmsYpI+bGmq+iapDcriKA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.28.6", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", - "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", - "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", - "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.28.6.tgz", - "integrity": "sha512-4Wlbdl/sIZjzi/8St0evF0gEZrgOswVO6aOzqxh1kDZOl9WmLrHq2HtGhnOJZmHZYKP8WZ1MDLCt5DAWwRo57A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", - "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.28.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.28.6.tgz", - "integrity": "sha512-/wHc/paTUmsDYN7SZkpWxogTOBNnlx7nBQYfy6JJlCT7G3mVhltk3e++N7zV0XfgGsrqBxd4rJQt9H16I21Y1Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.28.5", - "@babel/helper-plugin-utils": "^7.28.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.3.tgz", - "integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/compat-data": "^7.28.0", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.27.1", - "@babel/plugin-syntax-import-attributes": "^7.27.1", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.28.0", - "@babel/plugin-transform-async-to-generator": "^7.27.1", - "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.0", - "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-class-static-block": "^7.28.3", - "@babel/plugin-transform-classes": "^7.28.3", - "@babel/plugin-transform-computed-properties": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", - "@babel/plugin-transform-dotall-regex": "^7.27.1", - "@babel/plugin-transform-duplicate-keys": "^7.27.1", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", - "@babel/plugin-transform-dynamic-import": "^7.27.1", - "@babel/plugin-transform-explicit-resource-management": "^7.28.0", - "@babel/plugin-transform-exponentiation-operator": "^7.27.1", - "@babel/plugin-transform-export-namespace-from": "^7.27.1", - "@babel/plugin-transform-for-of": "^7.27.1", - "@babel/plugin-transform-function-name": "^7.27.1", - "@babel/plugin-transform-json-strings": "^7.27.1", - "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", - "@babel/plugin-transform-member-expression-literals": "^7.27.1", - "@babel/plugin-transform-modules-amd": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-modules-systemjs": "^7.27.1", - "@babel/plugin-transform-modules-umd": "^7.27.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", - "@babel/plugin-transform-new-target": "^7.27.1", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", - "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.28.0", - "@babel/plugin-transform-object-super": "^7.27.1", - "@babel/plugin-transform-optional-catch-binding": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/plugin-transform-private-methods": "^7.27.1", - "@babel/plugin-transform-private-property-in-object": "^7.27.1", - "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.3", - "@babel/plugin-transform-regexp-modifiers": "^7.27.1", - "@babel/plugin-transform-reserved-words": "^7.27.1", - "@babel/plugin-transform-shorthand-properties": "^7.27.1", - "@babel/plugin-transform-spread": "^7.27.1", - "@babel/plugin-transform-sticky-regex": "^7.27.1", - "@babel/plugin-transform-template-literals": "^7.27.1", - "@babel/plugin-transform-typeof-symbol": "^7.27.1", - "@babel/plugin-transform-unicode-escapes": "^7.27.1", - "@babel/plugin-transform-unicode-property-regex": "^7.27.1", - "@babel/plugin-transform-unicode-regex": "^7.27.1", - "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.14", - "babel-plugin-polyfill-corejs3": "^0.13.0", - "babel-plugin-polyfill-regenerator": "^0.6.5", - "core-js-compat": "^3.43.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "peer": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" - } - }, "node_modules/@babel/runtime": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", @@ -3539,17 +1936,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", - "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=14.17.0" - } - }, "node_modules/@dual-bundle/import-meta-resolve": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.2.1.tgz", @@ -5379,18 +3765,6 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", @@ -5407,457 +3781,6 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "node_modules/@jsonjoy.com/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/buffers": { - "version": "17.65.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-17.65.0.tgz", - "integrity": "sha512-eBrIXd0/Ld3p9lpDDlMaMn6IEfWqtHMD+z61u0JrIiPzsV1r7m6xDZFRxJyvIFTEO+SWdYF9EiQbXZGd8BzPfA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/codegen": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-1.0.0.tgz", - "integrity": "sha512-E8Oy+08cmCf0EK/NMxpaJZmOxPqM+6iSe2S4nlSBrPZOORoDJILxtbSUEDKQyTamm/BVAhIGllOBNU79/dwf0g==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-core": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-core/-/fs-core-4.56.10.tgz", - "integrity": "sha512-PyAEA/3cnHhsGcdY+AmIU+ZPqTuZkDhCXQ2wkXypdLitSpd6d5Ivxhnq4wa2ETRWFVJGabYynBWxIijOswSmOw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.56.10", - "@jsonjoy.com/fs-node-utils": "4.56.10", - "thingies": "^2.5.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-fsa": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-fsa/-/fs-fsa-4.56.10.tgz", - "integrity": "sha512-/FVK63ysNzTPOnCCcPoPHt77TOmachdMS422txM4KhxddLdbW1fIbFMYH0AM0ow/YchCyS5gqEjKLNyv71j/5Q==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/fs-core": "4.56.10", - "@jsonjoy.com/fs-node-builtins": "4.56.10", - "@jsonjoy.com/fs-node-utils": "4.56.10", - "thingies": "^2.5.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node/-/fs-node-4.56.10.tgz", - "integrity": "sha512-7R4Gv3tkUdW3dXfXiOkqxkElxKNVdd8BDOWC0/dbERd0pXpPY+s2s1Mino+aTvkGrFPiY+mmVxA7zhskm4Ue4Q==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/fs-core": "4.56.10", - "@jsonjoy.com/fs-node-builtins": "4.56.10", - "@jsonjoy.com/fs-node-utils": "4.56.10", - "@jsonjoy.com/fs-print": "4.56.10", - "@jsonjoy.com/fs-snapshot": "4.56.10", - "glob-to-regex.js": "^1.0.0", - "thingies": "^2.5.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node-builtins": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-builtins/-/fs-node-builtins-4.56.10.tgz", - "integrity": "sha512-uUnKz8R0YJyKq5jXpZtkGV9U0pJDt8hmYcLRrPjROheIfjMXsz82kXMgAA/qNg0wrZ1Kv+hrg7azqEZx6XZCVw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node-to-fsa": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-to-fsa/-/fs-node-to-fsa-4.56.10.tgz", - "integrity": "sha512-oH+O6Y4lhn9NyG6aEoFwIBNKZeYy66toP5LJcDOMBgL99BKQMUf/zWJspdRhMdn/3hbzQsZ8EHHsuekbFLGUWw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/fs-fsa": "4.56.10", - "@jsonjoy.com/fs-node-builtins": "4.56.10", - "@jsonjoy.com/fs-node-utils": "4.56.10" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-node-utils": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-node-utils/-/fs-node-utils-4.56.10.tgz", - "integrity": "sha512-8EuPBgVI2aDPwFdaNQeNpHsyqPi3rr+85tMNG/lHvQLiVjzoZsvxA//Xd8aB567LUhy4QS03ptT+unkD/DIsNg==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/fs-node-builtins": "4.56.10" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-print": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-print/-/fs-print-4.56.10.tgz", - "integrity": "sha512-JW4fp5mAYepzFsSGrQ48ep8FXxpg4niFWHdF78wDrFGof7F3tKDJln72QFDEn/27M1yHd4v7sKHHVPh78aWcEw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/fs-node-utils": "4.56.10", - "tree-dump": "^1.1.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/fs-snapshot/-/fs-snapshot-4.56.10.tgz", - "integrity": "sha512-DkR6l5fj7+qj0+fVKm/OOXMGfDFCGXLfyHkORH3DF8hxkpDgIHbhf/DwncBMs2igu/ST7OEkexn1gIqoU6Y+9g==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/buffers": "^17.65.0", - "@jsonjoy.com/fs-node-utils": "4.56.10", - "@jsonjoy.com/json-pack": "^17.65.0", - "@jsonjoy.com/util": "^17.65.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/base64": { - "version": "17.65.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-17.65.0.tgz", - "integrity": "sha512-Xrh7Fm/M0QAYpekSgmskdZYnFdSGnsxJ/tHaolA4bNwWdG9i65S8m83Meh7FOxyJyQAdo4d4J97NOomBLEfkDQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/codegen": { - "version": "17.65.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/codegen/-/codegen-17.65.0.tgz", - "integrity": "sha512-7MXcRYe7n3BG+fo3jicvjB0+6ypl2Y/bQp79Sp7KeSiiCgLqw4Oled6chVv07/xLVTdo3qa1CD0VCCnPaw+RGA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pack": { - "version": "17.65.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-17.65.0.tgz", - "integrity": "sha512-e0SG/6qUCnVhHa0rjDJHgnXnbsacooHVqQHxspjvlYQSkHm+66wkHw6Gql+3u/WxI/b1VsOdUi0M+fOtkgKGdQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/base64": "17.65.0", - "@jsonjoy.com/buffers": "17.65.0", - "@jsonjoy.com/codegen": "17.65.0", - "@jsonjoy.com/json-pointer": "17.65.0", - "@jsonjoy.com/util": "17.65.0", - "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0", - "tree-dump": "^1.1.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/json-pointer": { - "version": "17.65.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-17.65.0.tgz", - "integrity": "sha512-uhTe+XhlIZpWOxgPcnO+iSCDgKKBpwkDVTyYiXX9VayGV8HSFVJM67M6pUE71zdnXF1W0Da21AvnhlmdwYPpow==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/util": "17.65.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/fs-snapshot/node_modules/@jsonjoy.com/util": { - "version": "17.65.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-17.65.0.tgz", - "integrity": "sha512-cWiEHZccQORf96q2y6zU3wDeIVPeidmGqd9cNKJRYoVHTV0S1eHPy5JTbHpMnGfDvtvujQwQozOqgO9ABu6h0w==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/buffers": "17.65.0", - "@jsonjoy.com/codegen": "17.65.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/json-pack": { - "version": "1.21.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.21.0.tgz", - "integrity": "sha512-+AKG+R2cfZMShzrF2uQw34v3zbeDYUqnQ+jg7ORic3BGtfw9p/+N6RJbq/kkV8JmYZaINknaEQ2m0/f693ZPpg==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/base64": "^1.1.2", - "@jsonjoy.com/buffers": "^1.2.0", - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/json-pointer": "^1.0.2", - "@jsonjoy.com/util": "^1.9.0", - "hyperdyperid": "^1.2.0", - "thingies": "^2.5.0", - "tree-dump": "^1.1.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/json-pack/node_modules/@jsonjoy.com/buffers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", - "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/json-pointer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pointer/-/json-pointer-1.0.2.tgz", - "integrity": "sha512-Fsn6wM2zlDzY1U+v4Nc8bo3bVqgfNTGcn6dMgs6FjrEnt4ZCe60o6ByKRjOGlI2gow0aE/Q41QOigdTqkyK5fg==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/codegen": "^1.0.0", - "@jsonjoy.com/util": "^1.9.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/util": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.9.0.tgz", - "integrity": "sha512-pLuQo+VPRnN8hfPqUTLTHk126wuYdXVxE6aDmjSeV4NCAgyxWbiOIeNJVtID3h1Vzpoi9m4jXezf73I6LgabgQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/buffers": "^1.0.0", - "@jsonjoy.com/codegen": "^1.0.0" - }, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, - "node_modules/@jsonjoy.com/util/node_modules/@jsonjoy.com/buffers": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jsonjoy.com/buffers/-/buffers-1.2.1.tgz", - "integrity": "sha512-12cdlDwX4RUM3QxmUbVJWqZ/mrK6dFQH4Zxq6+r1YXKXYBNgZXndx2qbCJwh3+WWkCSn67IjnlG3XYTvmvYtgA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, "node_modules/@keyv/bigmap": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", @@ -5888,14 +3811,6 @@ "integrity": "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==", "license": "MIT" }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", - "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/@listr2/prompt-adapter-inquirer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-3.0.1.tgz", @@ -6708,24 +4623,6 @@ "node": ">= 10" } }, - "node_modules/@ngtools/webpack": { - "version": "20.3.15", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-20.3.15.tgz", - "integrity": "sha512-7bH91SdVN9xawMg4G+OoQG+nFz9/huUXx/MptR5iOsuRUpNo3PQwMNr07VTRV8o4YAtTTRIf2Yam4fNkb3rRAg==", - "dev": true, - "license": "MIT", - "peer": true, - "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" - }, - "peerDependencies": { - "@angular/compiler-cli": "^20.0.0", - "typescript": ">=5.8 <6.0", - "webpack": "^5.54.0" - } - }, "node_modules/@ngx-translate/core": { "version": "16.0.4", "resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-16.0.4.tgz", @@ -6756,6 +4653,7 @@ "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "2.0.5", @@ -6769,6 +4667,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -6778,6 +4677,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.scandir": "2.1.5", @@ -8183,52 +6083,6 @@ "@babel/types": "^7.28.2" } }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/bonjour": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", - "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", - "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/express-serve-static-core": "*", - "@types/node": "*" - } - }, "node_modules/@types/d3-dispatch": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", @@ -8294,30 +6148,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, "node_modules/@types/esquery": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/@types/esquery/-/esquery-1.5.4.tgz", @@ -8342,34 +6172,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/express": { - "version": "4.17.25", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.25.tgz", - "integrity": "sha512-dVd04UKsfpINUnK0yBoYHDF3xu7xVH4BuDotC/xGuycx4CgbP48X/KF/586bcObxT0HENHXEU8Nqtu6NR+eKhw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "^1" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.19.8", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.8.tgz", - "integrity": "sha512-02S5fmqeoKzVZCHPZid4b8JH2eM5HzQLZWN2FohQEy/0eXTq8VXZfSN6Pcr3F6N9R/vNrj7cpgbhjie6m/1tCA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, "node_modules/@types/graceful-fs": { "version": "4.1.9", "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", @@ -8380,25 +6182,6 @@ "@types/node": "*" } }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@types/http-proxy": { - "version": "1.17.17", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.17.tgz", - "integrity": "sha512-ED6LB+Z1AVylNTu7hdzuBqOgMnvG/ld6wGCG8wFnAzKX5uyW2K3WD52v0gnLCTK/VLpXtKckgWuyScYK6cSPaw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -8513,14 +6296,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/@types/node": { "version": "22.15.29", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.29.tgz", @@ -8531,31 +6306,12 @@ "undici-types": "~6.21.0" } }, - "node_modules/@types/node-forge": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", - "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/pako": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/pako/-/pako-2.0.4.tgz", "integrity": "sha512-VWDCbrLeVXJM9fihYodcLiIv0ku+AlOa/TQ1SvYOaBuyrSKgEcro95LJyIsJ4vSo6BXIxOKxiJAat04CmST9Fw==", "license": "MIT" }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/@types/raf": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/@types/raf/-/raf-3.4.3.tgz", @@ -8563,22 +6319,6 @@ "license": "MIT", "optional": true }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@types/retry": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", - "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/@types/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", @@ -8586,53 +6326,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/send": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", - "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/serve-index": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", - "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.10.tgz", - "integrity": "sha512-tRs1dB+g8Itk72rlSI2ZrW6vZg0YrLI81iQSTkMmOqnqCaNr/8Ek4VwWcN5vZgCYWbg/JJSGBlUaYGAOP73qBw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "<1" - } - }, - "node_modules/@types/serve-static/node_modules/@types/send": { - "version": "0.17.6", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.6.tgz", - "integrity": "sha512-Uqt8rPBE8SY0RK8JB1EzVOIZ32uqy8HwdxCnoCOsYrvnswqmFZ/k+9Ikidlk/ImhsdvBsloHbAlewb2IEBV/Og==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, "node_modules/@types/sinonjs__fake-timers": { "version": "8.1.1", "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz", @@ -8647,17 +6340,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/sockjs": { - "version": "0.3.36", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", - "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/stack-utils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", @@ -8699,17 +6381,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*" - } - }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -9082,198 +6753,6 @@ "vite": "^6.0.0 || ^7.0.0" } }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true - }, "node_modules/@yarnpkg/lockfile": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", @@ -9376,20 +6855,6 @@ "acorn-walk": "^8.0.2" } }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, "node_modules/acorn-jsx": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", @@ -9413,37 +6878,6 @@ "node": ">=0.4.0" } }, - "node_modules/adjust-sourcemap-loader": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", - "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "loader-utils": "^2.0.0", - "regex-parser": "^2.2.11" - }, - "engines": { - "node": ">=8.9" - } - }, - "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, "node_modules/agent-base": { "version": "7.1.4", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", @@ -9503,20 +6937,6 @@ } } }, - "node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, "node_modules/algoliasearch": { "version": "5.35.0", "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.35.0.tgz", @@ -9582,20 +7002,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", - "dev": true, - "engines": [ - "node >= 0.8.0" - ], - "license": "Apache-2.0", - "peer": true, - "bin": { - "ansi-html": "bin/ansi-html" - } - }, "node_modules/ansi-regex": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", @@ -9624,12 +7030,14 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, "license": "MIT" }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, "license": "ISC", "dependencies": { "normalize-path": "^3.0.0", @@ -9643,6 +7051,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -9792,6 +7201,7 @@ "version": "5.0.2", "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true, "license": "MIT" }, "node_modules/argparse": { @@ -9811,14 +7221,6 @@ "node": ">= 0.4" } }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -9895,45 +7297,6 @@ "node": ">= 4.0.0" } }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "peer": true, - "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -9997,24 +7360,6 @@ "@babel/core": "^7.8.0" } }, - "node_modules/babel-loader": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-10.0.0.tgz", - "integrity": "sha512-z8jt+EdS61AMw22nSfoNJAZ0vrtmhPRVi6ghL3rCeRZI8cdNYFiV5xeV3HbE7rlZZNmGH8BVccwWt8/ED0QOHA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "find-up": "^5.0.0" - }, - "engines": { - "node": "^18.20.0 || ^20.10.0 || >=22.0.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0", - "webpack": ">=5.61.0" - } - }, "node_modules/babel-plugin-istanbul": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", @@ -10075,62 +7420,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.15", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.15.tgz", - "integrity": "sha512-hR3GwrRwHUfYwGfrisXPIDP3JcYfBrW7wKE7+Au6wDYl7fm/ka1NEII6kORzxNU556JjfidZeBsO10kYvtV1aw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/compat-data": "^7.28.6", - "@babel/helper-define-polyfill-provider": "^0.6.6", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "peer": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", - "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5", - "core-js-compat": "^3.43.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.6.tgz", - "integrity": "sha512-hYm+XLYRMvupxiQzrvXUj7YyvFFVfv5gI0R71AJzudg1g2AI2vyCPPIFEBjk162/wFzti3inBHo7isWFuEVS/A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.6" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, "node_modules/babel-preset-current-node-syntax": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", @@ -10325,14 +7614,6 @@ "node": ">=10.0.0" } }, - "node_modules/batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/bcrypt-pbkdf": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", @@ -10363,21 +7644,11 @@ "node": ">=14.0.0" } }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": "*" - } - }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, "license": "MIT", "engines": { "node": ">=8" @@ -10424,18 +7695,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/bonjour-service": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", - "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "fast-deep-equal": "^3.1.3", - "multicast-dns": "^7.2.5" - } - }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", @@ -10456,6 +7715,7 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, "license": "MIT", "dependencies": { "fill-range": "^7.1.1" @@ -10568,23 +7828,6 @@ "dev": true, "license": "MIT" }, - "node_modules/bundle-name": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", - "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "run-applescript": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -10758,6 +8001,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -10883,6 +8127,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, "license": "MIT", "dependencies": { "readdirp": "^4.0.1" @@ -10904,17 +8149,6 @@ "node": ">=18" } }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.0" - } - }, "node_modules/chromium-bidi": { "version": "13.0.1", "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-13.0.1.tgz", @@ -11188,36 +8422,6 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/clone-deep/node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -11367,70 +8571,6 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", - "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.1.0", - "safe-buffer": "5.2.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/compression/node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -11438,17 +8578,6 @@ "dev": true, "license": "MIT" }, - "node_modules/connect-history-api-fallback": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", - "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.8" - } - }, "node_modules/content-disposition": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz", @@ -11477,6 +8606,7 @@ "version": "1.9.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true, "license": "MIT" }, "node_modules/cookie": { @@ -11499,45 +8629,6 @@ "node": ">=6.6.0" } }, - "node_modules/copy-anything": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", - "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "is-what": "^3.14.1" - }, - "funding": { - "url": "https://github.com/sponsors/mesqueeb" - } - }, - "node_modules/copy-webpack-plugin": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-13.0.1.tgz", - "integrity": "sha512-J+YV3WfhY6W/Xf9h+J1znYuqTye2xkBUIGyTPWuBAT27qajBa5mR4f8WBmfDY3YjRftT2kqZZiLi1qf0H+UOFw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "glob-parent": "^6.0.1", - "normalize-path": "^3.0.0", - "schema-utils": "^4.2.0", - "serialize-javascript": "^6.0.2", - "tinyglobby": "^0.2.12" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - } - }, "node_modules/core-js": { "version": "3.48.0", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.48.0.tgz", @@ -11550,21 +8641,6 @@ "url": "https://opencollective.com/core-js" } }, - "node_modules/core-js-compat": { - "version": "3.48.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.48.0.tgz", - "integrity": "sha512-OM4cAF3D6VtH/WkLtWvyNC56EZVXsZdU3iqaMG2B4WvYrlqU831pc4UtG5yp0sE9z8Y02wVN7PjW5Zf9Gt0f1Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "browserslist": "^4.28.1" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, "node_modules/core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", @@ -11761,43 +8837,6 @@ "utrie": "^1.0.2" } }, - "node_modules/css-loader": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", - "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.27.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, "node_modules/css-select": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", @@ -11846,6 +8885,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, "license": "MIT", "bin": { "cssesc": "bin/cssesc" @@ -12633,52 +9673,6 @@ "node": ">=0.10.0" } }, - "node_modules/default-browser": { - "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", - "peer": true, - "dependencies": { - "bundle-name": "^4.1.0", - "default-browser-id": "^5.0.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/default-browser-id": { - "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", - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/degenerator": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", @@ -12723,18 +9717,6 @@ "node": ">= 0.8" } }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -12756,14 +9738,6 @@ "node": ">=8" } }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/devtools-protocol": { "version": "0.0.1551306", "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1551306.tgz", @@ -12775,6 +9749,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true, "license": "Apache-2.0" }, "node_modules/diff": { @@ -12814,21 +9789,8 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "license": "MIT" - }, - "node_modules/dns-packet": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", - "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" - }, - "engines": { - "node": ">=6" - } + "license": "MIT" }, "node_modules/dom-serializer": { "version": "2.0.0", @@ -13016,17 +9978,6 @@ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "license": "MIT" }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 4" - } - }, "node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", @@ -13070,21 +10021,6 @@ "once": "^1.4.0" } }, - "node_modules/enhanced-resolve": { - "version": "5.18.4", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.4.tgz", - "integrity": "sha512-LgQMM4WXU3QI+SYgEc2liRgznaD5ojbmY3sb8LxyguVkIg5FxdpTkvk72te2R38/TGKxH634oLxXRGY6d7AP+Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/enquirer": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", @@ -13142,21 +10078,6 @@ "dev": true, "license": "MIT" }, - "node_modules/errno": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", - "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "prr": "~1.0.1" - }, - "bin": { - "errno": "cli.js" - } - }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -13187,14 +10108,6 @@ "node": ">= 0.4" } }, - "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/es-object-atoms": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", @@ -13965,6 +10878,7 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, "license": "MIT", "dependencies": { "@nodelib/fs.stat": "^2.0.2", @@ -13981,6 +10895,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -14045,25 +10960,12 @@ "version": "1.20.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, "license": "ISC", "dependencies": { "reusify": "^1.0.4" } }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -14150,6 +11052,7 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" @@ -14241,28 +11144,6 @@ "dev": true, "license": "ISC" }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "peer": true, - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -14316,21 +11197,6 @@ "node": ">= 0.6" } }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, "node_modules/fresh": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", @@ -14381,6 +11247,7 @@ "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -14395,6 +11262,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -14595,6 +11463,7 @@ "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.3" @@ -14603,24 +11472,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob-to-regex.js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/glob-to-regex.js/-/glob-to-regex.js-1.2.0.tgz", - "integrity": "sha512-QMwlOQKU/IzqMUOAZWubUOT8Qft+Y0KQWnX9nK3ch0CJg0tTp4TvGZsTfudYKv2NzoQSyPcnA6TYeIQ3jGichQ==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, "node_modules/glob-to-regexp": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", @@ -14781,14 +11632,6 @@ "lodash": "^4.17.15" } }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/handlebars": { "version": "4.7.8", "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", @@ -14877,6 +11720,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -14885,17 +11729,6 @@ "node": ">= 0.4" } }, - "node_modules/hono": { - "version": "4.11.7", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz", - "integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=16.9.0" - } - }, "node_modules/hookified": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", @@ -14926,56 +11759,6 @@ "node": "20 || >=22" } }, - "node_modules/hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "node_modules/hpack.js/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/hpack.js/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/hpack.js/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, "node_modules/html-encoding-sniffer": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", @@ -15155,14 +11938,6 @@ "dev": true, "license": "BSD-2-Clause" }, - "node_modules/http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/http-errors": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", @@ -15184,30 +11959,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/http-parser-js": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", - "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, "node_modules/http-proxy-agent": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", @@ -15236,33 +11987,6 @@ "node": ">= 6.0.0" } }, - "node_modules/http-proxy-middleware": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.5.tgz", - "integrity": "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/http-proxy": "^1.17.15", - "debug": "^4.3.6", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.3", - "is-plain-object": "^5.0.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/http-proxy/node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/http-signature": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.4.0.tgz", @@ -15318,17 +12042,6 @@ "url": "https://github.com/sponsors/typicode" } }, - "node_modules/hyperdyperid": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", - "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.18" - } - }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -15346,20 +12059,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true, - "license": "ISC", - "peer": true, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -15419,21 +12118,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/image-size": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", - "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/immediate": { "version": "3.0.6", "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", @@ -15578,6 +12262,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" @@ -15590,6 +12275,7 @@ "version": "2.16.1", "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, "license": "MIT", "dependencies": { "hasown": "^2.0.2" @@ -15601,27 +12287,11 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -15654,6 +12324,7 @@ "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" @@ -15662,26 +12333,6 @@ "node": ">=0.10.0" } }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-installed-globally": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", @@ -15712,24 +12363,11 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-network-error": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.3.0.tgz", - "integrity": "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.12.0" @@ -15745,20 +12383,6 @@ "node": ">=8" } }, - "node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-plain-object": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", @@ -15815,31 +12439,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-what": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", - "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", @@ -15852,17 +12451,6 @@ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", "license": "ISC" }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/isstream": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", @@ -16893,6 +13481,7 @@ "version": "1.21.7", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, "license": "MIT", "bin": { "jiti": "bin/jiti.js" @@ -17267,17 +13856,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/karma-source-map-support": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", - "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "source-map-support": "^0.5.5" - } - }, "node_modules/keyv": { "version": "5.6.0", "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", @@ -17315,18 +13893,6 @@ "dev": true, "license": "MIT" }, - "node_modules/launch-editor": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.12.0.tgz", - "integrity": "sha512-giOHXoOtifjdHqUamwKq6c49GzBdLjvxrd2D+Q4V6uOHopJv7p9VJxikDsQ/CBXZbEITgUqSVHXLTG3VhPP1Dg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "picocolors": "^1.1.1", - "shell-quote": "^1.8.3" - } - }, "node_modules/lazy-ass": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/lazy-ass/-/lazy-ass-1.6.0.tgz", @@ -17379,114 +13945,6 @@ "safe-buffer": "~5.1.0" } }, - "node_modules/less": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/less/-/less-4.4.0.tgz", - "integrity": "sha512-kdTwsyRuncDfjEs0DlRILWNvxhDG/Zij4YLO4TMJgDLW+8OzpfkdPnRgrsRuY1o+oaxJGWsps5f/RVBgGmmN0w==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "copy-anything": "^2.0.1", - "parse-node-version": "^1.0.1", - "tslib": "^2.3.0" - }, - "bin": { - "lessc": "bin/lessc" - }, - "engines": { - "node": ">=14" - }, - "optionalDependencies": { - "errno": "^0.1.1", - "graceful-fs": "^4.1.2", - "image-size": "~0.5.0", - "make-dir": "^2.1.0", - "mime": "^1.4.1", - "needle": "^3.1.0", - "source-map": "~0.6.0" - } - }, - "node_modules/less-loader": { - "version": "12.3.0", - "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.3.0.tgz", - "integrity": "sha512-0M6+uYulvYIWs52y0LqN4+QM9TqWAohYSNTo4htE8Z7Cn3G/qQMEmktfHmyJT23k+20kU9zHH2wrfFXkxNLtVw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "less": "^3.5.0 || ^4.0.0", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/less/node_modules/make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/less/node_modules/pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "engines": { - "node": ">=6" - } - }, - "node_modules/less/node_modules/semver": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", - "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", - "dev": true, - "license": "ISC", - "optional": true, - "peer": true, - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/less/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "optional": true, - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -17511,25 +13969,6 @@ "node": ">= 0.8.0" } }, - "node_modules/license-webpack-plugin": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", - "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "webpack-sources": "^3.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "webpack-sources": { - "optional": true - } - } - }, "node_modules/lie": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz", @@ -17552,6 +13991,7 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, "license": "MIT", "engines": { "node": ">=14" @@ -17564,6 +14004,7 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, "license": "MIT" }, "node_modules/lint-staged": { @@ -18027,32 +14468,6 @@ "win32" ] }, - "node_modules/loader-runner": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", - "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/loader-utils": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", - "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 12.13.0" - } - }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -18075,14 +14490,6 @@ "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", "license": "MIT" }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/lodash.escaperegexp": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", @@ -18513,37 +14920,6 @@ "node": ">= 0.8" } }, - "node_modules/memfs": { - "version": "4.56.10", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.56.10.tgz", - "integrity": "sha512-eLvzyrwqLHnLYalJP7YZ3wBe79MXktMdfQbvMrVD80K+NhrIukCVBvgP30zTJYEEDh9hZ/ep9z0KOdD7FSHo7w==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "@jsonjoy.com/fs-core": "4.56.10", - "@jsonjoy.com/fs-fsa": "4.56.10", - "@jsonjoy.com/fs-node": "4.56.10", - "@jsonjoy.com/fs-node-builtins": "4.56.10", - "@jsonjoy.com/fs-node-to-fsa": "4.56.10", - "@jsonjoy.com/fs-node-utils": "4.56.10", - "@jsonjoy.com/fs-print": "4.56.10", - "@jsonjoy.com/fs-snapshot": "4.56.10", - "@jsonjoy.com/json-pack": "^1.11.0", - "@jsonjoy.com/util": "^1.9.0", - "glob-to-regex.js": "^1.0.1", - "thingies": "^2.5.0", - "tree-dump": "^1.0.3", - "tslib": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, "node_modules/meow": { "version": "13.2.0", "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", @@ -18581,26 +14957,17 @@ "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 8" } }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, "license": "MIT", "dependencies": { "braces": "^3.0.3", @@ -18614,6 +14981,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -18622,20 +14990,6 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, - "license": "MIT", - "peer": true, - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", @@ -18682,28 +15036,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/mini-css-extract-plugin": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz", - "integrity": "sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "schema-utils": "^4.0.0", - "tapable": "^2.2.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, "node_modules/mini-svg-data-uri": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", @@ -18713,14 +15045,6 @@ "mini-svg-data-uri": "cli.js" } }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true, - "license": "ISC", - "peer": true - }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -18978,21 +15302,6 @@ "win32" ] }, - "node_modules/multicast-dns": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", - "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" - } - }, "node_modules/mute-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", @@ -19007,6 +15316,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0", @@ -19018,6 +15328,7 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, "funding": [ { "type": "github", @@ -19039,40 +15350,6 @@ "dev": true, "license": "MIT" }, - "node_modules/needle": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", - "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "iconv-lite": "^0.6.3", - "sax": "^1.2.4" - }, - "bin": { - "needle": "bin/needle" - }, - "engines": { - "node": ">= 4.4.x" - } - }, - "node_modules/needle/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -19179,17 +15456,6 @@ "webidl-conversions": "^3.0.0" } }, - "node_modules/node-forge": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", - "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", - "dev": true, - "license": "(BSD-3-Clause OR GPL-2.0)", - "peer": true, - "engines": { - "node": ">= 6.13.0" - } - }, "node_modules/node-gyp": { "version": "12.2.0", "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-12.2.0.tgz", @@ -19304,17 +15570,6 @@ "node": ">=0.10.0" } }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/npm-bundled": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-5.0.0.tgz", @@ -19474,6 +15729,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -19483,6 +15739,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -19501,14 +15758,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/on-finished": { "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", @@ -19522,17 +15771,6 @@ "node": ">= 0.8" } }, - "node_modules/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.8" - } - }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -19559,26 +15797,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/open": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.2.0.tgz", - "integrity": "sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "wsl-utils": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -19840,36 +16058,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/p-retry": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", - "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/retry": "0.12.2", - "is-network-error": "^1.0.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-retry/node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 4" - } - }, "node_modules/p-try": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", @@ -20035,17 +16223,6 @@ "dev": true, "license": "MIT" }, - "node_modules/parse-node-version": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", - "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.10" - } - }, "node_modules/parse5": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", @@ -20154,6 +16331,7 @@ "version": "1.0.7", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, "license": "MIT" }, "node_modules/path-scurry": { @@ -20260,6 +16438,7 @@ "version": "2.3.0", "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -20269,6 +16448,7 @@ "version": "4.0.7", "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -20417,6 +16597,7 @@ "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "dev": true, "funding": [ { "type": "opencollective", @@ -20445,6 +16626,7 @@ "version": "15.1.0", "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-15.1.0.tgz", "integrity": "sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==", + "dev": true, "license": "MIT", "dependencies": { "postcss-value-parser": "^4.0.0", @@ -20462,6 +16644,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.1.0.tgz", "integrity": "sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==", + "dev": true, "funding": [ { "type": "opencollective", @@ -20487,6 +16670,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, "funding": [ { "type": "opencollective", @@ -20525,39 +16709,6 @@ } } }, - "node_modules/postcss-loader": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", - "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "cosmiconfig": "^9.0.0", - "jiti": "^1.20.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, "node_modules/postcss-media-query-parser": { "version": "0.2.3", "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", @@ -20565,77 +16716,11 @@ "dev": true, "license": "MIT" }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", - "dev": true, - "license": "ISC", - "peer": true, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", - "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^7.0.0", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", - "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, "node_modules/postcss-nested": { "version": "6.2.0", "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.2.0.tgz", "integrity": "sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==", + "dev": true, "funding": [ { "type": "opencollective", @@ -20661,6 +16746,7 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -20749,6 +16835,7 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, "license": "MIT" }, "node_modules/prelude-ls": { @@ -20998,15 +17085,6 @@ "node": ">=22" } }, - "node_modules/prr": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true - }, "node_modules/psl": { "version": "1.15.0", "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", @@ -21139,6 +17217,7 @@ "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, "funding": [ { "type": "github", @@ -21165,17 +17244,6 @@ "performance-now": "^2.1.0" } }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, "node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -21213,6 +17281,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, "license": "MIT", "dependencies": { "pify": "^2.3.0" @@ -21257,6 +17326,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, "license": "MIT", "engines": { "node": ">= 14.18.0" @@ -21270,30 +17340,9 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true, "license": "Apache-2.0" }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", - "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/regenerator-runtime": { "version": "0.13.11", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", @@ -21301,55 +17350,6 @@ "license": "MIT", "optional": true }, - "node_modules/regex-parser": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.1.tgz", - "integrity": "sha512-yXLRqatcCuKtVHsWrNg0JL3l1zGfdXeEvDa0bdu4tCDQw0RpMDZsqbkyRTUnKMR0tXF627V2oEWjBEaEdqTwtQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/regexpu-core": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.4.0.tgz", - "integrity": "sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.2", - "regjsgen": "^0.8.0", - "regjsparser": "^0.13.0", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.2.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/regjsparser": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.13.0.tgz", - "integrity": "sha512-NZQZdC5wOE/H3UT28fVGL+ikOZcEzfMGk/c3iN9UGxzWHMa1op7274oyiUVrAG4B2EuFhus8SvkaYnhvW92p9Q==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "jsesc": "~3.1.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, "node_modules/request-progress": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-3.0.0.tgz", @@ -21391,6 +17391,7 @@ "version": "1.22.10", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, "license": "MIT", "dependencies": { "is-core-module": "^2.16.0", @@ -21440,51 +17441,6 @@ "node": ">=4" } }, - "node_modules/resolve-url-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", - "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "adjust-sourcemap-loader": "^4.0.0", - "convert-source-map": "^1.7.0", - "loader-utils": "^2.0.0", - "postcss": "^8.2.14", - "source-map": "0.6.1" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/resolve-url-loader/node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/resolve-url-loader/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/resolve.exports": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", @@ -21530,6 +17486,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, "license": "MIT", "engines": { "iojs": ">=1.0.0", @@ -21632,24 +17589,11 @@ "node": ">= 18" } }, - "node_modules/run-applescript": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.1.0.tgz", - "integrity": "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, "funding": [ { "type": "github", @@ -21731,60 +17675,6 @@ "@parcel/watcher": "^2.4.1" } }, - "node_modules/sass-loader": { - "version": "16.0.5", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.5.tgz", - "integrity": "sha512-oL+CMBXrj6BZ/zOq4os+UECPL+bWqt6OAC6DWS8Ln8GZRcMDjlJ4JC3FBDuHJdYaFWIdKNIBYmtZtK2MaMkNIw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "neo-async": "^2.6.2" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", - "sass": "^1.3.0", - "sass-embedded": "*", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/sax": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.4.tgz", - "integrity": "sha512-1n3r/tGXO6b6VXMdFT54SHzT9ytu9yr7TaELowdYpMqY/Ao7EnlQGmAQ1+RatX7Tkkdm6hONI2owqNx2aZj5Sw==", - "dev": true, - "license": "BlueOak-1.0.0", - "optional": true, - "peer": true, - "engines": { - "node": ">=11.0.0" - } - }, "node_modules/saxes": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", @@ -21797,73 +17687,11 @@ "node": ">=10" } }, - "node_modules/schema-utils": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", - "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/schema-utils/node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/selfsigned": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", - "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node-forge": "^1.3.0", - "node-forge": "^1" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/semver": { "version": "7.7.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -21926,126 +17754,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/serve-index": { - "version": "1.9.2", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.2.tgz", - "integrity": "sha512-KDj11HScOaLmrPxl70KYNW1PksP4Nb/CLL2yvC+Qd2kHMPEEpfc4Re2e4FOay+bC/+XQl/7zAcWON3JVo5v3KQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "accepts": "~1.3.8", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.8.0", - "mime-types": "~2.1.35", - "parseurl": "~1.3.3" - }, - "engines": { - "node": ">= 0.8.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/serve-index/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-index/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/http-errors": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", - "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/serve-index/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, "node_modules/serve-static": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz", @@ -22079,20 +17787,6 @@ "dev": true, "license": "ISC" }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -22114,20 +17808,6 @@ "node": ">=8" } }, - "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -22298,19 +17978,6 @@ "npm": ">= 3.0.0" } }, - "node_modules/sockjs": { - "version": "0.3.24", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", - "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "faye-websocket": "^0.11.3", - "uuid": "^8.3.2", - "websocket-driver": "^0.7.4" - } - }, "node_modules/socks": { "version": "2.8.7", "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", @@ -22355,47 +18022,12 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } }, - "node_modules/source-map-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", - "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "iconv-lite": "^0.6.3", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.72.1" - } - }, - "node_modules/source-map-loader/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -22453,40 +18085,6 @@ "dev": true, "license": "CC0-1.0" }, - "node_modules/spdy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - } - }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -22962,6 +18560,7 @@ "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, "license": "MIT", "dependencies": { "@jridgewell/gen-mapping": "^0.3.2", @@ -22984,6 +18583,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, "license": "MIT", "engines": { "node": ">= 6" @@ -23039,6 +18639,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -23132,6 +18733,7 @@ "version": "3.4.19", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.19.tgz", "integrity": "sha512-3ofp+LL8E+pK/JuPLPggVAIaEuhvIz4qNcf3nA1Xn2o/7fb7s/TYpHhwGDv1ZU3PkBluUVaF8PyCHcm48cKLWQ==", + "dev": true, "license": "MIT", "dependencies": { "@alloc/quick-lru": "^5.2.0", @@ -23169,6 +18771,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, "license": "MIT", "dependencies": { "anymatch": "~3.1.2", @@ -23193,6 +18796,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, "license": "ISC", "dependencies": { "is-glob": "^4.0.1" @@ -23205,6 +18809,7 @@ "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, "license": "MIT", "engines": { "node": ">=8.6" @@ -23217,6 +18822,7 @@ "version": "6.1.2", "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "dev": true, "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -23230,6 +18836,7 @@ "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, "license": "MIT", "dependencies": { "picomatch": "^2.2.1" @@ -23238,21 +18845,6 @@ "node": ">=8.10.0" } }, - "node_modules/tapable": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", - "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/tar": { "version": "7.5.7", "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.7.tgz", @@ -23306,86 +18898,6 @@ "node": ">=18" } }, - "node_modules/terser": { - "version": "5.43.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz", - "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.14.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.16", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.16.tgz", - "integrity": "sha512-h9oBFCWrq78NyWWVcSwZarJkZ01c2AyGrzs1crmHZO3QUg9D61Wu4NPjBy69n7JqylFF5y+CsUZYmYEIZ3mR+Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser-webpack-plugin/node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -23469,6 +18981,7 @@ "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, "license": "MIT", "dependencies": { "any-promise": "^1.0.0" @@ -23478,6 +18991,7 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, "license": "MIT", "dependencies": { "thenify": ">= 3.1.0 < 4" @@ -23486,24 +19000,6 @@ "node": ">=0.8" } }, - "node_modules/thingies": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/thingies/-/thingies-2.5.0.tgz", - "integrity": "sha512-s+2Bwztg6PhWUD7XMfeYm5qliDdSiZm7M7n8KjTkIsm3l/2lgVRc2/Gx/v+ZX8lT4FMA+i8aQvhcWylldc+ZNw==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.18" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "^2" - } - }, "node_modules/throttleit": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", @@ -23521,14 +19017,6 @@ "dev": true, "license": "MIT" }, - "node_modules/thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/tinyglobby": { "version": "0.2.14", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.14.tgz", @@ -23591,6 +19079,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, "license": "MIT", "dependencies": { "is-number": "^7.0.0" @@ -23644,24 +19133,6 @@ "url": "https://github.com/sponsors/chrvadala" } }, - "node_modules/tree-dump": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.1.0.tgz", - "integrity": "sha512-rMuvhU4MCDbcbnleZTFezWsaZXRFemSqAM+7jPnzUl1fo9w3YEKOxAeui0fz3OI4EU4hf23iyA7uQRVko+UaBA==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=10.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - }, - "peerDependencies": { - "tslib": "2" - } - }, "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", @@ -23689,6 +19160,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, "license": "Apache-2.0" }, "node_modules/ts-jest": { @@ -23995,14 +19467,6 @@ "url": "https://opencollective.com/express" } }, - "node_modules/typed-assert": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", - "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/typed-event-target": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/typed-event-target/-/typed-event-target-4.1.0.tgz", @@ -24029,7 +19493,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "devOptional": true, + "dev": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -24060,54 +19524,6 @@ "dev": true, "license": "MIT" }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", - "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", - "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", - "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=4" - } - }, "node_modules/unique-filename": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-5.0.0.tgz", @@ -24247,17 +19663,6 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.4.0" - } - }, "node_modules/utrie": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/utrie/-/utrie-1.0.2.tgz", @@ -24486,17 +19891,6 @@ "node": ">=10.13.0" } }, - "node_modules/wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "minimalistic-assert": "^1.0.0" - } - }, "node_modules/weak-lru-cache": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", @@ -24577,664 +19971,6 @@ "node": ">=12" } }, - "node_modules/webpack": { - "version": "5.104.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.104.1.tgz", - "integrity": "sha512-Qphch25abbMNtekmEGJmeRUhLDbe+QfiWTiqpKYkpCOWY64v9eyl+KRRLmqOFA2AvKPpc9DC6+u2n76tQLBoaA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.28.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.4", - "es-module-lexer": "^2.0.0", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.3.1", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.3", - "tapable": "^2.3.0", - "terser-webpack-plugin": "^5.3.16", - "watchpack": "^2.4.4", - "webpack-sources": "^3.3.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-dev-middleware": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", - "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "colorette": "^2.0.10", - "memfs": "^4.6.0", - "mime-types": "^2.1.31", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - } - } - }, - "node_modules/webpack-dev-server": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.2.tgz", - "integrity": "sha512-QcQ72gh8a+7JO63TAx/6XZf/CWhgMzu5m0QirvPfGvptOusAxG12w2+aua1Jkjr7hzaWDnJ2n6JFeexMHI+Zjg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/bonjour": "^3.5.13", - "@types/connect-history-api-fallback": "^1.5.4", - "@types/express": "^4.17.21", - "@types/express-serve-static-core": "^4.17.21", - "@types/serve-index": "^1.9.4", - "@types/serve-static": "^1.15.5", - "@types/sockjs": "^0.3.36", - "@types/ws": "^8.5.10", - "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.2.1", - "chokidar": "^3.6.0", - "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^2.0.0", - "express": "^4.21.2", - "graceful-fs": "^4.2.6", - "http-proxy-middleware": "^2.0.9", - "ipaddr.js": "^2.1.0", - "launch-editor": "^2.6.1", - "open": "^10.0.3", - "p-retry": "^6.2.0", - "schema-utils": "^4.2.0", - "selfsigned": "^2.4.1", - "serve-index": "^1.9.1", - "sockjs": "^0.3.24", - "spdy": "^4.0.2", - "webpack-dev-middleware": "^7.4.2", - "ws": "^8.18.0" - }, - "bin": { - "webpack-dev-server": "bin/webpack-dev-server.js" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-dev-server/node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-server/node_modules/body-parser": { - "version": "1.20.4", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", - "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bytes": "~3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "~1.2.0", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "on-finished": "~2.4.1", - "qs": "~6.14.0", - "raw-body": "~2.5.3", - "type-is": "~1.6.18", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/webpack-dev-server/node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/webpack-dev-server/node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-server/node_modules/cookie-signature": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.7.tgz", - "integrity": "sha512-NXdYc3dLr47pBkpUCHtKSwIOQXLVn8dZEuywboCOJY/osA0wFSLlSawr3KN8qXJEyX66FcONTH8EIlVuK0yyFA==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/webpack-dev-server/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/webpack-dev-server/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/webpack-dev-server/node_modules/express": { - "version": "4.22.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", - "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "~1.20.3", - "content-disposition": "~0.5.4", - "content-type": "~1.0.4", - "cookie": "~0.7.1", - "cookie-signature": "~1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.3.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "~0.1.12", - "proxy-addr": "~2.0.7", - "qs": "~6.14.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "~0.19.0", - "serve-static": "~1.16.2", - "setprototypeof": "1.2.0", - "statuses": "~2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/webpack-dev-server/node_modules/finalhandler": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.2.tgz", - "integrity": "sha512-aA4RyPcd3badbdABGDuTXCMTtOneUCAYH/gxoYRTZlIJdF0YPWuGqiAsIrhNnnqdXGswYk6dGujem4w80UJFhg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "~2.4.1", - "parseurl": "~1.3.3", - "statuses": "~2.0.2", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/webpack-dev-server/node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-server/node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "dev": true, - "license": "ISC", - "peer": true, - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", - "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } - } - }, - "node_modules/webpack-dev-server/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/webpack-dev-server/node_modules/ipaddr.js": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", - "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/webpack-dev-server/node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-server/node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "dev": true, - "license": "MIT", - "peer": true, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/webpack-dev-server/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-server/node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/webpack-dev-server/node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/webpack-dev-server/node_modules/raw-body": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", - "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "bytes": "~3.1.2", - "http-errors": "~2.0.1", - "iconv-lite": "~0.4.24", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/webpack-dev-server/node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/webpack-dev-server/node_modules/send": { - "version": "0.19.2", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.2.tgz", - "integrity": "sha512-VMbMxbDeehAxpOtWJXlcUS5E8iXh6QmN+BkRX1GARS3wRaXEEgzCcB10gTQazO42tpNIya8xIyNx8fll1OFPrg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "~0.5.2", - "http-errors": "~2.0.1", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "~2.4.1", - "range-parser": "~1.2.1", - "statuses": "~2.0.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/webpack-dev-server/node_modules/serve-static": { - "version": "1.16.3", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.3.tgz", - "integrity": "sha512-x0RTqQel6g5SY7Lg6ZreMmsOzncHFU7nhnRWkKgWuMTu5NN0DR5oruckMqRvacAN9d5w6ARnRBXl9xhDCgfMeA==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "~0.19.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/webpack-dev-server/node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-merge": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", - "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/webpack-merge/node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "dev": true, - "license": "BSD-3-Clause", - "peer": true, - "bin": { - "flat": "cli.js" - } - }, - "node_modules/webpack-sources": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", - "dev": true, - "license": "MIT", - "peer": true, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack-subresource-integrity": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", - "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "typed-assert": "^1.0.8" - }, - "engines": { - "node": ">= 12" - }, - "peerDependencies": { - "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", - "webpack": "^5.12.0" - }, - "peerDependenciesMeta": { - "html-webpack-plugin": { - "optional": true - } - } - }, - "node_modules/webpack/node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/webpack/node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true, - "license": "BSD-2-Clause", - "peer": true, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/webpack/node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true, - "license": "MIT", - "peer": true - }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/whatwg-encoding": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", @@ -25301,14 +20037,6 @@ "node": ">= 8" } }, - "node_modules/wildcard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -25409,23 +20137,6 @@ } } }, - "node_modules/wsl-utils": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", - "integrity": "sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==", - "dev": true, - "license": "MIT", - "peer": true, - "dependencies": { - "is-wsl": "^3.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/xml": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", @@ -25468,7 +20179,7 @@ "version": "2.8.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.2.tgz", "integrity": "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A==", - "devOptional": true, + "dev": true, "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/src/app/accounts/accounts-routing.module.ts b/src/app/accounts/accounts-routing.module.ts index 566c9762f..83efecf30 100644 --- a/src/app/accounts/accounts-routing.module.ts +++ b/src/app/accounts/accounts-routing.module.ts @@ -12,6 +12,8 @@ import { Routes, RouterModule } from '@angular/router'; import { Route } from '../core/route/route.service'; import { AccountsListComponent } from './accounts-list/accounts-list.component'; import { PartyMasterComponent } from '../deposits/party-master/party-master.component'; +import { CreatePartyComponent } from '../deposits/party-master/create-party/create-party.component'; +import { DueForClosureComponent } from '../deposits/party-master/due-for-closure/due-for-closure.component'; import { DailyTransactionEntryComponent } from '../deposits/daily-transaction-entry/daily-transaction-entry.component'; import { DdsReportsComponent } from '../deposits/dds-reports/dds-reports.component'; import { ReportViewerComponent } from '../deposits/dds-reports/report-viewer/report-viewer.component'; @@ -48,8 +50,26 @@ const routes: Routes = [ }, { path: 'dds', - component: PartyMasterComponent, - data: { title: 'Daily Deposit Scheme — Party Master', breadcrumb: 'Party Master' } + data: { title: 'Daily Deposit Scheme — Party Master', breadcrumb: 'Party Master' }, + children: [ + { path: '', component: PartyMasterComponent }, + { + path: 'create', + component: CreatePartyComponent, + data: { title: 'Create Party', breadcrumb: 'Create' } + }, + { + // Ahead of the :partyId routes, so "dueforclosure" is not read as a party id. + path: 'dueforclosure', + component: DueForClosureComponent, + data: { title: 'Accounts Due for Closure', breadcrumb: 'Due for Closure' } + }, + { + path: ':partyId/edit', + component: CreatePartyComponent, + data: { title: 'Edit Party', breadcrumb: 'Edit' } + } + ] }, { path: 'dds-collection', diff --git a/src/app/core/shell/shell.component.html b/src/app/core/shell/shell.component.html index 9a32f5b1f..b91425694 100644 --- a/src/app/core/shell/shell.component.html +++ b/src/app/core/shell/shell.component.html @@ -1048,7 +1048,7 @@ - @@ -1057,7 +1057,7 @@ - - Date - + + @@ -61,75 +130,61 @@ Tran. Type - @for (type of transactionTypes; track type) { - {{ type }} + @for (type of transactionTypes; track type.label) { + + {{ type.label }}@if (!type.supported) { — not available yet} + } - @if (isPartyTran) { + + @if (isPartyTran && agentInfo.name) {
-

Today's Collections

-
- @if (collections.length === 0) { -

No collection rows added yet.

- } + @if (pendingLoading) { +

Reading this agent's entries...

+ } @else if (pendingEntries.length === 0) { +

+ {{ agentInfo.name }} has written nothing down for this date. Nothing to settle. +

+ } @else { + @for (entry of pendingEntries; track entry.id) { +
+ {{ entry.partyCode }} + {{ entry.partyName }} + {{ entry.transactionMode }} + {{ entry.amount | number: '1.2-2' }} +
+ } - @for (row of collections.controls; track row; let i = $index) { -
- - Party Code - - - - TRTP - - @for (mode of transactionModes; track mode) { - {{ mode }} - } - - - - AC Type - - - - A/C No. - - - - Amount - - - - Cheque No - - - - Token No - - - - Description - - - +
+ {{ pendingEntries.length }} {{ pendingEntries.length === 1 ? 'entry' : 'entries' }} + {{ pendingTotal | number: '1.2-2' }}
+ + +

+ Take {{ pendingTotal | number: '1.2-2' }} from {{ agentInfo.name }}, then post. +

}
} @@ -232,8 +287,16 @@ -
diff --git a/src/app/deposits/daily-transaction-entry/daily-transaction-entry.component.scss b/src/app/deposits/daily-transaction-entry/daily-transaction-entry.component.scss index 0b527e5a0..6255bc5fa 100644 --- a/src/app/deposits/daily-transaction-entry/daily-transaction-entry.component.scss +++ b/src/app/deposits/daily-transaction-entry/daily-transaction-entry.component.scss @@ -9,9 +9,11 @@ $primary: #0f766e; .dte-container { - max-width: 1080px; + // Wider than a normal form page: a collection row carries six fields side by side, and a clerk + // keying a round should see the whole of it. + max-width: 1280px; margin: 0 auto; - padding: 24px 16px; + padding: 20px 16px; } .dte-header { @@ -93,6 +95,7 @@ $primary: #0f766e; margin-bottom: 12px; } +// A sensible floor for the fields laid out with flex in .dte-controls, where there is room to spare. .dte-field { min-width: 180px; @@ -101,9 +104,18 @@ $primary: #0f766e; } } +// Inside the collection grid the track decides the width, not the field. Left to its own 180px +// floor a field refuses to shrink into a narrower track and overruns the one beside it. +.dte-collection-row .dte-field { + width: 100%; + min-width: 0; +} + .dte-section { padding: 16px 0; border-top: 1px solid rgb(0 0 0 / 8%); + // A very narrow window scrolls the grid sideways rather than the whole page. + overflow-x: auto; &__header { display: flex; @@ -140,9 +152,21 @@ $primary: #0f766e; gap: 8px 16px; } +/* + * One collection per row. Fixed columns rather than an even split, so the row holds its shape as + * fields come and go - Cheque No and Token No only appear for a non-cash entry - and the party + * picker gets the width its "code · name · daily amount" needs. + */ .dte-collection-row { display: grid; - grid-template-columns: repeat(7, 1fr) auto; + grid-template-columns: + minmax(0, 2.2fr) /* Party */ + minmax(0, 1fr) /* TRTP */ + minmax(0, 1fr) /* AC Type */ + minmax(0, 1.4fr) /* A/C No. */ + minmax(0, 1fr) /* Amount */ + minmax(0, 1.6fr) /* Description */ + auto; /* remove */ gap: 8px 12px; align-items: start; padding: 10px 0; @@ -151,12 +175,27 @@ $primary: #0f766e; &:first-of-type { border-top: none; } + + // Cheque No and Token No push Description and the remove button onto a second line, which is + // what a non-cash entry should look like rather than a row running off the screen. + &:has(.dte-field--instrument) { + grid-template-columns: + minmax(0, 2.2fr) minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.4fr) minmax(0, 1fr) + minmax(0, 1fr) minmax(0, 1fr) minmax(0, 1.6fr) auto; + } } .dte-collection-row .dte-field--wide { grid-column: span 2; } +@media (width <= 1200px) { + .dte-collection-row, + .dte-collection-row:has(.dte-field--instrument) { + grid-template-columns: repeat(3, minmax(0, 1fr)) auto; + } +} + .dte-remove-row { color: #b91c1c; margin-top: 8px; @@ -185,3 +224,172 @@ $primary: #0f766e; grid-template-columns: repeat(2, 1fr); } } + +/* Resolved name under an Agent / Party Code field — the check that the right record was found. */ +.dte-hint--found { + color: #15803d; + font-weight: 500; +} + +.dte-hint--missing { + color: #b91c1c; + font-weight: 500; +} + +/* Marks an agent in the list who is no longer collecting - only offered for settlement. */ +.dte-agent-off { + color: #b45309; + font-size: 12px; + font-style: italic; + margin-left: 6px; +} + +/* The figure the clerk actually verifies against the passbook before saving. */ +.dte-after { + font-weight: 600; + color: #0f766e; +} + +/* What the agent should be handing over for this round. */ +.dte-total { + display: flex; + justify-content: flex-end; + align-items: baseline; + gap: 14px; + padding: 10px 4px 2px; + border-top: 1px solid rgb(0 0 0 / 10%); + font-size: 0.88rem; + color: rgb(0 0 0 / 60%); + + strong { + font-size: 1.1rem; + color: rgb(0 0 0 / 85%); + font-variant-numeric: tabular-nums; + } +} + +/* Marks a transaction type the system does not handle yet, so it reads as pending rather than broken. */ +.dte-soon { + font-size: 0.78rem; + font-style: italic; + color: rgb(0 0 0 / 45%); +} + + +/* + * Marks a party this agent has already collected from today, on the row where it is picked. + * + * Amber, not red: a customer paying twice in a day is legitimate - arrears, or two days at once - + * so this is a second look, not an error. Blocking it would stop real work; saying nothing lets a + * whole round be credited twice. + */ +.dte-hint--again { + color: #b45309; + font-weight: 500; +} + +/* + * Where a party stands against its instalments. Three states rather than one colour, because they + * call for three different things from the clerk: ahead means take nothing, behind means take more + * than the daily amount, level means take the daily amount as usual. + * + * Behind is amber, not red - falling a few days behind is ordinary in daily collection and is the + * agent's job to recover, not an error. Red here would cry wolf on most rows. + */ +.dte-hint--ahead, +.dte-sched--ahead { + color: #15803d; + font-weight: 500; +} + +.dte-hint--behind, +.dte-sched--behind { + color: #b45309; + font-weight: 500; +} + +.dte-hint--level, +.dte-sched--level { + color: rgb(0 0 0 / 55%); +} + +/* In the party panel the schedule line carries more weight than the fields around it. */ +.dte-sched { + font-weight: 500; +} + +/* Says why a party in the list cannot be picked, beside the party rather than in a tooltip. */ +.dte-covered { + color: #15803d; + font-size: 0.8rem; +} + +/* + * An amount that looks like a slip. Red rather than the amber used for arrears: arrears are ordinary + * and the agent's job to recover, while this is a figure that probably was not handed over at all, + * and once saved it cannot be taken back. It still does not block - the clerk decides. + */ +.dte-hint--overpay { + color: #b91c1c; + font-weight: 500; +} + + +/* + The agent's written-down round, as the counter reads it. + + Laid out as lines rather than as the input grid this replaced: there is nothing to fill in here, + only a list to check a bag of cash against, and controls would invite editing something the clerk + has no business editing. +*/ +.dte-pending-row { + display: flex; + align-items: baseline; + gap: 12px; + padding: 10px 2px; + border-bottom: 1px solid rgba(0, 0, 0, 0.08); + + &:last-of-type { + border-bottom: none; + } + + &__code { + font-weight: 600; + min-width: 84px; + } + + &__name { + flex: 1; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + opacity: 0.75; + } + + &__mode { + font-size: 12px; + text-transform: uppercase; + letter-spacing: 0.5px; + opacity: 0.6; + } + + /* Amounts are read down the column and compared, so the digits have to line up. */ + &__amount { + font-variant-numeric: tabular-nums; + font-weight: 600; + min-width: 96px; + text-align: right; + } +} + +/* The one figure the clerk acts on, said as a sentence so it is not read as another column total. */ +.dte-expected { + margin: 14px 2px 0; + font-size: 14px; + line-height: 1.5; + + strong { + font-variant-numeric: tabular-nums; + } +} diff --git a/src/app/deposits/daily-transaction-entry/daily-transaction-entry.component.ts b/src/app/deposits/daily-transaction-entry/daily-transaction-entry.component.ts index 0eb3edb83..92d8bcaaa 100644 --- a/src/app/deposits/daily-transaction-entry/daily-transaction-entry.component.ts +++ b/src/app/deposits/daily-transaction-entry/daily-transaction-entry.component.ts @@ -7,13 +7,20 @@ */ /** Angular Imports */ -import { Component, inject } from '@angular/core'; +import { Component, OnInit, inject } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, UntypedFormArray, Validators } from '@angular/forms'; import { Router } from '@angular/router'; +import { MatDialog } from '@angular/material/dialog'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component'; + +/** Custom Components */ +import { DayCollectionDialogComponent } from './day-collection-dialog/day-collection-dialog.component'; +import { NoticeDialogComponent } from './notice-dialog/notice-dialog.component'; /** Custom Services */ import { OrganizationService } from 'app/organization/organization.service'; +import { AlertService } from 'app/core/alert/alert.service'; import { SettingsService } from 'app/settings/settings.service'; import { Dates } from 'app/core/utils/dates'; @@ -40,10 +47,12 @@ import { Dates } from 'app/core/utils/dates'; styleUrls: ['./daily-transaction-entry.component.scss'], imports: [...STANDALONE_SHARED_IMPORTS] }) -export class DailyTransactionEntryComponent { +export class DailyTransactionEntryComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); private router = inject(Router); private organizationService = inject(OrganizationService); + private alertService = inject(AlertService); + private dialog = inject(MatDialog); private settingsService = inject(SettingsService); private dateUtils = inject(Dates); @@ -53,13 +62,18 @@ export class DailyTransactionEntryComponent { 'Withdraw Deposite': 'WITHDRAW_DEPOSITE' }; + /** + * The legacy screen's six types. Only the three the backend actually distinguishes can be picked - + * the rest are shown but not selectable, so the screen stays honest about what the system does + * today instead of accepting an entry it will reject on save. + */ transactionTypes = [ - 'Party Tran', - 'Agent Tran', - 'Party Payment', - 'OD Entry', - 'OD Payment', - 'Withdraw Deposite' + { label: 'Party Tran', supported: true }, + { label: 'Agent Tran', supported: true }, + { label: 'Withdraw Deposite', supported: true }, + { label: 'Party Payment', supported: false }, + { label: 'OD Entry', supported: false }, + { label: 'OD Payment', supported: false } ]; transactionModes = [ @@ -129,6 +143,263 @@ export class DailyTransactionEntryComponent { currentBalance: 0 }; + agentLookupFailed = false; + + /** Every agent still on the books - picked from a list rather than keyed by id. */ + agents: any[] = []; + + /** + * The picked agent's parties, fetched once when the agent is chosen. Every party code typed into + * the grid is resolved from here, so a clerk keying a round of thirty collections waits on the + * network once rather than thirty times - and a code belonging to another agent's party simply + * will not resolve, which is the check that matters. + */ + agentParties: any[] = []; + + /** + * The party behind the row being worked on, shown in the panel beside the agent's - who this + * collection is for, what they owe daily, and what their balance becomes once it is saved. + */ + partyInfo: any = null; + + /** Which grid row the party panel is showing. */ + activeRow = -1; + + /** + * What has already been keyed for this agent on the chosen date. + * + * The real risk on this screen is not a wrong figure, it is the same collection book being keyed + * twice - an interruption mid-round, and every customer in it gets credited again while the agent + * appears to be holding cash he already handed over. Showing the round back removes the guesswork. + */ + todaysEntries: any[] = []; + + get todaysTotal(): number { + return this.todaysEntries.reduce((sum, entry) => sum + Number(entry.amount || 0), 0); + } + + /** + * What this party has already been collected for on this date, if anything. + * + * Shown on the row itself rather than in a summary above it. A customer paying twice in a day is + * legitimate - arrears, or two days at once - so this never blocks the entry. It only has to be + * where the clerk is already looking at the moment they pick the party, which a panel at the top + * of the screen is not. + */ + alreadyCollected(index: number): number { + const partyCode = String(this.collections.at(index)?.get('partyCode')?.value || ''); + if (!partyCode) { + return 0; + } + return this.todaysEntries + .filter((entry) => String(entry.partyCode) === partyCode) + .reduce((sum, entry) => sum + Number(entry.amount || 0), 0); + } + + /** + * Where a party stands against the instalments it owes, in the words the branch uses. + * + * The running balance says how much has been paid, never whether it was paid on time - a customer + * four days ahead and one eleven days behind can hold the same figure. This is what the clerk needs + * at the moment they pick the party, so it is worked out per row rather than shown in a summary. + * + * A shortfall is split in two because the bank treats the halves differently: what is still inside + * the catch-up window can be collected today, and what has fallen outside it is gone. Putting them + * together would have the clerk asking for money the bank has already written off. + */ + schedule(party: any): { state: 'ahead' | 'behind' | 'level'; text: string } | null { + const daily = Number(party?.dailyDepositAmount || 0); + if (!party || daily <= 0) { + return null; + } + + const advance = Number(party.advanceAmount || 0); + if (advance > 0) { + return { state: 'ahead', text: `${this.money(advance)} already paid ahead` }; + } + + // What to ask for today is the recoverable shortfall exactly as it stands - today's own + // instalment is already inside it, because the schedule counts the first instalment on the + // opening day itself. Adding the daily on top would charge today twice, which on an account + // opened this morning reads as two days owed on its first day. + // + // Instalments past the catch-up window are deliberately left out of both the figure and the + // words - they cannot be collected, so naming them would only have the clerk asking for money + // the bank has already written off, and reading a shortfall the amount does not cover. + const recoverable = Number(party.recoverableAmount || 0); + if (recoverable > 0) { + return { state: 'behind', text: `Take ${this.money(recoverable)} to complete today` }; + } + + return { state: 'level', text: 'Up to date' }; + } + + private money(value: number): string { + return value.toLocaleString('en-IN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); + } + + /** The party on a given collection row, so the row can show where that party stands. */ + partyOfRow(index: number): any { + const partyCode = String(this.collections.at(index)?.get('partyCode')?.value || ''); + return partyCode ? this.agentParties.find((p) => String(p.partyCode) === partyCode) : null; + } + + /** + * Says so plainly when the agent just picked has nobody to collect from. + * + * An empty party list looks identical to a list that has not loaded, and a clerk who reads it as + * the second will sit waiting for names that are never coming. Raised as a dialog rather than a + * line under the field because it means this agent cannot be used for a round at all - there is + * nothing to key, and the clerk should go back and pick another. + */ + private warnIfAgentHasNoParties(): void { + if (this.agentParties.length > 0) { + return; + } + const agentId = this.entryForm.get('agentId')?.value; + const agent = this.agents.find((a: any) => String(a.id) === String(agentId)); + this.dialog.open(NoticeDialogComponent, { + data: { + heading: 'No customers for this agent', + // Named for what is actually missing. The agent's own settlement account is a different + // thing and is usually there - saying "no accounts" sends whoever reads it to check the + // wrong record. + message: + `No customer accounts are assigned to ${agent?.agentCode || 'this agent'}, so there is nothing ` + + 'to collect. Assign customers to this agent first, or choose a different agent.' + }, + width: '420px', + autoFocus: false + }); + } + + /** + * Adds a row for days other than today - paid ahead, or owed from days gone by. + * + * Kept off the ordinary row and put behind its own button, because most collections are simply + * today's instalment: a box asking about days would sit empty on nearly every row, and a stray + * value left in one would charge a customer for days they did not pay for. + * + * The row it adds is filled in and left in the list rather than posted, so it is saved with the + * rest of the round and the clerk sees it beside the others before committing. + */ + addDayBasedRow(mode: 'advance' | 'previous'): void { + const dialogRef = this.dialog.open(DayCollectionDialogComponent, { + data: { mode, parties: this.agentParties }, + width: '460px', + autoFocus: false + }); + + dialogRef.afterClosed().subscribe((picked: any) => { + if (!picked) { + return; + } + this.addCollectionRow(); + const row = this.collections.at(this.collections.length - 1); + row.get('partyCode')?.setValue(picked.partyCode); + this.loadPartyForRow(this.collections.length - 1); + row.get('amount')?.setValue(picked.amount); + row.get('advanceDays')?.setValue(picked.advanceDays ?? ''); + row.get('arrearsDays')?.setValue(picked.arrearsDays ?? ''); + row.get('description')?.setValue( + `${picked.days} ${picked.days === 1 ? 'day' : 'days'} ${mode === 'advance' ? 'in advance' : 'arrears'}` + ); + this.recalculateRowBalance(this.collections.length - 1); + }); + } + + /** + * Questions an amount on an ordinary row that is not the day's instalment. + * + * Paying for other days now has its own buttons, and each works out the amount from the scheme + * itself. What is left in the plain Amount box is one thing: today. Anything else is either a + * mistyped figure - a zero too many turns a hundred rupees into a thousand - or days being keyed by + * hand, which is the very thing the buttons exist to stop. + * + * It warns and does not block. A customer can hand over an odd amount, and the clerk is the one who + * saw them do it; what they need is to be told before saving, because a collection cannot be taken + * back afterwards - it can only be carried into the days that follow. + */ + overpayWarning(index: number): string | null { + const party = this.partyOfRow(index); + const daily = Number(party?.dailyDepositAmount || 0); + const amount = Number(this.collections.at(index)?.get('amount')?.value || 0); + if (!party || daily <= 0 || amount <= 0) { + return null; + } + + // Never raised on a row the Advance or Previous Days button filled in: those figures are the + // scheme's own and the server checks them, so they cannot be the slip this is looking for. + const row = this.collections.at(index); + if (row?.get('advanceDays')?.value || row?.get('arrearsDays')?.value) { + return null; + } + + if (amount === daily) { + return null; + } + if (amount > daily) { + return ( + `${this.money(amount)} is more than the daily ${this.money(daily)}. ` + + 'Use Advance to pay ahead, or Previous Days to clear arrears.' + ); + } + return `${this.money(amount)} is less than the daily ${this.money(daily)}.`; + } + + /** + * Whether this party is already paid ahead far enough that today is covered. + * + * The agent still calls - the round does not skip a door - but there is nothing to collect, so the + * row is closed to entry rather than hidden. A party that vanished from the list would have the + * clerk wondering what had happened to it. + */ + isCoveredByAdvance(party: any): boolean { + return Number(party?.advanceDays || 0) > 0; + } + + /** + * The agent's own written-down collections for the chosen date, and whether they are being read. + * + * Held rather than typed. A Party Tran is no longer something the clerk composes - the agent + * composed it on their round, and this screen shows it back so the cash can be counted against it. + */ + pendingEntries: any[] = []; + pendingLoading = false; + + /** What the agent should be handing over: their own entries, added up. */ + get pendingTotal(): number { + return this.pendingEntries.reduce((sum: number, entry: any) => sum + Number(entry.amount || 0), 0); + } + + /** + * Reads what this agent has outstanding for the date on the form. + * + * Asked again whenever the agent or the date changes, because both pick out a different day's + * round - and a list left over from the previous choice is a list the clerk might settle. + */ + loadPendingEntries(): void { + const agentId = this.entryForm.get('agentId')?.value; + const date = this.entryForm.get('transactionDate')?.value; + if (!agentId || !date) { + this.pendingEntries = []; + return; + } + + this.pendingLoading = true; + const on = this.dateUtils.formatDate(date, 'yyyy-MM-dd'); + this.organizationService.getAgentCollectionDay(agentId, on).subscribe({ + next: (summary: any) => { + this.pendingEntries = (summary?.entries || []).filter((entry: any) => entry.status === 'PENDING'); + this.pendingLoading = false; + }, + error: () => { + this.pendingEntries = []; + this.pendingLoading = false; + } + }); + } + get isPartyTran(): boolean { return this.entryForm.get('transactionType').value === 'Party Tran'; } @@ -146,7 +417,7 @@ export class DailyTransactionEntryComponent { } private buildCollectionRow(): UntypedFormGroup { - return this.formBuilder.group({ + const row = this.formBuilder.group({ partyCode: [ '', Validators.required @@ -164,6 +435,14 @@ export class DailyTransactionEntryComponent { 0, Validators.required ], + /* + * Days taken in advance, when the customer is paying ahead rather than for today. Empty on an + * ordinary row. The amount follows from it - the scheme's daily instalment times the days - so + * the clerk picks days, not rupees, and cannot quote a figure the scheme does not charge. + */ + advanceDays: [''], + /* Days of arrears being cleared. Same idea as advanceDays, the other way round. */ + arrearsDays: [''], chequeNo: [''], tokenNo: [''], description: [''], @@ -180,6 +459,17 @@ export class DailyTransactionEntryComponent { } ] }); + + // Cash has no instrument behind it, so switching back to cash drops anything typed against a + // cheque or clearing entry rather than saving a stale number with it. + row.get('transactionMode')?.valueChanges.subscribe((mode) => { + if (mode === 'Cash') { + row.get('chequeNo')?.setValue('', { emitEvent: false }); + row.get('tokenNo')?.setValue('', { emitEvent: false }); + } + }); + + return row; } addCollectionRow(): void { @@ -190,28 +480,163 @@ export class DailyTransactionEntryComponent { this.collections.removeAt(index); } - loadAgent(): void { - const agentId = this.entryForm.get('agentId')?.value; - if (!agentId) { - return; - } - this.organizationService.getAgent(agentId).subscribe((agent: any) => { - const guarantors = agent.guarantors || []; - this.agentInfo = { - name: agent.agentNameId || '', - address: agent.agentAddress1 || '', - city: agent.agentCity || '', - accountNo: agent.agentSavingAccountNo || '', - guarantor1: guarantors[0]?.guarantorName || '', - guarantor2: guarantors[1]?.guarantorName || '', - currentBalance: agent.agentCurrentBalance || 0 - }; + ngOnInit(): void { + // Closed agents are off the books entirely; everyone else is kept, and which of them can be + // picked depends on the transaction type - see selectableAgents. + this.organizationService.getAgents().subscribe({ + next: (agents: any[]) => (this.agents = (agents || []).filter((agent: any) => !agent.closeDate)), + error: () => (this.agents = []) }); } + /** + * The agents that can be picked for the transaction type currently chosen. + * + * A deactivated agent no longer collects, so they are not offered for a collection or a transfer. + * Settlement is the exception: an agent switched off mid-round is still holding the cash they + * already took, and that cash has to be able to reach a branch. Leaving them out of the settlement + * list would strand it with no way to hand it in. + */ + get selectableAgents(): any[] { + if (this.isAgentTran) { + return this.agents; + } + return this.agents.filter((agent: any) => agent.active); + } + + /** Flags a picked agent that is no longer collecting, so the entry is not made by mistake. */ + get pickedAgentIsInactive(): boolean { + const agentId = this.entryForm.get('agentId')?.value; + if (!agentId) { + return false; + } + const picked = this.agents.find((agent: any) => agent.id === agentId); + return !!picked && !picked.active; + } + + /** Whether this row needs a cheque or token number - a cash collection has neither. */ + needsInstrument(index: number): boolean { + return this.collections.at(index)?.get('transactionMode')?.value !== 'Cash'; + } + + loadAgent(): void { + const agentId = this.entryForm.get('agentId')?.value; + this.agentLookupFailed = false; + this.partyInfo = null; + this.activeRow = -1; + if (!agentId) { + this.agentParties = []; + this.pendingEntries = []; + return; + } + + this.organizationService.getAgent(agentId).subscribe({ + next: (agent: any) => { + const guarantors = agent.guarantors || []; + this.agentInfo = { + name: agent.agentNameId || '', + address: agent.agentAddress1 || '', + city: agent.agentCity || '', + accountNo: agent.agentSavingAccountNo || '', + guarantor1: guarantors[0]?.guarantorName || '', + guarantor2: guarantors[1]?.guarantorName || '', + currentBalance: agent.agentCurrentBalance || 0 + }; + }, + error: () => { + this.agentLookupFailed = true; + this.agentInfo = { name: '', address: '', city: '', accountNo: '', guarantor1: '', guarantor2: '', currentBalance: 0 }; + } + }); + + this.organizationService.getPartiesOfAgent(agentId).subscribe({ + next: (parties: any[]) => { + this.agentParties = parties || []; + this.warnIfAgentHasNoParties(); + }, + error: () => (this.agentParties = []) + }); + + this.loadTodaysEntries(); + this.loadPendingEntries(); + } + + /** This agent's collections already recorded for the date on the screen. */ + loadTodaysEntries(): void { + const agentId = this.entryForm.get('agentId')?.value; + if (!agentId) { + this.todaysEntries = []; + return; + } + const onDate = this.dateUtils.formatDate(this.entryForm.get('transactionDate')?.value, 'yyyy-MM-dd'); + + this.organizationService.getPartyTransactionsOfAgent(agentId).subscribe({ + next: (transactions: any[]) => { + this.todaysEntries = (transactions || []).filter( + (transaction: any) => + transaction.transactionType === 'PARTY_TRAN' + && this.dateUtils.formatDate(transaction.transactionDate, 'yyyy-MM-dd') === onDate + ); + }, + error: () => (this.todaysEntries = []) + }); + } + + /** + * Resolves the party code typed into a row. A code this agent does not collect for is left + * unresolved rather than fetched anyway - an agent posting against someone else's party is the + * mistake this screen most needs to catch. + */ + loadPartyForRow(index: number): void { + const row = this.collections.at(index); + if (!row) { + return; + } + const partyCode = String(row.get('partyCode')?.value || '').trim(); + this.activeRow = index; + + const party = this.agentParties.find((candidate) => String(candidate.partyCode) === partyCode); + this.partyInfo = party || null; + + row.get('partyName')?.setValue(party?.partyName || '', { emitEvent: false }); + // The account credited is the party's own linked savings account, never keyed by hand. + row.get('accountNo')?.setValue(party?.creditToAccount || party?.partyCode || '', { emitEvent: false }); + row.get('accountType')?.setValue(party?.creditToAccountType || 'Savings', { emitEvent: false }); + + this.recalculateRowBalance(index); + } + + /** The party's balance once this row is saved - what the legacy screen called Bal. After This Tr. */ + recalculateRowBalance(index: number): void { + const row = this.collections.at(index); + if (!row) { + return; + } + const party = this.agentParties.find( + (candidate) => String(candidate.partyCode) === String(row.get('partyCode')?.value || '').trim() + ); + const current = Number(party?.currentBalance || 0); + const amount = Number(row.get('amount')?.value || 0); + row.get('balanceAfterTransaction')?.setValue(current + amount, { emitEvent: false }); + } + + /** The balance the party panel shows - live against whatever the active row currently holds. */ + get partyBalanceAfter(): number { + if (!this.partyInfo || this.activeRow < 0) { + return 0; + } + const row = this.collections.at(this.activeRow); + return Number(this.partyInfo.currentBalance || 0) + Number(row?.get('amount')?.value || 0); + } + + /** Total of the round being keyed in - what the agent should be handing over. */ + get collectionTotal(): number { + return this.collections.controls.reduce((sum, row) => sum + Number(row.get('amount')?.value || 0), 0); + } + + /** Re-reads what has already been keyed for this agent on the chosen date. */ retrieve(): void { - // No "my assigned parties" read API exists yet — this will pre-fill - // the collections grid with today's assigned parties once available. + this.loadTodaysEntries(); } viewLedger(): void { @@ -231,11 +656,65 @@ export class DailyTransactionEntryComponent { } exit(): void { - this.router.navigate(['/accounts/savings']); + this.router.navigate(['/accounts/dds']); } + /** + * Save, asking first when what is about to happen is a settlement. + * + * A settlement credits every customer on the list and records the cash as the agent's, in one + * act that cannot be taken back - so the screen says the figure out loud and waits. The question + * is the one the clerk is actually answering at the counter: is this much really in front of me. + * + * The double-entry guard this used to carry has moved to where the entries are now made. Two + * lines for one account on one day are refused on the agent's own screen, and refused again by + * the server when the day is settled; by the time a clerk sees the list there is nothing left to + * warn about. + */ save(): void { + if (!this.isPartyTran) { + this.postTransaction(); + return; + } + + if (this.pendingEntries.length === 0) { + return; + } + + const money = Number(this.pendingTotal).toLocaleString('en-IN', { minimumFractionDigits: 2 }); + const count = this.pendingEntries.length; + + this.dialog + .open(ConfirmationDialogComponent, { + data: { + type: 'warn', + heading: 'Settle these collections', + dialogContext: + `Take ${money} from ${this.agentInfo.name || 'the agent'} and post ` + + `${count} ${count === 1 ? 'collection' : 'collections'}? ` + + 'Every customer on the list is credited and the cash is recorded against the agent. ' + + 'This cannot be undone.' + } + }) + .afterClosed() + .subscribe((response: any) => { + if (response?.confirm) { + this.postTransaction(); + } + }); + } + + private postTransaction(): void { const raw = this.entryForm.getRawValue(); + + // A Party Tran is no longer a transaction this screen composes. The agent wrote the round down + // on their own; what happens here is that it is settled, all of it at once, through a call of + // its own. Everything else on this screen still posts a transaction the way it always did. + if (this.isPartyTran) { + this.settleCollections(); + return; + } + const dateFormat = this.settingsService.dateFormat; const locale = this.settingsService.language.code; const transactionType = DailyTransactionEntryComponent.TRANSACTION_TYPE_CODES[raw.transactionType]; @@ -248,18 +727,7 @@ export class DailyTransactionEntryComponent { locale }; - if (this.isPartyTran) { - payload.collections = (raw.collections || []).map((row: any) => ({ - partyCode: row.partyCode, - transactionMode: row.transactionMode, - accountType: row.accountType || undefined, - accountNo: row.accountNo, - amount: row.amount, - chequeNo: row.chequeNo || undefined, - tokenNo: row.tokenNo || undefined, - description: row.description || undefined - })); - } else if (this.isWithdrawDeposite) { + if (this.isWithdrawDeposite) { payload.creditorPartyCode = raw.creditorPartyCode; payload.debitPartyCode = raw.debitPartyCode; payload.withdrawAmount = raw.withdrawAmount; @@ -272,8 +740,19 @@ export class DailyTransactionEntryComponent { payload.description = raw.settlementDescription || undefined; } + // What was just posted, captured before the form is cleared, so the confirmation can name it. + const posted = this.postedSummary(raw); + this.organizationService.createPartyTransaction(payload).subscribe(() => { + // On a money screen a silent save is worse than no save: with nothing to confirm it, a clerk + // who looks away reasonably assumes it did not go through and posts the whole round again. + this.alertService.alert({ type: 'Success', message: posted }); + this.collections.clear(); + this.partyInfo = null; + this.activeRow = -1; + // The round just saved becomes part of what the screen shows back. + this.loadTodaysEntries(); this.entryForm.patchValue({ creditorPartyCode: '', debitPartyCode: '', @@ -287,4 +766,66 @@ export class DailyTransactionEntryComponent { this.loadAgent(); }); } + + /** + * Hands the agent's day over: every entry they wrote down for this date becomes a real credit. + * + * No amount is sent and none is asked for on screen. What settles is what the entries say - a + * figure typed at the counter would be a second opinion about a total the entries already give, + * and the two could differ. The clerk's part is to count the cash against that total and agree. + * + * All or none. A part-settled round would leave the branch holding cash against a list that no + * longer matched it, which is a worse position than simply running it again. + */ + private settleCollections(): void { + const raw = this.entryForm.getRawValue(); + if (this.pendingEntries.length === 0) { + return; + } + + const count = this.pendingEntries.length; + const total = this.pendingTotal; + const agent = this.agentInfo.name || 'agent'; + + this.organizationService + .settleAgentCollections({ + agentId: Number(raw.agentId), + entryDate: this.dateUtils.formatDate(raw.transactionDate, 'yyyy-MM-dd'), + dateFormat: 'yyyy-MM-dd', + locale: this.settingsService.language.code + }) + .subscribe(() => { + const money = Number(total).toLocaleString('en-IN', { minimumFractionDigits: 2 }); + this.alertService.alert({ + type: 'Success', + message: count + (count === 1 ? ' collection' : ' collections') + ' posted for ' + agent + ' - ' + money + }); + + // The list empties because those entries are settled now, and the agent's cash goes up by + // what was just taken - both are read again rather than worked out on screen. + this.loadPendingEntries(); + this.loadTodaysEntries(); + this.loadAgent(); + }); + } + + /** + * Says what was actually posted, in the terms the clerk was working in - a count and a total for a + * round of collections, the amount and who it came from for anything else. "Saved successfully" + * would confirm that something happened without confirming that the right thing happened. + */ + private postedSummary(raw: any): string { + const money = (value: any) => Number(value || 0).toLocaleString('en-IN', { minimumFractionDigits: 2 }); + const agent = this.agentInfo.name || 'agent'; + + if (this.isPartyTran) { + const rows = raw.collections || []; + const total = rows.reduce((sum: number, row: any) => sum + Number(row.amount || 0), 0); + return `${rows.length} ${rows.length === 1 ? 'collection' : 'collections'} posted for ${agent} — ${money(total)}`; + } + if (this.isAgentTran) { + return `${agent} settled ${money(raw.settlementAmount)} to the branch`; + } + return `${money(raw.withdrawAmount)} transferred from ${raw.debitPartyCode} to ${raw.creditorPartyCode}`; + } } diff --git a/src/app/deposits/daily-transaction-entry/day-collection-dialog/day-collection-dialog.component.html b/src/app/deposits/daily-transaction-entry/day-collection-dialog/day-collection-dialog.component.html new file mode 100644 index 000000000..553aeae7a --- /dev/null +++ b/src/app/deposits/daily-transaction-entry/day-collection-dialog/day-collection-dialog.component.html @@ -0,0 +1,58 @@ + + +

{{ title }}

+ + + + Party + + @for (p of eligibleParties; track p.id) { + + {{ p.partyCode }} · {{ p.partyName }} · Daily {{ p.dailyDepositAmount | number: '1.0-0' }} + + } + + @if (eligibleParties.length === 0) { + + No party has recoverable days + } + + + + Days + + @for (d of dayOptions; track d) { + {{ d }} {{ d === 1 ? 'day' : 'days' }} + } + + @if (isAdvance) { + Up to 5 days + } @else if (partyCode) { + Only days still within the catch-up window + } + + + + @if (amount > 0) { +
+ {{ amount | number: '1.2-2' }} + + {{ days }} × {{ party?.dailyDepositAmount | number: '1.2-2' }} daily + +
+ } +
+ + + + + diff --git a/src/app/deposits/daily-transaction-entry/day-collection-dialog/day-collection-dialog.component.scss b/src/app/deposits/daily-transaction-entry/day-collection-dialog/day-collection-dialog.component.scss new file mode 100644 index 000000000..3f31a5163 --- /dev/null +++ b/src/app/deposits/daily-transaction-entry/day-collection-dialog/day-collection-dialog.component.scss @@ -0,0 +1,42 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +.dcd-field { + width: 100%; +} + +.dcd-none { + color: #b45309; +} + +/* + * The figure the clerk quotes at the counter. Larger than the fields above it because it is the one + * thing the customer is told, and it is shown with its basis so the clerk can say where it came from + * rather than just naming a number. + */ +.dcd-amount { + display: flex; + align-items: baseline; + gap: 10px; + margin-top: 6px; + padding: 12px 14px; + border-radius: 6px; + background: rgb(15 118 110 / 8%); + + &__figure { + font-size: 1.3rem; + font-weight: 600; + color: #0f766e; + font-variant-numeric: tabular-nums; + } + + &__basis { + font-size: 0.8rem; + color: rgb(0 0 0 / 60%); + } +} diff --git a/src/app/deposits/daily-transaction-entry/day-collection-dialog/day-collection-dialog.component.ts b/src/app/deposits/daily-transaction-entry/day-collection-dialog/day-collection-dialog.component.ts new file mode 100644 index 000000000..cc3951f62 --- /dev/null +++ b/src/app/deposits/daily-transaction-entry/day-collection-dialog/day-collection-dialog.component.ts @@ -0,0 +1,117 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** Angular Imports */ +import { Component, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { MatSelectModule } from '@angular/material/select'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; + +/** + * Collecting for days other than today - either paid ahead, or owed from days gone by. + * + * Both are the same act from the counter's side: the customer is paying for a number of days, and + * what that comes to is the scheme's business, not the clerk's. So the clerk picks days and the + * amount follows. Typing an amount instead would let the daily figure be worked around a little at a + * time, and would quote the customer something the scheme does not charge. + * + * The two differ only in how many days are on offer. Paying ahead is capped by the bank's allowance. + * Paying arrears is capped by what is still inside the catch-up window - older instalments are gone, + * and offering them would have the clerk asking for money the bank has already written off. + */ +@Component({ + selector: 'mifosx-day-collection-dialog', + standalone: true, + imports: [ + CommonModule, + FormsModule, + MatDialogModule, + MatButtonModule, + MatSelectModule, + MatFormFieldModule, + MatInputModule + ], + templateUrl: './day-collection-dialog.component.html', + styleUrls: ['./day-collection-dialog.component.scss'] +}) +export class DayCollectionDialogComponent { + private dialogRef = inject(MatDialogRef); + /** { mode: 'advance' | 'previous', parties: [] } - the agent's parties, as the screen already has them. */ + data = inject(MAT_DIALOG_DATA); + + partyCode = ''; + days: number | '' = ''; + + get isAdvance(): boolean { + return this.data?.mode === 'advance'; + } + + get title(): string { + return this.isAdvance ? 'Collect in advance' : 'Collect previous days'; + } + + get party(): any { + return (this.data?.parties || []).find((p: any) => String(p.partyCode) === this.partyCode) || null; + } + + /** + * The parties this can be used on. + * + * Paying ahead is offered to anyone. Paying arrears is offered only where there are arrears still + * inside the window - on an account that is up to date there is nothing to collect for past days, + * and listing it would invite an entry that is really an advance in disguise. + */ + get eligibleParties(): any[] { + const parties = this.data?.parties || []; + return this.isAdvance ? parties : parties.filter((p: any) => Number(p.recoverableAmount || 0) > 0); + } + + /** + * How many days may be picked. Five ahead, matching the bank's allowance and the server's own + * check; for arrears, only what has not yet fallen out of the catch-up window. + */ + get dayOptions(): number[] { + const daily = Number(this.party?.dailyDepositAmount || 0); + if (!this.party || daily <= 0) { + return []; + } + const limit = this.isAdvance + ? 5 + : Math.floor(Number(this.party.recoverableAmount || 0) / daily); + return Array.from({ length: Math.max(0, limit) }, (_, index) => index + 1); + } + + get amount(): number { + return Number(this.party?.dailyDepositAmount || 0) * Number(this.days || 0); + } + + /** Clearing the days when the party changes - a count valid for one account is not for another. */ + partyChanged(): void { + this.days = ''; + } + + get canAdd(): boolean { + return !!this.partyCode && Number(this.days) > 0 && this.amount > 0; + } + + add(): void { + this.dialogRef.close({ + partyCode: this.partyCode, + days: Number(this.days), + amount: this.amount, + // Sent as days, not just an amount: the server recomputes what they come to and checks it + // against this account, so the figure quoted here is the one that gets posted. + advanceDays: this.isAdvance ? Number(this.days) : undefined, + arrearsDays: this.isAdvance ? undefined : Number(this.days) + }); + } +} diff --git a/src/app/deposits/daily-transaction-entry/notice-dialog/notice-dialog.component.html b/src/app/deposits/daily-transaction-entry/notice-dialog/notice-dialog.component.html new file mode 100644 index 000000000..ee905a5bc --- /dev/null +++ b/src/app/deposits/daily-transaction-entry/notice-dialog/notice-dialog.component.html @@ -0,0 +1,21 @@ + + +
+ +

{{ data.heading }}

+
+ + +

{{ data.message }}

+
+ + + + + diff --git a/src/app/deposits/daily-transaction-entry/notice-dialog/notice-dialog.component.scss b/src/app/deposits/daily-transaction-entry/notice-dialog/notice-dialog.component.scss new file mode 100644 index 000000000..1962f43c2 --- /dev/null +++ b/src/app/deposits/daily-transaction-entry/notice-dialog/notice-dialog.component.scss @@ -0,0 +1,44 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +.nd-head { + display: flex; + align-items: center; + gap: 12px; + padding: 18px 24px 4px; + + /* + * Teal rather than the amber or red the platform's other dialogs use. Nothing has gone wrong here + * and nothing is at risk - the clerk is being told a fact about the agent they picked, and a + * warning colour would send them looking for a fault that is not there. + */ + &__ic { + display: grid; + place-items: center; + width: 34px; + height: 34px; + border-radius: 8px; + background: rgb(15 118 110 / 10%); + color: #0f766e; + flex: none; + } + + &__title { + margin: 0; + font-size: 1.05rem; + font-weight: 600; + } +} + +.nd-message { + margin: 0; + max-width: 44ch; + font-size: 0.9rem; + line-height: 1.55; + color: rgb(0 0 0 / 70%); +} diff --git a/src/app/deposits/daily-transaction-entry/notice-dialog/notice-dialog.component.ts b/src/app/deposits/daily-transaction-entry/notice-dialog/notice-dialog.component.ts new file mode 100644 index 000000000..906d5b209 --- /dev/null +++ b/src/app/deposits/daily-transaction-entry/notice-dialog/notice-dialog.component.ts @@ -0,0 +1,39 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** Angular Imports */ +import { Component, inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; + +/** + * Something the clerk needs told, with nothing to decide. + * + * The platform's confirmation dialog asks a question - Cancel or Confirm - and its buttons imply a + * choice. Used for a plain statement it leaves the clerk hunting for what they are agreeing to, and + * "Cancel" reads as though the thing being described could be undone. The error dialog has the right + * single button but dresses the message in red, which turns "this agent has no parties" into a fault. + * + * So: one button, and the message stands on its own. + */ +@Component({ + selector: 'mifosx-notice-dialog', + standalone: true, + imports: [ + MatDialogModule, + MatButtonModule, + FaIconComponent + ], + templateUrl: './notice-dialog.component.html', + styleUrls: ['./notice-dialog.component.scss'] +}) +export class NoticeDialogComponent { + /** { heading, message } */ + data = inject(MAT_DIALOG_DATA); +} diff --git a/src/app/deposits/dds-reports/report-viewer/report-viewer.component.html b/src/app/deposits/dds-reports/report-viewer/report-viewer.component.html index 4b7601dc4..a61caf5f9 100644 --- a/src/app/deposits/dds-reports/report-viewer/report-viewer.component.html +++ b/src/app/deposits/dds-reports/report-viewer/report-viewer.component.html @@ -16,46 +16,59 @@
-
- - From Date - - - - + + @if (!hasNoFilters) { +
+ @if (usesDate) { + + {{ dateLabel }} — from + + + + - - To Date - - - - + + {{ dateLabel }} — to + + + + + } - - Agent From - - + @if (usesAgent) { + + Agent From + + - - Agent To - - + + Agent To + + + } - - Party From - - + @if (usesParty) { + + Party From + + - - Party To - - -
+ + Party To + + + } +
+ }
+
@@ -66,25 +79,66 @@ } @else if (rows.length === 0) {

No rows matched these filters.

} @else { -
- - - - @for (column of columns; track column) { - - } - - - - @for (row of rows; track row) { + +
+
+

{{ reportName }}

+ @if (usesDate) { +

+ {{ dateLabel | lowercase }} {{ ranFromDate | dateFormat }} and + {{ ranToDate | dateFormat }} +

+ } @else { +

as on {{ ranOnDate | dateFormat }}

+ } + +
+ +
+
{{ column }}
+ + @for (column of columns; track column) { - + } + + + @for (row of rows; track $index) { + + + @for (column of columns; track column) { + + } + + } + + @if (hasTotalledColumn) { + + + + @for (column of columns; track column; let first = $first) { + + } + + } - -
#{{ row[column] }}{{ column }}
{{ $index + 1 }}{{ cell(row, column) }}
+ @if (first) { + Total : + } @else if (isTotalledColumn(column)) { + {{ columnTotal(column) }} + } +
+ +
+ +

+ {{ rows.length }} {{ rows.length === 1 ? 'row' : 'rows' }} +

} diff --git a/src/app/deposits/dds-reports/report-viewer/report-viewer.component.scss b/src/app/deposits/dds-reports/report-viewer/report-viewer.component.scss index 454eb7c33..5fd19d364 100644 --- a/src/app/deposits/dds-reports/report-viewer/report-viewer.component.scss +++ b/src/app/deposits/dds-reports/report-viewer/report-viewer.component.scss @@ -94,6 +94,93 @@ $primary: #0f766e; padding: 8px 10px; border-bottom: 1px solid rgb(0 0 0 / 6%); } + + /* Figures line up column-wise so a wrong order of magnitude is visible at a glance. */ + .rv-num { + text-align: right; + font-variant-numeric: tabular-nums; + } + + /* The row number down the left, as the printed statement carried it. */ + .rv-sr { + width: 1%; + color: rgb(0 0 0 / 45%); + font-style: italic; + font-variant-numeric: tabular-nums; + } + + tfoot td { + border-bottom: none; + border-top: 2px solid rgb(0 0 0 / 20%); + font-weight: 600; + padding-top: 10px; + } +} + +/* ---- the statement block: heading, table, footer count ---- */ + +.rv-action-bar i { + margin-right: 6px; +} + +.rv-statement__head { + display: flex; + align-items: baseline; + flex-wrap: wrap; + gap: 4px 12px; + margin-bottom: 14px; +} + +.rv-statement__title { + margin: 0; + font-size: 17px; + font-weight: 600; + flex: 0 0 auto; +} + +.rv-statement__period { + margin: 0; + font-size: 13px; + color: rgb(0 0 0 / 55%); +} + +.rv-print { + margin-left: auto; + + i { + margin-right: 6px; + } +} + +.rv-statement__count { + margin: 10px 0 0; + font-size: 12px; + color: rgb(0 0 0 / 45%); + text-align: right; +} + +/* Printing a statement should give the statement, not the screen around it. */ +@media print { + .rv-header, + .rv-grid, + .rv-action-bar, + .rv-print { + display: none !important; + } + + .rv-card { + box-shadow: none; + padding: 0; + } + + .rv-table-scroll { + overflow-x: visible; + } + + .rv-table { + white-space: normal; + font-size: 11px; + } } @media (width <= 900px) { diff --git a/src/app/deposits/dds-reports/report-viewer/report-viewer.component.ts b/src/app/deposits/dds-reports/report-viewer/report-viewer.component.ts index 2ab8c9985..30732c80c 100644 --- a/src/app/deposits/dds-reports/report-viewer/report-viewer.component.ts +++ b/src/app/deposits/dds-reports/report-viewer/report-viewer.component.ts @@ -7,7 +7,7 @@ */ /** Angular Imports */ -import { Component, inject } from '@angular/core'; +import { Component, OnInit, inject } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @@ -37,7 +37,7 @@ import { Dates } from 'app/core/utils/dates'; styleUrls: ['./report-viewer.component.scss'], imports: [...STANDALONE_SHARED_IMPORTS] }) -export class ReportViewerComponent { +export class ReportViewerComponent implements OnInit { private route = inject(ActivatedRoute); private router = inject(Router); private formBuilder = inject(UntypedFormBuilder); @@ -66,6 +66,41 @@ export class ReportViewerComponent { 'Agent Commission Statement': 'AGENT_COMMISSION_STATEMENT' }; + /** + * Which filters each report actually uses, and what its date range means. + * + * Every report used to be offered the same six filters whether it read them or not — so a date + * range could be typed on the Agent Check List, which has no date in its query at all, and the + * same rows would come back however it was set. A filter that changes nothing is worse than no + * filter: it reads as a report that ignores what it was asked. + * + * Kept in step with the backend by hand; the source of truth is the switch in + * `DdsReportReadPlatformServiceImpl.runReport`. + */ + private static readonly REPORT_FILTERS: Record = { + AGENT_CHECK_LIST: {}, + PARTY_CHECK_LIST: { agent: true, party: true }, + PARTY_BALANCE: { agent: true, party: true }, + AGENT_WISE_BALANCE: { agent: true }, + BALANCE_SUMMARY: { agent: true, party: true }, + MASTER_DATA_EXPORT: { agent: true, party: true }, + + AGENT_WISE_MONTHLY_COLLECTION: { date: 'Collected between', agent: true }, + COLLECTION_SUMMARY_AGENT_WISE: { date: 'Collected between', agent: true }, + DAILY_COLLECTION_REPORT: { date: 'Collected between', agent: true }, + AGENT_PARTYWISE_DAILY_COLLECTION: { date: 'Collected between', agent: true, party: true }, + SUPPLEMENTARY_DDS: { date: 'Collected between', agent: true, party: true }, + + COMMISSION_REGISTER: { date: 'Posted between', agent: true }, + AGENT_COMMISSION_STATEMENT: { date: 'Posted between' }, + PARTY_INTEREST_REGISTER: { date: 'Posted between', party: true }, + INTEREST_CERTIFICATE: { date: 'Interest earned between', agent: true, party: true }, + + TRANSACTION_REGISTER: { date: 'Transacted between', agent: true, party: true }, + CLOSE_PARTY_AC_REGISTER: { date: 'Closed between', agent: true, party: true }, + MATURE_DATE_WISE: { date: 'Maturing between', agent: true, party: true } + }; + reportName: string = this.route.snapshot.paramMap.get('reportName') || 'Report'; hasRun = false; @@ -74,21 +109,198 @@ export class ReportViewerComponent { columns: string[] = []; rows: Array> = []; + /** The range the rows on screen were actually fetched for — not whatever the filter says now. */ + ranFromDate: Date | null = null; + ranToDate: Date | null = null; + + /** When a report carries no date range, the moment it was run is what dates its figures. */ + ranOnDate: Date | null = null; + + private static readonly MONEY = new Intl.NumberFormat('en-IN', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }); + + private static readonly DATE = new Intl.DateTimeFormat('en-GB', { + day: '2-digit', + month: '2-digit', + year: 'numeric' + }); + + /** + * One cell, rendered the way a printed statement shows it. + * + * Dates arrive from the platform as `[yyyy, m, d]` and numbers as raw decimals, so printing them + * as they come gives "2026,9,2" and "10052.6" — unreadable on a statement someone checks figures + * against. Amounts get Indian grouping and two decimals; identifiers that merely look numeric + * (account numbers, codes) are left alone, since grouping them would corrupt them. + */ + cell(row: Record, column: string): string { + const value = row[column]; + + if (value === null || value === undefined || value === '') { + return '—'; + } + if (Array.isArray(value) && value.length >= 3) { + return ReportViewerComponent.DATE.format(new Date(value[0], value[1] - 1, value[2])); + } + if (this.isAmountColumn(column) && typeof value === 'number') { + return ReportViewerComponent.MONEY.format(value); + } + return String(value); + } + + /** + * Whether a column holds money, and so should be right-aligned with two decimals. + * + * Decided from the column's name rather than its values: a count column and an account-number + * column are both numbers, and neither reads as an amount. + */ + isAmountColumn(column: string): boolean { + const name = column.toLowerCase(); + + // Ruled out first, because several of these also contain a money word: a rate is a percentage, + // "Total Accounts" is a count that happens to say total, and an account number is an identifier + // that would be corrupted by grouping. + // + // "agents" is matched exactly rather than as a substring: the two count columns are named + // "Total Agents" and "Active Agents", but "Cash With Agents" and "Commission Due To Agents" + // are money, and a substring test swept those two up and printed them ungrouped. + // + // A "type" column names a kind, never a quantity — "Interest Type" holds the word "Interest", + // which is enough for the money test below to claim it and put a meaningless total under it. + if ( + name.includes('a/c no') || + name.includes('code') || + name.includes('rate') || + name.includes('%') || + name.includes('count') || + name.includes('entries') || + name.includes('accounts') || + name === 'total agents' || + name === 'active agents' || + name.includes('type') || + name.includes('pan') + ) { + return false; + } + + return ( + name.includes('amount') || + name.includes('balance') || + name.includes('collect') || + name.includes('commission') || + name.includes('deduction') || + name.includes('deposit') || + name.includes('advance') || + name.includes('interest') || + name.includes('tds') || + name.includes('total') || + name.includes('diff') || + name.includes('cash') || + name.includes('debit') || + name.includes('credit') || + name.includes('settled') || + name.includes('shortfall') || + name.includes('due') + ); + } + + /** + * Whether a total under this column would mean anything. + * + * Not every amount is worth adding up. A "Balance After" on a transaction register is the running + * balance *at that moment*, so its column holds several snapshots of the same account at different + * times — summing them produces a figure that corresponds to nothing, and a reader would sooner or + * later try to reconcile it against something. + * + * A plain balance on a listing report is different: there each row is one account's position at + * one moment, and the sum is the real "total held", which is exactly what a branch wants to see. + */ + isTotalledColumn(column: string): boolean { + if (!this.isAmountColumn(column)) { + return false; + } + return !column.toLowerCase().includes('balance after'); + } + + /** The figure printed under a column, as the legacy statement's "Total :" row does. */ + columnTotal(column: string): string { + const sum = this.rows.reduce((running, row) => { + const value = row[column]; + return running + (typeof value === 'number' ? value : 0); + }, 0); + return ReportViewerComponent.MONEY.format(sum); + } + + /** + * Whether to print a Total row at all. + * + * A single-row report is already its own total - Balance Summary is one line of figures for the + * whole scheme, and repeating it underneath itself only invites the reader to look for a difference + * between the two. + */ + get hasTotalledColumn(): boolean { + return this.rows.length > 1 && this.columns.some((column) => this.isTotalledColumn(column)); + } + + print(): void { + window.print(); + } + filterForm: UntypedFormGroup = this.formBuilder.group({ - fromDate: [ - '', - Validators.required - ], - toDate: [ - '', - Validators.required - ], + fromDate: [''], + toDate: [''], agentFrom: [1], agentTo: [99999], partyFrom: [1], partyTo: [99999999] }); + private get filters(): { date?: string; agent?: boolean; party?: boolean } { + return ReportViewerComponent.REPORT_FILTERS[ReportViewerComponent.REPORT_KEYS[this.reportName]] ?? {}; + } + + /** True when this report's query actually reads a date range - only then is one asked for. */ + get usesDate(): boolean { + return !!this.filters.date; + } + + /** What this report's date range means, e.g. "Posted between" rather than a bare "From Date". */ + get dateLabel(): string { + return this.filters.date ?? ''; + } + + get usesAgent(): boolean { + return !!this.filters.agent; + } + + get usesParty(): boolean { + return !!this.filters.party; + } + + /** True when this report takes no filters at all, so there is nothing for anyone to fill in. */ + get hasNoFilters(): boolean { + return !this.usesDate && !this.usesAgent && !this.usesParty; + } + + ngOnInit(): void { + // A date is required exactly for the reports that read one, so a report without dates can be run + // straight away instead of demanding two values it will ignore. + if (this.usesDate) { + this.filterForm.get('fromDate')?.setValidators([Validators.required]); + this.filterForm.get('toDate')?.setValidators([Validators.required]); + this.filterForm.get('fromDate')?.updateValueAndValidity(); + this.filterForm.get('toDate')?.updateValueAndValidity(); + } + + // Nothing to narrow it by means nothing to decide, so the report simply opens showing its + // figures. Making someone press Run on an empty form asks a question with only one answer. + if (this.hasNoFilters) { + this.runReport(); + } + } + runReport(): void { const reportKey = ReportViewerComponent.REPORT_KEYS[this.reportName]; if (!reportKey) { @@ -96,20 +308,34 @@ export class ReportViewerComponent { } const raw = this.filterForm.getRawValue(); + // Only what this report reads is sent. The backend defaults the rest to the full range, so a + // report that ignores a filter is never handed a value that looks like it was applied. + const params: Record = {}; + if (this.usesDate) { + params['fromDate'] = this.dateUtils.getDate(raw.fromDate); + params['toDate'] = this.dateUtils.getDate(raw.toDate); + } + if (this.usesAgent) { + params['agentFrom'] = raw.agentFrom; + params['agentTo'] = raw.agentTo; + } + if (this.usesParty) { + params['partyFrom'] = raw.partyFrom; + params['partyTo'] = raw.partyTo; + } + this.organizationService - .runDdsReport(reportKey, { - fromDate: this.dateUtils.getDate(raw.fromDate), - toDate: this.dateUtils.getDate(raw.toDate), - agentFrom: raw.agentFrom, - agentTo: raw.agentTo, - partyFrom: raw.partyFrom, - partyTo: raw.partyTo - }) + .runDdsReport(reportKey, params) .subscribe((result: any) => { this.available = result.available; this.unavailableMessage = result.message || ''; this.columns = result.columns || []; this.rows = result.rows || []; + // Captured here so the printed heading names the range these rows came from, and does not + // silently change if the filter is edited without re-running. + this.ranFromDate = raw.fromDate; + this.ranToDate = raw.toDate; + this.ranOnDate = new Date(); this.hasRun = true; }); } diff --git a/src/app/deposits/party-master/CBS-Backend.code-workspace b/src/app/deposits/party-master/CBS-Backend.code-workspace new file mode 100644 index 000000000..5443c10bb --- /dev/null +++ b/src/app/deposits/party-master/CBS-Backend.code-workspace @@ -0,0 +1,11 @@ +{ + "folders": [ + { + "path": "../../../../../../CBS-Backend" + }, + { + "path": "../../../../.." + } + ], + "settings": {} +} \ No newline at end of file diff --git a/src/app/deposits/party-master/create-party/create-party.component.html b/src/app/deposits/party-master/create-party/create-party.component.html new file mode 100644 index 000000000..66ba08cdd --- /dev/null +++ b/src/app/deposits/party-master/create-party/create-party.component.html @@ -0,0 +1,975 @@ + + +
+
+ +

{{ partyId ? 'Edit Party' : 'Party Master Entry' }}

+ @if (isClosed) { + Closed — view only + } +
+ +
+ + + +
+
+
+ +

Customer & Scheme

+ Who this account is for, which scheme it runs under, and which agent collects for it +
+
+
+ + + + + + @for (client of clientOptions; track client.id) { + +
+ {{ client.id }} + {{ client.displayName }} + {{ client.officeName }} +
+
+ } + + @if (clientListIsPartial) { + + Showing {{ clientOptions.length }} of {{ clientTotal }} — type a name to find the rest + + } +
+ @if (loadedClientName) { + {{ loadedClientName }} + } @else if (clientLookupFailed) { + No client with this ID + } +
+
+ +
+ + + + + + @for (type of subTypeOptions; track type.id) { + +
+ {{ type.accountTypeCode }} + {{ type.accountTypeDescription }} + {{ type.accountTypeGroup }} +
+
+ } + @if (subTypeOptions.length === 0) { + No scheme matches that. + } +
+ @if (subTypes.length === 0) { + No active account type — create one first + } +
+
+ +
+ + + + + + @for (agent of agentOptions; track agent.id) { + +
+ {{ agent.agentCode }} + {{ agent.agentNameId }} + + + {{ agent.active ? agent.homeBranch : 'not collecting' }} + +
+
+ } + @if (agentOptions.length === 0) { + No agent matches that. + } +
+ @if (loadedAgentName && !agentLookupFailed) { + {{ loadedAgentName }} + } @else if (loadedAgentName && agentLookupFailed) { + {{ loadedAgentName }} is deactivated + } @else if (agentLookupFailed) { + No agent with this ID + } +
+
+ + +
+ + + + @for (office of offices; track office.id) { + {{ office.name }} + } + + @if (officesError) { + {{ officesError }} + } @else { + The branch this account belongs to + } + +
+ + + @if (savingsAccountNo) { +
+ + +
+ } @else if (partyId) { +
+ + +
+ } @else { +
+ + +
+ } + +
+ + + + +
+ +
+ + + + +
+ +
+ + + + Yes + No + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + Male + Female + Other + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + @for (op of operations; track op) { + {{ op }} + } + + +
+ +
+ + + + + + +
+ +
+ + + + @for (occ of occupations; track occ) { + {{ occ }} + } + + +
+ +
+ + + + @for (r of riskLevels; track r) { + {{ r }} + } + + +
+
+
+ +
+
+ +

Minor / Guardian

+ Only for a minor's account — the guardian who operates it until they come of age +
+
+
+ + + + @for (opt of adultMinorOptions; track opt) { + {{ opt }} + } + + +
+ + @if (isMinorAccount) { +
+ + + + + + Turns 18 on + +
+ +
+ + + + + + @for (client of guardianOptions; track client.id) { + +
+ {{ client.id }} + {{ client.displayName }} + {{ client.officeName }} +
+
+ } +
+ @if (loadedGuardianName) { + {{ loadedGuardianName }} + } +
+
+ +
+ + + + +
+ +
+ + + + To the minor + +
+ } +
+ @if (!isMinorAccount) { +

An adult's account needs no guardian details.

+ } +
+ +
+
+ +

Scheme Terms

+ How much is collected daily, for how long, and what it matures to +
+
+
+ + + + + + + +
+ +
+ + + + +
+ +
+ + + + + Calculated + +
+ +
+ + + + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + @if (partyForm.get('days')?.hasError('exceedsTerm')) { + Only {{ termDays }} days in this term + } @else if (termDays) { + Term is {{ termDays }} days + } + +
+ +
+ + + + Collection book no. + +
+ +
+ + + + Calculated + +
+ +
+ + + + % + +
+ +
+ + + + @for (type of withdrawTypes; track type) { + {{ type }} + } + + +
+
+
+ +
+
+ +

Balances & Status

+ Kept by collections and the nightly batch — shown here, never typed +
+
+
+ OD1 Flag +
+
+ OD2 Flag +
+ +
+ + + + Yes + No + + @if (lienHeld) { + Nothing can leave this account until it is released + } + +
+ + + @if (lienHeld) { +
+ + + + @for (reason of lienReasons; track reason) { + {{ reason }} + } + + What this money is being held for + +
+ + + @if (lienReasonIsOther) { +
+ + + + Write the reason in your own words + +
+ } + } + +
+ + + + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ + + + +
+ +
+ Is SMS Active +
+
+ Is Email Active +
+ +
+ + + + +
+ +
+ + + + + + +
+ +
+ + + + +
+ +
+ + + + @for (code of freezeCodes; track code) { + {{ code }} + } + + +
+ +
+ + + + @for (code of advCodes; track code) { + {{ code }} + } + + +
+
+
+ + @if (creditToRequired) { +
+
+ +

Maturity Payout Routing

+
+

Transfer To Saving — say where the maturity proceeds should land.

+
+
+ + + + @for (office of offices; track office.id) { + {{ office.name }} + } + + @if (officesError) { + {{ officesError }} + } + +
+ + +
+ + + + @for (account of creditToAccounts; track account.id) { + + {{ account.accountNo }} · {{ account.productName }} + + } + + @if (creditToAccounts.length === 0) { + + {{ loadedClientName || 'This customer' }} has no other savings account to credit + + } + +
+ + +
+ + + + +
+
+
+ } +
+
+ + +
+
+
+
+ + + + Yes + No + + +
+ + @if (nomineesRequired) { + + Percentage + Sequence + + + + + @if (splitByPercentage && nominees.length > 0) { + + } @else if (splitBySequence && nominees.length > 0) { + + } + } +
+ + @if (!nomineesRequired) { +

Set Nominee to Yes to nominate someone for this account.

+ } + + @for (nominee of nominees.controls; track nominee; let i = $index) { +
+
+ + + + + + +
+
+ + + + +
+
+ + + + +
+
+ + + + + + +
+
+ + + + + From date of birth + +
+
+ + + + +
+
+ + + + Male + Female + Other + + +
+
+ + + + @if (isNomineeMinor(nominee)) { + Required for a minor + } + +
+ @if (splitByPercentage) { +
+ + + + +
+ } @else { +
+ + + + +
+ } + +
+ } +
+
+
+
+ + @if (isClosed) { +
+ + This account is closed. No further collections or edits are possible. +
+ } + + + @if (minorTurnedMajor) { +
+ + + This account was opened for a minor who came of age on + {{ majorOn | dateFormat }}. The guardian can no longer + operate it — take fresh KYC, a signature and a new mandate from the account holder. + +
+ } + +
+ + + + @if (partyId && !isClosed && canCloseEarly) { + + } + @if (partyId && !isClosed) { + + } + +
+ +
+
diff --git a/src/app/deposits/party-master/create-party/create-party.component.scss b/src/app/deposits/party-master/create-party/create-party.component.scss new file mode 100644 index 000000000..df7e0918a --- /dev/null +++ b/src/app/deposits/party-master/create-party/create-party.component.scss @@ -0,0 +1,526 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +$primary: #0f766e; + +.pm-container { + // Wider than the usual form page: the extra width is what keeps this many fields off a + // second screen. + max-width: 1280px; + margin: 0 auto; + padding: 12px 16px 20px; +} + +.pm-header { + display: flex; + align-items: center; + gap: 14px; + margin-bottom: 10px; + + &__title { + margin: 0; + font-size: 18px; + font-weight: 500; + } +} + +.pm-back { + color: $primary; +} + +.pm-card { + background: var(--md-sys-color-surface, #fff); + border-radius: 8px; + box-shadow: 0 2px 8px rgb(0 0 0 / 8%); +} + +.pm-tabs { + ::ng-deep .mat-mdc-tab-body-wrapper { + min-height: 300px; + } +} + +.pm-tab-content { + padding: 20px 24px; +} + +.pm-section { + padding: 16px 0; + border-bottom: 1px solid rgb(0 0 0 / 8%); + + &:last-of-type { + border-bottom: none; + } + + &__header { + display: flex; + align-items: center; + // The subtitle sits beside the title where there is room and drops below it where there is not. + flex-wrap: wrap; + gap: 2px 10px; + margin-bottom: 16px; + + i { + color: $primary; + } + + h3 { + margin: 0; + font-size: 16px; + font-weight: 600; + // Held at its natural width: letting it shrink breaks a two-word heading onto three lines. + flex: 0 0 auto; + white-space: nowrap; + } + } +} + +.pm-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 2px 18px; + align-items: start; +} + +/* + * Label to the left of its field, the way the screen this replaces laid them out. A stacked label + * costs a whole row of height per field, and with 60-odd fields that is the difference between an + * operator seeing the form at once and scrolling through three screens of it. + */ +.pm-f { + display: grid; + grid-template-columns: 104px minmax(0, 1fr); + align-items: start; + column-gap: 7px; + + &--wide { + grid-column: span 2; + } +} + +.pm-f-lbl { + font-size: 11.5px; + line-height: 1.25; + text-align: right; + color: rgb(0 0 0 / 68%); + padding-top: 7px; + + // Required marker, in the same red the legacy screen used. + b { + color: #c1121f; + font-weight: 400; + margin-left: 2px; + } +} + +.pm-field { + width: 100%; +} + +.pm-checkbox-field { + display: flex; + align-items: center; + height: 56px; +} + +.pm-radio-group { + display: flex; + align-items: center; + gap: 16px; + height: 56px; +} + +.pm-add-nominee { + color: $primary; + height: fit-content; + align-self: center; +} + +/* + * The running total of the shares entered, beside the button that adds another. Reads as ordinary + * text while the split still adds up and turns to the warning colour when it does not, so the clerk + * sees the state of the whole nomination without having to try to save it. + */ +.pm-share-total { + align-self: center; + margin-left: 12px; + font-size: 13px; + color: rgb(0 0 0 / 60%); +} + +.pm-share-total-bad { + color: #b3261e; + font-weight: 500; +} + +.pm-empty { + color: rgb(0 0 0 / 50%); + font-size: 14px; + margin: 0 0 8px; +} + +/* + * One nominee, laid out as fields that wrap rather than columns that must all fit. + * + * A fixed column per field only works while the count is fixed, and it is not - the Share % field + * appears and disappears with the split mode, and a tenth column pushed the remove button onto a + * line of its own. Each field instead states the width it needs to stay readable and the row breaks + * where it runs out of space, so nothing is squeezed and nothing is orphaned. + */ +.pm-nominee-row { + display: flex; + flex-wrap: wrap; + gap: 4px 12px; + align-items: start; + padding: 12px 0; + border-top: 1px dashed rgb(0 0 0 / 10%); + + &:first-of-type { + border-top: none; + } + + /* Names and addresses need room; a date, Yes/No or a percentage does not. */ + .pm-f { + flex: 1 1 170px; + min-width: 0; + } + + .pm-f--date { + flex: 0 1 140px; + } + + .pm-f--tight { + flex: 0 1 100px; + } +} + +.pm-remove-nominee { + color: #b91c1c; + margin-top: 8px; +} + +.pm-action-bar { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 16px 24px; + border-top: 1px solid rgb(0 0 0 / 8%); +} + +.pm-callout { + display: flex; + align-items: center; + gap: 10px; + margin: 0 24px; + padding: 12px 16px; + background: rgb(185 28 28 / 8%); + border: 1px solid rgb(185 28 28 / 25%); + border-radius: 6px; + color: #b91c1c; + font-size: 0.9rem; +} + +@media (width <= 1000px) { + .pm-grid { + grid-template-columns: repeat(2, 1fr); + } + +} + +/* Resolved name shown under a Client / Agent / Guardian id, as the legacy screen did in red. */ +.pm-hint--found { + color: #15803d; + font-weight: 500; +} + +.pm-hint--missing { + color: #b91c1c; + font-weight: 500; +} + +/* Explains why a block of fields is absent, so the screen does not just look unfinished. */ +.pm-note { + margin: 4px 0 0; + font-size: 0.8rem; + color: rgb(0 0 0 / 55%); + font-style: italic; +} + +.pm-closed-badge { + margin-left: 12px; + padding: 3px 11px; + border-radius: 11px; + font-size: 0.72rem; + font-weight: 500; + background: rgb(0 0 0 / 7%); + color: rgb(0 0 0 / 60%); +} + +/* Says whose details a block of fields is for, so the screen explains itself. */ +.pm-section__sub { + flex: 1 1 auto; + min-width: 0; + font-size: 0.78rem; + font-weight: 400; + line-height: 1.45; + color: rgb(0 0 0 / 55%); +} + +.pm-search-ic { + color: rgb(0 0 0 / 38%); + font-size: 0.85rem; +} + +/* + * Money reads right to left, so a column of figures only lines up when it is right-aligned; the + * tabular figures keep every digit the same width so the decimal points stack. + */ +input.pm-money { + text-align: right; + font-variant-numeric: tabular-nums; +} + +/* One row of a Client / Sub Type search list: the id, the name, and where it belongs. */ +.pm-opt { + display: flex; + align-items: baseline; + gap: 10px; + overflow: hidden; +} + +/* The "there are more" footer row. Quieter than a real option, since it is not one. */ +.pm-opt-more { + font-size: 0.75rem; + font-style: italic; + opacity: 0.75; +} + +.pm-opt-id { + flex: 0 0 auto; + min-width: 52px; + font-family: ui-monospace, 'SF Mono', 'Cascadia Mono', monospace; + font-size: 0.78rem; + color: var(--pl-accent, #b45309); +} + +/* + The name is the reason the row exists, so it gets whatever width is going and is the last thing + to be cut. Before this the branch was fixed-width and the name carried the ellipsis, so in the + narrow panel every customer read as a single letter. +*/ + +.pm-opt-main { + flex: 1 1 auto; + min-width: 8rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.pm-opt-sub { + flex: 0 1 auto; + min-width: 0; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-size: 0.75rem; + color: rgb(0 0 0 / 50%); +} + +/* + * Dense data-entry layout. + * + * The screen this replaces fitted the whole form on one page - an officer opening accounts all day + * reads it at a glance and never scrolls. Material's default field height and spacing turn the same + * 56 fields into three screens' worth, so the whole card is tightened: shorter fields, smaller type, + * and just enough room under each field for the hint that names the client or agent it resolved. + */ +/* + * With the label moved outside, the box only has to hold the value - so it loses the height Material + * reserves for a floating label, and the form fits a screen instead of three. + */ +.pm-card { + ::ng-deep { + .mat-mdc-form-field { + font-size: 12.5px; + } + + .mat-mdc-form-field-infix { + min-height: 26px; + padding-top: 3px !important; + padding-bottom: 3px !important; + border-top: 0; + } + + .mat-mdc-text-field-wrapper { + padding-left: 8px; + padding-right: 8px; + } + + .mat-mdc-input-element, + .mat-mdc-select-value-text { + font-size: 12.5px; + } + + // Room for one line of hint - the resolved client/agent name that confirms the right record. + .mat-mdc-form-field-subscript-wrapper { + height: 14px; + min-height: 14px; + } + + .mat-mdc-form-field-hint { + font-size: 10.5px; + line-height: 14px; + } + + // Suffix icons and the datepicker toggle default to a 48px touch target, which reopens all the + // height just saved. + .mat-mdc-icon-button.mat-mdc-button-base { + width: 26px; + height: 26px; + padding: 3px; + } + + .mat-mdc-form-field-icon-suffix { + padding-left: 2px; + + .mat-datepicker-toggle svg { + width: 15px; + height: 15px; + } + } + + .mat-mdc-tab-body-wrapper { + min-height: 0; + } + } +} + +.pm-tab-content { + padding: 8px 16px; +} + +/* + * Blocks drawn the way the screen this replaces drew them: a hairline between groups of fields, + * with the group's name as a quiet caption above it rather than a heading bar of its own. An + * operator who has filled this form a hundred times navigates by field position; one filling their + * first still gets told which block is which. + */ +.pm-section { + padding: 8px 0 2px; + border-bottom: 1px solid rgb(0 0 0 / 11%); + + &__header { + margin-bottom: 6px; + gap: 0 10px; + + // Decoration the legacy screen did without, and the row is tighter for losing it. + i { + display: none; + } + + h3 { + font-size: 11px; + font-weight: 700; + letter-spacing: 0.07em; + text-transform: uppercase; + color: $primary; + } + } +} + +.pm-section__sub { + font-size: 0.72rem; + color: rgb(0 0 0 / 45%); +} + +.pm-grid { + gap: 0 12px; +} + +.pm-checkbox-field, +.pm-radio-group { + height: 28px; + + ::ng-deep label { + font-size: 12.5px; + } +} + +// The nominee grid is its own row of fields, so its labels sit above rather than beside - there is +// no room for a label column eight fields across. +.pm-nominee-row { + gap: 0 10px; + padding: 3px 0; + + .pm-f { + grid-template-columns: minmax(0, 1fr); + row-gap: 1px; + } + + .pm-f-lbl { + text-align: left; + padding-top: 0; + font-size: 10.5px; + } +} + +.pm-note { + margin: 0; + font-size: 0.74rem; +} + +.pm-action-bar { + padding: 10px 18px; +} + +/* + The customer's real deposit account. Not a form field - nothing here is typed, it is what the + bank's books say - so it reads as a fact rather than sitting in an input box pretending to be one. +*/ +.pm-account { + display: flex; + flex-direction: column; + gap: 2px; + padding: 3px 0; + min-height: 26px; + justify-content: center; +} + +.pm-account-no { + font-family: ui-monospace, 'SF Mono', 'Cascadia Mono', monospace; + font-size: 0.85rem; + letter-spacing: 0.02em; +} + +.pm-account-meta { + font-size: 0.75rem; + color: rgb(0 0 0 / 55%); +} + +.pm-account-live { + color: #15803d; + font-weight: 500; +} + +/* Said plainly: this party has no account behind it, which is a real gap, not a blank field. */ +.pm-account-missing { + font-size: 0.75rem; + font-style: italic; + color: #9b2c2c; +} + +/* + Something the branch has to do, rather than something that has gone wrong - so it reads as an + instruction in the warning colour, not an error in red like the closed-account notice. +*/ +.pm-callout--action { + background: rgb(180 83 9 / 8%); + border-color: rgb(180 83 9 / 25%); + color: #b45309; +} diff --git a/src/app/deposits/party-master/create-party/create-party.component.ts b/src/app/deposits/party-master/create-party/create-party.component.ts new file mode 100644 index 000000000..24c97ea01 --- /dev/null +++ b/src/app/deposits/party-master/create-party/create-party.component.ts @@ -0,0 +1,1764 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** Angular Imports */ +import { Component, OnInit, inject } from '@angular/core'; +import { + UntypedFormBuilder, + UntypedFormGroup, + UntypedFormArray, + Validators, + ValidatorFn, + ValidationErrors, + AbstractControl +} from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { MatTabsModule } from '@angular/material/tabs'; +import { MatCheckbox } from '@angular/material/checkbox'; +import { MatRadioModule } from '@angular/material/radio'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatDialog } from '@angular/material/dialog'; +import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** Custom Services */ +import { OrganizationService } from 'app/organization/organization.service'; +import { ProductsService } from 'app/products/products.service'; +import { ClientsService } from 'app/clients/clients.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { Dates } from 'app/core/utils/dates'; + +/** Custom Components */ +import { PrematureCloseDialogComponent } from '../premature-close-dialog/premature-close-dialog.component'; + +/** Custom Directives */ +import { RestrictInputDirective } from 'app/directives/restrict-input.directive'; + +/** + * Party Master Entry form. + * + * The customer's DDS account - opened under a scheme (Sub Type, chosen from the DDS Account Type + * master) and assigned to the agent who will collect for it daily. Carries its own KYC, scheme + * terms, two-stage overdue flags and a nominee list. Modelled on the legacy Party Master Entry + * screen, field for field. + * + * The legacy screen filled the identity block from the Client ID and showed the agent's name beside + * the agent field; both are reproduced here, and the same lookup fills the guardian's name for a + * minor's account. That field read "Sub Agent" in the legacy system, which had a tier this one does + * not - SubAgent was folded into Agent, so it is named for what it actually points at. Fields the legacy screen greyed out are either client-derived, calculated, or + * written by collections and the batch job - none of them are typed here. + * + * "Close Account" (command=close) permanently closes the Party once its balance is zero - the + * backend rejects the command otherwise. A closed Party can no longer be edited or receive new + * Party Tran collections. + * + * savingsAccountNo optionally links this Party to an existing, non-closed real Savings Account of + * the same client (resolved by account number, since that's what an officer has to hand) - + * clarified with the business (2026-08-24) as "link it if the client already has one, otherwise + * leave it". There is no unlink once linked. + */ +@Component({ + selector: 'mifosx-create-party', + templateUrl: './create-party.component.html', + styleUrls: ['./create-party.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatTabsModule, + MatCheckbox, + MatRadioModule, + MatAutocompleteModule, + // Holds the identifier fields to the shape their kind takes, at the keystroke. Imported here + // because a standalone component silently ignores a directive it has not imported - the + // attribute stays on the element and simply does nothing. + RestrictInputDirective + ] +}) +export class CreatePartyComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private router = inject(Router); + private route = inject(ActivatedRoute); + private organizationService = inject(OrganizationService); + private productsService = inject(ProductsService); + private clientsService = inject(ClientsService); + private settingsService = inject(SettingsService); + private dateUtils = inject(Dates); + private dialog = inject(MatDialog); + + /* + * What each identifier has to look like - the same shapes the server holds them to. + * + * Checked here as well so the clerk is told while the customer is still at the counter, rather + * than after a save that comes back rejected. The server checks them again regardless: a screen + * is a convenience, not a guarantee. + */ + private static readonly MOBILE_RE = /^[6-9][0-9]{9}$/; + private static readonly TELEPHONE_RE = /^[0-9+()\-\s]{6,20}$/; + private static readonly PINCODE_RE = /^[1-9][0-9]{5}$/; + /** Twelve digits, or the masked form CKYC hands back. */ + private static readonly AADHAAR_RE = /^([0-9]{12}|[Xx]{8}[0-9]{4})$/; + private static readonly PAN_RE = /^[A-Z]{5}[0-9]{4}[A-Z]$/; + private static readonly CKYC_RE = /^[0-9]{14}$/; + + partyId: number | null = null; + + /** + * The earliest opening date the picker will offer on a new account - today, so the past is greyed + * out. The server refuses a past date on creation too; this only stops the clerk reaching for one. + * + * Null while editing. An account already on the books opened in the past by definition, so a floor + * of today would put every existing party into a state its own form would not accept. + */ + get minOpeningDate(): Date | null { + return this.partyId === null ? new Date() : null; + } + + selectedTabIndex = 0; + partyStatus = 'ACTIVE'; + linkedSavingsAccountId: number | null = null; + + /** + * The customer's real deposit account, as the bank's own books hold it. Read from the account + * itself rather than copied onto the party, so what shows here cannot drift from what is true. + */ + savingsAccountNo: string | null = null; + savingsProductName: string | null = null; + savingsAccountStatus: string | null = null; + savingsAccountBalance: number | null = null; + + /** + * A minor account holder who has come of age. The guardian's authority ended on that day, so the + * branch has to take fresh KYC and a new mandate - shown as a call to action rather than a field. + */ + majorOn: string | null = null; + minorTurnedMajor = false; + + /** Shown beside the id fields once the record behind them has been resolved. */ + loadedClientName = ''; + clientLookupFailed = false; + loadedAgentName = ''; + agentLookupFailed = false; + loadedGuardianName = ''; + + /** Clients matching what has been typed - an officer knows the customer's name, rarely their id. */ + clientOptions: any[] = []; + + /** + * The first page of clients, shown the moment the field is focused so the officer can pick from a + * list instead of having to know something to type. Fetched once and kept. + * + * Capped rather than "all": it is a page of names on a branch with 28 customers today and a bank + * with far more later, and the field has to stay usable in both. Typing still searches every + * client, not just these - see searchClients. + */ + private initialClients: any[] = []; + private static readonly CLIENT_LIST_SIZE = 50; + + /** How many clients exist in total, to say so when the list below is only part of them. */ + clientTotal = 0; + + /** True while the options being shown are the opening list rather than search results. */ + showingClientList = false; + + get clientListIsPartial(): boolean { + return this.showingClientList && this.clientTotal > this.clientOptions.length; + } + guardianOptions: any[] = []; + /** The account types matching what has been typed into Sub Type. */ + subTypeOptions: any[] = []; + + /** + * The agents on offer. Every agent is fetched once and filtered here rather than asked of the + * server per keystroke - there is a handful of them, not a branch's worth of customers. + */ + agentOptions: any[] = []; + private allAgents: any[] = []; + + get isClosed(): boolean { + return this.partyStatus === 'CLOSED'; + } + + /** The DDS Account Type master - what the legacy screen's Sub Type dropdown was fed from. */ + subTypes: any[] = []; + + operations = [ + 'Self', + 'Guardian', + 'Joint' + ]; + occupations = [ + 'Business', + 'Service', + 'Agriculture', + 'Homemaker', + 'Student', + 'Retired', + 'Other' + ]; + riskLevels = [ + 'Low', + 'Medium', + 'High' + ]; + adultMinorOptions = [ + 'Adult', + 'Minor' + ]; + withdrawTypes = [ + 'Cash', + 'Transfer', + 'Clearing' + ]; + freezeCodes = [ + 'Normal', + 'Debit Freeze', + 'Credit Freeze', + 'Total Freeze' + ]; + advCodes = [ + 'None', + 'Transfer To Saving', + 'Renew' + ]; + + /** The bank's own branches - where maturity proceeds can be credited. */ + /** + * Why a deposit gets held, offered as a list rather than typed. + * + * Free text meant the same reason arrived four ways - "Loan 1234", "loan no 1234", "LOAN#1234" - + * and anything counting them read four different reasons. Picked from a list they group properly, + * and the clerk types only where the list genuinely does not cover it. + */ + lienReasons = [ + 'Loan security', + 'Court order', + 'Tax attachment', + 'Deceased — claim pending', + 'Under investigation', + 'Other' + ]; + + offices: any[] = []; + officesError = ''; + + /** + * The customer's own savings accounts, for Credit To A/C. + * + * Picked rather than typed. The whole matured amount lands here years after somebody fills this + * in, and the live data showed what free text costs: one party carried "1234567890111", which is + * no account at all. Only savings accounts are offered - a fixed or recurring deposit cannot take + * the credit - and only this customer's own, which is what a maturity instruction means in any + * core system. The backend refuses anything else, whatever the screen sends. + */ + creditToAccounts: any[] = []; + + /** The Adv Code that makes the account say where its maturity proceeds should land. */ + private static readonly TRANSFER_TO_SAVING = 'Transfer To Saving'; + + /** Filled from the Client ID, as on the legacy screen. */ + private static readonly CLIENT_DERIVED_FIELDS = [ + 'partyName', + 'partyName1', + 'address1', + 'address2', + 'address3', + 'city', + 'pincode', + 'gender', + 'birthDate', + 'telephone', + 'mobileNoSms' + ]; + + private static readonly MINOR_FIELDS = [ + 'minorDate', + 'guardianClientId', + 'minAccountOpt', + 'relation' + ]; + + private static readonly CREDIT_TO_FIELDS = [ + 'creditToBranch', + 'creditToAccountType', + 'creditToAccount' + ]; + + partyForm: UntypedFormGroup = this.formBuilder.group({ + clientId: [ + '', + Validators.required + ], + subType: [ + '', + Validators.required + ], + agentId: [ + '', + Validators.required + ], + // The branch this account belongs to. Its own, not the collecting agent's - an agent of one + // branch may hold accounts for customers of another, so it is asked for rather than inherited. + officeId: [ + '', + Validators.required + ], + savingsAccountNo: [''], + lastAccount: [ + { + value: '', + disabled: true + } + ], + partyCode: [ + { + value: '', + disabled: true + } + ], + nominal: [ + 'No', + Validators.required + ], + aadharNo: ['', Validators.pattern(CreatePartyComponent.AADHAAR_RE)], + partyName: [ + '', + Validators.required + ], + partyName1: [''], + address1: [ + '', + Validators.required + ], + address2: [''], + address3: [''], + city: [ + '', + Validators.required + ], + pincode: [ + '', + [ + Validators.required, + Validators.pattern(CreatePartyComponent.PINCODE_RE) + ] + ], + gender: [''], + panCardNo: ['', Validators.pattern(CreatePartyComponent.PAN_RE)], + telephone: ['', Validators.pattern(CreatePartyComponent.TELEPHONE_RE)], + mobileNoSms: [ + '', + [ + Validators.required, + Validators.pattern(CreatePartyComponent.MOBILE_RE) + ] + ], + operation: [ + 'Self', + Validators.required + ], + birthDate: [''], + occupation: [''], + risk: [''], + ckycId: ['', Validators.pattern(CreatePartyComponent.CKYC_RE)], + adultMinor: [ + 'Adult', + Validators.required + ], + // Enabled only for a minor's account - see applyAdultMinorRules(). + minorDate: [ + { + value: '', + disabled: true + } + ], + guardianClientId: [ + { + value: '', + disabled: true + } + ], + minAccountOpt: [ + { + value: '', + disabled: true + } + ], + relation: [ + { + value: '', + disabled: true + } + ], + openingDate: [ + '', + Validators.required + ], + month: [''], + // Worked out from the opening date and the tenure - never typed. + maturityDate: [ + { + value: '', + disabled: true + } + ], + reopenDate: [''], + depositAmount: [0], + dailyDepositAmount: [ + 0, + Validators.required + ], + days: [ + '', + Validators.required + ], + ledgerNo: [''], + maturityAmount: [ + { + value: 0, + disabled: true + } + ], + interestRate: [0], + withdrawType: [''], + // The overdue flags, balances and interest figures below are written by collections and the + // DDS batch job. They are shown so an officer can see where the account stands, never typed. + od1Flag: [ + { + value: false, + disabled: true + } + ], + od2Flag: [ + { + value: false, + disabled: true + } + ], + lien: ['No'], + // Asked for only while the lien is set - the rule is applied from the flag rather + // than stated here, so switching it off does not leave a required field behind. + lienReason: [''], + // What the clerk types when the list does not cover it. Kept apart from lienReason so the list + // stays a list - the one value the server stores is worked out at save. + lienReasonOther: [''], + payDate: [ + { + value: '', + disabled: true + } + ], + paymentAmount: [ + { + value: 0, + disabled: true + } + ], + currentBalance: [ + { + value: 0, + disabled: true + } + ], + openingBalance: [ + { + value: 0, + disabled: true + } + ], + isSmsActive: [true], + isEmailActive: [false], + interestCalc: [ + { + value: 0, + disabled: true + } + ], + interestDate: [ + { + value: '', + disabled: true + } + ], + spDescription: [''], + freezeCode: ['Normal'], + advCode: ['None'], + // Mandatory only when Adv Code is Transfer To Saving - see applyAdvCodeRules(). + creditToBranch: [''], + creditToAccountType: [''], + creditToAccount: [''], + nomineeApplicable: ['No'], + nomineeSplitMode: ['Percentage'], + nominees: this.formBuilder.array([]) + }); + + ngOnInit(): void { + this.productsService.getDdsAccountTypes().subscribe((accountTypes: any[]) => { + // A retired scheme takes no new accounts, but one already on it must still show when edited. + this.subTypes = (accountTypes || []).filter((accountType) => accountType.active); + this.subTypeOptions = this.subTypes; + }); + + // Branches are picked, never typed: a free-text branch cannot be matched back to a real office + // when the proceeds are actually moved. If the list cannot be fetched the reason is shown on the + // field - an empty dropdown with no explanation looks like the bank has no branches. + this.organizationService.getOffices().subscribe({ + next: (offices: any) => { + this.offices = Array.isArray(offices) ? offices : offices?.pageItems || []; + this.officesError = this.offices.length === 0 ? 'No branches on file' : ''; + }, + error: (error: any) => { + this.officesError = error?.error?.defaultUserMessage || error?.error?.developerMessage + || `Could not load branches (${error?.status || 'no response'})`; + } + }); + + const routeId = this.route.snapshot.paramMap.get('partyId'); + if (routeId) { + this.organizationService.getParty(routeId).subscribe((party: any) => this.patchFormFromParty(party)); + } + + this.partyForm.get('days')?.addValidators(this.withinTerm()); + + this.wireClientLookup(); + this.wireSubTypeSearch(); + this.wireAgentLookup(); + this.wireGuardianLookup(); + this.wireConditionalRules(); + this.wireMaturityCalculation(); + } + + // ---------------------------------------------------------------- lookups + + /** + * Fills the identity block from the customer, the way the legacy screen did - except that the + * customer can be found by name as well as by id, since an officer at the counter is told a name. + * The control holds the picked client object, or whatever has been typed so far. + */ + private wireClientLookup(): void { + this.partyForm + .get('clientId') + ?.valueChanges.pipe(debounceTime(400), distinctUntilChanged()) + .subscribe((value) => { + // Always replace: changing the customer must not leave the previous one's details behind. + for (const controlName of CreatePartyComponent.CLIENT_DERIVED_FIELDS) { + this.partyForm.get(controlName)?.setValue(''); + } + this.loadedClientName = ''; + this.clientLookupFailed = false; + + // Picked from the list. + if (value && typeof value === 'object') { + this.clientOptions = []; + this.fillFromClient(value.id, value.displayName); + return; + } + + const typed = String(value ?? '').trim(); + if (!typed) { + // Emptying the field puts the opening list back, rather than leaving nothing to pick from. + this.showClientList(); + return; + } + + // A bare number is an id typed straight in; anything else is a name to search for. A number + // can be both, so both are tried and whichever matches wins. + if (/^\d+$/.test(typed)) { + this.fillFromClient(typed, ''); + } + this.searchClients(typed); + }); + } + + private searchClients(term: string): void { + this.clientsService.getFilteredClients('displayName', 'ASC', false, term).subscribe({ + next: (response: any) => { + const found = response?.pageItems || response || []; + this.showingClientList = false; + this.clientOptions = Array.isArray(found) ? found.slice(0, 25) : []; + }, + error: () => (this.clientOptions = []) + }); + } + + /** + * Opens the customer list when the field is focused. + * + * Without this the officer has to know something before anything appears, which is backwards: the + * point of the field is to find a customer, and a blank box is no help in doing that. A customer + * already picked is left alone - reopening the list over a chosen name only invites a misclick. + */ + onClientFocus(): void { + if (this.partyForm.get('clientId')?.value) { + return; + } + this.showClientList(); + } + + /** Shows the opening list, fetching it the first time it is asked for. */ + private showClientList(): void { + if (this.initialClients.length > 0) { + this.showingClientList = true; + this.clientOptions = this.initialClients; + return; + } + + this.clientsService.getClients('displayName', 'ASC', 0, CreatePartyComponent.CLIENT_LIST_SIZE).subscribe({ + next: (response: any) => { + const found = response?.pageItems || response || []; + this.initialClients = Array.isArray(found) ? found : []; + this.clientTotal = response?.totalFilteredRecords ?? this.initialClients.length; + this.showingClientList = true; + this.clientOptions = this.initialClients; + }, + // A failed list is not a failed screen - the officer can still type a name or an id. + error: () => (this.clientOptions = []) + }); + } + + private fillFromClient(clientId: string | number, knownName: string): void { + this.clientsService.getClientData(String(clientId), true).subscribe({ + next: (client: any) => { + this.loadedClientName = client?.displayName || knownName || ''; + this.clientLookupFailed = false; + this.setField('partyName', client?.displayName); + this.setField('mobileNoSms', client?.mobileNo); + this.setField('gender', client?.gender?.name || client?.gender); + const dateOfBirth = this.toDate(client?.dateOfBirth); + if (dateOfBirth) { + this.partyForm.get('birthDate')?.setValue(dateOfBirth); + } + // Proceeds usually go back to an account at the customer's own branch, so that is offered + // first - any branch can still be picked, since the account may well be at another. + const creditToBranch = this.partyForm.get('creditToBranch'); + if (client?.officeName && !creditToBranch?.value) { + creditToBranch?.setValue(client.officeName, { emitEvent: false }); + } + + this.loadClientAddress(String(clientId)); + this.loadClientIdentifiers(String(clientId)); + this.loadCreditToAccounts(String(clientId)); + }, + error: () => { + this.clientLookupFailed = true; + } + }); + } + + /** + * The customer's own accounts a maturity may be credited to. + * + * Worked out by the server, which applies the same conditions the payout enforces years later: an + * active savings account of theirs, not a fixed or recurring deposit, and not a DDS scheme account + * - those are collected on daily and pay out at their own maturity, so naming one here would send + * this maturity into another collection account instead of back to the customer. + * + * Deciding it here instead let the list and the refusal drift apart, and the accounts on offer + * came to include a scheme account of a party that had already closed. + */ + private loadCreditToAccounts(clientId: string): void { + this.organizationService.getCreditableAccounts(clientId).subscribe({ + next: (accounts: any) => (this.creditToAccounts = accounts || []), + error: () => (this.creditToAccounts = []) + }); + } + + /** + * Fills the account type from the account chosen. An account already is of a type, and asking for + * both lets the two disagree - the account is the half that is actually true. + */ + onCreditToAccountPicked(accountNo: string): void { + const account = this.creditToAccounts.find((each: any) => each.accountNo === accountNo); + if (account?.productName) { + this.partyForm.get('creditToAccountType')?.setValue(account.productName); + } + } + + /** + * Aadhar, PAN and CKYC are not plain client fields - they are KYC identifiers filed against the + * client, each tagged with a document type. They are matched by that type's name so the screen + * shows the same numbers the client record already holds, rather than asking for them twice. + */ + private loadClientIdentifiers(clientId: string): void { + this.clientsService.getClientIdentifiers(clientId).subscribe({ + next: (identifiers: any) => { + for (const identifier of Array.isArray(identifiers) ? identifiers : []) { + const words = this.wordsOf(identifier?.documentType?.name); + const documentKey = identifier?.documentKey; + if (!documentKey) { + continue; + } + if (words.includes('aadhaar') || words.includes('aadhar')) { + this.setField('aadharNo', documentKey); + } else if (words.includes('pan')) { + this.setField('panCardNo', documentKey); + } else if (words.includes('ckyc')) { + this.setField('ckycId', documentKey); + } + } + }, + // A client with no identifiers on file is normal - the officer just types them. + error: () => undefined + }); + } + + /** What the Client ID box shows for a picked client, rather than "[object Object]". */ + displayClient = (value: any): string => { + if (!value) { + return ''; + } + return typeof value === 'object' ? `${value.id} — ${value.displayName}` : String(value); + }; + + private loadClientAddress(clientId: string): void { + this.clientsService.getClientAddressData(clientId, true).subscribe({ + next: (addresses: any) => { + const address = Array.isArray(addresses) ? addresses[0] : addresses; + if (!address) { + return; + } + this.setField('address1', address.addressLine1 || address.street); + this.setField('address2', address.addressLine2); + this.setField('address3', address.addressLine3); + this.setField('city', address.city); + this.setField('pincode', address.postalCode); + }, + // A client with no address on file is normal - the officer just types it. + error: () => undefined + }); + } + + /** Shows the collecting agent's name beside the id, as the legacy screen did in red. */ + private wireAgentLookup(): void { + this.partyForm + .get('agentId') + ?.valueChanges.pipe(debounceTime(400), distinctUntilChanged()) + .subscribe((value) => { + this.loadedAgentName = ''; + this.agentLookupFailed = false; + + // Picked from the list - the agent is already in hand, nothing to look up. + if (value && typeof value === 'object') { + this.agentOptions = []; + this.loadedAgentName = value.agentNameId || value.clientName || ''; + this.agentLookupFailed = !value.active; + return; + } + + const typed = String(value ?? '').trim(); + if (!typed) { + this.showAgentList(); + return; + } + + this.filterAgents(typed); + + // A bare number is an id typed straight in, the way this field always worked. + if (!/^\d+$/.test(typed)) { + return; + } + this.organizationService.getAgent(typed).subscribe({ + next: (agent: any) => { + this.loadedAgentName = agent?.agentNameId || agent?.clientName || ''; + this.agentLookupFailed = !agent?.active; + }, + error: () => { + this.agentLookupFailed = true; + } + }); + }); + } + + /** + * Opens the agent list on focus, so the field can be answered by picking rather than by knowing + * an id. A chosen agent is left alone - reopening over it only invites a misclick. + */ + onAgentFocus(): void { + if (this.partyForm.get('agentId')?.value) { + return; + } + this.showAgentList(); + } + + private showAgentList(): void { + if (this.allAgents.length > 0) { + this.agentOptions = this.allAgents; + return; + } + this.organizationService.getAgents().subscribe({ + next: (agents: any[]) => { + this.allAgents = agents || []; + this.agentOptions = this.allAgents; + }, + // A failed list is not a failed screen - an id can still be typed. + error: () => (this.agentOptions = []) + }); + } + + /** Matches on code or name, since an officer may know either. */ + private filterAgents(term: string): void { + const needle = term.toLowerCase(); + const match = (agent: any) => + String(agent.agentCode || '').toLowerCase().includes(needle) || + String(agent.agentNameId || '').toLowerCase().includes(needle) || + String(agent.id) === term; + + if (this.allAgents.length > 0) { + this.agentOptions = this.allAgents.filter(match); + return; + } + this.organizationService.getAgents().subscribe({ + next: (agents: any[]) => { + this.allAgents = agents || []; + this.agentOptions = this.allAgents.filter(match); + }, + error: () => (this.agentOptions = []) + }); + } + + displayAgent = (value: any): string => { + if (!value) { + return ''; + } + return typeof value === 'object' ? `${value.agentCode} — ${value.agentNameId}` : String(value); + }; + + /** A minor's account names a guardian, found the same way the customer is. */ + private wireGuardianLookup(): void { + this.partyForm + .get('guardianClientId') + ?.valueChanges.pipe(debounceTime(400), distinctUntilChanged()) + .subscribe((value) => { + this.loadedGuardianName = ''; + + if (value && typeof value === 'object') { + this.guardianOptions = []; + this.loadedGuardianName = value.displayName || ''; + return; + } + + const typed = String(value ?? '').trim(); + if (!typed) { + this.guardianOptions = []; + return; + } + + if (/^\d+$/.test(typed)) { + this.clientsService.getClientData(typed, true).subscribe({ + next: (client: any) => (this.loadedGuardianName = client?.displayName || ''), + error: () => undefined + }); + } + this.clientsService.getFilteredClients('displayName', 'ASC', false, typed).subscribe({ + next: (response: any) => { + const found = response?.pageItems || response || []; + this.guardianOptions = Array.isArray(found) ? found.slice(0, 25) : []; + }, + error: () => (this.guardianOptions = []) + }); + }); + } + + /** + * Sub Type is picked from the DDS Account Type master. Once a bank runs a dozen schemes a plain + * dropdown is a scroll, so it filters on either the code or the scheme's name as you type. + */ + private wireSubTypeSearch(): void { + this.partyForm.get('subType')?.valueChanges.subscribe((value) => { + const term = String(value ?? '').toLowerCase().trim(); + this.subTypeOptions = !term + ? this.subTypes + : this.subTypes.filter( + (type) => + String(type.accountTypeCode || '').toLowerCase().includes(term) || + String(type.accountTypeDescription || '').toLowerCase().includes(term) || + String(type.accountTypeGroup || '').toLowerCase().includes(term) + ); + }); + } + + /** Opening the box should offer everything, not just what the current value happens to match. */ + showAllSubTypes(): void { + this.subTypeOptions = this.subTypes; + } + + displaySubType = (code: any): string => { + if (!code) { + return ''; + } + const found = this.subTypes.find((type) => type.accountTypeCode === code); + return found ? `${found.accountTypeDescription} (${found.accountTypeCode})` : String(code); + }; + + // ----------------------------------------------------------- conditionals + + private wireConditionalRules(): void { + this.partyForm.get('adultMinor')?.valueChanges.subscribe(() => this.applyAdultMinorRules()); + this.partyForm.get('advCode')?.valueChanges.subscribe(() => this.applyAdvCodeRules()); + this.partyForm.get('lien')?.valueChanges.subscribe(() => this.applyLienRules()); + this.partyForm.get('lienReason')?.valueChanges.subscribe(() => this.applyLienRules()); + this.partyForm.get('nomineeApplicable')?.valueChanges.subscribe(() => this.applyNomineeRules()); + // Switching the split re-rules every nominee already on the form, not just the ones added after + // - a clerk who names three nominees and then picks Percentage must still be asked for shares. + this.partyForm.get('nomineeSplitMode')?.valueChanges.subscribe((mode) => { + this.nominees.controls.forEach((group) => this.applySplitModeTo(group, mode)); + }); + this.applyAdultMinorRules(); + this.applyAdvCodeRules(); + } + + /** + * A minor's account has to say when the holder comes of age and who operates it until then; on an + * adult's account those four fields are meaningless, so they stay disabled and empty. + */ + private applyAdultMinorRules(): void { + const isMinor = this.partyForm.get('adultMinor')?.value === 'Minor'; + for (const controlName of CreatePartyComponent.MINOR_FIELDS) { + const control = this.partyForm.get(controlName); + if (!control) { + continue; + } + if (isMinor) { + control.enable({ emitEvent: false }); + // minAccountOpt is an optional note on how the account may be operated. + if (controlName !== 'minAccountOpt') { + control.setValidators(Validators.required); + } + } else { + control.clearValidators(); + control.setValue('', { emitEvent: false }); + control.disable({ emitEvent: false }); + this.loadedGuardianName = ''; + } + control.updateValueAndValidity({ emitEvent: false }); + } + } + + /** + * Transfer To Saving means the maturity proceeds land in a savings account, so the account has to + * name which one. Any other Adv Code leaves nothing to credit, and the three fields go away. + */ + private applyAdvCodeRules(): void { + const transfers = this.partyForm.get('advCode')?.value === CreatePartyComponent.TRANSFER_TO_SAVING; + for (const controlName of CreatePartyComponent.CREDIT_TO_FIELDS) { + const control = this.partyForm.get(controlName); + if (!control) { + continue; + } + if (transfers) { + control.setValidators(Validators.required); + } else { + control.clearValidators(); + control.setValue('', { emitEvent: false }); + } + control.updateValueAndValidity({ emitEvent: false }); + } + } + + get creditToRequired(): boolean { + return this.partyForm.get('advCode')?.value === CreatePartyComponent.TRANSFER_TO_SAVING; + } + + get isMinorAccount(): boolean { + return this.partyForm.get('adultMinor')?.value === 'Minor'; + } + + get nomineesRequired(): boolean { + return this.partyForm.get('nomineeApplicable')?.value === 'Yes'; + } + + + + /** + * Which entry in the list a stored reason belongs to. + * + * Anything not on the list was typed under "Other", including reasons saved before there was a + * list - so it comes back as "Other" with the original words intact rather than being lost to a + * dropdown that has no room for it. + */ + private listedLienReason(stored: string | null | undefined): string { + if (!stored) { + return ''; + } + return this.lienReasons.includes(stored) ? stored : 'Other'; + } + + /** True while this account's money is being held, which is what asks for a reason. */ + get lienHeld(): boolean { + return this.partyForm?.get('lien')?.value === 'Yes'; + } + + /** + * A held account has to say what it is held for; a free one has nothing to explain. + * + * The reason is cleared when the hold is lifted rather than left behind, so the form never reads + * "Lien: No" beside an explanation of a hold that no longer exists - the server clears it for the + * same reason. + */ + private applyLienRules(): void { + const reason = this.partyForm.get('lienReason'); + const other = this.partyForm.get('lienReasonOther'); + if (!reason || !other) { + return; + } + + if (this.lienHeld) { + reason.setValidators(Validators.required); + } else { + reason.clearValidators(); + reason.setValue('', { emitEvent: false }); + } + reason.updateValueAndValidity({ emitEvent: false }); + + // Only "Other" asks for words of its own - picking it and leaving it blank says no more than + // leaving the whole thing blank did. + if (this.lienHeld && this.lienReasonIsOther) { + other.setValidators([ + Validators.required, + Validators.maxLength(200) + ]); + } else { + other.clearValidators(); + other.setValue('', { emitEvent: false }); + } + other.updateValueAndValidity({ emitEvent: false }); + } + + /** True while the clerk has chosen to write the reason themselves. */ + get lienReasonIsOther(): boolean { + return this.partyForm?.get('lienReason')?.value === 'Other'; + } + + /** Saying an account has a nominee but naming none would leave the nomination incomplete. */ + private applyNomineeRules(): void { + if (this.nomineesRequired && this.nominees.length === 0) { + this.addNominee(); + } + if (!this.nomineesRequired) { + this.nominees.clear(); + } + } + + // ----------------------------------------------------------- calculations + + /** + * Maturity date and amount follow from the opening date and the tenure, so they are recomputed + * whenever one of those changes rather than being typed - which is why the legacy screen greyed + * them out. + */ + private wireMaturityCalculation(): void { + // The term drives everything downstream, so changing it refills Days as well. + for (const controlName of ['openingDate', 'month']) { + this.partyForm.get(controlName)?.valueChanges.subscribe(() => this.recalculateTerm()); + } + // Days typed by hand stands: it only moves the maturity date when no month was given. + this.partyForm.get('days')?.valueChanges.subscribe(() => { + this.recalculateMaturityDate(); + this.recalculateMaturityAmount(); + }); + for (const controlName of ['dailyDepositAmount', 'interestRate']) { + this.partyForm.get(controlName)?.valueChanges.subscribe(() => this.recalculateMaturityAmount()); + } + } + + private recalculateTerm(): void { + this.recalculateMaturityDate(); + // Days is not refilled from the term: whether collection runs every day or skips Sundays and + // holidays is the bank's own rule, and guessing it would quietly put a wrong instalment count - + // and so a wrong maturity amount - in front of the customer. It is only checked, below. + this.partyForm.get('days')?.updateValueAndValidity({ emitEvent: false }); + this.recalculateMaturityAmount(); + } + + private recalculateMaturityDate(): void { + const raw = this.partyForm.getRawValue(); + const openingDate = this.toDate(raw.openingDate); + const months = Number(raw.month) || 0; + const days = Number(raw.days) || 0; + + if (!openingDate || (months <= 0 && days <= 0)) { + this.partyForm.get('maturityDate')?.setValue('', { emitEvent: false }); + return; + } + + const maturity = new Date(openingDate.getTime()); + if (months > 0) { + maturity.setMonth(maturity.getMonth() + months); + } else { + maturity.setDate(maturity.getDate() + days); + } + this.partyForm.get('maturityDate')?.setValue(maturity, { emitEvent: false }); + } + + /** Calendar days the term covers, once a term in months has been given. */ + get termDays(): number | null { + const raw = this.partyForm.getRawValue(); + const openingDate = this.toDate(raw.openingDate); + const months = Number(raw.month) || 0; + if (!openingDate || months <= 0) { + return null; + } + const maturity = new Date(openingDate.getTime()); + maturity.setMonth(maturity.getMonth() + months); + return Math.round((maturity.getTime() - openingDate.getTime()) / (24 * 60 * 60 * 1000)); + } + + /** + * More instalments than there are days in the term is not a bank rule to be checked, it is + * arithmetic: 365 daily collections cannot happen inside three months. Left unchecked it puts a + * three-month maturity date beside a maturity amount worked out over a year, and the customer is + * quoted a figure the account will never reach. + * + * Fewer instalments than days is normal and passes silently - that is just a bank that does not + * collect on Sundays and holidays. + */ + private withinTerm(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const term = this.termDays; + const days = Number(control.value) || 0; + if (term === null || days <= 0 || days <= term) { + return null; + } + return { exceedsTerm: { term } }; + }; + } + + private recalculateMaturityAmount(): void { + const raw = this.partyForm.getRawValue(); + this.partyForm + .get('maturityAmount') + ?.setValue( + this.maturityAmountOf(Number(raw.dailyDepositAmount) || 0, Number(raw.days) || 0, Number(raw.interestRate) || 0), + { emitEvent: false } + ); + } + + /** + * Standard recurring-deposit simple interest: each daily instalment earns for the time left to + * maturity, so the first earns on the full term and the last on none. That averages out to + * n(n+1)/2 instalment-days of interest on top of the n instalments themselves. + * + * At a zero rate this is simply the total of the instalments. + */ + private maturityAmountOf(dailyAmount: number, days: number, ratePercent: number): number { + if (dailyAmount <= 0 || days <= 0) { + return 0; + } + const principal = dailyAmount * days; + const interest = ((dailyAmount * days * (days + 1)) / 2) * (ratePercent / 100) * (1 / 365); + return Math.round((principal + interest) * 100) / 100; + } + + // ---------------------------------------------------------------- nominees + + get nominees(): UntypedFormArray { + return this.partyForm.get('nominees') as UntypedFormArray; + } + + private buildNomineeGroup(): UntypedFormGroup { + const group = this.formBuilder.group({ + nominationDate: [ + '', + Validators.required + ], + name: [ + '', + Validators.required + ], + // A nominee the bank cannot reach, or whose age it does not know, is a name and nothing more - + // and the age is what decides whether a guardian has to be named at all. + mobileNo: [ + '', + [ + Validators.required, + Validators.pattern(CreatePartyComponent.MOBILE_RE) + ] + ], + birthDate: [ + '', + Validators.required + ], + relation: [ + '', + Validators.required + ], + gender: [ + '', + Validators.required + ], + guardianName: [''], + // One of these belongs to each split and neither to both: a share under Percentage, a place + // in the order under Sequence. Required-ness is set from the split mode rather than here, so + // switching the mode does not leave a stale rule behind. + sharePercent: [''], + sequenceNo: [''] + }); + + this.applySplitModeTo(group, this.partyForm?.get('nomineeSplitMode')?.value); + + // A nominee who is a minor cannot receive the proceeds themselves, so a guardian is named. + // Driven by the date of birth, which is also what decides whether they are a minor at all. The + // clerk is asked for the date once and the rest follows, rather than being asked a second time + // in a way that can contradict the first. + const applyGuardianRule = () => { + const guardian = group.get('guardianName'); + if (!guardian) { + return; + } + if (this.isNomineeMinor(group)) { + guardian.setValidators(Validators.required); + } else { + guardian.clearValidators(); + guardian.setValue('', { emitEvent: false }); + } + guardian.updateValueAndValidity({ emitEvent: false }); + }; + + // Both dates decide it: being under age is the gap between them, so a correction to either can + // turn a nominee into a minor or out of one. + group.get('birthDate')?.valueChanges.subscribe(applyGuardianRule); + group.get('nominationDate')?.valueChanges.subscribe(applyGuardianRule); + + return group; + } + + addNominee(): void { + this.nominees.push(this.buildNomineeGroup()); + } + + removeNominee(index: number): void { + this.nominees.removeAt(index); + } + + /** True while the deposit is being split by share rather than by order of entitlement. */ + get splitByPercentage(): boolean { + return this.partyForm?.get('nomineeSplitMode')?.value === 'Percentage'; + } + + /** What the shares entered so far come to, so the clerk can see the remainder as they type. */ + get nomineeShareTotal(): number { + return this.nominees.controls.reduce((sum, group) => sum + Number(group.get('sharePercent')?.value || 0), 0); + } + + /** + * A nomination that does not account for the whole deposit names nobody for the remainder, so the + * form will not submit one. Shown as a running total rather than only on save, because a clerk + * who has typed four shares wants to know which one to change, not that the set is wrong. + * + * Silent until at least one share has been typed. A blank form has nothing wrong with it yet, and + * opening the tab to a red warning tells the clerk off for work they have not had a chance to do. + */ + get nomineeShareError(): string | null { + if (!this.splitByPercentage || this.nominees.length === 0 || !this.anyShareEntered) { + return null; + } + const total = this.nomineeShareTotal; + if (total === 100) { + return null; + } + const shortBy = Math.round((100 - total) * 100) / 100; + return shortBy > 0 ? `${shortBy}% still to allocate` : `${Math.abs(shortBy)}% over`; + } + + /** Whether the clerk has started allocating at all, which is what turns the total into a warning. */ + private get anyShareEntered(): boolean { + return this.nominees.controls.some((group) => String(group.get('sharePercent')?.value ?? '').trim() !== ''); + } + + /** True while the deposit passes down an order of entitlement rather than being split by share. */ + get splitBySequence(): boolean { + return this.partyForm?.get('nomineeSplitMode')?.value === 'Sequence'; + } + + /** + * That every place in the order is filled exactly once. + * + * Two nominees given the same place leaves the bank to choose between them, and a gap leaves + * nobody at that place - both are the order failing to say what it exists to say. Checked across + * the whole list, because 1 and 3 are each perfectly valid on their own. + */ + get nomineeOrderError(): string | null { + if (!this.splitBySequence || this.nominees.length === 0) { + return null; + } + const entered = this.nominees.controls + .map((group) => String(group.get('sequenceNo')?.value ?? '').trim()) + .filter((value) => value !== '') + .map(Number); + + if (entered.length === 0) { + return null; + } + const duplicate = entered.find((value, index) => entered.indexOf(value) !== index); + if (duplicate !== undefined) { + return `Order ${duplicate} is used twice.`; + } + if (entered.length < this.nominees.length) { + return 'Every nominee needs a place in the order.'; + } + const missing = Array.from({ length: this.nominees.length }, (_, i) => i + 1).filter((n) => !entered.includes(n)); + return missing.length > 0 ? `Order ${missing.join(', ')} is missing.` : null; + } + + /** + * Turns on whichever of the two the split actually uses, and clears the other. + * + * Leaving the unused one populated would send the server a share on a Sequence nomination, or an + * order on a Percentage one - a second answer to a question that split does not ask. + */ + private applySplitModeTo(group: AbstractControl, splitMode: string): void { + const share = group.get('sharePercent'); + const order = group.get('sequenceNo'); + const byPercentage = splitMode === 'Percentage'; + + if (share) { + if (byPercentage) { + share.setValidators([ + Validators.required, + Validators.min(0.01), + Validators.max(100) + ]); + } else { + share.clearValidators(); + share.setValue('', { emitEvent: false }); + } + share.updateValueAndValidity({ emitEvent: false }); + } + + if (order) { + if (byPercentage) { + order.clearValidators(); + order.setValue('', { emitEvent: false }); + } else { + order.setValidators([ + Validators.required, + Validators.min(1) + ]); + } + order.updateValueAndValidity({ emitEvent: false }); + } + } + + /** + * Whether this nominee was under age on the day they were nominated, worked out from their date + * of birth rather than from a separate Yes/No. + * + * Measured against the nomination date, not today, so the form agrees with what the server + * stores: a nominee who has since come of age was still a minor on the day they were named, and + * the guardian named beside them belongs to that day. + */ + isNomineeMinor(control: AbstractControl): boolean { + const birth = control.get('birthDate')?.value; + const nominated = control.get('nominationDate')?.value; + if (!birth || !nominated) { + return false; + } + const birthDate = new Date(birth); + const majorityOn = new Date(nominated); + majorityOn.setFullYear(majorityOn.getFullYear() - 18); + return birthDate > majorityOn; + } + + // ------------------------------------------------------------------ actions + + cancel(): void { + this.partyId = null; + this.partyStatus = 'ACTIVE'; + this.linkedSavingsAccountId = null; + this.savingsAccountNo = null; + this.savingsProductName = null; + this.savingsAccountStatus = null; + this.savingsAccountBalance = null; + this.majorOn = null; + this.minorTurnedMajor = false; + this.loadedClientName = ''; + this.loadedAgentName = ''; + this.loadedGuardianName = ''; + this.clientLookupFailed = false; + this.agentLookupFailed = false; + this.partyForm.reset(); + this.partyForm.patchValue({ + nominal: 'No', + operation: 'Self', + adultMinor: 'Adult', + lien: 'No', + lienReason: '', + isSmsActive: true, + isEmailActive: false, + freezeCode: 'Normal', + advCode: 'None', + nomineeApplicable: 'No', + nomineeSplitMode: 'Percentage', + currentBalance: 0, + openingBalance: 0, + maturityAmount: 0, + interestRate: 0 + }); + this.nominees.clear(); + this.applyAdultMinorRules(); + this.applyAdvCodeRules(); + } + + exit(): void { + this.router.navigate(['/accounts/dds']); + } + + save(): void { + const raw = this.partyForm.getRawValue(); + const dateFormat = this.settingsService.dateFormat; + const locale = this.settingsService.language.code; + const asDate = (value: any) => (value ? this.dateUtils.formatDate(value, dateFormat) : undefined); + + const payload: any = { + // Either a client picked from the search list or an id typed straight in. + clientId: this.idOf(raw.clientId), + // idOf, not Number: the control now holds the picked agent object, and Number({}) is NaN. + agentId: this.idOf(raw.agentId), + officeId: raw.officeId, + savingsAccountNo: this.linkedSavingsAccountId ? undefined : raw.savingsAccountNo || undefined, + subType: raw.subType, + lastAccount: raw.lastAccount || undefined, + nominal: raw.nominal === 'Yes', + aadharNo: raw.aadharNo || undefined, + partyName: raw.partyName, + partyName1: raw.partyName1 || undefined, + address1: raw.address1, + address2: raw.address2 || undefined, + address3: raw.address3 || undefined, + city: raw.city, + pincode: raw.pincode, + gender: raw.gender || undefined, + panCardNo: raw.panCardNo || undefined, + telephone: raw.telephone || undefined, + mobileNoSms: raw.mobileNoSms, + operation: raw.operation, + birthDate: asDate(raw.birthDate), + occupation: raw.occupation || undefined, + risk: raw.risk || undefined, + ckycId: raw.ckycId || undefined, + adultMinor: raw.adultMinor, + minorDate: asDate(raw.minorDate), + guardianClientId: raw.guardianClientId ? this.idOf(raw.guardianClientId) : undefined, + minAccountOpt: raw.minAccountOpt || undefined, + relation: raw.relation || undefined, + openingDate: asDate(raw.openingDate), + month: raw.month || undefined, + reopenDate: asDate(raw.reopenDate), + depositAmount: raw.depositAmount || undefined, + dailyDepositAmount: raw.dailyDepositAmount, + days: raw.days, + ledgerNo: raw.ledgerNo || undefined, + interestRate: raw.interestRate || 0, + withdrawType: raw.withdrawType || undefined, + lien: raw.lien === 'Yes', + // Sent only when the money is actually held. Releasing it clears the reason on the + // server, so a released account does not carry an explanation of a hold it no + // longer has. + lienReason: raw.lien === 'Yes' ? (raw.lienReason === 'Other' ? raw.lienReasonOther : raw.lienReason) : undefined, + isSmsActive: !!raw.isSmsActive, + isEmailActive: !!raw.isEmailActive, + spDescription: raw.spDescription || undefined, + freezeCode: raw.freezeCode || undefined, + advCode: raw.advCode || undefined, + creditToBranch: raw.creditToBranch || undefined, + creditToAccountType: raw.creditToAccountType || undefined, + creditToAccount: raw.creditToAccount || undefined, + nomineeApplicable: raw.nomineeApplicable === 'Yes', + nomineeSplitMode: raw.nomineeSplitMode || undefined, + dateFormat, + locale, + nominees: (raw.nominees || []).map((nominee: any) => ({ + // Sent through the same asDate as every other date on this form. A payload states one + // dateFormat and the server reads all of it that way, so a nominee date written in a second + // format is one the server is right to refuse. + nominationDate: asDate(nominee.nominationDate), + name: nominee.name, + mobileNo: nominee.mobileNo || undefined, + birthDate: asDate(nominee.birthDate), + relation: nominee.relation, + gender: nominee.gender, + guardianName: nominee.guardianName || undefined, + // Only the one its split actually uses is sent. The other is left out entirely, so the + // record says it was not stated rather than carrying a zero the server would have to tell + // apart from a real entitlement or a real place in the order. + sharePercent: this.splitByPercentage ? Number(nominee.sharePercent) : undefined, + sequenceNo: this.splitBySequence ? Number(nominee.sequenceNo) : undefined + })) + }; + + if (this.partyId) { + this.organizationService.updateParty(this.partyId, payload).subscribe(() => this.exit()); + } else { + this.organizationService.createParty(payload).subscribe(() => this.exit()); + } + } + + closeAccount(): void { + if (!this.partyId) { + return; + } + const dateFormat = this.settingsService.dateFormat; + const locale = this.settingsService.language.code; + this.organizationService + .closeParty(this.partyId, { + closeDate: this.dateUtils.formatDate(new Date(), dateFormat), + dateFormat, + locale + }) + .subscribe(() => this.exit()); + } + + /** + * Whether this account can still be closed before its term is up. + * + * Only while the maturity date is ahead: once it has passed the account is paid out and closed on + * its own terms, and there is no shortfall to settle. The scheme's lock-in is deliberately not + * checked here - that period lives on the scheme, and a copy of the rule on this screen would drift + * from the one the server applies. An account inside its lock-in is offered the button and refused + * with the date it becomes closable, which tells the officer more than a missing button would. + */ + /** + * The client and agent as ids the API understands. Both fields are autocompletes, so each holds + * either the object picked from the list or the raw text typed into it - the dialog needs the + * client to look up which accounts can take the payout, and the agent's code only to name who is + * being charged. + */ + private get clientIdOfForm(): any { + const value = this.partyForm.get('clientId')?.value; + return value && typeof value === 'object' ? value.id : value; + } + + private get agentCodeOfForm(): string { + const value = this.partyForm.get('agentId')?.value; + return value && typeof value === 'object' ? value.agentCode : String(value ?? ''); + } + + get canCloseEarly(): boolean { + const maturity = this.partyForm.get('maturityDate')?.value; + if (!maturity) { + return false; + } + const matured = maturity instanceof Date ? maturity : new Date(maturity); + return !isNaN(matured.getTime()) && matured.getTime() > Date.now(); + } + + /** + * Closing early. The dialog asks the one thing the bank leaves open - whether the customer or the + * agent carries the cost - and shows what the scheme charges for each. Everything else, including + * the lock-in, is applied by the server. + */ + closeEarly(): void { + if (!this.partyId) { + return; + } + const dialogRef = this.dialog.open(PrematureCloseDialogComponent, { + data: { + partyCode: this.partyForm.get('partyCode')?.value, + partyName: this.partyForm.get('partyName')?.value, + subType: this.partyForm.get('subType')?.value, + interestRate: this.partyForm.get('interestRate')?.value, + maturityDate: this.partyForm.get('maturityDate')?.value, + currentBalance: this.partyForm.get('currentBalance')?.value, + clientId: this.clientIdOfForm, + agentCode: this.agentCodeOfForm + }, + width: '560px', + autoFocus: false + }); + + dialogRef.afterClosed().subscribe((closeCommand: any) => { + if (!closeCommand) { + return; + } + // The date goes up the way every other date on this system does - formatted to the user's own + // setting, with that setting and their locale alongside so the server reads it back the same + // way. An ordinary close sends it like this too; two ways of closing an account should not + // disagree about something as plain as what day it is. + const dateFormat = this.settingsService.dateFormat; + const locale = this.settingsService.language.code; + this.organizationService + .prematureCloseParty(this.partyId as number, { + ...closeCommand, + closeDate: this.dateUtils.formatDate(new Date(), dateFormat), + dateFormat, + locale + }) + .subscribe(() => this.exit()); + // A refusal - inside the lock-in, held under lien, KYC outstanding - is shown by the platform's + // own error handler, which names the reason. Repeating it here would say the same thing twice. + }); + } + + // ------------------------------------------------------------------ helpers + + /** + * Splits a document type's name into lower-case words, so an identifier can be recognised by a + * whole word rather than a substring. + * + * Banks name these types freely - the records here carry "PAN Card" and "Aadhaar Card", while the + * code list also offers bare "PAN" and "AADHAAR" - so an exact-name match misses them. A substring + * match is worse: "COMPANY EMP ID" contains "pan", and would be filed as the customer's PAN. + */ + private wordsOf(name: any): string[] { + return String(name || '') + .toLowerCase() + .split(/[^a-z0-9]+/) + .filter((word) => word.length > 0); + } + + /** + * Indian digit grouping - 12,34,567.89 rather than 1,234,567.89. An officer reading the first + * says "twelve lakh" at a glance; the second has to be counted. + */ + private static readonly MONEY = new Intl.NumberFormat('en-IN', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }); + + /** + * A figure the system keeps rather than the officer types, shown as money: grouped, always to + * paise, and right-aligned so a column of them lines up. The control keeps the plain number. + */ + money(controlName: string): string { + const value = Number(this.partyForm.get(controlName)?.value); + return Number.isFinite(value) ? CreatePartyComponent.MONEY.format(value) : ''; + } + + /** A client field holds either the record picked from the search list or an id typed straight in. */ + private idOf(value: any): number { + return Number(value && typeof value === 'object' ? value.id : value); + } + + private setField(controlName: string, value: any): void { + if (value === null || value === undefined || value === '') { + return; + } + this.partyForm.get(controlName)?.setValue(String(value)); + } + + /** Fineract hands dates back as [yyyy, mm, dd] in places and as a string in others. */ + private toDate(value: any): Date | null { + if (!value) { + return null; + } + if (value instanceof Date) { + return value; + } + if (Array.isArray(value) && value.length >= 3) { + return new Date(value[0], value[1] - 1, value[2]); + } + const parsed = new Date(value); + return isNaN(parsed.getTime()) ? null : parsed; + } + + private patchFormFromParty(party: any): void { + this.partyId = party.id; + this.partyStatus = party.status || 'ACTIVE'; + this.linkedSavingsAccountId = party.savingsAccountId || null; + this.savingsAccountNo = party.savingsAccountNo || null; + this.savingsProductName = party.savingsProductName || null; + this.savingsAccountStatus = party.savingsAccountStatus || null; + this.savingsAccountBalance = party.savingsAccountBalance ?? null; + this.majorOn = party.majorOn || null; + this.minorTurnedMajor = party.minorTurnedMajor === true; + this.loadedClientName = party.clientName || ''; + + // Needed for the Credit To A/C dropdown to show the saved account as chosen rather than blank, + // so it is fetched on edit as well as when a client is first looked up. + if (party.clientId) { + this.loadCreditToAccounts(String(party.clientId)); + } + this.loadedAgentName = party.agentName || ''; + this.partyForm.patchValue({ + clientId: party.clientId, + subType: party.subType, + agentId: party.agentId, + officeId: party.officeId, + partyCode: party.partyCode, + lastAccount: party.lastAccount, + nominal: party.nominal ? 'Yes' : 'No', + aadharNo: party.aadharNo, + partyName: party.partyName, + partyName1: party.partyName1, + address1: party.address1, + address2: party.address2, + address3: party.address3, + city: party.city, + pincode: party.pincode, + gender: party.gender, + panCardNo: party.panCardNo, + telephone: party.telephone, + mobileNoSms: party.mobileNoSms, + operation: party.operation, + birthDate: this.toDate(party.birthDate), + occupation: party.occupation, + risk: party.risk, + ckycId: party.ckycId, + adultMinor: party.adultMinor, + minorDate: this.toDate(party.minorDate), + guardianClientId: party.guardianClientId, + minAccountOpt: party.minAccountOpt, + relation: party.relation, + openingDate: this.toDate(party.openingDate), + month: party.month, + maturityDate: this.toDate(party.maturityDate), + reopenDate: this.toDate(party.reopenDate), + depositAmount: party.depositAmount, + dailyDepositAmount: party.dailyDepositAmount, + days: party.days, + ledgerNo: party.ledgerNo, + maturityAmount: party.maturityAmount, + interestRate: party.interestRate, + withdrawType: party.withdrawType, + od1Flag: party.od1Flag, + od2Flag: party.od2Flag, + lien: party.lien ? 'Yes' : 'No', + lienReason: this.listedLienReason(party.lienReason), + lienReasonOther: this.listedLienReason(party.lienReason) === 'Other' ? party.lienReason : '', + payDate: this.toDate(party.payDate), + paymentAmount: party.paymentAmount, + currentBalance: party.currentBalance, + openingBalance: party.openingBalance, + isSmsActive: party.isSmsActive, + isEmailActive: party.isEmailActive, + interestCalc: party.interestCalc, + interestDate: this.toDate(party.interestDate), + spDescription: party.spDescription, + freezeCode: party.freezeCode, + advCode: party.advCode, + creditToBranch: party.creditToBranch, + creditToAccountType: party.creditToAccountType, + creditToAccount: party.creditToAccount, + nomineeApplicable: party.nomineeApplicable ? 'Yes' : 'No', + nomineeSplitMode: party.nomineeSplitMode + }); + + this.nominees.clear(); + (party.nominees || []).forEach((nominee: any) => { + const group = this.buildNomineeGroup(); + group.patchValue({ + nominationDate: this.toDate(nominee.nominationDate), + name: nominee.name, + mobileNo: nominee.mobileNo, + birthDate: this.toDate(nominee.birthDate), + relation: nominee.relation, + gender: nominee.gender, + guardianName: nominee.guardianName, + sharePercent: nominee.sharePercent, + sequenceNo: nominee.sequenceNo + }); + this.nominees.push(group); + }); + + this.applyAdultMinorRules(); + this.applyAdvCodeRules(); + + // A closed party is a record to look at, not to change. + if (this.isClosed) { + this.partyForm.disable({ emitEvent: false }); + } + } +} diff --git a/src/app/deposits/party-master/due-for-closure/due-for-closure.component.html b/src/app/deposits/party-master/due-for-closure/due-for-closure.component.html new file mode 100644 index 000000000..e07a1271b --- /dev/null +++ b/src/app/deposits/party-master/due-for-closure/due-for-closure.component.html @@ -0,0 +1,163 @@ + + +
+
+
+
+
+

Accounts Due for Closure

+

Matured DDS accounts still open — pay the customer, then close the account.

+
+
+
+ +
+
+ + @if (!loading && parties.length > 0) { +
+
+
+
+
{{ parties.length | number }}
+
Outstanding
+
+
+
+
+
+
{{ totalPayable | number: '1.2-2' }}
+
Still to Pay
+
+
+ + @if (awaitingCloseCount > 0) { +
+
+
+
{{ awaitingCloseCount | number }}
+
Ready to Close
+
+
+ } +
+ } + +
+ @if (loading) { +

Loading…

+ } @else if (parties.length === 0) { + +
+
+

Nothing to close

+

No DDS account has matured and been left open. Every matured account has been paid out and closed.

+
+ } @else { +
+ + + + + + + + + + + + + @for (party of parties; track party.id) { + + + + + + + + + } + +
PartyCustomerCollected ByMatured OnPayableAction
+
+
{{ party.partyCode }}
+ +
+
{{ party.partyName }} +
+
{{ party.agentName || '—' }}
+
{{ party.agentCode }}
+
+
+
+
{{ party.maturityDate | dateFormat }}
+ +
+ {{ daysOverdue(party) }} {{ daysOverdue(party) === 1 ? 'day' : 'days' }} ago +
+
+
+ @if (awaitingPayout(party)) { + {{ party.currentBalance | number: '1.2-2' }} + } @else { + + Paid out + } + + + @if (heldUnderLien(party)) { + + + + Held under lien + + } @else if (awaitingPayout(party)) { + + } @else { + + } +
+
+ } +
+
diff --git a/src/app/deposits/party-master/due-for-closure/due-for-closure.component.scss b/src/app/deposits/party-master/due-for-closure/due-for-closure.component.scss new file mode 100644 index 000000000..473e99186 --- /dev/null +++ b/src/app/deposits/party-master/due-for-closure/due-for-closure.component.scss @@ -0,0 +1,97 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +@use 'assets/styles/product-list' as pl; + +@include pl.product-list-styles; + +/* An empty list here means the work is done, so the mark is the reassuring colour, not the grey one. */ +.dfc-empty-ic { + color: #15803d; +} + +.dfc-account { + font-family: ui-monospace, 'SF Mono', 'Cascadia Mono', monospace; + font-size: 0.75rem; +} + +/* + * A maturity the bank has been sitting on for over a month. Called out because the date alone reads + * the same whether it passed yesterday or in the spring, and the two are not the same problem. + */ +.dfc-stale { + color: #b45309; + font-weight: 500; +} + +/* + * An account whose money has already gone out. Words rather than 0.00, so a settled account is not + * read as one that was never collected on - both would show the same figure. + */ +.dfc-paid { + color: #15803d; + font-weight: 600; + font-size: 0.78rem; +} + +/* + * The close action. Green rather than the list's usual blue because it is the step that finishes the + * account, and it only ever appears on a row already paid. + */ +.dfc-close-btn { + color: #15803d; + border-color: rgb(21 128 61 / 30%); + background: rgb(21 128 61 / 6%); + + &:hover { + background: rgb(21 128 61 / 12%); + border-color: rgb(21 128 61 / 45%); + } +} + +/* + * A deposit pledged against a loan. Not a button, because there is nothing for the clerk to do here + * until the loan is cleared — showing one would only lead to a refusal. + */ +.dfc-lien { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border: 1px solid rgb(90 74 156 / 30%); + border-radius: 4px; + background: rgb(90 74 156 / 8%); + color: #5a4a9c; + font-size: 0.78rem; + font-weight: 600; + cursor: help; +} + +/* Plain table rather than mat-table: this list is short by design and needs no sort or paging. */ +.dfc-table { + th, + td { + padding: 10px 14px; + text-align: left; + border-bottom: 1px solid rgb(0 0 0 / 6%); + } + + th { + font-size: 0.72rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: rgb(0 0 0 / 55%); + background: rgb(0 0 0 / 2%); + } + + .num { + text-align: right; + font-variant-numeric: tabular-nums; + } +} diff --git a/src/app/deposits/party-master/due-for-closure/due-for-closure.component.ts b/src/app/deposits/party-master/due-for-closure/due-for-closure.component.ts new file mode 100644 index 000000000..7792b2bda --- /dev/null +++ b/src/app/deposits/party-master/due-for-closure/due-for-closure.component.ts @@ -0,0 +1,194 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** Angular Imports */ +import { Component, OnInit, inject } from '@angular/core'; +import { MatDialog } from '@angular/material/dialog'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** Custom Services */ +import { OrganizationService } from 'app/organization/organization.service'; +import { AlertService } from 'app/core/alert/alert.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { Dates } from 'app/core/utils/dates'; + +/** Custom Components */ +import { PayoutDialogComponent } from '../payout-dialog/payout-dialog.component'; +import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component'; + +/** + * The DDS accounts whose term has run out and which are still open - the branch's list of closures + * waiting to be done. + * + * A list of work, not a report. Nothing closes itself: a matured account is money the bank now owes + * the customer, and where it goes has to be agreed with them, so each one waits here until an + * officer deals with it. Before this existed the maturity date simply passed and nobody was told - + * the bank held the money and the customer had to think to ask for it. + * + * Two steps, not one, and an account stays on this list through both: + * + * balance owing pay it out - to another account of theirs, or into matured-deposits payable + * balance nil paid; close it + * + * Splitting them is what a branch actually does. The officer pays, sees the balance fall to nil and + * the money land where it was meant to, and only then closes. One click doing both would close + * accounts whose payment nobody had checked, and a closed account is far harder to put right. + * + * Oldest maturity first, because that is the one the bank has been holding longest. + */ +@Component({ + selector: 'mifosx-due-for-closure', + templateUrl: './due-for-closure.component.html', + styleUrls: ['./due-for-closure.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent + ] +}) +export class DueForClosureComponent implements OnInit { + private organizationService = inject(OrganizationService); + private dialog = inject(MatDialog); + private alertService = inject(AlertService); + private settingsService = inject(SettingsService); + private dateUtils = inject(Dates); + + parties: any[] = []; + loading = true; + + ngOnInit(): void { + this.load(); + } + + private load(): void { + this.loading = true; + this.organizationService.getPartiesDueForClosure().subscribe({ + next: (parties: any) => { + this.parties = Array.isArray(parties) ? parties : parties?.pageItems || []; + this.loading = false; + }, + error: () => { + this.parties = []; + this.loading = false; + } + }); + } + + /** What the bank is holding in total across every matured account still open. */ + get totalPayable(): number { + return this.parties.reduce((sum, party) => sum + Number(party.currentBalance || 0), 0); + } + + /** + * Whether this one is still waiting on its payment. Anything left on the account is the customer's + * money, so it is paid before the account can be closed. + */ + awaitingPayout(party: any): boolean { + return Number(party.currentBalance || 0) > 0; + } + + /** + * Whether this deposit is standing as security for a loan. + * + * The term has ended and the money is owed, but it cannot leave while it is pledged - so the row + * says so rather than offering a Pay Out the server would refuse. A customer who needed cash before + * their term ended borrowed against the deposit instead of breaking it; the deposit is paid when + * that loan is cleared. + */ + heldUnderLien(party: any): boolean { + return Number(party.amountHeldUnderLien || 0) > 0; + } + + /** How many are already paid and now only need closing - the quicker half of the list. */ + get awaitingCloseCount(): number { + return this.parties.filter((party) => !this.awaitingPayout(party)).length; + } + + /** + * How long the bank has been sitting on this one. Shown because a maturity from three months ago + * is a different problem from one from yesterday, and the list alone would not say so. + */ + daysOverdue(party: any): number { + const matured = this.toDate(party.maturityDate); + if (!matured) { + return 0; + } + return Math.max(0, Math.floor((Date.now() - matured.getTime()) / 86400000)); + } + + /** The API sends dates as [year, month, day]; month is 1-based there and 0-based in Date. */ + private toDate(value: any): Date | null { + if (Array.isArray(value) && value.length >= 3) { + return new Date(value[0], value[1] - 1, value[2]); + } + return value ? new Date(value) : null; + } + + payOut(party: any): void { + const dialogRef = this.dialog.open(PayoutDialogComponent, { + data: party, + width: '520px', + autoFocus: false + }); + + dialogRef.afterClosed().subscribe((payoutCommand: any) => { + if (!payoutCommand) { + return; + } + this.organizationService.payOutParty(party.id, payoutCommand).subscribe({ + next: () => { + this.alertService.alert({ + type: 'Party Paid Out', + message: payoutCommand.toSavingsAccountNo + ? `${party.partyCode} paid into ${payoutCommand.toSavingsAccountNo}. Close the account once you have checked it.` + : `${party.partyCode} paid into Matured Deposits Payable, ready to transfer out. Close the account once you have checked it.` + }); + this.load(); + } + // A refusal is shown by the platform's own error handler, which names the reason - repeating + // it here would say the same thing twice. + }); + }); + } + + /** + * Closes an account whose money has already gone out. The server refuses one still carrying a + * balance, so this cannot close an account out from under an unpaid customer. + * + * Asked for first, because closing is the one step here with no way back. + */ + closeAccount(party: any): void { + const confirmation = this.dialog.open(ConfirmationDialogComponent, { + data: { + heading: `Close ${party.partyCode}?`, + dialogContext: `${party.partyName}'s account has been paid out in full and can be closed. It cannot be reopened.`, + type: 'warn' + } + }); + + confirmation.afterClosed().subscribe((response: any) => { + if (!response?.confirm) { + return; + } + const dateFormat = this.settingsService.dateFormat; + const locale = this.settingsService.language.code; + this.organizationService + .closeParty(party.id, { + closeDate: this.dateUtils.formatDate(new Date(), dateFormat), + dateFormat, + locale + }) + .subscribe({ + next: () => { + this.alertService.alert({ type: 'Party Closed', message: `${party.partyCode} is closed.` }); + this.load(); + } + }); + }); + } +} diff --git a/src/app/deposits/party-master/nominee-list-dialog/nominee-list-dialog.component.html b/src/app/deposits/party-master/nominee-list-dialog/nominee-list-dialog.component.html new file mode 100644 index 000000000..d9bf7327f --- /dev/null +++ b/src/app/deposits/party-master/nominee-list-dialog/nominee-list-dialog.component.html @@ -0,0 +1,73 @@ + + +

+ Nominees + {{ data.partyCode }} · {{ data.partyName }} +

+ + + @if (nominees.length === 0) { +

Nobody is nominated on this account.

+ } @else { +

+ @if (bySequence) { + Paid in the order listed — the first named who can be found receives the deposit. + } @else { + Split by share of the deposit. + } +

+ + + + + + + + + @if (!bySequence) { + + } + + + + @for (nominee of nominees; track nominee.id; let i = $index) { + + + + + + + @if (!bySequence) { + + } + + } + +
@if (bySequence) { Order } @else { # }NameRelationNominatedShare
{{ nominee.sequenceNo ?? i + 1 }} + {{ nominee.name }} + @if (nominee.minor) { + minor · guardian {{ nominee.guardianName || '—' }} + } + {{ nominee.relation }}{{ nominee.nominationDate | dateFormat }} + @if (nominee.sharePercent !== null && nominee.sharePercent !== undefined) { + {{ nominee.sharePercent }}% + } @else { + — + } +
+ + @if (shareWarning) { +

{{ shareWarning }}

+ } + } +
+ + + + diff --git a/src/app/deposits/party-master/nominee-list-dialog/nominee-list-dialog.component.scss b/src/app/deposits/party-master/nominee-list-dialog/nominee-list-dialog.component.scss new file mode 100644 index 000000000..cfcbc78ea --- /dev/null +++ b/src/app/deposits/party-master/nominee-list-dialog/nominee-list-dialog.component.scss @@ -0,0 +1,84 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +.nd-title { + display: flex; + flex-direction: column; + gap: 2px; + margin-bottom: 0; +} + +.nd-party { + font-size: 13px; + font-weight: 400; + color: rgb(0 0 0 / 55%); +} + +.nd-body { + min-width: 420px; + padding-top: 8px; +} + +.nd-mode { + margin: 0 0 12px; + font-size: 13px; + color: rgb(0 0 0 / 60%); +} + +.nd-empty { + margin: 8px 0; + color: rgb(0 0 0 / 55%); +} + +.nd-table { + width: 100%; + border-collapse: collapse; + font-size: 14px; + + th { + text-align: left; + font-weight: 500; + font-size: 12px; + letter-spacing: 0.04em; + text-transform: uppercase; + color: rgb(0 0 0 / 50%); + padding: 0 8px 6px 0; + border-bottom: 1px solid rgb(0 0 0 / 10%); + } + + td { + padding: 8px 8px 8px 0; + border-bottom: 1px solid rgb(0 0 0 / 6%); + vertical-align: top; + } +} + +/* Shares line up to be read down the column, not across the row. */ +.nd-num { + text-align: right; + font-variant-numeric: tabular-nums; + padding-right: 0; +} + +.nd-seq { + width: 32px; + color: rgb(0 0 0 / 45%); + font-variant-numeric: tabular-nums; +} + +.nd-minor { + display: block; + font-size: 12px; + color: rgb(0 0 0 / 50%); +} + +.nd-warn { + margin: 12px 0 0; + font-size: 13px; + color: #b3261e; +} diff --git a/src/app/deposits/party-master/nominee-list-dialog/nominee-list-dialog.component.ts b/src/app/deposits/party-master/nominee-list-dialog/nominee-list-dialog.component.ts new file mode 100644 index 000000000..e6e5b4c10 --- /dev/null +++ b/src/app/deposits/party-master/nominee-list-dialog/nominee-list-dialog.component.ts @@ -0,0 +1,82 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** Angular Imports */ +import { Component, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { MAT_DIALOG_DATA, MatDialogModule } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; + +/** Custom Pipes */ +import { DateFormatPipe } from 'app/pipes/date-format.pipe'; + +/** + * Who this account is nominated to, read without opening it for editing. + * + * A clerk asked "who is on this account" has one question, not an intention to change anything, and + * the edit form answers it only after a load, a tab and a scroll - with every field live the whole + * time. This states it and closes. + * + * What is shown follows the split the account was nominated under: a share per nominee under + * Percentage, and the order they are paid in under Sequence, because that ordering is the whole of + * what Sequence records. + */ +@Component({ + selector: 'mifosx-nominee-list-dialog', + standalone: true, + imports: [ + CommonModule, + MatDialogModule, + MatButtonModule, + DateFormatPipe + ], + templateUrl: './nominee-list-dialog.component.html', + styleUrls: ['./nominee-list-dialog.component.scss'] +}) +export class NomineeListDialogComponent { + /** { partyCode, partyName, splitMode, nominees } */ + data = inject(MAT_DIALOG_DATA); + + get bySequence(): boolean { + return this.data?.splitMode === 'Sequence'; + } + + get nominees(): any[] { + return this.data?.nominees || []; + } + + /** + * What the shares come to. Shown only when it is not the whole deposit - a nomination recorded + * before shares were captured reads as complete otherwise, and the remainder has nobody named + * to receive it. + */ + get shareTotal(): number | null { + if (this.bySequence) { + return null; + } + const stated = this.nominees.filter((n) => n?.sharePercent !== null && n?.sharePercent !== undefined); + if (stated.length === 0) { + return null; + } + return stated.reduce((sum, n) => sum + Number(n.sharePercent || 0), 0); + } + + get shareWarning(): string | null { + if (this.bySequence) { + // Under Sequence the order is the whole of the nomination, so an order nobody stated is the + // same gap a missing share is under Percentage. + const stated = this.nominees.filter((n) => n?.sequenceNo !== null && n?.sequenceNo !== undefined); + return stated.length === this.nominees.length ? null : 'No order was recorded against these nominees.'; + } + const total = this.shareTotal; + if (total === null) { + return 'No shares were recorded against these nominees.'; + } + return total === 100 ? null : `The shares come to ${total}%, not 100%.`; + } +} diff --git a/src/app/deposits/party-master/party-master.component.html b/src/app/deposits/party-master/party-master.component.html index 416b9a6c7..218f2639d 100644 --- a/src/app/deposits/party-master/party-master.component.html +++ b/src/app/deposits/party-master/party-master.component.html @@ -6,538 +6,221 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
-
- -

Party Master Entry

+
+
+
+
+
+

Party Master

+

The DDS accounts on file — each one a customer collected from daily.

+
+
+
+
+ + {{ totalCount }} + {{ totalCount === 1 ? 'party' : 'parties' }} +
+ +
-
-
- - -
-
-
- -

Identity

-
-
- - Client ID - - - - - Sub Type - - @for (type of subTypes; track type) { - {{ type }} - } - - - - - Agent ID - - - - @if (linkedSavingsAccountId) { - - Savings Account - - - } @else { - - Savings Account No (if any) - - - } - - - Party Code - - - - - Last A/c - - - - - Nominal - - Yes - No - - - - - Party Name - - - - - Party Name 1 - - -
-
- -
-
- -

Address & Contact

-
-
- - Address 1 - - - - - Address 2 - - - - - Address 3 - - - - - City - - - - - Pincode - - - - - Gender - - Male - Female - Other - - - - - Telephone - - - - - Mobile No (SMS) - - -
-
- -
-
- -

KYC

-
-
- - Aadhar No - - - - - PAN Card No - - - - - CKYC ID - - - - - Operation - - @for (op of operations; track op) { - {{ op }} - } - - - - - Birth Date - - - - - - - Occupation - - @for (occ of occupations; track occ) { - {{ occ }} - } - - - - - Risk - - @for (r of riskLevels; track r) { - {{ r }} - } - - -
-
- -
-
- -

Minor / Guardian

-
-
- - Adult / Minor - - @for (opt of adultMinorOptions; track opt) { - {{ opt }} - } - - - - - Minor Date - - - - - - - Guardian Client ID - - - - - Min. A/c Opt - - - - - Relation - - -
-
- -
-
- -

Scheme Terms

-
-
- - Opening Date - - - - - - - Month - - - - - Maturity Date - - - - - Reopen Date - - - - - - - Deposit Amt - - - - - Daily Deposit Amt - - - - - Days - - - - - Ledger No - - - - - Mat. Amt - - - - - Interest Rate - - - - - Withdraw Type - - @for (type of withdrawTypes; track type) { - {{ type }} - } - - -
-
- -
-
- -

Status & Overdue Flags

-
-
-
- OD1 Flag -
-
- OD2 Flag -
- - - Lien - - Yes - No - - - - - Pay Date - - - - - - - Payment Amount - - - - - Current Bal - - - - - Opening Bal - - - -
- Is SMS Active -
-
- Is Email Active -
- - - Int.Calc - - - - - Interest Date - - - - - - - Sp.Description - - - - - Fr'z CD - - @for (code of freezeCodes; track code) { - {{ code }} - } - - - - - Adv Code - - @for (code of advCodes; track code) { - {{ code }} - } - - -
-
- - @if (partyForm.get('advCode').value === 'Transfer To Saving') { -
-
- -

Maturity Payout Routing

-
-
- - Credit To Branch - - - - - Credit To A/C Type - - - - - Credit To A/C - - -
-
- } -
-
- - -
-
-
- - Nominee - - Yes - No - - - - - Percentage - Sequence - - - -
- - @if (nominees.length === 0) { -

No nominees added yet.

- } - - @for (nominee of nominees.controls; track nominee; let i = $index) { -
- - Nom.Dt - - - - - - Name - - - - Mobile No - - - - Birth Dt - - - - - - Minor - - Yes - No - - - - Relation - - - - Gender - - Male - Female - Other - - - - Guar. Name - - - -
- } -
-
-
-
- - @if (isClosed) { -
- - This account is closed. No further collections or edits are possible. -
- } - -
- - - - @if (partyId && !isClosed) { - + + @if (dueForClosureCount > 0) { + + + + {{ dueForClosureCount }} + {{ dueForClosureCount === 1 ? 'account has' : 'accounts have' }} matured and + {{ dueForClosureCount === 1 ? 'is' : 'are' }} still open — + + @if (dueForClosurePayable > 0) { + {{ dueForClosurePayable | number: '1.2-2' }} still owed to customers. + } @else { + paid in full, waiting to be closed. } -
-
+ } @else { +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Party Code +
+
+
+
{{ p.partyCode }}
+
{{ p.city || '—' }}
+
+
+
Party Name{{ p.partyName }}Sub Type + {{ p.subTypeDescription || p.subType }} + Collected By +
+
{{ p.agentName || '—' }}
+
{{ p.agentCode }}
+
+
DDS Account + @if (p.savingsAccountNo) { +
+ +
{{ p.savingsAccountStatus }}
+
+ } @else { + + } +
Daily Deposit{{ p.dailyDepositAmount | number: '1.2-2' }}Current Balance + {{ p.currentBalance | number: '1.2-2' }} + Status + + {{ p.status === 'CLOSED' ? 'Closed' : 'Active' }} + + Nominees + @if (nomineeCount(p) > 0) { + + } @else { + None + } + Actions + +
+ + @if (filteredCount === 0 && totalCount > 0) { +
+ + No parties match your filter. +
+ } + + +
+ }
diff --git a/src/app/deposits/party-master/party-master.component.scss b/src/app/deposits/party-master/party-master.component.scss index baa31be66..b235f0f44 100644 --- a/src/app/deposits/party-master/party-master.component.scss +++ b/src/app/deposits/party-master/party-master.component.scss @@ -6,159 +6,128 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -$primary: #0f766e; +@use 'assets/styles/product-list' as pl; -.pm-container { - max-width: 1080px; - margin: 0 auto; - padding: 24px 16px; +@include pl.product-list-styles; + +/* Party-specific amber accent */ +:host { + --pl-accent: #b45309; + --pl-accent-l: #fef3c7; } -.pm-header { +/* A closed party stays listed - its collections and interest are still on the books. */ +.pm-row--closed td:not(.num) { + opacity: 0.55; +} + +.pm-pill { + display: inline-block; + padding: 3px 11px; + border-radius: 11px; + font-size: 0.72rem; + font-weight: 500; + line-height: 1.7; + background: #dcfce7; + color: #15803d; + white-space: nowrap; +} + +.pm-pill--off { + background: rgb(0 0 0 / 7%); + color: rgb(0 0 0 / 52%); +} + +/* The customer's real deposit account number, in the list. */ +.pm-list-account { + font-family: ui-monospace, 'SF Mono', 'Cascadia Mono', monospace; + font-size: 0.8rem; +} + +/* + A party with no account behind it. Called out rather than left blank: the balance beside it is + money the bank's own books have no record of, which is the thing worth noticing here. +*/ +.pm-list-no-account { + font-size: 0.75rem; + font-style: italic; + color: #9b2c2c; +} + +/* + Matured accounts still open. Deliberately loud: it is money the bank owes a customer who has not + been told, and every day it sits there is a day the bank is holding somebody else's money. Styled + as a link to the work rather than a notice about it - reading it should be the same click as + starting on it. +*/ +.pm-due-banner { display: flex; align-items: center; - gap: 16px; - margin-bottom: 20px; - - &__title { - margin: 0; - font-size: 22px; - font-weight: 500; - } -} - -.pm-back { - color: $primary; -} - -.pm-card { - background: var(--md-sys-color-surface, #fff); - border-radius: 8px; - box-shadow: 0 2px 8px rgb(0 0 0 / 8%); -} - -.pm-tabs { - ::ng-deep .mat-mdc-tab-body-wrapper { - min-height: 300px; - } -} - -.pm-tab-content { - padding: 20px 24px; -} - -.pm-section { - padding: 16px 0; - border-bottom: 1px solid rgb(0 0 0 / 8%); - - &:last-of-type { - border-bottom: none; - } - - &__header { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 16px; - - i { - color: $primary; - } - - h3 { - margin: 0; - font-size: 16px; - font-weight: 600; - flex: 1; - } - } -} - -.pm-grid { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 8px 16px; - align-items: start; -} - -.pm-field { - width: 100%; - - &--wide { - grid-column: span 2; - } -} - -.pm-checkbox-field { - display: flex; - align-items: center; - height: 56px; -} - -.pm-radio-group { - display: flex; - align-items: center; - gap: 16px; - height: 56px; -} - -.pm-add-nominee { - color: $primary; - height: fit-content; - align-self: center; -} - -.pm-empty { - color: rgb(0 0 0 / 50%); - font-size: 14px; - margin: 0 0 8px; -} - -.pm-nominee-row { - display: grid; - grid-template-columns: repeat(8, 1fr) auto; - gap: 8px 12px; - align-items: start; - padding: 10px 0; - border-top: 1px dashed rgb(0 0 0 / 10%); - - &:first-of-type { - border-top: none; - } -} - -.pm-remove-nominee { - color: #b91c1c; - margin-top: 8px; -} - -.pm-action-bar { - display: flex; - justify-content: flex-end; gap: 12px; - padding: 16px 24px; - border-top: 1px solid rgb(0 0 0 / 8%); -} - -.pm-callout { - display: flex; - align-items: center; - gap: 10px; - margin: 0 24px; + margin: 0 0 14px; padding: 12px 16px; - background: rgb(185 28 28 / 8%); - border: 1px solid rgb(185 28 28 / 25%); - border-radius: 6px; - color: #b91c1c; - font-size: 0.9rem; -} + background: rgb(180 83 9 / 8%); + border: 1px solid rgb(180 83 9 / 28%); + border-radius: 8px; + color: #92400e; + text-decoration: none; + transition: background 0.15s ease; -@media (width <= 1000px) { - .pm-grid { - grid-template-columns: repeat(2, 1fr); + &:hover { + background: rgb(180 83 9 / 13%); } - .pm-nominee-row { - grid-template-columns: repeat(2, 1fr); + &__ic { + font-size: 1.05rem; + flex: 0 0 auto; + } + + &__text { + flex: 1 1 auto; + font-size: 0.88rem; + line-height: 1.45; + } + + &__cta { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 0.82rem; + font-weight: 600; + white-space: nowrap; } } + +@media (width <= 700px) { + .pm-due-banner { + flex-wrap: wrap; + } +} + +/* + * The nominee count, which opens the list of names rather than the account itself. + * + * Styled as text that can be clicked rather than as a second action button: the row already has one + * action, and a pair of equal-looking buttons would put "look at this" beside "change this" as + * though the two were the same kind of thing. + */ +.pl-nominee-btn { + border: 0; + background: none; + padding: 0; + font: inherit; + color: var(--pl-accent); + font-weight: 500; + cursor: pointer; + text-decoration: underline; + text-underline-offset: 3px; + + &:hover { + text-decoration-thickness: 2px; + } +} + +.pl-muted { + color: rgb(0 0 0 / 40%); +} diff --git a/src/app/deposits/party-master/party-master.component.ts b/src/app/deposits/party-master/party-master.component.ts index 125525b16..d6641447a 100644 --- a/src/app/deposits/party-master/party-master.component.ts +++ b/src/app/deposits/party-master/party-master.component.ts @@ -7,42 +7,40 @@ */ /** Angular Imports */ -import { Component, inject } from '@angular/core'; -import { UntypedFormBuilder, UntypedFormGroup, UntypedFormArray, Validators } from '@angular/forms'; -import { Router } from '@angular/router'; -import { MatTabsModule } from '@angular/material/tabs'; -import { MatCheckbox } from '@angular/material/checkbox'; -import { MatRadioModule } from '@angular/material/radio'; +import { Component, OnInit, ViewChild, inject } from '@angular/core'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort, MatSortHeader } from '@angular/material/sort'; +import { + MatTable, + MatTableDataSource, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow +} from '@angular/material/table'; +import { MatTooltip } from '@angular/material/tooltip'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { MatDialog } from '@angular/material/dialog'; + /** Custom Services */ import { OrganizationService } from 'app/organization/organization.service'; -import { SettingsService } from 'app/settings/settings.service'; -import { Dates } from 'app/core/utils/dates'; + +/** Custom Components */ +import { NomineeListDialogComponent } from './nominee-list-dialog/nominee-list-dialog.component'; /** - * Party Master Entry component. + * Party Master list. * - * The customer's DDS account — linked to an Agent for daily collection, - * carrying its own scheme terms, KYC, two-stage overdue flags, and a - * nominee list. Modelled directly on the legacy Party Master Entry screen - * this replaces. - * - * Backed by its own DDS Party API (`/dds/parties`). The underlying account - * does not yet reuse the Recurring Deposit account engine (see Product - * Setup) — that linkage is a follow-up once the RD-account-specific fields - * this screen doesn't collect (interest/compounding periods, chart - * selection, ...) have a home. Until then the scheme/balance fields here - * are the Party's own. - * - * "Close Account" (command=close) permanently closes the Party once its - * balance is zero - the backend rejects the command otherwise. A closed - * Party can no longer be edited or receive new Party Tran collections. - * - * savingsAccountNo optionally links this Party to an existing, non-closed real Savings Account of - * the same client (resolved by account number, since that's what an officer has to hand) - - * clarified with the business (2026-08-24) as "link it if the client already has one, otherwise - * leave it". There is no unlink once linked. + * The DDS accounts on file - each one a customer's daily deposit account, opened under a scheme + * (Sub Type) and assigned to the agent who collects for it. Follows the same list -> create/edit + * shape as Agent Master and the product lists, and shares their stylesheet. */ @Component({ selector: 'mifosx-party-master', @@ -50,478 +48,132 @@ import { Dates } from 'app/core/utils/dates'; styleUrls: ['./party-master.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, - MatTabsModule, - MatCheckbox, - MatRadioModule + FaIconComponent, + MatTable, + MatSort, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatSortHeader, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, + MatPaginator, + MatTooltip ] }) -export class PartyMasterComponent { - private formBuilder = inject(UntypedFormBuilder); - private router = inject(Router); +export class PartyMasterComponent implements OnInit { private organizationService = inject(OrganizationService); - private settingsService = inject(SettingsService); - private dateUtils = inject(Dates); + private dialog = inject(MatDialog); - partyId: number | null = null; - selectedTabIndex = 0; - partyStatus = 'ACTIVE'; - linkedSavingsAccountId: number | null = null; - - get isClosed(): boolean { - return this.partyStatus === 'CLOSED'; - } - - subTypes = [ - 'Daily Deposit Scheme', - 'Masik Dhan Varsha Deposit' - ]; - operations = [ - 'Self', - 'Guardian', - 'Joint' - ]; - occupations = [ - 'Business', - 'Service', - 'Agriculture', - 'Homemaker', - 'Student', - 'Retired', - 'Other' - ]; - riskLevels = [ - 'Low', - 'Medium', - 'High' - ]; - adultMinorOptions = [ - 'Adult', - 'Minor' - ]; - withdrawTypes = [ - 'Cash', - 'Transfer', - 'Clearing' - ]; - freezeCodes = [ - 'Normal', - 'Debit Freeze', - 'Credit Freeze', - 'Total Freeze' - ]; - advCodes = [ - 'None', - 'Transfer To Saving', - 'Renew' + displayedColumns: string[] = [ + 'partyCode', + 'partyName', + 'subType', + 'agentName', + 'savingsAccountNo', + 'dailyDepositAmount', + 'currentBalance', + 'nominees', + 'status', + 'actions' ]; - partyForm: UntypedFormGroup = this.formBuilder.group({ - clientId: [ - '', - Validators.required - ], - subType: [ - '', - Validators.required - ], - agentId: [ - '', - Validators.required - ], - savingsAccountNo: [''], - lastAccount: [ - { - value: '', - disabled: true - } - ], - partyCode: [ - { - value: '', - disabled: true - } - ], - nominal: [ - 'No', - Validators.required - ], - aadharNo: [''], - partyName: [ - '', - Validators.required - ], - partyName1: [''], - address1: [ - '', - Validators.required - ], - address2: [''], - address3: [''], - city: [ - '', - Validators.required - ], - pincode: [ - '', - Validators.required - ], - gender: [''], - panCardNo: [''], - telephone: [''], - mobileNoSms: [ - '', - Validators.required - ], - operation: [ - 'Self', - Validators.required - ], - birthDate: [''], - occupation: [''], - risk: [''], - ckycId: [''], - adultMinor: [ - 'Adult', - Validators.required - ], - minorDate: [''], - guardianClientId: [''], - minAccountOpt: [''], - relation: [''], - openingDate: [ - '', - Validators.required - ], - month: [''], - maturityDate: [ - { - value: '', - disabled: true - } - ], - reopenDate: [''], - depositAmount: [0], - dailyDepositAmount: [ - 0, - Validators.required - ], - days: [ - '', - Validators.required - ], - ledgerNo: [ - { - value: '', - disabled: true - } - ], - maturityAmount: [ - { - value: 0, - disabled: true - } - ], - interestRate: [ - { - value: 0, - disabled: true - } - ], - withdrawType: [''], - od1Flag: [ - { - value: false, - disabled: true - } - ], - od2Flag: [ - { - value: false, - disabled: true - } - ], - lien: ['No'], - payDate: [''], - paymentAmount: [0], - currentBalance: [ - { - value: 0, - disabled: true - } - ], - openingBalance: [0], - isSmsActive: [true], - isEmailActive: [false], - interestCalc: [ - { - value: 0, - disabled: true - } - ], - interestDate: [''], - spDescription: [''], - freezeCode: ['Normal'], - advCode: ['None'], - creditToBranch: [''], - creditToAccountType: [''], - creditToAccount: [''], - nomineeApplicable: ['No'], - nomineeSplitMode: ['Percentage'], - nominees: this.formBuilder.array([]) - }); - - get nominees(): UntypedFormArray { - return this.partyForm.get('nominees') as UntypedFormArray; - } - - private buildNomineeGroup(): UntypedFormGroup { - return this.formBuilder.group({ - nominationDate: [ - '', - Validators.required - ], - name: [ - '', - Validators.required - ], - mobileNo: [''], - birthDate: [''], - minor: [ - 'No', - Validators.required - ], - relation: [ - '', - Validators.required - ], - gender: [ - '', - Validators.required - ], - guardianName: [''] + /** + * Who a party is nominated to, without opening it for editing. + * + * Asked often enough to belong on the list, and answered from what the list already holds - the + * parties come back with their nominees on them, so this opens rather than fetches. + */ + showNominees(party: any): void { + this.dialog.open(NomineeListDialogComponent, { + data: { + partyCode: party.partyCode, + partyName: party.partyName, + splitMode: party.nomineeSplitMode, + nominees: party.nominees || [] + }, + autoFocus: false }); } - addNominee(): void { - this.nominees.push(this.buildNomineeGroup()); + /** How many people are named on an account, which is what the list column shows. */ + nomineeCount(party: any): number { + return (party?.nominees || []).length; } - removeNominee(index: number): void { - this.nominees.removeAt(index); + /** + * Matured accounts still open, and what they add up to. Shown here because this is the screen an + * officer already opens - money the bank owes a customer should not wait on somebody thinking to + * run a report. + */ + dueForClosureCount = 0; + dueForClosurePayable = 0; + + dataSource: MatTableDataSource = new MatTableDataSource([]); + parties: any[] = []; + filteredCount = 0; + + @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator; + @ViewChild(MatSort, { static: true }) sort: MatSort; + + get totalCount(): number { + return this.parties.length; } - retrieve(): void { - const clientId = this.partyForm.get('clientId')?.value; - if (!clientId) { - return; - } + get activeCount(): number { + return this.parties.filter((party) => party.status !== 'CLOSED').length; + } + + /** What the whole book is worth today - every open account's running balance added up. */ + get totalCurrentBalance(): number { + return this.parties.reduce((sum, party) => sum + Number(party.currentBalance || 0), 0); + } + + /** What the agents should be collecting between them on a full collection day. */ + get totalDailyDue(): number { + return this.parties + .filter((party) => party.status !== 'CLOSED') + .reduce((sum, party) => sum + Number(party.dailyDepositAmount || 0), 0); + } + + ngOnInit(): void { this.organizationService.getParties().subscribe((parties: any[]) => { - const match = (parties || []).find((party) => String(party.clientId) === String(clientId)); - if (match) { - this.patchFormFromParty(match); + this.parties = parties || []; + this.dataSource = new MatTableDataSource(this.parties); + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + this.filteredCount = this.parties.length; + this.dataSource.connect().subscribe((rows) => (this.filteredCount = rows.length)); + }); + + this.loadDueForClosure(); + } + + /** + * Matured accounts still open. A separate call rather than filtered out of the list above, because + * the server decides what counts as matured - comparing dates here would drift from it the first + * time either changed. + */ + private loadDueForClosure(): void { + this.organizationService.getPartiesDueForClosure().subscribe({ + next: (due: any) => { + const rows = Array.isArray(due) ? due : due?.pageItems || []; + this.dueForClosureCount = rows.length; + this.dueForClosurePayable = rows.reduce((sum: number, party: any) => sum + Number(party.currentBalance || 0), 0); + }, + // The party list is the point of this screen; a failed banner must not take it down with it. + error: () => { + this.dueForClosureCount = 0; + this.dueForClosurePayable = 0; } }); } - cancel(): void { - this.partyId = null; - this.partyStatus = 'ACTIVE'; - this.linkedSavingsAccountId = null; - this.partyForm.reset(); - this.partyForm.patchValue({ - nominal: 'No', - operation: 'Self', - adultMinor: 'Adult', - lien: 'No', - isSmsActive: true, - isEmailActive: false, - freezeCode: 'Normal', - advCode: 'None', - nomineeApplicable: 'No', - nomineeSplitMode: 'Percentage', - currentBalance: 0, - maturityAmount: 0, - interestRate: 0 - }); - this.nominees.clear(); - } - - exit(): void { - this.router.navigate(['/accounts/savings']); - } - - save(): void { - const raw = this.partyForm.getRawValue(); - const dateFormat = this.settingsService.dateFormat; - const locale = this.settingsService.language.code; - const asDate = (value: any) => (value ? this.dateUtils.formatDate(value, dateFormat) : undefined); - - const payload: any = { - clientId: Number(raw.clientId), - agentId: Number(raw.agentId), - savingsAccountNo: this.linkedSavingsAccountId ? undefined : raw.savingsAccountNo || undefined, - subType: raw.subType, - lastAccount: raw.lastAccount || undefined, - nominal: raw.nominal === 'Yes', - aadharNo: raw.aadharNo || undefined, - partyName: raw.partyName, - partyName1: raw.partyName1 || undefined, - address1: raw.address1, - address2: raw.address2 || undefined, - address3: raw.address3 || undefined, - city: raw.city, - pincode: raw.pincode, - gender: raw.gender || undefined, - panCardNo: raw.panCardNo || undefined, - telephone: raw.telephone || undefined, - mobileNoSms: raw.mobileNoSms, - operation: raw.operation, - birthDate: asDate(raw.birthDate), - occupation: raw.occupation || undefined, - risk: raw.risk || undefined, - ckycId: raw.ckycId || undefined, - adultMinor: raw.adultMinor, - minorDate: asDate(raw.minorDate), - guardianClientId: raw.guardianClientId ? Number(raw.guardianClientId) : undefined, - minAccountOpt: raw.minAccountOpt || undefined, - relation: raw.relation || undefined, - openingDate: asDate(raw.openingDate), - month: raw.month || undefined, - reopenDate: asDate(raw.reopenDate), - depositAmount: raw.depositAmount || undefined, - dailyDepositAmount: raw.dailyDepositAmount, - days: raw.days, - withdrawType: raw.withdrawType || undefined, - lien: raw.lien === 'Yes', - payDate: asDate(raw.payDate), - paymentAmount: raw.paymentAmount || undefined, - openingBalance: raw.openingBalance || 0, - isSmsActive: !!raw.isSmsActive, - isEmailActive: !!raw.isEmailActive, - interestDate: asDate(raw.interestDate), - spDescription: raw.spDescription || undefined, - freezeCode: raw.freezeCode || undefined, - advCode: raw.advCode || undefined, - creditToBranch: raw.creditToBranch || undefined, - creditToAccountType: raw.creditToAccountType || undefined, - creditToAccount: raw.creditToAccount || undefined, - nomineeApplicable: raw.nomineeApplicable === 'Yes', - nomineeSplitMode: raw.nomineeSplitMode || undefined, - dateFormat, - locale, - nominees: (raw.nominees || []).map((nominee: any) => ({ - nominationDate: this.dateUtils.getDate(nominee.nominationDate), - name: nominee.name, - mobileNo: nominee.mobileNo || undefined, - birthDate: nominee.birthDate ? this.dateUtils.getDate(nominee.birthDate) : undefined, - minor: nominee.minor === 'Yes', - relation: nominee.relation, - gender: nominee.gender, - guardianName: nominee.guardianName || undefined - })) - }; - - if (this.partyId) { - this.organizationService.updateParty(this.partyId, payload).subscribe(() => this.exit()); - } else { - this.organizationService.createParty(payload).subscribe(() => this.exit()); - } - } - - closeAccount(): void { - if (!this.partyId) { - return; - } - const dateFormat = this.settingsService.dateFormat; - const locale = this.settingsService.language.code; - this.organizationService - .closeParty(this.partyId, { - closeDate: this.dateUtils.formatDate(new Date(), dateFormat), - dateFormat, - locale - }) - .subscribe(() => this.exit()); - } - - private patchFormFromParty(party: any): void { - this.partyId = party.id; - this.partyStatus = party.status || 'ACTIVE'; - this.linkedSavingsAccountId = party.savingsAccountId || null; - this.partyForm.patchValue({ - clientId: party.clientId, - subType: party.subType, - agentId: party.agentId, - partyCode: party.partyCode, - lastAccount: party.lastAccount, - nominal: party.nominal ? 'Yes' : 'No', - aadharNo: party.aadharNo, - partyName: party.partyName, - partyName1: party.partyName1, - address1: party.address1, - address2: party.address2, - address3: party.address3, - city: party.city, - pincode: party.pincode, - gender: party.gender, - panCardNo: party.panCardNo, - telephone: party.telephone, - mobileNoSms: party.mobileNoSms, - operation: party.operation, - birthDate: party.birthDate ? new Date(party.birthDate) : null, - occupation: party.occupation, - risk: party.risk, - ckycId: party.ckycId, - adultMinor: party.adultMinor, - minorDate: party.minorDate ? new Date(party.minorDate) : null, - guardianClientId: party.guardianClientId, - minAccountOpt: party.minAccountOpt, - relation: party.relation, - openingDate: party.openingDate ? new Date(party.openingDate) : null, - month: party.month, - maturityDate: party.maturityDate, - reopenDate: party.reopenDate ? new Date(party.reopenDate) : null, - depositAmount: party.depositAmount, - dailyDepositAmount: party.dailyDepositAmount, - days: party.days, - ledgerNo: party.ledgerNo, - maturityAmount: party.maturityAmount, - interestRate: party.interestRate, - withdrawType: party.withdrawType, - od1Flag: party.od1Flag, - od2Flag: party.od2Flag, - lien: party.lien ? 'Yes' : 'No', - payDate: party.payDate ? new Date(party.payDate) : null, - paymentAmount: party.paymentAmount, - currentBalance: party.currentBalance, - openingBalance: party.openingBalance, - isSmsActive: party.isSmsActive, - isEmailActive: party.isEmailActive, - interestCalc: party.interestCalc, - interestDate: party.interestDate ? new Date(party.interestDate) : null, - spDescription: party.spDescription, - freezeCode: party.freezeCode, - advCode: party.advCode, - creditToBranch: party.creditToBranch, - creditToAccountType: party.creditToAccountType, - creditToAccount: party.creditToAccount, - nomineeApplicable: party.nomineeApplicable ? 'Yes' : 'No', - nomineeSplitMode: party.nomineeSplitMode - }); - - this.nominees.clear(); - (party.nominees || []).forEach((nominee: any) => { - const group = this.buildNomineeGroup(); - group.patchValue({ - nominationDate: nominee.nominationDate ? new Date(nominee.nominationDate) : null, - name: nominee.name, - mobileNo: nominee.mobileNo, - birthDate: nominee.birthDate ? new Date(nominee.birthDate) : null, - minor: nominee.minor ? 'Yes' : 'No', - relation: nominee.relation, - gender: nominee.gender, - guardianName: nominee.guardianName - }); - this.nominees.push(group); - }); + applyFilter(filterValue: string): void { + this.dataSource.filter = filterValue.trim().toLowerCase(); } } diff --git a/src/app/deposits/party-master/payout-dialog/payout-dialog.component.html b/src/app/deposits/party-master/payout-dialog/payout-dialog.component.html new file mode 100644 index 000000000..4e724598e --- /dev/null +++ b/src/app/deposits/party-master/payout-dialog/payout-dialog.component.html @@ -0,0 +1,79 @@ + + +

Pay out {{ data.partyCode }}

+ + + +
+
+ Customer + {{ data.partyName }} +
+
+ Matured on + {{ data.maturityDate | dateFormat }} +
+
+ Amount payable + {{ data.currentBalance | number: '1.2-2' }} +
+
+ + @if (loading) { + + } @else { +

Where should this go?

+ + + + Into another account of theirs here + @if (ownAccounts.length === 0) { + {{ data.partyName }} has no other savings account at this bank + } @else { + Moved across today + } + + + @if (destination === 'own' && ownAccounts.length > 0) { + + } + + + To an account at another bank + + Held in Matured Deposits Payable, then sent by NEFT or IMPS from Fund Transfers + + + + + @if (destination === 'other') { + +
+ The money comes off {{ data.partyCode }} today and sits in the bank's Matured Deposits Payable + account until the transfer is made from Fund Transfers — so it is accounted for the whole + time, and a failed transfer loses nothing. +
+ } + } +
+ + + + + diff --git a/src/app/deposits/party-master/payout-dialog/payout-dialog.component.scss b/src/app/deposits/party-master/payout-dialog/payout-dialog.component.scss new file mode 100644 index 000000000..7d58ea20a --- /dev/null +++ b/src/app/deposits/party-master/payout-dialog/payout-dialog.component.scss @@ -0,0 +1,92 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* What is being paid and to whom. Read against the customer standing there, so it comes first. */ +.po-summary { + display: flex; + flex-direction: column; + gap: 6px; + padding: 12px 14px; + margin-bottom: 18px; + background: rgb(0 0 0 / 3%); + border-radius: 6px; + + &__row { + display: flex; + justify-content: space-between; + gap: 16px; + font-size: 0.85rem; + + /* The figure the officer is about to hand over - given the weight that deserves. */ + &--amount { + padding-top: 6px; + border-top: 1px solid rgb(0 0 0 / 8%); + font-size: 1rem; + } + } + + &__label { + color: rgb(0 0 0 / 55%); + } + + &__value { + font-weight: 500; + font-variant-numeric: tabular-nums; + } +} + +.po-question { + margin: 0 0 10px; + font-size: 0.9rem; + font-weight: 500; +} + +.po-choices { + display: flex; + flex-direction: column; + gap: 4px; +} + +/* + * Each choice carries its own consequence underneath it. Two radio labels alone would not tell an + * officer that one keeps the money inside the bank and the other starts it on its way out. + */ +.po-choice { + display: block; + padding: 4px 0; + + &__title { + display: block; + font-size: 0.88rem; + } + + &__note { + display: block; + font-size: 0.75rem; + color: rgb(0 0 0 / 55%); + line-height: 1.4; + } +} + +.po-account { + width: 100%; + margin: 8px 0 4px 32px; + max-width: calc(100% - 32px); +} + +/* What will actually happen, in the words the customer will ask about. */ +.po-callout { + margin-top: 12px; + padding: 10px 12px; + background: rgb(180 83 9 / 8%); + border: 1px solid rgb(180 83 9 / 22%); + border-radius: 6px; + color: #92400e; + font-size: 0.78rem; + line-height: 1.5; +} diff --git a/src/app/deposits/party-master/payout-dialog/payout-dialog.component.ts b/src/app/deposits/party-master/payout-dialog/payout-dialog.component.ts new file mode 100644 index 000000000..120900150 --- /dev/null +++ b/src/app/deposits/party-master/payout-dialog/payout-dialog.component.ts @@ -0,0 +1,109 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** Angular Imports */ +import { Component, OnInit, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { MatRadioModule } from '@angular/material/radio'; +import { MatSelectModule } from '@angular/material/select'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; + +/** Custom Services */ +import { OrganizationService } from 'app/organization/organization.service'; + +/** Custom Pipes */ +import { DateFormatPipe } from 'app/pipes/date-format.pipe'; + +/** + * Where a matured DDS account's money should go. + * + * Two destinations, and the difference is whether the money leaves the bank. Another account of the + * customer's here is a move across the books. Anywhere else means a transfer out, which clears later + * or fails - so the money is parked in the matured-deposits payable account first and the branch + * sends it on from there, rather than being launched into a gap where it has left one account and + * arrived at none. + * + * The amount is not asked for. It is whatever the account holds, worked out by the server: a figure + * typed here could pay out more than the customer has, or quietly leave some behind. + */ +@Component({ + selector: 'mifosx-payout-dialog', + standalone: true, + imports: [ + CommonModule, + FormsModule, + MatDialogModule, + MatButtonModule, + MatRadioModule, + MatSelectModule, + MatFormFieldModule, + MatInputModule, + MatProgressBarModule, + DateFormatPipe + ], + templateUrl: './payout-dialog.component.html', + styleUrls: ['./payout-dialog.component.scss'] +}) +export class PayoutDialogComponent implements OnInit { + private organizationService = inject(OrganizationService); + private dialogRef = inject(MatDialogRef); + data = inject(MAT_DIALOG_DATA); + + /** 'own' moves it to another account here; 'other' parks it for a transfer to another bank. */ + destination: 'own' | 'other' = 'own'; + + /** + * The customer's accounts this maturity can be paid into. + * + * Worked out by the server, which applies the same conditions the payout enforces: an active + * savings account of theirs, not a fixed or recurring deposit, and not one of their DDS scheme + * accounts. Filtering here instead let the two drift - the list once offered a scheme account + * belonging to a party that had already closed, which would have stranded the money. + */ + ownAccounts: any[] = []; + selectedAccountNo = ''; + loading = true; + + ngOnInit(): void { + this.organizationService.getCreditableAccounts(this.data.clientId).subscribe({ + next: (accounts: any) => { + this.ownAccounts = accounts || []; + + // Nothing here to pay into, so the only real choice is a transfer out - offered as the + // default rather than leaving the officer on an option that cannot be completed. + if (this.ownAccounts.length === 0) { + this.destination = 'other'; + } else if (this.ownAccounts.length === 1) { + this.selectedAccountNo = this.ownAccounts[0].accountNo; + } + this.loading = false; + }, + error: () => { + this.ownAccounts = []; + this.destination = 'other'; + this.loading = false; + } + }); + } + + get canPayOut(): boolean { + if (this.loading) { + return false; + } + return this.destination === 'other' || !!this.selectedAccountNo; + } + + payOut(): void { + this.dialogRef.close(this.destination === 'own' ? { toSavingsAccountNo: this.selectedAccountNo } : {}); + } +} diff --git a/src/app/deposits/party-master/premature-close-dialog/premature-close-dialog.component.html b/src/app/deposits/party-master/premature-close-dialog/premature-close-dialog.component.html new file mode 100644 index 000000000..fea269d3b --- /dev/null +++ b/src/app/deposits/party-master/premature-close-dialog/premature-close-dialog.component.html @@ -0,0 +1,118 @@ + + +

Close {{ data.partyCode }} early

+ + + @if (loading) { + + } + + +
+
+ Customer + {{ data.partyName }} +
+
+ Balance + {{ data.currentBalance | number: '1.2-2' }} +
+
+ Matures on + {{ data.maturityDate | dateFormat }} +
+
+ Scheme rate + {{ data.interestRate | number: '1.0-2' }}% +
+
+ +

Who bears the cost of closing early?

+ + + + + + + + + @if (scheme && scheme.prematureAgentCommissionPenalty > 0) { +

+ Either way, {{ data.agentCode }} is charged + {{ scheme.prematureAgentCommissionPenalty | number: '1.0-2' }}% + of the commission owed to them — the round was not kept going. +

+ } + +

Where should the balance go?

+ + + + Into another account of theirs at this bank + + Park it for transfer to another bank + + + @if (destination === 'own' && ownAccounts.length > 0) { + + Credit to account + + @for (account of ownAccounts; track account.accountNo) { + {{ account.accountNo }} — {{ account.productName }} + } + + + } + + @if (ownAccounts.length === 0 && !loading) { +

+ This customer has no other account here, so the balance is parked in matured-deposits payable for the + branch to send on. +

+ } + +

This closes the account. It cannot be reopened.

+
+ + + + + diff --git a/src/app/deposits/party-master/premature-close-dialog/premature-close-dialog.component.scss b/src/app/deposits/party-master/premature-close-dialog/premature-close-dialog.component.scss new file mode 100644 index 000000000..a20cc2ccb --- /dev/null +++ b/src/app/deposits/party-master/premature-close-dialog/premature-close-dialog.component.scss @@ -0,0 +1,112 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/* The account as it stands, so the choice below is made against figures rather than from memory. */ +.pcd-summary { + margin: 4px 0 20px; + padding: 12px 14px; + border-radius: 6px; + background: rgb(0 0 0 / 3%); +} + +.pcd-row { + display: flex; + justify-content: space-between; + gap: 16px; + padding: 3px 0; + font-size: 0.85rem; + + span { + color: rgb(0 0 0 / 60%); + } +} + +.pcd-amount { + font-variant-numeric: tabular-nums; +} + +.pcd-question { + margin: 20px 0 10px; + font-size: 0.9rem; + font-weight: 600; +} + +.pcd-choices { + display: flex; + flex-direction: column; + gap: 10px; +} + +/* + * Each answer is a card rather than a bare radio, because the two are not interchangeable - they take + * money from different people, and the consequence has to be readable before the choice is made. + */ +.pcd-choice { + display: flex; + align-items: flex-start; + gap: 4px; + padding: 10px 12px 12px; + border: 1px solid rgb(0 0 0 / 12%); + border-radius: 6px; + cursor: pointer; + + &:hover { + background: rgb(0 0 0 / 2%); + } +} + +.pcd-choice--on { + border-color: #b45309; + background: rgb(180 83 9 / 5%); +} + +.pcd-choice-body { + padding-top: 2px; +} + +.pcd-choice-title { + font-weight: 600; + font-size: 0.88rem; +} + +.pcd-choice-detail { + margin-top: 3px; + font-size: 0.8rem; + line-height: 1.5; + color: rgb(0 0 0 / 65%); +} + +.pcd-destinations { + display: flex; + flex-direction: column; + gap: 6px; +} + +.pcd-field { + width: 100%; + margin-top: 12px; +} + +.pcd-note { + margin: 12px 0 0; + font-size: 0.8rem; + line-height: 1.5; + color: rgb(0 0 0 / 65%); +} + +/* The one step here with no way back, so it is said plainly rather than left to the button colour. */ +.pcd-warn { + margin: 18px 0 0; + padding: 9px 12px; + border-radius: 6px; + border: 1px solid rgb(185 28 28 / 25%); + background: rgb(185 28 28 / 6%); + color: #b91c1c; + font-size: 0.8rem; + font-weight: 500; +} diff --git a/src/app/deposits/party-master/premature-close-dialog/premature-close-dialog.component.ts b/src/app/deposits/party-master/premature-close-dialog/premature-close-dialog.component.ts new file mode 100644 index 000000000..0cc913c5d --- /dev/null +++ b/src/app/deposits/party-master/premature-close-dialog/premature-close-dialog.component.ts @@ -0,0 +1,136 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** Angular Imports */ +import { Component, OnInit, inject } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; +import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog'; +import { MatButtonModule } from '@angular/material/button'; +import { MatRadioModule } from '@angular/material/radio'; +import { MatSelectModule } from '@angular/material/select'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; + +/** Custom Services */ +import { OrganizationService } from 'app/organization/organization.service'; +import { ProductsService } from 'app/products/products.service'; + +/** Custom Pipes */ +import { DateFormatPipe } from 'app/pipes/date-format.pipe'; + +/** + * Closing a DDS account before its term is up. + * + * A customer who stops saving early costs the bank the interest it had promised on money it no longer + * has for as long as it expected. Somebody has to carry that, and the bank allows two answers - so + * this asks which, and it is the only thing it asks. + * + * What each answer costs is the scheme's, not the clerk's: the reduced rate and the agent's share are + * set once when the scheme is created and shown here as fact. A figure typed at the counter is a + * figure that can be typed differently for the next customer, which is exactly what a published term + * is meant to prevent. + * + * The destination works as it does on a maturity payout - another account of theirs here, or parked + * in matured-deposits payable for a transfer out - because what is left over is still their money and + * has to reach them the same way. + */ +@Component({ + selector: 'mifosx-premature-close-dialog', + standalone: true, + imports: [ + CommonModule, + FormsModule, + MatDialogModule, + MatButtonModule, + MatRadioModule, + MatSelectModule, + MatFormFieldModule, + MatInputModule, + MatProgressBarModule, + DateFormatPipe + ], + templateUrl: './premature-close-dialog.component.html', + styleUrls: ['./premature-close-dialog.component.scss'] +}) +export class PrematureCloseDialogComponent implements OnInit { + private organizationService = inject(OrganizationService); + private productsService = inject(ProductsService); + private dialogRef = inject(MatDialogRef); + data = inject(MAT_DIALOG_DATA); + + /** Who carries the shortfall. No default - the two answers take money from different people. */ + borneBy: 'CUSTOMER' | 'AGENT' | '' = ''; + + /** 'own' moves what is left to another account here; 'other' parks it for a transfer out. */ + destination: 'own' | 'other' = 'own'; + + ownAccounts: any[] = []; + selectedAccountNo = ''; + loading = true; + + /** + * The scheme's terms, shown so the clerk can tell the customer what each answer costs before + * choosing. Null while loading, and left null if the scheme cannot be read - the choice is then + * still offered, because the server applies the terms either way and a screen that could not + * display them should not block a closure the customer has asked for. + */ + scheme: any = null; + + ngOnInit(): void { + this.organizationService.getCreditableAccounts(this.data.clientId).subscribe({ + next: (accounts: any) => { + this.ownAccounts = accounts || []; + if (this.ownAccounts.length === 0) { + this.destination = 'other'; + } else if (this.ownAccounts.length === 1) { + this.selectedAccountNo = this.ownAccounts[0].accountNo; + } + this.loading = false; + }, + error: () => { + this.ownAccounts = []; + this.destination = 'other'; + this.loading = false; + } + }); + + // The terms are read from the scheme the account runs on, so what is shown here is what the + // server will actually charge rather than a second copy that can drift from it. Matched on the + // code the party carries, which is the only handle it has on its scheme. + this.productsService.getDdsAccountTypes().subscribe({ + next: (schemes: any) => { + const all = Array.isArray(schemes) ? schemes : schemes?.pageItems || []; + this.scheme = all.find((s: any) => s.accountTypeCode === this.data.subType) || null; + }, + error: () => (this.scheme = null) + }); + } + + /** The rate the customer would be left on if they carry it. Never below nothing. */ + get reducedRate(): number { + const rate = Number(this.data.interestRate || 0); + const reduction = Number(this.scheme?.prematureInterestReduction || 0); + return Math.max(0, rate - reduction); + } + + get canClose(): boolean { + if (this.loading || !this.borneBy) { + return false; + } + return this.destination === 'other' || !!this.selectedAccountNo; + } + + close(): void { + this.dialogRef.close({ + shortfallBorneBy: this.borneBy, + ...(this.destination === 'own' && this.selectedAccountNo ? { toSavingsAccountNo: this.selectedAccountNo } : {}) + }); + } +} diff --git a/src/app/directives/directives.module.ts b/src/app/directives/directives.module.ts index 9b64b94da..81b0494d4 100644 --- a/src/app/directives/directives.module.ts +++ b/src/app/directives/directives.module.ts @@ -14,6 +14,7 @@ import { CommonModule } from '@angular/common'; import { HasPermissionDirective } from './has-permission/has-permission.directive'; import { FormatAmountDirective } from './format-amount.directive'; import { ValidateOnFocusDirective } from './validate-on-focus.directive'; +import { RestrictInputDirective } from './restrict-input.directive'; /** * Directives Module @@ -25,12 +26,14 @@ import { ValidateOnFocusDirective } from './validate-on-focus.directive'; CommonModule, HasPermissionDirective, FormatAmountDirective, - ValidateOnFocusDirective + ValidateOnFocusDirective, + RestrictInputDirective ], exports: [ HasPermissionDirective, FormatAmountDirective, - ValidateOnFocusDirective + ValidateOnFocusDirective, + RestrictInputDirective ] }) export class DirectivesModule {} diff --git a/src/app/directives/restrict-input.directive.ts b/src/app/directives/restrict-input.directive.ts new file mode 100644 index 000000000..176b32cbc --- /dev/null +++ b/src/app/directives/restrict-input.directive.ts @@ -0,0 +1,137 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Directive, HostListener, Input, ElementRef, inject } from '@angular/core'; +import { NgControl } from '@angular/forms'; + +/** The kinds of field this knows how to hold to a shape. */ +export type RestrictKind = 'digits' | 'phone' | 'pan' | 'aadhaar'; + +interface Rule { + /** Characters this kind will accept at all. */ + allowed: RegExp; + /** How many of them the field can hold, so it cannot be overfilled. */ + maxLength: number; + /** Whether what is typed is forced to upper case as it is entered. */ + upper?: boolean; +} + +const RULES: Record = { + digits: { allowed: /[0-9]/, maxLength: 20 }, + phone: { allowed: /[0-9+()\-\s]/, maxLength: 20 }, + // A PAN is five letters, four digits, one letter - and is always written in capitals. + pan: { allowed: /[A-Za-z0-9]/, maxLength: 10, upper: true }, + // Twelve digits, or the masked form CKYC returns, which is Xs and the last four digits. + aadhaar: { allowed: /[0-9Xx]/, maxLength: 12, upper: true } +}; + +/** + * Stops a field being filled with something it could never hold. + * + * A mobile number typed as letters, or a PAN of fifteen characters, is not a mistake worth + * discovering on save - the customer is at the counter now, and the clerk is looking at the field + * they just typed. Refusing the keystroke says so at the moment it happens, with nothing to read + * and nothing to undo. + * + * Pasting is handled too, and is where this earns its place: a number copied from a spreadsheet + * arrives with spaces, dashes or a +91 in front, and pasting is the one way into a field that + * bypasses every keystroke rule. What can be salvaged is kept, the rest dropped. + * + * This only refuses characters the field cannot contain. Whether what is left is a valid number is + * a separate question, answered by the form's own validators and again by the server - a screen + * restriction is a convenience, not a guarantee. + */ +@Directive({ + selector: '[mifosxRestrictInput]', + standalone: true +}) +export class RestrictInputDirective { + private el = inject(ElementRef); + private control = inject(NgControl, { optional: true }); + + @Input('mifosxRestrictInput') kind: RestrictKind = 'digits'; + + private get rule(): Rule { + return RULES[this.kind] ?? RULES.digits; + } + + /** + * Keys that move around or edit rather than enter a character, plus the usual copy/paste and + * select-all chords. Blocking these would leave the clerk unable to correct their own typing. + */ + private isEditingKey(event: KeyboardEvent): boolean { + const navigation = [ + 'Backspace', + 'Delete', + 'Tab', + 'Escape', + 'Enter', + 'Home', + 'End', + 'ArrowLeft', + 'ArrowRight', + 'ArrowUp', + 'ArrowDown' + ]; + return navigation.includes(event.key) || event.ctrlKey || event.metaKey; + } + + @HostListener('keydown', ['$event']) + onKeydown(event: KeyboardEvent): void { + if (this.isEditingKey(event) || event.key.length > 1) { + return; + } + const input = this.el.nativeElement as HTMLInputElement; + const hasSelection = input.selectionStart !== input.selectionEnd; + + if (!this.rule.allowed.test(event.key)) { + event.preventDefault(); + return; + } + // Replacing a selection does not make the field any longer, so only a plain insert can overfill. + if (!hasSelection && input.value.length >= this.rule.maxLength) { + event.preventDefault(); + } + } + + @HostListener('paste', ['$event']) + onPaste(event: ClipboardEvent): void { + event.preventDefault(); + const pasted = event.clipboardData?.getData('text') ?? ''; + const kept = Array.from(pasted) + .filter((ch) => this.rule.allowed.test(ch)) + .join('') + .slice(0, this.rule.maxLength); + this.write(this.rule.upper ? kept.toUpperCase() : kept); + } + + /** Upper case is applied as the field is left, so the caret does not jump while typing. */ + @HostListener('blur') + onBlur(): void { + if (!this.rule.upper) { + return; + } + const current = String(this.el.nativeElement.value ?? ''); + if (current !== current.toUpperCase()) { + this.write(current.toUpperCase()); + } + } + + /** + * Writes through the form control where there is one, so the value the form holds and the value + * on screen stay the same thing - setting only the element would leave the form with the old one. + */ + private write(value: string): void { + this.el.nativeElement.value = value; + if (this.control?.control) { + this.control.control.setValue(value); + } else { + this.el.nativeElement.dispatchEvent(new Event('input')); + } + } +} diff --git a/src/app/organization/agent-master/agent-master.component.html b/src/app/organization/agent-master/agent-master.component.html index 243121fd8..f5733e41e 100644 --- a/src/app/organization/agent-master/agent-master.component.html +++ b/src/app/organization/agent-master/agent-master.component.html @@ -6,350 +6,175 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
-
- -

Agent Master Entry

+
+
+
+
+
+

Agent Master

+

The DDS collection agents on file.

+
+
+
+
+ + {{ totalCount }} + {{ totalCount === 1 ? 'agent' : 'agents' }} +
+ +
-
-
-
-
- -

Agent Details

-
-
- - Client ID - - - - - Agent Code - - - - - Agent Name ID - - - - - Agent Address (Line 1) - - - - - Agent Address (Line 2) - - - - - Agent City - - - - - Agent PinCode - - - - - Agent Telephone - - - - - Home Branch - - -
+
+
+
+
+
{{ totalCount | number }}
+
Total Agents
- -
-
- -

Collection & Commission Settings

-
-
- - Com. Calculation - - Yes - No - - - - - TDS - - Yes - No - - - - - TDS % - - - - - Cur.TDS - - - - - TDS Amt - - - - - Limit - - - - - Machine Rent - - -
+
+
+
+
+
{{ activeCount | number }}
+
Active Agents
- -
-
- -

Settlement & Security Accounts

-
-
- - Saving Branch - - - - - Agent Saving A/C Type - - @for (accountType of agentSavingAccountTypes; track accountType) { - {{ accountType }} - } - - - - - Agent Saving A/C No - - - - - Trf Branch - - - - - Trf.Amt A/C Type - - - - - Trf.Amt A/C No - - - - - Agent Security Branch - - - - - Agent Security A/C Type - - @for (accountType of agentSavingAccountTypes; track accountType) { - {{ accountType }} - } - - - - - Agent Security A/C No - - - - - Agent Opening Date - - - - - - - Agent Birth Date - - - - - - - Agent Current Balance - - - - - Agent Opening Balance - - -
+
+
+
+
+
{{ totalCurrentBalance | number: '1.2-2' }}
+
Cash With Agents (Unsettled)
+
+
-
-
- -

Collection Limit History

- -
- - @if (limits.length === 0) { -

No limit periods added yet.

- } - - @for (limitRow of limits.controls; track limitRow; let i = $index) { -
- - From Date - - - - - - To Date - - - - - - Limit - - - -
- } +
+
+ +
-
-
- -

Guarantor's Detail

- -
- - @if (guarantors.length === 0) { -

No guarantors added yet.

- } - - @for (guarantor of guarantors.controls; track guarantor; let i = $index) { -
- - Client ID - - - - Guarantor Name - - - - Address 1 - - - - Address 2 - - - - City - - - - Pin Code - - - - Telephone - - - -
- } -
- -
-
- -

Deductions & Deposits

-
-
- - Comm - - - - - Other Comm - - - - - Ag.Deposit - - - - - Adv.Festival - - - - - Other Deduction - - -
-
- -
- - - -
- + } @else { +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Agent Code +
+
+
+
{{ a.agentCode }}
+
+
+
Agent Name{{ a.agentNameId }}Branch + @if (a.homeBranch) { + {{ a.homeBranch }} + } @else { + Not set + } + City{{ a.agentCity }}Saving A/C No + {{ a.agentSavingAccountNo }} + Current Balance + {{ a.agentCurrentBalance | number: '1.2-2' }} + Status + + {{ a.active ? 'Active' : 'Inactive' }} + + Created + {{ a.createdDate ? (a.createdDate | dateFormat: 'DD MMM YYYY, hh:mm A') : '—' }} + Actions + + @if (a.closeDate) { + Closed {{ a.closeDate | dateFormat }} + } @else { + + } +
+ + @if (filteredCount === 0 && totalCount > 0) { +
+ + No agents match your filter. +
+ } + + +
+ }
diff --git a/src/app/organization/agent-master/agent-master.component.scss b/src/app/organization/agent-master/agent-master.component.scss index 1532696bc..4f6cd4cfe 100644 --- a/src/app/organization/agent-master/agent-master.component.scss +++ b/src/app/organization/agent-master/agent-master.component.scss @@ -6,136 +6,54 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -$primary: #0f766e; +@use 'assets/styles/product-list' as pl; -.am-container { - max-width: 1040px; - margin: 0 auto; - padding: 24px 16px; +@include pl.product-list-styles; + +/* Agent-specific indigo accent */ +:host { + --pl-accent: #4338ca; + --pl-accent-l: #e0e7ff; } -.am-header { - display: flex; - align-items: center; - gap: 16px; - margin-bottom: 20px; - - &__title { - margin: 0; - font-size: 22px; - font-weight: 500; - } +/* A retired agent stays listed but reads as switched off. */ +.am-row--inactive td:not(.num) { + opacity: 0.55; } -.am-back { - color: $primary; +/* Closing is the last, irreversible step, so it does not read like the buttons beside it. */ +.pl-action-btn--close { + color: #9b2c2c; } -.am-card { - background: var(--md-sys-color-surface, #fff); - border-radius: 8px; - box-shadow: 0 2px 8px rgb(0 0 0 / 8%); - padding: 8px 24px 24px; +/* A closed agent shows when, in place of the actions it no longer has. */ +.am-closed-on { + font-size: 12px; + font-style: italic; + color: #9b2c2c; } -.am-section { - padding: 20px 0; - border-bottom: 1px solid rgb(0 0 0 / 8%); - - &:last-of-type { - border-bottom: none; - } - - &__header { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 16px; - - i { - color: $primary; - } - - h3 { - margin: 0; - font-size: 16px; - font-weight: 600; - flex: 1; - } - } +.am-pill { + display: inline-block; + padding: 3px 11px; + border-radius: 11px; + font-size: 0.72rem; + font-weight: 500; + line-height: 1.7; + background: #dcfce7; + color: #15803d; + white-space: nowrap; } -.am-grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 8px 16px; +.am-pill--off { + background: rgb(0 0 0 / 7%); + color: rgb(0 0 0 / 52%); } -.am-field { - width: 100%; - - &--wide { - grid-column: span 2; - } -} - -.am-add-guarantor { - color: $primary; -} - -.am-empty-guarantors { - color: rgb(0 0 0 / 50%); - font-size: 14px; - margin: 0 0 8px; -} - -.am-guarantor-row { - display: grid; - grid-template-columns: repeat(7, 1fr) auto; - gap: 8px 12px; - align-items: start; - padding: 10px 0; - border-top: 1px dashed rgb(0 0 0 / 10%); - - &:first-of-type { - border-top: none; - } -} - -.am-limit-row { - display: grid; - grid-template-columns: repeat(3, 1fr) auto; - gap: 8px 12px; - align-items: start; - padding: 10px 0; - border-top: 1px dashed rgb(0 0 0 / 10%); - - &:first-of-type { - border-top: none; - } -} - -.am-remove-guarantor { - color: #b91c1c; - margin-top: 8px; -} - -.am-action-bar { - display: flex; - justify-content: flex-end; - gap: 12px; - margin-top: 20px; - padding-top: 16px; - flex-wrap: wrap; -} - -@media (width <= 900px) { - .am-grid { - grid-template-columns: repeat(2, 1fr); - } - - .am-guarantor-row, - .am-limit-row { - grid-template-columns: repeat(2, 1fr); - } +/* An agent with no branch is an agent no branch's batch run will collect for - say so plainly + rather than showing an em dash that reads like "nothing to see here". */ +.am-no-branch { + font-size: 12px; + font-style: italic; + color: #9b2c2c; } diff --git a/src/app/organization/agent-master/agent-master.component.ts b/src/app/organization/agent-master/agent-master.component.ts index 4b35e05f4..aa6572d10 100644 --- a/src/app/organization/agent-master/agent-master.component.ts +++ b/src/app/organization/agent-master/agent-master.component.ts @@ -7,375 +7,122 @@ */ /** Angular Imports */ -import { Component, inject } from '@angular/core'; -import { UntypedFormBuilder, UntypedFormGroup, UntypedFormArray, Validators } from '@angular/forms'; -import { Router } from '@angular/router'; +import { Component, OnInit, ViewChild, inject } from '@angular/core'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort, MatSortHeader } from '@angular/material/sort'; +import { + MatTable, + MatTableDataSource, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow +} from '@angular/material/table'; +import { MatTooltip } from '@angular/material/tooltip'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; /** Custom Services */ import { OrganizationService } from 'app/organization/organization.service'; -import { SettingsService } from 'app/settings/settings.service'; -import { Dates } from 'app/core/utils/dates'; /** - * Agent Master Entry component. + * Agent Master list. * - * A DDS collection agent — either a person or an institution — registered - * with its own settlement savings account and one or more guarantors. As of - * 2026-08-24 the Agent also does the daily collection work directly (the - * SubAgent tier was removed): it carries the collection-limit history, - * TDS/commission settings and security account SubAgent used to own. - * - * Backed by its own DDS Agent API (`/dds/agents`) - independent of Staff/ - * Client CRUD, per the DDS agent/commission hierarchy being genuinely new - * feature work. + * The DDS collection agents on file. Follows the same list -> create/edit shape as the product lists + * and shares their stylesheet. */ @Component({ selector: 'mifosx-agent-master', templateUrl: './agent-master.component.html', styleUrls: ['./agent-master.component.scss'], - imports: [...STANDALONE_SHARED_IMPORTS] + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent, + MatTable, + MatSort, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatSortHeader, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, + MatPaginator, + MatTooltip + ] }) -export class AgentMasterComponent { - private formBuilder = inject(UntypedFormBuilder); - private router = inject(Router); +export class AgentMasterComponent implements OnInit { private organizationService = inject(OrganizationService); - private settingsService = inject(SettingsService); - private dateUtils = inject(Dates); - maxDate = new Date(); - agentId: number | null = null; - - agentSavingAccountTypes = [ - 'Savings Deposit Individual A/C', - 'Savings Deposit Joint A/C', - 'Current Account' + displayedColumns: string[] = [ + 'agentCode', + 'clientName', + 'homeBranch', + 'agentCity', + 'agentSavingAccountNo', + 'agentCurrentBalance', + 'status', + 'createdDate', + 'actions' ]; - agentForm: UntypedFormGroup = this.formBuilder.group({ - clientId: [ - '', - Validators.required - ], - agentCode: [ - { - value: '', - disabled: true - } - ], - agentNameId: [ - '', - Validators.required - ], - agentAddress1: [ - '', - Validators.required - ], - agentAddress2: [''], - agentCity: [ - '', - Validators.required - ], - agentPinCode: [ - '', - Validators.required - ], - agentTelephone: [''], - agentSavingAccountType: [ - '', - Validators.required - ], - agentSavingAccountNo: [ - '', - Validators.required - ], - agentOpeningDate: [ - '', - Validators.required - ], - agentBirthDate: [ - '', - Validators.required - ], - agentCurrentBalance: [ - { - value: 0, - disabled: true - } - ], - agentOpeningBalance: [ - { - value: 0, - disabled: true - } - ], - homeBranch: [ - '', - Validators.required - ], - tdsApplicable: [ - 'No', - Validators.required - ], - tdsPercent: [0], - currentTds: [ - { - value: 0, - disabled: true - } - ], - tdsAmount: [ - { - value: 0, - disabled: true - } - ], - savingBranch: [ - '', - Validators.required - ], - transferBranch: [''], - transferAmountAccountType: [''], - transferAccountNo: [''], - commissionCalculation: [ - 'Yes', - Validators.required - ], - limit: [0], - machineRent: [0], - agentSecurityBranch: [''], - agentSecurityAccountType: [''], - agentSecurityAccountNo: [''], - commission: [ - { - value: 0, - disabled: true - } - ], - otherCommission: [0], - agentDeposit: [0], - festivalAdvance: [0], - otherDeduction: [0], - limits: this.formBuilder.array([]), - guarantors: this.formBuilder.array([]) + dataSource: MatTableDataSource = new MatTableDataSource([]); + agents: any[] = []; + filteredCount = 0; + + @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator; + @ViewChild(MatSort, { static: true }) sort: MatSort; + + get totalCount(): number { + return this.agents.length; + } + + get activeCount(): number { + return this.agents.filter((agent) => agent.active).length; + } + + /** + * Cash the agents are collectively holding but have not settled to a branch yet — an agent's + * balance rises on every party collection and falls when it hands the money over. Inactive agents + * count too: money still out with a retired agent is exactly what this figure is for. + */ + get totalCurrentBalance(): number { + return this.agents.reduce((sum, agent) => sum + Number(agent.agentCurrentBalance || 0), 0); + } + + ngOnInit(): void { + this.loadAgents(); + } + + private loadAgents(): void { + this.organizationService.getAgents().subscribe((agents: any[]) => { + this.agents = agents || []; + this.dataSource = new MatTableDataSource(this.agents); + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + this.filteredCount = this.agents.length; + this.dataSource.connect().subscribe((rows) => (this.filteredCount = rows.length)); + }); + } + + applyFilter(filterValue: string): void { + this.dataSource.filter = filterValue.trim().toLowerCase(); + } + + private static readonly MONEY = new Intl.NumberFormat('en-IN', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 }); - get limits(): UntypedFormArray { - return this.agentForm.get('limits') as UntypedFormArray; - } - - get guarantors(): UntypedFormArray { - return this.agentForm.get('guarantors') as UntypedFormArray; - } - - private buildLimitGroup(): UntypedFormGroup { - return this.formBuilder.group({ - fromDate: [ - '', - Validators.required - ], - toDate: [ - '', - Validators.required - ], - limit: [ - 0, - Validators.required - ] - }); - } - - private buildGuarantorGroup(): UntypedFormGroup { - return this.formBuilder.group({ - clientId: [ - '', - Validators.required - ], - guarantorName: [ - '', - Validators.required - ], - address1: [ - '', - Validators.required - ], - address2: [''], - city: [ - '', - Validators.required - ], - pinCode: [ - '', - Validators.required - ], - telephone: [''] - }); - } - - addLimit(): void { - this.limits.push(this.buildLimitGroup()); - } - - removeLimit(index: number): void { - this.limits.removeAt(index); - } - - addGuarantor(): void { - this.guarantors.push(this.buildGuarantorGroup()); - } - - removeGuarantor(index: number): void { - this.guarantors.removeAt(index); - } - - retrieve(): void { - const clientId = this.agentForm.get('clientId')?.value; - if (!clientId) { - return; - } - this.organizationService.getAgents().subscribe((agents: any[]) => { - const match = (agents || []).find((agent) => String(agent.clientId) === String(clientId)); - if (match) { - this.patchFormFromAgent(match); - } - }); - } - - cancel(): void { - this.agentId = null; - this.agentForm.reset(); - this.agentForm.patchValue({ - agentCurrentBalance: 0, - agentOpeningBalance: 0, - tdsApplicable: 'No', - commissionCalculation: 'Yes', - currentTds: 0, - tdsAmount: 0, - commission: 0 - }); - this.limits.clear(); - this.guarantors.clear(); - } - - exit(): void { - this.router.navigate(['/organization']); - } - - save(): void { - const raw = this.agentForm.getRawValue(); - const dateFormat = this.settingsService.dateFormat; - const locale = this.settingsService.language.code; - - const payload: any = { - clientId: Number(raw.clientId), - agentNameId: raw.agentNameId, - agentAddress1: raw.agentAddress1, - agentAddress2: raw.agentAddress2 || undefined, - agentCity: raw.agentCity, - agentPinCode: raw.agentPinCode, - agentTelephone: raw.agentTelephone || undefined, - agentSavingAccountType: raw.agentSavingAccountType, - agentSavingAccountNo: raw.agentSavingAccountNo, - agentOpeningDate: this.dateUtils.formatDate(raw.agentOpeningDate, dateFormat), - agentBirthDate: this.dateUtils.formatDate(raw.agentBirthDate, dateFormat), - agentOpeningBalance: raw.agentOpeningBalance || 0, - homeBranch: raw.homeBranch, - tdsApplicable: raw.tdsApplicable === 'Yes', - tdsPercent: raw.tdsPercent || undefined, - savingBranch: raw.savingBranch, - transferBranch: raw.transferBranch || undefined, - transferAmountAccountType: raw.transferAmountAccountType || undefined, - transferAccountNo: raw.transferAccountNo || undefined, - commissionCalculation: raw.commissionCalculation === 'Yes', - limit: raw.limit || undefined, - machineRent: raw.machineRent || undefined, - agentSecurityBranch: raw.agentSecurityBranch || undefined, - agentSecurityAccountType: raw.agentSecurityAccountType || undefined, - agentSecurityAccountNo: raw.agentSecurityAccountNo || undefined, - otherCommission: raw.otherCommission || undefined, - agentDeposit: raw.agentDeposit || undefined, - festivalAdvance: raw.festivalAdvance || undefined, - otherDeduction: raw.otherDeduction || undefined, - dateFormat, - locale, - limits: (raw.limits || []).map((limit: any) => ({ - fromDate: this.dateUtils.getDate(limit.fromDate), - toDate: this.dateUtils.getDate(limit.toDate), - limit: limit.limit - })), - guarantors: (raw.guarantors || []).map((guarantor: any) => ({ - clientId: Number(guarantor.clientId), - guarantorName: guarantor.guarantorName, - address1: guarantor.address1, - address2: guarantor.address2 || undefined, - city: guarantor.city, - pinCode: guarantor.pinCode, - telephone: guarantor.telephone || undefined - })) - }; - - if (this.agentId) { - this.organizationService.updateAgent(this.agentId, payload).subscribe(() => this.exit()); - } else { - this.organizationService.createAgent(payload).subscribe(() => this.exit()); - } - } - - private patchFormFromAgent(agent: any): void { - this.agentId = agent.id; - this.agentForm.patchValue({ - clientId: agent.clientId, - agentCode: agent.agentCode, - agentNameId: agent.agentNameId, - agentAddress1: agent.agentAddress1, - agentAddress2: agent.agentAddress2, - agentCity: agent.agentCity, - agentPinCode: agent.agentPinCode, - agentTelephone: agent.agentTelephone, - agentSavingAccountType: agent.agentSavingAccountType, - agentSavingAccountNo: agent.agentSavingAccountNo, - agentOpeningDate: agent.agentOpeningDate ? new Date(agent.agentOpeningDate) : null, - agentBirthDate: agent.agentBirthDate ? new Date(agent.agentBirthDate) : null, - agentCurrentBalance: agent.agentCurrentBalance, - agentOpeningBalance: agent.agentOpeningBalance, - homeBranch: agent.homeBranch, - tdsApplicable: agent.tdsApplicable ? 'Yes' : 'No', - tdsPercent: agent.tdsPercent, - currentTds: agent.currentTds, - tdsAmount: agent.tdsAmount, - savingBranch: agent.savingBranch, - transferBranch: agent.transferBranch, - transferAmountAccountType: agent.transferAmountAccountType, - transferAccountNo: agent.transferAccountNo, - commissionCalculation: agent.commissionCalculation ? 'Yes' : 'No', - limit: agent.limit, - machineRent: agent.machineRent, - agentSecurityBranch: agent.agentSecurityBranch, - agentSecurityAccountType: agent.agentSecurityAccountType, - agentSecurityAccountNo: agent.agentSecurityAccountNo, - commission: agent.commission, - otherCommission: agent.otherCommission, - agentDeposit: agent.agentDeposit, - festivalAdvance: agent.festivalAdvance, - otherDeduction: agent.otherDeduction - }); - - this.limits.clear(); - (agent.limits || []).forEach((limit: any) => { - const group = this.buildLimitGroup(); - group.patchValue({ - fromDate: limit.fromDate ? new Date(limit.fromDate) : null, - toDate: limit.toDate ? new Date(limit.toDate) : null, - limit: limit.limit - }); - this.limits.push(group); - }); - - this.guarantors.clear(); - (agent.guarantors || []).forEach((guarantor: any) => { - const group = this.buildGuarantorGroup(); - group.patchValue(guarantor); - this.guarantors.push(group); - }); + money(value: any): string { + return AgentMasterComponent.MONEY.format(Number(value || 0)); } } diff --git a/src/app/organization/agent-master/create-agent/create-agent.component.html b/src/app/organization/agent-master/create-agent/create-agent.component.html new file mode 100644 index 000000000..088295fc1 --- /dev/null +++ b/src/app/organization/agent-master/create-agent/create-agent.component.html @@ -0,0 +1,253 @@ + + +
+
+ +

Agent Master Entry

+
+ +
+
+ +
+
+ +

Agent Details

+
+
+ + Client ID + + @if (clientLookupFailed) { + No client found with this ID + } @else if (loadedClientName) { + {{ loadedClientName }} + } + + + + Agent Code + + + + + Agent Name ID + + + + + Agent Address (Line 1) + + + + + Agent Address (Line 2) + + + + + Agent City + + + + + Agent PinCode + + + + + Agent Telephone + + + + + + Home Branch + + @for (office of offices; track office.id) { + {{ office.name }} + } + + The branch this agent's collections belong to + + + + + GL Account + + @for (account of glAccounts; track account.id) { + {{ account.glCode }} · {{ account.name }} + } + + This agent's collections pass through it. One agent to an account. + + + + + Agent Saving A/C Type + + @for (accountType of savingsAccountTypes; track accountType) { + {{ accountType }} + } + + + @if (savingsAccountTypes.length === 0) { + @if (loadedClientName) { + + {{ loadedClientName }} has no savings account — commission can only be paid into one + + } @else { + Enter a Client ID first — types come from that client's own accounts + } + } + + + + + Agent Saving A/C No + + @for (account of accountsOfChosenType; track account.id) { + {{ account.accountNo }} + } + + @if (accountsOfChosenType.length === 0) { + Choose an A/C Type first + } + + + + Agent Opening Date + + + + + + + Agent Birth Date + + + + +
+
+ +
+
+ +

Guarantor's Detail

+ +
+ + @if (guarantors.length === 0) { +

No guarantors added yet.

+ } + + @for (guarantor of guarantors.controls; track guarantor; let i = $index) { +
+ + Client ID + + @if (guarantor.get('clientId')?.hasError('selfGuarantor')) { + An agent cannot be its own guarantor + } + + + Guarantor Name + + + + Address 1 + + + + Address 2 + + + + City + + + + Pin Code + + + + Telephone + + + +
+ } +
+ +
+
+ +

Balances

+
+
+ + Agent Current Balance + + + + + Agent Opening Balance + + +
+
+ +
+ + + + +
+
+
+
diff --git a/src/app/organization/agent-master/create-agent/create-agent.component.scss b/src/app/organization/agent-master/create-agent/create-agent.component.scss new file mode 100644 index 000000000..a0247c6dd --- /dev/null +++ b/src/app/organization/agent-master/create-agent/create-agent.component.scss @@ -0,0 +1,146 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +$primary: #0f766e; + +.am-container { + max-width: 1040px; + margin: 0 auto; + padding: 24px 16px; +} + +.am-header { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 20px; + + &__title { + margin: 0; + font-size: 22px; + font-weight: 500; + } +} + +.am-back { + color: $primary; +} + +.am-card { + background: var(--md-sys-color-surface, #fff); + border-radius: 8px; + box-shadow: 0 2px 8px rgb(0 0 0 / 8%); + padding: 8px 24px 24px; +} + +.am-section { + padding: 20px 0; + border-bottom: 1px solid rgb(0 0 0 / 8%); + + &:last-of-type { + border-bottom: none; + } + + &__header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 16px; + + i { + color: $primary; + } + + h3 { + margin: 0; + font-size: 16px; + font-weight: 600; + flex: 1; + } + } +} + +.am-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px 16px; +} + +.am-field { + width: 100%; + + &--wide { + grid-column: span 2; + } +} + +.am-add-guarantor { + color: $primary; +} + +.am-empty-guarantors { + color: rgb(0 0 0 / 50%); + font-size: 14px; + margin: 0 0 8px; +} + +.am-guarantor-row { + display: grid; + grid-template-columns: repeat(7, 1fr) auto; + gap: 8px 12px; + align-items: start; + padding: 10px 0; + border-top: 1px dashed rgb(0 0 0 / 10%); + + &:first-of-type { + border-top: none; + } +} + +.am-limit-row { + display: grid; + grid-template-columns: repeat(3, 1fr) auto; + gap: 8px 12px; + align-items: start; + padding: 10px 0; + border-top: 1px dashed rgb(0 0 0 / 10%); + + &:first-of-type { + border-top: none; + } +} + +.am-remove-guarantor { + color: #b91c1c; + margin-top: 8px; +} + +.am-action-bar { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 20px; + padding-top: 16px; + flex-wrap: wrap; +} + +/* Client ID lookup came back empty — the officer typed an id that doesn't exist. */ +.am-hint--error { + color: #b3261e; +} + +@media (width <= 900px) { + .am-grid { + grid-template-columns: repeat(2, 1fr); + } + + .am-guarantor-row, + .am-limit-row { + grid-template-columns: repeat(2, 1fr); + } +} diff --git a/src/app/organization/agent-master/create-agent/create-agent.component.ts b/src/app/organization/agent-master/create-agent/create-agent.component.ts new file mode 100644 index 000000000..50e9b4844 --- /dev/null +++ b/src/app/organization/agent-master/create-agent/create-agent.component.ts @@ -0,0 +1,593 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** Angular Imports */ +import { Component, OnInit, inject } from '@angular/core'; +import { + UntypedFormBuilder, + UntypedFormGroup, + UntypedFormArray, + Validators, + ValidatorFn, + AbstractControl, + ValidationErrors +} from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { debounceTime, distinctUntilChanged } from 'rxjs/operators'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** Custom Services */ +import { OrganizationService } from 'app/organization/organization.service'; +import { AccountingService } from 'app/accounting/accounting.service'; +import { SystemService } from 'app/system/system.service'; +import { ClientsService } from 'app/clients/clients.service'; +import { SettingsService } from 'app/settings/settings.service'; +import { Dates } from 'app/core/utils/dates'; + +/** Custom Directives */ +import { RestrictInputDirective } from 'app/directives/restrict-input.directive'; + +/** Writes a value into a form group only when there is something to write. */ +function setGroupField(group: UntypedFormGroup, controlName: string, value: any): void { + if (value === null || value === undefined || value === '') { + return; + } + group.get(controlName)?.setValue(String(value)); +} + +/** + * Agent Master Entry component. + * + * A DDS collection agent — either a person or an institution — registered with its own settlement + * savings account and one or more guarantors. Modelled on the legacy Agent Master Entry screen, field + * for field. + * + * Entering a Client ID fills the name, address, city, pincode and telephone from that client, the way + * the legacy screen did, defaults the home branch to that client's office, and stocks the account + * type and account number dropdowns from that client's own accounts. Filled fields stay editable. + * + * Backed by its own DDS Agent API (`/dds/agents`) — independent of Staff/Client CRUD, per the DDS + * agent/commission hierarchy being genuinely new feature work. + */ +@Component({ + selector: 'mifosx-create-agent', + templateUrl: './create-agent.component.html', + styleUrls: ['./create-agent.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + // See the Party form: an unimported directive is ignored without a word. + RestrictInputDirective + ] +}) +export class CreateAgentComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private router = inject(Router); + private route = inject(ActivatedRoute); + private organizationService = inject(OrganizationService); + private clientsService = inject(ClientsService); + private accountingService = inject(AccountingService); + private systemService = inject(SystemService); + private settingsService = inject(SettingsService); + private dateUtils = inject(Dates); + + /** + * The latest date of birth the picker will offer: eighteen years ago today. + * + * It was previously today, which let a clerk save an agent born on the day they were entered — + * the picker opens on today, so a stray Enter was enough. The figure matches the rule the server + * applies, so the calendar cannot offer a date the save would then refuse. + */ + maxBirthDate = new Date(new Date().getFullYear() - 18, new Date().getMonth(), new Date().getDate()); + agentId: number | null = null; + + /** Shown under the Client ID field once a client has been resolved. */ + loadedClientName = ''; + clientLookupFailed = false; + + /** + * The client's own open savings accounts, fetched once the client is resolved. The commission + * account is picked from these rather than typed, so it is always an account that exists and + * always belongs to this agent. + */ + clientSavingsAccounts: any[] = []; + + /** + * The account types this client actually holds, taken from their own accounts rather than a fixed + * list. A fixed list would offer types the client has nothing under, and its wording would have to + * match the product names by hand for the filter below to work at all. + */ + savingsAccountTypes: string[] = []; + + /** Fineract's deposit-account kinds: 100 savings, 200 fixed deposit, 300 recurring deposit. */ + private static readonly SAVINGS_DEPOSIT_TYPE = 100; + + /** Branches, for the Home Branch field. Defaulted from the client's own office. */ + offices: any[] = []; + + /** + * The ledger accounts an agent's collections may pass through. Header accounts are left out: they + * are headings over other accounts and carry no entries, so offering one only leads to a refusal. + */ + glAccounts: any[] = []; + + /** + * The savings product a DDS customer's own scheme account is opened under, read from the setting + * that names it rather than guessed from a product name a bank is free to change. + * + * Held so those accounts can be kept out of the agent's own list. They are savings accounts and + * pass every other test, but they belong to a customer's scheme: commission paid into one would be + * counted as that customer's deposits by every interest run afterwards. The server refuses it + * either way - this is so the choice is never offered rather than taken and then thrown back. + */ + private ddsProductId: number | null = null; + + /** + * The accounts on offer once a type has been chosen. Type is asked first, as the legacy screen + * did, so the officer narrows to a kind of account and then picks one of that kind. + */ + get accountsOfChosenType(): any[] { + const chosenType = this.agentForm?.get('agentSavingAccountType')?.value; + if (!chosenType) { + return []; + } + return this.clientSavingsAccounts.filter((account: any) => account.productName === chosenType); + } + + agentForm: UntypedFormGroup = this.formBuilder.group({ + clientId: [ + '', + Validators.required + ], + agentCode: [ + { + value: '', + disabled: true + } + ], + agentNameId: [ + '', + Validators.required + ], + agentAddress1: [ + '', + Validators.required + ], + agentAddress2: [''], + agentCity: [ + '', + Validators.required + ], + agentPinCode: [ + '', + Validators.required + ], + agentTelephone: [''], + // Which branch this agent collects out of. Holds the office's id, not its name - the name is + // only what the dropdown shows. The DDS batch runs filter by it. + officeId: [ + '', + Validators.required + ], + // Asked before the account number, which is then narrowed to accounts of this type. + agentSavingAccountType: [ + '', + Validators.required + ], + agentSavingAccountNo: [ + '', + Validators.required + ], + agentOpeningDate: [ + '', + Validators.required + ], + agentBirthDate: [ + '', + Validators.required + ], + agentCurrentBalance: [ + { + value: 0, + disabled: true + } + ], + agentOpeningBalance: [0], + /* + * Required. An agent sharing the scheme's one account leaves a balance that says money is + * outstanding without saying whose - which is exactly what this field was added to answer. + */ + glAccountId: [ + '', + Validators.required + ], + guarantors: this.formBuilder.array([]) + }); + + ngOnInit(): void { + this.organizationService.getOffices().subscribe({ + next: (offices: any) => (this.offices = Array.isArray(offices) ? offices : offices?.pageItems || []), + error: () => (this.offices = []) + }); + + this.systemService.getConfigurationByName('dds-deposit-product').subscribe({ + next: (config: any) => { + this.ddsProductId = config?.enabled && config?.value ? Number(config.value) : null; + // The client may already have been keyed while this was in flight, so the list is rebuilt + // rather than left showing an account this now knows to exclude. + const clientId = this.agentForm.get('clientId')?.value; + if (clientId) { this.loadClientSavingsAccounts(String(clientId)); } + }, + error: () => (this.ddsProductId = null) + }); + + this.accountingService.getChartOfAccounts().subscribe({ + next: (accounts: any) => { + const rows = Array.isArray(accounts) ? accounts : accounts?.pageItems || []; + // usage 1 is a detail account, 2 a header. Disabled accounts refuse every posting, so an + // agent pointed at one would fail on their first collection rather than here. + this.glAccounts = rows.filter((a: any) => a.usage?.id === 1 && !a.disabled); + }, + error: () => (this.glAccounts = []) + }); + + // An id in the route means this is an edit; load that agent into the form. + const routeId = this.route.snapshot.paramMap.get('agentId'); + if (routeId) { + this.organizationService.getAgent(routeId).subscribe((agent: any) => this.patchFormFromAgent(agent)); + } + + // Fill the client-derived fields as soon as typing settles - no need to tab out of the field. + this.agentForm + .get('clientId') + ?.valueChanges.pipe(debounceTime(500), distinctUntilChanged()) + .subscribe(() => { + this.loadClient(); + // A guarantor row that was fine may now be the agent itself, or the other way round. + for (const row of this.guarantors.controls) { + row.get('clientId')?.updateValueAndValidity(); + } + }); + } + + get guarantors(): UntypedFormArray { + return this.agentForm.get('guarantors') as UntypedFormArray; + } + + private buildGuarantorGroup(): UntypedFormGroup { + const group = this.formBuilder.group({ + clientId: [ + '', + [ + Validators.required, + this.notTheAgentItself() + ] + ], + guarantorName: [ + '', + Validators.required + ], + address1: [ + '', + Validators.required + ], + address2: [''], + city: [ + '', + Validators.required + ], + pinCode: [ + '', + Validators.required + ], + telephone: [''] + }); + + // A guarantor is a client too, so its row fills from the client the same way the agent's does. + group + .get('clientId') + ?.valueChanges.pipe(debounceTime(500), distinctUntilChanged()) + .subscribe((clientId) => this.loadGuarantorClient(group, clientId)); + + return group; + } + + /** + * An agent cannot stand guarantor for itself - a guarantee is only worth something if it comes + * from someone else. + */ + private notTheAgentItself(): ValidatorFn { + return (control: AbstractControl): ValidationErrors | null => { + const agentClientId = this.agentForm?.get('clientId')?.value; + if (!control.value || !agentClientId) { + return null; + } + return String(control.value) === String(agentClientId) ? { selfGuarantor: true } : null; + }; + } + + /** Refills a guarantor row from its Client ID, mirroring the agent's own client lookup. */ + private loadGuarantorClient(group: UntypedFormGroup, clientId: any): void { + for (const controlName of ['guarantorName', 'telephone', 'address1', 'address2', 'city', 'pinCode']) { + group.get(controlName)?.setValue(''); + } + if (!clientId) { + return; + } + + this.clientsService.getClientData(String(clientId), true).subscribe({ + next: (client: any) => { + setGroupField(group, 'guarantorName', client?.displayName); + setGroupField(group, 'telephone', client?.mobileNo); + this.clientsService.getClientAddressData(String(clientId), true).subscribe({ + next: (addresses: any) => { + const address = Array.isArray(addresses) ? addresses[0] : addresses; + if (!address) { + return; + } + setGroupField(group, 'address1', address.addressLine1 || address.street); + setGroupField(group, 'address2', address.addressLine2); + setGroupField(group, 'city', address.city); + setGroupField(group, 'pinCode', address.postalCode); + }, + error: () => undefined + }); + }, + // An id that matches no client just leaves the row for the officer to fill in. + error: () => undefined + }); + } + + addGuarantor(): void { + this.guarantors.push(this.buildGuarantorGroup()); + } + + removeGuarantor(index: number): void { + this.guarantors.removeAt(index); + } + + /** The fields the legacy screen filled from the client, rather than asking the officer for. */ + private static readonly CLIENT_DERIVED_FIELDS = [ + 'agentNameId', + 'agentTelephone', + 'agentAddress1', + 'agentAddress2', + 'agentCity', + 'agentPinCode' + ]; + + /** + * Refills the client-derived fields whenever the Client ID changes, as the legacy screen did. + * They always show the client currently entered: changing the id replaces them, clearing it + * empties them. + */ + loadClient(): void { + if (this.agentId) { + return; + } + this.clearClientDerivedFields(); + this.loadedClientName = ''; + this.clientLookupFailed = false; + // A different client has different accounts, so anything picked for the previous one is stale. + this.clientSavingsAccounts = []; + this.savingsAccountTypes = []; + this.agentForm.get('agentSavingAccountType')?.setValue(''); + this.agentForm.get('agentSavingAccountNo')?.setValue(''); + + const clientId = this.agentForm.get('clientId')?.value; + if (!clientId) { + return; + } + + this.clientsService.getClientData(String(clientId), true).subscribe({ + next: (client: any) => { + this.loadedClientName = client?.displayName || ''; + this.setField('agentNameId', client?.displayName); + this.setField('agentTelephone', client?.mobileNo); + // The branch the person is on the bank's books at. Offered as a starting point rather than + // forced, since an agent can be recorded at one office and collect out of another. Set as a + // number rather than through setField, whose String() would stop it matching an option. + if (client?.officeId) { + this.agentForm.get('officeId')?.setValue(Number(client.officeId)); + } + this.loadClientAddress(String(clientId)); + this.loadClientSavingsAccounts(String(clientId)); + }, + error: () => { + this.clientLookupFailed = true; + } + }); + } + + /** + * The client's own open savings accounts, to be picked from rather than typed. + * + * The account number used to be free text, and the live data showed the cost: agent AG0001 carried + * "1234567890", which matches no account at all - so every commission recorded against it had + * nowhere to go. Offering only the client's real accounts makes that impossible to key, and the + * account number then carries its correct product prefix by construction rather than by memory. + */ + private loadClientSavingsAccounts(clientId: string): void { + this.clientsService.getClientAccountData(clientId).subscribe({ + next: (accounts: any) => { + this.clientSavingsAccounts = (accounts?.savingsAccounts || []).filter( + (account: any) => + // Savings only. Commission is credited whenever a run posts, and a fixed deposit is a + // single sum placed for a term while a recurring one takes only its own instalments - + // neither accepts the credit, so the posting would fail rather than the choice. + // A DDS scheme account is a savings account too and so passes the test above. It is + // dropped by the product the setting names - not by a product called "DDS", which is a + // name the bank may change - so the list offers only accounts the agent can be paid in. + account.depositType?.id === CreateAgentComponent.SAVINGS_DEPOSIT_TYPE && + (this.ddsProductId === null || account.productId !== this.ddsProductId) && + (account.status?.active || account.status?.submittedAndPendingApproval || account.status?.approved) + ); + this.savingsAccountTypes = [ + ...new Set(this.clientSavingsAccounts.map((account: any) => account.productName).filter(Boolean)) + ]; + }, + error: () => { + this.clientSavingsAccounts = []; + this.savingsAccountTypes = []; + } + }); + } + + /** + * Clears the account number when the type changes. + * + * Without this the previously picked account stays in the field while the list below it now shows + * a different type - the form would submit an account that contradicts its own type. + */ + onSavingsAccountTypePicked(): void { + this.agentForm.get('agentSavingAccountNo')?.setValue(''); + } + + private loadClientAddress(clientId: string): void { + this.clientsService.getClientAddressData(clientId, true).subscribe({ + next: (addresses: any) => { + const address = Array.isArray(addresses) ? addresses[0] : addresses; + if (!address) { + return; + } + this.setField('agentAddress1', address.addressLine1 || address.street); + this.setField('agentAddress2', address.addressLine2); + this.setField('agentCity', address.city); + this.setField('agentPinCode', address.postalCode); + }, + // A client with no address on file is normal — the officer just types it. + error: () => undefined + }); + } + + private clearClientDerivedFields(): void { + for (const controlName of CreateAgentComponent.CLIENT_DERIVED_FIELDS) { + this.agentForm.get(controlName)?.setValue(''); + } + } + + private setField(controlName: string, value: any): void { + if (value === null || value === undefined || value === '') { + return; + } + this.agentForm.get(controlName)?.setValue(String(value)); + } + + retrieve(): void { + const clientId = this.agentForm.get('clientId')?.value; + if (!clientId) { + return; + } + this.organizationService.getAgents().subscribe((agents: any[]) => { + const match = (agents || []).find((agent) => String(agent.clientId) === String(clientId)); + if (match) { + this.patchFormFromAgent(match); + } + }); + } + + cancel(): void { + this.agentId = null; + this.loadedClientName = ''; + this.clientLookupFailed = false; + this.agentForm.reset(); + this.agentForm.patchValue({ + agentCurrentBalance: 0, + agentOpeningBalance: 0 + }); + this.guarantors.clear(); + } + + exit(): void { + this.router.navigate(['/organization/agent-master']); + } + + save(): void { + const raw = this.agentForm.getRawValue(); + const dateFormat = this.settingsService.dateFormat; + const locale = this.settingsService.language.code; + + const payload: any = { + clientId: Number(raw.clientId), + agentNameId: raw.agentNameId, + agentAddress1: raw.agentAddress1, + agentAddress2: raw.agentAddress2 || undefined, + agentCity: raw.agentCity, + agentPinCode: raw.agentPinCode, + agentTelephone: raw.agentTelephone || undefined, + officeId: Number(raw.officeId), + glAccountId: raw.glAccountId ? Number(raw.glAccountId) : undefined, + agentSavingAccountType: raw.agentSavingAccountType, + agentSavingAccountNo: raw.agentSavingAccountNo, + agentOpeningDate: this.dateUtils.formatDate(raw.agentOpeningDate, dateFormat), + agentBirthDate: this.dateUtils.formatDate(raw.agentBirthDate, dateFormat), + agentOpeningBalance: raw.agentOpeningBalance || 0, + dateFormat, + locale, + guarantors: (raw.guarantors || []).map((guarantor: any) => ({ + clientId: Number(guarantor.clientId), + guarantorName: guarantor.guarantorName, + address1: guarantor.address1, + address2: guarantor.address2 || undefined, + city: guarantor.city, + pinCode: guarantor.pinCode, + telephone: guarantor.telephone || undefined + })) + }; + + if (this.agentId) { + this.organizationService.updateAgent(this.agentId, payload).subscribe(() => this.exit()); + } else { + this.organizationService.createAgent(payload).subscribe(() => this.exit()); + } + } + + private patchFormFromAgent(agent: any): void { + this.agentId = agent.id; + this.loadedClientName = agent.clientName || ''; + this.clientLookupFailed = false; + this.agentForm.patchValue({ + clientId: agent.clientId, + agentCode: agent.agentCode, + agentNameId: agent.agentNameId, + agentAddress1: agent.agentAddress1, + agentAddress2: agent.agentAddress2, + agentCity: agent.agentCity, + agentPinCode: agent.agentPinCode, + agentTelephone: agent.agentTelephone, + officeId: agent.officeId ? Number(agent.officeId) : '', + glAccountId: agent.glAccountId ? Number(agent.glAccountId) : '', + agentSavingAccountType: agent.agentSavingAccountType, + agentSavingAccountNo: agent.agentSavingAccountNo, + agentOpeningDate: agent.agentOpeningDate ? new Date(agent.agentOpeningDate) : null, + agentBirthDate: agent.agentBirthDate ? new Date(agent.agentBirthDate) : null, + agentCurrentBalance: agent.agentCurrentBalance, + agentOpeningBalance: agent.agentOpeningBalance + }); + + // The account list is needed for the dropdown to show the already-saved account as chosen + // rather than blank, so it is fetched on edit as well as on the client lookup. + if (agent.clientId) { + this.loadClientSavingsAccounts(String(agent.clientId)); + } + + this.guarantors.clear(); + for (const guarantor of agent.guarantors || []) { + const group = this.buildGuarantorGroup(); + group.patchValue({ + clientId: guarantor.clientId, + guarantorName: guarantor.guarantorName, + address1: guarantor.address1, + address2: guarantor.address2, + city: guarantor.city, + pinCode: guarantor.pinCode, + telephone: guarantor.telephone + }); + this.guarantors.push(group); + } + } +} diff --git a/src/app/organization/commission-slab-master/commission-slab-master.component.html b/src/app/organization/commission-slab-master/commission-slab-master.component.html index 31c5b682b..b55152475 100644 --- a/src/app/organization/commission-slab-master/commission-slab-master.component.html +++ b/src/app/organization/commission-slab-master/commission-slab-master.component.html @@ -27,17 +27,67 @@ @for (slab of slabs; track slab; let i = $index) { - - {{ slab.slabType }} - {{ slab.slabFrom }} - {{ slab.slabTo }} - {{ slab.commissionPercent }}% - - - - + @if (editingIndex === i) { + + + + + + @for (type of slabTypes; track type) { + {{ type }} + } + + + + + + + + + + + + + + + + + + + + + + + + } @else { + + {{ slab.slabType }} + {{ slab.slabFrom }} + {{ slab.slabTo }} + {{ slab.commissionPercent }}% + + @if (slab.id) { + + + } @else { + + + } + + + } } @@ -66,11 +116,15 @@ + diff --git a/src/app/organization/commission-slab-master/commission-slab-master.component.scss b/src/app/organization/commission-slab-master/commission-slab-master.component.scss index 76eed8f2a..7ecabf415 100644 --- a/src/app/organization/commission-slab-master/commission-slab-master.component.scss +++ b/src/app/organization/commission-slab-master/commission-slab-master.component.scss @@ -9,7 +9,8 @@ $primary: #0f766e; .csm-container { - max-width: 760px; + // Four inputs sit side by side in the entry row; 760px squeezed them out of the card. + max-width: 980px; margin: 0 auto; padding: 24px 16px; } @@ -41,6 +42,14 @@ $primary: #0f766e; .csm-table { width: 100%; border-collapse: collapse; + table-layout: fixed; + + // Sized rather than left to the browser, so the entry row keeps its shape whatever is typed in. + th:nth-child(1), td:nth-child(1) { width: 26%; } + th:nth-child(2), td:nth-child(2) { width: 22%; } + th:nth-child(3), td:nth-child(3) { width: 22%; } + th:nth-child(4), td:nth-child(4) { width: 18%; } + th:nth-child(5), td:nth-child(5) { width: 12%; } th { text-align: left; @@ -61,6 +70,8 @@ $primary: #0f766e; .csm-inline-field { width: 100%; + // Without this the field holds its own 180px floor and pushes the next column out of the card. + min-width: 0; ::ng-deep .mat-mdc-text-field-wrapper { height: 44px; @@ -90,3 +101,44 @@ $primary: #0f766e; padding-top: 16px; border-top: 1px solid rgb(0 0 0 / 8%); } + +/* + * What the slabs actually cover, and any figure between them that none of them claims. + * + * A gap is legitimate - a bank may set no commission below some floor - so this reports rather than + * warns. But it has to be said out loud: the commission run finds no slab for a volume in a gap and + * moves on, so the agent is simply paid nothing that month and nothing on screen explains it. + */ +.csm-coverage { + margin-top: 14px; + padding: 12px 16px; + border: 1px solid rgb(0 0 0 / 12%); + border-radius: 6px; + background: rgb(0 0 0 / 2.5%); + display: flex; + flex-direction: column; + gap: 6px; + + &__row { + display: flex; + align-items: baseline; + gap: 10px; + flex-wrap: wrap; + font-size: 0.86rem; + } + + &__type { + font-weight: 600; + min-width: 90px; + } + + &__range { + color: rgb(0 0 0 / 62%); + font-variant-numeric: tabular-nums; + } + + &__gap { + color: #b45309; + font-variant-numeric: tabular-nums; + } +} diff --git a/src/app/organization/commission-slab-master/commission-slab-master.component.ts b/src/app/organization/commission-slab-master/commission-slab-master.component.ts index 563a0da46..c1a96c849 100644 --- a/src/app/organization/commission-slab-master/commission-slab-master.component.ts +++ b/src/app/organization/commission-slab-master/commission-slab-master.component.ts @@ -14,6 +14,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; /** Custom Services */ import { OrganizationService } from 'app/organization/organization.service'; +import { AlertService } from 'app/core/alert/alert.service'; /** * Commission Slab Master component. @@ -39,6 +40,7 @@ export class CommissionSlabMasterComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); private router = inject(Router); private organizationService = inject(OrganizationService); + private alertService = inject(AlertService); private static readonly SLAB_TYPE_CODES: Record = { Account: 'ACCOUNT', @@ -99,11 +101,85 @@ export class CommissionSlabMasterComponent implements OnInit { }); } + /** + * Where each slab type's coverage begins and ends, and any figure in between that no slab claims. + * + * A gap is not an error - a bank may well decide there is no commission below some floor - but it + * has to be visible. The commission run simply finds no slab for a volume that falls in a gap and + * moves on, so the agent is paid nothing for that month with nothing on screen to say why. + */ + get coverage(): Array<{ slabType: string; from: number; to: number; gaps: string[] }> { + return this.slabTypes + .map((slabType) => { + const rows = this.slabs + .filter((slab) => slab.slabType === slabType) + .map((slab) => ({ from: Number(slab.slabFrom), to: Number(slab.slabTo) })) + .sort((a, b) => a.from - b.from); + + if (rows.length === 0) { + return null; + } + + const gaps: string[] = []; + for (let i = 1; i < rows.length; i++) { + const previousEnd = rows[i - 1].to; + const nextStart = rows[i].from; + if (nextStart > previousEnd + 1) { + gaps.push(`${previousEnd + 1} - ${nextStart - 1}`); + } + } + + return { slabType, from: rows[0].from, to: rows[rows.length - 1].to, gaps }; + }) + .filter((entry): entry is { slabType: string; from: number; to: number; gaps: string[] } => entry !== null); + } + + /** + * The slab already on the table that this range would collide with, if any. + * + * The backend refuses overlapping slabs, but only when Save is pressed - by then the officer may + * have keyed several rows and has to work out which one it objected to. Catching it as the row is + * added names the clash while it is still the row in front of them. + */ + private clashingSlab(candidate: { slabType: string; slabFrom: number; slabTo: number }): any { + return this.slabs.find( + (slab) => + slab.slabType === candidate.slabType + && Number(candidate.slabFrom) <= Number(slab.slabTo) + && Number(candidate.slabTo) >= Number(slab.slabFrom) + ); + } + addSlab(): void { if (this.newSlabForm.invalid) { + // Marking the controls touched is what makes Material show which one is missing; without it + // the button simply does nothing and the officer has no idea what the screen is waiting for. + this.newSlabForm.markAllAsTouched(); + this.alertService.alert({ type: 'Error', message: 'Fill the slab type, range and percentage first' }); return; } - this.slabs.push(this.newSlabForm.getRawValue()); + + const candidate = this.newSlabForm.getRawValue(); + + if (Number(candidate.slabFrom) > Number(candidate.slabTo)) { + this.alertService.alert({ type: 'Error', message: 'Slab From cannot be greater than Slab To' }); + return; + } + if (Number(candidate.commissionPercent) > 100) { + this.alertService.alert({ type: 'Error', message: 'Commission cannot be more than 100%' }); + return; + } + + const clash = this.clashingSlab(candidate); + if (clash) { + this.alertService.alert({ + type: 'Error', + message: `Overlaps the existing ${clash.slabType} slab ${clash.slabFrom} to ${clash.slabTo}` + }); + return; + } + + this.slabs.push(candidate); this.newSlabForm.reset({ slabType: '', slabFrom: 0, @@ -112,10 +188,92 @@ export class CommissionSlabMasterComponent implements OnInit { }); } + /** Only ever an unsaved row - a saved slab is corrected, not taken away. */ removeSlab(index: number): void { this.slabs.splice(index, 1); } + /** Which saved row is currently open for correction, or -1 when none is. */ + editingIndex = -1; + + editSlabForm: UntypedFormGroup = this.formBuilder.group({ + slabType: [ + '', + Validators.required + ], + slabFrom: [ + 0, + Validators.required + ], + slabTo: [ + 0, + Validators.required + ], + commissionPercent: [ + 0, + Validators.required + ] + }); + + startEdit(index: number): void { + const slab = this.slabs[index]; + this.editingIndex = index; + this.editSlabForm.setValue({ + slabType: slab.slabType, + slabFrom: slab.slabFrom, + slabTo: slab.slabTo, + commissionPercent: slab.commissionPercent + }); + } + + cancelEdit(): void { + this.editingIndex = -1; + } + + saveEdit(): void { + const slab = this.slabs[this.editingIndex]; + const corrected = this.editSlabForm.getRawValue(); + + if (this.editSlabForm.invalid) { + this.editSlabForm.markAllAsTouched(); + this.alertService.alert({ type: 'Error', message: 'Fill the slab type, range and percentage' }); + return; + } + if (Number(corrected.slabFrom) > Number(corrected.slabTo)) { + this.alertService.alert({ type: 'Error', message: 'Slab From cannot be greater than Slab To' }); + return; + } + if (Number(corrected.commissionPercent) > 100) { + this.alertService.alert({ type: 'Error', message: 'Commission cannot be more than 100%' }); + return; + } + + this.organizationService + .updateCommissionSlab(slab.id, { + slabType: CommissionSlabMasterComponent.SLAB_TYPE_CODES[corrected.slabType] || corrected.slabType, + slabFrom: corrected.slabFrom, + slabTo: corrected.slabTo, + commissionPercent: corrected.commissionPercent, + locale: 'en' + }) + .subscribe({ + next: () => { + this.alertService.alert({ type: 'Success', message: 'Slab corrected' }); + this.editingIndex = -1; + this.loadSlabs(); + }, + // The backend refuses a correction that overlaps another slab; the reason has to reach the + // officer, not vanish. + error: (error: any) => { + this.alertService.alert({ + type: 'Error', + message: + error?.error?.errors?.[0]?.defaultUserMessage || error?.error?.defaultUserMessage || 'Could not correct the slab' + }); + } + }); + } + exit(): void { this.router.navigate(['/organization']); } @@ -123,6 +281,12 @@ export class CommissionSlabMasterComponent implements OnInit { save(): void { const newSlabs = this.slabs.filter((slab) => !slab.id); if (newSlabs.length === 0) { + // Save used to return here without a word, so a slab typed into the entry row but never added + // looked exactly like a slab that had been saved. + this.alertService.alert({ + type: 'Error', + message: 'Nothing new to save — add the slab to the table with + first' + }); return; } @@ -133,6 +297,23 @@ export class CommissionSlabMasterComponent implements OnInit { commissionPercent: slab.commissionPercent })); - this.organizationService.createCommissionSlabs(payload).subscribe(() => this.loadSlabs()); + this.organizationService.createCommissionSlabs(payload).subscribe({ + next: () => { + this.alertService.alert({ + type: 'Success', + message: `${newSlabs.length} ${newSlabs.length === 1 ? 'slab' : 'slabs'} saved` + }); + this.loadSlabs(); + }, + // The backend refuses an overlapping range; the officer needs to read why, not watch nothing + // happen and assume the save went through. + error: (error: any) => { + this.alertService.alert({ + type: 'Error', + message: + error?.error?.errors?.[0]?.defaultUserMessage || error?.error?.defaultUserMessage || 'Could not save the slabs' + }); + } + }); } } diff --git a/src/app/organization/dds-batch-processing/dds-batch-processing.component.html b/src/app/organization/dds-batch-processing/dds-batch-processing.component.html index be20aba12..ccd216bd7 100644 --- a/src/app/organization/dds-batch-processing/dds-batch-processing.component.html +++ b/src/app/organization/dds-batch-processing/dds-batch-processing.component.html @@ -11,7 +11,7 @@ -

DDS Interest & Commission Batch Processing

+

DDS Interest & Commission

@@ -20,8 +20,8 @@ Job Type - @for (type of jobTypes; track type) { - {{ type }} + @for (type of jobTypes; track type.code) { + {{ type.label }} } @@ -49,7 +49,7 @@ } - {{ isPostingJob ? 'Posting Date' : 'As-On Date' }} + {{ isPostingJob ? 'Posting Date' : 'Interest Date' }} @@ -65,69 +65,234 @@ - - Party From - - + + @if (!isCommissionJob) { + + Party From + + - - Party To - - + + Party To + + + }
- Generate runs a preview only — nothing is applied to any account. Post is a - separate, explicit step that commits the previewed amounts. + @if (isPostingJob) { + {{ jobTypeLabel }} moves what has already been calculated to where it belongs — into the + customer's balance, or out to the agent. Generate lists it first; nothing moves until you + press Post. + } @else { + {{ jobTypeLabel }} works out what has been earned since the last run and records it against + each account. It does not become spendable until the matching Posting job runs. + Generate previews it; nothing is recorded until you press Post. + }
- @if (hasGenerated) { + @if (generatedRun) {
-

Preview

+

{{ labelOf(generatedRun.jobType) }} — preview

+ + Run #{{ generatedRun.id }} · nothing applied yet +
+ @if (previewRows.length === 0) {

- No calculation preview available yet — this appears once the backend batch-job endpoint for "{{ - batchForm.get('jobType').value - }}" is connected. + Nothing to apply. Either this period has already been processed, or there was no activity in it.

} @else { - - - - - - - - - - @for (row of previewRows; track row) { - - - - - - } - -
AgentPartyAmount
{{ row.agent }}{{ row.party }}{{ row.amount | number: '1.2-2' }}
+
+ @if (showsCommissionBreakdown) { + + + + + + + + + + + + + + + @for (row of previewRows; track row.id) { + + + + + + + + + + + } + + + + + + + +
AgentPeriodCollectionRateCommissionTDSDeductionsPayable
+ {{ row.agentCode }} + {{ row.agentName }} + {{ row.fromDate | dateFormat }} – {{ row.toDate | dateFormat }}{{ money(row.baseAmount) }}{{ row.ratePercent }}%{{ money(row.grossAmount) }}{{ money(row.tdsAmount) }}{{ money(row.deductionAmount) }}{{ money(row.amount) }}
Total payable{{ money(previewTotal) }}
+ } @else { + + + + + @if (!isCommissionJob) { + + } + + + + + + @for (row of previewRows; track row.id) { + + + @if (!isCommissionJob) { + + } + + + + } + + + + + + + +
{{ isCommissionJob ? 'Agent' : 'Party' }}AgentPeriodAmount
+ @if (isCommissionJob) { + {{ row.agentCode }} + {{ row.agentName }} + } @else { + {{ row.partyCode }} + {{ row.partyName }} + } + {{ row.agentCode }}{{ row.fromDate | dateFormat }} – {{ row.toDate | dateFormat }}{{ money(row.amount) }}
Total{{ money(previewTotal) }}
+ } +
}
}
+ @if (generatedRun) { + + } -
+ + + @if (postingRunId) { +
+ + + Posting is running. + It continues even if you leave this screen — the list below updates when it finishes. + +
+ } + + +
+
+ +

Previous runs

+
+ + @if (recentRuns.length === 0) { +

No batch has been run yet.

+ } @else { +
+ + + + + + + + + + + + + + @for (run of recentRuns; track run.id) { + + + + + + + + + + + } + +
#JobDateEntriesAmountStatusPosted On
{{ run.id }}{{ labelOf(run.jobType) }}{{ run.jobDate | dateFormat }}{{ run.totalRows }}{{ money(run.totalAmount) }} + @switch (run.status) { + @case ('POSTED') { + Posted + } + @case ('POSTING') { + + Running + + } + @case ('FAILED') { + + Failed + + } + @default { + Not posted + } + } + {{ run.postedOnDate ? (run.postedOnDate | dateFormat) : '—' }}
+
+ } +
diff --git a/src/app/organization/dds-batch-processing/dds-batch-processing.component.scss b/src/app/organization/dds-batch-processing/dds-batch-processing.component.scss index 499ff754e..26dbefd94 100644 --- a/src/app/organization/dds-batch-processing/dds-batch-processing.component.scss +++ b/src/app/organization/dds-batch-processing/dds-batch-processing.component.scss @@ -9,16 +9,20 @@ $primary: #0f766e; .bp-container { - max-width: 900px; + // The commission breakdown carries eight columns; anything narrower makes it scroll on every + // screen rather than only on small ones. + max-width: 1180px; margin: 0 auto; padding: 24px 16px; + display: flex; + flex-direction: column; + gap: 20px; } .bp-header { display: flex; align-items: center; gap: 16px; - margin-bottom: 20px; &__title { margin: 0; @@ -95,8 +99,15 @@ $primary: #0f766e; margin: 0; font-size: 16px; font-weight: 600; + flex: 0 0 auto; } } + + &__note { + margin-left: auto; + font-size: 12px; + color: rgb(0 0 0 / 45%); + } } .bp-empty { @@ -105,23 +116,118 @@ $primary: #0f766e; margin: 0; } +// A wide table scrolls inside its own box rather than pushing the whole page sideways. +.bp-table-scroll { + overflow-x: auto; +} + .bp-table { width: 100%; border-collapse: collapse; + font-size: 13px; th { text-align: left; - font-size: 13px; + font-size: 12px; text-transform: uppercase; letter-spacing: 0.04em; color: rgb(0 0 0 / 55%); padding: 8px 10px; border-bottom: 2px solid rgb(0 0 0 / 10%); + white-space: nowrap; } td { - padding: 8px 10px; + padding: 7px 10px; border-bottom: 1px solid rgb(0 0 0 / 6%); + vertical-align: top; + } + + tfoot td { + border-bottom: none; + border-top: 2px solid rgb(0 0 0 / 10%); + font-weight: 600; + } + + // Figures line up column-wise so a wrong order of magnitude is visible at a glance. + .bp-num { + text-align: right; + font-variant-numeric: tabular-nums; + white-space: nowrap; + } + + .bp-minus { + color: #b91c1c; + } + + .bp-total { + font-weight: 600; + } + + .bp-period { + white-space: nowrap; + color: rgb(0 0 0 / 60%); + } +} + +.bp-code { + display: block; + font-weight: 600; +} + +.bp-sub { + display: block; + font-size: 12px; + color: rgb(0 0 0 / 55%); +} + +.bp-status { + display: inline-block; + padding: 2px 8px; + border-radius: 10px; + font-size: 12px; + font-weight: 600; + background: rgb(180 83 9 / 12%); + color: #92400e; + + &--posted { + background: rgb(15 118 110 / 12%); + color: $primary; + } + + /* Still crediting. Blue rather than the amber of a run nobody has posted — one is work in hand, + the other is work forgotten, and a clerk who reads them the same way runs it twice. */ + &--running { + background: rgb(22 84 143 / 12%); + color: #16548f; + display: inline-flex; + align-items: center; + gap: 5px; + } + + &--failed { + background: rgb(163 42 28 / 12%); + color: #a32a1c; + cursor: help; + } +} + +/* Says a run is still going after the clerk has left the button behind. */ +.bp-running { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 16px; + padding: 12px 16px; + border: 1px solid rgb(22 84 143 / 25%); + border-left: 3px solid #16548f; + border-radius: 6px; + background: rgb(22 84 143 / 6%); + color: #16548f; + font-size: 13px; + + strong { + font-weight: 600; } } @@ -132,6 +238,10 @@ $primary: #0f766e; margin-top: 20px; padding-top: 16px; border-top: 1px solid rgb(0 0 0 / 8%); + + i { + margin-right: 6px; + } } @media (width <= 900px) { diff --git a/src/app/organization/dds-batch-processing/dds-batch-processing.component.ts b/src/app/organization/dds-batch-processing/dds-batch-processing.component.ts index 2b6bf5cad..a4ed266a6 100644 --- a/src/app/organization/dds-batch-processing/dds-batch-processing.component.ts +++ b/src/app/organization/dds-batch-processing/dds-batch-processing.component.ts @@ -10,60 +10,70 @@ import { Component, OnInit, inject } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; import { Router } from '@angular/router'; +import { MatDialog } from '@angular/material/dialog'; +import { MatTooltip } from '@angular/material/tooltip'; +import { DateAdapter, MAT_DATE_FORMATS } from '@angular/material/core'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component'; +import { DdsDateAdapter, DDS_DATE_FORMATS } from './dds-date-adapter'; /** Custom Services */ import { OrganizationService } from 'app/organization/organization.service'; +import { AlertService } from 'app/core/alert/alert.service'; import { SettingsService } from 'app/settings/settings.service'; import { Dates } from 'app/core/utils/dates'; /** - * DDS Interest & Commission Batch Processing component. + * DDS Interest & Commission batch processing — the legacy Interest Part menu. * - * Six batch job types, each following the same deliberate two-step - * pattern from the legacy system: Calculate first (a dry-run preview), - * then Post as a separate, explicit confirmation — so a wrong calculation - * is never applied by accident. Modelled on the legacy Interest - * Calculation screen this replaces. + * Eight menu entries, which are four Calculate/Post pairs. A *calculation* job works out what has + * been earned since the last one and parks it: interest against the customer's account, commission + * against the agent. Neither is spendable yet. The matching *posting* job then moves that parked + * figure where it belongs — into the customer's balance, or out to the agent. * - * Backed by the DDS Batch Run API (`/dds/batchruns`). Only the three - * "Calculation" job types can actually be generated - the three "Posting" - * entries exist in the dropdown for parity with the legacy menu, but Post - * always applies whatever run was most recently generated in this session, - * regardless of which dropdown entry is currently selected (matching how - * the Post button here is only ever gated on hasGenerated, not on jobType). + * Every job is itself a two-step Generate then Post. Generate saves what it worked out and changes + * nothing else, so a figure that looks wrong is discarded by simply not posting it. */ @Component({ selector: 'mifosx-dds-batch-processing', templateUrl: './dds-batch-processing.component.html', styleUrls: ['./dds-batch-processing.component.scss'], - imports: [...STANDALONE_SHARED_IMPORTS] + imports: [ + ...STANDALONE_SHARED_IMPORTS, + // The reason a run failed is too long to sit in the column, and too useful to leave out. + MatTooltip + ], + // Day-first dates, as the legacy screen showed them. Declared here rather than application-wide: + // a @Component's providers reach only this component, so no other screen's datepicker changes. + providers: [ + { provide: DateAdapter, useClass: DdsDateAdapter }, + { provide: MAT_DATE_FORMATS, useValue: DDS_DATE_FORMATS } + ] }) export class DdsBatchProcessingComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); private router = inject(Router); private organizationService = inject(OrganizationService); + private alertService = inject(AlertService); + private dialog = inject(MatDialog); private settingsService = inject(SettingsService); private dateUtils = inject(Dates); - private static readonly JOB_TYPE_CODES: Record = { - 'Interest Calculation': 'INTEREST_CALCULATION', - 'OD Interest Calculation': 'OD_INTEREST_CALCULATION', - 'Commission Calculation': 'COMMISSION_CALCULATION' - }; - - private currentBatchRunId: number | null = null; - - offices: any[] = []; - selectedOfficeNames = new Set(); - - jobTypes = [ - 'Interest Calculation', - 'Interest Posting', - 'OD Interest Calculation', - 'OD Interest Posting', - 'Commission Calculation', - 'Commission Posting' + /** + * The menu as the legacy system lists it, paired with what the API calls each job. Ordered as + * calculate-then-post within each pair, matching the numbering on the legacy screen. + */ + jobTypes: Array<{ label: string; code: string }> = [ + { label: 'Interest Calculation', code: 'INTEREST_CALCULATION' }, + { label: 'Interest Posting', code: 'INTEREST_POSTING' }, + { label: 'OD Interest Calculation', code: 'OD_INTEREST_CALCULATION' }, + { label: 'OD Interest Posting', code: 'OD_INTEREST_POSTING' }, + { label: 'Commission Calculation', code: 'COMMISSION_CALCULATION' }, + { label: 'Commission Posting', code: 'COMMISSION_POSTING' }, + // Last, and on its own: the others move money onto accounts that stay open, while this one pays + // an account out and shuts it. Listing it beside a calculate/post pair would suggest it is the + // second half of one. + { label: 'Maturity Auto Close', code: 'MATURITY_AUTO_CLOSE' } ]; branchScopes = [ @@ -71,12 +81,23 @@ export class DdsBatchProcessingComponent implements OnInit { 'Selected' ]; - hasGenerated = false; - previewRows: Array<{ agent: string; party: string; amount: number }> = []; + private static readonly MONEY = new Intl.NumberFormat('en-IN', { + minimumFractionDigits: 2, + maximumFractionDigits: 2 + }); + + offices: any[] = []; + selectedOfficeNames = new Set(); + + /** The run currently previewed on screen, or null once it has been posted or discarded. */ + generatedRun: any = null; + previewRows: any[] = []; + + recentRuns: any[] = []; batchForm: UntypedFormGroup = this.formBuilder.group({ jobType: [ - 'Interest Calculation', + 'INTEREST_CALCULATION', Validators.required ], branchScope: [ @@ -105,18 +126,56 @@ export class DdsBatchProcessingComponent implements OnInit { ] }); + get jobType(): string { + return this.batchForm.get('jobType').value; + } + + get jobTypeLabel(): string { + return this.jobTypes.find((type) => type.code === this.jobType)?.label ?? this.jobType; + } + + /** Commission is per agent, so the party range does not narrow it. */ + get isCommissionJob(): boolean { + return this.jobType.startsWith('COMMISSION'); + } + get isPostingJob(): boolean { - return (this.batchForm.get('jobType').value as string).includes('Posting'); + return this.jobType.endsWith('_POSTING'); } get isBranchSelected(): boolean { return this.batchForm.get('branchScope')?.value === 'Selected'; } + /** + * Only a commission calculation has a breakdown worth showing: it is the one job where the figure + * applied is not the figure worked out, because TDS and the standing deductions come off it. + */ + get showsCommissionBreakdown(): boolean { + return this.jobType === 'COMMISSION_CALCULATION'; + } + + /** What Generate found, in words — the same figure the Post button is about to apply. */ + get previewTotal(): number { + return this.previewRows.reduce((total, row) => total + Number(row.amount || 0), 0); + } + ngOnInit(): void { this.organizationService.getOffices().subscribe((offices: any[]) => { this.offices = offices || []; }); + this.loadRecentRuns(); + } + + money(value: any): string { + if (value === null || value === undefined || value === '') { + return '—'; + } + return DdsBatchProcessingComponent.MONEY.format(Number(value)); + } + + labelOf(code: string): string { + return this.jobTypes.find((type) => type.code === code)?.label ?? code; } toggleOffice(officeName: string): void { @@ -127,59 +186,202 @@ export class DdsBatchProcessingComponent implements OnInit { } } + /** + * What every past run did and whether it was applied. Without this a generated-but-unposted run is + * invisible the moment the page is left, and there is no way to tell whether a month's interest + * was ever actually posted. + */ + private loadRecentRuns(): void { + this.organizationService.getDdsBatchRuns().subscribe((runs: any[]) => { + this.recentRuns = runs || []; + }); + } + generate(): void { const raw = this.batchForm.getRawValue(); - const jobType = DdsBatchProcessingComponent.JOB_TYPE_CODES[raw.jobType]; - - if (!jobType) { - // A "Posting" entry was selected - there is nothing to calculate for - // it, only to post (see class-level note on how Post is wired). - this.hasGenerated = true; - this.previewRows = []; - return; - } - const dateFormat = this.settingsService.dateFormat; + const payload: any = { - jobType, + jobType: raw.jobType, branchScope: raw.branchScope, jobDate: this.dateUtils.formatDate(raw.jobDate, dateFormat), agentFrom: raw.agentFrom, agentTo: raw.agentTo, - partyFrom: raw.partyFrom, - partyTo: raw.partyTo, dateFormat, locale: this.settingsService.language.code }; + // The party range narrows an interest job to particular accounts; commission is worked out per + // agent on their whole collection, so sending a range there would only imply a filter that has + // no effect. + if (!this.isCommissionJob) { + payload.partyFrom = raw.partyFrom; + payload.partyTo = raw.partyTo; + } + if (raw.branchScope === 'Selected') { payload.officeNames = Array.from(this.selectedOfficeNames); } this.organizationService.generateDdsBatchRun(payload).subscribe((result: any) => { - this.currentBatchRunId = result.resourceId; - this.organizationService.getDdsBatchRun(this.currentBatchRunId).subscribe((batchRun: any) => { - this.previewRows = (batchRun.rows || []).map((row: any) => ({ - agent: row.agentCode, - party: row.partyCode || '—', - amount: row.amount - })); - this.hasGenerated = true; + this.organizationService.getDdsBatchRun(result.resourceId).subscribe((batchRun: any) => { + this.generatedRun = batchRun; + this.previewRows = batchRun.rows || []; + + // A run that found nothing is the normal outcome of a period already processed, not a + // failure — but it has to say so, or an empty table reads as the screen being broken. + if (this.previewRows.length === 0) { + this.alertService.alert({ + type: 'Information', + message: `${this.jobTypeLabel} found nothing to apply. Either this period has already been processed, or there was no activity in it.` + }); + } else { + this.alertService.alert({ + type: 'Success', + message: `${this.jobTypeLabel}: ${this.previewRows.length} ${this.previewRows.length === 1 ? 'entry' : 'entries'} totalling ${this.money(this.previewTotal)} calculated. Nothing has been applied yet — review and press Post.` + }); + } + + this.loadRecentRuns(); }); }); } post(): void { - if (!this.currentBatchRunId) { + if (!this.generatedRun || this.previewRows.length === 0) { return; } - this.organizationService.postDdsBatchRun(this.currentBatchRunId).subscribe(() => { - this.currentBatchRunId = null; - this.hasGenerated = false; + + const count = this.previewRows.length; + const total = this.money(this.previewTotal); + const isPosting = this.isPostingJob; + + // Post is the step that moves money, and it cannot be undone from this screen — so it names + // exactly what is about to change rather than asking a generic "are you sure". + this.dialog + .open(ConfirmationDialogComponent, { + data: { + type: 'warn', + heading: `Post ${this.jobTypeLabel}?`, + dialogContext: + `${count} ${count === 1 ? 'entry' : 'entries'} totalling ${total} will be applied. ` + + (isPosting + ? 'This credits the customers or pays the agents, and appears on the ledger. ' + : 'This records what has been earned against each account. ') + + 'It cannot be undone from this screen.' + } + }) + .afterClosed() + .subscribe((response: any) => { + if (response?.confirm) { + this.applyRun(count, total); + } + }); + } + + /** + * Applies the run, and says what actually happened rather than assuming it finished. + * + * A crediting run costs what a savings deposit costs on every account it touches, so the server + * takes it off the request and does it in the background - the reply comes back in a moment while + * the work is still going on. Reporting that as "posted" would tell the clerk a run had finished + * seconds after it began, and they would go looking for credits that were still being written. + * + * So the run is followed until it settles. A calculation run is done by the time the reply lands + * and says so at once; a crediting run says it is under way, and the list beneath refreshes itself + * until the server marks it posted - or failed, with the reason the server recorded. + */ + private applyRun(count: number, total: string): void { + const label = this.jobTypeLabel; + const runId = this.generatedRun.id; + const crediting = this.isPostingJob; + + this.organizationService.postDdsBatchRun(runId).subscribe(() => { + this.generatedRun = null; this.previewRows = []; + this.loadRecentRuns(); + + if (!crediting) { + this.alertService.alert({ + type: 'Success', + message: `${label} posted — ${count} ${count === 1 ? 'entry' : 'entries'}, ${total}.` + }); + return; + } + + this.postingRunId = runId; + this.alertService.alert({ + type: 'Posting', + message: `${label} is running — ${count} ${count === 1 ? 'entry' : 'entries'}, ${total}. ` + + 'It continues in the background; this list will update when it is done.' + }); + this.followRun(runId, label); }); } + /** The crediting run being followed, if one is. The screen reads this to show it is under way. */ + postingRunId: number | null = null; + + /** + * Watches a background run until the server marks it settled. + * + * Polled rather than pushed: a batch that takes minutes needs no faster news than this, and a poll + * costs nothing when the answer is one row. It gives up after a while rather than asking forever - + * the run is still on the list either way, and a clerk can read its status there. + */ + private followRun(runId: number, label: string): void { + let checksLeft = DdsBatchProcessingComponent.MAX_STATUS_CHECKS; + + const check = () => { + this.organizationService.getDdsBatchRuns().subscribe({ + next: (runs: any[]) => { + const run = (runs || []).find((each: any) => each.id === runId); + const status = run?.status; + + if (status === 'POSTED' || status === 'FAILED') { + this.postingRunId = null; + this.loadRecentRuns(); + this.alertService.alert( + status === 'POSTED' + ? { type: 'Success', message: `${label} posted — ${run.rowsPosted ?? run.totalRows} entries applied.` } + : { type: 'Error', message: `${label} failed: ${run.failureReason || 'see the run for details'}` } + ); + return; + } + + if (--checksLeft > 0) { + setTimeout(check, DdsBatchProcessingComponent.STATUS_CHECK_INTERVAL_MS); + } else { + // Still going after the wait. Left on the list rather than reported either way - saying + // "failed" for a run that is merely long would be worse than saying nothing. + this.postingRunId = null; + this.loadRecentRuns(); + } + }, + // A failed status check says nothing about the run itself, which carries on regardless. + error: () => { + if (--checksLeft > 0) { + setTimeout(check, DdsBatchProcessingComponent.STATUS_CHECK_INTERVAL_MS); + } else { + this.postingRunId = null; + } + } + }); + }; + + setTimeout(check, DdsBatchProcessingComponent.STATUS_CHECK_INTERVAL_MS); + } + + private static readonly STATUS_CHECK_INTERVAL_MS = 4000; + /** Twenty minutes of checking, which is longer than any run measured here has taken. */ + private static readonly MAX_STATUS_CHECKS = 300; + + /** Drops the preview without applying it — the safe way out of a calculation that looks wrong. */ + discard(): void { + this.generatedRun = null; + this.previewRows = []; + } + exit(): void { this.router.navigate(['/organization']); } diff --git a/src/app/organization/dds-batch-processing/dds-date-adapter.ts b/src/app/organization/dds-batch-processing/dds-date-adapter.ts new file mode 100644 index 000000000..10d4dd8b5 --- /dev/null +++ b/src/app/organization/dds-batch-processing/dds-date-adapter.ts @@ -0,0 +1,71 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Injectable } from '@angular/core'; +import { NativeDateAdapter, MatDateFormats } from '@angular/material/core'; + +/** + * Day-first dates for the DDS batch screen. + * + * Angular Material's stock adapter shows and reads dates the American way — 12/31/2026 — and its + * parser hands typed input to `Date.parse`, which cannot read a day-first date at all: typing + * 31/12/2026 yields no date, so the field silently empties and reports itself invalid. + * + * On a screen where the date decides which period's interest is calculated, both halves matter: a + * clerk who reads 12/31 as the 12th of some month picks the wrong period, and one who types the date + * they are used to gets an error with nothing to explain it. + * + * Scoped to this component rather than provided application-wide. Every other datepicker in the app + * has the same behaviour, and changing it globally would alter screens outside DDS. + */ +@Injectable() +export class DdsDateAdapter extends NativeDateAdapter { + /** Matches d/m/yyyy, with or without leading zeros, on / . or - separators. */ + private static readonly DAY_FIRST = /^(\d{1,2})[/.-](\d{1,2})[/.-](\d{4})$/; + + override parse(value: any): Date | null { + if (typeof value === 'string') { + const match = DdsDateAdapter.DAY_FIRST.exec(value.trim()); + if (match) { + const day = Number(match[1]); + const month = Number(match[2]); + const year = Number(match[3]); + + const parsed = new Date(year, month - 1, day); + // JavaScript rolls an out-of-range day into the next month rather than rejecting it, so + // 31/02/2026 would quietly become the 3rd of March. Reading the parts back catches that. + const isRealDate = + parsed.getFullYear() === year && parsed.getMonth() === month - 1 && parsed.getDate() === day; + return isRealDate ? parsed : null; + } + } + return super.parse(value); + } + + override format(date: Date, displayFormat: any): string { + if (displayFormat === 'input') { + return `${this.pad(date.getDate())}/${this.pad(date.getMonth() + 1)}/${date.getFullYear()}`; + } + return super.format(date, displayFormat); + } + + private pad(value: number): string { + return value < 10 ? `0${value}` : `${value}`; + } +} + +/** Tells the datepicker to render the field itself day-first; the calendar keeps its usual labels. */ +export const DDS_DATE_FORMATS: MatDateFormats = { + parse: { dateInput: 'input' }, + display: { + dateInput: 'input', + monthYearLabel: { year: 'numeric', month: 'short' }, + dateA11yLabel: { year: 'numeric', month: 'long', day: 'numeric' }, + monthYearA11yLabel: { year: 'numeric', month: 'long' } + } +}; diff --git a/src/app/organization/organization-routing.module.ts b/src/app/organization/organization-routing.module.ts index 0ddb76ce3..64fb7f7ba 100644 --- a/src/app/organization/organization-routing.module.ts +++ b/src/app/organization/organization-routing.module.ts @@ -18,6 +18,7 @@ import { OrganizationComponent } from './organization.component'; import { LoanProvisioningCriteriaComponent } from './loan-provisioning-criteria/loan-provisioning-criteria.component'; import { OfficesComponent } from './offices/offices.component'; import { AgentMasterComponent } from './agent-master/agent-master.component'; +import { CreateAgentComponent } from './agent-master/create-agent/create-agent.component'; import { CommissionSlabMasterComponent } from './commission-slab-master/commission-slab-master.component'; import { DdsBatchProcessingComponent } from './dds-batch-processing/dds-batch-processing.component'; import { EmployeesComponent } from './employees/employees.component'; @@ -242,8 +243,23 @@ const routes: Routes = [ }, { path: 'agent-master', - component: AgentMasterComponent, - data: { title: 'Agent Master', breadcrumb: 'Agent Master' } + data: { title: 'Agent Master', breadcrumb: 'Agent Master' }, + children: [ + { + path: '', + component: AgentMasterComponent + }, + { + path: 'create', + component: CreateAgentComponent, + data: { title: 'Create Agent', breadcrumb: 'Create' } + }, + { + path: ':agentId/edit', + component: CreateAgentComponent, + data: { title: 'Edit Agent', breadcrumb: 'Edit', routeParamBreadcrumb: false } + } + ] }, { path: 'commission-slab-master', diff --git a/src/app/organization/organization.service.ts b/src/app/organization/organization.service.ts index 6aab81df1..44e296c6e 100644 --- a/src/app/organization/organization.service.ts +++ b/src/app/organization/organization.service.ts @@ -272,6 +272,17 @@ export class OrganizationService { return this.http.get('/dds/parties'); } + /** + * The parties a given agent collects for — what the daily transaction screen works from, since + * it handles one agent's round at a time. + * + * @param {string | number} agentId Agent ID. + * @returns {Observable} That agent's DDS Parties. + */ + getPartiesOfAgent(agentId: string | number): Observable { + return this.http.get('/dds/parties', { params: new HttpParams().set('agentId', String(agentId)) }); + } + /** * @param {string | number} partyId Party ID. * @returns {Observable} DDS Party. @@ -306,6 +317,65 @@ export class OrganizationService { return this.http.post(`/dds/parties/${partyId}?command=close`, closeCommand); } + /** + * The DDS accounts whose term has run out and which are still open - the branch's list of closures + * waiting to be done. Nothing closes itself: a matured account is money the bank owes the customer, + * and where it goes has to be agreed with them. + * + * @returns {Observable} Accounts due for closure, oldest maturity first. + */ + getPartiesDueForClosure(): Observable { + return this.http.get('/dds/parties/dueforclosure'); + } + + /** + * The customer's own accounts a DDS maturity can be paid into - active savings accounts of theirs, + * with fixed and recurring deposits and their other DDS scheme accounts left out. + * + * The server applies the same conditions the payout itself does, so this list can never offer a + * choice that is then refused on save. Filtering here instead meant the two drifted apart: the + * accounts on offer once included a scheme account of a party that had already closed. + * + * @param {string | number} clientId Client ID. + * @returns {Observable} id, accountNo, productName and balance for each. + */ + getCreditableAccounts(clientId: string | number): Observable { + return this.http.get('/dds/parties/creditaccounts', { params: { clientId: String(clientId) } }); + } + + /** + * Pays a matured DDS account out and closes it. What is paid is what the account holds, not the + * figure quoted at opening. + * + * @param {string | number} partyId Party ID. + * @param {any} payoutCommand { toSavingsAccountNo } - another savings account of the same customer + * to move the money into. Omit it when they have none here: the money is then parked in the + * matured-deposits payable account for the branch to send on to their account at another bank. + * @returns {Observable} + */ + payOutParty(partyId: string | number, payoutCommand: any): Observable { + return this.http.post(`/dds/parties/${partyId}?command=payout`, payoutCommand); + } + + /** + * Closes a DDS account before its term is up, and pays out what is left - one act, unlike a + * maturity payout, which leaves the account open for the clerk to close separately. + * + * @param {string | number} partyId Party ID. + * @param {any} closeCommand { shortfallBorneBy, closeDate, dateFormat, locale, toSavingsAccountNo? }. + * closeDate, dateFormat and locale go up exactly as they do on an ordinary close. + * shortfallBorneBy is mandatory + * and is CUSTOMER (their interest is recalculated at the scheme's reduced rate and the + * difference taken back) or AGENT (the customer keeps a full month's interest and the agent + * funds the days left in it). The agent is charged a share of their commission either way. + * toSavingsAccountNo works as it does for a payout. The lock-in, the rate reduction and the + * agent's share come from the scheme and are applied by the server. + * @returns {Observable} + */ + prematureCloseParty(partyId: string | number, closeCommand: any): Observable { + return this.http.post(`/dds/parties/${partyId}?command=prematureclose`, closeCommand); + } + /** * @param {any} transaction Daily transaction batch (PARTY_TRAN collections or a WITHDRAW_DEPOSITE transfer) to post. * @returns {Observable} @@ -314,6 +384,39 @@ export class OrganizationService { return this.http.post('/dds/partytransactions', transaction); } + /** + * What an agent wrote down on their round for a date, with what it adds up to. + * + * The counter reads it to count cash against; the agent's own screen shows it back as the record + * of their round. One call for both, so the two can never disagree about the same day. + */ + getAgentCollectionDay(agentId: any, entryDate: string): Observable { + return this.http.get(`/dds/collectionentries?agentId=${agentId}&entryDate=${entryDate}`); + } + + /** + * Turns an agent's outstanding entries for a date into real credits - all of them, together. + * + * No amount is sent. What settles is what the entries say, to the rupee: a figure typed at the + * counter would be a second opinion about a total the entries already give. + */ + settleAgentCollections(settlement: any): Observable { + return this.http.post('/dds/collectionentries/settle', settlement); + } + + /** + * An agent's DDS transactions. The daily entry screen uses this to show what has already been + * keyed for the agent today, so the same collection book is not entered twice. + * + * @param {string | number} agentId Agent ID. + * @returns {Observable} That agent's transactions. + */ + getPartyTransactionsOfAgent(agentId: string | number): Observable { + return this.http.get('/dds/partytransactions', { + params: new HttpParams().set('agentId', String(agentId)) + }); + } + /** * @returns {Observable} DDS Commission Slabs data */ @@ -329,6 +432,18 @@ export class OrganizationService { return this.http.post('/dds/commissionslabs', { slabs }); } + /** + * Corrects one saved commission slab. The backend re-checks the corrected range against the other + * slabs of its type and refuses an overlap. + * + * @param {string | number} slabId Commission slab ID. + * @param {any} slab The corrected slab. + * @returns {Observable} Command result. + */ + updateCommissionSlab(slabId: string | number, slab: any): Observable { + return this.http.put(`/dds/commissionslabs/${slabId}`, slab); + } + /** * @param {any} batch Batch run filters (jobType, branchScope, jobDate, agentFrom/To, partyFrom/To) to generate a preview for. * @returns {Observable} @@ -345,6 +460,24 @@ export class OrganizationService { return this.http.get(`/dds/batchruns/${batchRunId}`); } + /** + * @returns {Observable} Every batch run, most recent first — who ran what, and whether it was posted. + */ + getDdsBatchRuns(): Observable { + return this.http.get('/dds/batchruns'); + } + + /** + * Ends the relationship with an agent. Refused while they still have accounts assigned, hold + * collected cash, or are owed commission — distinct from deactivating, which is never refused. + * + * @param {string | number} agentId Agent ID. + * @returns {Observable} + */ + closeAgent(agentId: string | number): Observable { + return this.http.post(`/dds/agents/${agentId}`, {}); + } + /** * @param {string | number} batchRunId Batch run ID to post - applies exactly the rows it previewed. * @returns {Observable} diff --git a/src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/create-daily-deposit-scheme-product.component.html b/src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/create-daily-deposit-scheme-product.component.html deleted file mode 100644 index aeeca63ce..000000000 --- a/src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/create-daily-deposit-scheme-product.component.html +++ /dev/null @@ -1,131 +0,0 @@ - - -
-
- -

{{ 'labels.heading.Create Daily Deposit Scheme Product' | translate }}

-
- - - - - - - - - {{ 'labels.inputs.DETAILS' | translate }} - -
- -
-
- - - - - CBS Base Chart - -
- @if (activeBaseVersion) { -
-
-
-

{{ activeBaseVersion.versionLabel }}

-

- Active CBS base chart - Effective from {{ activeBaseVersion.effectiveFromDate }} -

-
- Active -
- - @if (activeBaseVersion.remarks) { -

{{ activeBaseVersion.remarks }}

- } - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Sr.{{ slab.srNo }}Particular{{ slab.description }}From{{ slab.fromPeriod }}To{{ slab.toPeriod ?? 'N/A' }}Period Type{{ slab.periodTypeLabel }}Annual Rate (%) - {{ slab.annualInterestRate | number: '1.2-2' }} -
-
- } @else { -

Loading CBS base chart...

- } -
-
-
- -
- -
- -
-
-
diff --git a/src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/create-daily-deposit-scheme-product.component.scss b/src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/create-daily-deposit-scheme-product.component.scss deleted file mode 100644 index 6f9abbbfd..000000000 --- a/src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/create-daily-deposit-scheme-product.component.scss +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -$primary: #0f766e; - -.rdp-container { - max-width: 960px; - margin: 0 auto; - padding: 24px 16px; -} - -.rdp-header { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 20px; - - &__icon { - font-size: 20px; - color: $primary; - } - - &__title { - margin: 0; - font-size: 22px; - font-weight: 500; - } -} - -.rdp-tabs { - background: var(--md-sys-color-surface, #fff); - border-radius: 8px; - box-shadow: 0 2px 8px rgb(0 0 0 / 8%); - - ::ng-deep { - .mat-mdc-tab-header { - border-bottom: 1px solid rgb(0 0 0 / 8%); - } - - .mat-mdc-tab { - min-width: 100px; - padding: 0 12px; - height: 52px; - } - - .mat-mdc-tab-body-wrapper { - min-height: 200px; - } - - .mat-mdc-tab-body { - overflow: visible !important; - } - - .mat-mdc-tab-body-content { - overflow: visible !important; - } - } -} - -.rdp-tab-icon { - margin-right: 8px; - font-size: 14px; -} - -.rdp-tab-content { - padding: 24px 16px; -} - -.base-rate-card { - padding: 16px; - border: 1px solid #dbe4f0; - border-radius: 14px; - background: linear-gradient(180deg, #f8fbff 0%, #fff 100%); -} - -.base-rate-header { - display: flex; - align-items: start; - justify-content: space-between; - gap: 12px; - margin-bottom: 12px; -} - -.base-rate-title { - margin: 0; - font-size: 18px; - font-weight: 600; -} - -.base-rate-meta { - margin: 6px 0 0; - color: #5b6b7f; - display: flex; - gap: 12px; - flex-wrap: wrap; -} - -.base-rate-badge { - padding: 4px 10px; - border-radius: 999px; - background: #dcfce7; - color: #166534; - font-size: 12px; - font-weight: 700; -} - -.base-rate-remarks { - margin: 0 0 12px; - color: #334155; -} - -.base-rate-table { - width: 100%; - border-collapse: collapse; -} - -.rdp-action-bar { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 20px; - padding: 16px 0; - border-top: 1px solid rgb(0 0 0 / 8%); - - &__cancel { - color: rgb(0 0 0 / 60%); - } - - &__right { - display: flex; - gap: 12px; - } -} diff --git a/src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/create-daily-deposit-scheme-product.component.ts b/src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/create-daily-deposit-scheme-product.component.ts deleted file mode 100644 index 741b1425a..000000000 --- a/src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/create-daily-deposit-scheme-product.component.ts +++ /dev/null @@ -1,346 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -/** Angular Imports */ -import { AfterViewInit, Component, ViewChild, inject } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; - -/** Custom Components — Daily Deposit Scheme's own stepper, kept independent - * of Recurring Deposit Products so future changes to either never cross over. */ -import { DailyDepositSchemeProductDetailsStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-details-step/daily-deposit-scheme-product-details-step.component'; -import { DailyDepositSchemeProductCurrencyStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-currency-step/daily-deposit-scheme-product-currency-step.component'; -import { DailyDepositSchemeProductTermsStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-terms-step/daily-deposit-scheme-product-terms-step.component'; -import { DailyDepositSchemeProductSettingsStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-settings-step/daily-deposit-scheme-product-settings-step.component'; -import { DailyDepositSchemeProductChargesStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-charges-step/daily-deposit-scheme-product-charges-step.component'; -import { DailyDepositSchemeProductAccountingStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-accounting-step/daily-deposit-scheme-product-accounting-step.component'; -import { - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow -} from '@angular/material/table'; - -/** Custom Services */ -import { ProductsService } from 'app/products/products.service'; -import { SettingsService } from 'app/settings/settings.service'; -import { Accounting } from 'app/core/utils/accounting'; -import { Dates } from 'app/core/utils/dates'; -import { MatTabsModule } from '@angular/material/tabs'; -import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; - -@Component({ - selector: 'mifosx-create-daily-deposit-scheme-product', - templateUrl: './create-daily-deposit-scheme-product.component.html', - styleUrls: ['./create-daily-deposit-scheme-product.component.scss'], - imports: [ - ...STANDALONE_SHARED_IMPORTS, - MatTabsModule, - DailyDepositSchemeProductDetailsStepComponent, - DailyDepositSchemeProductCurrencyStepComponent, - DailyDepositSchemeProductTermsStepComponent, - DailyDepositSchemeProductSettingsStepComponent, - DailyDepositSchemeProductChargesStepComponent, - DailyDepositSchemeProductAccountingStepComponent, - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow - ] -}) -export class CreateDailyDepositSchemeProductComponent implements AfterViewInit { - private route = inject(ActivatedRoute); - private productsService = inject(ProductsService); - private router = inject(Router); - private settingsService = inject(SettingsService); - private accounting = inject(Accounting); - private dateUtils = inject(Dates); - - @ViewChild(DailyDepositSchemeProductDetailsStepComponent, { static: true }) - detailsStep: DailyDepositSchemeProductDetailsStepComponent; - @ViewChild(DailyDepositSchemeProductCurrencyStepComponent, { static: true }) - currencyStep: DailyDepositSchemeProductCurrencyStepComponent; - @ViewChild(DailyDepositSchemeProductTermsStepComponent, { static: true }) - termsStep: DailyDepositSchemeProductTermsStepComponent; - @ViewChild(DailyDepositSchemeProductSettingsStepComponent, { static: true }) - settingsStep: DailyDepositSchemeProductSettingsStepComponent; - @ViewChild(DailyDepositSchemeProductChargesStepComponent, { static: true }) - chargesStep: DailyDepositSchemeProductChargesStepComponent; - @ViewChild(DailyDepositSchemeProductAccountingStepComponent, { static: true }) - accountingStep: DailyDepositSchemeProductAccountingStepComponent; - - productsTemplate: any; - accountingRuleData: string[] = []; - selectedTabIndex = 0; - rdInterestRates: any[] = []; - readonly rdSlabColumns = [ - 'srNo', - 'description', - 'fromPeriod', - 'toPeriod', - 'periodTypeLabel', - 'annualInterestRate' - ]; - - constructor() { - this.route.data.subscribe((data: { dailyDepositSchemeProductsTemplate: any }) => { - this.productsTemplate = data.dailyDepositSchemeProductsTemplate; - }); - this.accountingRuleData = this.accounting.getAccountingRulesForSavings(); - this.productsService.getFdBaseInterestRates().subscribe({ - next: (rates: any[]) => { - this.rdInterestRates = rates || []; - }, - error: () => { - this.rdInterestRates = []; - } - }); - } - - ngAfterViewInit(): void { - // The DDS product screen only shows the legacy Account Type Creation fields (Details tab) - - // currency/terms/settings/charges/accounting still exist underneath (the account-creation - // engine needs them), but they're filled in here with fixed sensible defaults rather than - // asked of the user, since the legacy screen never asked for them either. - const template = this.productsTemplate || {}; - - this.currencyForm.patchValue({ - currencyCode: this.currencyForm.value.currencyCode || 'INR', - digitsAfterDecimal: this.currencyForm.value.digitsAfterDecimal ?? 2 - }); - - const firstOptionId = (options: any[]) => options?.[0]?.id ?? ''; - this.termsForm.patchValue({ - interestCompoundingPeriodType: firstOptionId(template.interestCompoundingPeriodTypeOptions), - interestCalculationDaysInYearType: firstOptionId(template.interestCalculationDaysInYearTypeOptions), - minDepositAmount: this.termsForm.value.minDepositAmount || 1, - maxDepositAmount: this.termsForm.value.maxDepositAmount || 999999999 - }); - - // accountingRule 1 = None, matching the legacy screen (which never asked for GL mappings) - - // this also clears every accounting-tab required-field validator, since those only apply - // under Cash/Accrual accounting. - this.accountingForm.patchValue({ accountingRule: 1 }); - - this.detailsForm.get('autoInterestPost')?.valueChanges.subscribe(() => this.syncAutoInterestPost()); - this.detailsForm.get('autoInterestPostWhen')?.valueChanges.subscribe(() => this.syncAutoInterestPost()); - this.syncAutoInterestPost(); - } - - /** Mirrors the simple "Auto Interest Post" / "Auto Interest Post When" / "Auto Interest Post - * Day" fields onto the (hidden) terms-step fields that the account-creation engine actually - * reads. */ - private syncAutoInterestPost(): void { - const autoPost = this.detailsForm?.value.autoInterestPost ?? true; - const postingPeriodType = this.detailsForm?.value.autoInterestPostWhen; - const postingDay = this.detailsForm?.value.autoInterestPostDay; - this.termsForm?.patchValue({ - autoPost, - interestPostingPeriodType: postingPeriodType || this.termsForm.value.interestPostingPeriodType, - frequencyFixDay: postingDay || this.termsForm.value.frequencyFixDay - }); - } - - get detailsForm() { - return this.detailsStep?.DailyDepositSchemeProductDetailsForm; - } - - get currencyForm() { - return this.currencyStep?.DailyDepositSchemeProductCurrencyForm; - } - - get termsForm() { - return this.termsStep?.DailyDepositSchemeProductTermsForm; - } - - get settingsForm() { - return this.settingsStep?.DailyDepositSchemeProductSettingsForm; - } - - get accountingForm() { - return this.accountingStep?.DailyDepositSchemeProductAccountingForm; - } - - get productFormValid() { - return !!this.detailsForm?.valid; - } - - get activeBaseVersion() { - const chart = - this.rdInterestRates.find((version: any) => version.fdType === 'DOUBLE' && version.isActive) ?? - this.rdInterestRates.find((version: any) => version.fdType === 'DOUBLE') ?? - this.rdInterestRates[0]; - if (chart) { - return { - ...chart, - versionLabel: chart.versionLabel?.replace('FD', 'DDS'), - remarks: chart.remarks?.replace('FD', 'DDS') - }; - } - return null; - } - - get activeBaseSlabs() { - // The base chart is borrowed from an FD product purely for its period/amount-range - // *structure* - the account-creation engine needs some chart to exist. Its actual rate is - // never used: every slab's annualInterestRate is overridden with the Details tab's own - // Interest Rate field, so DDS never depends on FD's real interest figures. - const rate = this.detailsForm?.value.interestRate; - return (this.activeBaseVersion?.slabs || []).map((slab: any) => ({ - ...slab, - annualInterestRate: rate ?? slab.annualInterestRate - })); - } - - get product() { - // accountTypeGroup / autoInterestPost / autoInterestPostWhen / autoInterestPostDay are the - // legacy Account Type Creation screen's own fields - not real product API parameters. The - // auto-interest-post trio is already mirrored onto termsStep (autoPost / - // interestPostingPeriodType / frequencyFixDay) via syncAutoInterestPost(); accountTypeGroup - // is folded into the description here instead. - const { - accountTypeGroup, - autoInterestPost, - autoInterestPostWhen, - autoInterestPostDay, - interestRate, - ...detailsPayload - } = this.detailsStep.DailyDepositSchemeProductDetails; - const description = [ - accountTypeGroup, - detailsPayload.description - ] - .filter((part) => !!part) - .join(' - '); - - return { - ...detailsPayload, - description, - ...this.currencyStep.DailyDepositSchemeProductCurrency, - ...this.termsStep.DailyDepositSchemeProductTerms, - ...this.settingsStep.DailyDepositSchemeProductSettings, - ...this.chargesStep.DailyDepositSchemeProductCharges, - ...this.accountingStep.DailyDepositSchemeProductAccounting, - charts: this.productCharts, - rdSlabs: this.activeBaseSlabs - }; - } - - get productCharts() { - const activeBaseVersion = this.activeBaseVersion; - if (!activeBaseVersion?.slabs?.length) { - return []; - } - const dateFormat = this.settingsService.dateFormat; - - return [ - { - name: activeBaseVersion.versionLabel || 'CBS Base Chart', - description: activeBaseVersion.remarks || activeBaseVersion.versionLabel || 'CBS base chart', - fromDate: - this.dateUtils.formatDate(activeBaseVersion.effectiveFromDate, dateFormat) || - activeBaseVersion.effectiveFromDate, - ...(activeBaseVersion.effectiveToDate - ? { - endDate: - this.dateUtils.formatDate(activeBaseVersion.effectiveToDate, dateFormat) || - activeBaseVersion.effectiveToDate - } - : {}), - isPrimaryGroupingByAmount: false, - locale: this.settingsService.language.code, - dateFormat, - chartSlabs: this.activeBaseSlabs.map((slab: any) => { - const chartSlab: any = { - periodType: slab.periodTypeEnum, - fromPeriod: slab.fromPeriod, - toPeriod: slab.toPeriod, - annualInterestRate: slab.annualInterestRate, - description: slab.description || '', - incentives: [] as any[] - }; - - if (slab.amountRangeFrom !== null && slab.amountRangeFrom !== undefined && slab.amountRangeFrom !== '') { - chartSlab.amountRangeFrom = slab.amountRangeFrom; - } - - if (slab.amountRangeTo !== null && slab.amountRangeTo !== undefined && slab.amountRangeTo !== '') { - chartSlab.amountRangeTo = slab.amountRangeTo; - } - - return chartSlab; - }) - } - ] as any[]; - } - - submit() { - const product: any = this.pruneEmptyValues({ - ...this.product, - charts: this.productCharts, - charges: (this.product.charges ?? []).map((charge: any) => ({ id: charge.id })), - locale: this.settingsService.language.code, - productCategory: 'DDS' - }); - if (!product.description) { - product.description = ''; - } - delete product.advancedAccountingRules; - this.productsService.createRecurringDepositProduct(product).subscribe((response: any) => { - this.router.navigate( - [ - '../', - response.resourceId - ], - { relativeTo: this.route } - ); - }); - } - - private pruneEmptyValues(value: T): T { - if (Array.isArray(value)) { - return value.map((item) => this.pruneEmptyValues(item)).filter((item) => item !== undefined) as T; - } - - if (value && typeof value === 'object') { - return Object.entries(value as Record).reduce( - (acc, [ - key, - currentValue - ]) => { - const cleanedValue = this.pruneEmptyValues(currentValue); - if (cleanedValue === undefined) { - return acc; - } - return { - ...acc, - [key]: cleanedValue - }; - }, - {} as Record - ) as T; - } - - if (value === '' || value === null || value === undefined) { - return undefined as T; - } - - return value; - } -} diff --git a/src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/daily-deposit-scheme-products-template.resolver.ts b/src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/daily-deposit-scheme-products-template.resolver.ts deleted file mode 100644 index f70da1eda..000000000 --- a/src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/daily-deposit-scheme-products-template.resolver.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -/** Angular Imports */ -import { Injectable, inject } from '@angular/core'; - -/** rxjs Imports */ -import { Observable } from 'rxjs'; - -/** Custom Services */ -import { ProductsService } from '../../products.service'; - -/** - * Daily Deposit Scheme Products template resolver. - * - * Reuses the Recurring Deposit product template — a Daily Deposit Scheme - * product is created through the same underlying product-creation engine. - */ -@Injectable() -export class DailyDepositSchemeProductsTemplateResolver { - private productsService = inject(ProductsService); - - /** - * Returns the Daily Deposit Scheme products template. - * @returns {Observable} - */ - resolve(): Observable { - return this.productsService.getRecurringDepositProductsTemplate(); - } -} diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-accounting-step/daily-deposit-scheme-product-accounting-step.component.html b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-accounting-step/daily-deposit-scheme-product-accounting-step.component.html deleted file mode 100644 index f7fd053a9..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-accounting-step/daily-deposit-scheme-product-accounting-step.component.html +++ /dev/null @@ -1,121 +0,0 @@ - - -
-
-
- - - -

Accounting

-
- -
-
-
-

Accounting Rule

-
-
- Cash -
-
- - @if (isCashOrAccrualAccounting()) { - @for (section of accountingSectionConfigs; track section.key) { -
-
-

{{ section.title }}

- -
-
- @for (row of section.rows; track row.key) { - @if ( - (!row.accrualOnly || isAccrualAccounting()) && (!row.dormancyOnly || isDormancyTrackingEnabled()) - ) { -
-
- - {{ section.pillLabel }} - - {{ row.label }} -
- -
- } - } -
- -
- @for ( - otherRow of getOtherGlMappingsArray(section.otherMappingsControlName).controls; - track $index; - let i = $index - ) { -
-
-
- - {{ section.pillLabel }} - - -
- -
- - GL Code - - @for (glAccount of section.otherAccountOptions; track glAccount.id) { - - {{ '(' + glAccount.glCode + ') ' + glAccount.name }} - - } - - -
-
- -
- } -
-
- } - } -
-
-
diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-accounting-step/daily-deposit-scheme-product-accounting-step.component.scss b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-accounting-step/daily-deposit-scheme-product-accounting-step.component.scss deleted file mode 100644 index bac63a1a2..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-accounting-step/daily-deposit-scheme-product-accounting-step.component.scss +++ /dev/null @@ -1,303 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -$border: #e2e8f0; -$gray-50: #f8f9fa; -$gray-200: #e8eaed; -$gray-700: #334155; -$gray-900: #202124; - -.rdpa-section { - border: 1px solid $border; - border-radius: 10px; - background: #fff; -} - -.rdpa-section__header { - display: flex; - align-items: center; - gap: 10px; - padding: 12px 24px; - background: $gray-50; - border-bottom: 1px solid $gray-200; - - h3 { - margin: 0; - font-size: 14px; - font-weight: 600; - color: $gray-900; - } -} - -.rdpa-section__icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - border-radius: 6px; - background: #eff6ff; - color: #2563eb; - font-size: 13px; -} - -.rdpa-section__body { - padding: 16px 24px 20px; -} - -.rdpa-group { - & + & { - margin-top: 20px; - } - - &__header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - margin: 0 0 12px; - } - - h4 { - margin: 0 0 12px; - font-size: 12px; - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; - color: #94a3b8; - } -} - -.rdpa-add-btn { - border: 0; - background: transparent; - color: #2563eb; - font-size: 12px; - font-weight: 700; - padding: 0; - cursor: pointer; -} - -.rdpa-list { - display: flex; - flex-direction: column; - gap: 10px; -} - -.rdpa-list + .rdpa-list { - margin-top: 12px; -} - -.rdpa-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - padding: 10px 12px; - border: 1px solid $border; - border-radius: 8px; - background: #fff; - - @media (width <= 900px) { - flex-direction: column; - align-items: stretch; - } -} - -.rdpa-row:has(mifosx-gl-account-selector) { - align-items: stretch; -} - -.rdpa-row--other { - border-style: dashed; - border-color: #cbd5e1; - background: #f8fafc; - display: grid; - grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr); - align-items: center; - padding: 12px 16px; - - @media (width <= 1100px) { - grid-template-columns: 1fr; - } -} - -.rdpa-other-row-wrap { - display: grid; - grid-template-columns: minmax(0, 1fr) auto; - align-items: center; - gap: 12px; - - @media (width <= 1100px) { - grid-template-columns: 1fr; - gap: 8px; - } -} - -.rdpa-row__meta { - display: flex; - align-items: center; - gap: 10px; - min-width: 0; - flex: 1 1 auto; -} - -.rdpa-row__meta--other { - min-width: 0; - flex: 1 1 auto; - align-self: stretch; -} - -.rdpa-row__label { - font-size: 14px; - font-weight: 600; - color: $gray-700; -} - -.rdpa-row__input { - flex: 0 0 22rem; - max-width: 22rem; - min-width: 18rem; - - @media (width <= 900px) { - width: 100%; - max-width: 100%; - min-width: 0; - } -} - -.rdpa-row__input :is(mifosx-gl-account-selector) { - width: 100%; - display: block; -} - -.rdpa-row__input :is(mifosx-gl-account-selector) :is(.layout-column) { - width: 100%; - margin-right: 0 !important; -} - -.rdpa-row__input :is(mifosx-gl-account-selector) :is(mat-form-field) { - width: 100%; -} - -.rdpa-row__actions { - display: flex; - align-items: center; - gap: 10px; - min-width: 0; -} - -.rdpa-row__actions--other { - justify-content: flex-end; - align-self: stretch; - width: 100%; - - @media (width <= 1100px) { - justify-content: flex-start; - } -} - -.rdpa-row__name { - width: 100%; - min-width: 0; - max-width: 24rem; - height: 40px; - border: 1px solid #cbd5e1; - border-radius: 6px; - padding: 0 12px; - font-size: 14px; - background: #fff; - - @media (width <= 900px) { - max-width: 100%; - } -} - -.rdpa-row__name--other-account { - max-width: 100%; - flex: 1 1 auto; - height: 48px; -} - -.rdpa-row__other-field { - width: 100%; - min-width: 0; -} - -.rdpa-row__other-field :is(mat-form-field) { - width: 100%; -} - -.rdpa-row__other-field :is(.mat-mdc-form-field-required-marker) { - display: none; -} - -.rdpa-remove-btn { - border: 0; - background: transparent; - color: #ef4444; - font-size: 18px; - line-height: 1; - cursor: pointer; - padding: 0 4px; -} - -.rdpa-remove-btn--side { - place-self: center end; - margin-right: 4px; - - @media (width <= 1100px) { - justify-self: start; - margin-right: 0; - } -} - -.rdpa-radio-group { - display: flex; - gap: 24px; - flex-wrap: wrap; -} - -.rdpa-pill { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 2px 8px; - border-radius: 999px; - font-size: 10px; - font-weight: 700; - letter-spacing: 0.04em; - text-transform: uppercase; - white-space: nowrap; - - &--green { - background: #dcfce7; - color: #16a34a; - } - - &--amber { - background: #fef3c7; - color: #d97706; - } - - &--blue { - background: #dbeafe; - color: #2563eb; - } -} - -@media (width <= 900px) { - .rdpa-row__meta { - margin-bottom: 4px; - } - - .rdpa-row__input { - flex: 1 1 auto; - max-width: 100%; - } -} diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-accounting-step/daily-deposit-scheme-product-accounting-step.component.ts b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-accounting-step/daily-deposit-scheme-product-accounting-step.component.ts deleted file mode 100644 index 09ef9c1d7..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-accounting-step/daily-deposit-scheme-product-accounting-step.component.ts +++ /dev/null @@ -1,423 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import { Component, Input, OnInit, inject } from '@angular/core'; -import { - FormsModule, - ReactiveFormsModule, - UntypedFormArray, - UntypedFormBuilder, - UntypedFormControl, - UntypedFormGroup, - Validators -} from '@angular/forms'; -import { MatRadioButton, MatRadioGroup } from '@angular/material/radio'; -import { GlAccountSelectorComponent } from 'app/shared/accounting/gl-account-selector/gl-account-selector.component'; -import { Accounting } from 'app/core/utils/accounting'; -import { GLAccount } from 'app/shared/models/general.model'; -import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; - -type AccountingSectionKey = 'assets' | 'liabilities' | 'expenses' | 'income'; - -type AccountingRowConfig = { - key: string; - label: string; - inputLabel: string; - controlName: string; - options: GLAccount[]; - required?: boolean; - accrualOnly?: boolean; - dormancyOnly?: boolean; -}; - -type AccountingSectionConfig = { - key: AccountingSectionKey; - title: string; - pillLabel: string; - pillClass: 'green' | 'amber' | 'blue'; - rows: AccountingRowConfig[]; - otherMappingsControlName: string; - otherAccountOptions: GLAccount[]; -}; - -type OtherGlMappingValue = { - accountName: string; - glAccountId: number | null; -}; - -@Component({ - selector: 'mifosx-daily-deposit-scheme-product-accounting-step', - templateUrl: './daily-deposit-scheme-product-accounting-step.component.html', - styleUrls: ['./daily-deposit-scheme-product-accounting-step.component.scss'], - imports: [ - ...STANDALONE_SHARED_IMPORTS, - FormsModule, - ReactiveFormsModule, - MatRadioGroup, - MatRadioButton, - GlAccountSelectorComponent - ] -}) -export class DailyDepositSchemeProductAccountingStepComponent implements OnInit { - private formBuilder = inject(UntypedFormBuilder); - private accounting = inject(Accounting); - - @Input() DailyDepositSchemeProductsTemplate: any; - @Input() accountingRuleData: any; - @Input() DailyDepositSchemeProductFormValid: boolean; - @Input() isDormancyTrackingActive: any; - - DailyDepositSchemeProductAccountingForm: UntypedFormGroup; - assetAccountOptions: GLAccount[] = []; - liabilityAccountOptions: GLAccount[] = []; - expenseAccountOptions: GLAccount[] = []; - incomeAccountOptions: GLAccount[] = []; - accountingSectionConfigs: AccountingSectionConfig[] = []; - - constructor() { - this.createDailyDepositSchemeProductAccountingForm(); - this.setConditionalControls(); - } - - ngOnInit() { - const accountingMappings = this.DailyDepositSchemeProductsTemplate?.accountingMappings || {}; - const mappings = { - ...accountingMappings, - otherGlMappingsMap: this.DailyDepositSchemeProductsTemplate?.otherGlMappingsMap - }; - const accountingMappingOptions = this.DailyDepositSchemeProductsTemplate?.accountingMappingOptions || {}; - // Defaults to Cash (2) for a brand-new product (no existing accountingRule yet), but preserves - // the product's own current accounting rule when editing - previously this was hardcoded to 2 - // unconditionally, which silently forced every edited product onto Cash-based accounting (and - // its required GL mappings) even for products that were and should remain "None". - const accountingRuleId = this.DailyDepositSchemeProductsTemplate?.accountingRule?.id ?? 2; - - this.assetAccountOptions = accountingMappingOptions.assetAccountOptions || []; - this.liabilityAccountOptions = accountingMappingOptions.liabilityAccountOptions || []; - this.expenseAccountOptions = accountingMappingOptions.expenseAccountOptions || []; - this.incomeAccountOptions = accountingMappingOptions.incomeAccountOptions || []; - - this.buildAccountingSectionConfigs(); - - this.DailyDepositSchemeProductAccountingForm.patchValue({ accountingRule: accountingRuleId }); - this.syncAccountingControls(accountingRuleId); - this.patchAccountingValues(mappings); - this.setDormancyControl(); - } - - createDailyDepositSchemeProductAccountingForm() { - this.DailyDepositSchemeProductAccountingForm = this.formBuilder.group({ - accountingRule: [2], - savingsReferenceAccountId: [''], - savingsControlAccountId: [''], - transfersInSuspenseAccountId: [''], - interestOnSavingsAccountId: [''], - incomeFromFeeAccountId: [''], - incomeFromPenaltyAccountId: [''], - feesReceivableAccountId: [''], - penaltiesReceivableAccountId: [''], - interestPayableAccountId: [''], - escheatLiabilityId: [''], - assetsOtherGlMappings: this.formBuilder.array([]), - liabilitiesOtherGlMappings: this.formBuilder.array([]), - expensesOtherGlMappings: this.formBuilder.array([]), - incomeOtherGlMappings: this.formBuilder.array([]) - }); - } - - private setConditionalControls() { - this.DailyDepositSchemeProductAccountingForm.get('accountingRule')?.valueChanges.subscribe((rule: number) => { - this.syncAccountingControls(rule); - }); - } - - private patchAccountingValues(mappings: any) { - this.DailyDepositSchemeProductAccountingForm.patchValue({ - savingsReferenceAccountId: mappings.savingsReferenceAccount?.id || '', - savingsControlAccountId: mappings.savingsControlAccount?.id || '', - transfersInSuspenseAccountId: mappings.transfersInSuspenseAccount?.id || '', - interestOnSavingsAccountId: mappings.interestOnSavingsAccount?.id || '', - incomeFromFeeAccountId: mappings.incomeFromFeeAccount?.id || '', - incomeFromPenaltyAccountId: mappings.incomeFromPenaltyAccount?.id || '', - feesReceivableAccountId: mappings.feeReceivableAccount?.id || '', - penaltiesReceivableAccountId: mappings.penaltyReceivableAccount?.id || '', - interestPayableAccountId: mappings.interestPayableAccount?.id || '', - escheatLiabilityId: mappings.escheatLiabilityAccount?.id || '' - }); - - if (this.isCashOrAccrualAccounting()) { - const glMappings = mappings.otherGlMappingsMap || mappings.otherGlMappings; - this.setOtherGlMappings('assetsOtherGlMappings', glMappings?.assets || [], this.assetAccountOptions); - this.setOtherGlMappings( - 'liabilitiesOtherGlMappings', - glMappings?.liabilities || [], - this.liabilityAccountOptions - ); - this.setOtherGlMappings('expensesOtherGlMappings', glMappings?.expenses || [], this.expenseAccountOptions); - this.setOtherGlMappings('incomeOtherGlMappings', glMappings?.income || [], this.incomeAccountOptions); - } - } - - private createOtherGlMappingGroup(otherGlMapping: Partial = {}) { - return this.formBuilder.group({ - accountName: [ - otherGlMapping.accountName || '', - Validators.required - ], - glAccountId: [ - otherGlMapping.glAccountId || '', - Validators.required - ] - }); - } - - getOtherGlMappingsArray(controlName: string): UntypedFormArray { - return this.DailyDepositSchemeProductAccountingForm.get(controlName) as UntypedFormArray; - } - - private setOtherGlMappings(controlName: string, otherGlMappings: any[], options: GLAccount[]) { - const arr = this.getOtherGlMappingsArray(controlName); - arr.clear(); - (otherGlMappings || []).forEach((m) => { - arr.push( - this.createOtherGlMappingGroup({ - accountName: m.accountName, - glAccountId: m.glAccountId || options.find((o) => o.glCode === m.glCode)?.id || null - }) - ); - }); - } - - addOtherGlMapping(controlName: string) { - this.getOtherGlMappingsArray(controlName).push(this.createOtherGlMappingGroup()); - } - - removeOtherGlMapping(controlName: string, index: number) { - this.getOtherGlMappingsArray(controlName).removeAt(index); - } - - getOtherGlMappingControl(controlName: string, index: number, fieldName: string): UntypedFormControl { - return this.getOtherGlMappingsArray(controlName).at(index).get(fieldName) as UntypedFormControl; - } - - private clearOtherGlMappings() { - [ - 'assetsOtherGlMappings', - 'liabilitiesOtherGlMappings', - 'expensesOtherGlMappings', - 'incomeOtherGlMappings' - ].forEach((c) => this.getOtherGlMappingsArray(c).clear()); - } - - private getOtherGlMappings() { - const map = (controlName: string, options: GLAccount[]) => - this.getOtherGlMappingsArray(controlName) - .value.filter((item: OtherGlMappingValue) => !!item.accountName || !!item.glAccountId) - .map((item: OtherGlMappingValue) => ({ - accountName: item.accountName, - glCode: options.find((o) => o.id === item.glAccountId)?.glCode || '' - })) - .filter((item: { accountName: string; glCode: string }) => !!item.accountName || !!item.glCode); - - const otherGlMappings = { - assets: map('assetsOtherGlMappings', this.assetAccountOptions), - liabilities: map('liabilitiesOtherGlMappings', this.liabilityAccountOptions), - expenses: map('expensesOtherGlMappings', this.expenseAccountOptions), - income: map('incomeOtherGlMappings', this.incomeAccountOptions) - }; - return Object.values(otherGlMappings).some((v) => v.length) ? otherGlMappings : null; - } - - private setDormancyControl() { - this.isDormancyTrackingActive?.valueChanges?.subscribe(() => { - this.syncAccountingControls(this.DailyDepositSchemeProductAccountingForm.value.accountingRule); - }); - } - - isDormancyTrackingEnabled(): boolean { - return this.isDormancyTrackingActive?.value ?? this.isDormancyTrackingActive ?? false; - } - - private syncAccountingControls(accountingRule: number) { - const setRequired = (name: string, required: boolean) => { - const ctrl = this.DailyDepositSchemeProductAccountingForm.get(name); - if (!ctrl) return; - if (required) { - ctrl.setValidators(Validators.required); - } else { - ctrl.clearValidators(); - } - ctrl.updateValueAndValidity({ emitEvent: false }); - }; - - const cashOrAccrual = this.accounting.isCashOrAccrualAccountingRuleId(accountingRule); - const accrualOnly = this.accounting.isAccrualAccountingRuleId(accountingRule); - - setRequired('savingsReferenceAccountId', cashOrAccrual); - setRequired('savingsControlAccountId', cashOrAccrual); - setRequired('transfersInSuspenseAccountId', cashOrAccrual); - setRequired('interestOnSavingsAccountId', cashOrAccrual); - setRequired('incomeFromFeeAccountId', cashOrAccrual); - setRequired('incomeFromPenaltyAccountId', cashOrAccrual); - setRequired('feesReceivableAccountId', accrualOnly); - setRequired('penaltiesReceivableAccountId', accrualOnly); - setRequired('interestPayableAccountId', accrualOnly); - setRequired('escheatLiabilityId', cashOrAccrual && this.isDormancyTrackingEnabled()); - - if (!cashOrAccrual) { - this.clearOtherGlMappings(); - } - } - - buildAccountingSectionConfigs() { - this.accountingSectionConfigs = [ - { - key: 'assets', - title: 'Assets', - pillLabel: 'Asset', - pillClass: 'green', - rows: [ - { - key: 'savingsReferenceAccountId', - label: 'Savings Reference Account', - inputLabel: 'Savings reference account', - controlName: 'savingsReferenceAccountId', - options: this.assetAccountOptions, - required: true - }, - { - key: 'feesReceivableAccountId', - label: 'Fees Receivable Account', - inputLabel: 'Fees receivable account', - controlName: 'feesReceivableAccountId', - options: this.assetAccountOptions, - accrualOnly: true - }, - { - key: 'penaltiesReceivableAccountId', - label: 'Penalties Receivable Account', - inputLabel: 'Penalties receivable account', - controlName: 'penaltiesReceivableAccountId', - options: this.assetAccountOptions, - accrualOnly: true - } - ], - otherMappingsControlName: 'assetsOtherGlMappings', - otherAccountOptions: this.assetAccountOptions - }, - { - key: 'liabilities', - title: 'Liabilities', - pillLabel: 'Liability', - pillClass: 'green', - rows: [ - { - key: 'savingsControlAccountId', - label: 'Savings Control Account', - inputLabel: 'Savings control account', - controlName: 'savingsControlAccountId', - options: this.liabilityAccountOptions, - required: true - }, - { - key: 'transfersInSuspenseAccountId', - label: 'Transfers In Suspense Account', - inputLabel: 'Transfers in suspense account', - controlName: 'transfersInSuspenseAccountId', - options: this.liabilityAccountOptions, - required: true - }, - { - key: 'interestPayableAccountId', - label: 'Interest Payable Account', - inputLabel: 'Interest payable account', - controlName: 'interestPayableAccountId', - options: this.liabilityAccountOptions, - accrualOnly: true - }, - { - key: 'escheatLiabilityId', - label: 'Escheat Liability Account', - inputLabel: 'Escheat liability account', - controlName: 'escheatLiabilityId', - options: this.liabilityAccountOptions, - dormancyOnly: true - } - ], - otherMappingsControlName: 'liabilitiesOtherGlMappings', - otherAccountOptions: this.liabilityAccountOptions - }, - { - key: 'expenses', - title: 'Expenses', - pillLabel: 'Expense', - pillClass: 'amber', - rows: [ - { - key: 'interestOnSavingsAccountId', - label: 'Interest On Savings Account', - inputLabel: 'Interest on savings account', - controlName: 'interestOnSavingsAccountId', - options: this.expenseAccountOptions, - required: true - } - ], - otherMappingsControlName: 'expensesOtherGlMappings', - otherAccountOptions: this.expenseAccountOptions - }, - { - key: 'income', - title: 'Income', - pillLabel: 'Income', - pillClass: 'blue', - rows: [ - { - key: 'incomeFromFeeAccountId', - label: 'Income From Fees', - inputLabel: 'Income from fees', - controlName: 'incomeFromFeeAccountId', - options: this.incomeAccountOptions, - required: true - }, - { - key: 'incomeFromPenaltyAccountId', - label: 'Income From Penalties', - inputLabel: 'Income from penalties', - controlName: 'incomeFromPenaltyAccountId', - options: this.incomeAccountOptions, - required: true - } - ], - otherMappingsControlName: 'incomeOtherGlMappings', - otherAccountOptions: this.incomeAccountOptions - } - ]; - } - - get DailyDepositSchemeProductAccounting() { - const { - assetsOtherGlMappings, - liabilitiesOtherGlMappings, - expensesOtherGlMappings, - incomeOtherGlMappings, - ...accounting - } = this.DailyDepositSchemeProductAccountingForm.value; - const otherGlMappings = this.getOtherGlMappings(); - return { ...accounting, ...(otherGlMappings ? { otherGlMappings } : {}) }; - } - - isCashOrAccrualAccounting(): boolean { - return this.accounting.isCashOrAccrualAccountingRuleId( - this.DailyDepositSchemeProductAccountingForm.value.accountingRule - ); - } - - isAccrualAccounting(): boolean { - return this.accounting.isAccrualAccountingRuleId(this.DailyDepositSchemeProductAccountingForm.value.accountingRule); - } -} diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-charges-step/daily-deposit-scheme-product-charges-step.component.html b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-charges-step/daily-deposit-scheme-product-charges-step.component.html deleted file mode 100644 index b2c701878..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-charges-step/daily-deposit-scheme-product-charges-step.component.html +++ /dev/null @@ -1,85 +0,0 @@ - - -
-
- -

{{ 'labels.heading.Charges' | translate }}

-
-
-
- - {{ 'labels.inputs.Charge' | translate }} - - @for (charge of chargeData | chargesFilter: chargesDataSource : currencyCode.value; track charge) { - - {{ charge.name }} - - } - - - -
- - @if (chargesDataSource.length > 0) { -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Name' | translate }} - {{ charge.name + ', ' + charge.currency.displaySymbol }} - {{ 'labels.inputs.Type' | translate }} - {{ charge.chargeCalculationType.value }} - {{ 'labels.inputs.Amount' | translate }} - {{ charge.amount }} - {{ 'labels.inputs.Collected On' | translate }} - {{ charge.chargeTimeType.value }} - {{ 'labels.inputs.Actions' | translate }} - -
-
- } -
-
diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-charges-step/daily-deposit-scheme-product-charges-step.component.scss b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-charges-step/daily-deposit-scheme-product-charges-step.component.scss deleted file mode 100644 index 661815e4c..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-charges-step/daily-deposit-scheme-product-charges-step.component.scss +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -.rdpch-section { - border: 1px solid #e2e8f0; - border-radius: 10px; - overflow: hidden; - background: #fff; - - &__header { - display: flex; - align-items: center; - gap: 10px; - padding: 12px 24px; - background: #f8f9fa; - border-bottom: 1px solid #e8eaed; - - i { - font-size: 14px; - color: #0ea5e9; - } - - h3 { - margin: 0; - font-size: 14px; - font-weight: 600; - color: #202124; - } - } - - &__body { - padding: 16px 24px; - } -} - -.rdpch-add-row { - display: flex; - align-items: center; - gap: 16px; - flex-wrap: wrap; - - &__select { - flex: 1; - min-width: 200px; - } -} - -.rdpch-add-btn { - height: 36px; - border-radius: 8px; - font-size: 13px; - font-weight: 600; -} - -.rdpch-table-wrapper { - border: 1px solid #e2e8f0; - border-radius: 8px; - overflow: hidden; - margin-top: 12px; - - table { - width: 100%; - } -} diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-charges-step/daily-deposit-scheme-product-charges-step.component.ts b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-charges-step/daily-deposit-scheme-product-charges-step.component.ts deleted file mode 100644 index 4db6f9ae9..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-charges-step/daily-deposit-scheme-product-charges-step.component.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import { Component, Input, OnInit, inject } from '@angular/core'; -import { UntypedFormControl } from '@angular/forms'; -import { MatDialog } from '@angular/material/dialog'; - -import { TranslateService } from '@ngx-translate/core'; -import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; -import { MatButton, MatIconButton } from '@angular/material/button'; -import { FaIconComponent } from '@fortawesome/angular-fontawesome'; -import { - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow -} from '@angular/material/table'; -import { ChargesFilterPipe } from '../../../../pipes/charges-filter.pipe'; -import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; - -@Component({ - selector: 'mifosx-daily-deposit-scheme-product-charges-step', - templateUrl: './daily-deposit-scheme-product-charges-step.component.html', - styleUrls: ['./daily-deposit-scheme-product-charges-step.component.scss'], - imports: [ - ...STANDALONE_SHARED_IMPORTS, - FaIconComponent, - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatIconButton, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow, - ChargesFilterPipe - ] -}) -export class DailyDepositSchemeProductChargesStepComponent implements OnInit { - dialog = inject(MatDialog); - private translateService = inject(TranslateService); - - @Input() DailyDepositSchemeProductsTemplate: any; - @Input() currencyCode: UntypedFormControl; - - chargeData: any; - - chargesDataSource: {}[]; - displayedColumns: string[] = [ - 'name', - 'chargeCalculationType', - 'amount', - 'chargeTimeType', - 'action' - ]; - - ngOnInit() { - this.chargeData = this.DailyDepositSchemeProductsTemplate.chargeOptions; - if (!(this.DailyDepositSchemeProductsTemplate === undefined) && this.DailyDepositSchemeProductsTemplate.id) { - this.chargesDataSource = this.DailyDepositSchemeProductsTemplate.charges; - } else { - this.chargesDataSource = []; - } - this.currencyCode.valueChanges.subscribe(() => (this.chargesDataSource = [])); - } - - addCharge(charge: any) { - this.chargesDataSource = this.chargesDataSource.concat([charge.value]); - charge.value = ''; - } - - deleteCharge(charge: any) { - const deleteChargeDialogRef = this.dialog.open(DeleteDialogComponent, { - data: { deleteContext: this.translateService.instant('labels.inputs.Charge') + ' ' + charge.name } - }); - deleteChargeDialogRef.afterClosed().subscribe((response: any) => { - if (response.delete) { - this.chargesDataSource.splice(this.chargesDataSource.indexOf(charge), 1); - this.chargesDataSource = this.chargesDataSource.concat([]); - } - }); - } - - get DailyDepositSchemeProductCharges() { - return { - charges: this.chargesDataSource - }; - } -} diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-currency-step/daily-deposit-scheme-product-currency-step.component.html b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-currency-step/daily-deposit-scheme-product-currency-step.component.html deleted file mode 100644 index 86af4f7ea..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-currency-step/daily-deposit-scheme-product-currency-step.component.html +++ /dev/null @@ -1,89 +0,0 @@ - - -
-
-
- - Currency & Account Status Configuration -
-
-
- - {{ 'labels.inputs.Currency' | translate }} - - @for (currency of currencyData; track currency) { - - {{ currency.name }} - - } - - Default: Indian Rupee - auto-set for domestic products - - {{ 'labels.inputs.Currency' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.Decimal Places' | translate }} - - @for (places of decimalPlacesData; track places) { - {{ places }} - } - - - {{ 'labels.inputs.Decimal Places' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - -
- -
-
-

Dormancy Alert

-

Enable automated notifications when an account becomes inactive based on product rules.

-
- - yes - no - -
- -
-
-

Nomination Facility

-

Allow account holders to designate individuals to receive funds in the event of account holder's death.

-
- - yes - no - -
- -
-
-

Auto Debit Facility

-

Enable automatic deduction of funds for recurring payments, utility bills, or EMI obligations.

-
- - yes - no - -
-
-
-
diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-currency-step/daily-deposit-scheme-product-currency-step.component.scss b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-currency-step/daily-deposit-scheme-product-currency-step.component.scss deleted file mode 100644 index d7a2abd40..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-currency-step/daily-deposit-scheme-product-currency-step.component.scss +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -$accent: #0ea5e9; -$border: #e2e8f0; -$gray-50: #f8f9fa; -$gray-200: #e8eaed; -$gray-900: #202124; - -.rdpc-section { - border: 1px solid $border; - border-radius: 10px; - background: #fff; - - & + & { - margin-top: 16px; - } - - &__header { - display: flex; - align-items: center; - gap: 10px; - padding: 12px 24px; - background: $gray-50; - border-bottom: 1px solid $gray-200; - - i { - font-size: 14px; - color: $accent; - } - - span { - margin: 0; - font-size: 14px; - font-weight: 600; - color: $gray-900; - } - } -} - -.rdpc-body { - background: #fff; -} - -.rdpc-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 4px 16px; - padding: 16px 24px; - - @media (width <= 600px) { - grid-template-columns: 1fr; - } -} - -.rdpc-field { - width: 100%; -} - -.rdpc-alert { - display: flex; - align-items: center; - justify-content: space-between; - gap: 24px; - padding: 18px 24px 24px; - border-top: 1px solid $border; - background: #f8fafc; - - @media (width <= 760px) { - flex-direction: column; - align-items: flex-start; - } - - &__content { - h4 { - margin: 0 0 6px; - font-size: 14px; - font-weight: 600; - color: $gray-900; - } - - p { - margin: 0; - font-size: 12px; - line-height: 1.5; - color: #64748b; - max-width: 540px; - } - } - - &__choices { - display: flex; - align-items: center; - gap: 18px; - flex-wrap: wrap; - } -} diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-currency-step/daily-deposit-scheme-product-currency-step.component.ts b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-currency-step/daily-deposit-scheme-product-currency-step.component.ts deleted file mode 100644 index 098800d33..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-currency-step/daily-deposit-scheme-product-currency-step.component.ts +++ /dev/null @@ -1,104 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import { Component, OnInit, Input, inject } from '@angular/core'; -import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; -import { MatTooltip } from '@angular/material/tooltip'; -import { MatRadioGroup, MatRadioButton } from '@angular/material/radio'; -import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; - -@Component({ - selector: 'mifosx-daily-deposit-scheme-product-currency-step', - templateUrl: './daily-deposit-scheme-product-currency-step.component.html', - styleUrls: ['./daily-deposit-scheme-product-currency-step.component.scss'], - imports: [ - ...STANDALONE_SHARED_IMPORTS, - MatTooltip, - MatRadioGroup, - MatRadioButton - ] -}) -export class DailyDepositSchemeProductCurrencyStepComponent implements OnInit { - private formBuilder = inject(UntypedFormBuilder); - - @Input() DailyDepositSchemeProductsTemplate: any; - - DailyDepositSchemeProductCurrencyForm: UntypedFormGroup; - - currencyData: any; - decimalPlacesData = [ - 2, - 0, - 3 - ]; - - constructor() { - this.createDailyDepositSchemeProductCurrencyForm(); - } - - ngOnInit() { - this.currencyData = this.DailyDepositSchemeProductsTemplate.currencyOptions.filter( - (c: { code: string }) => c.code === 'INR' - ); - - this.DailyDepositSchemeProductCurrencyForm.patchValue({ - currencyCode: this.DailyDepositSchemeProductsTemplate.currency?.code || this.currencyData?.[0]?.code || '', - digitsAfterDecimal: - this.DailyDepositSchemeProductsTemplate.digitsAfterDecimal ?? - this.DailyDepositSchemeProductsTemplate.currency?.decimalPlaces ?? - 2, - dormancyAlert: this.DailyDepositSchemeProductsTemplate.dormancyAlert ?? false, - isDormancyTrackingActive: this.DailyDepositSchemeProductsTemplate.isDormancyTrackingActive ?? false, - daysToInactive: this.DailyDepositSchemeProductsTemplate.daysToInactive ?? 30, - daysToDormancy: this.DailyDepositSchemeProductsTemplate.daysToDormancy ?? 60, - daysToEscheat: this.DailyDepositSchemeProductsTemplate.daysToEscheat ?? 90, - nominationFacility: this.DailyDepositSchemeProductsTemplate.nominationFacility ?? false, - autoDebitFacility: this.DailyDepositSchemeProductsTemplate.autoDebitFacility ?? false - }); - } - - createDailyDepositSchemeProductCurrencyForm() { - this.DailyDepositSchemeProductCurrencyForm = this.formBuilder.group({ - currencyCode: [ - '', - Validators.required - ], - digitsAfterDecimal: [ - 2, - [ - Validators.required, - Validators.min(0) - ] - ], - dormancyAlert: [false], - isDormancyTrackingActive: [false], - daysToInactive: [30], - daysToDormancy: [60], - daysToEscheat: [90], - nominationFacility: [false], - autoDebitFacility: [false] - }); - } - - get DailyDepositSchemeProductCurrency() { - const formValue = this.DailyDepositSchemeProductCurrencyForm.value; - const result: any = { - currencyCode: formValue.currencyCode, - digitsAfterDecimal: formValue.digitsAfterDecimal, - dormancyAlert: formValue.dormancyAlert, - nominationFacility: formValue.nominationFacility, - autoDebitFacility: formValue.autoDebitFacility - }; - if (formValue.dormancyAlert) { - result.daysToInactive = formValue.daysToInactive; - result.daysToDormancy = formValue.daysToDormancy; - result.daysToEscheat = formValue.daysToEscheat; - } - return result; - } -} diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-details-step/daily-deposit-scheme-product-details-step.component.html b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-details-step/daily-deposit-scheme-product-details-step.component.html deleted file mode 100644 index 2fae4cfe6..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-details-step/daily-deposit-scheme-product-details-step.component.html +++ /dev/null @@ -1,77 +0,0 @@ - - -
-
-
- -

Account Type Creation

-
-
- - Account Type Group - - @if (DailyDepositSchemeProductDetailsForm.controls.accountTypeGroup.hasError('required')) { - Account Type Group is required - } - - - - A/c Type - - @if (DailyDepositSchemeProductDetailsForm.controls.shortName.hasError('required')) { - A/c Type is required - } - - - - A/c Type Description - - @if (DailyDepositSchemeProductDetailsForm.controls.name.hasError('required')) { - A/c Type Description is required - } - - - - Interest Rate (%) - - @if (DailyDepositSchemeProductDetailsForm.controls.interestRate.hasError('required')) { - Interest Rate is required - } - - - - Auto Interest Post - - @for (option of yesNoOptions; track option.value) { - {{ option.label }} - } - - - - @if (DailyDepositSchemeProductDetailsForm.get('autoInterestPost')?.value) { - - Auto Interest Post When - - @for (option of autoInterestPostWhenData; track option.id) { - {{ option.value }} - } - - @if (DailyDepositSchemeProductDetailsForm.controls.autoInterestPostWhen.hasError('required')) { - Auto Interest Post When is required - } - - - - Auto Interest Post Day - - - } -
-
-
diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-details-step/daily-deposit-scheme-product-details-step.component.scss b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-details-step/daily-deposit-scheme-product-details-step.component.scss deleted file mode 100644 index ca87b04f4..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-details-step/daily-deposit-scheme-product-details-step.component.scss +++ /dev/null @@ -1,81 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -$accent: #0ea5e9; -$border: #e2e8f0; -$gray-50: #f8f9fa; -$gray-200: #e8eaed; -$gray-900: #202124; - -.rdpd-section { - border: 1px solid $border; - border-radius: 10px; - background: #fff; - - & + & { - margin-top: 16px; - } - - &__header { - display: flex; - align-items: center; - gap: 10px; - padding: 12px 24px; - background: $gray-50; - border-bottom: 1px solid $gray-200; - - i { - font-size: 14px; - color: $accent; - } - - h3 { - margin: 0; - font-size: 14px; - font-weight: 600; - color: $gray-900; - } - } -} - -.rdpd-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 4px 16px; - padding: 16px 24px; - - @media (width <= 600px) { - grid-template-columns: 1fr; - } -} - -.rdpd-grid--classification { - grid-template-columns: repeat(3, 1fr); - - @media (width <= 900px) { - grid-template-columns: repeat(2, 1fr); - } - - @media (width <= 600px) { - grid-template-columns: 1fr; - } -} - -.rdpd-field { - width: 100%; - - &--full { - grid-column: 1 / -1; - } -} - -.rdpd-checkbox-row { - display: flex; - align-items: center; - min-height: 56px; -} diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-details-step/daily-deposit-scheme-product-details-step.component.ts b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-details-step/daily-deposit-scheme-product-details-step.component.ts deleted file mode 100644 index 65f517f15..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-details-step/daily-deposit-scheme-product-details-step.component.ts +++ /dev/null @@ -1,177 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import { Component, OnInit, OnChanges, Input, SimpleChanges, inject, ChangeDetectorRef } from '@angular/core'; -import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; -import { MatTooltip } from '@angular/material/tooltip'; -import { CdkTextareaAutosize } from '@angular/cdk/text-field'; -import { Dates } from 'app/core/utils/dates'; -import { SettingsService } from 'app/settings/settings.service'; -import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; - -@Component({ - selector: 'mifosx-daily-deposit-scheme-product-details-step', - templateUrl: './daily-deposit-scheme-product-details-step.component.html', - styleUrls: ['./daily-deposit-scheme-product-details-step.component.scss'], - imports: [ - ...STANDALONE_SHARED_IMPORTS, - MatTooltip, - CdkTextareaAutosize - ] -}) -export class DailyDepositSchemeProductDetailsStepComponent implements OnInit, OnChanges { - private formBuilder = inject(UntypedFormBuilder); - private dateUtils = inject(Dates); - private settingsService = inject(SettingsService); - private cdr = inject(ChangeDetectorRef); - - @Input() DailyDepositSchemeProductsTemplate: any; - - DailyDepositSchemeProductDetailsForm: UntypedFormGroup; - accountTypeData: string[] = []; - customerSegmentData: string[] = []; - liabilityAccountOptions: any[] = []; - /** "Auto Interest Post When" options - the legacy screen's own field, sourced from the same - * enum the (hidden) interest-posting-period setting uses, so the two always stay in sync. */ - autoInterestPostWhenData: any[] = []; - yesNoOptions: Array<{ value: boolean; label: string }> = [ - { value: true, label: 'Yes' }, - { value: false, label: 'No' } - ]; - - constructor() { - this.createDailyDepositSchemeProductDetailsForm(); - } - - ngOnInit() { - if (this.DailyDepositSchemeProductsTemplate) { - this.applyTemplate(this.DailyDepositSchemeProductsTemplate); - } - } - - ngOnChanges(changes: SimpleChanges) { - if (changes.DailyDepositSchemeProductsTemplate?.currentValue) { - this.applyTemplate(changes.DailyDepositSchemeProductsTemplate.currentValue); - } - } - - createDailyDepositSchemeProductDetailsForm() { - this.DailyDepositSchemeProductDetailsForm = this.formBuilder.group({ - accountTypeGroup: [ - '', - Validators.required - ], - name: [ - '', - Validators.required - ], - shortName: [ - '', - Validators.required - ], - description: [''], - autoInterestPost: [true], - autoInterestPostWhen: [ - '', - Validators.required - ], - autoInterestPostDay: [''], - interestRate: [ - 2, - [ - Validators.required, - Validators.min(0) - ] - ], - accountType: [ - null, - Validators.required - ], - glAccountId: [null], - effectiveTo: [''], - endDate: [''], - introducerRequired: [false], - shareLinkingEnabled: [false] - }); - } - - get minEndDate(): Date | null { - const v = this.DailyDepositSchemeProductDetailsForm.get('effectiveTo')?.value; - if (!v) return null; - const d = new Date(v); - d.setDate(d.getDate() + 1); - return d; - } - - private getOptionNames(options: any[]): string[] { - if (!Array.isArray(options)) { - return []; - } - return options - .map((option) => option?.name ?? option?.value ?? option) - .filter((value) => typeof value === 'string' && value.trim().length > 0); - } - - private parseDate(dateValue: any): Date | null { - if (!dateValue) return null; - if (Array.isArray(dateValue)) { - return new Date(dateValue[0], dateValue[1] - 1, dateValue[2]); - } - if (dateValue instanceof Date) { - return dateValue; - } - return new Date(dateValue); - } - - compareById(o1: any, o2: any): boolean { - return o1 != null && o2 != null && Number(o1) === Number(o2); - } - - private applyTemplate(template: any) { - this.accountTypeData = this.getOptionNames(template.accountTypeOptions); - this.liabilityAccountOptions = template.accountingMappingOptions?.liabilityAccountOptions ?? []; - this.autoInterestPostWhenData = template.interestPostingPeriodTypeOptions ?? []; - this.cdr.detectChanges(); - - const isEditing = !!template.id; - this.DailyDepositSchemeProductDetailsForm.patchValue({ - accountTypeGroup: template.accountTypeGroup || 'DDS', - name: isEditing ? template.name : template.name || '', - shortName: isEditing ? template.shortName : template.shortName || '', - description: template.description || '', - autoInterestPost: template.interestPostingPeriodType ? true : (template.autoInterestPost ?? true), - autoInterestPostWhen: template.interestPostingPeriodType?.id ?? this.autoInterestPostWhenData[0]?.id ?? '', - autoInterestPostDay: template.autoInterestPostDay ?? template.frequencyFixDay ?? '', - interestRate: template.nominalAnnualInterestRate ?? 2, - accountType: template.accountType ?? this.accountTypeData[0] ?? null, - glAccountId: template.glAccountId ?? null, - effectiveTo: this.parseDate(template.effectiveTo), - endDate: this.parseDate(template.endDate), - introducerRequired: template.introducerRequired === true || template.introducerRequired === 'Yes', - shareLinkingEnabled: !!template.shareLinkingEnabled - }); - } - - get DailyDepositSchemeProductDetails() { - const formValue = { ...this.DailyDepositSchemeProductDetailsForm.value }; - const dateFormat = this.settingsService.dateFormat; - - if (formValue.effectiveTo instanceof Date) { - formValue.effectiveTo = this.dateUtils.formatDate(formValue.effectiveTo, dateFormat) || ''; - } - if (formValue.endDate instanceof Date) { - formValue.endDate = this.dateUtils.formatDate(formValue.endDate, dateFormat) || ''; - } - - // accountTypeGroup / autoInterestPost / autoInterestPostWhen are the legacy screen's own - // fields - they don't map to a real product API parameter by that name, so they're folded - // into the actual payload fields (description, autoPost, interestPostingPeriodType) by the - // parent component rather than sent through as-is. - return formValue; - } -} diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-interest-rate-chart-step/daily-deposit-scheme-product-interest-rate-chart-step.component.html b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-interest-rate-chart-step/daily-deposit-scheme-product-interest-rate-chart-step.component.html deleted file mode 100644 index 7b799e488..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-interest-rate-chart-step/daily-deposit-scheme-product-interest-rate-chart-step.component.html +++ /dev/null @@ -1,109 +0,0 @@ - - - -
-
- -

Bank Interest Rate Chart  Reference / Editable

- - -
- - @if (_referenceVersionLabel) { -
- Version: {{ _referenceVersionLabel }} - @if (_referenceRemarks) { -  | {{ _referenceRemarks }} - } -
- } - - @if (_editableSlabs.length === 0) { -
-

No interest rate slabs defined. Click Add Slab to add one.

-
- } - - @if (_editableSlabs.length > 0) { -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Sr.{{ slab.srNo }}Particular - - From - - To - - Period{{ slab.periodTypeLabel }}Rate (%) - -
-
- - - } -
diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-interest-rate-chart-step/daily-deposit-scheme-product-interest-rate-chart-step.component.scss b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-interest-rate-chart-step/daily-deposit-scheme-product-interest-rate-chart-step.component.scss deleted file mode 100644 index 21f95779c..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-interest-rate-chart-step/daily-deposit-scheme-product-interest-rate-chart-step.component.scss +++ /dev/null @@ -1,215 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -.rdpi-section { - border: 1px solid #e2e8f0; - border-radius: 10px; - overflow: hidden; - background: #fff; - - & + & { - margin-top: 16px; - } - - &__header { - display: flex; - align-items: center; - gap: 10px; - padding: 12px 24px; - background: #f8f9fa; - border-bottom: 1px solid #e8eaed; - - i { - font-size: 14px; - color: #0ea5e9; - } - - h3 { - margin: 0; - font-size: 14px; - font-weight: 600; - color: #202124; - } - } -} - -.rdpi-spacer { - flex: 1; -} - -.rdpi-content { - padding: 16px 24px; -} - -.rdpi-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 4px 16px; -} - -.rdpi-field { - width: 100%; - - &--full { - grid-column: 1 / -1; - } -} - -.rdpi-checkbox-row { - display: flex; - align-items: center; - padding: 4px 0; -} - -.rdpi-add-btn { - height: 36px; - border-radius: 8px; - font-size: 13px; - font-weight: 600; -} - -.rdpi-chart-block { - border: 1px solid #e2e8f0; - border-radius: 8px; - padding: 16px; - margin-bottom: 16px; - background: #fafbfc; -} - -.rdpi-chart-actions { - display: flex; - justify-content: flex-end; -} - -.rdpi-slab-actions { - display: flex; - justify-content: center; - padding: 8px 0 16px; -} - -.rdpi-empty-msg { - text-align: center; - padding: 16px; - - h3 { - margin: 0; - font-size: 14px; - font-weight: 500; - color: #5f6368; - } -} - -.rdpi-table-wrapper { - border: 1px solid #e2e8f0; - border-radius: 8px; - overflow: hidden; - margin-top: 8px; - - table { - width: 100%; - } - - tr.incentives-row { - height: 0; - - > td { - padding: 0; - } - } -} - -.rdpi-incentives { - overflow: hidden; -} - -.rdpi-incentive-card { - border-radius: 0; - padding: 16px; -} - -.rdpi-incentive-title { - margin: 0 0 8px; - font-size: 14px; - font-weight: 600; - color: #202124; -} - -.rdpi-incentive-add { - margin-bottom: 12px; -} - -@media (width <= 600px) { - .rdpi-grid { - grid-template-columns: 1fr; - } -} - -/* ── Editable reference-rate table ───────────────────────────── */ -.rdpi-ref-section { - margin-top: 24px; - border-color: #bfdbfe; - background: #f0f7ff; - - .rdpi-section__header { - background: #dbeafe; - color: #1e3a5f; - } -} - -.rdpi-ref-badge { - font-size: 11px; - font-weight: 500; - background: #3b82f6; - color: #fff; - border-radius: 10px; - padding: 2px 8px; - vertical-align: middle; -} - -.rdpi-ref-meta { - padding: 8px 16px; - font-size: 13px; - color: #555; - border-bottom: 1px solid #bfdbfe; -} - -.rdpi-ref-table { - width: 100%; -} - -.rdpi-inline-input { - border: 1px solid #cbd5e1; - border-radius: 4px; - padding: 3px 6px; - font-size: 13px; - width: 100%; - background: #fff; - - &:focus { - outline: none; - border-color: #3b82f6; - } - - &--num { - width: 70px; - } - - &--rate { - width: 80px; - } -} - -.rdpi-ref-footer { - padding: 10px 16px; -} - -.rdpi-ref-hint { - font-size: 12px; - color: #64748b; - margin: 0; -} diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-interest-rate-chart-step/daily-deposit-scheme-product-interest-rate-chart-step.component.ts b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-interest-rate-chart-step/daily-deposit-scheme-product-interest-rate-chart-step.component.ts deleted file mode 100644 index f8ac4d193..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-interest-rate-chart-step/daily-deposit-scheme-product-interest-rate-chart-step.component.ts +++ /dev/null @@ -1,717 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -/** Angular Imports */ -import { Component, OnInit, OnChanges, Input, inject } from '@angular/core'; -import { - UntypedFormGroup, - UntypedFormBuilder, - Validators, - UntypedFormArray, - ReactiveFormsModule -} from '@angular/forms'; -import { MatDialog } from '@angular/material/dialog'; -import { animate, state, style, transition, trigger } from '@angular/animations'; - -/** Custom Components */ -import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component'; -import { DepositProductIncentiveFormDialogComponent } from 'app/products/deposit-product-incentive-form-dialog/deposit-product-incentive-form-dialog.component'; - -/** Dialog Components */ -import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base'; -import { SelectBase } from 'app/shared/form-dialog/formfield/model/select-base'; -import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base'; - -import { SettingsService } from 'app/settings/settings.service'; -import { Dates } from 'app/core/utils/dates'; -import { TranslateService } from '@ngx-translate/core'; -import { MatButton } from '@angular/material/button'; -import { FaIconComponent } from '@fortawesome/angular-fontawesome'; - -import { MatDivider } from '@angular/material/divider'; -import { MatCheckbox } from '@angular/material/checkbox'; -import { - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow -} from '@angular/material/table'; -import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper'; -import { FindPipe } from '../../../../pipes/find.pipe'; -import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; - -@Component({ - selector: 'mifosx-daily-deposit-scheme-product-interest-rate-chart-step', - templateUrl: './daily-deposit-scheme-product-interest-rate-chart-step.component.html', - styleUrls: ['./daily-deposit-scheme-product-interest-rate-chart-step.component.scss'], - animations: [ - trigger('expandChartSlab', [ - state('collapsed', style({ height: '0px', minHeight: '0' })), - state('expanded', style({ height: '*' })), - transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')) - ]) - ], - imports: [ - ...STANDALONE_SHARED_IMPORTS, - FaIconComponent, - MatDivider, - MatButton, - MatCheckbox, - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow, - FindPipe - ] -}) -export class DailyDepositSchemeProductInterestRateChartStepComponent implements OnInit { - private formBuilder = inject(UntypedFormBuilder); - dialog = inject(MatDialog); - private dateUtils = inject(Dates); - private settingsService = inject(SettingsService); - private translateService = inject(TranslateService); - - @Input() DailyDepositSchemeProductsTemplate: any; - /** Reference rate chart fetched from m_rd_interest_rate_effective / m_rd_interest_rate_slab_history */ - @Input() set rdInterestRates(rates: any[]) { - if (rates && rates.length > 0) { - // Use the first active version's slabs as the editable working copy - const activeVersion = rates.find((v: any) => v.isActive) ?? rates[0]; - this._referenceVersionLabel = activeVersion.versionLabel; - this._referenceRemarks = activeVersion.remarks; - // Deep-clone slabs so the original is not mutated - this._editableSlabs = (activeVersion.slabs ?? []).map((s: any) => ({ ...s })); - this.ensureSlabRangeRules(this._editableSlabs); - } - } - - /** Slabs the user is editing in the reference-rate table */ - _editableSlabs: any[] = []; - _referenceVersionLabel = ''; - _referenceRemarks = ''; - - /** Columns for the editable reference-rate table */ - readonly refSlabColumns = [ - 'srNo', - 'description', - 'fromPeriod', - 'toPeriod', - 'periodTypeLabel', - 'annualInterestRate' - ]; - - /** Returns the payload to save as a new rate version (called by parent after product save) */ - get modifiedRdInterestRatePayload(): any { - return { - versionLabel: this._referenceVersionLabel || 'Rate Version ' + new Date().toISOString().slice(0, 10), - remarks: this._referenceRemarks, - slabs: this._editableSlabs.map((s: any, idx: number) => ({ - srNo: s.srNo ?? idx + 1, - description: s.description, - periodTypeEnum: s.periodTypeEnum ?? 1, - fromPeriod: s.fromPeriod, - toPeriod: s.toPeriod ?? null, - annualInterestRate: s.annualInterestRate - })) - }; - } - - editSlab(index: number, field: string, value: any) { - const slabs = this._editableSlabs.map((slab: any) => ({ ...slab })); - const current = slabs[index]; - if (!current) { - return; - } - - const nextValue = this.normalizeSlabValue(value); - const oldFrom = this.normalizeSlabValue(current.fromPeriod); - const oldTo = this.normalizeSlabValue(current.toPeriod); - - current[field] = nextValue; - - // Keep the first slab anchored at 0 so the chart always starts from zero. - if (index === 0 && field === 'fromPeriod') { - current.fromPeriod = 0; - } - - if (field === 'fromPeriod' && nextValue === null) { - current.fromPeriod = oldFrom; - } - - if (field === 'fromPeriod') { - const effectiveFrom = this.normalizeSlabValue(current.fromPeriod); - const delta = effectiveFrom - oldFrom; - - if (current.toPeriod !== null && current.toPeriod !== undefined) { - current.toPeriod = this.normalizeSlabValue(current.toPeriod) + delta; - } - - this.shiftFollowingSlabs(slabs, index, delta); - } - - if (field === 'toPeriod') { - if (index < slabs.length - 1 && nextValue === null) { - // Non-final slabs should always have a closing boundary so later slabs can stay contiguous. - current.toPeriod = oldTo; - } else { - const delta = (this.normalizeSlabValue(current.toPeriod) ?? 0) - (oldTo ?? 0); - this.shiftFollowingSlabs(slabs, index, delta); - } - } - - this.ensureSlabRangeRules(slabs); - this._editableSlabs = slabs; - } - - addSlab() { - const slabs = this._editableSlabs.map((slab: any) => ({ ...slab })); - const nextSr = slabs.length + 1; - const lastSlab = slabs[slabs.length - 1]; - const lastFrom = this.normalizeSlabValue(lastSlab?.fromPeriod) ?? 0; - - if (lastSlab && this.normalizeSlabValue(lastSlab.toPeriod) === null) { - // Split the open-ended tail so the new slab can become the new last row. - // This keeps the sequence contiguous while preserving the existing tail start. - lastSlab.toPeriod = lastFrom; - } - - const previousSlab = slabs[slabs.length - 1]; - this._editableSlabs = [ - ...slabs, - { - srNo: nextSr, - description: '', - periodTypeEnum: 1, - periodTypeLabel: 'Days', - fromPeriod: slabs.length === 0 ? 0 : this.nextFromPeriod(previousSlab), - toPeriod: null, - annualInterestRate: null - } - ]; - this.ensureSlabRangeRules(this._editableSlabs); - } - - private normalizeSlabValue(value: any): number | null { - if (value === null || value === undefined || value === '') { - return null; - } - - const parsed = Number(value); - return Number.isNaN(parsed) ? null : parsed; - } - - private nextFromPeriod(previousSlab: any): number { - const previousTo = this.normalizeSlabValue(previousSlab?.toPeriod); - const previousFrom = this.normalizeSlabValue(previousSlab?.fromPeriod) ?? 0; - return previousTo === null ? previousFrom + 1 : previousTo + 1; - } - - private shiftFollowingSlabs(slabs: any[], index: number, delta: number): void { - if (!delta) { - return; - } - - for (let i = index + 1; i < slabs.length; i++) { - const slab = slabs[i]; - const currentFrom = this.normalizeSlabValue(slab.fromPeriod); - const currentTo = this.normalizeSlabValue(slab.toPeriod); - - if (currentFrom !== null) { - slab.fromPeriod = currentFrom + delta; - } - - if (currentTo !== null) { - slab.toPeriod = currentTo + delta; - } - } - } - - private ensureSlabRangeRules(slabs: any[]): void { - if (slabs.length === 0) { - return; - } - - slabs[0].fromPeriod = 0; - - for (let i = 1; i < slabs.length; i++) { - const previous = slabs[i - 1]; - const previousTo = this.normalizeSlabValue(previous.toPeriod); - if (previousTo === null) { - slabs[i].fromPeriod = this.normalizeSlabValue(slabs[i].fromPeriod); - continue; - } - - slabs[i].fromPeriod = previousTo + 1; - } - - // Keep the last slab open-ended so the chart ends with null. - slabs[slabs.length - 1].toPeriod = null; - - // Prevent inverted ranges on intermediate rows. - for (let i = 0; i < slabs.length - 1; i++) { - const fromPeriod = this.normalizeSlabValue(slabs[i].fromPeriod); - const toPeriod = this.normalizeSlabValue(slabs[i].toPeriod); - if (fromPeriod !== null && toPeriod !== null && toPeriod < fromPeriod) { - slabs[i].toPeriod = fromPeriod; - } - } - } - - DailyDepositSchemeProductInterestRateChartForm: UntypedFormGroup; - - periodTypeData: any; - entityTypeData: any; - attributeNameData: any; - conditionTypeData: any; - genderData: any; - clientTypeData: any; - clientClassificationData: any; - incentiveTypeData: any; - - chartSlabsDisplayedColumns: any[] = []; - chartSlabsIncentivesDisplayedColumns: string[] = ['incentives']; - incentivesDisplayedColumns: string[] = [ - 'entityType', - 'attributeName', - 'conditionType', - 'attributeValue', - 'incentiveType', - 'amount', - 'actions' - ]; - - minDate = new Date(2000, 0, 1); - maxDate = new Date(new Date().setFullYear(new Date().getFullYear() + 10)); - - expandChartSlabIndex: number[] = []; - chartDetailData: any = []; - chartsDetail: any[] = []; - - /** - * @param {FormBuilder} formBuilder Form Builder. - * @param {MatDialog} dialog Dialog reference. - * @param {Dates} dateUtils Date Utils to format date. - * @param {SettingsService} settingsService Settings Service. - */ - - constructor() { - this.createDailyDepositSchemeProductInterestRateChartForm(); - } - - ngOnInit() { - this.periodTypeData = this.DailyDepositSchemeProductsTemplate.chartTemplate.periodTypes; - this.entityTypeData = this.DailyDepositSchemeProductsTemplate.chartTemplate.entityTypeOptions; - this.attributeNameData = this.DailyDepositSchemeProductsTemplate.chartTemplate.attributeNameOptions; - this.conditionTypeData = this.DailyDepositSchemeProductsTemplate.chartTemplate.conditionTypeOptions; - this.genderData = this.DailyDepositSchemeProductsTemplate.chartTemplate.genderOptions; - this.clientTypeData = this.DailyDepositSchemeProductsTemplate.chartTemplate.clientTypeOptions; - this.clientClassificationData = this.DailyDepositSchemeProductsTemplate.chartTemplate.clientClassificationOptions; - this.incentiveTypeData = this.DailyDepositSchemeProductsTemplate.chartTemplate.incentiveTypeOptions; - - // assignFormData() intentionally not called — old chart form is removed from UI. - // The DailyDepositSchemeProductInterestRateChartForm is kept only for stepControl - // validity; it remains empty (valid) so the preview tab becomes available. - } - - // Assign form the values for edit Daily Deposit Scheme - assignFormData() { - this.addChart(); - const isChartArray = Array.isArray(this.DailyDepositSchemeProductsTemplate.activeChart); - if (this.DailyDepositSchemeProductsTemplate.activeChart) { - if (!isChartArray) { - this.chartDetailData.push(this.DailyDepositSchemeProductsTemplate.activeChart); - } else { - this.chartDetailData = this.DailyDepositSchemeProductsTemplate.activeChart; - } - } - - // Build the array of Objects from the retrived value - this.getChartsDetailsData(); - - // Iterates for every chart in charts - this.charts.controls.forEach((chartDetailControl: UntypedFormGroup, i: number) => { - if (!this.chartsDetail[i]) { - return; - } - - // Iterate for every chartSlab in chart - this.chartsDetail[i].chartSlabs.forEach((chartSlabDetail: any, j: number) => { - const chartSlabInfo = this.formBuilder.group({ - id: [chartSlabDetail.id], - amountRangeFrom: [chartSlabDetail.amountRangeFrom], - amountRangeTo: [chartSlabDetail.amountRangeTo], - annualInterestRate: [ - chartSlabDetail.annualInterestRate, - Validators.required - ], - description: [ - chartSlabDetail.description, - Validators.required - ], - fromPeriod: [ - chartSlabDetail.fromPeriod, - Validators.required - ], - toPeriod: [chartSlabDetail.toPeriod], - periodType: [ - chartSlabDetail.periodType, - Validators.required - ], - incentives: this.formBuilder.array([]) - }); - const formArray = chartDetailControl.controls['chartSlabs'] as UntypedFormArray; - formArray.push(chartSlabInfo); - - // Iterate for every slab in chartSlab - const chartIncentiveControl = (chartDetailControl.controls['chartSlabs'] as UntypedFormArray).controls[j]; - - // Iterate to input all the incentive for particular chart slab - this.chartsDetail[i].chartSlabs[j].incentives.forEach((chartIncentiveDetail: any) => { - const incentiveInfo = this.formBuilder.group({ - amount: [ - chartIncentiveDetail.amount, - Validators.required - ], - attributeName: [ - chartIncentiveDetail.attributeName, - Validators.required - ], - attribureValue: [ - chartIncentiveDetail.attribureValue, - Validators.required - ], - conditionType: [ - chartIncentiveDetail.conditionType, - Validators.required - ], - entityType: [ - chartIncentiveDetail.entityType, - Validators.required - ], - incentiveType: [ - chartIncentiveDetail.incentiveType, - Validators.required - ] - }); - const newFormArray = (chartIncentiveControl as UntypedFormGroup).controls['incentives'] as UntypedFormArray; - newFormArray.push(incentiveInfo); - }); - }); - }); - } - - getChartsDetailsData() { - this.chartDetailData.forEach((chartData: any) => { - const chart: { - id?: any; - endDate: string | Date; - fromDate: string | Date; - isPrimaryGroupingByAmount: any; - name: any; - chartSlabs: any[]; - } = { - endDate: chartData.endDate ? new Date(chartData.endDate) : '', - fromDate: chartData.fromDate ? new Date(chartData.fromDate) : '', - isPrimaryGroupingByAmount: chartData.isPrimaryGroupingByAmount, - name: chartData.name, - chartSlabs: this.getChartSlabsData(chartData) - }; - if (chartData.id) { - chart['id'] = chartData.id; - } - this.chartsDetail.push(chart); - }); - this.DailyDepositSchemeProductInterestRateChartForm.patchValue({ - charts: this.chartsDetail - }); - } - - getChartSlabsData(chartData: any) { - const chartSlabs: any[] = []; - let chartSlabData: any[] = []; - const isChartSlabArray = Array.isArray(chartData.chartSlabs); - if (!isChartSlabArray) { - chartSlabData.push(chartData.chartSlabs); - } else { - chartSlabData = chartData.chartSlabs; - } - - chartSlabData.forEach((eachChartSlabData: any) => { - const chartSlab: { - id?: any; - periodType: any; - amountRangeFrom: any; - amountRangeTo: any; - annualInterestRate: any; - description: any; - fromPeriod: any; - toPeriod: any; - incentives: any[]; - } = { - periodType: eachChartSlabData.periodType.id, - amountRangeFrom: eachChartSlabData.amountRangeFrom, - amountRangeTo: eachChartSlabData.amountRangeTo, - annualInterestRate: eachChartSlabData.annualInterestRate, - description: eachChartSlabData.description ? eachChartSlabData.description : '', - fromPeriod: eachChartSlabData.fromPeriod, - toPeriod: eachChartSlabData.toPeriod, - incentives: this.getIncentivesData(chartSlabData) - }; - if (eachChartSlabData.id) { - chartSlab['id'] = eachChartSlabData.id; - } - chartSlabs.push(chartSlab); - }); - return chartSlabs; - } - - getIncentivesData(chartSlabData: any) { - const incentives: any[] = []; - let incentiveDatas: any[] = []; - if (chartSlabData.incentives) { - const isChartIncentiveArray = Array.isArray(chartSlabData.incentives); - if (!isChartIncentiveArray) { - incentiveDatas.push(chartSlabData.incentives); - } else { - incentiveDatas = chartSlabData.incentives; - } - incentiveDatas.forEach((incentiveData: any) => { - const incentive = { - amount: incentiveData.amount, - attributeName: incentiveData.attributeName, - attributeValue: incentiveData.attributeValue, - conditionType: incentiveData.conditionType, - entityType: incentiveData.entityType, - incentiveType: incentiveData.incentiveType - }; - incentives.push(incentive); - }); - } - return incentives; - } - - createDailyDepositSchemeProductInterestRateChartForm() { - this.DailyDepositSchemeProductInterestRateChartForm = this.formBuilder.group({ - charts: this.formBuilder.array([]) - }); - } - - get charts(): UntypedFormArray { - return this.DailyDepositSchemeProductInterestRateChartForm.get('charts') as UntypedFormArray; - } - - createChartForm(): UntypedFormGroup { - return this.formBuilder.group({ - id: [null], - name: [''], - description: [''], - fromDate: [ - '', - Validators.required - ], - endDate: [''], - isPrimaryGroupingByAmount: [false], - chartSlabs: this.formBuilder.array([], Validators.required) - }); - } - - addChart() { - this.charts.push(this.createChartForm()); - this.setConditionalControls(this.charts.length - 1); - } - - setConditionalControls(chartIndex: number) { - this.chartSlabsDisplayedColumns[chartIndex] = [ - 'period', - 'amountRange', - 'annualInterestRate', - 'description', - 'actions' - ]; - this.charts - .at(chartIndex) - .get('isPrimaryGroupingByAmount') - .valueChanges.subscribe((isPrimaryGroupingByAmount: boolean) => { - this.chartSlabsDisplayedColumns[chartIndex] = isPrimaryGroupingByAmount ? [ - 'amountRange', - 'period' - ] : [ - 'period', - 'amountRange' - ]; - this.chartSlabsDisplayedColumns[chartIndex].push('annualInterestRate', 'description', 'actions'); - }); - } - - getIncentives(chartSlabs: UntypedFormArray, chartSlabIndex: number): UntypedFormArray { - return chartSlabs.at(chartSlabIndex).get('incentives') as UntypedFormArray; - } - - addChartSlab(chartSlabs: UntypedFormArray) { - const data = { ...this.getData('Slab') }; - const dialogRef = this.dialog.open(FormDialogComponent, { data }); - dialogRef.afterClosed().subscribe((response: any) => { - if (response.data) { - response.data.addControl('incentives', this.formBuilder.array([])); - chartSlabs.push(response.data); - } - }); - } - - addIncentive(incentives: UntypedFormArray) { - const data = { ...this.getData('Incentive'), entityType: this.entityTypeData[0].id }; - const dialogRef = this.dialog.open(DepositProductIncentiveFormDialogComponent, { data }); - dialogRef.afterClosed().subscribe((response: any) => { - if (response.data) { - incentives.push(response.data); - } - }); - } - - editChartSlab(chartSlabs: UntypedFormArray, chartSlabIndex: number) { - const data = { ...this.getData('Slab', chartSlabs.at(chartSlabIndex).value), layout: { addButtonText: 'Edit' } }; - const dialogRef = this.dialog.open(FormDialogComponent, { data }); - dialogRef.afterClosed().subscribe((response: any) => { - if (response.data) { - chartSlabs.at(chartSlabIndex).patchValue(response.data.value); - } - }); - } - - editIncentive(incentives: UntypedFormArray, incentiveIndex: number) { - const data = { - ...this.getData('Incentive', incentives.at(incentiveIndex).value), - layout: { addButtonText: 'Edit' } - }; - const dialogRef = this.dialog.open(DepositProductIncentiveFormDialogComponent, { data }); - dialogRef.afterClosed().subscribe((response: any) => { - if (response.data) { - incentives.at(incentiveIndex).patchValue(response.data.value); - } - }); - } - - getData(formType: string, values?: any) { - switch (formType) { - case 'Slab': - return { - title: this.translateService.instant('labels.inputs.Slab'), - formfields: this.getSlabFormfields(values) - }; - case 'Incentive': - return { values, chartTemplate: this.DailyDepositSchemeProductsTemplate.chartTemplate }; - } - } - - getSlabFormfields(values?: any) { - const formfields: FormfieldBase[] = [ - new SelectBase({ - controlName: 'periodType', - label: this.translateService.instant('labels.inputs.Period Type'), - value: values ? values.periodType : this.periodTypeData[0].id, - options: { label: 'value', value: 'id', data: this.periodTypeData }, - required: true, - order: 1 - }), - new InputBase({ - controlName: 'fromPeriod', - label: this.translateService.instant('labels.inputs.Period From'), - value: values ? values.fromPeriod : undefined, - type: 'number', - required: true, - order: 2, - min: 0 - }), - new InputBase({ - controlName: 'toPeriod', - label: this.translateService.instant('labels.inputs.Period To'), - value: values ? values.toPeriod : undefined, - type: 'number', - order: 3, - min: 0 - }), - new InputBase({ - controlName: 'amountRangeFrom', - label: this.translateService.instant('labels.inputs.Amount Range From'), - value: values ? values.amountRangeFrom : undefined, - type: 'number', - order: 4, - min: 0 - }), - new InputBase({ - controlName: 'amountRangeTo', - label: this.translateService.instant('labels.inputs.Amount Range To'), - value: values ? values.amountRangeTo : undefined, - type: 'number', - order: 5, - min: 0 - }), - new InputBase({ - controlName: 'annualInterestRate', - label: this.translateService.instant('labels.inputs.Interest'), - value: values ? values.annualInterestRate : undefined, - type: 'number', - required: true, - order: 6, - min: 0 - }), - new InputBase({ - controlName: 'description', - label: this.translateService.instant('labels.inputs.Description'), - value: values ? values.description : undefined, - required: true, - order: 7 - }) - ]; - return formfields; - } - - get DailyDepositSchemeProductInterestRateChart() { - // TODO: Update once language and date settings are setup - const dateFormat = 'YYYY-MM-DD'; - const locale = this.settingsService.language.code; - const DailyDepositSchemeProductInterestRateChart = this.DailyDepositSchemeProductInterestRateChartForm.value; - for (const chart of DailyDepositSchemeProductInterestRateChart.charts) { - chart.locale = locale; - chart.dateFormat = 'yyyy-MM-dd'; - if (chart.fromDate instanceof Date) { - chart.fromDate = this.dateUtils.formatDateAsString(chart.fromDate, dateFormat); - } - if (chart.endDate) { - if (chart.endDate instanceof Date) { - chart.endDate = this.dateUtils.formatDateAsString(chart.endDate, dateFormat); - } - } - if (chart.endDate === '') { - delete chart.endDate; - } - if (chart.description === '') { - delete chart.description; - } - if (chart.id === null) { - delete chart.id; - } - } - return { - ...DailyDepositSchemeProductInterestRateChart, - rdSlabs: this._editableSlabs - }; - } -} diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-preview-step/daily-deposit-scheme-product-preview-step.component.html b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-preview-step/daily-deposit-scheme-product-preview-step.component.html deleted file mode 100644 index 2227219c8..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-preview-step/daily-deposit-scheme-product-preview-step.component.html +++ /dev/null @@ -1,732 +0,0 @@ - - -
- -

{{ DailyDepositSchemeProduct.name }}

- - - -

{{ 'labels.inputs.Details' | translate }}

- - -
- {{ 'labels.inputs.Short Name' | translate }}: - {{ DailyDepositSchemeProduct.shortName }} -
- - @if (DailyDepositSchemeProduct.description) { -
- {{ 'labels.inputs.Description' | translate }}: - {{ DailyDepositSchemeProduct.description }} -
- } - - - @if ( - DailyDepositSchemeProduct.accountType || - DailyDepositSchemeProduct.customerSegment || - DailyDepositSchemeProduct.glAccountId || - DailyDepositSchemeProduct.effectiveTo || - DailyDepositSchemeProduct.introducerRequired !== undefined || - DailyDepositSchemeProduct.shareLinkingEnabled !== undefined - ) { -

Product Classification

- - - @if (DailyDepositSchemeProduct.accountType) { -
- Account Type: - {{ DailyDepositSchemeProduct.accountType }} -
- } - - @if (DailyDepositSchemeProduct.glAccountId) { -
- GL Account (Liability): - {{ getGlAccountName(DailyDepositSchemeProduct.glAccountId) }} -
- } - - @if (DailyDepositSchemeProduct.effectiveTo) { -
- Effective To: - {{ DailyDepositSchemeProduct.effectiveTo | dateFormat }} -
- } - - @if (DailyDepositSchemeProduct.endDate) { -
- End Date: - {{ DailyDepositSchemeProduct.endDate | dateFormat }} -
- } - - @if (DailyDepositSchemeProduct.introducerRequired !== undefined) { -
- Introducer Required: - {{ - DailyDepositSchemeProduct.introducerRequired === true || - DailyDepositSchemeProduct.introducerRequired === 'Yes' - ? 'Yes' - : 'No' - }} -
- } - - @if (DailyDepositSchemeProduct.shareLinkingEnabled !== undefined) { -
- Deposit Account Link: - {{ DailyDepositSchemeProduct.shareLinkingEnabled | yesNo }} -
- } - } - - -

{{ 'labels.inputs.Currency' | translate }}

- - -
- {{ 'labels.inputs.Currency' | translate }}: - {{ - DailyDepositSchemeProduct.currencyCode - | find: DailyDepositSchemeProductsTemplate.currencyOptions : 'code' : 'name' - }} -
- -
- {{ 'labels.inputs.Decimal Places' | translate }}: - {{ DailyDepositSchemeProduct.digitsAfterDecimal }} -
- - @if (DailyDepositSchemeProduct.dormancyAlert !== undefined) { -
- Dormancy Alert: - {{ DailyDepositSchemeProduct.dormancyAlert | yesNo }} -
- } - - @if (DailyDepositSchemeProduct.nominationFacility !== undefined) { -
- Nomination Facility: - {{ DailyDepositSchemeProduct.nominationFacility | yesNo }} -
- } - - @if (DailyDepositSchemeProduct.autoDebitFacility !== undefined) { -
- Auto Debit Facility: - {{ DailyDepositSchemeProduct.autoDebitFacility | yesNo }} -
- } - - @if ( - DailyDepositSchemeProduct.minDepositAmount !== undefined || DailyDepositSchemeProduct.maxDepositAmount !== undefined - ) { - -

Deposit Amount

- - - @if (DailyDepositSchemeProduct.minDepositAmount !== undefined) { -
- Minimum: - {{ DailyDepositSchemeProduct.minDepositAmount }} -
- } - - @if (DailyDepositSchemeProduct.maxDepositAmount !== undefined) { -
- Maximum: - {{ DailyDepositSchemeProduct.maxDepositAmount }} -
- } - } - - -

{{ 'labels.inputs.Terms' | translate }}

- - -
- {{ 'labels.inputs.Nominal Annual Interest Rate' | translate }} (%): - {{ DailyDepositSchemeProduct.nominalAnnualInterestRate | formatNumber }} % -
- -
- {{ 'labels.inputs.Interest Compounding Period' | translate }}: - {{ - DailyDepositSchemeProduct.interestCompoundingPeriodType - | find: DailyDepositSchemeProductsTemplate.interestCompoundingPeriodTypeOptions : 'id' : 'value' - }} -
- -
- {{ 'labels.inputs.Interest Posting Period' | translate }}: - {{ - DailyDepositSchemeProduct.interestPostingPeriodType - | find: DailyDepositSchemeProductsTemplate.interestPostingPeriodTypeOptions : 'id' : 'value' - }} -
- - @if (DailyDepositSchemeProduct.interestCalculateSelector) { -
- {{ 'labels.inputs.Interest Calculation Type' | translate }}: - {{ - DailyDepositSchemeProduct.interestCalculateSelector - | find: DailyDepositSchemeProductsTemplate.interestCalculationTypeOption : 'id' : 'value' - }} -
- } - -
- {{ 'labels.inputs.Interest Calculated using' | translate }}: - {{ - DailyDepositSchemeProduct.interestCalculationType - | find: DailyDepositSchemeProductsTemplate.interestCalculationTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
- -
- {{ 'labels.inputs.Days in Year' | translate }}: - {{ - DailyDepositSchemeProduct.interestCalculationDaysInYearType - | find: DailyDepositSchemeProductsTemplate.interestCalculationDaysInYearTypeOptions : 'id' : 'value' - }} -
- - @if (DailyDepositSchemeProduct.effectiveDate) { -
- Effective Date: - {{ DailyDepositSchemeProduct.effectiveDate | dateFormat }} -
- } - - @if (DailyDepositSchemeProduct.averagePeriod) { -
- Average Period: - {{ DailyDepositSchemeProduct.averagePeriod }} -
- } - -
- Auto Interest Post: - {{ DailyDepositSchemeProduct.autoPost | yesNo }} -
- - @if ( - DailyDepositSchemeProduct.frequencyFixDay !== undefined && - DailyDepositSchemeProduct.frequencyFixDay !== null && - DailyDepositSchemeProduct.frequencyFixDay !== '' - ) { -
- Frequency (Fix Day): - {{ DailyDepositSchemeProduct.frequencyFixDay }} -
- } - - @if (DailyDepositSchemeProduct.lastInterestDate) { -
- Last Interest Date: - {{ DailyDepositSchemeProduct.lastInterestDate | dateFormat }} -
- } - - -

{{ 'labels.heading.Settings' | translate }}

- - -
- Loan Facility: - {{ DailyDepositSchemeProduct.loanFacility | yesNo }} -
- -
- Premature Withdrawal: - {{ DailyDepositSchemeProduct.prematureWithdrawalOption | yesNo }} -
- -
- Tenure Flexibility: - {{ DailyDepositSchemeProduct.tenureFlexibility | yesNo }} -
- -
- Lien Marked: - {{ DailyDepositSchemeProduct.lienMarked | yesNo }} -
- -
- {{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}: - {{ DailyDepositSchemeProduct.withdrawalFeeForTransfers | yesNo }} -
- -
- Enable Lock-in Period: - {{ DailyDepositSchemeProduct.enableLockinPeriod | yesNo }} -
- - @if (DailyDepositSchemeProduct.enableLockinPeriod && DailyDepositSchemeProduct.lockinPeriodFrequency) { -
- Lock-in Period: - - {{ DailyDepositSchemeProduct.lockinPeriodFrequency }} - {{ getLockinPeriodFrequencyTypeLabel(DailyDepositSchemeProduct.lockinPeriodFrequencyType) }} - -
- } - -
- Maturity Instructions: - {{ DailyDepositSchemeProduct.maturityInstructions | yesNo }} -
- -
- Credit Maturity Proceeds: - {{ DailyDepositSchemeProduct.creditMaturityProceeds | yesNo }} -
- -
- Penalty Grace Period (Missed Installments): - {{ DailyDepositSchemeProduct.penaltyGracePeriodMissedInstallments | yesNo }} -
- -
- Panel Request Applicable: - {{ DailyDepositSchemeProduct.panelRequestApplicable | yesNo }} -
- -
- No Penalty (Nominee / Heir Closure): - {{ DailyDepositSchemeProduct.noPenaltyNomineeHeirClosure | yesNo }} -
- -
- Deceased Settlement: - {{ DailyDepositSchemeProduct.deceasedSettlement | yesNo }} -
- -
- Allow Debit Txn After Due Date: - {{ DailyDepositSchemeProduct.allowDebitTxnAfterDueDate | yesNo }} -
- -
- Account Closed with Zero Balance in Day Closing: - {{ DailyDepositSchemeProduct.accountClosedZeroBalanceInDayClosing | yesNo }} -
- - @if (DailyDepositSchemeProduct.minBalanceForInterestCalculation) { -
- Min Balance for Interest Calculation: - {{ DailyDepositSchemeProduct.minBalanceForInterestCalculation | formatNumber }} -
- } - - - @if (DailyDepositSchemeProduct.rdSlabs?.length) { - -

{{ 'labels.heading.Interest Rate Charts' | translate }}

- - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
Sr.{{ slab.srNo }}Particular{{ slab.description }}From{{ slab.fromPeriod }}To{{ slab.toPeriod ?? '—' }}Period{{ slab.periodTypeLabel }}Rate (%) - {{ slab.annualInterestRate }} -
-
- } - - - @if (DailyDepositSchemeProduct.charges?.length) { -
-

{{ 'labels.inputs.Charges' | translate }}

- - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Name' | translate }}{{ charge.name + ', ' + charge.currency.displaySymbol }}{{ 'labels.inputs.Type' | translate }}{{ charge.chargeCalculationType.value }}{{ 'labels.inputs.Amount' | translate }}{{ charge.amount | formatNumber }}{{ 'labels.inputs.Collected On' | translate }}{{ charge.chargeTimeType.value }}
-
- } - - -

{{ 'labels.heading.Accounting' | translate }}

- - - @if (isCashOrAccrualAccounting()) { -
-
-

GL Mapping

-
- -
-
Assets
-
- Savings Reference Account - {{ - formatGlAccountById( - DailyDepositSchemeProduct.savingsReferenceAccountId, - DailyDepositSchemeProductsTemplate?.accountingMappingOptions.assetAccountOptions - ) - }} -
- @if (isAccrualAccounting()) { -
- Fees Receivable Account - {{ - formatGlAccountById( - DailyDepositSchemeProduct.feesReceivableAccountId, - DailyDepositSchemeProductsTemplate?.accountingMappingOptions.assetAccountOptions - ) - }} -
-
- Penalties Receivable Account - {{ - formatGlAccountById( - DailyDepositSchemeProduct.penaltiesReceivableAccountId, - DailyDepositSchemeProductsTemplate?.accountingMappingOptions.assetAccountOptions - ) - }} -
- } -
- -
-
Liabilities
-
- Savings Control Account - {{ - formatGlAccountById( - DailyDepositSchemeProduct.savingsControlAccountId, - DailyDepositSchemeProductsTemplate?.accountingMappingOptions.liabilityAccountOptions - ) - }} -
-
- Transfers In Suspense Account - {{ - formatGlAccountById( - DailyDepositSchemeProduct.transfersInSuspenseAccountId, - DailyDepositSchemeProductsTemplate?.accountingMappingOptions.liabilityAccountOptions - ) - }} -
- @if (isAccrualAccounting()) { -
- Interest Payable Account - {{ - formatGlAccountById( - DailyDepositSchemeProduct.interestPayableAccountId, - DailyDepositSchemeProductsTemplate?.accountingMappingOptions.liabilityAccountOptions - ) - }} -
- } -
- -
-
Expenses
-
- Interest on Savings Account - {{ - formatGlAccountById( - DailyDepositSchemeProduct.interestOnSavingsAccountId, - DailyDepositSchemeProductsTemplate?.accountingMappingOptions.expenseAccountOptions - ) - }} -
-
- -
-
Income
-
- Income from Fees - {{ - formatGlAccountById( - DailyDepositSchemeProduct.incomeFromFeeAccountId, - DailyDepositSchemeProductsTemplate?.accountingMappingOptions.incomeAccountOptions - ) - }} -
-
- Income from Penalties - {{ - formatGlAccountById( - DailyDepositSchemeProduct.incomeFromPenaltyAccountId, - DailyDepositSchemeProductsTemplate?.accountingMappingOptions.incomeAccountOptions - ) - }} -
-
-
- } - - - @if (DailyDepositSchemeProduct.otherGlMappings) { -
-
-

Other GL Mappings

-
- - @if (DailyDepositSchemeProduct.otherGlMappings.assets?.length) { - - - - - - - - - - - -
Asset Account{{ row.accountName }}GL Account - {{ - formatGlAccountByCode( - row.glCode, - DailyDepositSchemeProductsTemplate?.accountingMappingOptions.assetAccountOptions - ) - }} -
- } - - @if (DailyDepositSchemeProduct.otherGlMappings.liabilities?.length) { - - - - - - - - - - - -
Liability Account{{ row.accountName }}GL Account - {{ - formatGlAccountByCode( - row.glCode, - DailyDepositSchemeProductsTemplate?.accountingMappingOptions.liabilityAccountOptions - ) - }} -
- } - - @if (DailyDepositSchemeProduct.otherGlMappings.expenses?.length) { - - - - - - - - - - - -
Expense Account{{ row.accountName }}GL Account - {{ - formatGlAccountByCode( - row.glCode, - DailyDepositSchemeProductsTemplate?.accountingMappingOptions.expenseAccountOptions - ) - }} -
- } - - @if (DailyDepositSchemeProduct.otherGlMappings.income?.length) { - - - - - - - - - - - -
Income Account{{ row.accountName }}GL Account - {{ - formatGlAccountByCode( - row.glCode, - DailyDepositSchemeProductsTemplate?.accountingMappingOptions.incomeAccountOptions - ) - }} -
- } -
- } - - - @if ( - DailyDepositSchemeProduct.advancedAccountingRules && - (DailyDepositSchemeProduct.paymentChannelToFundSourceMappings?.length || - DailyDepositSchemeProduct.feeToIncomeAccountMappings?.length || - DailyDepositSchemeProduct.penaltyToIncomeAccountMappings?.length) - ) { -
-

{{ 'labels.heading.Advanced Accounting Rules' | translate }}

- - - @if (DailyDepositSchemeProduct.paymentChannelToFundSourceMappings?.length) { -
-

{{ 'labels.heading.Fund Sources for Payment Channels' | translate }}

- - - - - - - - - - - -
{{ 'labels.inputs.Payment Type' | translate }} - {{ item.paymentTypeId | find: DailyDepositSchemeProductsTemplate.paymentTypeOptions : 'id' : 'name' }} - {{ 'labels.inputs.Fund Source' | translate }} - {{ - item.fundSourceAccountId - | find - : DailyDepositSchemeProductsTemplate?.accountingMappingOptions.assetAccountOptions - : 'id' - : 'name' - }} -
-
- } - - @if (DailyDepositSchemeProduct.feeToIncomeAccountMappings?.length) { -
-

{{ 'labels.heading.Fees to Specific Income Accounts' | translate }}

- - - - - - - - - - - -
{{ 'labels.inputs.Fees' | translate }} - {{ item.chargeId | find: DailyDepositSchemeProductsTemplate.chargeOptions : 'id' : 'name' }} - {{ 'labels.inputs.Income Account' | translate }} - {{ - item.incomeAccountId - | find - : DailyDepositSchemeProductsTemplate?.accountingMappingOptions.incomeAccountOptions - : 'id' - : 'name' - }} -
-
- } - - @if (DailyDepositSchemeProduct.penaltyToIncomeAccountMappings?.length) { -
-

{{ 'labels.heading.Penalties to Specific Income Accounts' | translate }}

- - - - - - - - - - - -
{{ 'labels.inputs.Penalty' | translate }} - {{ item.chargeId | find: DailyDepositSchemeProductsTemplate.penaltyOptions : 'id' : 'name' }} - {{ 'labels.inputs.Income Account' | translate }} - {{ - item.incomeAccountId - | find - : DailyDepositSchemeProductsTemplate?.accountingMappingOptions.incomeAccountOptions - : 'id' - : 'name' - }} -
-
- } -
- } -
diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-preview-step/daily-deposit-scheme-product-preview-step.component.scss b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-preview-step/daily-deposit-scheme-product-preview-step.component.scss deleted file mode 100644 index 33a0e1852..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-preview-step/daily-deposit-scheme-product-preview-step.component.scss +++ /dev/null @@ -1,131 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -@use 'assets/styles/colours' as *; - -.mat-h3 { - margin-top: 10px !important; - font-weight: 300; - color: $status-approved; -} - -.mat-elevation-z1 { - margin: 1em 0 1.5em; -} - -h2, -h3, -h4 { - margin: 0; - font-weight: 500; -} - -table { - width: 100%; -} - -span { - margin: 0.5em 0; -} - -mat-divider { - margin: 0 0 0.5em; -} - -/* ── GL mapping summary (same as savings product preview) ── */ -.spa-gl-summary, -.spa-other-gl-summary { - display: flex; - flex-direction: column; - gap: 0.9rem; - margin-top: 1.25rem; -} - -.spa-gl-summary__header { - display: flex; - align-items: center; - justify-content: space-between; - padding: 0.25rem 0 0.5rem; -} - -.spa-gl-section { - display: flex; - flex-direction: column; - gap: 0.75rem; - padding: 1rem 1.15rem; - border: 1px solid #e3e7f2; - border-radius: 12px; - background: #fff; - - h5 { - font-size: 0.9rem; - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; - color: #91a0bc; - } -} - -.spa-gl-row { - display: grid; - grid-template-columns: minmax(220px, 40%) 1fr; - gap: 1rem; - align-items: baseline; - padding: 0.2rem 0; -} - -.spa-gl-row__label { - color: #1f2f4a; - font-weight: 500; -} - -.spa-gl-row__value { - color: #101828; - font-weight: 600; - word-break: break-word; -} - -.spa-other-gl-table { - margin-top: 0.5rem; - margin-bottom: 0.75rem; - table-layout: fixed; - width: 100%; - - &:first-of-type { - margin-top: 0.25rem; - } - - th.mat-mdc-header-cell, - td.mat-mdc-cell { - padding-left: 1.25rem; - padding-right: 1.25rem; - } - - th.mat-mdc-header-cell:first-child, - td.mat-mdc-cell:first-child { - width: 38%; - } - - th.mat-mdc-header-cell:last-child, - td.mat-mdc-cell:last-child { - width: 62%; - word-break: break-word; - } -} - -.spa-other-gl-summary .mat-elevation-z1 { - border-radius: 10px; - overflow: hidden; -} - -@media (width <= 768px) { - .spa-gl-row { - grid-template-columns: 1fr; - gap: 0.35rem; - } -} diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-preview-step/daily-deposit-scheme-product-preview-step.component.ts b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-preview-step/daily-deposit-scheme-product-preview-step.component.ts deleted file mode 100644 index 062751880..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-preview-step/daily-deposit-scheme-product-preview-step.component.ts +++ /dev/null @@ -1,142 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges, inject } from '@angular/core'; -import { Accounting } from 'app/core/utils/accounting'; -import { OptionData } from 'app/shared/models/option-data.model'; -import { MatDivider } from '@angular/material/divider'; -import { - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow -} from '@angular/material/table'; -import { FaIconComponent } from '@fortawesome/angular-fontawesome'; -import { FindPipe } from '../../../../pipes/find.pipe'; -import { DateFormatPipe } from '../../../../pipes/date-format.pipe'; -import { FormatNumberPipe } from '../../../../pipes/format-number.pipe'; -import { YesnoPipe } from '../../../../pipes/yesno.pipe'; -import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; - -@Component({ - selector: 'mifosx-daily-deposit-scheme-product-preview-step', - templateUrl: './daily-deposit-scheme-product-preview-step.component.html', - styleUrls: ['./daily-deposit-scheme-product-preview-step.component.scss'], - imports: [ - ...STANDALONE_SHARED_IMPORTS, - MatDivider, - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - FaIconComponent, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow, - FindPipe, - DateFormatPipe, - FormatNumberPipe, - YesnoPipe - ] -}) -export class DailyDepositSchemeProductPreviewStepComponent implements OnInit, OnChanges { - private accounting = inject(Accounting); - - @Input() DailyDepositSchemeProductsTemplate: any; - @Input() chartSlabsDisplayedColumns: any[] = []; - @Input() accountingRuleData: any; - @Input() DailyDepositSchemeProduct: any; - @Output() submitEvent = new EventEmitter(); - - chargesDisplayedColumns: string[] = [ - 'name', - 'chargeCalculationType', - 'amount', - 'chargeTimeType' - ]; - paymentFundSourceDisplayedColumns: string[] = [ - 'paymentTypeId', - 'fundSourceAccountId' - ]; - feesPenaltyIncomeDisplayedColumns: string[] = [ - 'chargeId', - 'incomeAccountId' - ]; - - readonly rdSlabColumns = [ - 'srNo', - 'description', - 'fromPeriod', - 'toPeriod', - 'periodTypeLabel', - 'annualInterestRate' - ]; - - accountingRule: OptionData | undefined; - - ngOnInit() { - this.setCurrentValues(); - } - - ngOnChanges(_changes: SimpleChanges): void { - this.setCurrentValues(); - } - - setCurrentValues(): void { - if (this.DailyDepositSchemeProduct) { - this.accountingRule = this.accounting.getAccountingRuleFrom(this.DailyDepositSchemeProduct.accountingRule); - } - } - - isNoneAccounting(): boolean { - return this.accounting.isNoneAccountingRuleId(this.DailyDepositSchemeProduct.accountingRule); - } - - isCashOrAccrualAccounting(): boolean { - return this.accounting.isCashOrAccrualAccountingRuleId(this.DailyDepositSchemeProduct.accountingRule); - } - - isAccrualAccounting(): boolean { - return this.accounting.isAccrualAccountingRuleId(this.DailyDepositSchemeProduct.accountingRule); - } - - getLockinPeriodFrequencyTypeLabel(value: any): string { - if (!value && value !== 0) return ''; - const options = this.DailyDepositSchemeProductsTemplate?.lockinPeriodFrequencyTypeOptions || []; - const found = options.find((o: any) => o.id === value); - return found?.value || String(value); - } - - getGlAccountName(id: any): string { - if (!id) return ''; - const accounts = this.DailyDepositSchemeProductsTemplate?.accountingMappingOptions?.liabilityAccountOptions ?? []; - const found = accounts.find((a: any) => a.id === id || a.id === +id); - return found ? `${found.name} (${found.glCode})` : String(id); - } - - formatGlAccountById(glAccountId: number, options: any[]): string { - if (!glAccountId || !Array.isArray(options)) return '-'; - const account = this.accounting.glAccountLookUp(glAccountId, options as any); - return account ? `(${account.glCode}) ${account.name}` : '-'; - } - - formatGlAccountByCode(glCode: string, options: any[]): string { - if (!glCode || !Array.isArray(options)) return glCode || '-'; - const account = options.find((o: any) => o?.glCode === glCode); - return account ? `(${account.glCode}) ${account.name}` : glCode; - } -} diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-settings-step/daily-deposit-scheme-product-settings-step.component.html b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-settings-step/daily-deposit-scheme-product-settings-step.component.html deleted file mode 100644 index 804ae09e9..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-settings-step/daily-deposit-scheme-product-settings-step.component.html +++ /dev/null @@ -1,123 +0,0 @@ - - -
- -
-
- - Premature option -
-
-
- Loan Facility - Premature Withdrawal option - Tenure Flexibility - Lien Marked - Apply Withdrawal Fee for Transfers -
-
-
- - -
-
- - {{ 'labels.inputs.Lock-in Period' | translate }} -
-
-
- Enable Lock-in Period - -
- - @if (DailyDepositSchemeProductSettingsForm.value.enableLockinPeriod) { -
- - Frequency* - - - - - Type* - - @for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) { - - {{ lockinPeriodFrequencyType.value | translateKey: 'catalogs' }} - - } - - -
- } -
-
- - -
-
- - Maturity Instructions -
-
-
- Maturity Instructions - -
-
- Option to credit maturity proceeds directly to the linked account or reinvest in another product (like - FD). - -
-
- Penalty / Grace Period for Missed Installments - -
-
- Panel Request Applicable - -
-
- No penalty for nominee / legal heir closure - -
-
- Deceased Settlement - -
-
- Allow Debit Txn after due date - -
-
-
- - -
-
- - Additional Rules -
-
-
-
- - Account Closed with Zero balance in Day Closing - -
- - - Balance Required for Interest Calculation - - -
-
-
-
diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-settings-step/daily-deposit-scheme-product-settings-step.component.scss b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-settings-step/daily-deposit-scheme-product-settings-step.component.scss deleted file mode 100644 index 3aefca054..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-settings-step/daily-deposit-scheme-product-settings-step.component.scss +++ /dev/null @@ -1,164 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -$accent: #0ea5e9; -$border: #e2e8f0; -$gray-50: #f8f9fa; -$gray-200: #e8eaed; -$gray-900: #202124; - -.rdps-section { - border: 1px solid $border; - border-radius: 10px; - background: #fff; - - & + & { - margin-top: 16px; - } - - &__header { - display: flex; - align-items: center; - gap: 10px; - padding: 12px 24px; - background: $gray-50; - border-bottom: 1px solid $gray-200; - - i { - font-size: 14px; - color: $accent; - } - - span:not(.rdps-section__badge) { - margin: 0; - font-size: 14px; - font-weight: 600; - color: $gray-900; - } - } -} - -.rdps-section__badge { - display: inline-flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - border-radius: 6px; - background: #eff6ff; - font-size: 14px; -} - -.rdps-body { - background: #fff; - padding: 18px 24px 24px; -} - -.rdps-field { - width: 100%; - - &--full { - grid-column: 1 / -1; - } -} - -.rdps-premature-row { - display: flex; - align-items: center; - gap: 24px; - flex-wrap: wrap; - - mat-checkbox { - font-size: 14px; - color: $gray-900; - } -} - -.rdps-layout { - display: grid; - gap: 16px; -} - -.rdps-layout--additional { - grid-template-columns: repeat(2, minmax(0, 1fr)); - - @media (width <= 1100px) { - grid-template-columns: 1fr; - } -} - -.rdps-toggle { - display: flex; - align-items: center; - min-height: 40px; - - mat-checkbox { - font-size: 14px; - color: $gray-900; - } - - &--full { - min-height: auto; - margin-bottom: 16px; - } -} - -.rdps-body--list { - padding: 0 24px; -} - -.rdps-list-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - padding: 14px 0; - border-bottom: 1px solid $gray-200; - font-size: 14px; - color: $gray-900; - - &:last-child { - border-bottom: none; - } - - span { - max-width: 85%; - line-height: 1.4; - } -} - -.rdps-lockin-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 16px; - margin-bottom: 16px; - padding-bottom: 16px; - border-bottom: 1px solid $gray-200; - font-size: 14px; - color: $gray-900; -} - -.rdps-lockin-fields { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 16px; - - &--single { - grid-template-columns: minmax(0, 1fr); - max-width: 50%; - } - - @media (width <= 640px) { - grid-template-columns: 1fr; - - &--single { - max-width: 100%; - } - } -} diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-settings-step/daily-deposit-scheme-product-settings-step.component.ts b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-settings-step/daily-deposit-scheme-product-settings-step.component.ts deleted file mode 100644 index b7f3d26ac..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-settings-step/daily-deposit-scheme-product-settings-step.component.ts +++ /dev/null @@ -1,95 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import { Component, OnInit, Input, inject } from '@angular/core'; -import { UntypedFormGroup, UntypedFormBuilder, ReactiveFormsModule } from '@angular/forms'; -import { MatCheckbox } from '@angular/material/checkbox'; -import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; - -@Component({ - selector: 'mifosx-daily-deposit-scheme-product-settings-step', - templateUrl: './daily-deposit-scheme-product-settings-step.component.html', - styleUrls: ['./daily-deposit-scheme-product-settings-step.component.scss'], - imports: [ - ...STANDALONE_SHARED_IMPORTS, - MatCheckbox - ] -}) -export class DailyDepositSchemeProductSettingsStepComponent implements OnInit { - private formBuilder = inject(UntypedFormBuilder); - - @Input() DailyDepositSchemeProductsTemplate: any; - - DailyDepositSchemeProductSettingsForm: UntypedFormGroup; - - lockinPeriodFrequencyTypeData: any; - - constructor() { - this.createDailyDepositSchemeProductSettingsForm(); - } - - ngOnInit() { - this.lockinPeriodFrequencyTypeData = this.DailyDepositSchemeProductsTemplate.lockinPeriodFrequencyTypeOptions; - - if (this.DailyDepositSchemeProductsTemplate?.id) { - this.DailyDepositSchemeProductSettingsForm.patchValue({ - loanFacility: this.DailyDepositSchemeProductsTemplate.loanFacility ?? false, - prematureWithdrawalOption: this.DailyDepositSchemeProductsTemplate.prematureWithdrawalOption ?? false, - tenureFlexibility: this.DailyDepositSchemeProductsTemplate.tenureFlexibility ?? false, - lienMarked: this.DailyDepositSchemeProductsTemplate.lienMarked ?? false, - withdrawalFeeForTransfers: this.DailyDepositSchemeProductsTemplate.withdrawalFeeForTransfers ?? false, - enableLockinPeriod: this.DailyDepositSchemeProductsTemplate.enableLockinPeriod ?? false, - lockinPeriodFrequency: this.DailyDepositSchemeProductsTemplate.lockinPeriodFrequency ?? '', - lockinPeriodFrequencyType: this.DailyDepositSchemeProductsTemplate.lockinPeriodFrequencyType?.id ?? '', - maturityInstructions: this.DailyDepositSchemeProductsTemplate.maturityInstructions ?? false, - creditMaturityProceeds: this.DailyDepositSchemeProductsTemplate.creditMaturityProceeds ?? false, - penaltyGracePeriodMissedInstallments: - this.DailyDepositSchemeProductsTemplate.penaltyGracePeriodMissedInstallments ?? false, - panelRequestApplicable: this.DailyDepositSchemeProductsTemplate.panelRequestApplicable ?? false, - noPenaltyNomineeHeirClosure: this.DailyDepositSchemeProductsTemplate.noPenaltyNomineeHeirClosure ?? false, - deceasedSettlement: this.DailyDepositSchemeProductsTemplate.deceasedSettlement ?? false, - allowDebitTxnAfterDueDate: this.DailyDepositSchemeProductsTemplate.allowDebitTxnAfterDueDate ?? false, - accountClosedZeroBalanceInDayClosing: - this.DailyDepositSchemeProductsTemplate.accountClosedZeroBalanceInDayClosing ?? false, - minBalanceForInterestCalculation: this.DailyDepositSchemeProductsTemplate.minBalanceForInterestCalculation ?? '' - }); - } - } - - createDailyDepositSchemeProductSettingsForm() { - this.DailyDepositSchemeProductSettingsForm = this.formBuilder.group({ - loanFacility: [false], - prematureWithdrawalOption: [false], - tenureFlexibility: [false], - lienMarked: [false], - withdrawalFeeForTransfers: [false], - enableLockinPeriod: [false], - lockinPeriodFrequency: [''], - lockinPeriodFrequencyType: [''], - maturityInstructions: [false], - creditMaturityProceeds: [false], - penaltyGracePeriodMissedInstallments: [false], - panelRequestApplicable: [false], - noPenaltyNomineeHeirClosure: [false], - deceasedSettlement: [false], - allowDebitTxnAfterDueDate: [false], - accountClosedZeroBalanceInDayClosing: [false], - minBalanceForInterestCalculation: [''] - }); - } - - get DailyDepositSchemeProductSettings() { - const settings = { ...this.DailyDepositSchemeProductSettingsForm.value }; - for (const key in settings) { - if (settings[key] === '') { - delete settings[key]; - } - } - return settings; - } -} diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-terms-step/daily-deposit-scheme-product-terms-step.component.html b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-terms-step/daily-deposit-scheme-product-terms-step.component.html deleted file mode 100644 index 45c6f2100..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-terms-step/daily-deposit-scheme-product-terms-step.component.html +++ /dev/null @@ -1,192 +0,0 @@ - - -
-
-
- - Deposit Amount -
-
-
- - Minimum - - - - - Maximum - - -
-
-
-
-
- - Interest Rate & Posting Terms -
-
-
- ℹ️ - - Daily Deposit Scheme base rate: - {{ DailyDepositSchemeProductTermsForm.get('nominalAnnualInterestRate').value }}% - -
- -
- - {{ 'labels.inputs.Nominal Annual Interest' | translate }} - - @if (DailyDepositSchemeProductTermsForm.get('nominalAnnualInterestRate').hasError('required')) { - - {{ 'labels.inputs.Nominal Annual Interest' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - @if (DailyDepositSchemeProductTermsForm.get('nominalAnnualInterestRate').hasError('min')) { - {{ 'labels.inputs.Nominal Annual Interest' | translate }} must be zero or greater - } - - - - Effective Date - - - - - - - {{ 'labels.inputs.Interest Calculation Type' | translate }} - - @for (opt of interestCalculationTypeOptionData; track opt.id) { - {{ opt.value }} - } - - -
- -
- - Interest Calculate - - @for (mode of interestCalculationTypeData; track mode.id) { - {{ mode.value | translateKey: 'catalogs' }} - } - - - {{ 'labels.inputs.Interest Calculation Type' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - Interest Compounding Period - - @for (opt of interestCompoundingPeriodTypeData; track opt.id) { - {{ opt.value | translateKey: 'catalogs' }} - } - - Interest Compounding Period is required - - - - Days in Year - - @for (opt of interestCalculationDaysInYearTypeData; track opt.id) { - {{ opt.value | translateKey: 'catalogs' }} - } - - Days in Year is required - - - @if (DailyDepositSchemeProductTermsForm.value.interestCalculationType === 2) { - - Average Period - - @for (period of periodFrequencyData; track period.id) { - {{ period.value }} - } - - - } - - - {{ 'labels.inputs.Interest Posting Period' | translate }} - - @for (interestPostingPeriodType of interestPostingPeriodTypeData; track interestPostingPeriodType) { - - {{ interestPostingPeriodType.value | translateKey: 'catalogs' }} - - } - - - {{ 'labels.inputs.Interest Posting Period' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - -
- - -
-
-
diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-terms-step/daily-deposit-scheme-product-terms-step.component.scss b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-terms-step/daily-deposit-scheme-product-terms-step.component.scss deleted file mode 100644 index b2e03a37d..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-terms-step/daily-deposit-scheme-product-terms-step.component.scss +++ /dev/null @@ -1,151 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -$accent: #0ea5e9; -$border: #e2e8f0; -$gray-50: #f8f9fa; -$gray-200: #e8eaed; -$gray-900: #202124; - -.rdpt-section { - border: 1px solid $border; - border-radius: 10px; - background: #fff; - - & + & { - margin-top: 16px; - } - - &__header { - display: flex; - align-items: center; - gap: 10px; - padding: 12px 24px; - background: $gray-50; - border-bottom: 1px solid $gray-200; - - i { - font-size: 14px; - color: $accent; - } - - h3, - span:not(.rdpt-section__badge) { - margin: 0; - font-size: 14px; - font-weight: 600; - color: $gray-900; - } - } -} - -.rdpt-section__badge { - display: inline-flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - border-radius: 6px; - background: #eff6ff; - font-size: 14px; -} - -.rdpt-body { - background: #fff; -} - -.rdpt-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 4px 16px; - padding: 16px 24px; - - &--three { - grid-template-columns: repeat(3, 1fr); - } - - @media (width <= 600px) { - grid-template-columns: 1fr; - - &--three { - grid-template-columns: 1fr; - } - } -} - -.rdpt-field { - width: 100%; - - &--full { - grid-column: 1 / -1; - } -} - -.rdpt-banner { - display: flex; - align-items: center; - gap: 10px; - margin: 16px 24px 0; - padding: 12px 14px; - background: #eff6ff; - border: 1px solid #dbeafe; - border-radius: 8px; - color: #1d4ed8; - font-size: 13px; - - &__icon { - font-size: 14px; - } -} - -.rdpt-footer { - padding: 0 24px 24px; - border-top: 1px solid #f1f5f9; -} - -.rdpt-footer__grid { - display: grid; - grid-template-columns: repeat(3, 1fr); - gap: 16px; - padding-top: 18px; - - @media (width <= 900px) { - grid-template-columns: 1fr; - } -} - -.rdpt-toggle-card { - padding: 12px 0; - - h4 { - margin: 0 0 8px; - font-size: 14px; - font-weight: 600; - color: $gray-900; - } - - &__choices { - display: flex; - align-items: center; - gap: 16px; - flex-wrap: wrap; - } -} - -.rdpt-note { - display: flex; - align-items: center; - gap: 10px; - margin-top: 18px; - padding: 12px 14px; - border: 1px solid #fde68a; - background: #fffbeb; - color: #b45309; - border-radius: 8px; - font-size: 12px; -} diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-terms-step/daily-deposit-scheme-product-terms-step.component.ts b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-terms-step/daily-deposit-scheme-product-terms-step.component.ts deleted file mode 100644 index 0d42c8ff1..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-terms-step/daily-deposit-scheme-product-terms-step.component.ts +++ /dev/null @@ -1,169 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import { Component, OnInit, Input, inject } from '@angular/core'; -import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; -import { MatTooltip } from '@angular/material/tooltip'; -import { Dates } from 'app/core/utils/dates'; -import { SettingsService } from 'app/settings/settings.service'; -import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; - -@Component({ - selector: 'mifosx-daily-deposit-scheme-product-terms-step', - templateUrl: './daily-deposit-scheme-product-terms-step.component.html', - styleUrls: ['./daily-deposit-scheme-product-terms-step.component.scss'], - imports: [ - ...STANDALONE_SHARED_IMPORTS, - MatTooltip - ] -}) -export class DailyDepositSchemeProductTermsStepComponent implements OnInit { - private formBuilder = inject(UntypedFormBuilder); - private dateUtils = inject(Dates); - private settingsService = inject(SettingsService); - - @Input() DailyDepositSchemeProductsTemplate: any; - - DailyDepositSchemeProductTermsForm: UntypedFormGroup; - - interestCompoundingPeriodTypeData: any; - interestPostingPeriodTypeData: any; - interestCalculationTypeData: any; - interestCalculationTypeOptionData: any; - interestCalculationDaysInYearTypeData: any; - periodFrequencyData: any[] = []; - yesNoOptions: Array<{ value: boolean; label: string }> = [ - { value: true, label: 'Yes' }, - { value: false, label: 'No' } - ]; - - constructor() { - this.createDailyDepositSchemeProductTermsForm(); - } - - ngOnInit() { - this.interestCompoundingPeriodTypeData = - this.DailyDepositSchemeProductsTemplate.interestCompoundingPeriodTypeOptions; - this.interestPostingPeriodTypeData = this.DailyDepositSchemeProductsTemplate.interestPostingPeriodTypeOptions; - // Standard enum (1=Daily, 2=Average) → "Interest Calculate" dropdown → interestCalculationType form control - this.interestCalculationTypeData = this.DailyDepositSchemeProductsTemplate.interestCalculationTypeOptions; - // CBS code values → "Interest Calculation Type" dropdown → interestCalculateSelector form control - this.interestCalculationTypeOptionData = - this.DailyDepositSchemeProductsTemplate.interestCalculationTypeOption || []; - this.interestCalculationDaysInYearTypeData = - this.DailyDepositSchemeProductsTemplate.interestCalculationDaysInYearTypeOptions; - this.periodFrequencyData = this.DailyDepositSchemeProductsTemplate.periodFrequencyOptions || []; - this.setYesNoOptions(this.DailyDepositSchemeProductsTemplate?.yesNoOptions); - - const isEditing = !!this.DailyDepositSchemeProductsTemplate.id; - const defaultPeriod = this.periodFrequencyData[0]?.value || 'Monthly'; - const savedEnumTypeId = - this.DailyDepositSchemeProductsTemplate.interestCalculationType?.id || - this.interestCalculationTypeData?.[0]?.id || - 1; - const savedSelectorId = isEditing - ? (this.DailyDepositSchemeProductsTemplate.interestCalculateSelector ?? - this.interestCalculationTypeOptionData?.[0]?.id ?? - null) - : (this.interestCalculationTypeOptionData?.[0]?.id ?? null); - - this.DailyDepositSchemeProductTermsForm.patchValue({ - nominalAnnualInterestRate: this.DailyDepositSchemeProductsTemplate.nominalAnnualInterestRate ?? 0, - interestCompoundingPeriodType: this.DailyDepositSchemeProductsTemplate.interestCompoundingPeriodType?.id, - interestPostingPeriodType: this.DailyDepositSchemeProductsTemplate.interestPostingPeriodType?.id, - interestCalculationType: savedEnumTypeId, - interestCalculationDaysInYearType: this.DailyDepositSchemeProductsTemplate.interestCalculationDaysInYearType?.id, - effectiveDate: this.DailyDepositSchemeProductsTemplate.effectiveDate - ? this.parseDate(this.DailyDepositSchemeProductsTemplate.effectiveDate) - : '', - interestCalculateSelector: savedSelectorId, - averagePeriod: isEditing ? this.DailyDepositSchemeProductsTemplate.averagePeriod || defaultPeriod : defaultPeriod, - autoPost: isEditing ? (this.DailyDepositSchemeProductsTemplate.autoPost ?? true) : true, - frequencyFixDay: isEditing ? (this.DailyDepositSchemeProductsTemplate.frequencyFixDay ?? 30) : 30, - lastInterestDate: this.DailyDepositSchemeProductsTemplate.lastInterestDate - ? this.parseDate(this.DailyDepositSchemeProductsTemplate.lastInterestDate) - : '', - minDepositAmount: this.DailyDepositSchemeProductsTemplate.minDepositAmount ?? '', - maxDepositAmount: this.DailyDepositSchemeProductsTemplate.maxDepositAmount ?? '' - }); - } - - private parseDate(dateValue: any): Date | null { - if (!dateValue) return null; - if (Array.isArray(dateValue)) { - return new Date(dateValue[0], dateValue[1] - 1, dateValue[2]); - } - if (dateValue instanceof Date) return dateValue; - return new Date(dateValue); - } - - private setYesNoOptions(options: any[]) { - if (!Array.isArray(options)) return; - const mapped = options - .map((opt: any) => { - const raw = opt?.name ?? opt?.value ?? opt; - const norm = typeof raw === 'string' ? raw.trim().toUpperCase() : raw; - if (norm === 'YES') return { value: true, label: 'Yes' }; - if (norm === 'NO') return { value: false, label: 'No' }; - return null; - }) - .filter(Boolean); - if (mapped.length) this.yesNoOptions = mapped; - } - - createDailyDepositSchemeProductTermsForm() { - this.DailyDepositSchemeProductTermsForm = this.formBuilder.group({ - nominalAnnualInterestRate: [ - '', - [ - Validators.required, - Validators.min(0) - ] - ], - interestCompoundingPeriodType: [ - '', - Validators.required - ], - interestPostingPeriodType: [ - '', - Validators.required - ], - interestCalculationType: [ - '', - Validators.required - ], - interestCalculationDaysInYearType: [ - '', - Validators.required - ], - effectiveDate: [''], - interestCalculateSelector: [1], - averagePeriod: ['Monthly'], - autoPost: [true], - frequencyFixDay: [ - 30, - [Validators.min(0)] - ], - lastInterestDate: [''], - minDepositAmount: [''], - maxDepositAmount: [''] - }); - } - - get DailyDepositSchemeProductTerms() { - const formValue = { ...this.DailyDepositSchemeProductTermsForm.value }; - const dateFormat = this.settingsService.dateFormat; - - if (formValue.effectiveDate instanceof Date) { - formValue.effectiveDate = this.dateUtils.formatDate(formValue.effectiveDate, dateFormat) || ''; - } - formValue.minDepositAmount = formValue.minDepositAmount; - formValue.maxDepositAmount = formValue.maxDepositAmount; - return formValue; - } -} diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product.resolver.ts b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product.resolver.ts deleted file mode 100644 index 0ec83acee..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product.resolver.ts +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -/** Angular Imports */ -import { Injectable, inject } from '@angular/core'; -import { ActivatedRouteSnapshot } from '@angular/router'; - -/** rxjs Imports */ -import { Observable } from 'rxjs'; - -/** Custom Services */ -import { ProductsService } from '../products.service'; - -/** - * Daily Deposit Scheme Product data resolver. - * - * Reuses the Recurring Deposit product read API — a Daily Deposit Scheme - * product is a Recurring Deposit product under the hood. - */ -@Injectable() -export class DailyDepositSchemeProductResolver { - private productsService = inject(ProductsService); - - /** - * Returns the Daily Deposit Scheme product data. - * @returns {Observable} - */ - resolve(route: ActivatedRouteSnapshot): Observable { - const productId = route.parent.paramMap.get('productId'); - return this.productsService.getRecurringDepositProduct(productId); - } -} diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.component.html b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.component.html deleted file mode 100644 index b2973f1f2..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.component.html +++ /dev/null @@ -1,172 +0,0 @@ - - -
-
-
-
-
-

{{ 'labels.heading.Daily Deposit Scheme Products' | translate }}

-

Manage Daily Deposit Scheme products — daily instalment amount, interest rates and charges.

-
-
-
-
- - {{ totalCount }} - {{ totalCount === 1 ? 'product' : 'products' }} -
- -
-
- -
-
-
-
-
{{ totalCount | number }}
-
Total DDS Products
-
-
-
-
-
-
{{ currencyCount | number }}
-
{{ currencyCount === 1 ? 'Currency' : 'Currencies' }} in use
-
-
-
-
-
-
{{ filteredCount | number }}
-
Showing after filter
-
-
-
- -
-
- -
- - @if (totalCount === 0) { -
-
-

No Daily Deposit Scheme products yet

-

Create your first Daily Deposit Scheme product to enable daily-instalment onboarding.

- -
- } @else { -
- - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Name' | translate }} -
-
-
-
{{ p.name }}
- @if (p.currency?.code) { -
{{ p.currency.code }}
- } -
-
-
{{ 'labels.inputs.Short Name' | translate }} - {{ p.shortName }} - Currency - @if (p.currency?.code) { - {{ p.currency.code }} - } @else { - — - } - Actions - - -
- - @if (filteredCount === 0 && totalCount > 0) { -
- - No products match your filter. -
- } - - -
- } -
-
diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.component.scss b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.component.scss deleted file mode 100644 index a880f9b22..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.component.scss +++ /dev/null @@ -1,17 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -@use 'assets/styles/product-list' as pl; - -@include pl.product-list-styles; - -/* Daily Deposit Scheme accent */ -:host { - --pl-accent: #0f766e; - --pl-accent-l: #ccfbf1; -} diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.component.ts b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.component.ts deleted file mode 100644 index 7af670304..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.component.ts +++ /dev/null @@ -1,124 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -/** Angular Imports */ -import { Component, OnInit, ViewChild, inject } from '@angular/core'; -import { MatPaginator } from '@angular/material/paginator'; -import { MatSort, MatSortHeader } from '@angular/material/sort'; -import { - MatTableDataSource, - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow -} from '@angular/material/table'; -import { ActivatedRoute } from '@angular/router'; -import { MatTooltip } from '@angular/material/tooltip'; -import { FaIconComponent } from '@fortawesome/angular-fontawesome'; -import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; - -/** - * Daily Deposit Scheme Products component. - * - * Lists Daily Deposit Scheme products. These are Recurring Deposit products - * under the hood (same create/read API) so this list reuses that data source. - */ -@Component({ - selector: 'mifosx-daily-deposit-scheme-products', - templateUrl: './daily-deposit-scheme-products.component.html', - styleUrls: ['./daily-deposit-scheme-products.component.scss'], - imports: [ - ...STANDALONE_SHARED_IMPORTS, - MatTooltip, - FaIconComponent, - MatTable, - MatSort, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatSortHeader, - MatCellDef, - MatCell, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow, - MatPaginator - ] -}) -export class DailyDepositSchemeProductsComponent implements OnInit { - private route = inject(ActivatedRoute); - - /** Data table data. */ - productData: any[] = []; - /** Columns to be displayed in the Daily Deposit Scheme products table. */ - displayedColumns: string[] = [ - 'name', - 'shortName', - 'currency', - 'actions' - ]; - /** Data source for the table. */ - dataSource: MatTableDataSource; - - filteredCount = 0; - - get totalCount(): number { - return this.productData?.length || 0; - } - - get currencyCount(): number { - const set = new Set(); - for (const p of this.productData || []) { - if (p?.currency?.code) set.add(p.currency.code); - } - return set.size; - } - - /** Paginator for the table. */ - @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator; - /** Sorter for the table. */ - @ViewChild(MatSort, { static: true }) sort: MatSort; - - constructor() { - this.route.data.subscribe((data: { dailyDepositSchemeProducts: any }) => { - this.productData = (data.dailyDepositSchemeProducts || []).filter( - (product: any) => product.productCategory === 'DDS' - ); - }); - } - - ngOnInit() { - this.setProducts(); - } - - /** - * Filters data in the table based on the passed value. - * @param {string} filterValue Value to filter data. - */ - applyFilter(filterValue: string) { - this.dataSource.filter = filterValue.trim().toLowerCase(); - } - - /** - * Initializes the data source, paginator and sorter for the table. - */ - setProducts() { - this.dataSource = new MatTableDataSource(this.productData); - this.dataSource.paginator = this.paginator; - this.dataSource.sort = this.sort; - this.filteredCount = this.totalCount; - this.dataSource.connect().subscribe((rows) => (this.filteredCount = rows.length)); - } -} diff --git a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.resolver.ts b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.resolver.ts deleted file mode 100644 index 544bdbe0c..000000000 --- a/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.resolver.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -/** Angular Imports */ -import { Injectable, inject } from '@angular/core'; - -/** rxjs Imports */ -import { Observable } from 'rxjs'; - -/** Custom Services */ -import { ProductsService } from '../products.service'; - -/** - * Daily Deposit Scheme Products data resolver. - * - * A Daily Deposit Scheme product is a Recurring Deposit product under the hood, - * so this reuses the same read API as Recurring Deposit Products. - */ -@Injectable() -export class DailyDepositSchemeProductsResolver { - private productsService = inject(ProductsService); - - /** - * Returns the Daily Deposit Scheme products data. - * @returns {Observable} - */ - resolve(): Observable { - return this.productsService.getRecurringDepositProducts(); - } -} diff --git a/src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/daily-deposit-scheme-product-and-template.resolver.ts b/src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/daily-deposit-scheme-product-and-template.resolver.ts deleted file mode 100644 index ca7c90dfb..000000000 --- a/src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/daily-deposit-scheme-product-and-template.resolver.ts +++ /dev/null @@ -1,35 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -/** Angular Imports */ -import { Injectable, inject } from '@angular/core'; -import { ActivatedRouteSnapshot } from '@angular/router'; - -/** rxjs Imports */ -import { Observable } from 'rxjs'; - -/** Custom Services */ -import { ProductsService } from '../../products.service'; - -/** - * Recurring Deposits Account Template resolver. - */ -@Injectable() -export class DailyDepositSchemeProductAndTemplateResolver { - private productsService = inject(ProductsService); - - /** - * Returns the Recurring Deposits Product and Template. - * @param {ActivatedRouteSnapshot} route Route Snapshot - * @returns {Observable} - */ - resolve(route: ActivatedRouteSnapshot): Observable { - const productId = route.parent.paramMap.get('productId'); - return this.productsService.getRecurringDepositProductAndTemplate(productId); - } -} diff --git a/src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/edit-daily-deposit-scheme-product.component.html b/src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/edit-daily-deposit-scheme-product.component.html deleted file mode 100644 index 3ce623701..000000000 --- a/src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/edit-daily-deposit-scheme-product.component.html +++ /dev/null @@ -1,202 +0,0 @@ - - -
-
- -

{{ 'labels.heading.Edit Daily Deposit Scheme Product' | translate }}

-
- - - - - - {{ 'labels.inputs.DETAILS' | translate }} - -
- -
-
- - - - - {{ 'labels.inputs.CURRENCY' | translate }} - -
- -
-
- - - - - {{ 'labels.inputs.TERMS' | translate }} - -
- -
-
- - - - - {{ 'labels.inputs.SETTINGS' | translate }} - -
- -
-
- - - - - CBS Base Chart - -
- @if (activeBaseVersion) { -
-
-
-

{{ activeBaseVersion.versionLabel }}

-

- Active CBS base chart - Effective from {{ activeBaseVersion.effectiveFromDate }} -

-
- Active -
- - @if (activeBaseVersion.remarks) { -

{{ activeBaseVersion.remarks }}

- } - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Sr.{{ slab.srNo }}Particular{{ slab.description }}From{{ slab.fromPeriod }}To{{ slab.toPeriod ?? 'N/A' }}Period Type{{ slab.periodTypeLabel }}Annual Rate (%) - {{ slab.annualInterestRate | number: '1.2-2' }} -
-
- } @else { -

Loading CBS base chart...

- } -
-
- - - - - {{ 'labels.inputs.CHARGES' | translate }} - -
- -
-
- - - - - {{ 'labels.inputs.ACCOUNTING' | translate }} - -
- - -
-
- - @if (DailyDepositSchemeProductFormValid) { - - - - {{ 'labels.inputs.PREVIEW' | translate }} - -
- -
-
- } -
- -
- -
- @if (selectedTabIndex > 0) { - - } - @if (selectedTabIndex < totalTabs - 1) { - - } - @if (DailyDepositSchemeProductFormValid && selectedTabIndex === totalTabs - 1) { - - } -
-
-
diff --git a/src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/edit-daily-deposit-scheme-product.component.scss b/src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/edit-daily-deposit-scheme-product.component.scss deleted file mode 100644 index c04c383f8..000000000 --- a/src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/edit-daily-deposit-scheme-product.component.scss +++ /dev/null @@ -1,138 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -$primary: #1353a0; - -.rdp-container { - max-width: 960px; - margin: 0 auto; - padding: 24px 16px; -} - -.rdp-header { - display: flex; - align-items: center; - gap: 10px; - margin-bottom: 20px; - - &__icon { - font-size: 20px; - color: $primary; - } - - &__title { - margin: 0; - font-size: 22px; - font-weight: 500; - } -} - -.rdp-tabs { - background: var(--md-sys-color-surface, #fff); - border-radius: 8px; - box-shadow: 0 2px 8px rgb(0 0 0 / 8%); - - ::ng-deep { - .mat-mdc-tab-header { - border-bottom: 1px solid rgb(0 0 0 / 8%); - } - - .mat-mdc-tab { - min-width: 100px; - padding: 0 12px; - height: 52px; - } - - .mat-mdc-tab-body-wrapper { - min-height: 200px; - } - - .mat-mdc-tab-body { - overflow: visible !important; - } - - .mat-mdc-tab-body-content { - overflow: visible !important; - } - } -} - -.rdp-tab-icon { - margin-right: 8px; - font-size: 14px; -} - -.rdp-tab-content { - padding: 24px 16px; -} - -.base-rate-card { - padding: 16px; - border: 1px solid #dbe4f0; - border-radius: 14px; - background: linear-gradient(180deg, #f8fbff 0%, #fff 100%); -} - -.base-rate-header { - display: flex; - align-items: start; - justify-content: space-between; - gap: 12px; - margin-bottom: 12px; -} - -.base-rate-title { - margin: 0; - font-size: 18px; - font-weight: 600; -} - -.base-rate-meta { - margin: 6px 0 0; - color: #5b6b7f; - display: flex; - gap: 12px; - flex-wrap: wrap; -} - -.base-rate-badge { - padding: 4px 10px; - border-radius: 999px; - background: #dcfce7; - color: #166534; - font-size: 12px; - font-weight: 700; -} - -.base-rate-remarks { - margin: 0 0 12px; - color: #334155; -} - -.base-rate-table { - width: 100%; - border-collapse: collapse; -} - -.rdp-action-bar { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 20px; - padding: 16px 0; - border-top: 1px solid rgb(0 0 0 / 8%); - - &__cancel { - color: rgb(0 0 0 / 60%); - } - - &__right { - display: flex; - gap: 12px; - } -} diff --git a/src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/edit-daily-deposit-scheme-product.component.ts b/src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/edit-daily-deposit-scheme-product.component.ts deleted file mode 100644 index 38d715746..000000000 --- a/src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/edit-daily-deposit-scheme-product.component.ts +++ /dev/null @@ -1,305 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -/** Angular Imports */ -import { Component, ViewChild, inject } from '@angular/core'; -import { ActivatedRoute, Router } from '@angular/router'; - -/** Custom Components */ -import { DailyDepositSchemeProductDetailsStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-details-step/daily-deposit-scheme-product-details-step.component'; -import { DailyDepositSchemeProductCurrencyStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-currency-step/daily-deposit-scheme-product-currency-step.component'; -import { DailyDepositSchemeProductTermsStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-terms-step/daily-deposit-scheme-product-terms-step.component'; -import { DailyDepositSchemeProductSettingsStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-settings-step/daily-deposit-scheme-product-settings-step.component'; -import { DailyDepositSchemeProductChargesStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-charges-step/daily-deposit-scheme-product-charges-step.component'; -import { DailyDepositSchemeProductAccountingStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-accounting-step/daily-deposit-scheme-product-accounting-step.component'; -import { - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow -} from '@angular/material/table'; - -/** Custom Services */ -import { ProductsService } from 'app/products/products.service'; -import { SettingsService } from 'app/settings/settings.service'; -import { Accounting } from 'app/core/utils/accounting'; -import { Dates } from 'app/core/utils/dates'; -import { MatTabsModule } from '@angular/material/tabs'; -import { DailyDepositSchemeProductPreviewStepComponent } from '../daily-deposit-scheme-product-stepper/daily-deposit-scheme-product-preview-step/daily-deposit-scheme-product-preview-step.component'; -import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; - -@Component({ - selector: 'mifosx-edit-daily-deposit-scheme-product', - templateUrl: './edit-daily-deposit-scheme-product.component.html', - styleUrls: ['./edit-daily-deposit-scheme-product.component.scss'], - imports: [ - ...STANDALONE_SHARED_IMPORTS, - MatTabsModule, - DailyDepositSchemeProductDetailsStepComponent, - DailyDepositSchemeProductCurrencyStepComponent, - DailyDepositSchemeProductTermsStepComponent, - DailyDepositSchemeProductSettingsStepComponent, - DailyDepositSchemeProductChargesStepComponent, - DailyDepositSchemeProductAccountingStepComponent, - DailyDepositSchemeProductPreviewStepComponent, - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow - ] -}) -export class EditDailyDepositSchemeProductComponent { - private route = inject(ActivatedRoute); - private productsService = inject(ProductsService); - private router = inject(Router); - private settingsService = inject(SettingsService); - private accounting = inject(Accounting); - private dateUtils = inject(Dates); - - @ViewChild(DailyDepositSchemeProductDetailsStepComponent, { static: true }) - DailyDepositSchemeProductDetailsStep: DailyDepositSchemeProductDetailsStepComponent; - @ViewChild(DailyDepositSchemeProductCurrencyStepComponent, { static: true }) - DailyDepositSchemeProductCurrencyStep: DailyDepositSchemeProductCurrencyStepComponent; - @ViewChild(DailyDepositSchemeProductTermsStepComponent, { static: true }) - DailyDepositSchemeProductTermsStep: DailyDepositSchemeProductTermsStepComponent; - @ViewChild(DailyDepositSchemeProductSettingsStepComponent, { static: true }) - DailyDepositSchemeProductSettingsStep: DailyDepositSchemeProductSettingsStepComponent; - @ViewChild(DailyDepositSchemeProductChargesStepComponent, { static: true }) - DailyDepositSchemeProductChargesStep: DailyDepositSchemeProductChargesStepComponent; - @ViewChild(DailyDepositSchemeProductAccountingStepComponent, { static: true }) - DailyDepositSchemeProductAccountingStep: DailyDepositSchemeProductAccountingStepComponent; - - DailyDepositSchemeProductsTemplate: any; - accountingRuleData: string[] = []; - rdInterestRates: any[] = []; - readonly rdSlabColumns = [ - 'srNo', - 'description', - 'fromPeriod', - 'toPeriod', - 'periodTypeLabel', - 'annualInterestRate' - ]; - selectedTabIndex = 0; - - /** - * @param {ActivatedRoute} route Activated Route. - * @param {ProductsService} productsService Products Service. - * @param {Router} router Router for navigation. - * @param {SettingsService} settingsService Settings Service. - */ - - constructor() { - this.route.data.subscribe((data: { DailyDepositSchemeProductAndTemplate: any }) => { - this.DailyDepositSchemeProductsTemplate = data.DailyDepositSchemeProductAndTemplate; - this.productsService.getFdBaseInterestRates().subscribe({ - next: (rates: any[]) => { - this.rdInterestRates = rates || []; - }, - error: () => { - this.rdInterestRates = []; - } - }); - }); - this.accountingRuleData = this.accounting.getAccountingRulesForSavings(); - } - - get DailyDepositSchemeProductDetailsForm() { - return this.DailyDepositSchemeProductDetailsStep?.DailyDepositSchemeProductDetailsForm; - } - - get DailyDepositSchemeProductCurrencyForm() { - return this.DailyDepositSchemeProductCurrencyStep?.DailyDepositSchemeProductCurrencyForm; - } - - get DailyDepositSchemeProductTermsForm() { - return this.DailyDepositSchemeProductTermsStep?.DailyDepositSchemeProductTermsForm; - } - - get DailyDepositSchemeProductSettingsForm() { - return this.DailyDepositSchemeProductSettingsStep?.DailyDepositSchemeProductSettingsForm; - } - - get DailyDepositSchemeProductAccountingForm() { - return this.DailyDepositSchemeProductAccountingStep?.DailyDepositSchemeProductAccountingForm; - } - - get totalTabs(): number { - return this.DailyDepositSchemeProductFormValid ? 8 : 7; - } - - get DailyDepositSchemeProductFormValid() { - return !!this.DailyDepositSchemeProductDetailsForm?.valid; - } - - get activeBaseVersion() { - const chart = - this.rdInterestRates.find((version: any) => version.fdType === 'DOUBLE' && version.isActive) ?? - this.rdInterestRates.find((version: any) => version.fdType === 'DOUBLE') ?? - this.rdInterestRates[0]; - if (chart) { - return { - ...chart, - versionLabel: chart.versionLabel?.replace('FD', 'RD'), - remarks: chart.remarks?.replace('FD', 'RD') - }; - } - return null; - } - - get activeBaseSlabs() { - return this.activeBaseVersion?.slabs || []; - } - - get DailyDepositSchemeProduct() { - // accountTypeGroup / autoInterestPost / autoInterestPostWhen are the legacy Account Type - // Creation screen's own fields (added for the simplified Create form) - not real product API - // parameters, so they must not leak into the update payload the way a plain spread would. - // accountTypeGroup is deliberately NOT re-folded into description here (unlike Create): it - // was already folded in once at creation time, and this getter re-runs on every change - // detection cycle while editing, so re-prepending it here would keep stacking duplicate - // "Group - " prefixes onto the same description. autoInterestPost/When aren't re-synced onto - // the Terms tab either, since Edit's own Terms/Settings/Accounting tabs remain fully visible - // and directly editable. - const { accountTypeGroup, autoInterestPost, autoInterestPostWhen, autoInterestPostDay, ...detailsPayload } = - this.DailyDepositSchemeProductDetailsStep.DailyDepositSchemeProductDetails; - - return { - ...detailsPayload, - ...this.DailyDepositSchemeProductCurrencyStep.DailyDepositSchemeProductCurrency, - ...this.DailyDepositSchemeProductTermsStep.DailyDepositSchemeProductTerms, - ...this.DailyDepositSchemeProductSettingsStep.DailyDepositSchemeProductSettings, - ...this.DailyDepositSchemeProductChargesStep.DailyDepositSchemeProductCharges, - ...this.DailyDepositSchemeProductAccountingStep.DailyDepositSchemeProductAccounting, - // charts intentionally omitted here - the backend no longer requires a charts payload for RD - // updates once rdSlabs is present, and this getter's chart data comes from the shared CBS - // base-rate template (not this product's own chart), which caused spurious - // "chart.overlapping.from.and.end.dates" errors when a product's real chart had different - // dates. Still included in DailyDepositSchemeProductPreview below for the read-only display tab. - rdSlabs: this.activeBaseSlabs - }; - } - - get DailyDepositSchemeProductPreview() { - return { - ...this.DailyDepositSchemeProduct, - charts: this.DailyDepositSchemeProductCharts, - rdSlabs: this.activeBaseSlabs - }; - } - - get DailyDepositSchemeProductCharts() { - const activeBaseVersion = this.activeBaseVersion; - if (!activeBaseVersion?.slabs?.length) { - return []; - } - const dateFormat = this.settingsService.dateFormat; - - return [ - { - name: activeBaseVersion.versionLabel || 'CBS Base Chart', - description: activeBaseVersion.remarks || activeBaseVersion.versionLabel || 'CBS base chart', - fromDate: - this.dateUtils.formatDate(activeBaseVersion.effectiveFromDate, dateFormat) || - activeBaseVersion.effectiveFromDate, - ...(activeBaseVersion.effectiveToDate - ? { - endDate: - this.dateUtils.formatDate(activeBaseVersion.effectiveToDate, dateFormat) || - activeBaseVersion.effectiveToDate - } - : {}), - isPrimaryGroupingByAmount: false, - locale: this.settingsService.language.code, - dateFormat, - chartSlabs: activeBaseVersion.slabs.map((slab: any) => { - const chartSlab: any = { - periodType: slab.periodTypeEnum, - fromPeriod: slab.fromPeriod, - toPeriod: slab.toPeriod, - annualInterestRate: slab.annualInterestRate, - description: slab.description || '', - incentives: [] as any[] - }; - - if (slab.amountRangeFrom !== null && slab.amountRangeFrom !== undefined && slab.amountRangeFrom !== '') { - chartSlab.amountRangeFrom = slab.amountRangeFrom; - } - - if (slab.amountRangeTo !== null && slab.amountRangeTo !== undefined && slab.amountRangeTo !== '') { - chartSlab.amountRangeTo = slab.amountRangeTo; - } - - return chartSlab; - }) - } - ] as any[]; - } - - submit() { - const DailyDepositSchemeProduct: any = this.pruneEmptyValues({ - ...this.DailyDepositSchemeProduct, - charges: (this.DailyDepositSchemeProduct.charges ?? []).map((charge: any) => ({ id: charge.id })), - locale: this.settingsService.language.code, - dateFormat: this.settingsService.dateFormat - }); - if (!DailyDepositSchemeProduct.description) { - DailyDepositSchemeProduct.description = ''; - } - delete DailyDepositSchemeProduct.advancedAccountingRules; - const productId = this.DailyDepositSchemeProductsTemplate.id; - this.productsService - .updateRecurringDepositProduct(productId, DailyDepositSchemeProduct) - .subscribe((response: any) => { - this.router.navigate(['../'], { relativeTo: this.route }); - }); - } - - private pruneEmptyValues(value: T): T { - if (Array.isArray(value)) { - return value.map((item) => this.pruneEmptyValues(item)).filter((item) => item !== undefined) as T; - } - - if (value && typeof value === 'object') { - return Object.entries(value as Record).reduce( - (acc, [ - key, - currentValue - ]) => { - const cleanedValue = this.pruneEmptyValues(currentValue); - if (cleanedValue === undefined) { - return acc; - } - return { - ...acc, - [key]: cleanedValue - }; - }, - {} as Record - ) as T; - } - - if (value === '' || value === null || value === undefined) { - return undefined as T; - } - - return value; - } -} diff --git a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-datatable-tab/daily-deposit-scheme-datatable-tab.component.html b/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-datatable-tab/daily-deposit-scheme-datatable-tab.component.html deleted file mode 100644 index c8d7995ea..000000000 --- a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-datatable-tab/daily-deposit-scheme-datatable-tab.component.html +++ /dev/null @@ -1,17 +0,0 @@ - - -
- - -
diff --git a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-datatable-tab/daily-deposit-scheme-datatable-tab.component.scss b/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-datatable-tab/daily-deposit-scheme-datatable-tab.component.scss deleted file mode 100644 index af9f4c075..000000000 --- a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-datatable-tab/daily-deposit-scheme-datatable-tab.component.scss +++ /dev/null @@ -1,7 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ diff --git a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-datatable-tab/daily-deposit-scheme-datatable-tab.component.ts b/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-datatable-tab/daily-deposit-scheme-datatable-tab.component.ts deleted file mode 100644 index 4d5a895ed..000000000 --- a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-datatable-tab/daily-deposit-scheme-datatable-tab.component.ts +++ /dev/null @@ -1,38 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import { Component, inject } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { EntityDatatableTabComponent } from '../../../../shared/tabs/entity-datatable-tab/entity-datatable-tab.component'; -import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; - -@Component({ - selector: 'mifosx-daily-deposit-scheme-datatable-tab', - templateUrl: './daily-deposit-scheme-datatable-tab.component.html', - styleUrls: ['./daily-deposit-scheme-datatable-tab.component.scss'], - imports: [ - ...STANDALONE_SHARED_IMPORTS, - EntityDatatableTabComponent - ] -}) -export class DailyDepositSchemeDatatableTabComponent { - private route = inject(ActivatedRoute); - - entityId: string; - entityDatatable: any; - multiRowDatatableFlag: boolean; - - constructor() { - this.entityId = this.route.parent.parent.snapshot.paramMap.get('productId'); - - this.route.data.subscribe((data: { recurringDepositDatatable: any }) => { - this.entityDatatable = data.recurringDepositDatatable; - this.multiRowDatatableFlag = this.entityDatatable.columnHeaders[0].columnName === 'id' ? true : false; - }); - } -} diff --git a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-general-tab/daily-deposit-scheme-general-tab.component.html b/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-general-tab/daily-deposit-scheme-general-tab.component.html deleted file mode 100644 index 8881ab235..000000000 --- a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-general-tab/daily-deposit-scheme-general-tab.component.html +++ /dev/null @@ -1,843 +0,0 @@ - - -
- -
-
-
-
-
Daily Deposit Scheme Product
-

{{ DailyDepositSchemeProduct.name }}

-
- {{ DailyDepositSchemeProduct.shortName }} - @if (DailyDepositSchemeProduct.currency?.code) { - {{ DailyDepositSchemeProduct.currency.code }} - } - - - {{ DailyDepositSchemeProduct.endDate ? 'Inactive' : 'Active' }} - -
- @if (DailyDepositSchemeProduct.description) { -

{{ DailyDepositSchemeProduct.description }}

- } -
-
- -
- - -
-
-
-

{{ 'labels.inputs.Details' | translate }}

-
-
-
- {{ 'labels.inputs.Short Name' | translate }} - {{ DailyDepositSchemeProduct.shortName }} -
- @if (DailyDepositSchemeProduct.description) { -
- {{ 'labels.inputs.Description' | translate }} - {{ DailyDepositSchemeProduct.description }} -
- } -
-
- - -
-
-
-

Product Classification

-
-
- @if (DailyDepositSchemeProduct.accountType) { -
- Account Type - {{ DailyDepositSchemeProduct.accountType }} -
- } - @if (DailyDepositSchemeProduct.customerSegment) { -
- Customer Segment - {{ DailyDepositSchemeProduct.customerSegment }} -
- } - @if (DailyDepositSchemeProduct.glAccountId) { -
- GL Account (Liability) - {{ - DailyDepositSchemeProduct.glAccountName || DailyDepositSchemeProduct.glAccountId - }} -
- } - @if (DailyDepositSchemeProduct.effectiveTo) { -
- Effective To - {{ DailyDepositSchemeProduct.effectiveTo | dateFormat }} -
- } - @if (DailyDepositSchemeProduct.endDate) { -
- End Date - {{ DailyDepositSchemeProduct.endDate | dateFormat }} -
- } -
- Introducer Required - - - {{ DailyDepositSchemeProduct.introducerRequired | yesNo }} - - -
-
- Deposit Account Link - - - {{ DailyDepositSchemeProduct.shareLinkingEnabled | yesNo }} - - -
-
-
- - -
-
-
-

{{ 'labels.inputs.Currency' | translate }}

-
-
-
- {{ 'labels.inputs.Currency' | translate }} - ({{ DailyDepositSchemeProduct.currency.code }}) {{ DailyDepositSchemeProduct.currency.name }} -
-
- {{ 'labels.inputs.Decimal Places' | translate }} - {{ DailyDepositSchemeProduct.currency.decimalPlaces }} -
-
- Dormancy Alert - {{ DailyDepositSchemeProduct.dormancyAlert | yesNo }} -
-
- Balance Swap Option - {{ DailyDepositSchemeProduct.balanceSwapOption | yesNo }} -
-
- MSME - {{ DailyDepositSchemeProduct.msme | yesNo }} -
-
- Nomination Facility - {{ DailyDepositSchemeProduct.nominationFacility | yesNo }} -
-
- Auto Debit Facility - {{ DailyDepositSchemeProduct.autoDebitFacility | yesNo }} -
-
-
- - -
-
-
-

{{ 'labels.inputs.Terms' | translate }}

-
-
-
- {{ 'labels.inputs.Deposit Amount' | translate }} - - {{ DailyDepositSchemeProduct.depositAmount | formatNumber }} - (Min: {{ DailyDepositSchemeProduct.minDepositAmount || DailyDepositSchemeProduct.depositAmount }}, Max: - {{ DailyDepositSchemeProduct.maxDepositAmount || DailyDepositSchemeProduct.depositAmount }}) - -
-
- {{ 'labels.inputs.Nominal Annual Interest' | translate }} - {{ DailyDepositSchemeProduct.nominalAnnualInterestRate | formatNumber - }}% -
- @if (DailyDepositSchemeProduct.effectiveDate) { -
- Effective Date - {{ DailyDepositSchemeProduct.effectiveDate | dateFormat }} -
- } - @if (DailyDepositSchemeProduct.interestCalculationTypeName) { -
- {{ 'labels.inputs.Interest Calculation Type' | translate }} - {{ DailyDepositSchemeProduct.interestCalculationTypeName }} -
- } -
- Interest Calculate - {{ DailyDepositSchemeProduct.interestCalculationType?.value }} -
- @if (DailyDepositSchemeProduct.averagePeriod) { -
- Average Period - {{ DailyDepositSchemeProduct.averagePeriod }} -
- } -
- {{ 'labels.inputs.Interest Posting Period' | translate }} - {{ DailyDepositSchemeProduct.interestPostingPeriodType.value }} -
-
- {{ 'labels.inputs.Interest Compounding Period' | translate }} - {{ DailyDepositSchemeProduct.interestCompoundingPeriodType.value }} -
-
- {{ 'labels.inputs.Days in Year' | translate }} - {{ DailyDepositSchemeProduct.interestCalculationDaysInYearType.value }} -
-
- Auto Post - - - {{ DailyDepositSchemeProduct.autoPost | yesNo }} - - -
- @if (DailyDepositSchemeProduct.frequencyFixDay) { -
- Frequency (Fix Day) - {{ DailyDepositSchemeProduct.frequencyFixDay }} -
- } - @if (DailyDepositSchemeProduct.lastInterestDate) { -
- Last Interest Date - {{ DailyDepositSchemeProduct.lastInterestDate | dateFormat }} -
- } -
-
- - -
-
-
-

{{ 'labels.inputs.Settings' | translate }}

-
- -

Premature Option

-
-
- Loan Facility - - - {{ DailyDepositSchemeProduct.loanFacility | yesNo }} - - -
-
- Premature Withdrawal Option - - - {{ DailyDepositSchemeProduct.prematureWithdrawalOption | yesNo }} - - -
-
- Tenure Flexibility - - - {{ DailyDepositSchemeProduct.tenureFlexibility | yesNo }} - - -
-
- Lien Marked - - - {{ DailyDepositSchemeProduct.lienMarked | yesNo }} - - -
-
- {{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }} - - - {{ DailyDepositSchemeProduct.withdrawalFeeForTransfers | yesNo }} - - -
-
- -

{{ 'labels.inputs.Lock-in Period' | translate }}

-
-
- Enable Lock-in Period - - - {{ DailyDepositSchemeProduct.enableLockinPeriod | yesNo }} - - -
- @if (DailyDepositSchemeProduct.lockinPeriodFrequency) { -
- {{ 'labels.inputs.Lock-in Period' | translate }} - {{ DailyDepositSchemeProduct.lockinPeriodFrequency }} - {{ DailyDepositSchemeProduct.lockinPeriodFrequencyType?.value }} -
- } -
- -

Maturity Instructions

-
-
- Maturity Instructions - - - {{ DailyDepositSchemeProduct.maturityInstructions | yesNo }} - - -
-
- Credit Maturity Proceeds - - - {{ DailyDepositSchemeProduct.creditMaturityProceeds | yesNo }} - - -
-
- Penalty Grace Period (Missed Installments) - - - {{ DailyDepositSchemeProduct.penaltyGracePeriodMissedInstallments | yesNo }} - - -
-
- Panel Request Applicable - - - {{ DailyDepositSchemeProduct.panelRequestApplicable | yesNo }} - - -
-
- No Penalty (Nominee / Heir Closure) - - - {{ DailyDepositSchemeProduct.noPenaltyNomineeHeirClosure | yesNo }} - - -
-
- Deceased Settlement - - - {{ DailyDepositSchemeProduct.deceasedSettlement | yesNo }} - - -
-
- Allow Debit Txn After Due Date - - - {{ DailyDepositSchemeProduct.allowDebitTxnAfterDueDate | yesNo }} - - -
-
- -

Additional Rules

-
-
- Account Closed Zero Balance in Day Closing - - - {{ DailyDepositSchemeProduct.accountClosedZeroBalanceInDayClosing | yesNo }} - - -
- @if (DailyDepositSchemeProduct.minBalanceForInterestCalculation) { -
- {{ 'labels.inputs.Balance Required for Interest Calculation' | translate }} - {{ DailyDepositSchemeProduct.minBalanceForInterestCalculation | formatNumber }} -
- } - @if (DailyDepositSchemeProduct.minDepositTerm) { -
- {{ 'labels.inputs.Minimum Deposit Term' | translate }} - {{ DailyDepositSchemeProduct.minDepositTerm }} - {{ DailyDepositSchemeProduct.minDepositTermType?.value }} -
- } - @if (DailyDepositSchemeProduct.maxDepositTerm) { -
- {{ 'labels.inputs.Maximum Deposit Term' | translate }} - {{ DailyDepositSchemeProduct.maxDepositTerm }} - {{ DailyDepositSchemeProduct.maxDepositTermType?.value }} -
- } - @if (DailyDepositSchemeProduct.inMultipleOfDepositTerm) { -
- {{ 'labels.inputs.Deposit Term In multiple of' | translate }} - {{ DailyDepositSchemeProduct.inMultipleOfDepositTerm }} - - {{ DailyDepositSchemeProduct.inMultipleOfDepositTermType?.value }} -
- } - @if (DailyDepositSchemeProduct.isMandatoryDeposit !== undefined) { -
- {{ 'labels.inputs.Daily Deposit Scheme Type' | translate }} - {{ - DailyDepositSchemeProduct.isMandatoryDeposit - ? ('labels.text.Mandatory' | translate) - : ('labels.text.Voluntary' | translate) - }} -
- } - @if (DailyDepositSchemeProduct.preClosurePenalInterest) { -
- {{ 'labels.inputs.Apply penal interest (less)' | translate }} - {{ DailyDepositSchemeProduct.preClosurePenalInterest }}% On - {{ DailyDepositSchemeProduct.preClosurePenalInterestOnType?.value }} -
- } -
- {{ 'labels.inputs.Withhold Tax is Applicable' | translate }} - - - {{ DailyDepositSchemeProduct.withHoldTax | yesNo }} - - -
- @if (DailyDepositSchemeProduct.withHoldTax) { -
- {{ 'labels.inputs.Tax Group' | translate }} - {{ DailyDepositSchemeProduct.taxGroup?.name }} -
- } -
-
- - - @if (DailyDepositSchemeProduct.activeChart) { -
-
-
-

{{ 'labels.heading.Interest Rate Charts' | translate }}

-
- - -
- @if (DailyDepositSchemeProduct.activeChart.name) { - {{ DailyDepositSchemeProduct.activeChart.name }} - } - {{ DailyDepositSchemeProduct.activeChart.fromDate | dateFormat }} - @if (DailyDepositSchemeProduct.activeChart.endDate) { - - {{ DailyDepositSchemeProduct.activeChart.endDate | dateFormat }} - } - - Primary by Amount: {{ DailyDepositSchemeProduct.activeChart.isPrimaryGroupingByAmount | yesNo }} - -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Sr.{{ i + 1 }}Particular{{ chartSlab.description }}From{{ chartSlab.fromPeriod }}To{{ chartSlab.toPeriod ?? '—' }}Period{{ chartSlab.periodType?.value }}Rate (%) - {{ chartSlab.annualInterestRate }} - - - -
-

{{ 'labels.heading.Incentives' | translate }}

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Entity Type' | translate }} - {{ - incentive.entityType - | find: DailyDepositSchemeProductTemplate.chartTemplate.entityTypeOptions : 'id' : 'value' - }} - {{ 'labels.inputs.Attribute Name' | translate }} - {{ - incentive.attributeName - | find - : DailyDepositSchemeProductTemplate.chartTemplate.attributeNameOptions - : 'id' - : 'value' - }} - {{ 'labels.inputs.Condition Type' | translate }} - {{ - incentive.conditionType - | find - : DailyDepositSchemeProductTemplate.chartTemplate.conditionTypeOptions - : 'id' - : 'value' - }} - {{ 'labels.inputs.Attribute Value' | translate }} - @switch (incentive.attributeName) { - @case (2) { - {{ - incentive.attributeValue - | find: DailyDepositSchemeProductTemplate.chartTemplate.genderOptions : 'id' : 'name' - }} - } - @case (3) { - {{ incentive.attributeValue }} - } - @case (4) { - {{ - incentive.attributeValue - | find - : DailyDepositSchemeProductTemplate.chartTemplate.clientTypeOptions - : 'id' - : 'name' - }} - } - @case (5) { - {{ - incentive.attributeValue - | find - : DailyDepositSchemeProductTemplate.chartTemplate.clientClassificationOptions - : 'id' - : 'name' - }} - } - } - {{ 'labels.inputs.Incentive Type' | translate }} - {{ - incentive.incentiveType - | find - : DailyDepositSchemeProductTemplate.chartTemplate.incentiveTypeOptions - : 'id' - : 'value' - }} - {{ 'labels.inputs.Amount' | translate }}{{ incentive.amount }}
-
-
-
-
-
- } - - - @if (DailyDepositSchemeProduct.charges.length) { -
-
-
-

{{ 'labels.heading.Charges' | translate }}

-
-
- - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Name' | translate }}{{ charge.name }}{{ 'labels.inputs.Type' | translate }}{{ charge.chargeCalculationType.value }}{{ 'labels.inputs.Amount' | translate }}{{ charge.amount | formatNumber }}{{ 'labels.inputs.Collected On' | translate }}{{ charge.chargeTimeType.value }}
-
-
- } - - -
-
-
-

{{ 'labels.heading.Accounting' | translate }}

-
- -
- - - @if ( - DailyDepositSchemeProduct.paymentChannelToFundSourceMappings?.length || - DailyDepositSchemeProduct.feeToIncomeAccountMappings?.length || - DailyDepositSchemeProduct.penaltyToIncomeAccountMappings?.length - ) { -
-
-
-

{{ 'labels.inputs.Advanced Accounting Rules' | translate }}

-
- @if (DailyDepositSchemeProduct.paymentChannelToFundSourceMappings?.length) { -

{{ 'labels.inputs.Fund Sources for Payment Channels' | translate }}

-
- - - - - - - - - - - -
{{ 'labels.inputs.Payment Type' | translate }}{{ p.paymentType.name }}{{ 'labels.inputs.Fund Source' | translate }}{{ p.fundSourceAccount.name }}
-
- } - @if (DailyDepositSchemeProduct.feeToIncomeAccountMappings?.length) { -

{{ 'labels.inputs.Fees to Specific Income Accounts' | translate }}

-
- - - - - - - - - - - -
{{ 'labels.inputs.Fee/Charge' | translate }}{{ f.charge.name }}{{ 'labels.inputs.Income Account' | translate }}{{ f.incomeAccount.name }}
-
- } - @if (DailyDepositSchemeProduct.penaltyToIncomeAccountMappings?.length) { -

{{ 'labels.inputs.Penalties to Specific Income Accounts' | translate }}

-
- - - - - - - - - - - -
{{ 'labels.inputs.Penalty' | translate }}{{ p.charge.name }}{{ 'labels.inputs.Income Account' | translate }}{{ p.incomeAccount.name }}
-
- } -
- } -
diff --git a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-general-tab/daily-deposit-scheme-general-tab.component.scss b/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-general-tab/daily-deposit-scheme-general-tab.component.scss deleted file mode 100644 index 9e7b167c3..000000000 --- a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-general-tab/daily-deposit-scheme-general-tab.component.scss +++ /dev/null @@ -1,528 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -:host { - --vsp-navy: #0b1e3d; - --vsp-blue: #1d4ed8; - --vsp-blue-l: #eff6ff; - --vsp-teal: #0e7e6b; - --vsp-teal-l: #ccfbf1; - --vsp-purple: #6d28d9; - --vsp-purple-l: #ede9fe; - --vsp-indigo: #3730a3; - --vsp-indigo-l: #e0e7ff; - --vsp-amber: #b45309; - --vsp-amber-l: #fef3c7; - --vsp-green: #15803d; - --vsp-green-l: #dcfce7; - --vsp-red: #c0392b; - --vsp-red-l: #fee2e2; - --vsp-pink: #be185d; - --vsp-pink-l: #fce7f3; - --vsp-g50: #f8fafc; - --vsp-g100: #f1f5f9; - --vsp-g200: #e2e8f0; - --vsp-g500: #64748b; - --vsp-g700: #334155; - --vsp-g900: #0f172a; - - display: block; - font-family: - 'DM Sans', - system-ui, - -apple-system, - sans-serif; - color: var(--vsp-g900); - font-size: 13.5px; -} - -.vsp-root { - padding: 4px 4px 24px; - display: flex; - flex-direction: column; - gap: 14px; -} - -.vsp-hero { - background: linear-gradient(135deg, var(--vsp-navy) 0%, var(--vsp-blue) 100%); - border-radius: 14px; - padding: 22px 22px 22px 18px; - display: flex; - align-items: flex-start; - gap: 18px; - box-shadow: - 0 10px 24px rgb(11 30 61 / 18%), - 0 4px 10px rgb(11 30 61 / 12%); - color: #fff; - position: relative; - overflow: hidden; -} - -.vsp-hero::before { - content: ''; - position: absolute; - top: -60px; - right: -60px; - width: 240px; - height: 240px; - border-radius: 50%; - background: radial-gradient(circle, rgb(255 255 255 / 8%) 0%, transparent 70%); - pointer-events: none; -} - -.vsp-hero-left { - display: flex; - align-items: flex-start; - gap: 16px; - flex: 1; - min-width: 0; -} - -.vsp-hero-icon { - width: 56px; - height: 56px; - border-radius: 14px; - background: rgb(255 255 255 / 16%); - border: 1px solid rgb(255 255 255 / 22%); - display: inline-flex; - align-items: center; - justify-content: center; - font-size: 24px; - flex-shrink: 0; -} - -.vsp-hero-text { - flex: 1; - min-width: 0; -} - -.vsp-hero-eyebrow { - font-size: 10.5px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.12em; - color: rgb(255 255 255 / 70%); - margin-bottom: 4px; -} - -.vsp-hero-title { - margin: 0 0 10px; - font-size: 22px; - font-weight: 700; - letter-spacing: 0.01em; - color: #fff; - line-height: 1.2; -} - -.vsp-hero-meta { - display: flex; - align-items: center; - gap: 8px; - flex-wrap: wrap; -} - -.vsp-hero-desc { - margin: 12px 0 0; - font-size: 13px; - color: rgb(255 255 255 / 82%); - line-height: 1.5; - max-width: 720px; -} - -.vsp-edit-btn { - display: inline-flex; - align-items: center; - gap: 8px; - height: 38px; - padding: 0 16px; - border-radius: 10px; - background: rgb(255 255 255 / 14%); - color: #fff; - border: 1px solid rgb(255 255 255 / 28%); - font-size: 13px; - font-weight: 700; - letter-spacing: 0.02em; - cursor: pointer; - text-decoration: none; - white-space: nowrap; - font-family: inherit; - flex-shrink: 0; - transition: all 0.16s ease; - - fa-icon { - font-size: 12px; - } - - &:hover { - background: rgb(255 255 255 / 24%); - border-color: rgb(255 255 255 / 50%); - } -} - -.vsp-pill { - display: inline-flex; - align-items: center; - gap: 5px; - font-size: 11px; - font-weight: 700; - letter-spacing: 0.04em; - text-transform: uppercase; - padding: 4px 10px; - border-radius: 999px; - font-family: inherit; - - fa-icon { - font-size: 9.5px; - } - - &--code { - background: rgb(255 255 255 / 18%); - color: #fff; - border: 1px solid rgb(255 255 255 / 24%); - font-family: 'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace; - } - - &--teal { - background: var(--vsp-teal-l); - color: var(--vsp-teal); - border: 1px solid #99f6e4; - } - - &--blue { - background: var(--vsp-blue-l); - color: var(--vsp-blue); - border: 1px solid #dbeafe; - } - - &--green { - background: var(--vsp-green-l); - color: var(--vsp-green); - border: 1px solid #bbf7d0; - } - - &--amber { - background: var(--vsp-amber-l); - color: var(--vsp-amber); - border: 1px solid #fde68a; - } - - &--red { - background: var(--vsp-red-l); - color: var(--vsp-red); - border: 1px solid #fecaca; - } - - &--gray { - background: var(--vsp-g100); - color: var(--vsp-g700); - border: 1px solid var(--vsp-g200); - } -} - -.vsp-section { - background: #fff; - border: 1px solid var(--vsp-g200); - border-radius: 12px; - padding: 16px 20px; - box-shadow: 0 1px 2px rgb(15 23 42 / 4%); -} - -.vsp-section-h { - display: flex; - align-items: center; - gap: 12px; - margin-bottom: 14px; - padding-bottom: 12px; - border-bottom: 1px dashed var(--vsp-g200); - - h3 { - margin: 0; - font-size: 14px; - font-weight: 700; - color: var(--vsp-g900); - letter-spacing: 0.02em; - } -} - -.vsp-section-ic { - width: 36px; - height: 36px; - border-radius: 10px; - display: inline-flex; - align-items: center; - justify-content: center; - font-size: 14px; - flex-shrink: 0; - - &--blue { - background: var(--vsp-blue-l); - color: var(--vsp-blue); - } - - &--teal { - background: var(--vsp-teal-l); - color: var(--vsp-teal); - } - - &--purple { - background: var(--vsp-purple-l); - color: var(--vsp-purple); - } - - &--indigo { - background: var(--vsp-indigo-l); - color: var(--vsp-indigo); - } - - &--amber { - background: var(--vsp-amber-l); - color: var(--vsp-amber); - } - - &--green { - background: var(--vsp-green-l); - color: var(--vsp-green); - } - - &--red { - background: var(--vsp-red-l); - color: var(--vsp-red); - } - - &--pink { - background: var(--vsp-pink-l); - color: var(--vsp-pink); - } -} - -.vsp-sub-h { - margin: 16px 0 10px; - font-size: 12px; - font-weight: 700; - color: var(--vsp-g700); - text-transform: uppercase; - letter-spacing: 0.06em; -} - -.vsp-grid { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 10px 24px; -} - -@media (width <=600px) { - .vsp-grid { - grid-template-columns: 1fr; - } -} - -.vsp-kv { - display: flex; - flex-direction: column; - gap: 3px; - padding: 8px 0; - border-bottom: 1px dashed var(--vsp-g100); - - &--span { - grid-column: 1 / -1; - } -} - -.vsp-k { - font-size: 10.5px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--vsp-g500); -} - -.vsp-v { - font-size: 13.5px; - color: var(--vsp-g900); - font-weight: 500; - word-break: break-word; - - strong { - font-weight: 700; - } -} - -.vsp-v--accent { - font-size: 18px; - font-weight: 700; - color: var(--vsp-blue); - letter-spacing: 0.01em; -} - -.vsp-suffix { - font-size: 12px; - font-weight: 600; - color: var(--vsp-g500); - margin-left: 4px; -} - -.vsp-table-wrap { - overflow-x: auto; - border: 1px solid var(--vsp-g200); - border-radius: 10px; - margin-top: 4px; -} - -/* ───────── CHART META PILLS ───────── */ -.vsp-chart-meta { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 8px; - margin-bottom: 14px; -} - -.vsp-chart-pill { - display: inline-flex; - align-items: center; - gap: 6px; - font-size: 12px; - font-weight: 600; - padding: 5px 12px; - border-radius: 8px; - background: var(--vsp-g100); - border: 1px solid var(--vsp-g200); - color: var(--vsp-g700); - - fa-icon { - font-size: 11px; - color: var(--vsp-g500); - } - - &--green { - background: var(--vsp-green-l); - border-color: #bbf7d0; - color: var(--vsp-green); - } - - &--red { - background: var(--vsp-red-l); - border-color: #fecaca; - color: var(--vsp-red); - } -} - -/* ───────── RATE BADGE ───────── */ -.vsp-rate-badge { - display: inline-block; - font-size: 13px; - font-weight: 700; - color: var(--vsp-blue); - background: var(--vsp-blue-l); - border: 1px solid #dbeafe; - border-radius: 6px; - padding: 3px 10px; - letter-spacing: 0.02em; -} - -/* ───────── CELL SUB TEXT ───────── */ -.vsp-cell-sub { - display: block; - font-size: 10.5px; - color: var(--vsp-g500); - margin-top: 2px; -} - -/* ───────── INCENTIVE EXPAND ───────── */ -.vsp-incentive-btn { - display: inline-flex; - align-items: center; - gap: 6px; - font-size: 11.5px; - font-weight: 600; - padding: 5px 12px; - border-radius: 8px; - border: 1px solid var(--vsp-g200); - background: var(--vsp-g50); - color: var(--vsp-blue); - cursor: pointer; - font-family: inherit; - transition: all 0.14s ease; - - fa-icon { - font-size: 11px; - } - - &:hover { - background: var(--vsp-blue-l); - border-color: #dbeafe; - } -} - -.vsp-incentive-cell { - padding: 0 !important; -} - -.vsp-incentive-panel { - overflow: hidden; - padding: 0 14px; - background: var(--vsp-g50); - border-top: 1px dashed var(--vsp-g200); -} - -.vsp-incentive-title { - margin: 12px 0 8px; - font-size: 11px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.06em; - color: var(--vsp-g500); -} - -.vsp-table { - width: 100%; - background: #fff; - - th.mat-mdc-header-cell { - background: var(--vsp-g50); - color: var(--vsp-g700); - font-size: 11px; - font-weight: 700; - text-transform: uppercase; - letter-spacing: 0.06em; - padding: 11px 14px; - border-bottom: 1px solid var(--vsp-g200); - white-space: nowrap; - } - - td.mat-mdc-cell { - padding: 10px 14px; - border-bottom: 1px solid var(--vsp-g100); - font-size: 12.5px; - color: var(--vsp-g900); - } - - tbody tr:last-child td.mat-mdc-cell { - border-bottom: 0; - } - - tbody tr:not(.incentives-row, .vsp-row-even) td.mat-mdc-cell { - background: #fff !important; - } - - tbody tr.vsp-row-even td.mat-mdc-cell { - background: var(--vsp-g50) !important; - } - - tr.incentives-row { - height: 0; - - > td { - padding: 0; - } - - .incentives { - overflow: hidden; - } - } -} diff --git a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-general-tab/daily-deposit-scheme-general-tab.component.ts b/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-general-tab/daily-deposit-scheme-general-tab.component.ts deleted file mode 100644 index ca0a551e7..000000000 --- a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-general-tab/daily-deposit-scheme-general-tab.component.ts +++ /dev/null @@ -1,121 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import { Component, inject } from '@angular/core'; -import { ActivatedRoute, RouterLink } from '@angular/router'; -import { trigger, state, transition, animate, style } from '@angular/animations'; -import { FaIconComponent } from '@fortawesome/angular-fontawesome'; -import { MatDivider } from '@angular/material/divider'; - -import { - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow -} from '@angular/material/table'; -import { ViewSavingsAccountingDetailsComponent } from '../../../../shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component'; -import { FindPipe } from '../../../../pipes/find.pipe'; -import { DateFormatPipe } from '../../../../pipes/date-format.pipe'; -import { FormatNumberPipe } from '../../../../pipes/format-number.pipe'; -import { YesnoPipe } from '../../../../pipes/yesno.pipe'; -import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; - -@Component({ - selector: 'mifosx-daily-deposit-scheme-general-tab', - templateUrl: './daily-deposit-scheme-general-tab.component.html', - styleUrls: ['./daily-deposit-scheme-general-tab.component.scss'], - animations: [ - trigger('expandChartSlab', [ - state('collapsed', style({ height: '0px', minHeight: '0' })), - state('expanded', style({ height: '*' })), - transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')) - ]) - ], - imports: [ - ...STANDALONE_SHARED_IMPORTS, - RouterLink, - FaIconComponent, - MatDivider, - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow, - ViewSavingsAccountingDetailsComponent, - FindPipe, - DateFormatPipe, - FormatNumberPipe, - YesnoPipe - ] -}) -export class DailyDepositSchemeGeneralTabComponent { - private route = inject(ActivatedRoute); - - DailyDepositSchemeProduct: any; - DailyDepositSchemeProductTemplate: any; - expandChartSlabIndex: number | null = null; - - chartSlabsIncentivesDisplayedColumns: string[] = ['incentives']; - readonly chartSlabsBaseColumns = [ - 'srNo', - 'description', - 'fromPeriod', - 'toPeriod', - 'periodTypeLabel', - 'annualInterestRate' - ]; - - get chartSlabsDisplayedColumns(): string[] { - const slabs = this.DailyDepositSchemeProduct?.activeChart?.chartSlabs || []; - const hasIncentives = slabs.some((s: any) => s.incentives?.length); - return hasIncentives ? [ - ...this.chartSlabsBaseColumns, - 'actions' - ] : this.chartSlabsBaseColumns; - } - incentivesDisplayedColumns: string[] = [ - 'entityType', - 'attributeName', - 'conditionType', - 'attributeValue', - 'incentiveType', - 'amount' - ]; - chargesDisplayedColumns: string[] = [ - 'name', - 'type', - 'amount', - 'collectedon' - ]; - paymentFundSourceDisplayedColumns: string[] = [ - 'paymentTypeId', - 'fundSourceAccountId' - ]; - feesPenaltyIncomeDisplayedColumns: string[] = [ - 'chargeId', - 'incomeAccountId' - ]; - - constructor() { - this.route.data.subscribe((data: { DailyDepositSchemeProduct: any; DailyDepositSchemeProductsTemplate: any }) => { - this.DailyDepositSchemeProduct = data.DailyDepositSchemeProduct; - this.DailyDepositSchemeProductTemplate = data.DailyDepositSchemeProductsTemplate; - }); - } -} diff --git a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-interest-rates-tab/daily-deposit-scheme-interest-rates-tab.component.html b/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-interest-rates-tab/daily-deposit-scheme-interest-rates-tab.component.html deleted file mode 100644 index 2295a36d2..000000000 --- a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-interest-rates-tab/daily-deposit-scheme-interest-rates-tab.component.html +++ /dev/null @@ -1,68 +0,0 @@ - - -
- @if (!rdInterestRates || rdInterestRates.length === 0) { -

No CBS base interest rate versions defined.

- } - - - @for (version of rdInterestRates; track version.id) { - - - - - {{ version.versionLabel }} - - {{ version.isActive ? 'Active' : 'Inactive' }} - - Effective from: {{ version.effectiveFromDate }} - - - - - @if (version.remarks) { -

- {{ version.remarks }} -

- } - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Sr.{{ slab.srNo }}Particular{{ slab.description }}From{{ slab.fromPeriod }}To{{ slab.toPeriod ?? '—' }}Period Type{{ slab.periodTypeLabel }}Annual Rate (%) - {{ slab.annualInterestRate | number: '1.2-2' }} -
-
- } -
-
diff --git a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-interest-rates-tab/daily-deposit-scheme-interest-rates-tab.component.scss b/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-interest-rates-tab/daily-deposit-scheme-interest-rates-tab.component.scss deleted file mode 100644 index 1728940c9..000000000 --- a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-interest-rates-tab/daily-deposit-scheme-interest-rates-tab.component.scss +++ /dev/null @@ -1,40 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -.rd-version-header { - display: flex; - align-items: center; - gap: 12px; -} - -.rd-active-badge { - background: #4caf50; - color: #fff; - border-radius: 4px; - padding: 2px 8px; - font-size: 12px; -} - -.rd-inactive-badge { - background: #9e9e9e; - color: #fff; - border-radius: 4px; - padding: 2px 8px; - font-size: 12px; -} - -.rd-slab-table { - width: 100%; - margin-top: 8px; -} - -.no-rates { - padding: 24px; - color: #757575; - text-align: center; -} diff --git a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-interest-rates-tab/daily-deposit-scheme-interest-rates-tab.component.ts b/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-interest-rates-tab/daily-deposit-scheme-interest-rates-tab.component.ts deleted file mode 100644 index ef3ce4220..000000000 --- a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-interest-rates-tab/daily-deposit-scheme-interest-rates-tab.component.ts +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import { Component, inject } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; -import { DecimalPipe } from '@angular/common'; -import { - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow -} from '@angular/material/table'; -import { MatExpansionModule } from '@angular/material/expansion'; -import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; - -@Component({ - selector: 'mifosx-rd-interest-rates-tab', - templateUrl: './rd-interest-rates-tab.component.html', - styleUrls: ['./rd-interest-rates-tab.component.scss'], - imports: [ - ...STANDALONE_SHARED_IMPORTS, - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow, - MatExpansionModule, - DecimalPipe - ] -}) -export class DailyDepositSchemeInterestRatesTabComponent { - private route = inject(ActivatedRoute); - - rdInterestRates: any[] = []; - - readonly slabColumns = [ - 'srNo', - 'description', - 'fromPeriod', - 'toPeriod', - 'periodTypeLabel', - 'annualInterestRate' - ]; - - constructor() { - this.route.data.subscribe((data: { rdInterestRates: any[] }) => { - this.rdInterestRates = data.rdInterestRates || []; - }); - } -} diff --git a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-interest-rates-tab/daily-deposit-scheme-interest-rates.resolver.ts b/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-interest-rates-tab/daily-deposit-scheme-interest-rates.resolver.ts deleted file mode 100644 index 68e82a666..000000000 --- a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-interest-rates-tab/daily-deposit-scheme-interest-rates.resolver.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import { inject } from '@angular/core'; -import { ResolveFn } from '@angular/router'; -import { ProductsService } from '../../../products.service'; - -export const rdInterestRatesResolver: ResolveFn = (route) => { - const productsService = inject(ProductsService); - return productsService.getRdBaseInterestRates(); -}; diff --git a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/view-daily-deposit-scheme-product.component.html b/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/view-daily-deposit-scheme-product.component.html deleted file mode 100644 index 041cff962..000000000 --- a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/view-daily-deposit-scheme-product.component.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - - - - - - diff --git a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/view-daily-deposit-scheme-product.component.scss b/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/view-daily-deposit-scheme-product.component.scss deleted file mode 100644 index 74229104a..000000000 --- a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/view-daily-deposit-scheme-product.component.scss +++ /dev/null @@ -1,22 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -.product-card { - margin: 0 auto; - max-width: 80rem; - width: 90%; - padding: 0; - - .navigation-tabs { - overflow: auto; - } - - i:hover { - cursor: pointer; - } -} diff --git a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/view-daily-deposit-scheme-product.component.ts b/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/view-daily-deposit-scheme-product.component.ts deleted file mode 100644 index 5e871f560..000000000 --- a/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/view-daily-deposit-scheme-product.component.ts +++ /dev/null @@ -1,44 +0,0 @@ -/** - * Copyright since 2025 Mifos Initiative - * - * This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. - */ - -import { Component, inject } from '@angular/core'; -import { ActivatedRoute, RouterLinkActive, RouterLink, RouterOutlet, Router } from '@angular/router'; -import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs'; -import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; - -@Component({ - selector: 'mifosx-view-daily-deposit-scheme-product', - templateUrl: './view-daily-deposit-scheme-product.component.html', - styleUrls: ['./view-daily-deposit-scheme-product.component.scss'], - imports: [ - ...STANDALONE_SHARED_IMPORTS, - MatTabNav, - MatTabLink, - RouterLinkActive, - RouterLink, - MatTabNavPanel, - RouterOutlet - ] -}) -export class ViewDailyDepositSchemeProductComponent { - private route = inject(ActivatedRoute); - private router = inject(Router); - - recurringDepositDatatables: any = []; - - constructor() { - this.route.data.subscribe((data: { recurringDepositDatatables: any }) => { - this.recurringDepositDatatables = []; - data.recurringDepositDatatables.forEach((datatable: any) => { - if (datatable.entitySubType === 'Daily Deposit Scheme') { - this.recurringDepositDatatables.push(datatable); - } - }); - }); - } -} diff --git a/src/app/products/dds-account-types/create-dds-account-type/create-dds-account-type.component.html b/src/app/products/dds-account-types/create-dds-account-type/create-dds-account-type.component.html new file mode 100644 index 000000000..761aa3737 --- /dev/null +++ b/src/app/products/dds-account-types/create-dds-account-type/create-dds-account-type.component.html @@ -0,0 +1,126 @@ + + +
+
+
+
+
+

Account Type Creation

+

{{ isEdit ? 'Update this Daily Deposit scheme variant.' : 'Register a Daily Deposit scheme variant.' }}

+
+
+ +
+
+ + Account Type Group + + @for (group of accountTypeGroups; track group) { + {{ group }} + } + + + + + A/c.Type + + + + + A/c. Type Description + + + + + Auto Interest Post + + Yes + No + + + + + Auto Interest Post When + + @for (option of autoInterestPostWhenOptions; track option) { + {{ option }} + } + + + + + @if (autoPostOn) { + + Auto Interest Post Day + + + } + + + + Maturity Auto Close + + Yes + No + + Closes the account on its maturity date, if it names a savings account to be paid into + +
+ + +

Premature Closure

+

+ Applied when an account on this scheme is closed before its term ends. The branch chooses at the time + whether the customer or the agent bears the cost; these set what that costs. +

+ +
+ + Minimum Lock-in (months) + + Before this, early closure is refused outright. 0 allows it from day one. + + + + Customer: interest rate reduced by (%) + + Taken off the rate for the whole period, not only the part left to run. + + + + Agent: commission reduced by (%) + + Charged whoever bears the interest — the round was not kept going. + +
+ +
+ + +
+
+
+
diff --git a/src/app/products/dds-account-types/create-dds-account-type/create-dds-account-type.component.scss b/src/app/products/dds-account-types/create-dds-account-type/create-dds-account-type.component.scss new file mode 100644 index 000000000..67afb70be --- /dev/null +++ b/src/app/products/dds-account-types/create-dds-account-type/create-dds-account-type.component.scss @@ -0,0 +1,112 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +.dat-root { + padding: 20px 24px 40px; +} + +.dat-card { + background: #fff; + border: 1px solid rgb(0 0 0 / 8%); + border-radius: 10px; + box-shadow: 0 1px 3px rgb(0 0 0 / 6%); + overflow: hidden; + max-width: 880px; + margin: 0 auto; +} + +.dat-head { + display: flex; + align-items: center; + gap: 14px; + padding: 20px 24px; + border-bottom: 1px solid rgb(0 0 0 / 7%); + + h2 { + margin: 0; + font-size: 1.2rem; + font-weight: 600; + } + + p { + margin: 2px 0 0; + font-size: 0.83rem; + color: rgb(0 0 0 / 55%); + } +} + +.dat-head__ic { + width: 46px; + height: 46px; + border-radius: 12px; + display: grid; + place-items: center; + font-size: 1.05rem; + color: #fff; + background: linear-gradient(135deg, #0f766e, #134e4a); + flex: none; +} + +.dat-grid { + display: grid; + grid-template-columns: repeat(2, minmax(220px, 1fr)); + gap: 8px 20px; + padding: 26px 24px 8px; +} + +.dat-field { + width: 100%; +} + +.dat-field--wide { + grid-column: 1 / -1; +} + +/* + * Divides the scheme's own settings from the terms it charges for closing early. A rule rather than a + * second card, because it is the same record being filled in - the break is to stop three money + * figures reading as more of the posting settings above them. + */ +.dat-section { + margin: 20px 0 0; + padding: 18px 24px 0; + border-top: 1px solid rgb(0 0 0 / 7%); + font-size: 0.82rem; + font-weight: 600; + letter-spacing: 0.04em; + text-transform: uppercase; + color: #0f766e; +} + +.dat-section-note { + margin: 6px 0 0; + padding: 0 24px; + max-width: 68ch; + font-size: 0.8rem; + line-height: 1.5; + color: rgb(0 0 0 / 60%); +} + +/* The note already carries the spacing, so the grid under it does not add its own again. */ +.dat-section-note + .dat-grid { + padding-top: 14px; +} + +.dat-actions { + display: flex; + justify-content: flex-end; + gap: 12px; + padding: 16px 24px; + border-top: 1px solid rgb(0 0 0 / 7%); +} + +@media (width <= 720px) { + .dat-grid { + grid-template-columns: 1fr; + } +} diff --git a/src/app/products/dds-account-types/create-dds-account-type/create-dds-account-type.component.ts b/src/app/products/dds-account-types/create-dds-account-type/create-dds-account-type.component.ts new file mode 100644 index 000000000..dc7911ba3 --- /dev/null +++ b/src/app/products/dds-account-types/create-dds-account-type/create-dds-account-type.component.ts @@ -0,0 +1,210 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** Angular Imports */ +import { Component, OnInit, inject } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** Custom Services */ +import { ProductsService } from 'app/products/products.service'; +import { AlertService } from 'app/core/alert/alert.service'; +import { SettingsService } from 'app/settings/settings.service'; + +/** + * Account Type Creation — the legacy DDS screen this replaces, field for field: group, code, + * description, and whether interest posts automatically and how often. Nothing else: DDS never used + * the deposit-product engine, so there are no terms, charges or accounting tabs behind this. + * + * The same form handles create and edit; an `accountTypeId` in the route switches it to edit. + */ +@Component({ + selector: 'mifosx-create-dds-account-type', + templateUrl: './create-dds-account-type.component.html', + styleUrls: ['./create-dds-account-type.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent + ] +}) +export class CreateDdsAccountTypeComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private router = inject(Router); + private route = inject(ActivatedRoute); + private productsService = inject(ProductsService); + private alertService = inject(AlertService); + private settingsService = inject(SettingsService); + + accountTypeGroups = ['DDS']; + + autoInterestPostWhenOptions = [ + 'Monthly', + 'Quarterly', + 'Half Yearly', + 'Yearly' + ]; + + /** Set when the route carries an id — the form is then editing rather than creating. */ + accountTypeId: number | null = null; + + accountTypeForm: UntypedFormGroup = this.formBuilder.group({ + accountTypeGroup: [ + 'DDS', + Validators.required + ], + accountTypeCode: [ + '', + Validators.required + ], + accountTypeDescription: [ + '', + Validators.required + ], + autoInterestPost: [ + true, + Validators.required + ], + autoInterestPostWhen: [ + 'Monthly', + Validators.required + ], + autoInterestPostDay: [''], + /* + * Defaults to No, so a scheme created without a thought about it keeps behaving the way every + * scheme behaves today - the clerk pays out matured accounts by hand. Turning it on is a + * decision somebody makes, not one they inherit. + */ + maturityAutoClose: [ + false, + Validators.required + ], + /* + * What this scheme charges when an account is closed before its term is up. Set here rather than + * asked of the clerk at closing time: the terms are the bank's, and the same for every account on + * the scheme. Defaults are the bank's stated terms - 12 months, 1% and 1%. + */ + prematureLockinMonths: [ + 12, + [ + Validators.required, + Validators.min(0), + Validators.max(600) + ] + ], + prematureInterestReduction: [ + 1, + [ + Validators.required, + Validators.min(0), + Validators.max(100) + ] + ], + prematureAgentCommissionPenalty: [ + 1, + [ + Validators.required, + Validators.min(0), + Validators.max(100) + ] + ] + }); + + get isEdit(): boolean { + return this.accountTypeId !== null; + } + + /** + * "Auto Interest Post Day" only means anything while auto-posting is on, and the legacy screen + * hid it the same way. "When" stays visible either way, as it did there. + */ + get autoPostOn(): boolean { + return !!this.accountTypeForm.get('autoInterestPost')?.value; + } + + ngOnInit(): void { + // The posting day is required exactly while auto-posting is on, and is cleared when it is off. + const dayControl = this.accountTypeForm.get('autoInterestPostDay'); + const applyDayRule = (autoPostOn: boolean) => { + if (autoPostOn) { + dayControl?.setValidators([Validators.required]); + } else { + dayControl?.clearValidators(); + dayControl?.setValue(''); + } + dayControl?.updateValueAndValidity(); + }; + applyDayRule(this.autoPostOn); + this.accountTypeForm.get('autoInterestPost')?.valueChanges.subscribe((value) => applyDayRule(!!value)); + + const routeId = this.route.snapshot.paramMap.get('accountTypeId'); + if (!routeId) { + return; + } + this.accountTypeId = Number(routeId); + this.productsService.getDdsAccountType(this.accountTypeId).subscribe((accountType: any) => { + this.accountTypeForm.patchValue({ + accountTypeGroup: accountType.accountTypeGroup, + accountTypeCode: accountType.accountTypeCode, + accountTypeDescription: accountType.accountTypeDescription, + autoInterestPost: !!accountType.autoInterestPost, + autoInterestPostWhen: accountType.autoInterestPostWhen || 'Monthly', + autoInterestPostDay: accountType.autoInterestPostDay ?? '', + maturityAutoClose: !!accountType.maturityAutoClose, + // ?? rather than ||, because zero is a real answer here - a scheme with no lock-in, or one + // that charges nothing - and must not be replaced by the default. + prematureLockinMonths: accountType.prematureLockinMonths ?? 12, + prematureInterestReduction: accountType.prematureInterestReduction ?? 1, + prematureAgentCommissionPenalty: accountType.prematureAgentCommissionPenalty ?? 1 + }); + }); + } + + cancel(): void { + this.backToList(); + } + + submit(): void { + if (this.accountTypeForm.invalid) { + return; + } + const raw = this.accountTypeForm.getRawValue(); + const payload = { + ...raw, + // The posting day only applies while auto-posting is on; send nothing for it otherwise. + autoInterestPostDay: raw.autoInterestPost && raw.autoInterestPostDay ? Number(raw.autoInterestPostDay) : undefined, + // Sent as numbers, not the strings a text input yields - the backend reads them as decimals. + prematureLockinMonths: Number(raw.prematureLockinMonths), + prematureInterestReduction: Number(raw.prematureInterestReduction), + prematureAgentCommissionPenalty: Number(raw.prematureAgentCommissionPenalty), + // Numeric parameters have to carry the locale they were entered in. + locale: this.settingsService.language.code + }; + + const done = () => { + this.alertService.alert({ + type: 'Success', + message: this.isEdit + ? `Account type ${payload.accountTypeCode} updated` + : `Account type ${payload.accountTypeCode} created` + }); + this.backToList(); + }; + + if (this.accountTypeId !== null) { + this.productsService.updateDdsAccountType(this.accountTypeId, payload).subscribe(done); + } else { + this.productsService.createDdsAccountType(payload).subscribe(done); + } + } + + private backToList(): void { + this.router.navigate(['/products/dds-account-types']); + } +} diff --git a/src/app/products/dds-account-types/dds-account-types.component.html b/src/app/products/dds-account-types/dds-account-types.component.html new file mode 100644 index 000000000..5fbea59c9 --- /dev/null +++ b/src/app/products/dds-account-types/dds-account-types.component.html @@ -0,0 +1,141 @@ + + +
+
+
+
+
+

DDS Account Types

+

Scheme variants a Daily Deposit account can be opened under.

+
+
+
+
+ + {{ totalCount }} + {{ totalCount === 1 ? 'type' : 'types' }} +
+ +
+
+ +
+
+
+
+
{{ totalCount | number }}
+
Total Account Types
+
+
+
+
+
+
{{ filteredCount | number }}
+
Showing after filter
+
+
+
+ +
+
+ +
+ + @if (totalCount === 0) { +
+
+

No account types yet

+

Create one so Daily Deposit accounts can be opened against it.

+ +
+ } @else { +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
A/c Type +
+
+
+
{{ t.accountTypeCode }}
+
{{ t.accountTypeGroup }}
+
+
+
Description{{ t.accountTypeDescription }}Auto Interest Post + @if (t.autoInterestPost) { + {{ t.autoInterestPostWhen || 'Yes' }} + @if (t.autoInterestPostDay) { + day {{ t.autoInterestPostDay }} + } + } @else { + No + } + Created + {{ t.createdDate ? (t.createdDate | dateFormat: 'DD MMM YYYY, hh:mm A') : '—' }} + Actions + +
+ + @if (filteredCount === 0 && totalCount > 0) { +
+ + No account types match your filter. +
+ } + + +
+ } +
+
diff --git a/src/app/products/dds-account-types/dds-account-types.component.scss b/src/app/products/dds-account-types/dds-account-types.component.scss new file mode 100644 index 000000000..9d6517e68 --- /dev/null +++ b/src/app/products/dds-account-types/dds-account-types.component.scss @@ -0,0 +1,39 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +@use 'assets/styles/product-list' as pl; + +@include pl.product-list-styles; + +/* DDS-specific teal accent */ +:host { + --pl-accent: #0f766e; + --pl-accent-l: #ccfbf1; +} + +/* A retired account type stays listed but reads as switched off. */ +.dat-row--inactive td:not(.num) { + opacity: 0.55; +} + +.dat-pill { + display: inline-block; + padding: 3px 11px; + border-radius: 11px; + font-size: 0.72rem; + font-weight: 500; + line-height: 1.7; + background: #dcfce7; + color: #15803d; + white-space: nowrap; +} + +.dat-pill--off { + background: rgb(0 0 0 / 7%); + color: rgb(0 0 0 / 52%); +} diff --git a/src/app/products/dds-account-types/dds-account-types.component.ts b/src/app/products/dds-account-types/dds-account-types.component.ts new file mode 100644 index 000000000..9b3e13e57 --- /dev/null +++ b/src/app/products/dds-account-types/dds-account-types.component.ts @@ -0,0 +1,112 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +/** Angular Imports */ +import { Component, OnInit, ViewChild, inject } from '@angular/core'; +import { MatPaginator } from '@angular/material/paginator'; +import { MatSort, MatSortHeader } from '@angular/material/sort'; +import { + MatTable, + MatTableDataSource, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow +} from '@angular/material/table'; +import { MatTooltip } from '@angular/material/tooltip'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** Custom Services */ +import { ProductsService } from 'app/products/products.service'; + +/** + * DDS Account Types list. + * + * The scheme variants a Party account can be opened under - the legacy "Account Type Creation" + * record. Master data only, deliberately not a deposit product: DDS never used the deposit-product + * engine, and interest is accrued off each Party's own rate by the DDS batch job. + * + * Follows the same list -> create/edit shape as every other product list in this section, and + * shares their stylesheet. + */ +@Component({ + selector: 'mifosx-dds-account-types', + templateUrl: './dds-account-types.component.html', + styleUrls: ['./dds-account-types.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent, + MatTable, + MatSort, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatSortHeader, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, + MatPaginator, + MatTooltip + ] +}) +export class DdsAccountTypesComponent implements OnInit { + private productsService = inject(ProductsService); + + displayedColumns: string[] = [ + 'accountTypeCode', + 'accountTypeDescription', + 'autoInterestPost', + 'createdDate', + 'actions' + ]; + + dataSource: MatTableDataSource = new MatTableDataSource([]); + accountTypes: any[] = []; + filteredCount = 0; + + @ViewChild(MatPaginator, { static: true }) paginator: MatPaginator; + @ViewChild(MatSort, { static: true }) sort: MatSort; + + get totalCount(): number { + return this.accountTypes.length; + } + + ngOnInit(): void { + this.loadAccountTypes(); + } + + private loadAccountTypes(): void { + this.productsService.getDdsAccountTypes().subscribe((accountTypes: any[]) => { + this.accountTypes = accountTypes || []; + this.dataSource = new MatTableDataSource(this.accountTypes); + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + this.filteredCount = this.accountTypes.length; + this.dataSource.connect().subscribe((rows) => (this.filteredCount = rows.length)); + }); + } + + applyFilter(filterValue: string): void { + this.dataSource.filter = filterValue.trim().toLowerCase(); + } + + /** + * Retires or restores an account type. Never a delete: Parties are opened against an account + * type, so removing it would orphan every account already using it. Deactivating takes it out of + * the pick lists for new accounts and leaves existing ones untouched. + */ +} diff --git a/src/app/products/products-routing.module.ts b/src/app/products/products-routing.module.ts index 9c0111bef..86c35731d 100644 --- a/src/app/products/products-routing.module.ts +++ b/src/app/products/products-routing.module.ts @@ -30,12 +30,8 @@ import { EditShareProductComponent } from './share-products/edit-share-product/e import { ManageTaxConfigurationsComponent } from './manage-tax-configurations/manage-tax-configurations.component'; import { RecurringDepositProductsComponent } from './recurring-deposit-products/recurring-deposit-products.component'; import { ViewRecurringDepositProductComponent } from './recurring-deposit-products/view-recurring-deposit-product/view-recurring-deposit-product.component'; -import { DailyDepositSchemeProductsComponent } from './daily-deposit-scheme-products/daily-deposit-scheme-products.component'; -import { CreateDailyDepositSchemeProductComponent } from './daily-deposit-scheme-products/create-daily-deposit-scheme-product/create-daily-deposit-scheme-product.component'; -import { ViewDailyDepositSchemeProductComponent } from './daily-deposit-scheme-products/view-daily-deposit-scheme-product/view-daily-deposit-scheme-product.component'; -import { DailyDepositSchemeGeneralTabComponent } from './daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-general-tab/daily-deposit-scheme-general-tab.component'; -import { DailyDepositSchemeDatatableTabComponent } from './daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-datatable-tab/daily-deposit-scheme-datatable-tab.component'; -import { EditDailyDepositSchemeProductComponent } from './daily-deposit-scheme-products/edit-daily-deposit-scheme-product/edit-daily-deposit-scheme-product.component'; +import { DdsAccountTypesComponent } from './dds-account-types/dds-account-types.component'; +import { CreateDdsAccountTypeComponent } from './dds-account-types/create-dds-account-type/create-dds-account-type.component'; import { ChargesComponent } from './charges/charges.component'; import { ViewChargeComponent } from './charges/view-charge/view-charge.component'; import { CreateChargeComponent } from './charges/create-charge/create-charge.component'; @@ -81,10 +77,6 @@ import { ShareProductResolver } from './share-products/share-product-resolver'; import { ShareProductAndTemplateResolver } from './share-products/edit-share-product/share-product-and-template.resolver'; import { RecurringDepositProductsResolver } from './recurring-deposit-products/recurring-deposit-products.resolver'; import { RecurringDepositProductResolver } from './recurring-deposit-products/recurring-deposit-product.resolver'; -import { DailyDepositSchemeProductsResolver } from './daily-deposit-scheme-products/daily-deposit-scheme-products.resolver'; -import { DailyDepositSchemeProductsTemplateResolver } from './daily-deposit-scheme-products/create-daily-deposit-scheme-product/daily-deposit-scheme-products-template.resolver'; -import { DailyDepositSchemeProductResolver } from './daily-deposit-scheme-products/daily-deposit-scheme-product.resolver'; -import { DailyDepositSchemeProductAndTemplateResolver } from './daily-deposit-scheme-products/edit-daily-deposit-scheme-product/daily-deposit-scheme-product-and-template.resolver'; import { ChargesResolver } from './charges/charges.resolver'; import { ChargeResolver } from './charges/charge.resolver'; import { ChargesTemplateResolver } from './charges/charges-template.resolver'; @@ -735,79 +727,22 @@ const routes: Routes = [ ] }, { - path: 'daily-deposit-scheme-products', - data: { title: 'Daily Deposit Scheme Products', breadcrumb: 'Daily Deposit Scheme Products' }, + path: 'dds-account-types', + data: { title: 'DDS Account Types', breadcrumb: 'DDS Account Types' }, children: [ - { - path: 'create', - component: CreateDailyDepositSchemeProductComponent, - data: { title: 'Create Daily Deposit Scheme Product', breadcrumb: 'Create' }, - resolve: { - dailyDepositSchemeProductsTemplate: DailyDepositSchemeProductsTemplateResolver - } - }, { path: '', - component: DailyDepositSchemeProductsComponent, - resolve: { - dailyDepositSchemeProducts: DailyDepositSchemeProductsResolver - } + component: DdsAccountTypesComponent }, { - path: ':productId', - data: { - title: 'View Daily Deposit Scheme Product', - breadcrumb: 'productId', - routeParamBreadcrumb: 'productId' - }, - resolve: { - recurringDepositDatatables: SavingProductDatatablesResolver - }, - children: [ - { - path: '', - component: ViewDailyDepositSchemeProductComponent, - children: [ - { - path: '', - redirectTo: 'general', - pathMatch: 'full' - }, - { - path: 'general', - component: DailyDepositSchemeGeneralTabComponent, - resolve: { - dailyDepositSchemeProduct: DailyDepositSchemeProductResolver - } - }, - { - path: 'datatables', - children: [ - { - path: ':datatableName', - component: DailyDepositSchemeDatatableTabComponent, - data: { title: 'Data Table View', routeParamBreadcrumb: 'datatableName' }, - resolve: { - recurringDepositDatatable: SavingProductDatatableResolver - } - } - ] - } - ] - }, - { - path: 'edit', - data: { - title: 'Edit Daily Deposit Scheme Product', - breadcrumb: 'edit', - routeParamBreadcrumb: false - }, - component: EditDailyDepositSchemeProductComponent, - resolve: { - DailyDepositSchemeProductAndTemplate: DailyDepositSchemeProductAndTemplateResolver - } - } - ] + path: 'create', + component: CreateDdsAccountTypeComponent, + data: { title: 'Create Account Type', breadcrumb: 'Create' } + }, + { + path: ':accountTypeId/edit', + component: CreateDdsAccountTypeComponent, + data: { title: 'Edit Account Type', breadcrumb: 'Edit', routeParamBreadcrumb: false } } ] }, @@ -1091,10 +1026,6 @@ const routes: Routes = [ ShareProductAndTemplateResolver, RecurringDepositProductsResolver, RecurringDepositProductResolver, - DailyDepositSchemeProductsResolver, - DailyDepositSchemeProductsTemplateResolver, - DailyDepositSchemeProductResolver, - DailyDepositSchemeProductAndTemplateResolver, ChargesResolver, ChargeResolver, ChargesTemplateAndResolver, diff --git a/src/app/products/products.component.html b/src/app/products/products.component.html index 07d63d8db..aa9618577 100644 --- a/src/app/products/products.component.html +++ b/src/app/products/products.component.html @@ -125,20 +125,14 @@
-
- +
+
-

{{ 'labels.heading.Daily Deposit Scheme Products' | translate }}

-

- {{ 'labels.text.Add, modify or inactivate a Daily Deposit Scheme product' | translate }} -

+

DDS Account Types

+

Create a Daily Deposit Scheme account type — group, code and auto-interest posting

diff --git a/src/app/products/products.service.ts b/src/app/products/products.service.ts index c986955bc..f7b29aa21 100644 --- a/src/app/products/products.service.ts +++ b/src/app/products/products.service.ts @@ -235,6 +235,43 @@ export class ProductsService { return this.http.get('/recurringdepositproducts/template'); } + /** + * DDS Account Types — the legacy "Account Type Creation" master (group / code / description / + * auto-interest-post). Master data only, deliberately not a deposit product: DDS accrues + * interest off each Party's own rate, so there is no product engine behind this. + * + * @returns {Observable} DDS account types + */ + getDdsAccountTypes(): Observable { + return this.http.get('/dds/accounttypes'); + } + + /** + * @param {string} accountTypeId DDS account type id + * @returns {Observable} A single DDS account type + */ + getDdsAccountType(accountTypeId: string | number): Observable { + return this.http.get(`/dds/accounttypes/${accountTypeId}`); + } + + /** + * @param {any} accountType DDS account type to create + * @returns {Observable} + */ + createDdsAccountType(accountType: any): Observable { + return this.http.post('/dds/accounttypes', accountType); + } + + /** + * @param {string} accountTypeId DDS account type id + * @param {any} accountType Changes to apply + * @returns {Observable} + */ + updateDdsAccountType(accountTypeId: string | number, accountType: any): Observable { + return this.http.put(`/dds/accounttypes/${accountTypeId}`, accountType); + } + + /** * @returns {Observable} Charges data. */ diff --git a/src/app/system/configurations/global-configurations-tab/global-configurations-tab.component.html b/src/app/system/configurations/global-configurations-tab/global-configurations-tab.component.html index c4c147847..4128299e6 100644 --- a/src/app/system/configurations/global-configurations-tab/global-configurations-tab.component.html +++ b/src/app/system/configurations/global-configurations-tab/global-configurations-tab.component.html @@ -53,7 +53,18 @@ {{ 'labels.inputs.Value' | translate }} - {{ configuration.value }} + + {{ configuration.value }} + + @if (glAccountLabel(configuration); as glAccount) { + {{ glAccount }} + } + diff --git a/src/app/system/configurations/global-configurations-tab/global-configurations-tab.component.scss b/src/app/system/configurations/global-configurations-tab/global-configurations-tab.component.scss index 4561ef8a6..9141035e4 100644 --- a/src/app/system/configurations/global-configurations-tab/global-configurations-tab.component.scss +++ b/src/app/system/configurations/global-configurations-tab/global-configurations-tab.component.scss @@ -9,3 +9,14 @@ table { width: 100%; } + +/* + * The ledger account a setting points at. Quieter than the value itself, because the id is what the + * setting actually holds and what gets typed when it is edited - the name is here to say what that + * id means, not to replace it. + */ +.gc-gl-name { + margin-left: 8px; + color: rgb(0 0 0 / 60%); + font-size: 0.82rem; +} diff --git a/src/app/system/configurations/global-configurations-tab/global-configurations-tab.component.ts b/src/app/system/configurations/global-configurations-tab/global-configurations-tab.component.ts index 80a29ad45..a64e47121 100644 --- a/src/app/system/configurations/global-configurations-tab/global-configurations-tab.component.ts +++ b/src/app/system/configurations/global-configurations-tab/global-configurations-tab.component.ts @@ -29,6 +29,7 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router'; /** Custom Services */ import { SystemService } from '../../system.service'; +import { AccountingService } from 'app/accounting/accounting.service'; import { PopoverService } from '../../../configuration-wizard/popover/popover.service'; import { ConfigurationWizardService } from '../../../configuration-wizard/configuration-wizard.service'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; @@ -71,9 +72,22 @@ export class GlobalConfigurationsTabComponent implements OnInit, AfterViewInit { private router = inject(Router); private configurationWizardService = inject(ConfigurationWizardService); private popoverService = inject(PopoverService); + private accountingService = inject(AccountingService); /** Configuration data. */ configurationData: any; + + /** + * The chart of accounts, so a setting that holds a ledger account's id can be shown by its name. + * + * Several settings store nothing but a number - "44" says which account the money moves through, + * and says it only to the database. An officer checking that the scheme is posting where it should + * would otherwise have to carry the number to the Chart of Accounts and look it up. + * + * Left empty if the list cannot be read - a user without permission on the chart of accounts still + * sees every setting and its value, just without the name beside it. + */ + private glAccountsById = new Map(); /** Columns to be displayed in configurations table. */ displayedColumns: string[] = [ 'name', @@ -119,6 +133,36 @@ export class GlobalConfigurationsTabComponent implements OnInit, AfterViewInit { */ ngOnInit() { this.setConfigurationData(); + this.loadGlAccounts(); + } + + private loadGlAccounts(): void { + this.accountingService.getGlAccounts().subscribe({ + next: (accounts: any) => { + for (const account of accounts || []) { + this.glAccountsById.set(Number(account.id), account); + } + }, + // The settings are the point of this screen; a chart of accounts that will not load must not + // take them down with it. The values still show, only the names beside them are missing. + error: () => this.glAccountsById.clear() + }); + } + + /** + * The ledger account a setting points at, as "name (code)" - the same way the product screens show + * one. Empty for every setting that does not hold a ledger account id, which is most of them. + * + * Only settings whose name marks them as an account mapping are resolved. Reading every numeric + * setting as an account id would put a name against figures that are nothing of the sort - a + * retry count of 3 would come back as whatever account 3 happens to be. + */ + glAccountLabel(configuration: any): string { + if (!configuration?.name?.endsWith('-account')) { + return ''; + } + const account = this.glAccountsById.get(Number(configuration.value)); + return account ? `${account.name} (${account.glCode})` : ''; } /** diff --git a/src/environments/.env.ts b/src/environments/.env.ts index fd0d2a130..0f8cfa339 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -9,8 +9,8 @@ /* tslint:disable */ export default { 'mifos_x': { - 'version': '260813', - 'hash': '5a6fb1f9' + 'version': '260824', + 'hash': '7ef75c90' }, 'allow_switching_backend_instance': true };