From e2eff9914f04de2ac38d69294423fbc65764c7ad Mon Sep 17 00:00:00 2001 From: tarunmalviya Date: Wed, 23 Sep 2026 11:54:49 +0530 Subject: [PATCH] DDS: agent collection entry screen, per-agent GL account, maturity auto close The Daily Transaction Entry screen no longer composes a Party Tran. An agent records their own round elsewhere; this screen reads back what they wrote down, shows what it adds up to, and settles the day once a clerk has counted the cash against it. Nothing is typed here and nothing is edited - what settles is what the entries say, to the rupee. Agent Master now asks which ledger account the agent's collections pass through, one agent to an account, and offers only the accounts that can carry them. The DDS scheme product is kept out of the agent's own savings list for the same reason the server refuses it: debits are blocked on those accounts, so commission paid into one would go in and never come out. The DDS product carries a Maturity Auto Close setting, asked as Yes/No to match the Auto Interest Post question directly above it, and Batch Processing offers the run that applies it. Also: the report viewer stops printing two money columns ungrouped and stops totalling a column that holds a word, and an agent's birth date cannot be picked later than eighteen years ago. Co-Authored-By: Claude Opus 5 (1M context) --- package-lock.json | 5437 +---------------- src/app/accounts/accounts-routing.module.ts | 24 +- src/app/core/shell/shell.component.html | 6 +- .../daily-transaction-entry.component.html | 191 +- .../daily-transaction-entry.component.scss | 214 +- .../daily-transaction-entry.component.ts | 621 +- .../day-collection-dialog.component.html | 58 + .../day-collection-dialog.component.scss | 42 + .../day-collection-dialog.component.ts | 117 + .../notice-dialog.component.html | 21 + .../notice-dialog.component.scss | 44 + .../notice-dialog/notice-dialog.component.ts | 39 + .../report-viewer.component.html | 144 +- .../report-viewer.component.scss | 87 + .../report-viewer/report-viewer.component.ts | 262 +- .../party-master/CBS-Backend.code-workspace | 11 + .../create-party/create-party.component.html | 975 +++ .../create-party/create-party.component.scss | 526 ++ .../create-party/create-party.component.ts | 1764 ++++++ .../due-for-closure.component.html | 163 + .../due-for-closure.component.scss | 97 + .../due-for-closure.component.ts | 194 + .../nominee-list-dialog.component.html | 73 + .../nominee-list-dialog.component.scss | 84 + .../nominee-list-dialog.component.ts | 82 + .../party-master/party-master.component.html | 739 +-- .../party-master/party-master.component.scss | 257 +- .../party-master/party-master.component.ts | 620 +- .../payout-dialog.component.html | 79 + .../payout-dialog.component.scss | 92 + .../payout-dialog/payout-dialog.component.ts | 109 + .../premature-close-dialog.component.html | 118 + .../premature-close-dialog.component.scss | 112 + .../premature-close-dialog.component.ts | 136 + src/app/directives/directives.module.ts | 7 +- .../directives/restrict-input.directive.ts | 137 + .../agent-master/agent-master.component.html | 495 +- .../agent-master/agent-master.component.scss | 156 +- .../agent-master/agent-master.component.ts | 449 +- .../create-agent/create-agent.component.html | 253 + .../create-agent/create-agent.component.scss | 146 + .../create-agent/create-agent.component.ts | 593 ++ .../commission-slab-master.component.html | 96 +- .../commission-slab-master.component.scss | 54 +- .../commission-slab-master.component.ts | 185 +- .../dds-batch-processing.component.html | 245 +- .../dds-batch-processing.component.scss | 118 +- .../dds-batch-processing.component.ts | 322 +- .../dds-batch-processing/dds-date-adapter.ts | 71 + .../organization-routing.module.ts | 20 +- src/app/organization/organization.service.ts | 133 + ...aily-deposit-scheme-product.component.html | 131 - ...aily-deposit-scheme-product.component.scss | 138 - ...-daily-deposit-scheme-product.component.ts | 346 -- ...posit-scheme-products-template.resolver.ts | 35 - ...eme-product-accounting-step.component.html | 121 - ...eme-product-accounting-step.component.scss | 303 - ...cheme-product-accounting-step.component.ts | 423 -- ...scheme-product-charges-step.component.html | 85 - ...scheme-product-charges-step.component.scss | 69 - ...t-scheme-product-charges-step.component.ts | 103 - ...cheme-product-currency-step.component.html | 89 - ...cheme-product-currency-step.component.scss | 102 - ...-scheme-product-currency-step.component.ts | 104 - ...scheme-product-details-step.component.html | 77 - ...scheme-product-details-step.component.scss | 81 - ...t-scheme-product-details-step.component.ts | 177 - ...ct-interest-rate-chart-step.component.html | 109 - ...ct-interest-rate-chart-step.component.scss | 215 - ...duct-interest-rate-chart-step.component.ts | 717 --- ...scheme-product-preview-step.component.html | 732 --- ...scheme-product-preview-step.component.scss | 131 - ...t-scheme-product-preview-step.component.ts | 142 - ...cheme-product-settings-step.component.html | 123 - ...cheme-product-settings-step.component.scss | 164 - ...-scheme-product-settings-step.component.ts | 95 - ...t-scheme-product-terms-step.component.html | 192 - ...t-scheme-product-terms-step.component.scss | 151 - ...sit-scheme-product-terms-step.component.ts | 169 - .../daily-deposit-scheme-product.resolver.ts | 37 - ...ily-deposit-scheme-products.component.html | 172 - ...ily-deposit-scheme-products.component.scss | 17 - ...daily-deposit-scheme-products.component.ts | 124 - .../daily-deposit-scheme-products.resolver.ts | 35 - ...it-scheme-product-and-template.resolver.ts | 35 - ...aily-deposit-scheme-product.component.html | 202 - ...aily-deposit-scheme-product.component.scss | 138 - ...-daily-deposit-scheme-product.component.ts | 305 - ...eposit-scheme-datatable-tab.component.html | 17 - ...eposit-scheme-datatable-tab.component.scss | 7 - ...-deposit-scheme-datatable-tab.component.ts | 38 - ...-deposit-scheme-general-tab.component.html | 843 --- ...-deposit-scheme-general-tab.component.scss | 528 -- ...ly-deposit-scheme-general-tab.component.ts | 121 - ...t-scheme-interest-rates-tab.component.html | 68 - ...t-scheme-interest-rates-tab.component.scss | 40 - ...sit-scheme-interest-rates-tab.component.ts | 66 - ...-deposit-scheme-interest-rates.resolver.ts | 16 - ...aily-deposit-scheme-product.component.html | 39 - ...aily-deposit-scheme-product.component.scss | 22 - ...-daily-deposit-scheme-product.component.ts | 44 - .../create-dds-account-type.component.html | 126 + .../create-dds-account-type.component.scss | 112 + .../create-dds-account-type.component.ts | 210 + .../dds-account-types.component.html | 141 + .../dds-account-types.component.scss | 39 + .../dds-account-types.component.ts | 112 + src/app/products/products-routing.module.ts | 95 +- src/app/products/products.component.html | 14 +- src/app/products/products.service.ts | 37 + .../global-configurations-tab.component.html | 13 +- .../global-configurations-tab.component.scss | 11 + .../global-configurations-tab.component.ts | 44 + src/environments/.env.ts | 4 +- 114 files changed, 10259 insertions(+), 15955 deletions(-) create mode 100644 src/app/deposits/daily-transaction-entry/day-collection-dialog/day-collection-dialog.component.html create mode 100644 src/app/deposits/daily-transaction-entry/day-collection-dialog/day-collection-dialog.component.scss create mode 100644 src/app/deposits/daily-transaction-entry/day-collection-dialog/day-collection-dialog.component.ts create mode 100644 src/app/deposits/daily-transaction-entry/notice-dialog/notice-dialog.component.html create mode 100644 src/app/deposits/daily-transaction-entry/notice-dialog/notice-dialog.component.scss create mode 100644 src/app/deposits/daily-transaction-entry/notice-dialog/notice-dialog.component.ts create mode 100644 src/app/deposits/party-master/CBS-Backend.code-workspace create mode 100644 src/app/deposits/party-master/create-party/create-party.component.html create mode 100644 src/app/deposits/party-master/create-party/create-party.component.scss create mode 100644 src/app/deposits/party-master/create-party/create-party.component.ts create mode 100644 src/app/deposits/party-master/due-for-closure/due-for-closure.component.html create mode 100644 src/app/deposits/party-master/due-for-closure/due-for-closure.component.scss create mode 100644 src/app/deposits/party-master/due-for-closure/due-for-closure.component.ts create mode 100644 src/app/deposits/party-master/nominee-list-dialog/nominee-list-dialog.component.html create mode 100644 src/app/deposits/party-master/nominee-list-dialog/nominee-list-dialog.component.scss create mode 100644 src/app/deposits/party-master/nominee-list-dialog/nominee-list-dialog.component.ts create mode 100644 src/app/deposits/party-master/payout-dialog/payout-dialog.component.html create mode 100644 src/app/deposits/party-master/payout-dialog/payout-dialog.component.scss create mode 100644 src/app/deposits/party-master/payout-dialog/payout-dialog.component.ts create mode 100644 src/app/deposits/party-master/premature-close-dialog/premature-close-dialog.component.html create mode 100644 src/app/deposits/party-master/premature-close-dialog/premature-close-dialog.component.scss create mode 100644 src/app/deposits/party-master/premature-close-dialog/premature-close-dialog.component.ts create mode 100644 src/app/directives/restrict-input.directive.ts create mode 100644 src/app/organization/agent-master/create-agent/create-agent.component.html create mode 100644 src/app/organization/agent-master/create-agent/create-agent.component.scss create mode 100644 src/app/organization/agent-master/create-agent/create-agent.component.ts create mode 100644 src/app/organization/dds-batch-processing/dds-date-adapter.ts delete mode 100644 src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/create-daily-deposit-scheme-product.component.html delete mode 100644 src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/create-daily-deposit-scheme-product.component.scss delete mode 100644 src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/create-daily-deposit-scheme-product.component.ts delete mode 100644 src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/daily-deposit-scheme-products-template.resolver.ts delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product.resolver.ts delete mode 100644 src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.component.html delete mode 100644 src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.component.scss delete mode 100644 src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.component.ts delete mode 100644 src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.resolver.ts delete mode 100644 src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/daily-deposit-scheme-product-and-template.resolver.ts delete mode 100644 src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/edit-daily-deposit-scheme-product.component.html delete mode 100644 src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/edit-daily-deposit-scheme-product.component.scss delete mode 100644 src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/edit-daily-deposit-scheme-product.component.ts delete mode 100644 src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-datatable-tab/daily-deposit-scheme-datatable-tab.component.html delete mode 100644 src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-datatable-tab/daily-deposit-scheme-datatable-tab.component.scss delete mode 100644 src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-datatable-tab/daily-deposit-scheme-datatable-tab.component.ts delete mode 100644 src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-general-tab/daily-deposit-scheme-general-tab.component.html delete mode 100644 src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-general-tab/daily-deposit-scheme-general-tab.component.scss delete mode 100644 src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/daily-deposit-scheme-general-tab/daily-deposit-scheme-general-tab.component.ts delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 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 delete mode 100644 src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/view-daily-deposit-scheme-product.component.html delete mode 100644 src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/view-daily-deposit-scheme-product.component.scss delete mode 100644 src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/view-daily-deposit-scheme-product.component.ts create mode 100644 src/app/products/dds-account-types/create-dds-account-type/create-dds-account-type.component.html create mode 100644 src/app/products/dds-account-types/create-dds-account-type/create-dds-account-type.component.scss create mode 100644 src/app/products/dds-account-types/create-dds-account-type/create-dds-account-type.component.ts create mode 100644 src/app/products/dds-account-types/dds-account-types.component.html create mode 100644 src/app/products/dds-account-types/dds-account-types.component.scss create mode 100644 src/app/products/dds-account-types/dds-account-types.component.ts 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 };