From 57a61b3057bd3cbb0ecf7b2ae9c751514406458c Mon Sep 17 00:00:00 2001 From: tarunmalviya Date: Tue, 25 Aug 2026 10:56:03 +0530 Subject: [PATCH] DDS: Agent settlement (Agent Tran), Close Party account UI, reports UI, Party-Savings link field --- src/app/accounts/accounts-routing.module.ts | 26 + src/app/core/shell/shell.component.html | 9 +- .../daily-transaction-entry.component.html | 241 +++++ .../daily-transaction-entry.component.scss | 187 ++++ .../daily-transaction-entry.component.ts | 290 ++++++ .../dds-reports/dds-reports.component.html | 33 + .../dds-reports/dds-reports.component.scss | 97 ++ .../dds-reports/dds-reports.component.ts | 73 ++ .../report-viewer.component.html | 94 ++ .../report-viewer.component.scss | 103 +++ .../report-viewer/report-viewer.component.ts | 120 +++ .../party-master/party-master.component.html | 543 +++++++++++ .../party-master/party-master.component.scss | 164 ++++ .../party-master/party-master.component.ts | 527 +++++++++++ .../agent-master/agent-master.component.html | 355 ++++++++ .../agent-master/agent-master.component.scss | 141 +++ .../agent-master/agent-master.component.ts | 381 ++++++++ .../commission-slab-master.component.html | 93 ++ .../commission-slab-master.component.scss | 92 ++ .../commission-slab-master.component.ts | 138 +++ .../dds-batch-processing.component.html | 133 +++ .../dds-batch-processing.component.scss | 145 +++ .../dds-batch-processing.component.ts | 186 ++++ .../organization-routing.module.ts | 18 + .../organization/organization.component.html | 41 + src/app/organization/organization.service.ts | 135 +++ ...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 + src/app/products/products-routing.module.ts | 91 ++ src/app/products/products.component.html | 19 + 78 files changed, 12711 insertions(+), 2 deletions(-) create mode 100644 src/app/deposits/daily-transaction-entry/daily-transaction-entry.component.html create mode 100644 src/app/deposits/daily-transaction-entry/daily-transaction-entry.component.scss create mode 100644 src/app/deposits/daily-transaction-entry/daily-transaction-entry.component.ts create mode 100644 src/app/deposits/dds-reports/dds-reports.component.html create mode 100644 src/app/deposits/dds-reports/dds-reports.component.scss create mode 100644 src/app/deposits/dds-reports/dds-reports.component.ts create mode 100644 src/app/deposits/dds-reports/report-viewer/report-viewer.component.html create mode 100644 src/app/deposits/dds-reports/report-viewer/report-viewer.component.scss create mode 100644 src/app/deposits/dds-reports/report-viewer/report-viewer.component.ts create mode 100644 src/app/deposits/party-master/party-master.component.html create mode 100644 src/app/deposits/party-master/party-master.component.scss create mode 100644 src/app/deposits/party-master/party-master.component.ts create mode 100644 src/app/organization/agent-master/agent-master.component.html create mode 100644 src/app/organization/agent-master/agent-master.component.scss create mode 100644 src/app/organization/agent-master/agent-master.component.ts create mode 100644 src/app/organization/commission-slab-master/commission-slab-master.component.html create mode 100644 src/app/organization/commission-slab-master/commission-slab-master.component.scss create mode 100644 src/app/organization/commission-slab-master/commission-slab-master.component.ts create mode 100644 src/app/organization/dds-batch-processing/dds-batch-processing.component.html create mode 100644 src/app/organization/dds-batch-processing/dds-batch-processing.component.scss create mode 100644 src/app/organization/dds-batch-processing/dds-batch-processing.component.ts create mode 100644 src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/create-daily-deposit-scheme-product.component.html create mode 100644 src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/create-daily-deposit-scheme-product.component.scss create mode 100644 src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/create-daily-deposit-scheme-product.component.ts create mode 100644 src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/daily-deposit-scheme-products-template.resolver.ts create 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 create 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 create 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 create 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 create 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 create 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 create 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 create 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 create 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 create 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 create 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 create 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 create 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 create 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 create 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 create 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 create 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 create 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 create 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 create 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 create 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 create 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 create 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 create 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 create mode 100644 src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product.resolver.ts create mode 100644 src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.component.html create mode 100644 src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.component.scss create mode 100644 src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.component.ts create mode 100644 src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.resolver.ts create mode 100644 src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/daily-deposit-scheme-product-and-template.resolver.ts create mode 100644 src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/edit-daily-deposit-scheme-product.component.html create mode 100644 src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/edit-daily-deposit-scheme-product.component.scss create mode 100644 src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/edit-daily-deposit-scheme-product.component.ts create 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 create 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 create 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 create 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 create 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 create 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 create 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 create 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 create 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 create 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 create mode 100644 src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/view-daily-deposit-scheme-product.component.html create mode 100644 src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/view-daily-deposit-scheme-product.component.scss create mode 100644 src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/view-daily-deposit-scheme-product.component.ts diff --git a/src/app/accounts/accounts-routing.module.ts b/src/app/accounts/accounts-routing.module.ts index 3654ae7de..566c9762f 100644 --- a/src/app/accounts/accounts-routing.module.ts +++ b/src/app/accounts/accounts-routing.module.ts @@ -11,6 +11,10 @@ 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 { 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'; const routes: Routes = [ Route.withShell([ @@ -41,6 +45,28 @@ const routes: Routes = [ title: 'Recurring Deposit Accounts', breadcrumb: 'Recurring Deposit Accounts' } + }, + { + path: 'dds', + component: PartyMasterComponent, + data: { title: 'Daily Deposit Scheme — Party Master', breadcrumb: 'Party Master' } + }, + { + path: 'dds-collection', + component: DailyTransactionEntryComponent, + data: { title: 'Daily Transaction Entry', breadcrumb: 'Daily Transaction Entry' } + }, + { + path: 'dds-reports', + data: { title: 'DDS Reports', breadcrumb: 'DDS Reports' }, + children: [ + { path: '', component: DdsReportsComponent }, + { + path: 'view/:reportName', + component: ReportViewerComponent, + data: { title: 'DDS Report', breadcrumb: 'reportName', routeParamBreadcrumb: 'reportName' } + } + ] } ] } diff --git a/src/app/core/shell/shell.component.html b/src/app/core/shell/shell.component.html index 023c6c8b3..9a32f5b1f 100644 --- a/src/app/core/shell/shell.component.html +++ b/src/app/core/shell/shell.component.html @@ -1048,14 +1048,16 @@ - +
Agent
-
Reports
+ diff --git a/src/app/deposits/daily-transaction-entry/daily-transaction-entry.component.html b/src/app/deposits/daily-transaction-entry/daily-transaction-entry.component.html new file mode 100644 index 000000000..553dbf281 --- /dev/null +++ b/src/app/deposits/daily-transaction-entry/daily-transaction-entry.component.html @@ -0,0 +1,241 @@ + + +
+
+ +

Daily Transaction Detail

+
+ +
+
+
+
+
Agent Information
+
+
Name
+
{{ agentInfo.name || '—' }}
+
Address
+
{{ agentInfo.address || '—' }}
+
City
+
{{ agentInfo.city || '—' }}
+
A/C
+
{{ agentInfo.accountNo || '—' }}
+
Guarantor
+
{{ agentInfo.guarantor1 || '—' }}
+
Guarantor
+
{{ agentInfo.guarantor2 || '—' }}
+
Curr Bal
+
{{ agentInfo.currentBalance | number: '1.2-2' }}
+
+
+ +
+
Party Information
+

Populated once a party is selected in a collection row below.

+
+
+ +
+ + Agent + + + + + + + Date + + + + + + + Tran. Type + + @for (type of transactionTypes; track type) { + {{ type }} + } + + +
+ + @if (isPartyTran) { +
+
+ +

Today's Collections

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

No collection rows added yet.

+ } + + @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 + + + +
+ } +
+ } + + @if (isAgentTran) { +
+
+ +

Agent Settlement

+
+

+ Cash the agent hands over to the branch, deducted from the agent's collected balance shown above. +

+
+ + Amount + + + + TRTP + + @for (mode of transactionModes; track mode) { + {{ mode }} + } + + + + AC Type + + + + A/C No. + + + + Description + + +
+
+ } + + @if (isWithdrawDeposite) { +
+
+ +

Withdraw Deposite

+
+
+ + Creditor Party Code + + + + Creditor Party Name + + + + Creditor Curr Bal + + + + + Debit Party Code + + + + Debit Party Name + + + + Debit Curr Bal + + + + + Amount + + + + Description + + +
+
+ } + + @if (!isPartyTran && !isWithdrawDeposite && !isAgentTran) { +
+

+ "{{ entryForm.get('transactionType').value }}" entry screen — same shape as Party Tran, to be filled in once + the backend distinguishes these transaction types. +

+
+ } + +
+ + + + + + +
+
+
+
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 new file mode 100644 index 000000000..0b527e5a0 --- /dev/null +++ b/src/app/deposits/daily-transaction-entry/daily-transaction-entry.component.scss @@ -0,0 +1,187 @@ +/** + * 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; + +.dte-container { + max-width: 1080px; + margin: 0 auto; + padding: 24px 16px; +} + +.dte-header { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 20px; + + &__title { + margin: 0; + font-size: 22px; + font-weight: 500; + } +} + +.dte-back { + color: $primary; +} + +.dte-card { + background: var(--md-sys-color-surface, #fff); + border-radius: 8px; + box-shadow: 0 2px 8px rgb(0 0 0 / 8%); + padding: 20px 24px 24px; +} + +.dte-info-panels { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + margin-bottom: 20px; +} + +.dte-info-panel { + border: 1px solid rgb(0 0 0 / 10%); + border-radius: 8px; + padding: 12px 16px; + background: rgb(15 118 110 / 4%); + + &__title { + font-weight: 600; + font-size: 13px; + text-transform: uppercase; + letter-spacing: 0.04em; + color: $primary; + margin-bottom: 8px; + } + + &__hint { + margin: 0; + color: rgb(0 0 0 / 50%); + font-size: 13px; + } + + dl { + display: grid; + grid-template-columns: auto 1fr; + gap: 4px 12px; + margin: 0; + } + + dt { + font-weight: 500; + color: rgb(0 0 0 / 60%); + font-size: 13px; + } + + dd { + margin: 0; + font-size: 13px; + } +} + +.dte-controls { + display: flex; + align-items: flex-start; + gap: 12px; + flex-wrap: wrap; + margin-bottom: 12px; +} + +.dte-field { + min-width: 180px; + + &--wide { + grid-column: span 2; + } +} + +.dte-section { + padding: 16px 0; + border-top: 1px solid rgb(0 0 0 / 8%); + + &__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; + } + } +} + +.dte-add-row { + color: $primary; +} + +.dte-empty { + color: rgb(0 0 0 / 50%); + font-size: 14px; + margin: 0; +} + +.dte-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px 16px; +} + +.dte-collection-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; + } +} + +.dte-collection-row .dte-field--wide { + grid-column: span 2; +} + +.dte-remove-row { + color: #b91c1c; + margin-top: 8px; +} + +.dte-action-bar { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 20px; + padding-top: 16px; + border-top: 1px solid rgb(0 0 0 / 8%); + flex-wrap: wrap; +} + +@media (width <= 1000px) { + .dte-info-panels { + grid-template-columns: 1fr; + } + + .dte-grid { + grid-template-columns: repeat(2, 1fr); + } + + .dte-collection-row { + grid-template-columns: repeat(2, 1fr); + } +} 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 new file mode 100644 index 000000000..0eb3edb83 --- /dev/null +++ b/src/app/deposits/daily-transaction-entry/daily-transaction-entry.component.ts @@ -0,0 +1,290 @@ +/** + * 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 { UntypedFormBuilder, UntypedFormGroup, UntypedFormArray, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +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'; + +/** + * Daily Transaction Entry component. + * + * The screen a collection agent uses at the end of a day's round: pick a + * transaction type, then batch-enter that day's collections against + * assigned parties (Party Tran), settle collected cash to the branch + * (Agent Tran), or record a withdrawal/transfer between accounts (Withdraw + * Deposite). Modelled directly on the legacy Daily Transaction Detail + * screen this replaces. + * + * Backed by the DDS Party Transaction API (`/dds/partytransactions`) for + * Party Tran, Agent Tran and Withdraw Deposite - the three types this + * screen actually collects fields for; Party Payment / OD Entry / OD + * Payment remain placeholders until the backend distinguishes them too. + * "Retrieve" (a ready-made list of today's assigned parties) isn't wired - + * that needs a dedicated per-agent Party lookup this MVP doesn't have yet. + */ +@Component({ + selector: 'mifosx-daily-transaction-entry', + templateUrl: './daily-transaction-entry.component.html', + styleUrls: ['./daily-transaction-entry.component.scss'], + imports: [...STANDALONE_SHARED_IMPORTS] +}) +export class DailyTransactionEntryComponent { + private formBuilder = inject(UntypedFormBuilder); + private router = inject(Router); + private organizationService = inject(OrganizationService); + private settingsService = inject(SettingsService); + private dateUtils = inject(Dates); + + private static readonly TRANSACTION_TYPE_CODES: Record = { + 'Party Tran': 'PARTY_TRAN', + 'Agent Tran': 'AGENT_TRAN', + 'Withdraw Deposite': 'WITHDRAW_DEPOSITE' + }; + + transactionTypes = [ + 'Party Tran', + 'Agent Tran', + 'Party Payment', + 'OD Entry', + 'OD Payment', + 'Withdraw Deposite' + ]; + + transactionModes = [ + 'Cash', + 'Transfer', + 'Clearing' + ]; + + entryForm: UntypedFormGroup = this.formBuilder.group({ + agentId: [ + '', + Validators.required + ], + transactionType: [ + 'Party Tran', + Validators.required + ], + transactionDate: [ + new Date(), + Validators.required + ], + // Withdraw Deposite fields + creditorPartyCode: [''], + creditorPartyName: [ + { + value: '', + disabled: true + } + ], + creditorPartyBalance: [ + { + value: 0, + disabled: true + } + ], + debitPartyCode: [''], + debitPartyName: [ + { + value: '', + disabled: true + } + ], + debitPartyBalance: [ + { + value: 0, + disabled: true + } + ], + withdrawAmount: [0], + withdrawDescription: [''], + // Agent Tran fields + settlementAmount: [0], + settlementMode: ['Cash'], + settlementAccountType: [''], + settlementAccountNo: [''], + settlementDescription: [''], + collections: this.formBuilder.array([]) + }); + + agentInfo = { + name: '', + address: '', + city: '', + accountNo: '', + guarantor1: '', + guarantor2: '', + currentBalance: 0 + }; + + get isPartyTran(): boolean { + return this.entryForm.get('transactionType').value === 'Party Tran'; + } + + get isWithdrawDeposite(): boolean { + return this.entryForm.get('transactionType').value === 'Withdraw Deposite'; + } + + get isAgentTran(): boolean { + return this.entryForm.get('transactionType').value === 'Agent Tran'; + } + + get collections(): UntypedFormArray { + return this.entryForm.get('collections') as UntypedFormArray; + } + + private buildCollectionRow(): UntypedFormGroup { + return this.formBuilder.group({ + partyCode: [ + '', + Validators.required + ], + transactionMode: [ + 'Cash', + Validators.required + ], + accountType: [''], + accountNo: [ + '', + Validators.required + ], + amount: [ + 0, + Validators.required + ], + chequeNo: [''], + tokenNo: [''], + description: [''], + partyName: [ + { + value: '', + disabled: true + } + ], + balanceAfterTransaction: [ + { + value: 0, + disabled: true + } + ] + }); + } + + addCollectionRow(): void { + this.collections.push(this.buildCollectionRow()); + } + + removeCollectionRow(index: number): void { + 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 + }; + }); + } + + retrieve(): void { + // No "my assigned parties" read API exists yet — this will pre-fill + // the collections grid with today's assigned parties once available. + } + + viewLedger(): void { + // No ledger view exists yet for this agent. + } + + signature(): void { + // No signature capture/verification flow exists yet. + } + + cancel(): void { + this.entryForm.reset({ + transactionType: 'Party Tran', + transactionDate: new Date() + }); + this.collections.clear(); + } + + exit(): void { + this.router.navigate(['/accounts/savings']); + } + + save(): void { + const raw = this.entryForm.getRawValue(); + const dateFormat = this.settingsService.dateFormat; + const locale = this.settingsService.language.code; + const transactionType = DailyTransactionEntryComponent.TRANSACTION_TYPE_CODES[raw.transactionType]; + + const payload: any = { + agentId: Number(raw.agentId), + transactionType, + transactionDate: this.dateUtils.formatDate(raw.transactionDate, dateFormat), + dateFormat, + 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) { + payload.creditorPartyCode = raw.creditorPartyCode; + payload.debitPartyCode = raw.debitPartyCode; + payload.withdrawAmount = raw.withdrawAmount; + payload.withdrawDescription = raw.withdrawDescription || undefined; + } else if (this.isAgentTran) { + payload.amount = raw.settlementAmount; + payload.transactionMode = raw.settlementMode; + payload.accountType = raw.settlementAccountType || undefined; + payload.accountNo = raw.settlementAccountNo || undefined; + payload.description = raw.settlementDescription || undefined; + } + + this.organizationService.createPartyTransaction(payload).subscribe(() => { + this.collections.clear(); + this.entryForm.patchValue({ + creditorPartyCode: '', + debitPartyCode: '', + withdrawAmount: 0, + withdrawDescription: '', + settlementAmount: 0, + settlementAccountType: '', + settlementAccountNo: '', + settlementDescription: '' + }); + this.loadAgent(); + }); + } +} diff --git a/src/app/deposits/dds-reports/dds-reports.component.html b/src/app/deposits/dds-reports/dds-reports.component.html new file mode 100644 index 000000000..befe2fe1f --- /dev/null +++ b/src/app/deposits/dds-reports/dds-reports.component.html @@ -0,0 +1,33 @@ + + +
+
+ +

DDS Reports

+
+ + @for (group of reportGroups; track group.title) { +
+
{{ group.title }}
+
+ @for (report of group.reports; track report) { + +
+ +
+
{{ report }}
+ +
+ } +
+
+ } +
diff --git a/src/app/deposits/dds-reports/dds-reports.component.scss b/src/app/deposits/dds-reports/dds-reports.component.scss new file mode 100644 index 000000000..56830830a --- /dev/null +++ b/src/app/deposits/dds-reports/dds-reports.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/. + */ + +$primary: #0f766e; + +.dr-container { + max-width: 1080px; + margin: 0 auto; + padding: 24px 16px; +} + +.dr-header { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 24px; + + &__title { + margin: 0; + font-size: 22px; + font-weight: 500; + } +} + +.dr-back { + color: $primary; +} + +.dr-group { + margin-bottom: 24px; + + &__title { + font-size: 13px; + text-transform: uppercase; + letter-spacing: 0.05em; + color: $primary; + font-weight: 700; + margin-bottom: 10px; + } +} + +.dr-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 12px; +} + +.dr-card { + display: flex; + align-items: center; + gap: 12px; + background: var(--md-sys-color-surface, #fff); + border: 1px solid rgb(0 0 0 / 8%); + border-radius: 8px; + padding: 12px 14px; + text-decoration: none; + color: inherit; + transition: box-shadow 0.15s ease; + + &:hover { + box-shadow: 0 2px 8px rgb(0 0 0 / 10%); + } + + &__icon { + color: $primary; + font-size: 16px; + flex: none; + } + + &__title { + flex: 1; + font-size: 13px; + font-weight: 500; + } + + &__arrow { + color: rgb(0 0 0 / 30%); + font-size: 12px; + } +} + +@media (width <= 900px) { + .dr-grid { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (width <= 600px) { + .dr-grid { + grid-template-columns: 1fr; + } +} diff --git a/src/app/deposits/dds-reports/dds-reports.component.ts b/src/app/deposits/dds-reports/dds-reports.component.ts new file mode 100644 index 000000000..63af55d01 --- /dev/null +++ b/src/app/deposits/dds-reports/dds-reports.component.ts @@ -0,0 +1,73 @@ +/** + * 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 } from '@angular/core'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** + * DDS Reports hub component. + * + * Lists every DDS report from the legacy Reports menu, grouped the same + * way (As-On-Date, Interest, Daily/Balance/Master, Transaction). Each + * links to the shared report viewer, parameterised by report name. + * + * UI only for now: no report actually queries the backend yet — see + * ReportViewerComponent. + */ +@Component({ + selector: 'mifosx-dds-reports', + templateUrl: './dds-reports.component.html', + styleUrls: ['./dds-reports.component.scss'], + imports: [...STANDALONE_SHARED_IMPORTS] +}) +export class DdsReportsComponent { + reportGroups = [ + { + title: 'As On Date', + reports: [ + 'Supplementary-DDS (As On)', + 'Agent Check List', + 'Party Check List-As On', + 'Party Balance', + 'Agent Wise Balance', + 'Agent Wise Monthly Collection', + 'Commission Register', + 'Collection Summary Agent Wise', + 'Close Party Ac Register', + 'Agent Partywise Daily Collection', + 'Daily Collection Balance', + 'Interest Certificate', + 'Mature Date Wise Report' + ] + }, + { + title: 'Interest', + reports: [ + 'Party Int. Register', + 'Commission Register' + ] + }, + { + title: 'Daily / Balance / Master', + reports: [ + 'Daily Collection Report', + 'Balance Summary', + 'Master Data Export' + ] + }, + { + title: 'Transaction', + reports: ['Transaction Register'] + }, + { + title: 'Statements', + reports: ['Agent Commission Statement'] + } + ]; +} 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 new file mode 100644 index 000000000..4b7601dc4 --- /dev/null +++ b/src/app/deposits/dds-reports/report-viewer/report-viewer.component.html @@ -0,0 +1,94 @@ + + +
+
+ +

{{ reportName }}

+
+ +
+
+
+ + From Date + + + + + + + To Date + + + + + + + Agent From + + + + + Agent To + + + + + Party From + + + + + Party To + + +
+ +
+ + +
+ + @if (hasRun) { +
+ @if (!available) { +

{{ unavailableMessage }}

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

No rows matched these filters.

+ } @else { +
+ + + + @for (column of columns; track column) { + + } + + + + @for (row of rows; track row) { + + @for (column of columns; track column) { + + } + + } + +
{{ column }}
{{ row[column] }}
+
+ } +
+ } +
+
+
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 new file mode 100644 index 000000000..454eb7c33 --- /dev/null +++ b/src/app/deposits/dds-reports/report-viewer/report-viewer.component.scss @@ -0,0 +1,103 @@ +/** + * 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; + +.rv-container { + max-width: 1100px; + margin: 0 auto; + padding: 24px 16px; +} + +.rv-header { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 20px; + + &__title { + margin: 0; + font-size: 22px; + font-weight: 500; + } +} + +.rv-back { + color: $primary; +} + +.rv-card { + background: var(--md-sys-color-surface, #fff); + border-radius: 8px; + box-shadow: 0 2px 8px rgb(0 0 0 / 8%); + padding: 20px 24px 24px; +} + +.rv-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px 16px; +} + +.rv-field { + width: 100%; +} + +.rv-action-bar { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 20px; + padding-top: 16px; + border-top: 1px solid rgb(0 0 0 / 8%); +} + +.rv-results { + margin-top: 20px; + padding-top: 16px; + border-top: 1px solid rgb(0 0 0 / 8%); +} + +.rv-empty { + color: rgb(0 0 0 / 50%); + font-size: 14px; + margin: 0; + text-align: center; + padding: 24px 0; +} + +.rv-table-scroll { + overflow-x: auto; +} + +.rv-table { + width: 100%; + border-collapse: collapse; + white-space: nowrap; + + th { + text-align: left; + font-size: 13px; + 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%); + } + + td { + padding: 8px 10px; + border-bottom: 1px solid rgb(0 0 0 / 6%); + } +} + +@media (width <= 900px) { + .rv-grid { + grid-template-columns: repeat(2, 1fr); + } +} 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 new file mode 100644 index 000000000..2ab8c9985 --- /dev/null +++ b/src/app/deposits/dds-reports/report-viewer/report-viewer.component.ts @@ -0,0 +1,120 @@ +/** + * 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 { ActivatedRoute, Router } from '@angular/router'; +import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** Custom Services */ +import { OrganizationService } from 'app/organization/organization.service'; +import { Dates } from 'app/core/utils/dates'; + +/** + * Generic DDS report viewer. + * + * One shared screen for all 13+ DDS reports — the report name comes from + * the route and is mapped to a backend report key. Every report in the + * legacy system shared the same basic filter shape (date, agent range, + * party range), so this avoids building a bespoke screen per report - the + * backend returns a generic {columns, rows} shape this renders as one + * table regardless of which report it is. + * + * One legacy report (Close Party Ac Register) has no corresponding data in + * the backend yet - Party accounts have no closed/open status at all. Run + * Report still calls through, and the backend's `available: false` + + * message is shown instead of a table. + */ +@Component({ + selector: 'mifosx-dds-report-viewer', + templateUrl: './report-viewer.component.html', + styleUrls: ['./report-viewer.component.scss'], + imports: [...STANDALONE_SHARED_IMPORTS] +}) +export class ReportViewerComponent { + private route = inject(ActivatedRoute); + private router = inject(Router); + private formBuilder = inject(UntypedFormBuilder); + private organizationService = inject(OrganizationService); + private dateUtils = inject(Dates); + + private static readonly REPORT_KEYS: Record = { + 'Supplementary-DDS (As On)': 'SUPPLEMENTARY_DDS', + 'Agent Check List': 'AGENT_CHECK_LIST', + 'Party Check List-As On': 'PARTY_CHECK_LIST', + 'Party Balance': 'PARTY_BALANCE', + 'Agent Wise Balance': 'AGENT_WISE_BALANCE', + 'Agent Wise Monthly Collection': 'AGENT_WISE_MONTHLY_COLLECTION', + 'Commission Register': 'COMMISSION_REGISTER', + 'Collection Summary Agent Wise': 'COLLECTION_SUMMARY_AGENT_WISE', + 'Close Party Ac Register': 'CLOSE_PARTY_AC_REGISTER', + 'Agent Partywise Daily Collection': 'AGENT_PARTYWISE_DAILY_COLLECTION', + 'Daily Collection Balance': 'DAILY_COLLECTION_REPORT', + 'Interest Certificate': 'INTEREST_CERTIFICATE', + 'Mature Date Wise Report': 'MATURE_DATE_WISE', + 'Party Int. Register': 'PARTY_INTEREST_REGISTER', + 'Daily Collection Report': 'DAILY_COLLECTION_REPORT', + 'Balance Summary': 'BALANCE_SUMMARY', + 'Master Data Export': 'MASTER_DATA_EXPORT', + 'Transaction Register': 'TRANSACTION_REGISTER', + 'Agent Commission Statement': 'AGENT_COMMISSION_STATEMENT' + }; + + reportName: string = this.route.snapshot.paramMap.get('reportName') || 'Report'; + + hasRun = false; + available = true; + unavailableMessage = ''; + columns: string[] = []; + rows: Array> = []; + + filterForm: UntypedFormGroup = this.formBuilder.group({ + fromDate: [ + '', + Validators.required + ], + toDate: [ + '', + Validators.required + ], + agentFrom: [1], + agentTo: [99999], + partyFrom: [1], + partyTo: [99999999] + }); + + runReport(): void { + const reportKey = ReportViewerComponent.REPORT_KEYS[this.reportName]; + if (!reportKey) { + return; + } + const raw = this.filterForm.getRawValue(); + + 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 + }) + .subscribe((result: any) => { + this.available = result.available; + this.unavailableMessage = result.message || ''; + this.columns = result.columns || []; + this.rows = result.rows || []; + this.hasRun = true; + }); + } + + exit(): void { + this.router.navigate(['../../'], { relativeTo: this.route }); + } +} diff --git a/src/app/deposits/party-master/party-master.component.html b/src/app/deposits/party-master/party-master.component.html new file mode 100644 index 000000000..416b9a6c7 --- /dev/null +++ b/src/app/deposits/party-master/party-master.component.html @@ -0,0 +1,543 @@ + + +
+
+ +

Party Master Entry

+
+ +
+
+ + +
+
+
+ +

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) { + + } + +
+
+
+
diff --git a/src/app/deposits/party-master/party-master.component.scss b/src/app/deposits/party-master/party-master.component.scss new file mode 100644 index 000000000..baa31be66 --- /dev/null +++ b/src/app/deposits/party-master/party-master.component.scss @@ -0,0 +1,164 @@ +/** + * 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 { + max-width: 1080px; + margin: 0 auto; + padding: 24px 16px; +} + +.pm-header { + 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; + 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); + } + + .pm-nominee-row { + grid-template-columns: repeat(2, 1fr); + } +} diff --git a/src/app/deposits/party-master/party-master.component.ts b/src/app/deposits/party-master/party-master.component.ts new file mode 100644 index 000000000..125525b16 --- /dev/null +++ b/src/app/deposits/party-master/party-master.component.ts @@ -0,0 +1,527 @@ +/** + * 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 { 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 { 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'; + +/** + * Party Master Entry component. + * + * 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. + */ +@Component({ + selector: 'mifosx-party-master', + templateUrl: './party-master.component.html', + styleUrls: ['./party-master.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatTabsModule, + MatCheckbox, + MatRadioModule + ] +}) +export class PartyMasterComponent { + private formBuilder = inject(UntypedFormBuilder); + private router = inject(Router); + private organizationService = inject(OrganizationService); + private settingsService = inject(SettingsService); + private dateUtils = inject(Dates); + + 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' + ]; + + 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: [''] + }); + } + + addNominee(): void { + this.nominees.push(this.buildNomineeGroup()); + } + + removeNominee(index: number): void { + this.nominees.removeAt(index); + } + + retrieve(): void { + const clientId = this.partyForm.get('clientId')?.value; + if (!clientId) { + return; + } + this.organizationService.getParties().subscribe((parties: any[]) => { + const match = (parties || []).find((party) => String(party.clientId) === String(clientId)); + if (match) { + this.patchFormFromParty(match); + } + }); + } + + 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); + }); + } +} diff --git a/src/app/organization/agent-master/agent-master.component.html b/src/app/organization/agent-master/agent-master.component.html new file mode 100644 index 000000000..243121fd8 --- /dev/null +++ b/src/app/organization/agent-master/agent-master.component.html @@ -0,0 +1,355 @@ + + +
+
+ +

Agent Master Entry

+
+ +
+
+
+
+ +

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 + + +
+
+ +
+
+ +

Collection & Commission Settings

+
+
+ + Com. Calculation + + Yes + No + + + + + TDS + + Yes + No + + + + + TDS % + + + + + Cur.TDS + + + + + TDS Amt + + + + + Limit + + + + + Machine Rent + + +
+
+ +
+
+ +

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 + + +
+
+ +
+
+ +

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 + + +
+
+ +
+ + + + +
+
+
+
diff --git a/src/app/organization/agent-master/agent-master.component.scss b/src/app/organization/agent-master/agent-master.component.scss new file mode 100644 index 000000000..1532696bc --- /dev/null +++ b/src/app/organization/agent-master/agent-master.component.scss @@ -0,0 +1,141 @@ +/** + * 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; +} + +@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/agent-master.component.ts b/src/app/organization/agent-master/agent-master.component.ts new file mode 100644 index 000000000..4b35e05f4 --- /dev/null +++ b/src/app/organization/agent-master/agent-master.component.ts @@ -0,0 +1,381 @@ +/** + * 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 { UntypedFormBuilder, UntypedFormGroup, UntypedFormArray, Validators } from '@angular/forms'; +import { Router } from '@angular/router'; +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. + * + * 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. + */ +@Component({ + selector: 'mifosx-agent-master', + templateUrl: './agent-master.component.html', + styleUrls: ['./agent-master.component.scss'], + imports: [...STANDALONE_SHARED_IMPORTS] +}) +export class AgentMasterComponent { + private formBuilder = inject(UntypedFormBuilder); + private router = inject(Router); + 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' + ]; + + 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([]) + }); + + 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); + }); + } +} 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 new file mode 100644 index 000000000..31c5b682b --- /dev/null +++ b/src/app/organization/commission-slab-master/commission-slab-master.component.html @@ -0,0 +1,93 @@ + + +
+
+ +

Commission Slab Master

+
+ +
+ + + + + + + + + + + + @for (slab of slabs; track slab; let i = $index) { + + + + + + + + } + + + + + + + + + +
Slab TypeSlab FromSlab ToComm Per
{{ slab.slabType }}{{ slab.slabFrom }}{{ slab.slabTo }}{{ slab.commissionPercent }}% + +
+ + + @for (type of slabTypes; track type) { + {{ type }} + } + + + + + + + + + + + + + + + + +
+ + @if (slabs.length === 0) { +

No commission slabs defined yet — add one using the row above.

+ } + +
+ + +
+
+
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 new file mode 100644 index 000000000..76eed8f2a --- /dev/null +++ b/src/app/organization/commission-slab-master/commission-slab-master.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/. + */ + +$primary: #0f766e; + +.csm-container { + max-width: 760px; + margin: 0 auto; + padding: 24px 16px; +} + +.csm-header { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 20px; + + &__title { + margin: 0; + font-size: 22px; + font-weight: 500; + } +} + +.csm-back { + color: $primary; +} + +.csm-card { + background: var(--md-sys-color-surface, #fff); + border-radius: 8px; + box-shadow: 0 2px 8px rgb(0 0 0 / 8%); + padding: 16px 24px 24px; +} + +.csm-table { + width: 100%; + border-collapse: collapse; + + th { + text-align: left; + font-size: 13px; + 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%); + } + + td { + padding: 6px 10px; + border-bottom: 1px solid rgb(0 0 0 / 6%); + vertical-align: middle; + } +} + +.csm-inline-field { + width: 100%; + + ::ng-deep .mat-mdc-text-field-wrapper { + height: 44px; + } +} + +.csm-row-actions { + text-align: center; + width: 48px; +} + +.csm-new-row { + background: rgb(15 118 110 / 4%); +} + +.csm-empty { + color: rgb(0 0 0 / 50%); + font-size: 14px; + margin: 12px 0 0; +} + +.csm-action-bar { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 20px; + padding-top: 16px; + border-top: 1px solid rgb(0 0 0 / 8%); +} 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 new file mode 100644 index 000000000..563a0da46 --- /dev/null +++ b/src/app/organization/commission-slab-master/commission-slab-master.component.ts @@ -0,0 +1,138 @@ +/** + * 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 { Router } from '@angular/router'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +/** Custom Services */ +import { OrganizationService } from 'app/organization/organization.service'; + +/** + * Commission Slab Master component. + * + * Two kinds of slab: ACCOUNT (commission percentage on collection volume) + * and COMMISSION (a senior-agent override on another agent's commission - + * deferred: there is no agent hierarchy modelled yet, see the DDS punch + * list). Modelled directly on the legacy Commission Slab Master screen this + * replaces. + * + * Backed by its own DDS Commission Slab API (`/dds/commissionslabs`) - an + * append-only chart, same as the legacy screen: existing rows load on open, + * newly-added rows post on Save, and there is no edit/delete of a + * previously-saved row. + */ +@Component({ + selector: 'mifosx-commission-slab-master', + templateUrl: './commission-slab-master.component.html', + styleUrls: ['./commission-slab-master.component.scss'], + imports: [...STANDALONE_SHARED_IMPORTS] +}) +export class CommissionSlabMasterComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private router = inject(Router); + private organizationService = inject(OrganizationService); + + private static readonly SLAB_TYPE_CODES: Record = { + Account: 'ACCOUNT', + Commission: 'COMMISSION' + }; + private static readonly SLAB_TYPE_LABELS: Record = { + ACCOUNT: 'Account', + COMMISSION: 'Commission' + }; + + slabTypes = [ + 'Account', + 'Commission' + ]; + + displayedColumns: string[] = [ + 'slabType', + 'slabFrom', + 'slabTo', + 'commissionPercent', + 'actions' + ]; + + slabs: Array<{ id?: number; slabType: string; slabFrom: number; slabTo: number; commissionPercent: number }> = []; + + newSlabForm: UntypedFormGroup = this.formBuilder.group({ + slabType: [ + '', + Validators.required + ], + slabFrom: [ + 0, + Validators.required + ], + slabTo: [ + 0, + Validators.required + ], + commissionPercent: [ + 0, + Validators.required + ] + }); + + ngOnInit(): void { + this.loadSlabs(); + } + + private loadSlabs(): void { + this.organizationService.getCommissionSlabs().subscribe((slabs: any[]) => { + this.slabs = (slabs || []).map((slab) => ({ + id: slab.id, + slabType: CommissionSlabMasterComponent.SLAB_TYPE_LABELS[slab.slabType] || slab.slabType, + slabFrom: slab.slabFrom, + slabTo: slab.slabTo, + commissionPercent: slab.commissionPercent + })); + }); + } + + addSlab(): void { + if (this.newSlabForm.invalid) { + return; + } + this.slabs.push(this.newSlabForm.getRawValue()); + this.newSlabForm.reset({ + slabType: '', + slabFrom: 0, + slabTo: 0, + commissionPercent: 0 + }); + } + + removeSlab(index: number): void { + this.slabs.splice(index, 1); + } + + exit(): void { + this.router.navigate(['/organization']); + } + + save(): void { + const newSlabs = this.slabs.filter((slab) => !slab.id); + if (newSlabs.length === 0) { + return; + } + + const payload = newSlabs.map((slab) => ({ + slabType: CommissionSlabMasterComponent.SLAB_TYPE_CODES[slab.slabType] || slab.slabType, + slabFrom: slab.slabFrom, + slabTo: slab.slabTo, + commissionPercent: slab.commissionPercent + })); + + this.organizationService.createCommissionSlabs(payload).subscribe(() => this.loadSlabs()); + } +} 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 new file mode 100644 index 000000000..be20aba12 --- /dev/null +++ b/src/app/organization/dds-batch-processing/dds-batch-processing.component.html @@ -0,0 +1,133 @@ + + +
+
+ +

DDS Interest & Commission Batch Processing

+
+ +
+
+
+ + Job Type + + @for (type of jobTypes; track type) { + {{ type }} + } + + + + + Branch + + @for (scope of branchScopes; track scope) { + {{ scope }} + } + + + + @if (isBranchSelected) { +
+ @for (office of offices; track office.id) { + + {{ office.name }} + + } + @if (offices.length === 0) { + Loading branches... + } +
+ } + + + {{ isPostingJob ? 'Posting Date' : 'As-On Date' }} + + + + + + + Agent From + + + + + Agent To + + + + + Party From + + + + + 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 (hasGenerated) { +
+
+ +

Preview

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

+ No calculation preview available yet — this appears once the backend batch-job endpoint for "{{ + batchForm.get('jobType').value + }}" is connected. +

+ } @else { + + + + + + + + + + @for (row of previewRows; track row) { + + + + + + } + +
AgentPartyAmount
{{ row.agent }}{{ row.party }}{{ row.amount | number: '1.2-2' }}
+ } +
+ } + +
+ + + +
+
+
+
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 new file mode 100644 index 000000000..499ff754e --- /dev/null +++ b/src/app/organization/dds-batch-processing/dds-batch-processing.component.scss @@ -0,0 +1,145 @@ +/** + * 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; + +.bp-container { + max-width: 900px; + margin: 0 auto; + padding: 24px 16px; +} + +.bp-header { + display: flex; + align-items: center; + gap: 16px; + margin-bottom: 20px; + + &__title { + margin: 0; + font-size: 22px; + font-weight: 500; + } +} + +.bp-back { + color: $primary; +} + +.bp-card { + background: var(--md-sys-color-surface, #fff); + border-radius: 8px; + box-shadow: 0 2px 8px rgb(0 0 0 / 8%); + padding: 20px 24px 24px; +} + +.bp-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 8px 16px; +} + +.bp-field { + width: 100%; + + &--wide { + grid-column: span 3; + } +} + +.bp-branch-checklist { + display: flex; + flex-wrap: wrap; + gap: 8px 20px; + align-items: center; + padding: 8px 0; +} + +.bp-callout { + display: flex; + gap: 10px; + align-items: flex-start; + background: rgb(15 118 110 / 6%); + border-left: 3px solid $primary; + border-radius: 0 8px 8px 0; + padding: 12px 16px; + margin: 16px 0; + font-size: 13px; + + i { + color: $primary; + margin-top: 2px; + } +} + +.bp-section { + padding: 16px 0; + border-top: 1px solid rgb(0 0 0 / 8%); + + &__header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 12px; + + i { + color: $primary; + } + + h3 { + margin: 0; + font-size: 16px; + font-weight: 600; + } + } +} + +.bp-empty { + color: rgb(0 0 0 / 50%); + font-size: 14px; + margin: 0; +} + +.bp-table { + width: 100%; + border-collapse: collapse; + + th { + text-align: left; + font-size: 13px; + 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%); + } + + td { + padding: 8px 10px; + border-bottom: 1px solid rgb(0 0 0 / 6%); + } +} + +.bp-action-bar { + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 20px; + padding-top: 16px; + border-top: 1px solid rgb(0 0 0 / 8%); +} + +@media (width <= 900px) { + .bp-grid { + grid-template-columns: repeat(2, 1fr); + } + + .bp-field--wide { + grid-column: span 2; + } +} 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 new file mode 100644 index 000000000..2b6bf5cad --- /dev/null +++ b/src/app/organization/dds-batch-processing/dds-batch-processing.component.ts @@ -0,0 +1,186 @@ +/** + * 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 { Router } from '@angular/router'; +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'; + +/** + * DDS Interest & Commission Batch Processing component. + * + * 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. + * + * 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). + */ +@Component({ + selector: 'mifosx-dds-batch-processing', + templateUrl: './dds-batch-processing.component.html', + styleUrls: ['./dds-batch-processing.component.scss'], + imports: [...STANDALONE_SHARED_IMPORTS] +}) +export class DdsBatchProcessingComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private router = inject(Router); + private organizationService = inject(OrganizationService); + 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' + ]; + + branchScopes = [ + 'All', + 'Selected' + ]; + + hasGenerated = false; + previewRows: Array<{ agent: string; party: string; amount: number }> = []; + + batchForm: UntypedFormGroup = this.formBuilder.group({ + jobType: [ + 'Interest Calculation', + Validators.required + ], + branchScope: [ + 'All', + Validators.required + ], + jobDate: [ + new Date(), + Validators.required + ], + agentFrom: [ + 1, + Validators.required + ], + agentTo: [ + 99999, + Validators.required + ], + partyFrom: [ + 1, + Validators.required + ], + partyTo: [ + 99999999, + Validators.required + ] + }); + + get isPostingJob(): boolean { + return (this.batchForm.get('jobType').value as string).includes('Posting'); + } + + get isBranchSelected(): boolean { + return this.batchForm.get('branchScope')?.value === 'Selected'; + } + + ngOnInit(): void { + this.organizationService.getOffices().subscribe((offices: any[]) => { + this.offices = offices || []; + }); + } + + toggleOffice(officeName: string): void { + if (this.selectedOfficeNames.has(officeName)) { + this.selectedOfficeNames.delete(officeName); + } else { + this.selectedOfficeNames.add(officeName); + } + } + + 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, + 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 + }; + + 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; + }); + }); + } + + post(): void { + if (!this.currentBatchRunId) { + return; + } + this.organizationService.postDdsBatchRun(this.currentBatchRunId).subscribe(() => { + this.currentBatchRunId = null; + this.hasGenerated = false; + this.previewRows = []; + }); + } + + exit(): void { + this.router.navigate(['/organization']); + } +} diff --git a/src/app/organization/organization-routing.module.ts b/src/app/organization/organization-routing.module.ts index 58bd89ba6..0ddb76ce3 100644 --- a/src/app/organization/organization-routing.module.ts +++ b/src/app/organization/organization-routing.module.ts @@ -17,6 +17,9 @@ import { Route } from '../core/route/route.service'; 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 { 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'; import { CreateEmployeeComponent } from './employees/create-employee/create-employee.component'; import { ViewEmployeeComponent } from './employees/view-employee/view-employee.component'; @@ -237,6 +240,21 @@ const routes: Routes = [ } ] }, + { + path: 'agent-master', + component: AgentMasterComponent, + data: { title: 'Agent Master', breadcrumb: 'Agent Master' } + }, + { + path: 'commission-slab-master', + component: CommissionSlabMasterComponent, + data: { title: 'Commission Slab Master', breadcrumb: 'Commission Slab Master' } + }, + { + path: 'dds-batch-processing', + component: DdsBatchProcessingComponent, + data: { title: 'DDS Interest & Commission Batch', breadcrumb: 'DDS Batch Processing' } + }, { path: 'employees', data: { title: 'Manage Employees', breadcrumb: 'Manage Employees' }, diff --git a/src/app/organization/organization.component.html b/src/app/organization/organization.component.html index f54ef9507..e13b1e18b 100644 --- a/src/app/organization/organization.component.html +++ b/src/app/organization/organization.component.html @@ -90,6 +90,47 @@
+
+ +
+ +
+
+

Agent Master

+

+ Register Daily Deposit Scheme collection agents, their commission/TDS settings, limits and guarantors +

+
+ +
+
+ +
+ +
+ +
+
+

Commission Slab Master

+

Define collection-volume and override commission percentage slabs

+
+ +
+
+ +
+ +
+ +
+
+

DDS Interest & Commission Batch

+

Calculate then post interest, OD interest, and commission for DDS accounts

+
+ +
+
+
diff --git a/src/app/organization/organization.service.ts b/src/app/organization/organization.service.ts index a5f84ca38..6aab81df1 100644 --- a/src/app/organization/organization.service.ts +++ b/src/app/organization/organization.service.ts @@ -233,6 +233,141 @@ export class OrganizationService { return this.http.get('/currencies'); } + /** + * @returns {Observable} DDS Agents data + */ + getAgents(): Observable { + return this.http.get('/dds/agents'); + } + + /** + * @param {string | number} agentId Agent ID. + * @returns {Observable} DDS Agent. + */ + getAgent(agentId: string | number): Observable { + return this.http.get(`/dds/agents/${agentId}`); + } + + /** + * @param {any} agent Agent to be created. + * @returns {Observable} + */ + createAgent(agent: any): Observable { + return this.http.post('/dds/agents', agent); + } + + /** + * @param {string | number} agentId Agent ID. + * @param {any} agent Agent changes to apply. + * @returns {Observable} + */ + updateAgent(agentId: string | number, agent: any): Observable { + return this.http.put(`/dds/agents/${agentId}`, agent); + } + + /** + * @returns {Observable} DDS Parties data + */ + getParties(): Observable { + return this.http.get('/dds/parties'); + } + + /** + * @param {string | number} partyId Party ID. + * @returns {Observable} DDS Party. + */ + getParty(partyId: string | number): Observable { + return this.http.get(`/dds/parties/${partyId}`); + } + + /** + * @param {any} party Party to be created. + * @returns {Observable} + */ + createParty(party: any): Observable { + return this.http.post('/dds/parties', party); + } + + /** + * @param {string | number} partyId Party ID. + * @param {any} party Party changes to apply. + * @returns {Observable} + */ + updateParty(partyId: string | number, party: any): Observable { + return this.http.put(`/dds/parties/${partyId}`, party); + } + + /** + * @param {string | number} partyId Party ID. + * @param {any} closeCommand { closeDate } + * @returns {Observable} + */ + closeParty(partyId: string | number, closeCommand: any): Observable { + return this.http.post(`/dds/parties/${partyId}?command=close`, closeCommand); + } + + /** + * @param {any} transaction Daily transaction batch (PARTY_TRAN collections or a WITHDRAW_DEPOSITE transfer) to post. + * @returns {Observable} + */ + createPartyTransaction(transaction: any): Observable { + return this.http.post('/dds/partytransactions', transaction); + } + + /** + * @returns {Observable} DDS Commission Slabs data + */ + getCommissionSlabs(): Observable { + return this.http.get('/dds/commissionslabs'); + } + + /** + * @param {any[]} slabs New commission slab rows to add to the chart. + * @returns {Observable} + */ + createCommissionSlabs(slabs: any[]): Observable { + return this.http.post('/dds/commissionslabs', { slabs }); + } + + /** + * @param {any} batch Batch run filters (jobType, branchScope, jobDate, agentFrom/To, partyFrom/To) to generate a preview for. + * @returns {Observable} + */ + generateDdsBatchRun(batch: any): Observable { + return this.http.post('/dds/batchruns', batch); + } + + /** + * @param {string | number} batchRunId Batch run ID. + * @returns {Observable} Batch run, including its previewed/posted rows. + */ + getDdsBatchRun(batchRunId: string | number): Observable { + return this.http.get(`/dds/batchruns/${batchRunId}`); + } + + /** + * @param {string | number} batchRunId Batch run ID to post - applies exactly the rows it previewed. + * @returns {Observable} + */ + postDdsBatchRun(batchRunId: string | number): Observable { + return this.http.post(`/dds/batchruns/${batchRunId}`, {}); + } + + /** + * @param {string} reportKey One of the backend DDS report keys. + * @param {any} filters fromDate, toDate (ISO strings), agentFrom/To, partyFrom/To. + * @returns {Observable} { reportKey, available, message, columns, rows } + */ + runDdsReport(reportKey: string, filters: any): Observable { + let httpParams = new HttpParams(); + Object.keys(filters || {}).forEach((key) => { + if (filters[key] !== undefined && filters[key] !== null && filters[key] !== '') { + httpParams = httpParams.set(key, filters[key]); + } + }); + return this.http.get(`/dds/reports/${reportKey}`, { params: httpParams }); + } + /** * @param {any[]} currencies * @returns {Observable} Currencies data 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 new file mode 100644 index 000000000..aeeca63ce --- /dev/null +++ b/src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/create-daily-deposit-scheme-product.component.html @@ -0,0 +1,131 @@ + + +
+
+ +

{{ '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 new file mode 100644 index 000000000..6f9abbbfd --- /dev/null +++ b/src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/create-daily-deposit-scheme-product.component.scss @@ -0,0 +1,138 @@ +/** + * 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 new file mode 100644 index 000000000..741b1425a --- /dev/null +++ b/src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/create-daily-deposit-scheme-product.component.ts @@ -0,0 +1,346 @@ +/** + * 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 new file mode 100644 index 000000000..f70da1eda --- /dev/null +++ b/src/app/products/daily-deposit-scheme-products/create-daily-deposit-scheme-product/daily-deposit-scheme-products-template.resolver.ts @@ -0,0 +1,35 @@ +/** + * 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 new file mode 100644 index 000000000..f7fd053a9 --- /dev/null +++ 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 @@ -0,0 +1,121 @@ + + +
+
+
+ + + +

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 new file mode 100644 index 000000000..bac63a1a2 --- /dev/null +++ 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 @@ -0,0 +1,303 @@ +/** + * 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 new file mode 100644 index 000000000..09ef9c1d7 --- /dev/null +++ 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 @@ -0,0 +1,423 @@ +/** + * 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 new file mode 100644 index 000000000..b2c701878 --- /dev/null +++ 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 @@ -0,0 +1,85 @@ + + +
+
+ +

{{ '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 new file mode 100644 index 000000000..661815e4c --- /dev/null +++ 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 @@ -0,0 +1,69 @@ +/** + * 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 new file mode 100644 index 000000000..4db6f9ae9 --- /dev/null +++ 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 @@ -0,0 +1,103 @@ +/** + * 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 new file mode 100644 index 000000000..86af4f7ea --- /dev/null +++ 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 @@ -0,0 +1,89 @@ + + +
+
+
+ + 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 new file mode 100644 index 000000000..d7a2abd40 --- /dev/null +++ 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 @@ -0,0 +1,102 @@ +/** + * 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 new file mode 100644 index 000000000..098800d33 --- /dev/null +++ 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 @@ -0,0 +1,104 @@ +/** + * 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 new file mode 100644 index 000000000..2fae4cfe6 --- /dev/null +++ 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 @@ -0,0 +1,77 @@ + + +
+
+
+ +

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 new file mode 100644 index 000000000..ca87b04f4 --- /dev/null +++ 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 @@ -0,0 +1,81 @@ +/** + * 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 new file mode 100644 index 000000000..65f517f15 --- /dev/null +++ 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 @@ -0,0 +1,177 @@ +/** + * 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 new file mode 100644 index 000000000..7b799e488 --- /dev/null +++ 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 @@ -0,0 +1,109 @@ + + + +
+
+ +

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 new file mode 100644 index 000000000..21f95779c --- /dev/null +++ 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 @@ -0,0 +1,215 @@ +/** + * 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 new file mode 100644 index 000000000..f8ac4d193 --- /dev/null +++ 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 @@ -0,0 +1,717 @@ +/** + * 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 new file mode 100644 index 000000000..2227219c8 --- /dev/null +++ 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 @@ -0,0 +1,732 @@ + + +
+ +

{{ 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 new file mode 100644 index 000000000..33a0e1852 --- /dev/null +++ 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 @@ -0,0 +1,131 @@ +/** + * 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 new file mode 100644 index 000000000..062751880 --- /dev/null +++ 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 @@ -0,0 +1,142 @@ +/** + * 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 new file mode 100644 index 000000000..804ae09e9 --- /dev/null +++ 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 @@ -0,0 +1,123 @@ + + +
+ +
+
+ + 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 new file mode 100644 index 000000000..3aefca054 --- /dev/null +++ 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 @@ -0,0 +1,164 @@ +/** + * 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 new file mode 100644 index 000000000..b7f3d26ac --- /dev/null +++ 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 @@ -0,0 +1,95 @@ +/** + * 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 new file mode 100644 index 000000000..45c6f2100 --- /dev/null +++ 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 @@ -0,0 +1,192 @@ + + +
+
+
+ + 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 new file mode 100644 index 000000000..b2e03a37d --- /dev/null +++ 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 @@ -0,0 +1,151 @@ +/** + * 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 new file mode 100644 index 000000000..0d42c8ff1 --- /dev/null +++ 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 @@ -0,0 +1,169 @@ +/** + * 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 new file mode 100644 index 000000000..0ec83acee --- /dev/null +++ b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-product.resolver.ts @@ -0,0 +1,37 @@ +/** + * 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 new file mode 100644 index 000000000..b2973f1f2 --- /dev/null +++ b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.component.html @@ -0,0 +1,172 @@ + + +
+
+
+
+
+

{{ '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 new file mode 100644 index 000000000..a880f9b22 --- /dev/null +++ b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.component.scss @@ -0,0 +1,17 @@ +/** + * 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 new file mode 100644 index 000000000..7af670304 --- /dev/null +++ b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.component.ts @@ -0,0 +1,124 @@ +/** + * 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 new file mode 100644 index 000000000..544bdbe0c --- /dev/null +++ b/src/app/products/daily-deposit-scheme-products/daily-deposit-scheme-products.resolver.ts @@ -0,0 +1,35 @@ +/** + * 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 new file mode 100644 index 000000000..ca7c90dfb --- /dev/null +++ b/src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/daily-deposit-scheme-product-and-template.resolver.ts @@ -0,0 +1,35 @@ +/** + * 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 new file mode 100644 index 000000000..3ce623701 --- /dev/null +++ b/src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/edit-daily-deposit-scheme-product.component.html @@ -0,0 +1,202 @@ + + +
+
+ +

{{ '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 new file mode 100644 index 000000000..c04c383f8 --- /dev/null +++ b/src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/edit-daily-deposit-scheme-product.component.scss @@ -0,0 +1,138 @@ +/** + * 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 new file mode 100644 index 000000000..38d715746 --- /dev/null +++ b/src/app/products/daily-deposit-scheme-products/edit-daily-deposit-scheme-product/edit-daily-deposit-scheme-product.component.ts @@ -0,0 +1,305 @@ +/** + * 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 new file mode 100644 index 000000000..c8d7995ea --- /dev/null +++ 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 @@ -0,0 +1,17 @@ + + +
+ + +
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 new file mode 100644 index 000000000..af9f4c075 --- /dev/null +++ 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 @@ -0,0 +1,7 @@ +/** + * 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 new file mode 100644 index 000000000..4d5a895ed --- /dev/null +++ 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 @@ -0,0 +1,38 @@ +/** + * 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 new file mode 100644 index 000000000..8881ab235 --- /dev/null +++ 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 @@ -0,0 +1,843 @@ + + +
+ +
+
+
+
+
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 new file mode 100644 index 000000000..9e7b167c3 --- /dev/null +++ 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 @@ -0,0 +1,528 @@ +/** + * 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 new file mode 100644 index 000000000..ca0a551e7 --- /dev/null +++ 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 @@ -0,0 +1,121 @@ +/** + * 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 new file mode 100644 index 000000000..2295a36d2 --- /dev/null +++ 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 @@ -0,0 +1,68 @@ + + +
+ @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 new file mode 100644 index 000000000..1728940c9 --- /dev/null +++ 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 @@ -0,0 +1,40 @@ +/** + * 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 new file mode 100644 index 000000000..ef3ce4220 --- /dev/null +++ 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 @@ -0,0 +1,66 @@ +/** + * 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 new file mode 100644 index 000000000..68e82a666 --- /dev/null +++ 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 @@ -0,0 +1,16 @@ +/** + * 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 new file mode 100644 index 000000000..041cff962 --- /dev/null +++ b/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/view-daily-deposit-scheme-product.component.html @@ -0,0 +1,39 @@ + + + + +
+ + + + + + 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 new file mode 100644 index 000000000..74229104a --- /dev/null +++ b/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/view-daily-deposit-scheme-product.component.scss @@ -0,0 +1,22 @@ +/** + * 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 new file mode 100644 index 000000000..5e871f560 --- /dev/null +++ b/src/app/products/daily-deposit-scheme-products/view-daily-deposit-scheme-product/view-daily-deposit-scheme-product.component.ts @@ -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/. + */ + +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/products-routing.module.ts b/src/app/products/products-routing.module.ts index 034086845..9c0111bef 100644 --- a/src/app/products/products-routing.module.ts +++ b/src/app/products/products-routing.module.ts @@ -30,6 +30,12 @@ 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 { ChargesComponent } from './charges/charges.component'; import { ViewChargeComponent } from './charges/view-charge/view-charge.component'; import { CreateChargeComponent } from './charges/create-charge/create-charge.component'; @@ -75,6 +81,10 @@ 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'; @@ -724,6 +734,83 @@ const routes: Routes = [ } ] }, + { + path: 'daily-deposit-scheme-products', + data: { title: 'Daily Deposit Scheme Products', breadcrumb: 'Daily Deposit Scheme Products' }, + children: [ + { + path: 'create', + component: CreateDailyDepositSchemeProductComponent, + data: { title: 'Create Daily Deposit Scheme Product', breadcrumb: 'Create' }, + resolve: { + dailyDepositSchemeProductsTemplate: DailyDepositSchemeProductsTemplateResolver + } + }, + { + path: '', + component: DailyDepositSchemeProductsComponent, + resolve: { + dailyDepositSchemeProducts: DailyDepositSchemeProductsResolver + } + }, + { + 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: 'fixed-deposit-products', data: { title: 'Fixed Deposit Products', breadcrumb: 'Fixed Deposit Products' }, @@ -1004,6 +1091,10 @@ 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 3345cdb55..07d63d8db 100644 --- a/src/app/products/products.component.html +++ b/src/app/products/products.component.html @@ -125,6 +125,25 @@
+
+ +
+ +
+
+

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

+

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

+
+ +
+
+