From 4679c6f729cb023dee4aa832d37f3bd7578f0269 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Mon, 13 Apr 2026 14:02:54 +0530 Subject: [PATCH 01/12] update saving product code --- .husky/{pre-commit => pre-commit.bak} | 0 .qwen/settings.json | 13 + .qwen/settings.json.orig | 7 + src/app/core/utils/accounting.ts | 4 +- .../create-saving-product.component.html | 29 +- .../create-saving-product.component.scss | 8 +- .../create-saving-product.component.ts | 40 +- .../edit-saving-product.component.html | 25 +- .../edit-saving-product.component.scss | 8 +- .../edit-saving-product.component.ts | 41 +- ...ing-product-accounting-step.component.html | 452 +++------- ...ing-product-accounting-step.component.scss | 361 ++++++-- ...aving-product-accounting-step.component.ts | 852 +++++++++--------- ...saving-product-charges-step.component.scss | 2 +- ...aving-product-currency-step.component.html | 119 ++- ...aving-product-currency-step.component.scss | 58 +- .../saving-product-currency-step.component.ts | 66 +- ...saving-product-details-step.component.html | 60 ++ ...saving-product-details-step.component.scss | 12 +- .../saving-product-details-step.component.ts | 139 ++- .../saving-product-limits-step.component.html | 351 ++++++++ .../saving-product-limits-step.component.scss | 257 ++++++ .../saving-product-limits-step.component.ts | 250 +++++ ...saving-product-preview-step.component.html | 512 ++++++++++- ...saving-product-preview-step.component.scss | 92 ++ .../saving-product-preview-step.component.ts | 185 +++- ...aving-product-settings-step.component.html | 377 ++------ ...aving-product-settings-step.component.scss | 127 ++- .../saving-product-settings-step.component.ts | 178 ++-- .../saving-product-terms-step.component.html | 240 ++--- .../saving-product-terms-step.component.scss | 85 +- .../saving-product-terms-step.component.ts | 140 ++- .../saving-product.resolver.ts | 2 +- .../saving-product-general-tab.component.html | 766 ++++++++++------ .../saving-product-general-tab.component.scss | 92 ++ .../saving-product-general-tab.component.ts | 37 + src/environments/.env.ts | 5 +- 37 files changed, 4103 insertions(+), 1889 deletions(-) rename .husky/{pre-commit => pre-commit.bak} (100%) create mode 100644 .qwen/settings.json create mode 100644 .qwen/settings.json.orig create mode 100644 src/app/products/saving-products/saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component.html create mode 100644 src/app/products/saving-products/saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component.scss create mode 100644 src/app/products/saving-products/saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component.ts diff --git a/.husky/pre-commit b/.husky/pre-commit.bak similarity index 100% rename from .husky/pre-commit rename to .husky/pre-commit.bak diff --git a/.qwen/settings.json b/.qwen/settings.json new file mode 100644 index 000000000..e16bd106b --- /dev/null +++ b/.qwen/settings.json @@ -0,0 +1,13 @@ +{ + "permissions": { + "allow": [ + "Bash(npx *)", + "Bash(git add *)", + "Bash(git commit *)", + "Bash(select-object *)", + "Bash(git config *)", + "Bash(set *)" + ] + }, + "$version": 3 +} \ No newline at end of file diff --git a/.qwen/settings.json.orig b/.qwen/settings.json.orig new file mode 100644 index 000000000..0399b79c8 --- /dev/null +++ b/.qwen/settings.json.orig @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(npx *)" + ] + } +} \ No newline at end of file diff --git a/src/app/core/utils/accounting.ts b/src/app/core/utils/accounting.ts index f48751e35..fbedb5c2f 100644 --- a/src/app/core/utils/accounting.ts +++ b/src/app/core/utils/accounting.ts @@ -87,9 +87,9 @@ export class Accounting { } else if (accountingRuleId === 2) { return { code: 'CASH', id: accountingRuleId, value: 'Cash' }; } else if (accountingRuleId === 3) { - return { code: 'ACCRUAL_PERIODIC', id: accountingRuleId, value: 'Accural (periodic)' }; + return { code: 'ACCRUAL_PERIODIC', id: accountingRuleId, value: 'Accrual (periodic)' }; } else if (accountingRuleId === 4) { - return { code: 'ACCRUAL_UPFRONT', id: accountingRuleId, value: 'Accural (upfront)' }; + return { code: 'ACCRUAL_UPFRONT', id: accountingRuleId, value: 'Accrual (upfront)' }; } return { code: 'INVALID', id: accountingRuleId, value: 'Invalid' }; } diff --git a/src/app/products/saving-products/create-saving-product/create-saving-product.component.html b/src/app/products/saving-products/create-saving-product/create-saving-product.component.html index 25f808f3b..94bca61e3 100644 --- a/src/app/products/saving-products/create-saving-product/create-saving-product.component.html +++ b/src/app/products/saving-products/create-saving-product/create-saving-product.component.html @@ -12,14 +12,21 @@

{{ 'labels.heading.Create Savings Product' | translate }}

- + {{ 'labels.inputs.DETAILS' | translate }}
- +
@@ -72,6 +79,18 @@ + + + + Digital + +
+ +
+
+ @@ -114,19 +133,19 @@
@if (selectedTabIndex > 0) { } @if (selectedTabIndex < totalTabs - 1) { } @if (savingProductFormValid && selectedTabIndex === totalTabs - 1) { } diff --git a/src/app/products/saving-products/create-saving-product/create-saving-product.component.scss b/src/app/products/saving-products/create-saving-product/create-saving-product.component.scss index 8bfaacb7f..7a61d6712 100644 --- a/src/app/products/saving-products/create-saving-product/create-saving-product.component.scss +++ b/src/app/products/saving-products/create-saving-product/create-saving-product.component.scss @@ -35,11 +35,11 @@ $primary: #1353a0; .sp-tabs { background: var(--md-sys-color-surface, #fff); border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + box-shadow: 0 2px 8px rgb(0 0 0 / 8%); ::ng-deep { .mat-mdc-tab-header { - border-bottom: 1px solid rgba(0, 0, 0, 0.08); + border-bottom: 1px solid rgb(0 0 0 / 8%); } .mat-mdc-tab { @@ -77,10 +77,10 @@ $primary: #1353a0; align-items: center; margin-top: 20px; padding: 16px 0; - border-top: 1px solid rgba(0, 0, 0, 0.08); + border-top: 1px solid rgb(0 0 0 / 8%); &__cancel { - color: rgba(0, 0, 0, 0.6); + color: rgb(0 0 0 / 60%); } &__right { diff --git a/src/app/products/saving-products/create-saving-product/create-saving-product.component.ts b/src/app/products/saving-products/create-saving-product/create-saving-product.component.ts index 2f8d66171..c7bab6636 100644 --- a/src/app/products/saving-products/create-saving-product/create-saving-product.component.ts +++ b/src/app/products/saving-products/create-saving-product/create-saving-product.component.ts @@ -1,4 +1,4 @@ -/** +/** * Copyright since 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public @@ -16,6 +16,7 @@ import { SavingProductCurrencyStepComponent } from '../saving-product-stepper/sa import { SavingProductTermsStepComponent } from '../saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component'; import { SavingProductSettingsStepComponent } from '../saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component'; import { SavingProductChargesStepComponent } from '../saving-product-stepper/saving-product-charges-step/saving-product-charges-step.component'; +import { SavingProductLimitsStepComponent } from '../saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component'; import { SavingProductAccountingStepComponent } from '../saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component'; /** Custom Services */ @@ -38,6 +39,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; SavingProductTermsStepComponent, SavingProductSettingsStepComponent, SavingProductChargesStepComponent, + SavingProductLimitsStepComponent, SavingProductAccountingStepComponent, SavingProductPreviewStepComponent ] @@ -58,6 +60,8 @@ export class CreateSavingProductComponent { savingProductSettingsStep: SavingProductSettingsStepComponent; @ViewChild(SavingProductChargesStepComponent, { static: true }) savingProductChargesStep: SavingProductChargesStepComponent; + @ViewChild(SavingProductLimitsStepComponent, { static: true }) + savingProductLimitsStep: SavingProductLimitsStepComponent; @ViewChild(SavingProductAccountingStepComponent, { static: true }) savingProductAccountingStep: SavingProductAccountingStepComponent; @@ -100,7 +104,7 @@ export class CreateSavingProductComponent { } get totalTabs(): number { - return this.savingProductFormValid ? 7 : 6; + return this.savingProductFormValid ? 8 : 7; } get savingProductFormValid() { @@ -120,18 +124,44 @@ export class CreateSavingProductComponent { ...this.savingProductTermsStep.savingProductTerms, ...this.savingProductSettingsStep.savingProductSettings, ...this.savingProductChargesStep.savingProductCharges, + ...this.savingProductLimitsStep.savingProductLimits, ...this.savingProductAccountingStep.savingProductAccounting }; } - submit() { - // TODO: Update once language and date settings are setup + private prepareSavingProductPayload() { const savingProduct = { ...this.savingProduct, charges: this.savingProduct.charges.map((charge: any) => ({ id: charge.id })), - locale: this.settingsService.language.code // locale required for nominalAnnualInterestRate + locale: this.settingsService.language.code, + dateFormat: 'dd MMMM yyyy' }; delete savingProduct.advancedAccountingRules; + + // Convert lockinPeriodFrequencyType from string to integer + // Backend expects: 0=DAYS, 1=WEEKS, 2=MONTHS, 3=YEARS + if (savingProduct.lockinPeriodFrequencyType) { + const frequencyTypeMapping: { [key: string]: number } = { + DAYS: 0, + WEEKS: 1, + MONTHS: 2, + YEARS: 3 + }; + savingProduct.lockinPeriodFrequencyType = frequencyTypeMapping[savingProduct.lockinPeriodFrequencyType]; + } + + Object.keys(savingProduct).forEach((key) => { + if (savingProduct[key] === '' || savingProduct[key] === null || savingProduct[key] === undefined) { + delete savingProduct[key]; + } + }); + + return savingProduct; + } + + submit() { + const savingProduct = this.prepareSavingProductPayload(); + console.log('Saving product submit payload:', savingProduct); this.productsService.createSavingProduct(savingProduct).subscribe((response: any) => { this.router.navigate( [ diff --git a/src/app/products/saving-products/edit-saving-product/edit-saving-product.component.html b/src/app/products/saving-products/edit-saving-product/edit-saving-product.component.html index dfc39c64b..708c73f55 100644 --- a/src/app/products/saving-products/edit-saving-product/edit-saving-product.component.html +++ b/src/app/products/saving-products/edit-saving-product/edit-saving-product.component.html @@ -12,7 +12,12 @@

{{ 'labels.heading.Edit Savings Product' | translate }}

- + @@ -74,6 +79,18 @@ + + + + Digital + +
+ +
+
+ @@ -116,19 +133,19 @@
@if (selectedTabIndex > 0) { } @if (selectedTabIndex < totalTabs - 1) { } @if (savingProductFormValidAndNotPristine && selectedTabIndex === totalTabs - 1) { } diff --git a/src/app/products/saving-products/edit-saving-product/edit-saving-product.component.scss b/src/app/products/saving-products/edit-saving-product/edit-saving-product.component.scss index 8bfaacb7f..7a61d6712 100644 --- a/src/app/products/saving-products/edit-saving-product/edit-saving-product.component.scss +++ b/src/app/products/saving-products/edit-saving-product/edit-saving-product.component.scss @@ -35,11 +35,11 @@ $primary: #1353a0; .sp-tabs { background: var(--md-sys-color-surface, #fff); border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + box-shadow: 0 2px 8px rgb(0 0 0 / 8%); ::ng-deep { .mat-mdc-tab-header { - border-bottom: 1px solid rgba(0, 0, 0, 0.08); + border-bottom: 1px solid rgb(0 0 0 / 8%); } .mat-mdc-tab { @@ -77,10 +77,10 @@ $primary: #1353a0; align-items: center; margin-top: 20px; padding: 16px 0; - border-top: 1px solid rgba(0, 0, 0, 0.08); + border-top: 1px solid rgb(0 0 0 / 8%); &__cancel { - color: rgba(0, 0, 0, 0.6); + color: rgb(0 0 0 / 60%); } &__right { diff --git a/src/app/products/saving-products/edit-saving-product/edit-saving-product.component.ts b/src/app/products/saving-products/edit-saving-product/edit-saving-product.component.ts index 673113c16..1ad5e4b11 100644 --- a/src/app/products/saving-products/edit-saving-product/edit-saving-product.component.ts +++ b/src/app/products/saving-products/edit-saving-product/edit-saving-product.component.ts @@ -1,4 +1,4 @@ -/** +/** * Copyright since 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public @@ -16,6 +16,7 @@ import { SavingProductCurrencyStepComponent } from '../saving-product-stepper/sa import { SavingProductTermsStepComponent } from '../saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component'; import { SavingProductSettingsStepComponent } from '../saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component'; import { SavingProductChargesStepComponent } from '../saving-product-stepper/saving-product-charges-step/saving-product-charges-step.component'; +import { SavingProductLimitsStepComponent } from '../saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component'; import { SavingProductAccountingStepComponent } from '../saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component'; /** Custom Services */ @@ -38,6 +39,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; SavingProductTermsStepComponent, SavingProductSettingsStepComponent, SavingProductChargesStepComponent, + SavingProductLimitsStepComponent, SavingProductAccountingStepComponent, SavingProductPreviewStepComponent ] @@ -58,6 +60,8 @@ export class EditSavingProductComponent { savingProductSettingsStep: SavingProductSettingsStepComponent; @ViewChild(SavingProductChargesStepComponent, { static: true }) savingProductChargesStep: SavingProductChargesStepComponent; + @ViewChild(SavingProductLimitsStepComponent, { static: true }) + savingProductLimitsStep: SavingProductLimitsStepComponent; @ViewChild(SavingProductAccountingStepComponent, { static: true }) savingProductAccountingStep: SavingProductAccountingStepComponent; @@ -100,7 +104,7 @@ export class EditSavingProductComponent { } get totalTabs(): number { - return this.savingProductFormValidAndNotPristine ? 7 : 6; + return this.savingProductFormValidAndNotPristine ? 8 : 7; } get savingProductFormValidAndNotPristine() { @@ -115,6 +119,7 @@ export class EditSavingProductComponent { !this.savingProductTermsForm.pristine || !this.savingProductSettingsForm.pristine || !this.savingProductChargesStep.pristine || + !this.savingProductLimitsStep.pristine || !this.savingProductAccountingForm.pristine) ); } @@ -126,18 +131,44 @@ export class EditSavingProductComponent { ...this.savingProductTermsStep.savingProductTerms, ...this.savingProductSettingsStep.savingProductSettings, ...this.savingProductChargesStep.savingProductCharges, + ...this.savingProductLimitsStep.savingProductLimits, ...this.savingProductAccountingStep.savingProductAccounting }; } - submit() { - // TODO: Update once language and date settings are setup + private prepareSavingProductPayload() { const savingProduct = { ...this.savingProduct, charges: this.savingProduct.charges.map((charge: any) => ({ id: charge.id })), - locale: this.settingsService.language.code // locale required for nominalAnnualInterestRate + locale: this.settingsService.language.code, + dateFormat: 'dd MMMM yyyy' }; delete savingProduct.advancedAccountingRules; + + // Convert lockinPeriodFrequencyType from string to integer + // Backend expects: 0=DAYS, 1=WEEKS, 2=MONTHS, 3=YEARS + if (savingProduct.lockinPeriodFrequencyType) { + const frequencyTypeMapping: { [key: string]: number } = { + DAYS: 0, + WEEKS: 1, + MONTHS: 2, + YEARS: 3 + }; + savingProduct.lockinPeriodFrequencyType = frequencyTypeMapping[savingProduct.lockinPeriodFrequencyType]; + } + + Object.keys(savingProduct).forEach((key) => { + if (savingProduct[key] === '' || savingProduct[key] === null || savingProduct[key] === undefined) { + delete savingProduct[key]; + } + }); + + return savingProduct; + } + + submit() { + const savingProduct = this.prepareSavingProductPayload(); + console.log('Saving product update payload:', savingProduct); this.productsService .updateSavingProduct(this.savingProductAndTemplate.id, savingProduct) .subscribe((response: any) => { diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.html b/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.html index 6214db809..64b0a162f 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.html +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.html @@ -1,4 +1,4 @@ -
-
- -

{{ 'labels.inputs.Accounting Rule' | translate }}

-
-
- - @for (accountingRule of accountingRuleData; track accountingRule; let i = $index) { - - {{ 'labels.accounting.' + accountingRule | translate }} - - } - -
-
- - @if (isCashOrAccrualAccounting()) { - -
-
- -

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

-
-
- - - - - @if (isAccrualAccounting()) { - - - - } - - @if (isAccrualAccounting() && allowOverdraft.value) { - - } -
+ + + +

Accounting

- -
-
- -

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

-
-
- - - - - @if (isAccrualAccounting()) { - - } - - @if (isDormancyTrackingActive.value) { - - } -
-
- - -
-
- -

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

-
-
- - - -
-
- - -
-
- -

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

-
-
- - - - - -
-
- - -
-
- -

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

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

Accounting Rule

+ + @for (accountingRule of accountingRuleData; track accountingRule; let i = $index) { + + {{ 'labels.accounting.' + accountingRule | translate }} + + } + +
- @if (savingProductAccountingForm.value.advancedAccountingRules) { - -
-
-

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

-
- - @if (paymentChannelToFundSourceMappings.value.length !== 0) { -
- - - - - - - - - - - - - - - -
{{ 'labels.inputs.Payment Type' | translate }} - {{ paymentFundSource.paymentTypeId | find: paymentTypeData : 'id' : 'name' }} - {{ 'labels.inputs.Fund Source' | translate }} - {{ paymentFundSource.fundSourceAccountId | find: combinedAccountData : 'id' : 'name' }} - {{ 'labels.inputs.Actions' | translate }} - - -
-
- } + {{ 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 }} + + } + + +
+
+ +
+ } +
- - - @if (existCharges()) { -
-
-

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

- -
- - @if (feeToIncomeAccountMappings.value.length !== 0) { -
- - - - - - - - - - - - - - - -
{{ 'labels.inputs.Fees' | translate }} - {{ feesIncome.chargeId | find: chargeData : 'id' : 'name' }} - {{ 'labels.inputs.Income Account' | translate }} - {{ feesIncome.incomeAccountId | find: incomeAccountData : 'id' : 'name' }} - {{ 'labels.inputs.Actions' | translate }} - - -
-
- } -
- - -
-
-

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

- -
- - @if (penaltyToIncomeAccountMappings.value.length !== 0) { -
- - - - - - - - - - - - - - - -
{{ 'labels.inputs.Penalty' | translate }} - {{ penaltyIncome.chargeId | find: penaltyData : 'id' : 'name' }} - {{ 'labels.inputs.Income Account' | translate }} - {{ penaltyIncome.incomeAccountId | find: incomeAccountData : 'id' : 'name' }} - {{ 'labels.inputs.Actions' | translate }} - - -
-
- } -
- } } -
+ }
- } + diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.scss b/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.scss index d9652bdb9..30125e801 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.scss +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.scss @@ -6,124 +6,309 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -$primary: #1353a0; -$accent: #0ea5e9; $border: #e2e8f0; $gray-50: #f8f9fa; $gray-200: #e8eaed; -$gray-500: #9aa0a6; -$gray-700: #5f6368; +$gray-500: #6b7280; +$gray-700: #334155; $gray-900: #202124; +$primary: #1353a0; .spa-section { border: 1px solid $border; border-radius: 10px; overflow: hidden; 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; - } - } } -.spa-body { - padding: 16px 24px; -} - -.spa-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 4px 16px; - padding: 16px 24px; - - @media (max-width: 600px) { - grid-template-columns: 1fr; - } -} - -.spa-field { - width: 100%; - - &--full { - grid-column: 1 / -1; - } -} - -.spa-radio-group { - display: flex; - gap: 24px; - flex-wrap: wrap; - - @media (max-width: 600px) { - flex-direction: column; - gap: 12px; - } -} - -.spa-checkbox-row { +.spa-section__header { display: flex; align-items: center; - padding: 4px 0; - margin-bottom: 12px; + 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; + } } -.spa-subsection { - margin-top: 16px; - padding-top: 16px; - border-top: 1px solid $gray-200; +.spa-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; +} + +.spa-section__body { + padding: 16px 24px 20px; +} + +.spa-group { + & + & { + margin-top: 20px; + } &__header { display: flex; align-items: center; justify-content: space-between; - gap: 12px; - margin-bottom: 12px; + gap: 16px; + margin: 0 0 12px; + } - h4 { - margin: 0; - font-size: 13px; - font-weight: 600; - color: $gray-700; - } + h4 { + margin: 0 0 12px; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #94a3b8; } } .spa-add-btn { - height: 32px !important; - border-radius: 8px !important; - font-size: 12px !important; - font-weight: 600 !important; - box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - white-space: nowrap; + border: 0; + background: transparent; + color: #2563eb; + font-size: 12px; + font-weight: 700; + padding: 0; + cursor: pointer; } -.spa-table-wrap { - border: 1px solid $border; +.spa-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.spa-list + .spa-list { + margin-top: 12px; +} + +.spa-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 10px 12px; + border: 1px solid #e2e8f0; border-radius: 8px; - overflow: hidden; + background: #fff; + + @media (width <= 900px) { + flex-direction: column; + align-items: stretch; + } } -.spa-table { +.spa-row:has(mifosx-gl-account-selector) { + align-items: stretch; +} + +.spa-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; + } +} + +.spa-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; + } +} + +.spa-row__meta { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + flex: 1 1 auto; +} + +.spa-row__meta--other { + min-width: 0; + flex: 1 1 auto; + align-self: stretch; +} + +.spa-row__label { + font-size: 14px; + font-weight: 600; + color: $gray-700; +} + +.spa-row__input { + flex: 0 0 22rem; + max-width: 22rem; + min-width: 18rem; + + @media (width <= 900px) { + width: 100%; + max-width: 100%; + min-width: 0; + } +} + +.spa-row__input--other { + flex: 1 1 auto; + max-width: 100%; + min-width: 0; +} + +.spa-row__input :is(mifosx-gl-account-selector) { + width: 100%; + display: block; +} + +.spa-row__input :is(mifosx-gl-account-selector) :is(.layout-column) { + width: 100%; + margin-right: 0 !important; +} + +.spa-row__input :is(mifosx-gl-account-selector) :is(mat-form-field) { width: 100%; } + +.spa-row__actions { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.spa-row__actions--other { + justify-content: flex-end; + align-self: stretch; + width: 100%; + + @media (width <= 1100px) { + justify-content: flex-start; + } +} + +.spa-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%; + } +} + +.spa-row__name--code { + max-width: 20rem; +} + +.spa-row__name--other-account { + max-width: 100%; + flex: 1 1 auto; + height: 48px; +} + +.spa-row__other-field { + width: 100%; + min-width: 0; +} + +.spa-row__other-field :is(mat-form-field) { + width: 100%; +} + +.spa-row__other-field :is(.mat-mdc-form-field-required-marker) { + display: none; +} + +.spa-remove-btn { + border: 0; + background: transparent; + color: #ef4444; + font-size: 18px; + line-height: 1; + cursor: pointer; + padding: 0 4px; +} + +.spa-remove-btn--side { + place-self: center end; + margin-right: 4px; + + @media (width <= 1100px) { + justify-self: start; + margin-right: 0; + } +} + +.spa-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; + } +} + +datalist { + display: none; +} + +@media (width <= 900px) { + .spa-row__meta { + margin-bottom: 4px; + } + + .spa-row__input { + flex: 1 1 auto; + max-width: 100%; + } +} diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.ts b/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.ts index 6a1d22664..d148f29c3 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.ts +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.ts @@ -1,4 +1,4 @@ -/** +/** * Copyright since 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public @@ -6,489 +6,495 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, OnInit, Input, inject } from '@angular/core'; +import { Component, Input, OnInit, inject } from '@angular/core'; import { - UntypedFormGroup, - UntypedFormBuilder, + FormsModule, + ReactiveFormsModule, UntypedFormArray, - Validators, + UntypedFormBuilder, UntypedFormControl, - ReactiveFormsModule + UntypedFormGroup, + Validators } from '@angular/forms'; -import { MatDialog } from '@angular/material/dialog'; - -import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component'; -import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; - -import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base'; -import { SelectBase } from 'app/shared/form-dialog/formfield/model/select-base'; -import { TranslateService } from '@ngx-translate/core'; -import { MatRadioGroup, MatRadioButton } from '@angular/material/radio'; -import { GlAccountSelectorComponent } from '../../../../shared/accounting/gl-account-selector/gl-account-selector.component'; -import { MatCheckbox } from '@angular/material/checkbox'; -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 { FindPipe } from '../../../../pipes/find.pipe'; +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-saving-product-accounting-step', templateUrl: './saving-product-accounting-step.component.html', styleUrls: ['./saving-product-accounting-step.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, + FormsModule, + ReactiveFormsModule, MatRadioGroup, MatRadioButton, - GlAccountSelectorComponent, - MatCheckbox, - FaIconComponent, - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatIconButton, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow, - FindPipe + GlAccountSelectorComponent ] }) export class SavingProductAccountingStepComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); - private dialog = inject(MatDialog); - private translateService = inject(TranslateService); + private accounting = inject(Accounting); @Input() savingProductsTemplate: any; @Input() accountingRuleData: any; - @Input() isDormancyTrackingActive: UntypedFormControl; + @Input() isDormancyTrackingActive: any; @Input() savingProductFormValid: boolean; - @Input() allowOverdraft: UntypedFormControl; + @Input() allowOverdraft: any; savingProductAccountingForm: UntypedFormGroup; - - chargeData: any; - penaltyData: any; - paymentTypeData: any; - assetAccountData: any; - incomeAccountData: any; - expenseAccountData: any; - liabilityAccountData: any; - incomeAndLiabilityAccountData: any; - combinedAccountData: any[]; - - paymentFundSourceDisplayedColumns: string[] = [ - 'paymentTypeId', - 'fundSourceAccountId', - 'actions' - ]; - feesPenaltyIncomeDisplayedColumns: string[] = [ - 'chargeId', - 'incomeAccountId', - 'actions' - ]; + liabilityAccountOptions: GLAccount[] = []; + expenseAccountOptions: GLAccount[] = []; + incomeAccountOptions: GLAccount[] = []; + assetAccountOptions: GLAccount[] = []; + accountingSectionConfigs: AccountingSectionConfig[] = []; constructor() { - this.createsavingProductAccountingForm(); + this.createSavingProductAccountingForm(); this.setConditionalControls(); } ngOnInit() { - this.chargeData = this.savingProductsTemplate.chargeOptions || []; - this.penaltyData = this.savingProductsTemplate.penaltyOptions || []; - this.paymentTypeData = this.savingProductsTemplate.paymentTypeOptions || []; - this.assetAccountData = this.savingProductsTemplate.accountingMappingOptions.assetAccountOptions || []; - this.incomeAccountData = this.savingProductsTemplate.accountingMappingOptions.incomeAccountOptions || []; - this.expenseAccountData = this.savingProductsTemplate.accountingMappingOptions.expenseAccountOptions || []; - this.liabilityAccountData = this.savingProductsTemplate.accountingMappingOptions.liabilityAccountOptions || []; - this.combinedAccountData = [ - ...this.assetAccountData, - ...this.incomeAccountData, - ...this.expenseAccountData, - ...this.liabilityAccountData - ]; - this.combinedAccountData.sort((a, b) => { - const valueA = a.name.toLowerCase(); - const valueB = b.name.toLowerCase(); - if (valueA < valueB) { - return -1; - } - if (valueA > valueB) { - return 1; - } - return 0; + const mappings = this.savingProductsTemplate?.accountingMappings || {}; + const accountingMappingOptions = this.savingProductsTemplate?.accountingMappingOptions || {}; + const accountingRuleId = this.savingProductsTemplate?.accountingRule?.id || 1; + this.assetAccountOptions = accountingMappingOptions.assetAccountOptions || []; + this.liabilityAccountOptions = accountingMappingOptions.liabilityAccountOptions || []; + this.expenseAccountOptions = accountingMappingOptions.expenseAccountOptions || []; + this.incomeAccountOptions = accountingMappingOptions.incomeAccountOptions || []; + this.buildAccountingSectionConfigs(); + this.savingProductAccountingForm.patchValue({ + accountingRule: accountingRuleId }); + this.syncAccountingControls(accountingRuleId); + this.patchAccountingValues(mappings); + this.setOverdraftControl(); + this.setDormancyControl(); + } + + createSavingProductAccountingForm() { + this.savingProductAccountingForm = this.formBuilder.group({ + accountingRule: [1], + assetsOtherGlMappings: this.formBuilder.array([]), + liabilitiesOtherGlMappings: this.formBuilder.array([]), + expensesOtherGlMappings: this.formBuilder.array([]), + incomeOtherGlMappings: this.formBuilder.array([]) + }); + } + + private setConditionalControls() { + this.savingProductAccountingForm.get('accountingRule')?.valueChanges.subscribe((accountingRule: number) => { + this.syncAccountingControls(accountingRule); + }); + } + + private patchAccountingValues(mappings: any) { this.savingProductAccountingForm.patchValue({ - accountingRule: this.savingProductsTemplate.accountingRule.id + savingsReferenceAccountId: mappings.savingsReferenceAccount?.id || '', + savingsControlAccountId: mappings.savingsControlAccount?.id || '', + transfersInSuspenseAccountId: mappings.transfersInSuspenseAccount?.id || '', + interestOnSavingsAccountId: mappings.interestOnSavingsAccount?.id || '', + incomeFromFeeAccountId: mappings.incomeFromFeeAccount?.id || '', + incomeFromPenaltyAccountId: mappings.incomeFromPenaltyAccount?.id || '', + incomeFromInterestId: mappings.incomeFromInterest?.id || '', + overdraftPortfolioControlId: mappings.overdraftPortfolioControl?.id || '', + writeOffAccountId: mappings.writeOffAccount?.id || '', + feesReceivableAccountId: mappings.feeReceivableAccount?.id || '', + penaltiesReceivableAccountId: mappings.penaltyReceivableAccount?.id || '', + interestPayableAccountId: mappings.interestPayableAccount?.id || '', + interestReceivableAccountId: mappings.interestReceivableAccount?.id || '', + escheatLiabilityId: mappings.escheatLiabilityAccount?.id || this.liabilityAccountOptions[0]?.id || '' }); if (this.isCashOrAccrualAccounting()) { - this.savingProductAccountingForm.patchValue({ - savingsReferenceAccountId: this.savingProductsTemplate.accountingMappings.savingsReferenceAccount.id, - overdraftPortfolioControlId: this.savingProductsTemplate.accountingMappings.overdraftPortfolioControl.id, - savingsControlAccountId: this.savingProductsTemplate.accountingMappings.savingsControlAccount.id, - transfersInSuspenseAccountId: this.savingProductsTemplate.accountingMappings.transfersInSuspenseAccount.id, - interestOnSavingsAccountId: this.savingProductsTemplate.accountingMappings.interestOnSavingsAccount.id, - writeOffAccountId: this.savingProductsTemplate.accountingMappings.writeOffAccount.id, - incomeFromFeeAccountId: this.savingProductsTemplate.accountingMappings.incomeFromFeeAccount.id, - incomeFromPenaltyAccountId: this.savingProductsTemplate.accountingMappings.incomeFromPenaltyAccount.id, - incomeFromInterestId: this.savingProductsTemplate.accountingMappings.incomeFromInterest.id, - advancedAccountingRules: - this.savingProductsTemplate.paymentChannelToFundSourceMappings || - this.savingProductsTemplate.feeToIncomeAccountMappings || - this.savingProductsTemplate.penaltyToIncomeAccountMappings || - this.savingProductsTemplate.accrualCharges - ? true - : false - }); - - if (this.isAccrualAccounting()) { - this.savingProductAccountingForm.patchValue({ - feesReceivableAccountId: this.savingProductsTemplate.accountingMappings.feeReceivableAccount.id, - penaltiesReceivableAccountId: this.savingProductsTemplate.accountingMappings.penaltyReceivableAccount.id, - interestReceivableAccountId: - this.savingProductsTemplate.accountingMappings.interestReceivableAccount == null || undefined - ? '' - : this.savingProductsTemplate.accountingMappings.interestReceivableAccount.id, - interestPayableAccountId: this.savingProductsTemplate.accountingMappings.interestPayableAccount.id - }); - } - - if (this.isDormancyTrackingActive.value) { - this.savingProductAccountingForm.patchValue({ - escheatLiabilityId: this.savingProductsTemplate.accountingMappings.escheatLiabilityAccount.id - }); - } - this.savingProductAccountingForm.setControl( - 'paymentChannelToFundSourceMappings', - this.formBuilder.array( - (this.savingProductsTemplate.paymentChannelToFundSourceMappings || []).map((paymentFundSource: any) => ({ - paymentTypeId: paymentFundSource.paymentType.id, - fundSourceAccountId: paymentFundSource.fundSourceAccount.id - })) - ) + this.setOtherGlMappings( + 'assetsOtherGlMappings', + mappings.otherGlMappings?.assets || [], + this.assetAccountOptions ); - this.savingProductAccountingForm.setControl( - 'feeToIncomeAccountMappings', - this.formBuilder.array( - (this.savingProductsTemplate.feeToIncomeAccountMappings || []).map((feesIncome: any) => ({ - chargeId: feesIncome.charge.id, - incomeAccountId: feesIncome.incomeAccount.id - })) - ) + this.setOtherGlMappings( + 'liabilitiesOtherGlMappings', + mappings.otherGlMappings?.liabilities || [], + this.liabilityAccountOptions ); - this.savingProductAccountingForm.setControl( - 'penaltyToIncomeAccountMappings', - this.formBuilder.array( - (this.savingProductsTemplate.penaltyToIncomeAccountMappings || []).map((penaltyIncome: any) => ({ - chargeId: penaltyIncome.charge.id, - incomeAccountId: penaltyIncome.incomeAccount.id - })) - ) + this.setOtherGlMappings( + 'expensesOtherGlMappings', + mappings.otherGlMappings?.expenses || [], + this.expenseAccountOptions + ); + this.setOtherGlMappings( + 'incomeOtherGlMappings', + mappings.otherGlMappings?.income || [], + this.incomeAccountOptions ); } } - createsavingProductAccountingForm() { - this.savingProductAccountingForm = this.formBuilder.group({ - accountingRule: [1] + private createOtherGlMappingGroup(otherGlMapping: Partial = {}) { + return this.formBuilder.group({ + accountName: [ + otherGlMapping.accountName || '', + Validators.required + ], + glAccountId: [ + otherGlMapping.glAccountId || '', + Validators.required + ] }); } - setConditionalControls() { - this.savingProductAccountingForm.get('accountingRule').valueChanges.subscribe((accountingRule: any) => { - if (accountingRule === 2 || accountingRule === 3) { - this.savingProductAccountingForm.addControl( - 'savingsReferenceAccountId', - new UntypedFormControl('', Validators.required) - ); - this.savingProductAccountingForm.addControl( - 'overdraftPortfolioControlId', - new UntypedFormControl('', Validators.required) - ); - this.savingProductAccountingForm.addControl( - 'savingsControlAccountId', - new UntypedFormControl('', Validators.required) - ); - this.savingProductAccountingForm.addControl( - 'transfersInSuspenseAccountId', - new UntypedFormControl('', Validators.required) - ); - this.savingProductAccountingForm.addControl( - 'interestOnSavingsAccountId', - new UntypedFormControl('', Validators.required) - ); - this.savingProductAccountingForm.addControl( - 'writeOffAccountId', - new UntypedFormControl('', Validators.required) - ); - this.savingProductAccountingForm.addControl( - 'incomeFromFeeAccountId', - new UntypedFormControl('', Validators.required) - ); - this.savingProductAccountingForm.addControl( - 'incomeFromPenaltyAccountId', - new UntypedFormControl('', Validators.required) - ); - this.savingProductAccountingForm.addControl( - 'incomeFromInterestId', - new UntypedFormControl('', Validators.required) - ); - this.savingProductAccountingForm.addControl('advancedAccountingRules', new UntypedFormControl(false)); + getOtherGlMappingsArray(controlName: string): UntypedFormArray { + return this.savingProductAccountingForm.get(controlName) as UntypedFormArray; + } - if (accountingRule === 3) { - this.savingProductAccountingForm.addControl( - 'feesReceivableAccountId', - new UntypedFormControl('', Validators.required) - ); - this.savingProductAccountingForm.addControl( - 'penaltiesReceivableAccountId', - new UntypedFormControl('', Validators.required) - ); - if (this.allowOverdraft.value) { - this.savingProductAccountingForm.addControl('interestReceivableAccountId', new UntypedFormControl('')); - } - this.allowOverdraft.valueChanges.subscribe((allowOverdraft: boolean) => { - if (allowOverdraft) { - this.savingProductAccountingForm.addControl('interestReceivableAccountId', new UntypedFormControl('')); - } else { - this.savingProductAccountingForm.removeControl('interestReceivableAccountId'); - } - }); - this.savingProductAccountingForm.addControl( - 'interestPayableAccountId', - new UntypedFormControl('', Validators.required) - ); - } - if (accountingRule === 2) { - this.savingProductAccountingForm.removeControl('feesReceivableAccountId'); - this.savingProductAccountingForm.removeControl('penaltiesReceivableAccountId'); - this.savingProductAccountingForm.removeControl('interestPayableAccountId'); - } + private setOtherGlMappings(controlName: string, otherGlMappings: any[], options: GLAccount[]) { + const otherGlMappingsArray = this.getOtherGlMappingsArray(controlName); + otherGlMappingsArray.clear(); + (otherGlMappings || []).forEach((otherGlMapping) => { + otherGlMappingsArray.push( + this.createOtherGlMappingGroup({ + accountName: otherGlMapping.accountName, + glAccountId: + otherGlMapping.glAccountId || options.find((option) => option.glCode === otherGlMapping.glCode)?.id || null + }) + ); + }); + } - if (this.isDormancyTrackingActive.value) { - this.savingProductAccountingForm.addControl( - 'escheatLiabilityId', - new UntypedFormControl('', Validators.required) - ); - } + addOtherGlMapping(controlName: string) { + this.getOtherGlMappingsArray(controlName).push(this.createOtherGlMappingGroup()); + } - this.isDormancyTrackingActive.valueChanges.subscribe((isDormancyTrackingActive: boolean) => { - if (isDormancyTrackingActive) { - this.savingProductAccountingForm.addControl( - 'escheatLiabilityId', - new UntypedFormControl('', Validators.required) - ); - } else { - this.savingProductAccountingForm.removeControl('escheatLiabilityId'); - } - }); + removeOtherGlMapping(controlName: string, index: number) { + this.getOtherGlMappingsArray(controlName).removeAt(index); + } - this.savingProductAccountingForm - .get('advancedAccountingRules') - .valueChanges.subscribe((advancedAccountingRules: boolean) => { - if (advancedAccountingRules) { - this.savingProductAccountingForm.addControl( - 'paymentChannelToFundSourceMappings', - this.formBuilder.array([]) - ); - this.savingProductAccountingForm.addControl('feeToIncomeAccountMappings', this.formBuilder.array([])); - this.savingProductAccountingForm.addControl('penaltyToIncomeAccountMappings', this.formBuilder.array([])); - } else { - this.savingProductAccountingForm.removeControl('paymentChannelToFundSourceMappings'); - this.savingProductAccountingForm.removeControl('feeToIncomeAccountMappings'); - this.savingProductAccountingForm.removeControl('penaltyToIncomeAccountMappings'); - } - }); + 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((controlName) => this.getOtherGlMappingsArray(controlName).clear()); + } + + private setOverdraftControl() { + this.allowOverdraft?.valueChanges?.subscribe(() => { + this.syncAccountingControls(this.savingProductAccountingForm.value.accountingRule); + }); + } + + private setDormancyControl() { + this.isDormancyTrackingActive?.valueChanges?.subscribe(() => { + this.syncAccountingControls(this.savingProductAccountingForm.value.accountingRule); + }); + } + + private getOtherGlMappings() { + const mapOtherGlMappings = (controlName: string, options: GLAccount[]) => + this.getOtherGlMappingsArray(controlName) + .value.filter((item: OtherGlMappingValue) => !!item.accountName || !!item.glAccountId) + .map((item: OtherGlMappingValue) => ({ + accountName: item.accountName, + glCode: options.find((option) => option.id === item.glAccountId)?.glCode || '' + })) + .filter((item: { accountName: string; glCode: string }) => !!item.accountName || !!item.glCode); + + const otherGlMappings = { + assets: mapOtherGlMappings('assetsOtherGlMappings', this.assetAccountOptions), + liabilities: mapOtherGlMappings('liabilitiesOtherGlMappings', this.liabilityAccountOptions), + expenses: mapOtherGlMappings('expensesOtherGlMappings', this.expenseAccountOptions), + income: mapOtherGlMappings('incomeOtherGlMappings', this.incomeAccountOptions) + }; + + return Object.values(otherGlMappings).some((value) => value.length) ? otherGlMappings : null; + } + + isDormancyTrackingEnabled(): boolean { + return this.isDormancyTrackingActive?.value ?? this.isDormancyTrackingActive ?? false; + } + + private syncAccountingControls(accountingRule: number) { + const isDormancyActive = this.isDormancyTrackingEnabled(); + + const addControl = (controlName: string, required = false) => { + if (!this.savingProductAccountingForm.contains(controlName)) { + this.savingProductAccountingForm.addControl( + controlName, + new UntypedFormControl('', required ? Validators.required : undefined) + ); + } else if (required) { + this.savingProductAccountingForm.get(controlName)?.setValidators(Validators.required); + this.savingProductAccountingForm.get(controlName)?.updateValueAndValidity({ emitEvent: false }); + } + }; + + const removeControl = (controlName: string) => { + if (this.savingProductAccountingForm.contains(controlName)) { + this.savingProductAccountingForm.removeControl(controlName); + } + }; + + if (this.accounting.isCashOrAccrualAccountingRuleId(accountingRule)) { + addControl('savingsReferenceAccountId', true); + addControl('savingsControlAccountId', true); + addControl('transfersInSuspenseAccountId', true); + addControl('interestOnSavingsAccountId', true); + addControl('incomeFromFeeAccountId', true); + addControl('incomeFromPenaltyAccountId', true); + addControl('incomeFromInterestId', true); + addControl('writeOffAccountId', true); + addControl('overdraftPortfolioControlId', false); + + if (this.accounting.isAccrualAccountingRuleId(accountingRule)) { + addControl('feesReceivableAccountId', true); + addControl('penaltiesReceivableAccountId', true); + addControl('interestPayableAccountId', true); + addControl('interestReceivableAccountId', false); } else { - this.savingProductAccountingForm.removeControl('savingsReferenceAccountId'); - this.savingProductAccountingForm.removeControl('overdraftPortfolioControlId'); - this.savingProductAccountingForm.removeControl('savingsControlAccountId'); - this.savingProductAccountingForm.removeControl('transfersInSuspenseAccountId'); - this.savingProductAccountingForm.removeControl('interestOnSavingsAccountId'); - this.savingProductAccountingForm.removeControl('writeOffAccountId'); - this.savingProductAccountingForm.removeControl('incomeFromFeeAccountId'); - this.savingProductAccountingForm.removeControl('incomeFromPenaltyAccountId'); - this.savingProductAccountingForm.removeControl('incomeFromInterestId'); - this.savingProductAccountingForm.removeControl('advancedAccountingRules'); - this.savingProductAccountingForm.removeControl('escheatLiabilityId'); - this.savingProductAccountingForm.removeControl('feesReceivableAccountId'); - this.savingProductAccountingForm.removeControl('penaltiesReceivableAccountId'); - this.savingProductAccountingForm.removeControl('interestReceivableAccountId'); - this.savingProductAccountingForm.removeControl('interestPayableAccountId'); + removeControl('feesReceivableAccountId'); + removeControl('penaltiesReceivableAccountId'); + removeControl('interestPayableAccountId'); + removeControl('interestReceivableAccountId'); } - }); - } - get paymentChannelToFundSourceMappings(): UntypedFormArray { - return this.savingProductAccountingForm.get('paymentChannelToFundSourceMappings') as UntypedFormArray; - } - - get feeToIncomeAccountMappings(): UntypedFormArray { - return this.savingProductAccountingForm.get('feeToIncomeAccountMappings') as UntypedFormArray; - } - - get penaltyToIncomeAccountMappings(): UntypedFormArray { - return this.savingProductAccountingForm.get('penaltyToIncomeAccountMappings') as UntypedFormArray; - } - - setSavingProductAccountingFormDirty() { - if (this.savingProductAccountingForm.pristine) { - this.savingProductAccountingForm.markAsDirty(); + if (isDormancyActive) { + addControl('escheatLiabilityId', false); + } else { + removeControl('escheatLiabilityId'); + } + } else { + removeControl('savingsReferenceAccountId'); + removeControl('savingsControlAccountId'); + removeControl('transfersInSuspenseAccountId'); + removeControl('interestOnSavingsAccountId'); + removeControl('incomeFromFeeAccountId'); + removeControl('incomeFromPenaltyAccountId'); + removeControl('incomeFromInterestId'); + removeControl('writeOffAccountId'); + removeControl('overdraftPortfolioControlId'); + removeControl('feesReceivableAccountId'); + removeControl('penaltiesReceivableAccountId'); + removeControl('interestPayableAccountId'); + removeControl('interestReceivableAccountId'); + removeControl('escheatLiabilityId'); + this.clearOtherGlMappings(); } } - existCharges(): boolean { - return this.chargeData.length > 0; - } - - add(formType: string, formArray: UntypedFormArray) { - const data = { ...this.getData(formType), pristine: false }; - const dialogRef = this.dialog.open(FormDialogComponent, { data, width: '20rem' }); - dialogRef.afterClosed().subscribe((response: any) => { - if (response.data) { - formArray.push(response.data); - this.setSavingProductAccountingFormDirty(); - } - }); - } - - edit(formType: string, formArray: UntypedFormArray, index: number) { - const data = { ...this.getData(formType, formArray.at(index).value), layout: { addButtonText: 'Edit' } }; - const dialogRef = this.dialog.open(FormDialogComponent, { data }); - dialogRef.afterClosed().subscribe((response: any) => { - if (response.data) { - formArray.at(index).patchValue(response.data.value); - this.setSavingProductAccountingFormDirty(); - } - }); - } - - delete(formArray: UntypedFormArray, index: number) { - const dialogRef = this.dialog.open(DeleteDialogComponent, { - data: { deleteContext: `this` } - }); - dialogRef.afterClosed().subscribe((response: any) => { - if (response.delete) { - formArray.removeAt(index); - this.setSavingProductAccountingFormDirty(); - } - }); - } - - getData(formType: string, values?: any) { - switch (formType) { - case 'PaymentFundSource': - return { - title: this.translateService.instant('labels.heading.Configure Fund Sources for Payment Channels'), - formfields: this.getPaymentFundSourceFormfields(values) - }; - case 'FeesIncome': - return { - title: this.translateService.instant('labels.heading.Map Fees to Specific Income Accounts'), - formfields: this.getFeesIncomeFormfields(values) - }; - case 'PenaltyIncome': - return { - title: this.translateService.instant('labels.heading.Map Penalties to Specific Income Accounts'), - formfields: this.getPenaltyIncomeFormfields(values) - }; - } - } - - getPaymentFundSourceFormfields(values?: any) { - const formfields: FormfieldBase[] = [ - new SelectBase({ - controlName: 'paymentTypeId', - label: this.translateService.instant('labels.inputs.Payment Type'), - value: values ? values.paymentTypeId : this.paymentTypeData[0].id, - options: { label: 'name', value: 'id', data: this.paymentTypeData }, - required: true, - order: 1 - }), - new SelectBase({ - controlName: 'fundSourceAccountId', - label: this.translateService.instant('labels.inputs.Fund Source'), - value: values ? values.fundSourceAccountId : this.combinedAccountData[0].id, - options: { label: 'name', value: 'id', data: this.combinedAccountData }, - required: true, - order: 2 - }) - ]; - return formfields; - } - - getFeesIncomeFormfields(values?: any) { - const formfields: FormfieldBase[] = [ - new SelectBase({ - controlName: 'chargeId', - label: this.translateService.instant('labels.inputs.Fees'), - value: values ? values.chargeId : this.chargeData[0].id, - options: { label: 'name', value: 'id', data: this.chargeData }, - required: true, - order: 1 - }), - new SelectBase({ - controlName: 'incomeAccountId', - label: this.translateService.instant('labels.inputs.Income Account'), - value: values ? values.incomeAccountId : this.incomeAccountData[0].id, - options: { label: 'name', value: 'id', data: this.incomeAccountData }, - required: true, - order: 2 - }) - ]; - return formfields; - } - - getPenaltyIncomeFormfields(values?: any) { - const formfields: FormfieldBase[] = [ - new SelectBase({ - controlName: 'chargeId', - label: this.translateService.instant('labels.inputs.Penalty'), - value: values ? values.chargeId : this.penaltyData[0].id, - options: { label: 'name', value: 'id', data: this.penaltyData }, - required: true, - order: 1 - }), - new SelectBase({ - controlName: 'incomeAccountId', - label: this.translateService.instant('labels.inputs.Income Account'), - value: values ? values.incomeAccountId : this.incomeAccountData[0].id, - options: { label: 'name', value: 'id', data: this.incomeAccountData }, - required: true, - order: 2 - }) - ]; - return formfields; - } - get savingProductAccounting() { - return this.savingProductAccountingForm.value; + const { + assetsOtherGlMappings, + liabilitiesOtherGlMappings, + expensesOtherGlMappings, + incomeOtherGlMappings, + ...savingProductAccounting + } = this.savingProductAccountingForm.value; + const otherGlMappings = this.getOtherGlMappings(); + + return { + ...savingProductAccounting, + ...(otherGlMappings ? { otherGlMappings } : {}) + }; } isCashOrAccrualAccounting(): boolean { - return ( - this.savingProductAccountingForm.value.accountingRule === 2 || - this.savingProductAccountingForm.value.accountingRule === 3 - ); + return this.accounting.isCashOrAccrualAccountingRuleId(this.savingProductAccountingForm.value.accountingRule); } isAccrualAccounting(): boolean { - return this.savingProductAccountingForm.value.accountingRule === 3; + return this.accounting.isAccrualAccountingRuleId(this.savingProductAccountingForm.value.accountingRule); + } + + 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: 'overdraftPortfolioControlId', + label: 'Overdraft Portfolio Control', + inputLabel: 'Overdraft portfolio control', + controlName: 'overdraftPortfolioControlId', + options: this.assetAccountOptions + }, + { + key: 'interestReceivableAccountId', + label: 'Interest Receivable Account', + inputLabel: 'Interest receivable account', + controlName: 'interestReceivableAccountId', + options: this.assetAccountOptions, + accrualOnly: 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 + }, + { + key: 'writeOffAccountId', + label: 'Write Off Account', + inputLabel: 'Write off account', + controlName: 'writeOffAccountId', + 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 + }, + { + key: 'incomeFromInterestId', + label: 'Income From Interest', + inputLabel: 'Income from interest', + controlName: 'incomeFromInterestId', + options: this.incomeAccountOptions, + required: true + } + ], + otherMappingsControlName: 'incomeOtherGlMappings', + otherAccountOptions: this.incomeAccountOptions + } + ]; } } diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-charges-step/saving-product-charges-step.component.scss b/src/app/products/saving-products/saving-product-stepper/saving-product-charges-step/saving-product-charges-step.component.scss index 33519f366..f272b71f6 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-charges-step/saving-product-charges-step.component.scss +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-charges-step/saving-product-charges-step.component.scss @@ -65,7 +65,7 @@ $gray-900: #202124; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; } - @media (max-width: 600px) { + @media (width <= 600px) { flex-direction: column; &__select { diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.html b/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.html index ef48d062a..799064f6a 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.html +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.html @@ -10,74 +10,69 @@
-

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

+ Currency & Account Status Configuration
-
- - {{ 'labels.inputs.Currency' | translate }} - - @for (currency of currencyData; track currency) { - - {{ currency.name }} - - } - - - {{ 'labels.inputs.Currency' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.Decimal Places' | translate }} - - - {{ 'labels.inputs.Decimal Places' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - -
-
- -
-
- -

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

-
-
-
- - {{ 'labels.inputs.Set the saving installment in multiples of' | translate }} - -
- - @if (savingProductCurrencyForm.value.setMultiples) { +
+
- {{ 'labels.inputs.Currency in multiples of' | translate }} - + {{ 'labels.inputs.Currency' | translate }} + + @for (currency of currencyData; track currency) { + + {{ currency.name }} + + } + + Default: Indian Rupee - auto-set for domestic products - {{ 'labels.inputs.Currency in multiples of' | translate }} {{ 'labels.commons.is' | translate }} + {{ '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 + +
diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.scss b/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.scss index c2d90f817..a287d691e 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.scss +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.scss @@ -44,27 +44,71 @@ $gray-900: #202124; } } +.spc-section__badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: #eff6ff; + font-size: 14px; +} + .spc-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { + @media (width <= 600px) { grid-template-columns: 1fr; } } +.spc-body { + background: #fff; +} + .spc-field { width: 100%; - - &--full { - grid-column: 1 / -1; - } } -.spc-checkbox-row { +.spc-alert { display: flex; align-items: center; - padding: 4px 0; + 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/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.ts b/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.ts index c8f2da5f8..63ae1b3db 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.ts +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.ts @@ -6,11 +6,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, OnInit, Input, inject, DestroyRef } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Component, OnInit, Input, inject } from '@angular/core'; import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; import { MatTooltip } from '@angular/material/tooltip'; -import { MatCheckbox } from '@angular/material/checkbox'; +import { MatRadioGroup, MatRadioButton } from '@angular/material/radio'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @@ -21,19 +20,24 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; imports: [ ...STANDALONE_SHARED_IMPORTS, MatTooltip, - FaIconComponent, - MatCheckbox + MatRadioGroup, + MatRadioButton, + FaIconComponent ] }) export class SavingProductCurrencyStepComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); - private destroyRef = inject(DestroyRef); @Input() savingProductsTemplate: any; savingProductCurrencyForm: UntypedFormGroup; currencyData: any; + decimalPlacesData = [ + 2, + 0, + 3 + ]; constructor() { this.createSavingProductCurrencyForm(); @@ -43,13 +47,11 @@ export class SavingProductCurrencyStepComponent implements OnInit { this.currencyData = this.savingProductsTemplate.currencyOptions; this.savingProductCurrencyForm.patchValue({ - currencyCode: this.savingProductsTemplate.currency.code || this.currencyData[0].code, - digitsAfterDecimal: this.savingProductsTemplate.digitsAfterDecimal ?? '', - setMultiples: !!this.savingProductsTemplate.inMultiplesOf, - inMultiplesOf: this.savingProductsTemplate.inMultiplesOf ?? '' + currencyCode: this.savingProductsTemplate.currency?.code || this.currencyData?.[0]?.code || '', + digitsAfterDecimal: this.savingProductsTemplate.digitsAfterDecimal ?? 2, + dormancyAlert: this.savingProductsTemplate.isDormancyTrackingActive ?? false, + nominationFacility: this.savingProductsTemplate.nominationFacility ?? false }); - - this.setupConditionalValidation(); } createSavingProductCurrencyForm() { @@ -59,39 +61,14 @@ export class SavingProductCurrencyStepComponent implements OnInit { Validators.required ], digitsAfterDecimal: [ - '', + 2, [ Validators.required, Validators.min(0) ] ], - setMultiples: [false], - inMultiplesOf: [''] - }); - } - - setupConditionalValidation() { - const inMultiplesOfControl = this.savingProductCurrencyForm.get('inMultiplesOf'); - const setMultiplesControl = this.savingProductCurrencyForm.get('setMultiples'); - if (setMultiplesControl?.value) { - inMultiplesOfControl?.setValidators([ - Validators.required, - Validators.min(1) - ]); - inMultiplesOfControl?.updateValueAndValidity(); - } - - setMultiplesControl?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((checked) => { - if (checked) { - inMultiplesOfControl?.setValidators([ - Validators.required, - Validators.min(1) - ]); - } else { - inMultiplesOfControl?.clearValidators(); - inMultiplesOfControl?.setValue(''); - } - inMultiplesOfControl?.updateValueAndValidity(); + dormancyAlert: [false], + nominationFacility: [false] }); } @@ -99,13 +76,12 @@ export class SavingProductCurrencyStepComponent implements OnInit { const formValue = this.savingProductCurrencyForm.value; const result: any = { currencyCode: formValue.currencyCode, - digitsAfterDecimal: formValue.digitsAfterDecimal + digitsAfterDecimal: formValue.digitsAfterDecimal, + dormancyAlert: formValue.dormancyAlert, + isDormancyTrackingActive: formValue.dormancyAlert, + nominationFacility: formValue.nominationFacility }; - if (formValue.inMultiplesOf !== '' && formValue.inMultiplesOf !== null && formValue.inMultiplesOf !== undefined) { - result.inMultiplesOf = formValue.inMultiplesOf; - } - return result; } } diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.html b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.html index 30f8f0d7d..8b101463b 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.html +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.html @@ -54,4 +54,64 @@
+ +
+
+ +

Product Classification

+
+ +
+ + Account Type + + @for (accountType of accountTypeData; track accountType) { + {{ accountType }} + } + + Account Type is required + + + + Customer Segment + + @for (customerSegment of customerSegmentData; track customerSegment) { + {{ customerSegment }} + } + + + + + Product Code + + + + + Effective To + + + + + + + End Date + + + + + + + Introducer Required + + @for (option of yesNoOptions; track option.value) { + {{ option.label }} + } + + + +
+ Share Linking Enabled +
+
+
diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.scss b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.scss index 343ad6b21..7b5c920ef 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.scss +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.scss @@ -46,11 +46,15 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { + @media (width <= 600px) { grid-template-columns: 1fr; } } +.spd-grid--classification { + grid-template-columns: repeat(3, 1fr); +} + .spd-field { width: 100%; @@ -58,3 +62,9 @@ $gray-900: #202124; grid-column: 1 / -1; } } + +.spd-checkbox-row { + display: flex; + align-items: center; + min-height: 56px; +} diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.ts b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.ts index febc0c9dd..01a118a8a 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.ts +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.ts @@ -6,11 +6,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, OnInit, Input, inject } from '@angular/core'; +import { Component, OnInit, OnChanges, Input, SimpleChanges, inject } 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 { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { Dates } from 'app/core/utils/dates'; +import { SettingsService } from 'app/settings/settings.service'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ @@ -24,12 +26,29 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; FaIconComponent ] }) -export class SavingProductDetailsStepComponent implements OnInit { +export class SavingProductDetailsStepComponent implements OnInit, OnChanges { private formBuilder = inject(UntypedFormBuilder); + private dateUtils = inject(Dates); + private settingsService = inject(SettingsService); @Input() savingProductsTemplate: any; savingProductDetailsForm: UntypedFormGroup; + accountTypeData: string[] = []; + customerSegmentData: string[] = []; + yesNoOptions: Array<{ + value: boolean; + label: string; + }> = [ + { + value: true, + label: 'Yes' + }, + { + value: false, + label: 'No' + } + ]; constructor() { this.createSavingProductDetailsForm(); @@ -37,11 +56,13 @@ export class SavingProductDetailsStepComponent implements OnInit { ngOnInit() { if (this.savingProductsTemplate) { - this.savingProductDetailsForm.patchValue({ - name: this.savingProductsTemplate.name, - shortName: this.savingProductsTemplate.shortName, - description: this.savingProductsTemplate.description - }); + this.applyTemplate(this.savingProductsTemplate); + } + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.savingProductsTemplate?.currentValue) { + this.applyTemplate(changes.savingProductsTemplate.currentValue); } } @@ -55,11 +76,111 @@ export class SavingProductDetailsStepComponent implements OnInit { '', Validators.required ], - description: [''] + description: [''], + accountType: [ + null, + Validators.required + ], + customerSegment: [null], + productCode: ['SB-IND-001'], + effectiveDate: [''], + effectiveTo: [''], + endDate: [''], + introducerRequired: [false], + shareLinkingEnabled: [false], + nominationFacility: [false], + dormancyAlert: [false] }); } + 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 applyTemplate(template: any) { + const accountTypeOptions = this.getOptionNames(template.accountTypeOptions); + const customerSegmentOptions = this.getOptionNames(template.customerSegmentOptions); + const yesNoOptions = this.getYesNoOptions(template.yesNoOptions); + + this.accountTypeData = accountTypeOptions; + this.customerSegmentData = customerSegmentOptions; + if (yesNoOptions.length) { + this.yesNoOptions = yesNoOptions; + } + + // Use existing product data if available (when editing), otherwise use template defaults + const isEditing = !!template.id; + this.savingProductDetailsForm.patchValue({ + name: isEditing ? template.name : template.name || '', + shortName: isEditing ? template.shortName : template.shortName || '', + description: template.description, + accountType: template.accountType ?? this.accountTypeData[0] ?? null, + customerSegment: template.customerSegment ?? this.customerSegmentData[0] ?? null, + productCode: template.productCode ?? 'SB-IND-001', + effectiveDate: template.effectiveDate ? new Date(template.effectiveDate) : '', + effectiveTo: template.effectiveTo ? new Date(template.effectiveTo) : '', + endDate: template.endDate ? new Date(template.endDate) : '', + introducerRequired: template.introducerRequired === true || template.introducerRequired === 'Yes', + shareLinkingEnabled: !!template.shareLinkingEnabled, + nominationFacility: !!template.nominationFacility, + dormancyAlert: !!template.dormancyAlert + }); + } + + private getYesNoOptions(options: any[]): Array<{ value: boolean; label: string }> { + if (!Array.isArray(options)) { + return []; + } + + return options + .map((option) => { + const rawValue = option?.name ?? option?.value ?? option; + const normalized = typeof rawValue === 'string' ? rawValue.trim().toUpperCase() : rawValue; + + if (normalized === 'YES') { + return { + value: true, + label: 'Yes' + }; + } + + if (normalized === 'NO') { + return { + value: false, + label: 'No' + }; + } + + return null; + }) + .filter((value) => !!value); + } + get savingProductDetails() { - return this.savingProductDetailsForm.value; + const formValue = { ...this.savingProductDetailsForm.value }; + const dateFormat = this.settingsService.dateFormat; + const prevEffectiveDate: Date = this.savingProductDetailsForm.value.effectiveDate; + const prevEffectiveTo: Date = this.savingProductDetailsForm.value.effectiveTo; + const prevEndDate: Date = this.savingProductDetailsForm.value.endDate; + + if (formValue.effectiveDate instanceof Date) { + formValue.effectiveDate = this.dateUtils.formatDate(prevEffectiveDate, dateFormat) || ''; + } + + if (formValue.effectiveTo instanceof Date) { + formValue.effectiveTo = this.dateUtils.formatDate(prevEffectiveTo, dateFormat) || ''; + } + + if (formValue.endDate instanceof Date) { + formValue.endDate = this.dateUtils.formatDate(prevEndDate, dateFormat) || ''; + } + + return formValue; } } diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component.html b/src/app/products/saving-products/saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component.html new file mode 100644 index 000000000..4224ebeaa --- /dev/null +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component.html @@ -0,0 +1,351 @@ + +
+
+ + + +
Transaction & Product Limits
+
R&D BASED DEFAULTS
+
+ +
+
+
Account Features (as per RBI guidelines)
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + @for (row of accountFacilityLimits; track row.limitType) { + + + + + + + } + +
Limit TypeGeneral Limit (Daily)Minor Account LimitSmall Saving Limit
{{ row.limitType }} + + + + + +
+
+ +
+

Deposit Limit (KYC)

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Account TypeKYC TypeLimit
General + + + +
Minor + + + +
Small Saving + + + +
+
+ +
+ Note: + Small Saving Account limits are strictly capped at Rs.1,00,000 maximum balance and Rs.10,000 monthly withdrawal + per RBI Master Circular. Minor accounts are restricted from RTGS and high-risk digital products (BEPG/BBPS) by + default. +
+
+
diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component.scss b/src/app/products/saving-products/saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component.scss new file mode 100644 index 000000000..f0f7cad32 --- /dev/null +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component.scss @@ -0,0 +1,257 @@ +/** + * 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: #d9e2ec; +$border-strong: #c7d2de; +$text: #24364b; +$muted: #64748b; +$header-bg: #f8fafc; +$highlight-bg: #eff6ff; + +.spl-section { + border: 1px solid $border; + border-radius: 12px; + overflow: hidden; + background: #fff; +} + +.spl-section__header { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + background: $header-bg; + border-bottom: 1px solid $border; +} + +.spl-section__icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 8px; + background: #eff6ff; + color: #2563eb; + font-size: 14px; + flex: 0 0 auto; +} + +.spl-section__title { + flex: 1 1 auto; + font-size: 16px; + font-weight: 600; + color: $text; +} + +.spl-section__badge { + flex: 0 0 auto; + padding: 5px 10px; + border-radius: 999px; + background: #dbeafe; + color: #1d4ed8; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.05em; + white-space: nowrap; +} + +.spl-section__body { + padding: 18px 20px 20px; +} + +.spl-subsection { + border: 1px solid $border; + border-radius: 12px; + overflow: hidden; + background: #fff; + + & + & { + margin-top: 16px; + } +} + +.spl-subsection__header { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; + background: $header-bg; + border-bottom: 1px solid $border; + color: $text; + font-size: 14px; + font-weight: 600; +} + +.spl-subsection__icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + border-radius: 8px; + background: #eff6ff; + color: #2563eb; + font-size: 13px; + flex: 0 0 auto; +} + +.spl-subsection__body { + padding: 16px 16px 18px; +} + +.spl-table-wrap { + overflow-x: auto; +} + +.spl-table { + width: 100%; + min-width: 0; + table-layout: fixed; + border-collapse: collapse; + border: 1px solid $border; + + col.spl-table__col--label { + width: 22%; + } + + col.spl-table__col--value { + width: 26%; + } + + th, + td { + border: 1px solid $border; + padding: 6px 8px; + vertical-align: middle; + } + + th { + background: #f8fafc; + color: #64748b; + text-align: center; + font-size: 11px; + font-weight: 700; + line-height: 1.3; + white-space: normal; + word-break: break-word; + } + + td:first-child { + background: #f8fafc; + font-weight: 600; + color: $text; + white-space: nowrap; + } + + td { + font-size: 13px; + overflow: hidden; + } +} + +.spl-table__highlight td:first-child { + color: #1d4ed8; +} + +.spl-input, +.spl-select { + width: 100%; + box-sizing: border-box; + font: inherit; +} + +.spl-input { + height: 30px; + border: 1px solid $border-strong; + border-radius: 2px; + padding: 0 8px; + background: #fff; +} + +.spl-input:disabled, +.spl-select:disabled { + background: #f8fafc; + color: $muted; +} + +.spl-select { + height: 30px; + border: 1px solid $border-strong; + border-radius: 2px; + padding: 0 6px; + background: #fff; +} + +.spl-input--stacked { + margin-top: 6px; +} + +.spl-input--disabled { + background: #f8fafc; + color: $muted; +} + +.spl-table__muted { + font-size: 11px; + color: #94a3b8; + letter-spacing: 0.03em; +} + +.spl-note { + margin-top: 16px; + padding: 12px 14px; + border: 1px solid $border; + border-radius: 8px; + background: #f8fafc; + color: $muted; + font-size: 11px; + line-height: 1.6; +} + +.spl-features { + margin-bottom: 16px; +} + +.spl-features__grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px 16px; + + @media (width <= 900px) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + @media (width <= 640px) { + grid-template-columns: 1fr; + } +} + +.spl-feature { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 13px; + color: $text; +} + +.spl-feature__label { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 18px; + font-weight: 600; + color: #334155; + + i { + color: #2563eb; + font-size: 12px; + width: 14px; + text-align: center; + } +} diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component.ts b/src/app/products/saving-products/saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component.ts new file mode 100644 index 000000000..0f057264f --- /dev/null +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component.ts @@ -0,0 +1,250 @@ +/** + * 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, OnChanges, SimpleChanges } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +type BooleanFeatureKey = + | 'atmFacility' + | 'atmWithdrawalFacility' + | 'upiFacility' + | 'posTransactionsFacility' + | 'chequeBook' + | 'impsFacility' + | 'smsAlerts' + | 'bbpsFacility' + | 'insuranceCover' + | 'bepgFacility' + | 'neftFacility' + | 'rtgsFacility' + | 'interBankTotalCapFacility' + | 'withdrawalFacility' + | 'depositFacilityKyc' + | 'virtualDebitCard'; + +interface FacilityLimitRow { + limitType: string; + generalLimit: string | number | null; + minorAccountLimit: string | number | null; + smallSavingLimit: string | number | null; +} + +@Component({ + selector: 'mifosx-saving-product-limits-step', + templateUrl: './saving-product-limits-step.component.html', + styleUrls: ['./saving-product-limits-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FormsModule + ] +}) +export class SavingProductLimitsStepComponent implements OnChanges { + @Input() savingProductsTemplate: any; + pristine = true; + + yesNoOptions: Array<{ + value: boolean; + label: string; + }> = [ + { + value: true, + label: 'Yes' + }, + { + value: false, + label: 'No' + } + ]; + + accountFacilityConfig: Record = { + atmFacility: true, + atmWithdrawalFacility: true, + upiFacility: true, + posTransactionsFacility: true, + chequeBook: true, + impsFacility: true, + smsAlerts: true, + bbpsFacility: true, + insuranceCover: false, + bepgFacility: true, + neftFacility: true, + rtgsFacility: true, + interBankTotalCapFacility: true, + withdrawalFacility: true, + depositFacilityKyc: true, + virtualDebitCard: false + }; + + accountFacilityLimits: FacilityLimitRow[] = [ + { + limitType: 'ATM Withdrawal', + generalLimit: 40000, + minorAccountLimit: 5000, + smallSavingLimit: 10000 + }, + { + limitType: 'POS Transactions', + generalLimit: 100000, + minorAccountLimit: 2000, + smallSavingLimit: 10000 + }, + { + limitType: 'UPI Limit', + generalLimit: 50000, + minorAccountLimit: 1000, + smallSavingLimit: 5000 + }, + { + limitType: 'IMPS Limit', + generalLimit: 50000, + minorAccountLimit: 100, + smallSavingLimit: 5000 + }, + { + limitType: 'BBPS', + generalLimit: 25000, + minorAccountLimit: 0, + smallSavingLimit: 2000 + }, + { + limitType: 'BEPG', + generalLimit: 25000, + minorAccountLimit: 0, + smallSavingLimit: 2000 + }, + { + limitType: 'NEFT Limit', + generalLimit: 1000000, + minorAccountLimit: 25000, + smallSavingLimit: 10000 + }, + { + limitType: 'RTGS Limit (Min 2L)', + generalLimit: 5000000, + minorAccountLimit: 'N/A', + smallSavingLimit: 'N/A' + }, + { + limitType: 'Inter-Bank Total Cap', + generalLimit: 5000000, + minorAccountLimit: 25000, + smallSavingLimit: 10000 + }, + { + limitType: 'Withdrawal Limit', + generalLimit: 'UNLIMITED', + minorAccountLimit: 10000, + smallSavingLimit: 10000 + } + ]; + + depositLimitKyc = { + general: { + kycType: 'Full KYC', + limit: 'No Limit' + }, + minor: { + kycType: 'Guardian KYC', + limit: 100000 + }, + smallSaving: { + kycType: 'Min KYC', + limit: 50000 + } + }; + + ngOnChanges(changes: SimpleChanges) { + if (changes.savingProductsTemplate?.currentValue) { + const template = changes.savingProductsTemplate.currentValue; + this.setYesNoOptions(template); + this.populateFromTemplate(template); + } + } + + markAsDirty() { + this.pristine = false; + } + + private populateFromTemplate(template: any) { + const isEditing = !!template.id; + + // Populate accountFacilityConfig from existing product + if (isEditing && template.accountFacilityConfig) { + this.accountFacilityConfig = { ...this.accountFacilityConfig, ...template.accountFacilityConfig }; + } + + // Populate accountFacilityLimits from existing product + if (isEditing && template.accountFacilityLimits && template.accountFacilityLimits.length > 0) { + this.accountFacilityLimits = template.accountFacilityLimits.map((limit: any) => ({ + limitType: limit.limitType || '', + generalLimit: limit.generalLimit ?? null, + minorAccountLimit: limit.minorAccountLimit ?? null, + smallSavingLimit: limit.smallSavingLimit ?? null + })); + } + + // Populate depositLimitKyc from existing product + if (isEditing && template.depositLimitKyc) { + this.depositLimitKyc = { + general: { + kycType: template.depositLimitKyc.general?.kycType || 'Full KYC', + limit: template.depositLimitKyc.general?.limit || 'No Limit' + }, + minor: { + kycType: template.depositLimitKyc.minor?.kycType || 'Guardian KYC', + limit: template.depositLimitKyc.minor?.limit ?? 100000 + }, + smallSaving: { + kycType: template.depositLimitKyc.smallSaving?.kycType || 'Min KYC', + limit: template.depositLimitKyc.smallSaving?.limit ?? 50000 + } + }; + } + } + + private setYesNoOptions(template: any) { + const options = Array.isArray(template?.yesNoOptions) ? template.yesNoOptions : []; + const mappedOptions = options + .map((option: any) => { + const rawValue = option?.name ?? option?.value ?? option; + const normalized = typeof rawValue === 'string' ? rawValue.trim().toUpperCase() : rawValue; + + if (normalized === 'YES') { + return { + value: true, + label: 'Yes' + }; + } + + if (normalized === 'NO') { + return { + value: false, + label: 'No' + }; + } + + return null; + }) + .filter((value: any) => !!value); + + if (mappedOptions.length) { + this.yesNoOptions = mappedOptions; + } + } + + get savingProductLimits() { + return { + accountFacilityConfig: { ...this.accountFacilityConfig }, + accountFacilityLimits: this.accountFacilityLimits.map((row) => ({ ...row })), + depositLimitKyc: { + ...this.depositLimitKyc + } + }; + } +} diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.html b/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.html index ec338e603..c75a6a993 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.html +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.html @@ -25,25 +25,107 @@ } + @if ( + savingProduct.accountType || + savingProduct.customerSegment || + savingProduct.productCode || + savingProduct.effectiveTo || + savingProduct.introducerRequired !== undefined || + savingProduct.shareLinkingEnabled !== undefined + ) { +

Product Classification

+ + + + @if (savingProduct.accountType) { +
+ Account Type: + {{ savingProduct.accountType }} +
+ } + + @if (savingProduct.customerSegment) { +
+ Customer Segment: + {{ savingProduct.customerSegment }} +
+ } + + @if (savingProduct.productCode) { +
+ Product Code: + {{ savingProduct.productCode }} +
+ } + + @if (savingProduct.effectiveTo) { +
+ Effective To: + {{ savingProduct.effectiveTo }} +
+ } + + @if (savingProduct.endDate) { +
+ End Date: + {{ savingProduct.endDate }} +
+ } + + @if (savingProduct.introducerRequired !== undefined) { +
+ Introducer Required: + {{ + savingProduct.introducerRequired === true || savingProduct.introducerRequired === 'Yes' ? 'Yes' : 'No' + }} +
+ } + + @if (savingProduct.shareLinkingEnabled !== undefined) { +
+ Share Linking Enabled: + {{ savingProduct.shareLinkingEnabled | yesNo }} +
+ } + } +

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

{{ 'labels.inputs.Currency' | translate }}: {{ - savingProduct.currencyCode | find: savingProductsTemplate.currencyOptions : 'code' : 'name' + savingProduct.currency?.name || + savingProduct.currency?.displayLabel || + (savingProduct.currencyCode | find: savingProductsTemplate?.currencyOptions : 'code' : 'name') || + savingProduct.currencyCode || + '-' }}
{{ 'labels.inputs.Decimal Places' | translate }}: - {{ savingProduct.digitsAfterDecimal ?? savingProduct.currency?.decimalPlaces }} + {{ savingProduct.digitsAfterDecimal ?? savingProduct.currency?.decimalPlaces ?? 2 }}
- @if (savingProduct.inMultiplesOf) { + @if (savingProduct.currency?.inMultiplesOf !== undefined && savingProduct.currency?.inMultiplesOf !== null) {
{{ 'labels.inputs.Currency in multiples of' | translate }}: - {{ savingProduct.inMultiplesOf }} + {{ savingProduct.currency.inMultiplesOf }} +
+ } + + @if (savingProduct.dormancyAlert !== undefined) { +
+ Dormancy Alert: + {{ savingProduct.dormancyAlert | yesNo }} +
+ } + + @if (savingProduct.nominationFacility !== undefined) { +
+ Nomination Facility: + {{ savingProduct.nominationFacility | yesNo }}
} @@ -59,7 +141,7 @@ {{ 'labels.inputs.Interest Compounding Period' | translate }}: {{ savingProduct.interestCompoundingPeriodType - | find: savingProductsTemplate.interestCompoundingPeriodTypeOptions : 'id' : 'value' + | find: savingProductsTemplate?.interestCompoundingPeriodTypeOptions : 'id' : 'value' | translateKey: 'catalogs' }} @@ -68,16 +150,16 @@ {{ 'labels.inputs.Interest Posting Period' | translate }}: {{ savingProduct.interestPostingPeriodType - | find: savingProductsTemplate.interestPostingPeriodTypeOptions : 'id' : 'value' + | find: savingProductsTemplate?.interestPostingPeriodTypeOptions : 'id' : 'value' | translateKey: 'catalogs' }}
- {{ 'labels.inputs.Interest Calculated using' | translate }}: + {{ 'labels.inputs.Interest Calculation Type' | translate }}: {{ savingProduct.interestCalculationType - | find: savingProductsTemplate.interestCalculationTypeOptions : 'id' : 'value' + | find: interestCalculationTypeOptionData : 'id' : 'value' | translateKey: 'catalogs' }}
@@ -86,11 +168,56 @@ {{ 'labels.inputs.Days in Year' | translate }}: {{ savingProduct.interestCalculationDaysInYearType - | find: savingProductsTemplate.interestCalculationDaysInYearTypeOptions : 'id' : 'value' + | find: savingProductsTemplate?.interestCalculationDaysInYearTypeOptions : 'id' : 'value' | translateKey: 'catalogs' }} + @if (savingProduct.effectiveDate) { +
+ Effective Date: + {{ savingProduct.effectiveDate }} +
+ } + +
+ Interest Calculate: + {{ + savingProduct.interestCalculateSelector + | find: savingProductsTemplate?.interestCalculationTypeOptions : 'id' : 'value' + }} +
+ + @if (savingProduct.averagePeriod) { +
+ Average Period: + {{ savingProduct.averagePeriod }} +
+ } + +
+ Auto Interest Post: + {{ savingProduct.autoPost | yesNo }} +
+ + @if ( + savingProduct.frequencyFixDay !== undefined && + savingProduct.frequencyFixDay !== null && + savingProduct.frequencyFixDay !== '' + ) { +
+ Frequency (Fix Day): + {{ savingProduct.frequencyFixDay }} +
+ } + + @if (savingProduct.lastInterestDate) { +
+ Last Interest Date: + {{ savingProduct.lastInterestDate }} +
+ } +

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

@@ -108,7 +235,8 @@ savingProduct.lockinPeriodFrequency + ' ' + (savingProduct.lockinPeriodFrequencyType - | find: savingProductsTemplate.lockinPeriodFrequencyTypeOptions : 'id' : 'value') | translateKey: 'catalogs' + | find: savingProductsTemplate?.lockinPeriodFrequencyTypeOptions : 'id' : 'value') + | translateKey: 'catalogs' }} } @@ -118,6 +246,28 @@ {{ savingProduct.withdrawalFeeForTransfers | yesNo }} + @if (savingProduct.balanceFrequency) { +
+ Balance Frequency: + {{ savingProduct.balanceFrequency }} +
+ } + +
+ Enable Lock-in Period: + {{ savingProduct.enableLockinPeriod | yesNo }} +
+ + @if (savingProduct.enableLockinPeriod && savingProduct.lockinPeriodFrequency) { +
+ Lock-in Period: + + {{ savingProduct.lockinPeriodFrequency }} + {{ getLockinPeriodFrequencyTypeLabel(savingProduct.lockinPeriodFrequencyType) }} + +
+ } + @if (savingProduct.minBalanceForInterestCalculation) {
{{ 'labels.inputs.Balance Required for Interest Calculation' | translate }}: @@ -146,7 +296,7 @@
{{ 'labels.inputs.Tax Group' | translate }}: {{ - savingProduct.taxGroupId | find: savingProductsTemplate.taxGroupOptions : 'id' : 'name' + savingProduct.taxGroupId | find: savingProductsTemplate?.taxGroupOptions : 'id' : 'name' }}
} @@ -156,6 +306,11 @@ {{ savingProduct.allowOverdraft | yesNo }}
+
+ Account Closed with Zero Balance in Day Closing: + {{ savingProduct.accountClosedZeroBalanceInDayClosing | yesNo }} +
+ @if (savingProduct.minOverdraftForInterestCalculation) {
} +

Account Facilities

+ + + @if (savingProduct.accountFacilityConfig) { +
+ @for (feature of accountFacilityFeatureItems; track feature.key) { +
+ {{ feature.label }}: + {{ savingProduct.accountFacilityConfig[feature.key] | yesNo }} +
+ } +
+ } + + @if (savingProduct.accountFacilityLimits?.length) { +
+

Account Facility Limits

+ + + + + + + + + + + + + + + + + + + +
Limit Type{{ row.limitType }}General Limit{{ row.generalLimit }}Minor Account Limit{{ row.minorAccountLimit }}Small Saving Limit{{ row.smallSavingLimit }}
+
+ } + + @if (savingProduct.depositLimitKyc) { +
+

Deposit Limit (KYC)

+
+ General: + {{ savingProduct.depositLimitKyc.general.kycType }} - {{ savingProduct.depositLimitKyc.general.limit }} +
+
+ Minor: + {{ savingProduct.depositLimitKyc.minor.kycType }} - {{ savingProduct.depositLimitKyc.minor.limit }} +
+
+ Small Saving: + + {{ savingProduct.depositLimitKyc.smallSaving.kycType }} - + {{ savingProduct.depositLimitKyc.smallSaving.limit }} + +
+
+ } + @if (savingProduct.charges.length) {

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

@@ -233,9 +459,262 @@

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

+ @if (isCashOrAccrualAccounting()) { +
+
+

GL Mapping

+
- - +
+
Assets
+
+ Savings reference account + {{ + formatGlAccountById( + savingProduct.savingsReferenceAccountId, + savingProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }} +
+
+ Overdraft portfolio control + {{ + formatGlAccountById( + savingProduct.overdraftPortfolioControlId, + savingProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }} +
+
+ Interest receivable account + {{ + formatGlAccountById( + savingProduct.interestReceivableAccountId, + savingProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }} +
+
+ Fees receivable account + {{ + formatGlAccountById( + savingProduct.feesReceivableAccountId, + savingProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }} +
+
+ Penalties receivable account + {{ + formatGlAccountById( + savingProduct.penaltiesReceivableAccountId, + savingProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }} +
+
+ +
+
Liabilities
+
+ Savings control account + {{ + formatGlAccountById( + savingProduct.savingsControlAccountId, + savingProductsTemplate?.accountingMappingOptions.liabilityAccountOptions + ) + }} +
+
+ Transfers in suspense account + {{ + formatGlAccountById( + savingProduct.transfersInSuspenseAccountId, + savingProductsTemplate?.accountingMappingOptions.liabilityAccountOptions + ) + }} +
+
+ Interest payable account + {{ + formatGlAccountById( + savingProduct.interestPayableAccountId, + savingProductsTemplate?.accountingMappingOptions.liabilityAccountOptions + ) + }} +
+ @if (savingProduct.isDormancyTrackingActive) { +
+ Escheat liability account + {{ + formatGlAccountById( + savingProduct.escheatLiabilityId, + savingProductsTemplate?.accountingMappingOptions.liabilityAccountOptions + ) + }} +
+ } +
+ +
+
Expenses
+
+ Interest on savings account + {{ + formatGlAccountById( + savingProduct.interestOnSavingsAccountId, + savingProductsTemplate?.accountingMappingOptions.expenseAccountOptions + ) + }} +
+
+ Write off account + {{ + formatGlAccountById( + savingProduct.writeOffAccountId, + savingProductsTemplate?.accountingMappingOptions.expenseAccountOptions + ) + }} +
+
+ +
+
Income
+
+ Income from fees + {{ + formatGlAccountById( + savingProduct.incomeFromFeeAccountId, + savingProductsTemplate?.accountingMappingOptions.incomeAccountOptions + ) + }} +
+
+ Income from penalties + {{ + formatGlAccountById( + savingProduct.incomeFromPenaltyAccountId, + savingProductsTemplate?.accountingMappingOptions.incomeAccountOptions + ) + }} +
+
+ Income from interest + {{ + formatGlAccountById( + savingProduct.incomeFromInterestId, + savingProductsTemplate?.accountingMappingOptions.incomeAccountOptions + ) + }} +
+
+
+ } + @if (savingProduct.otherGlMappings) { +
+
+

Other GL Mappings

+
+ + @if (savingProduct.otherGlMappings.assets?.length) { + + + + + + + + + + + +
Asset Account{{ row.accountName }}GL Account + {{ + formatGlAccountByCode(row.glCode, savingProductsTemplate?.accountingMappingOptions.assetAccountOptions) + }} +
+ } + + @if (savingProduct.otherGlMappings.liabilities?.length) { + + + + + + + + + + + +
Liability Account{{ row.accountName }}GL Account + {{ + formatGlAccountByCode( + row.glCode, + savingProductsTemplate?.accountingMappingOptions.liabilityAccountOptions + ) + }} +
+ } + + @if (savingProduct.otherGlMappings.expenses?.length) { + + + + + + + + + + + +
Expense Account{{ row.accountName }}GL Account + {{ + formatGlAccountByCode( + row.glCode, + savingProductsTemplate?.accountingMappingOptions.expenseAccountOptions + ) + }} +
+ } + + @if (savingProduct.otherGlMappings.income?.length) { + + + + + + + + + + + +
Income Account{{ row.accountName }}GL Account + {{ + formatGlAccountByCode(row.glCode, savingProductsTemplate?.accountingMappingOptions.incomeAccountOptions) + }} +
+ } +
+ } @if ( savingProduct.advancedAccountingRules && @@ -265,7 +744,7 @@ {{ paymentFundSource.fundSourceAccountId - | find: savingProductsTemplate.accountingMappingOptions.assetAccountOptions : 'id' : 'name' + | find: savingProductsTemplate?.accountingMappingOptions.assetAccountOptions : 'id' : 'name' }} @@ -289,7 +768,7 @@ {{ feesIncome.incomeAccountId - | find: savingProductsTemplate.accountingMappingOptions.incomeAccountOptions : 'id' : 'name' + | find: savingProductsTemplate?.accountingMappingOptions.incomeAccountOptions : 'id' : 'name' }} @@ -317,7 +796,7 @@ {{ penaltyIncome.incomeAccountId - | find: savingProductsTemplate.accountingMappingOptions.incomeAccountOptions : 'id' : 'name' + | find: savingProductsTemplate?.accountingMappingOptions.incomeAccountOptions : 'id' : 'name' }} @@ -329,4 +808,3 @@
}
- diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.scss b/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.scss index b4cb64249..f69c8fea0 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.scss +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.scss @@ -71,3 +71,95 @@ mat-divider { .margin-t { margin-top: 1em; } + +.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; +} + +.spa-gl-section 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%; +} + +.spa-other-gl-table:first-of-type { + margin-top: 0.25rem; +} + +.spa-other-gl-summary .mat-elevation-z1 { + border-radius: 10px; + overflow: hidden; +} + +.spa-other-gl-table th.mat-mdc-header-cell, +.spa-other-gl-table td.mat-mdc-cell { + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +.spa-other-gl-table th.mat-mdc-header-cell:first-child, +.spa-other-gl-table td.mat-mdc-cell:first-child { + width: 38%; +} + +.spa-other-gl-table th.mat-mdc-header-cell:last-child, +.spa-other-gl-table td.mat-mdc-cell:last-child { + width: 62%; + word-break: break-word; +} + +@media (width <= 768px) { + .spa-gl-row { + grid-template-columns: 1fr; + gap: 0.35rem; + } +} diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.ts b/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.ts index 4a396ecdf..d4cdec05e 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.ts +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.ts @@ -22,7 +22,6 @@ import { MatRowDef, MatRow } from '@angular/material/table'; -import { ViewSavingsAccountingDetailsComponent } from '../../../../shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { FindPipe } from '../../../../pipes/find.pipe'; import { FormatNumberPipe } from '../../../../pipes/format-number.pipe'; @@ -46,7 +45,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; MatHeaderRow, MatRowDef, MatRow, - ViewSavingsAccountingDetailsComponent, FaIconComponent, FindPipe, FormatNumberPipe, @@ -79,6 +77,25 @@ export class SavingProductPreviewStepComponent implements OnInit, OnChanges { accountingMappings: any = {}; accountingRule: OptionData; + interestCalculationTypeOptionData: any[] = []; + accountFacilityFeatureItems = [ + { key: 'atmFacility', label: 'ATM Facility' }, + { key: 'atmWithdrawalFacility', label: 'ATM Withdrawal Facility' }, + { key: 'upiFacility', label: 'UPI Facility' }, + { key: 'posTransactionsFacility', label: 'POS Transactions Facility' }, + { key: 'chequeBook', label: 'Cheque Book' }, + { key: 'impsFacility', label: 'IMPS Facility' }, + { key: 'smsAlerts', label: 'SMS Alerts' }, + { key: 'bbpsFacility', label: 'BBPS Facility' }, + { key: 'insuranceCover', label: 'Insurance Cover' }, + { key: 'bepgFacility', label: 'BEPG Facility' }, + { key: 'neftFacility', label: 'NEFT Facility' }, + { key: 'rtgsFacility', label: 'RTGS Facility' }, + { key: 'interBankTotalCapFacility', label: 'Inter-Bank Total Cap Facility' }, + { key: 'withdrawalFacility', label: 'Withdrawal Facility' }, + { key: 'depositFacilityKyc', label: 'Deposit Facility (KYC)' }, + { key: 'virtualDebitCard', label: 'Virtual Debit Card' } + ]; ngOnInit() { this.setCurrentValues(); @@ -89,62 +106,88 @@ export class SavingProductPreviewStepComponent implements OnInit, OnChanges { } setCurrentValues(): void { + if (!this.savingProduct || !this.savingProductsTemplate) { + return; + } + + this.interestCalculationTypeOptionData = + this.savingProductsTemplate.interestCalculationTypeOption || + this.savingProductsTemplate.interestCalculationTypeOptions || + []; this.accountingRule = this.accounting.getAccountingRuleFrom(this.savingProduct.accountingRule); - if (this.isCashOrAccrualAccounting()) { - const assetAccountData = this.savingProductsTemplate.accountingMappingOptions.assetAccountOptions || []; - const incomeAccountData = this.savingProductsTemplate.accountingMappingOptions.incomeAccountOptions || []; - const expenseAccountData = this.savingProductsTemplate.accountingMappingOptions.expenseAccountOptions || []; - const liabilityAccountData = this.savingProductsTemplate.accountingMappingOptions.liabilityAccountOptions || []; + if (!this.isCashOrAccrualAccounting()) { + return; + } + const assetAccountData = this.savingProductsTemplate?.accountingMappingOptions?.assetAccountOptions || []; + const incomeAccountData = this.savingProductsTemplate?.accountingMappingOptions?.incomeAccountOptions || []; + const expenseAccountData = this.savingProductsTemplate?.accountingMappingOptions?.expenseAccountOptions || []; + const liabilityAccountData = this.savingProductsTemplate?.accountingMappingOptions?.liabilityAccountOptions || []; + + this.accountingMappings = { + savingsReferenceAccount: this.accounting.glAccountLookUp( + this.savingProduct.savingsReferenceAccountId, + assetAccountData + ), + overdraftPortfolioControl: this.accounting.glAccountLookUp( + this.savingProduct.overdraftPortfolioControlId, + assetAccountData + ), + savingsControlAccount: this.accounting.glAccountLookUp( + this.savingProduct.savingsControlAccountId, + liabilityAccountData + ), + transfersInSuspenseAccount: this.accounting.glAccountLookUp( + this.savingProduct.transfersInSuspenseAccountId, + liabilityAccountData + ), + interestOnSavingsAccount: this.accounting.glAccountLookUp( + this.savingProduct.interestOnSavingsAccountId, + expenseAccountData + ), + writeOffAccount: this.accounting.glAccountLookUp(this.savingProduct.writeOffAccountId, expenseAccountData), + incomeFromFeeAccount: this.accounting.glAccountLookUp( + this.savingProduct.incomeFromFeeAccountId, + incomeAccountData + ), + incomeFromPenaltyAccount: this.accounting.glAccountLookUp( + this.savingProduct.incomeFromPenaltyAccountId, + incomeAccountData + ), + incomeFromInterest: this.accounting.glAccountLookUp(this.savingProduct.incomeFromInterestId, incomeAccountData) + }; + + if (this.isAccrualAccounting()) { this.accountingMappings = { - savingsReferenceAccount: this.accounting.glAccountLookUp( - this.savingProduct.savingsReferenceAccountId, - assetAccountData - ), - overdraftPortfolioControl: this.accounting.glAccountLookUp( - this.savingProduct.overdraftPortfolioControlId, - assetAccountData - ), - savingsControlAccount: this.accounting.glAccountLookUp( - this.savingProduct.savingsControlAccountId, - liabilityAccountData - ), - transfersInSuspenseAccount: this.accounting.glAccountLookUp( - this.savingProduct.transfersInSuspenseAccountId, - liabilityAccountData - ), - escheatLiability: this.accounting.glAccountLookUp(this.savingProduct.escheatLiabilityId, liabilityAccountData), - interestOnSavingsAccount: this.accounting.glAccountLookUp( - this.savingProduct.interestOnSavingsAccountId, - expenseAccountData - ), - writeOffAccount: this.accounting.glAccountLookUp(this.savingProduct.writeOffAccountId, expenseAccountData), - incomeFromFeeAccount: this.accounting.glAccountLookUp( - this.savingProduct.incomeFromFeeAccountId, - incomeAccountData - ), - incomeFromPenaltyAccount: this.accounting.glAccountLookUp( - this.savingProduct.incomeFromPenaltyAccountId, - incomeAccountData - ), - incomeFromInterest: this.accounting.glAccountLookUp(this.savingProduct.incomeFromInterestId, incomeAccountData) - }; - - if (this.isAccrualAccounting()) { - this.accountingMappings['feesReceivableAccount'] = this.accounting.glAccountLookUp( + ...this.accountingMappings, + feeReceivableAccount: this.accounting.glAccountLookUp( this.savingProduct.feesReceivableAccountId, assetAccountData - ); - this.accountingMappings['penaltiesReceivableAccount'] = this.accounting.glAccountLookUp( + ), + penaltyReceivableAccount: this.accounting.glAccountLookUp( this.savingProduct.penaltiesReceivableAccountId, assetAccountData - ); - this.accountingMappings['interestPayableAccount'] = this.accounting.glAccountLookUp( + ), + interestPayableAccount: this.accounting.glAccountLookUp( this.savingProduct.interestPayableAccountId, liabilityAccountData - ); - } + ), + interestReceivableAccount: this.accounting.glAccountLookUp( + this.savingProduct.interestReceivableAccountId, + assetAccountData + ) + }; + } + + if (this.savingProduct.isDormancyTrackingActive) { + this.accountingMappings = { + ...this.accountingMappings, + escheatLiabilityAccount: this.accounting.glAccountLookUp( + this.savingProduct.escheatLiabilityId, + liabilityAccountData + ) + }; } } @@ -155,4 +198,50 @@ export class SavingProductPreviewStepComponent implements OnInit, OnChanges { isAccrualAccounting(): boolean { return this.accounting.isAccrualAccounting(this.accountingRule); } + + getBooleanLabel(value: any): string { + return value === true || value === 'Yes' ? 'Yes' : 'No'; + } + + getLockinPeriodFrequencyTypeLabel(value: any): string { + switch (value) { + case 'DAYS': + return 'Days'; + case 'WEEKS': + return 'Weeks'; + case 'MONTHS': + return 'Months'; + case 'YEARS': + return 'Years'; + default: + return value ?? ''; + } + } + + getOptionLabel(value: any, options: any[], key: string, property: string): string { + if (value === null || value === undefined || !Array.isArray(options)) { + return '-'; + } + + const match = options.find((option) => option?.[key] === value); + return match?.[property] || '-'; + } + + 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 '-'; + } + + const account = options.find((option) => option?.glCode === glCode); + return account ? `(${account.glCode}) ${account.name}` : glCode; + } } diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component.html b/src/app/products/saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component.html index 9481f1ec8..8846d1e7a 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component.html +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component.html @@ -1,4 +1,4 @@ -
-
- -

{{ 'labels.inputs.Opening Balance' | translate }}

+ + Balance & Minimum Balance
-
- - {{ 'labels.inputs.Minimum Opening Balance' | translate }} - - @if (savingProductSettingsForm.get('minRequiredOpeningBalance').hasError('min')) { - - {{ 'labels.inputs.Minimum Opening Balance' | translate }} {{ 'labels.commons.must be' | translate }} - {{ 'labels.commons.a positive number' | translate }} - - } - +
+
+
+ + Minimum Opening Balance + + + +
+ + Enforce Minimum Balance + +
+ + + Minimum Balance (₹) + + Penal charge applies on breach + +
+ +
+ + Balance Frequency + + @for (balanceFrequency of periodFrequencyData; track balanceFrequency.id) { + {{ balanceFrequency.value }} + } + + + +
+ + Apply Withdrawal Fee for Transfers + +
+
+
-
- -

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

+ + Lock-in Period
-
-
- - {{ 'labels.inputs.Enable Lock-in Period' | translate }} - +
+
+ Enable Lock-in Period +
@if (savingProductSettingsForm.value.enableLockinPeriod) { - - {{ 'labels.inputs.Frequency' | translate }} - - @if (savingProductSettingsForm.get('lockinPeriodFrequency')?.hasError('required')) { - - {{ 'labels.inputs.Frequency' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - @if (savingProductSettingsForm.get('lockinPeriodFrequency')?.hasError('min')) { - {{ 'tooltips.Frequency must be greater than zero' | translate }} - } - @if (savingProductSettingsForm.get('lockinPeriodFrequency')?.hasError('pattern')) { - {{ 'tooltips.Frequency must be a positive integer' | translate }} - } - +
+ + Frequency* + + - - {{ 'labels.inputs.Type' | translate }} - - @for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) { - - {{ lockinPeriodFrequencyType.value | translateKey: 'catalogs' }} - - } - - @if (savingProductSettingsForm.get('lockinPeriodFrequencyType')?.hasError('required')) { - - {{ 'labels.inputs.Type' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - + + Type* + + @for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) { + + {{ lockinPeriodFrequencyType.value | translateKey: 'catalogs' }} + + } + + +
}
-
- -

{{ 'labels.inputs.Balance' | translate }} & {{ 'labels.inputs.Fees' | translate }}

+ + Additional Rules
-
-
- - {{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }} - +
+
+
+ + Account Closed with Zero balance in Day Closing + +
+ + + Balance Required for Interest Calculation + +
- - - {{ 'labels.inputs.Balance Required for Interest Calculation' | translate }} - - @if (savingProductSettingsForm.get('minBalanceForInterestCalculation').hasError('min')) { - - {{ 'labels.inputs.Balance Required for Interest Calculation' | translate }} - {{ 'labels.commons.must be' | translate }} - {{ 'labels.commons.a positive number' | translate }} - - } - - -
- - {{ 'labels.inputs.Enforce Minimum Balance' | translate }} - -
- - - {{ 'labels.inputs.Minimum Balance' | translate }} - - @if (savingProductSettingsForm.get('minRequiredBalance').hasError('min')) { - - {{ 'labels.inputs.Minimum Balance' | translate }} {{ 'labels.commons.must be' | translate }} - {{ 'labels.commons.a positive number' | translate }} - - } - -
-
- - -
-
- -

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

-
-
-
- - {{ 'labels.inputs.Is Withhold Tax Applicable' | translate }} - -
- - @if (savingProductSettingsForm.value.withHoldTax) { - - {{ 'labels.inputs.Tax Group' | translate }} - - @for (taxGroup of taxGroupData; track taxGroup) { - - {{ taxGroup.name }} - - } - - - {{ 'labels.inputs.Tax Group' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - } -
-
- - -
-
- -

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

-
-
-
- - {{ 'labels.inputs.Is Overdraft Allowed' | translate }} - -
- - @if (savingProductSettingsForm.value.allowOverdraft) { - - {{ 'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate }} - - - - - {{ 'labels.inputs.Nominal Annual Interest for Overdraft' | translate }} - - - - - {{ 'labels.inputs.Maximum Overdraft Amount Limit' | translate }} - - - } -
-
- - -
-
- -

{{ 'labels.inputs.Dormancy Tracking' | translate }}

-
-
-
- - {{ 'labels.inputs.Enable Dormancy Tracking' | translate }} - -
- - @if (savingProductSettingsForm.value.isDormancyTrackingActive) { - - {{ 'labels.inputs.Number of Days to Inactive sub-status' | translate }} - - - {{ 'labels.inputs.Number of Days to Inactive sub-status' | translate }} - {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.Number of Days to Dormant sub-status' | translate }} - - - {{ 'labels.inputs.Number of Days to Dormant sub-status' | translate }} - {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.Number of Days to Escheat' | translate }} - - - {{ 'labels.inputs.Number of Days to Escheat' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - }
diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component.scss b/src/app/products/saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component.scss index 0d8bb861f..191facb3f 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component.scss +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component.scss @@ -44,15 +44,20 @@ $gray-900: #202124; } } -.sps-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 4px 16px; - padding: 16px 24px; +.sps-section__badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: #eff6ff; + font-size: 14px; +} - @media (max-width: 600px) { - grid-template-columns: 1fr; - } +.sps-body { + background: #fff; + padding: 18px 24px 24px; } .sps-field { @@ -61,10 +66,112 @@ $gray-900: #202124; &--full { grid-column: 1 / -1; } + + &--compact { + width: 100%; + } } -.sps-checkbox-row { +.sps-layout { + display: grid; + gap: 16px; +} + +.sps-layout--balance { + gap: 22px; +} + +.sps-layout--lockin { + grid-template-columns: repeat(2, minmax(0, 1fr)); + + @media (width <= 640px) { + grid-template-columns: 1fr; + } +} + +.sps-layout--additional { + grid-template-columns: repeat(2, minmax(0, 1fr)); + + @media (width <= 1100px) { + grid-template-columns: 1fr; + } +} + +.sps-row { + display: grid; + gap: 18px; + align-items: start; +} + +.sps-row--three { + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + align-items: center; + + .sps-field--compact:first-child { + min-width: 0; + } + + .sps-toggle--inline { + align-self: center; + min-height: 56px; + } +} + +.sps-row--two { + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + + .sps-field--compact { + min-width: 0; + } +} + +.sps-toggle { display: flex; align-items: center; - padding: 4px 0; + min-height: 56px; + + mat-checkbox { + font-size: 14px; + color: $gray-900; + } + + &--full { + min-height: auto; + margin-bottom: 8px; + } + + &--inline { + padding-top: 0; + } + + &--right { + justify-content: flex-start; + } +} + +.sps-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; +} + +.sps-lockin-row mat-checkbox { + margin-right: 2px; +} + +.sps-lockin-fields { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + + @media (width <= 640px) { + grid-template-columns: 1fr; + } } diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component.ts b/src/app/products/saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component.ts index cc01b4769..2a8b96933 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component.ts +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component.ts @@ -1,4 +1,4 @@ -/** +/** * Copyright since 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public @@ -7,16 +7,8 @@ */ import { Component, OnInit, Input, inject } from '@angular/core'; -import { - UntypedFormGroup, - UntypedFormBuilder, - UntypedFormControl, - Validators, - ReactiveFormsModule -} from '@angular/forms'; -import { MatTooltip } from '@angular/material/tooltip'; +import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; import { MatCheckbox } from '@angular/material/checkbox'; -import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ @@ -25,9 +17,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; styleUrls: ['./saving-product-settings-step.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, - MatTooltip, - MatCheckbox, - FaIconComponent + MatCheckbox ] }) export class SavingProductSettingsStepComponent implements OnInit { @@ -37,8 +27,26 @@ export class SavingProductSettingsStepComponent implements OnInit { savingProductSettingsForm: UntypedFormGroup; - lockinPeriodFrequencyTypeData: any; - taxGroupData: any; + periodFrequencyData: any[] = []; + + lockinPeriodFrequencyTypeData = [ + { + id: 'DAYS', + value: 'value.days' + }, + { + id: 'WEEKS', + value: 'value.weeks' + }, + { + id: 'MONTHS', + value: 'value.months' + }, + { + id: 'YEARS', + value: 'value.years' + } + ]; constructor() { this.createSavingProductSettingsForm(); @@ -46,32 +54,34 @@ export class SavingProductSettingsStepComponent implements OnInit { } ngOnInit() { - this.lockinPeriodFrequencyTypeData = this.savingProductsTemplate.lockinPeriodFrequencyTypeOptions; - this.taxGroupData = this.savingProductsTemplate.taxGroupOptions; - - const hasLockinPeriod = - this.savingProductsTemplate.lockinPeriodFrequency && this.savingProductsTemplate.lockinPeriodFrequency > 0; + this.periodFrequencyData = this.savingProductsTemplate.periodFrequencyOptions || []; + const defaultPeriod = this.periodFrequencyData[0]?.value || 'Monthly'; + const isEditing = !!this.savingProductsTemplate.id; this.savingProductSettingsForm.patchValue({ minRequiredOpeningBalance: this.savingProductsTemplate.minRequiredOpeningBalance, - enableLockinPeriod: hasLockinPeriod, withdrawalFeeForTransfers: this.savingProductsTemplate.withdrawalFeeForTransfers, minBalanceForInterestCalculation: this.savingProductsTemplate.minBalanceForInterestCalculation, enforceMinRequiredBalance: this.savingProductsTemplate.enforceMinRequiredBalance, minRequiredBalance: this.savingProductsTemplate.minRequiredBalance, - allowOverdraft: this.savingProductsTemplate.allowOverdraft, - minOverdraftForInterestCalculation: this.savingProductsTemplate.minOverdraftForInterestCalculation, - nominalAnnualInterestRateOverdraft: this.savingProductsTemplate.nominalAnnualInterestRateOverdraft, - overdraftLimit: this.savingProductsTemplate.overdraftLimit, - withHoldTax: this.savingProductsTemplate.withHoldTax, - taxGroupId: this.savingProductsTemplate.taxGroup && this.savingProductsTemplate.taxGroup.id, - isDormancyTrackingActive: this.savingProductsTemplate.isDormancyTrackingActive, - daysToInactive: this.savingProductsTemplate.daysToInactive, - daysToDormancy: this.savingProductsTemplate.daysToDormancy, - daysToEscheat: this.savingProductsTemplate.daysToEscheat + balanceFrequency: isEditing ? this.savingProductsTemplate.balanceFrequency || defaultPeriod : defaultPeriod, + enableLockinPeriod: isEditing + ? this.savingProductsTemplate.lockinPeriodFrequency && this.savingProductsTemplate.lockinPeriodFrequency > 0 + : this.savingProductsTemplate.lockinPeriodFrequency && this.savingProductsTemplate.lockinPeriodFrequency > 0, + accountClosedZeroBalanceInDayClosing: isEditing + ? (this.savingProductsTemplate.accountClosedZeroBalanceInDayClosing ?? false) + : false, + isDormancyTrackingActive: isEditing ? (this.savingProductsTemplate.isDormancyTrackingActive ?? true) : true, + daysToInactive: isEditing ? this.savingProductsTemplate.daysToInactive || 30 : 30, + daysToDormancy: isEditing ? this.savingProductsTemplate.daysToDormancy || 60 : 60, + daysToEscheat: isEditing ? this.savingProductsTemplate.daysToEscheat || 90 : 90 }); - if (hasLockinPeriod) { + if ( + isEditing && + this.savingProductsTemplate.lockinPeriodFrequency && + this.savingProductsTemplate.lockinPeriodFrequency > 0 + ) { this.savingProductSettingsForm.patchValue({ lockinPeriodFrequency: this.savingProductsTemplate.lockinPeriodFrequency, lockinPeriodFrequencyType: @@ -87,7 +97,6 @@ export class SavingProductSettingsStepComponent implements OnInit { '', [Validators.min(0)] ], - enableLockinPeriod: [false], withdrawalFeeForTransfers: [false], minBalanceForInterestCalculation: [ '', @@ -98,95 +107,42 @@ export class SavingProductSettingsStepComponent implements OnInit { '', [Validators.min(0)] ], - allowOverdraft: [false], - withHoldTax: [false], - isDormancyTrackingActive: [false] + balanceFrequency: ['Monthly'], + enableLockinPeriod: [false], + lockinPeriodFrequency: [''], + lockinPeriodFrequencyType: [''], + accountClosedZeroBalanceInDayClosing: [false], + // Dormancy tracking fields (required when isDormancyTrackingActive is true) + isDormancyTrackingActive: [true], + daysToInactive: [30], + daysToDormancy: [60], + daysToEscheat: [90] }); } setConditionalControls() { this.savingProductSettingsForm.get('enableLockinPeriod').valueChanges.subscribe((enableLockinPeriod: any) => { if (enableLockinPeriod) { - this.savingProductSettingsForm.addControl( - 'lockinPeriodFrequency', - new UntypedFormControl('', [ - Validators.required, - Validators.min(1), - Validators.pattern('^[1-9]\\d*$') - ]) - ); - this.savingProductSettingsForm.addControl( - 'lockinPeriodFrequencyType', - new UntypedFormControl('', Validators.required) - ); + this.savingProductSettingsForm.get('lockinPeriodFrequency').setValidators([ + Validators.required, + Validators.min(1), + Validators.pattern('^[1-9]\\d*$') + ]); + this.savingProductSettingsForm.get('lockinPeriodFrequencyType').setValidators([ + Validators.required + ]); } else { - this.savingProductSettingsForm.removeControl('lockinPeriodFrequency'); - this.savingProductSettingsForm.removeControl('lockinPeriodFrequencyType'); + this.savingProductSettingsForm.get('lockinPeriodFrequency').setValue(''); + this.savingProductSettingsForm.get('lockinPeriodFrequencyType').setValue(''); + this.savingProductSettingsForm.get('lockinPeriodFrequency').clearValidators(); + this.savingProductSettingsForm.get('lockinPeriodFrequencyType').clearValidators(); } + this.savingProductSettingsForm.get('lockinPeriodFrequency').updateValueAndValidity(); + this.savingProductSettingsForm.get('lockinPeriodFrequencyType').updateValueAndValidity(); }); - - this.savingProductSettingsForm.get('allowOverdraft').valueChanges.subscribe((allowOverdraft: any) => { - if (allowOverdraft) { - this.savingProductSettingsForm.addControl( - 'minOverdraftForInterestCalculation', - new UntypedFormControl('', [Validators.min(0)]) - ); - this.savingProductSettingsForm.addControl( - 'nominalAnnualInterestRateOverdraft', - new UntypedFormControl('', [Validators.min(0)]) - ); - this.savingProductSettingsForm.addControl('overdraftLimit', new UntypedFormControl('', [Validators.min(0)])); - } else { - this.savingProductSettingsForm.removeControl('minOverdraftForInterestCalculation'); - this.savingProductSettingsForm.removeControl('nominalAnnualInterestRateOverdraft'); - this.savingProductSettingsForm.removeControl('overdraftLimit'); - } - }); - - this.savingProductSettingsForm.get('withHoldTax').valueChanges.subscribe((withHoldTax: any) => { - if (withHoldTax) { - this.savingProductSettingsForm.addControl('taxGroupId', new UntypedFormControl('', Validators.required)); - } else { - this.savingProductSettingsForm.removeControl('taxGroupId'); - } - }); - - this.savingProductSettingsForm - .get('isDormancyTrackingActive') - .valueChanges.subscribe((isDormancyTrackingActive: any) => { - if (isDormancyTrackingActive) { - this.savingProductSettingsForm.addControl( - 'daysToInactive', - new UntypedFormControl('', [ - Validators.required, - Validators.min(0) - ]) - ); - this.savingProductSettingsForm.addControl( - 'daysToDormancy', - new UntypedFormControl('', [ - Validators.required, - Validators.min(0) - ]) - ); - this.savingProductSettingsForm.addControl( - 'daysToEscheat', - new UntypedFormControl('', [ - Validators.required, - Validators.min(0) - ]) - ); - } else { - this.savingProductSettingsForm.removeControl('daysToInactive'); - this.savingProductSettingsForm.removeControl('daysToDormancy'); - this.savingProductSettingsForm.removeControl('daysToEscheat'); - } - }); } get savingProductSettings() { - const formValue = { ...this.savingProductSettingsForm.value }; - delete formValue.enableLockinPeriod; - return formValue; + return { ...this.savingProductSettingsForm.value }; } } diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.html b/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.html index 311ad5d01..c00f7e66b 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.html +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.html @@ -9,120 +9,148 @@
- -

{{ 'labels.inputs.Interest Rate' | translate }}

+ + Interest Rate & Posting Terms
-
- - {{ 'labels.inputs.Nominal Annual Interest' | translate }} - - @if (savingProductTermsForm.get('nominalAnnualInterestRate').hasError('required')) { +
+
+ ℹ️ + + Savings Account base rate: + {{ savingProductTermsForm.get('nominalAnnualInterestRate').value }}% + +
+ +
+ + {{ 'labels.inputs.Nominal Annual Interest' | translate }} + + @if (savingProductTermsForm.get('nominalAnnualInterestRate').hasError('required')) { + + {{ 'labels.inputs.Nominal Annual Interest' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + @if (savingProductTermsForm.get('nominalAnnualInterestRate').hasError('min')) { + {{ 'labels.inputs.Nominal Annual Interest' | translate }} must be zero or greater + } + + + + Effective Date + + + + + + + {{ 'labels.inputs.Interest Calculation Type' | translate }} + + @for (interestCalculationType of interestCalculationTypeOptionData; track interestCalculationType.id) { + + {{ interestCalculationType.value | translateKey: 'catalogs' }} + + } + - {{ 'labels.inputs.Nominal Annual Interest' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.inputs.Interest Calculation Type' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} + +
+ +
+ + Interest Calculate + + @for (mode of interestCalculationTypeData; track mode.id) { + {{ mode.value }} + } + + + + @if (savingProductTermsForm.value.interestCalculateSelector === 2) { + + Average Period + + @for (period of periodFrequencyData; track period.id) { + {{ period.value }} + } + + } - @if (savingProductTermsForm.get('nominalAnnualInterestRate').hasError('min')) { + + + {{ 'labels.inputs.Interest Posting Period' | translate }} + + @for (interestPostingPeriodType of interestPostingPeriodTypeData; track interestPostingPeriodType) { + + {{ interestPostingPeriodType.value | translateKey: 'catalogs' }} + + } + - {{ 'labels.inputs.Nominal Annual Interest' | translate }} must be zero or greater + {{ 'labels.inputs.Interest Posting Period' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + +
+ + -
- -
-
- -

{{ 'labels.inputs.Interest Compounding' | translate }}

-
-
- - {{ 'labels.inputs.Interest Compounding Period' | translate }} - - @for (interestCompoundingPeriodType of interestCompoundingPeriodTypeData; track interestCompoundingPeriodType) { - - {{ interestCompoundingPeriodType.value | translateKey: 'catalogs' }} - - } - - - {{ 'labels.inputs.Interest Compounding Period' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ '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 }} - - - - - {{ 'labels.inputs.Interest Calculated using' | translate }} - - @for (interestCalculationType of interestCalculationTypeData; track interestCalculationType) { - - {{ interestCalculationType.value | translateKey: 'catalogs' }} - - } - - - {{ 'labels.inputs.Interest Calculated using' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.Days in Year' | translate }} - - @for ( - interestCalculationDaysInYearType of interestCalculationDaysInYearTypeData; - track interestCalculationDaysInYearType - ) { - - {{ interestCalculationDaysInYearType.value | translateKey: 'catalogs' }} - - } - - - {{ 'labels.inputs.Days in Year' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - +
diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.scss b/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.scss index 10bb836ea..33445c26e 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.scss +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.scss @@ -44,17 +44,36 @@ $gray-900: #202124; } } +.spt-section__badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: #eff6ff; + font-size: 14px; +} + +.spt-body { + background: #fff; +} + .spt-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { + @media (width <= 600px) { grid-template-columns: 1fr; } } +.spt-grid--three { + grid-template-columns: repeat(3, 1fr); +} + .spt-field { width: 100%; @@ -62,3 +81,67 @@ $gray-900: #202124; grid-column: 1 / -1; } } + +.spt-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; + } +} + +.spt-footer { + padding: 0 24px 24px; + border-top: 1px solid #f1f5f9; +} + +.spt-footer__grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + padding-top: 18px; + + @media (width <= 900px) { + grid-template-columns: 1fr; + } +} + +.spt-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; + } +} + +.spt-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/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.ts b/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.ts index 15a488aa2..abedcdab7 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.ts +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.ts @@ -10,6 +10,8 @@ import { Component, OnInit, Input, inject } from '@angular/core'; import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; import { MatTooltip } from '@angular/material/tooltip'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { Dates } from 'app/core/utils/dates'; +import { SettingsService } from 'app/settings/settings.service'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ @@ -24,6 +26,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; }) export class SavingProductTermsStepComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); + private dateUtils = inject(Dates); + private settingsService = inject(SettingsService); @Input() savingProductsTemplate: any; @@ -32,8 +36,22 @@ export class SavingProductTermsStepComponent implements OnInit { 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.createSavingProductTermsForm(); } @@ -42,15 +60,91 @@ export class SavingProductTermsStepComponent implements OnInit { this.interestCompoundingPeriodTypeData = this.savingProductsTemplate.interestCompoundingPeriodTypeOptions; this.interestPostingPeriodTypeData = this.savingProductsTemplate.interestPostingPeriodTypeOptions; this.interestCalculationTypeData = this.savingProductsTemplate.interestCalculationTypeOptions; + this.interestCalculationTypeOptionData = + this.savingProductsTemplate.interestCalculationTypeOption || + this.savingProductsTemplate.interestCalculationTypeOptions; this.interestCalculationDaysInYearTypeData = this.savingProductsTemplate.interestCalculationDaysInYearTypeOptions; + this.periodFrequencyData = this.savingProductsTemplate.periodFrequencyOptions || []; + this.setYesNoOptions(this.savingProductsTemplate?.yesNoOptions); + + const isEditing = !!this.savingProductsTemplate.id; + const defaultPeriod = this.periodFrequencyData[0]?.value || 'Monthly'; this.savingProductTermsForm.patchValue({ - nominalAnnualInterestRate: this.savingProductsTemplate.nominalAnnualInterestRate, - interestCompoundingPeriodType: this.savingProductsTemplate.interestCompoundingPeriodType.id, - interestPostingPeriodType: this.savingProductsTemplate.interestPostingPeriodType.id, - interestCalculationType: this.savingProductsTemplate.interestCalculationType.id, - interestCalculationDaysInYearType: this.savingProductsTemplate.interestCalculationDaysInYearType.id + nominalAnnualInterestRate: isEditing + ? this.savingProductsTemplate.nominalAnnualInterestRate + : this.savingProductsTemplate.nominalAnnualInterestRate, + interestCompoundingPeriodType: isEditing + ? this.savingProductsTemplate.interestCompoundingPeriodType?.id + : this.savingProductsTemplate.interestCompoundingPeriodType?.id, + interestPostingPeriodType: isEditing + ? this.savingProductsTemplate.interestPostingPeriodType?.id + : this.savingProductsTemplate.interestPostingPeriodType?.id, + interestCalculationType: isEditing + ? this.savingProductsTemplate.interestCalculationTypeOption?.id || + this.interestCalculationTypeOptionData?.[0]?.id || + 1 + : this.interestCalculationTypeOptionData?.[0]?.id || 1, + interestCalculationDaysInYearType: isEditing + ? this.savingProductsTemplate.interestCalculationDaysInYearType?.id + : this.savingProductsTemplate.interestCalculationDaysInYearType?.id, + effectiveDate: this.savingProductsTemplate.effectiveDate + ? new Date(this.savingProductsTemplate.effectiveDate) + : '', + interestCalculateSelector: isEditing + ? this.savingProductsTemplate.interestCalculateSelector || 1 + : this.savingProductsTemplate.interestCalculationType?.id || this.interestCalculationTypeData?.[0]?.id || 1, + averagePeriod: isEditing ? this.savingProductsTemplate.averagePeriod || defaultPeriod : defaultPeriod, + autoPost: isEditing ? (this.savingProductsTemplate.autoPost ?? true) : true, + frequencyFixDay: isEditing ? this.savingProductsTemplate.frequencyFixDay || 30 : 30, + lastInterestDate: this.savingProductsTemplate.lastInterestDate + ? new Date(this.savingProductsTemplate.lastInterestDate) + : '' }); + + // Map code value IDs to backend enum values for interestCalculationType + // Backend expects: 1=Daily Balance, 2=Average Daily Balance + const currentInterestCalcType = this.savingProductTermsForm.get('interestCalculationType').value; + if (currentInterestCalcType && currentInterestCalcType > 2) { + // If it's a code value ID (like 340, 341), map it to backend enum + // Default to 1 (Daily Balance) for custom code values + this.savingProductTermsForm.patchValue({ + interestCalculationType: 1 + }); + } + } + + private setYesNoOptions(options: any[]) { + if (!Array.isArray(options)) { + return; + } + + const mappedOptions = options + .map((option: any) => { + const rawValue = option?.name ?? option?.value ?? option; + const normalized = typeof rawValue === 'string' ? rawValue.trim().toUpperCase() : rawValue; + + if (normalized === 'YES') { + return { + value: true, + label: 'Yes' + }; + } + + if (normalized === 'NO') { + return { + value: false, + label: 'No' + }; + } + + return null; + }) + .filter((value: any) => !!value); + + if (mappedOptions.length) { + this.yesNoOptions = mappedOptions; + } } createSavingProductTermsForm() { @@ -77,11 +171,41 @@ export class SavingProductTermsStepComponent implements OnInit { interestCalculationDaysInYearType: [ '', Validators.required - ] + ], + effectiveDate: [''], + interestCalculateSelector: [1], + averagePeriod: ['Monthly'], + autoPost: [true], + frequencyFixDay: [ + 30, + [Validators.min(0)] + ], + lastInterestDate: [''] }); } get savingProductTerms() { - return this.savingProductTermsForm.value; + const formValue = { ...this.savingProductTermsForm.value }; + const dateFormat = this.settingsService.dateFormat; + const prevEffectiveDate: Date = this.savingProductTermsForm.value.effectiveDate; + const prevLastInterestDate: Date = this.savingProductTermsForm.value.lastInterestDate; + + if (formValue.effectiveDate instanceof Date) { + formValue.effectiveDate = this.dateUtils.formatDate(prevEffectiveDate, dateFormat) || ''; + } + + if (formValue.lastInterestDate instanceof Date) { + formValue.lastInterestDate = this.dateUtils.formatDate(prevLastInterestDate, dateFormat) || ''; + } + + // Convert interestCalculationType from code value ID to backend enum value + // Backend expects: 1=Daily Balance, 2=Average Daily Balance + if (formValue.interestCalculationType && formValue.interestCalculationType > 2) { + formValue.interestCalculationType = 1; // Default to Daily Balance + } + + return { + ...formValue + }; } } diff --git a/src/app/products/saving-products/saving-product.resolver.ts b/src/app/products/saving-products/saving-product.resolver.ts index cf6f858dc..1a3926298 100644 --- a/src/app/products/saving-products/saving-product.resolver.ts +++ b/src/app/products/saving-products/saving-product.resolver.ts @@ -24,7 +24,7 @@ export class SavingProductResolver { private productsService = inject(ProductsService); /** - * Returns the saving product data. + * Returns the saving product data with template options for dropdown lookups. * @returns {Observable} */ resolve(route: ActivatedRouteSnapshot): Observable { diff --git a/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.html b/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.html index 211eb5268..c1a5138fa 100644 --- a/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.html +++ b/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.html @@ -18,304 +18,514 @@
-
-
-

{{ savingProduct.name }}

+
+

{{ savingProduct.name }}

- + +

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

-

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

+ +
+ {{ 'labels.inputs.Short Name' | translate }}: + {{ savingProduct.shortName }} +
- - -
- {{ 'labels.inputs.Short Name' | translate }}: - {{ savingProduct.shortName }} + @if (savingProduct.description) { +
+ {{ 'labels.inputs.Description' | translate }}: + {{ savingProduct.description }}
+ } - @if (savingProduct.description) { -
- {{ 'labels.inputs.Description' | translate }}: - {{ savingProduct.description }} + @if ( + savingProduct.accountType || + savingProduct.customerSegment || + savingProduct.productCode || + savingProduct.effectiveDate || + savingProduct.effectiveTo || + savingProduct.endDate || + savingProduct.introducerRequired !== undefined || + savingProduct.shareLinkingEnabled !== undefined + ) { +

Product Classification

+ + + + @if (savingProduct.accountType) { +
+ Account Type: + {{ savingProduct.accountType }}
} -

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

- - - -
- {{ 'labels.inputs.Currency' | translate }}: - {{ savingProduct.currency.name }} -
- -
- {{ 'labels.inputs.Decimal Places' | translate }}: - {{ savingProduct.currency.decimalPlaces }} -
- -
- {{ 'labels.inputs.Currency in multiples of' | translate }}: - {{ savingProduct.currency.inMultiplesOf }} -
- -

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

- - - -
- {{ 'labels.inputs.Nominal Annual Interest' | translate }}: - {{ savingProduct.nominalAnnualInterestRate | formatNumber }} % -
- -
- {{ 'labels.inputs.Interest Compounding Period' | translate }}: - {{ savingProduct.interestCompoundingPeriodType.value | translateKey: 'catalogs' }} -
- -
- {{ 'labels.inputs.Interest Posting Period' | translate }}: - {{ savingProduct.interestPostingPeriodType.value | translateKey: 'catalogs' }} -
- -
- {{ 'labels.inputs.Interest Calculated using' | translate }}: - {{ savingProduct.interestCalculationType.value | translateKey: 'catalogs' }} -
- -
- {{ 'labels.inputs.Days in Year' | translate }}: - {{ - savingProduct.interestCalculationDaysInYearType.value | translateKey: 'catalogs' - }} -
- -

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

- - - - @if (savingProduct.minRequiredOpeningBalance) { -
- {{ 'labels.inputs.Minimum Opening Balance' | translate }}: - {{ savingProduct.minRequiredOpeningBalance | formatNumber }} + @if (savingProduct.customerSegment) { +
+ Customer Segment: + {{ savingProduct.customerSegment }}
} - @if (savingProduct.lockinPeriodFrequency) { -
- {{ 'labels.inputs.Lock-in Period' | translate }}: + @if (savingProduct.productCode) { +
+ Product Code: + {{ savingProduct.productCode }} +
+ } + + @if (savingProduct.effectiveDate) { +
+ Effective Date: + {{ savingProduct.effectiveDate }} +
+ } + + @if (savingProduct.effectiveTo) { +
+ Effective To: + {{ savingProduct.effectiveTo }} +
+ } + + @if (savingProduct.endDate) { +
+ End Date: + {{ savingProduct.endDate }} +
+ } + + @if (savingProduct.introducerRequired !== undefined) { +
+ Introducer Required: {{ - savingProduct.lockinPeriodFrequency + ' ' + savingProduct.lockinPeriodFrequencyType.value + savingProduct.introducerRequired === true || savingProduct.introducerRequired === 'Yes' ? 'Yes' : 'No' }}
} -
- {{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}: - {{ savingProduct.withdrawalFeeForTransfers | yesNo }} -
- - @if (savingProduct.minBalanceForInterestCalculation) { -
- {{ 'labels.inputs.Balance Required for Interest Calculation' | translate }}: - {{ savingProduct.minBalanceForInterestCalculation | formatNumber }} + @if (savingProduct.shareLinkingEnabled !== undefined) { +
+ Share Linking Enabled: + {{ savingProduct.shareLinkingEnabled | yesNo }}
} + } -
- {{ 'labels.inputs.Enforce Minimum Balance' | translate }}: - {{ savingProduct.enforceMinRequiredBalance | yesNo }} -
+

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

- @if (savingProduct.minRequiredBalance) { -
- {{ 'labels.inputs.Minimum Balance' | translate }}: - {{ savingProduct.minRequiredBalance | formatNumber }} -
- } + -
- {{ 'labels.inputs.Withhold Tax is Applicable' | translate }}: - {{ savingProduct.withHoldTax | yesNo }} -
- - @if (savingProduct.withHoldTax) { -
- {{ 'labels.inputs.Tax Group' | translate }}: - {{ savingProduct.taxGroup.name }} -
- } - -
- {{ 'labels.inputs.Is Overdraft Allowed' | translate }}: - {{ savingProduct.allowOverdraft | yesNo }} -
- - @if (savingProduct.minOverdraftForInterestCalculation) { -
- {{ 'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate }}: - {{ savingProduct.minOverdraftForInterestCalculation }} -
- } - - @if (savingProduct.nominalAnnualInterestRateOverdraft) { -
- {{ 'labels.inputs.Nominal Annual Interest for Overdraft' | translate }}: - {{ savingProduct.nominalAnnualInterestRateOverdraft }} % -
- } - - @if (savingProduct.overdraftLimit) { -
- {{ 'labels.inputs.Maximum Overdraft Amount Limit' | translate }}: - {{ savingProduct.overdraftLimit | formatNumber }} -
- } - -
- {{ 'labels.inputs.Enable Dormancy Tracking' | translate }}: - {{ savingProduct.isDormancyTrackingActive | yesNo }} -
- - @if (savingProduct.isDormancyTrackingActive) { -
- {{ 'labels.inputs.Number of Days to Inactive sub-status' | translate }}: - {{ savingProduct.daysToInactive }} - {{ 'labels.inputs.Number of Days to Dormant sub-status' | translate }}: - {{ savingProduct.daysToDormancy }} - {{ 'labels.inputs.Number of Days to Escheat' | translate }}: - {{ savingProduct.daysToEscheat }} -
- } - - @if (savingProduct.charges.length) { -
-

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

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

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

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

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

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

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

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

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

- - - - - - - - - - - -
{{ 'labels.inputs.Fees' | translate }} - {{ feesIncome.charge.name }} - {{ 'labels.inputs.Income Account' | translate }} - ({{ feesIncome.incomeAccount.glCode }}) {{ feesIncome.incomeAccount.name }} -
-
- } - @if (savingProduct.penaltyToIncomeAccountMappings?.length) { -
-

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

- - - - - - - - - - - -
{{ 'labels.inputs.Penalty' | translate }} - {{ penaltyIncome.charge.name }} - {{ 'labels.inputs.Income Account' | translate }} - ({{ penaltyIncome.incomeAccount.glCode }}) {{ penaltyIncome.incomeAccount.name }} -
-
- } -
- } +
+ {{ 'labels.inputs.Currency' | translate }}: + {{ savingProduct.currency.name }}
+ +
+ {{ 'labels.inputs.Decimal Places' | translate }}: + {{ savingProduct.currency.decimalPlaces }} +
+ + @if (savingProduct.currency.inMultiplesOf !== undefined && savingProduct.currency.inMultiplesOf !== null) { +
+ {{ 'labels.inputs.Currency in multiples of' | translate }}: + {{ savingProduct.currency.inMultiplesOf }} +
+ } + + @if (savingProduct.dormancyAlert !== undefined) { +
+ Dormancy Alert: + {{ savingProduct.dormancyAlert | yesNo }} +
+ } + + @if (savingProduct.nominationFacility !== undefined) { +
+ Nomination Facility: + {{ savingProduct.nominationFacility | yesNo }} +
+ } + +

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

+ + + +
+ {{ 'labels.inputs.Nominal Annual Interest' | translate }}: + {{ savingProduct.nominalAnnualInterestRate | formatNumber }} % +
+ +
+ {{ 'labels.inputs.Interest Compounding Period' | translate }}: + {{ savingProduct.interestCompoundingPeriodType.value | translateKey: 'catalogs' }} +
+ +
+ {{ 'labels.inputs.Interest Posting Period' | translate }}: + {{ savingProduct.interestPostingPeriodType.value | translateKey: 'catalogs' }} +
+ +
+ {{ 'labels.inputs.Interest Calculated using' | translate }}: + {{ savingProduct.interestCalculationType.value | translateKey: 'catalogs' }} +
+ +
+ {{ 'labels.inputs.Days in Year' | translate }}: + {{ savingProduct.interestCalculationDaysInYearType.value | translateKey: 'catalogs' }} +
+ + @if (savingProduct.effectiveDate) { +
+ Effective Date: + {{ savingProduct.effectiveDate }} +
+ } + +
+ Interest Calculate: + {{ savingProduct.interestCalculateSelector }} +
+ + @if (savingProduct.averagePeriod) { +
+ Average Period: + {{ savingProduct.averagePeriod }} +
+ } + +
+ Auto Post: + {{ savingProduct.autoPost | yesNo }} +
+ + @if (savingProduct.frequencyFixDay) { +
+ Frequency Fix Day: + {{ savingProduct.frequencyFixDay }} +
+ } + + @if (savingProduct.lastInterestDate) { +
+ Last Interest Date: + {{ savingProduct.lastInterestDate }} +
+ } + +

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

+ + + + @if (savingProduct.minRequiredOpeningBalance) { +
+ {{ 'labels.inputs.Minimum Opening Balance' | translate }}: + {{ savingProduct.minRequiredOpeningBalance | formatNumber }} +
+ } + + @if (savingProduct.lockinPeriodFrequency) { +
+ {{ 'labels.inputs.Lock-in Period' | translate }}: + {{ + savingProduct.lockinPeriodFrequency + ' ' + savingProduct.lockinPeriodFrequencyType.value + }} +
+ } + +
+ {{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}: + {{ savingProduct.withdrawalFeeForTransfers | yesNo }} +
+ + @if (savingProduct.balanceFrequency) { +
+ Balance Frequency: + {{ savingProduct.balanceFrequency }} +
+ } + +
+ Enable Lock-in Period: + {{ savingProduct.enableLockinPeriod | yesNo }} +
+ + @if (savingProduct.minBalanceForInterestCalculation) { +
+ {{ 'labels.inputs.Balance Required for Interest Calculation' | translate }}: + {{ savingProduct.minBalanceForInterestCalculation | formatNumber }} +
+ } + +
+ {{ 'labels.inputs.Enforce Minimum Balance' | translate }}: + {{ savingProduct.enforceMinRequiredBalance | yesNo }} +
+ + @if (savingProduct.minRequiredBalance) { +
+ {{ 'labels.inputs.Minimum Balance' | translate }}: + {{ savingProduct.minRequiredBalance | formatNumber }} +
+ } + +
+ {{ 'labels.inputs.Withhold Tax is Applicable' | translate }}: + {{ savingProduct.withHoldTax | yesNo }} +
+ + @if (savingProduct.withHoldTax) { +
+ {{ 'labels.inputs.Tax Group' | translate }}: + {{ savingProduct.taxGroup.name }} +
+ } + +
+ {{ 'labels.inputs.Is Overdraft Allowed' | translate }}: + {{ savingProduct.allowOverdraft | yesNo }} +
+ +
+ Account Closed Zero Balance in Day Closing: + {{ savingProduct.accountClosedZeroBalanceInDayClosing | yesNo }} +
+ + @if (savingProduct.minOverdraftForInterestCalculation) { +
+ {{ 'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate }}: + {{ savingProduct.minOverdraftForInterestCalculation | formatNumber }} +
+ } + + @if (savingProduct.nominalAnnualInterestRateOverdraft) { +
+ {{ 'labels.inputs.Nominal Annual Interest for Overdraft' | translate }}: + {{ savingProduct.nominalAnnualInterestRateOverdraft | formatNumber }} % +
+ } + + @if (savingProduct.overdraftLimit) { +
+ {{ 'labels.inputs.Maximum Overdraft Amount Limit' | translate }}: + {{ savingProduct.overdraftLimit | formatNumber }} +
+ } + +
+ {{ 'labels.inputs.Enable Dormancy Tracking' | translate }}: + {{ savingProduct.isDormancyTrackingActive | yesNo }} +
+ + @if (savingProduct.isDormancyTrackingActive) { +
+ {{ 'labels.inputs.Number of Days to Inactive sub-status' | translate }}: + {{ savingProduct.daysToInactive }} + {{ 'labels.inputs.Number of Days to Dormant sub-status' | translate }}: + {{ savingProduct.daysToDormancy }} + {{ 'labels.inputs.Number of Days to Escheat' | translate }}: + {{ savingProduct.daysToEscheat }} +
+ } + +

Account Facilities

+ + + @if (savingProduct.accountFacilityConfig) { +
+ @for (feature of accountFacilityFeatureItems; track feature.key) { + @if (savingProduct.accountFacilityConfig[feature.key] !== undefined) { +
+ {{ feature.label }}: + {{ savingProduct.accountFacilityConfig[feature.key] | yesNo }} +
+ } + } +
+ } + + @if (savingProduct.accountFacilityLimits?.length) { +
+

Account Facility Limits

+ + + + + + + + + + + + + + + + + + + + +
Limit Type{{ row.limitType }}General Limit{{ row.generalLimit }}Minor Account Limit{{ row.minorAccountLimit }}Small Saving Limit{{ row.smallSavingLimit }}
+
+ } + + @if (savingProduct.depositLimitKyc) { +
+

Deposit Limit (KYC)

+
+ General: + {{ savingProduct.depositLimitKyc.general.kycType }} - {{ savingProduct.depositLimitKyc.general.limit }} +
+
+ Minor: + {{ savingProduct.depositLimitKyc.minor.kycType }} - {{ savingProduct.depositLimitKyc.minor.limit }} +
+
+ Small Saving: + {{ savingProduct.depositLimitKyc.smallSaving.kycType }} - + {{ savingProduct.depositLimitKyc.smallSaving.limit }} +
+
+ } + + @if (savingProduct.charges.length) { +
+

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

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

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

+ + + + + + + @if ( + savingProduct.paymentChannelToFundSourceMappings?.length || + savingProduct.feeToIncomeAccountMappings?.length || + savingProduct.penaltyToIncomeAccountMappings?.length + ) { +
+

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

+ + @if (savingProduct.paymentChannelToFundSourceMappings?.length) { +
+

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

+ + + + + + + + + + + +
{{ 'labels.inputs.Payment Type' | translate }} + {{ paymentFundSource.paymentType.name }} + {{ 'labels.inputs.Fund Source' | translate }} + ({{ paymentFundSource.fundSourceAccount.glCode }}) {{ paymentFundSource.fundSourceAccount.name }} +
+
+ } + @if (savingProduct.feeToIncomeAccountMappings?.length) { +
+

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

+ + + + + + + + + + + +
{{ 'labels.inputs.Fee/Charge' | translate }} + {{ fee.charge.name }} + {{ 'labels.inputs.Income Account' | translate }}({{ fee.incomeAccount.glCode }}) {{ fee.incomeAccount.name }}
+
+ } + @if (savingProduct.penaltyToIncomeAccountMappings?.length) { +
+

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

+ + + + + + + + + + + +
{{ 'labels.inputs.Penalty' | translate }} + {{ penalty.charge.name }} + {{ 'labels.inputs.Income Account' | translate }} + ({{ penalty.incomeAccount.glCode }}) {{ penalty.incomeAccount.name }} +
+
+ } +
+ }
diff --git a/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.scss b/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.scss index 258b214cd..84df78f65 100644 --- a/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.scss +++ b/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.scss @@ -71,3 +71,95 @@ mat-divider { .margin-t { margin-top: 1em; } + +.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; +} + +.spa-gl-section 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%; +} + +.spa-other-gl-table:first-of-type { + margin-top: 0.25rem; +} + +.spa-other-gl-summary .mat-elevation-z1 { + border-radius: 10px; + overflow: hidden; +} + +.spa-other-gl-table th.mat-mdc-header-cell, +.spa-other-gl-table td.mat-mdc-cell { + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +.spa-other-gl-table th.mat-mdc-header-cell:first-child, +.spa-other-gl-table td.mat-mdc-cell:first-child { + width: 38%; +} + +.spa-other-gl-table th.mat-mdc-header-cell:last-child, +.spa-other-gl-table td.mat-mdc-cell:last-child { + width: 62%; + word-break: break-word; +} + +@media (width <= 768px) { + .spa-gl-row { + grid-template-columns: 1fr; + gap: 0.35rem; + } +} diff --git a/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.ts b/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.ts index f52332234..63de34766 100644 --- a/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.ts +++ b/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.ts @@ -72,6 +72,25 @@ export class SavingProductGeneralTabComponent { 'incomeAccountId' ]; + accountFacilityFeatureItems = [ + { key: 'atmFacility', label: 'ATM Facility' }, + { key: 'atmWithdrawalFacility', label: 'ATM Withdrawal Facility' }, + { key: 'upiFacility', label: 'UPI Facility' }, + { key: 'posTransactionsFacility', label: 'POS Transactions Facility' }, + { key: 'chequeBook', label: 'Cheque Book' }, + { key: 'impsFacility', label: 'IMPS Facility' }, + { key: 'smsAlerts', label: 'SMS Alerts' }, + { key: 'bbpsFacility', label: 'BBPS Facility' }, + { key: 'insuranceCover', label: 'Insurance Cover' }, + { key: 'bepgFacility', label: 'BEPG Facility' }, + { key: 'neftFacility', label: 'NEFT Facility' }, + { key: 'rtgsFacility', label: 'RTGS Facility' }, + { key: 'interBankTotalCapFacility', label: 'Inter-Bank Total Cap Facility' }, + { key: 'withdrawalFacility', label: 'Withdrawal Facility' }, + { key: 'depositFacilityKyc', label: 'Deposit Facility (KYC)' }, + { key: 'virtualDebitCard', label: 'Virtual Debit Card' } + ]; + constructor() { this.route.data.subscribe((data: { savingProduct: any }) => { this.savingProduct = data.savingProduct; @@ -85,4 +104,22 @@ export class SavingProductGeneralTabComponent { isAccrualAccounting(): boolean { return this.accounting.isAccrualAccounting(this.savingProduct.accountingRule); } + + 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 '-'; + } + + const account = options.find((option) => option?.glCode === glCode); + return account ? `(${account.glCode}) ${account.name}` : glCode; + } } diff --git a/src/environments/.env.ts b/src/environments/.env.ts index 9dd2af68c..a46de5b84 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -5,13 +5,12 @@ * 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/. */ - // IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN! /* tslint:disable */ export default { 'mifos_x': { - 'version': '260407', - 'hash': 'gb890d24bb' + 'version': '260413', + 'hash': '79e455c4' }, 'allow_switching_backend_instance': true }; From fe61c925ae72513e459d59966b4e41ebc5ab91e2 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Wed, 15 Apr 2026 20:46:15 +0530 Subject: [PATCH 02/12] update current product code and RD code --- .ai/mcp/mcp.json | 0 .husky/{pre-commit.bak => pre-commit} | 0 .qwen/settings.json | 8 +- .../create-charge.component.scss | 4 +- .../edit-charge/edit-charge.component.scss | 4 +- .../view-charge/view-charge.component.scss | 4 +- .../create-collateral.component.scss | 4 +- .../edit-collateral.component.scss | 4 +- .../view-collateral.component.scss | 2 +- .../create-current-product.component.html | 69 +- .../create-current-product.component.scss | 8 +- .../create-current-product.component.ts | 109 ++- ...ent-product-accounting-step.component.html | 121 +++ ...ent-product-accounting-step.component.scss | 314 +++++++ ...rrent-product-accounting-step.component.ts | 495 +++++++++++ ...urrent-product-charges-step.component.html | 93 ++ ...urrent-product-charges-step.component.scss | 103 +++ .../current-product-charges-step.component.ts | 108 +++ ...rrent-product-currency-step.component.html | 111 +++ ...rrent-product-currency-step.component.scss | 114 +++ ...current-product-currency-step.component.ts | 94 +++ ...urrent-product-details-step.component.html | 117 +++ ...urrent-product-details-step.component.scss | 70 ++ .../current-product-details-step.component.ts | 190 +++++ ...current-product-limits-step.component.html | 351 ++++++++ ...current-product-limits-step.component.scss | 257 ++++++ .../current-product-limits-step.component.ts | 250 ++++++ ...urrent-product-preview-step.component.html | 799 ++++++++++++++++++ ...urrent-product-preview-step.component.scss | 165 ++++ .../current-product-preview-step.component.ts | 247 ++++++ ...rrent-product-settings-step.component.html | 189 +++++ ...rrent-product-settings-step.component.scss | 263 ++++++ ...current-product-settings-step.component.ts | 172 ++++ .../current-product-terms-step.component.html | 68 ++ .../current-product-terms-step.component.scss | 148 ++++ .../current-product-terms-step.component.ts | 112 +++ .../edit-current-product.component.html | 203 +++-- .../edit-current-product.component.scss | 92 +- .../edit-current-product.component.ts | 143 ++-- ...current-product-general-tab.component.html | 698 +++++++++++++-- .../current-product-general-tab.component.ts | 35 + ...reate-fixed-deposit-product.component.scss | 9 +- ...sit-product-accounting-step.component.scss | 4 +- ...eposit-product-charges-step.component.scss | 2 +- ...posit-product-currency-step.component.scss | 2 +- ...eposit-product-details-step.component.scss | 2 +- ...ct-interest-rate-chart-step.component.scss | 2 +- ...eposit-product-preview-step.component.scss | 2 +- ...posit-product-settings-step.component.scss | 2 +- ...-deposit-product-terms-step.component.scss | 2 +- .../create-floating-rate.component.scss | 2 +- .../edit-floating-rate.component.scss | 2 +- .../view-floating-rate.component.scss | 2 +- .../create-loan-product.component.scss | 8 +- ...oan-product-accounting-step.component.scss | 2 +- ...red-income-recognition-step.component.scss | 2 +- .../loan-product-charges-step.component.scss | 2 +- .../loan-product-currency-step.component.scss | 2 +- .../loan-product-details-step.component.scss | 2 +- ...roduct-interest-refund-step.component.scss | 2 +- .../loan-product-settings-step.component.scss | 4 +- .../loan-product-terms-step.component.scss | 8 +- .../create-bucket.component.scss | 2 +- .../edit-bucket/edit-bucket.component.scss | 2 +- .../create-range/create-range.component.scss | 4 +- .../edit-range/edit-range.component.scss | 4 +- .../manage-delinquency-buckets.component.scss | 2 +- .../create-tax-component.component.scss | 2 +- .../edit-tax-component.component.scss | 2 +- .../view-tax-component.component.scss | 6 +- .../manage-tax-configurations.component.scss | 4 +- .../create-tax-group.component.scss | 2 +- .../edit-tax-group.component.scss | 2 +- .../view-tax-group.component.scss | 6 +- .../create-product-mix.component.scss | 2 +- .../edit-product-mix.component.scss | 2 +- .../view-product-mix.component.scss | 2 +- src/app/products/products.component.scss | 28 +- ...e-recurring-deposit-product.component.html | 13 +- ...e-recurring-deposit-product.component.scss | 93 +- ...sit-product-accounting-step.component.html | 356 +++----- ...sit-product-accounting-step.component.scss | 307 ++++++- ...posit-product-accounting-step.component.ts | 714 ++++++++-------- ...eposit-product-charges-step.component.html | 9 +- ...eposit-product-charges-step.component.scss | 22 +- ...posit-product-currency-step.component.html | 150 ++-- ...posit-product-currency-step.component.scss | 69 +- ...deposit-product-currency-step.component.ts | 83 +- ...eposit-product-details-step.component.html | 68 +- ...eposit-product-details-step.component.scss | 40 +- ...-deposit-product-details-step.component.ts | 92 +- ...ct-interest-rate-chart-step.component.html | 50 +- ...ct-interest-rate-chart-step.component.scss | 2 +- ...eposit-product-preview-step.component.html | 30 +- ...eposit-product-preview-step.component.scss | 38 +- ...posit-product-settings-step.component.html | 292 +++---- ...posit-product-settings-step.component.scss | 129 ++- ...deposit-product-settings-step.component.ts | 130 +-- ...-deposit-product-terms-step.component.html | 223 +++-- ...-deposit-product-terms-step.component.scss | 108 ++- ...ng-deposit-product-terms-step.component.ts | 121 ++- ...ing-product-accounting-step.component.html | 17 +- ...aving-product-accounting-step.component.ts | 35 +- ...saving-product-details-step.component.html | 2 +- .../saving-product-details-step.component.ts | 4 +- .../saving-product-general-tab.component.html | 71 ++ .../create-share-product.component.scss | 8 +- ...are-product-accounting-step.component.scss | 2 +- .../share-product-charges-step.component.scss | 4 +- ...share-product-currency-step.component.scss | 3 +- .../share-product-details-step.component.scss | 3 +- .../share-product-preview-step.component.scss | 2 +- ...share-product-settings-step.component.scss | 2 +- .../share-product-terms-step.component.scss | 3 +- src/environments/.env.ts | 4 +- 115 files changed, 8057 insertions(+), 1717 deletions(-) create mode 100644 .ai/mcp/mcp.json rename .husky/{pre-commit.bak => pre-commit} (100%) create mode 100644 src/app/products/current-products/current-product-stepper/current-product-accounting-step/current-product-accounting-step.component.html create mode 100644 src/app/products/current-products/current-product-stepper/current-product-accounting-step/current-product-accounting-step.component.scss create mode 100644 src/app/products/current-products/current-product-stepper/current-product-accounting-step/current-product-accounting-step.component.ts create mode 100644 src/app/products/current-products/current-product-stepper/current-product-charges-step/current-product-charges-step.component.html create mode 100644 src/app/products/current-products/current-product-stepper/current-product-charges-step/current-product-charges-step.component.scss create mode 100644 src/app/products/current-products/current-product-stepper/current-product-charges-step/current-product-charges-step.component.ts create mode 100644 src/app/products/current-products/current-product-stepper/current-product-currency-step/current-product-currency-step.component.html create mode 100644 src/app/products/current-products/current-product-stepper/current-product-currency-step/current-product-currency-step.component.scss create mode 100644 src/app/products/current-products/current-product-stepper/current-product-currency-step/current-product-currency-step.component.ts create mode 100644 src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.html create mode 100644 src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.scss create mode 100644 src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.ts create mode 100644 src/app/products/current-products/current-product-stepper/current-product-limits-step/current-product-limits-step.component.html create mode 100644 src/app/products/current-products/current-product-stepper/current-product-limits-step/current-product-limits-step.component.scss create mode 100644 src/app/products/current-products/current-product-stepper/current-product-limits-step/current-product-limits-step.component.ts create mode 100644 src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.html create mode 100644 src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.scss create mode 100644 src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.ts create mode 100644 src/app/products/current-products/current-product-stepper/current-product-settings-step/current-product-settings-step.component.html create mode 100644 src/app/products/current-products/current-product-stepper/current-product-settings-step/current-product-settings-step.component.scss create mode 100644 src/app/products/current-products/current-product-stepper/current-product-settings-step/current-product-settings-step.component.ts create mode 100644 src/app/products/current-products/current-product-stepper/current-product-terms-step/current-product-terms-step.component.html create mode 100644 src/app/products/current-products/current-product-stepper/current-product-terms-step/current-product-terms-step.component.scss create mode 100644 src/app/products/current-products/current-product-stepper/current-product-terms-step/current-product-terms-step.component.ts diff --git a/.ai/mcp/mcp.json b/.ai/mcp/mcp.json new file mode 100644 index 000000000..e69de29bb diff --git a/.husky/pre-commit.bak b/.husky/pre-commit similarity index 100% rename from .husky/pre-commit.bak rename to .husky/pre-commit diff --git a/.qwen/settings.json b/.qwen/settings.json index e16bd106b..d2edcaf7c 100644 --- a/.qwen/settings.json +++ b/.qwen/settings.json @@ -6,8 +6,12 @@ "Bash(git commit *)", "Bash(select-object *)", "Bash(git config *)", - "Bash(set *)" + "Bash(set *)", + "Bash(git push)", + "Bash(xcopy *)", + "Bash(powershell *)", + "mcp__idea__build_project" ] }, "$version": 3 -} \ No newline at end of file +} diff --git a/src/app/products/charges/create-charge/create-charge.component.scss b/src/app/products/charges/create-charge/create-charge.component.scss index cc1d898a4..d48d0ed63 100644 --- a/src/app/products/charges/create-charge/create-charge.component.scss +++ b/src/app/products/charges/create-charge/create-charge.component.scss @@ -98,10 +98,10 @@ gap: 12px; margin-top: 20px; padding: 16px 0; - border-top: 1px solid rgba(0, 0, 0, 0.08); + border-top: 1px solid rgb(0 0 0 / 8%); } -@media (max-width: 600px) { +@media (width <= 600px) { .cc-grid { grid-template-columns: 1fr; } diff --git a/src/app/products/charges/edit-charge/edit-charge.component.scss b/src/app/products/charges/edit-charge/edit-charge.component.scss index 8263ebc96..3126303d6 100644 --- a/src/app/products/charges/edit-charge/edit-charge.component.scss +++ b/src/app/products/charges/edit-charge/edit-charge.component.scss @@ -98,10 +98,10 @@ gap: 12px; margin-top: 20px; padding: 16px 0; - border-top: 1px solid rgba(0, 0, 0, 0.08); + border-top: 1px solid rgb(0 0 0 / 8%); } -@media (max-width: 600px) { +@media (width <= 600px) { .ec-grid { grid-template-columns: 1fr; } diff --git a/src/app/products/charges/view-charge/view-charge.component.scss b/src/app/products/charges/view-charge/view-charge.component.scss index 07e5ecc49..fe770b1db 100644 --- a/src/app/products/charges/view-charge/view-charge.component.scss +++ b/src/app/products/charges/view-charge/view-charge.component.scss @@ -109,10 +109,10 @@ gap: 12px; margin-top: 20px; padding: 16px 0; - border-top: 1px solid rgba(0, 0, 0, 0.08); + border-top: 1px solid rgb(0 0 0 / 8%); } -@media (max-width: 600px) { +@media (width <= 600px) { .vc-top-bar { flex-direction: column; align-items: flex-start; diff --git a/src/app/products/collaterals/create-collateral/create-collateral.component.scss b/src/app/products/collaterals/create-collateral/create-collateral.component.scss index 8943f0478..627a547d4 100644 --- a/src/app/products/collaterals/create-collateral/create-collateral.component.scss +++ b/src/app/products/collaterals/create-collateral/create-collateral.component.scss @@ -72,10 +72,10 @@ gap: 12px; margin-top: 20px; padding: 16px 0; - border-top: 1px solid rgba(0, 0, 0, 0.08); + border-top: 1px solid rgb(0 0 0 / 8%); } -@media (max-width: 600px) { +@media (width <= 600px) { .ccl-section__body { grid-template-columns: 1fr; } diff --git a/src/app/products/collaterals/edit-collateral/edit-collateral.component.scss b/src/app/products/collaterals/edit-collateral/edit-collateral.component.scss index cfd828fc7..e46a033ad 100644 --- a/src/app/products/collaterals/edit-collateral/edit-collateral.component.scss +++ b/src/app/products/collaterals/edit-collateral/edit-collateral.component.scss @@ -72,10 +72,10 @@ gap: 12px; margin-top: 20px; padding: 16px 0; - border-top: 1px solid rgba(0, 0, 0, 0.08); + border-top: 1px solid rgb(0 0 0 / 8%); } -@media (max-width: 600px) { +@media (width <= 600px) { .ecl-section__body { grid-template-columns: 1fr; } diff --git a/src/app/products/collaterals/view-collateral/view-collateral.component.scss b/src/app/products/collaterals/view-collateral/view-collateral.component.scss index b99904792..2ea21f890 100644 --- a/src/app/products/collaterals/view-collateral/view-collateral.component.scss +++ b/src/app/products/collaterals/view-collateral/view-collateral.component.scss @@ -99,7 +99,7 @@ } } -@media (max-width: 600px) { +@media (width <= 600px) { .vcl-top-bar { flex-direction: column; align-items: flex-start; diff --git a/src/app/products/current-products/create-current-product/create-current-product.component.html b/src/app/products/current-products/create-current-product/create-current-product.component.html index 4114aa60a..6662df7d6 100644 --- a/src/app/products/current-products/create-current-product/create-current-product.component.html +++ b/src/app/products/current-products/create-current-product/create-current-product.component.html @@ -12,14 +12,21 @@

{{ 'labels.heading.Create Current Product' | translate }}

- + {{ 'labels.inputs.DETAILS' | translate }}
- +
@@ -29,9 +36,9 @@ {{ 'labels.inputs.CURRENCY' | translate }}
- +
@@ -41,9 +48,9 @@ {{ 'labels.inputs.TERMS' | translate }}
- +
@@ -53,9 +60,9 @@ {{ 'labels.inputs.SETTINGS' | translate }}
- +
@@ -65,10 +72,22 @@ {{ 'labels.inputs.CHARGES' | translate }}
- + > +
+ + + + + + Digital + +
+
@@ -78,13 +97,13 @@ {{ 'labels.inputs.ACCOUNTING' | translate }}
- + >
@@ -95,13 +114,13 @@ {{ 'labels.inputs.PREVIEW' | translate }}
- + >
} @@ -114,19 +133,19 @@
@if (selectedTabIndex > 0) { } @if (selectedTabIndex < totalTabs - 1) { } @if (formValid && selectedTabIndex === totalTabs - 1) { } diff --git a/src/app/products/current-products/create-current-product/create-current-product.component.scss b/src/app/products/current-products/create-current-product/create-current-product.component.scss index 946837366..98f8a7bee 100644 --- a/src/app/products/current-products/create-current-product/create-current-product.component.scss +++ b/src/app/products/current-products/create-current-product/create-current-product.component.scss @@ -35,11 +35,11 @@ $primary: #1353a0; .cp-tabs { background: var(--md-sys-color-surface, #fff); border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + box-shadow: 0 2px 8px rgb(0 0 0 / 8%); ::ng-deep { .mat-mdc-tab-header { - border-bottom: 1px solid rgba(0, 0, 0, 0.08); + border-bottom: 1px solid rgb(0 0 0 / 8%); } .mat-mdc-tab { @@ -77,10 +77,10 @@ $primary: #1353a0; align-items: center; margin-top: 20px; padding: 16px 0; - border-top: 1px solid rgba(0, 0, 0, 0.08); + border-top: 1px solid rgb(0 0 0 / 8%); &__cancel { - color: rgba(0, 0, 0, 0.6); + color: rgb(0 0 0 / 60%); } &__right { diff --git a/src/app/products/current-products/create-current-product/create-current-product.component.ts b/src/app/products/current-products/create-current-product/create-current-product.component.ts index da403853c..f0b8477d5 100644 --- a/src/app/products/current-products/create-current-product/create-current-product.component.ts +++ b/src/app/products/current-products/create-current-product/create-current-product.component.ts @@ -8,13 +8,14 @@ import { Component, ViewChild, inject } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { SavingProductDetailsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component'; -import { SavingProductCurrencyStepComponent } from '../../saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component'; -import { SavingProductTermsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component'; -import { SavingProductSettingsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component'; -import { SavingProductChargesStepComponent } from '../../saving-products/saving-product-stepper/saving-product-charges-step/saving-product-charges-step.component'; -import { SavingProductAccountingStepComponent } from '../../saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component'; -import { SavingProductPreviewStepComponent } from '../../saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component'; +import { CurrentProductDetailsStepComponent } from '../current-product-stepper/current-product-details-step/current-product-details-step.component'; +import { CurrentProductCurrencyStepComponent } from '../current-product-stepper/current-product-currency-step/current-product-currency-step.component'; +import { CurrentProductTermsStepComponent } from '../current-product-stepper/current-product-terms-step/current-product-terms-step.component'; +import { CurrentProductSettingsStepComponent } from '../current-product-stepper/current-product-settings-step/current-product-settings-step.component'; +import { CurrentProductChargesStepComponent } from '../current-product-stepper/current-product-charges-step/current-product-charges-step.component'; +import { CurrentProductLimitsStepComponent } from '../current-product-stepper/current-product-limits-step/current-product-limits-step.component'; +import { CurrentProductAccountingStepComponent } from '../current-product-stepper/current-product-accounting-step/current-product-accounting-step.component'; +import { CurrentProductPreviewStepComponent } from '../current-product-stepper/current-product-preview-step/current-product-preview-step.component'; import { ProductsService } from 'app/products/products.service'; import { SettingsService } from 'app/settings/settings.service'; import { Accounting } from 'app/core/utils/accounting'; @@ -28,13 +29,14 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; imports: [ ...STANDALONE_SHARED_IMPORTS, MatTabsModule, - SavingProductDetailsStepComponent, - SavingProductCurrencyStepComponent, - SavingProductTermsStepComponent, - SavingProductSettingsStepComponent, - SavingProductChargesStepComponent, - SavingProductAccountingStepComponent, - SavingProductPreviewStepComponent + CurrentProductDetailsStepComponent, + CurrentProductCurrencyStepComponent, + CurrentProductTermsStepComponent, + CurrentProductSettingsStepComponent, + CurrentProductChargesStepComponent, + CurrentProductLimitsStepComponent, + CurrentProductAccountingStepComponent, + CurrentProductPreviewStepComponent ] }) export class CreateCurrentProductComponent { @@ -44,13 +46,14 @@ export class CreateCurrentProductComponent { private settingsService = inject(SettingsService); private accounting = inject(Accounting); - @ViewChild(SavingProductDetailsStepComponent, { static: true }) detailsStep: SavingProductDetailsStepComponent; - @ViewChild(SavingProductCurrencyStepComponent, { static: true }) currencyStep: SavingProductCurrencyStepComponent; - @ViewChild(SavingProductTermsStepComponent, { static: true }) termsStep: SavingProductTermsStepComponent; - @ViewChild(SavingProductSettingsStepComponent, { static: true }) settingsStep: SavingProductSettingsStepComponent; - @ViewChild(SavingProductChargesStepComponent, { static: true }) chargesStep: SavingProductChargesStepComponent; - @ViewChild(SavingProductAccountingStepComponent, { static: true }) - accountingStep: SavingProductAccountingStepComponent; + @ViewChild(CurrentProductDetailsStepComponent, { static: true }) detailsStep: CurrentProductDetailsStepComponent; + @ViewChild(CurrentProductCurrencyStepComponent, { static: true }) currencyStep: CurrentProductCurrencyStepComponent; + @ViewChild(CurrentProductTermsStepComponent, { static: true }) termsStep: CurrentProductTermsStepComponent; + @ViewChild(CurrentProductSettingsStepComponent, { static: true }) settingsStep: CurrentProductSettingsStepComponent; + @ViewChild(CurrentProductChargesStepComponent, { static: true }) chargesStep: CurrentProductChargesStepComponent; + @ViewChild(CurrentProductLimitsStepComponent, { static: true }) limitsStep: CurrentProductLimitsStepComponent; + @ViewChild(CurrentProductAccountingStepComponent, { static: true }) + accountingStep: CurrentProductAccountingStepComponent; currentProductsTemplate: any; accountingRuleData: string[] = []; @@ -64,23 +67,26 @@ export class CreateCurrentProductComponent { } get detailsForm() { - return this.detailsStep.savingProductDetailsForm; + return this.detailsStep.CurrentProductDetailsForm; } get currencyForm() { - return this.currencyStep.savingProductCurrencyForm; + return this.currencyStep.CurrentProductCurrencyForm; } get termsForm() { - return this.termsStep.savingProductTermsForm; + return this.termsStep.CurrentProductTermsForm; } get settingsForm() { - return this.settingsStep.savingProductSettingsForm; + return this.settingsStep.CurrentProductSettingsForm; + } + get limitsForm(): void { + return; // Limits step doesn't have a form, it's data-driven } get accountingForm() { - return this.accountingStep.savingProductAccountingForm; + return this.accountingStep.CurrentProductAccountingForm; } get totalTabs(): number { - return this.formValid ? 7 : 6; + return this.formValid ? 8 : 7; } get formValid() { @@ -95,23 +101,50 @@ export class CreateCurrentProductComponent { get currentProduct() { return { - ...this.detailsStep.savingProductDetails, - ...this.currencyStep.savingProductCurrency, - ...this.termsStep.savingProductTerms, - ...this.settingsStep.savingProductSettings, - ...this.chargesStep.savingProductCharges, - ...this.accountingStep.savingProductAccounting + ...this.detailsStep.CurrentProductDetails, + ...this.currencyStep.CurrentProductCurrency, + ...this.termsStep.CurrentProductTerms, + ...this.settingsStep.CurrentProductSettings, + ...this.chargesStep.CurrentProductCharges, + ...this.limitsStep.CurrentProductLimits, + ...this.accountingStep.CurrentProductAccounting }; } - submit() { - const product = { + private prepareCurrentProductPayload() { + const currentProduct = { ...this.currentProduct, charges: this.currentProduct.charges.map((charge: any) => ({ id: charge.id })), - locale: this.settingsService.language.code + locale: this.settingsService.language.code, + dateFormat: 'dd MMMM yyyy' }; - delete product.advancedAccountingRules; - this.productsService.createCurrentProduct(product).subscribe((response: any) => { + delete currentProduct.advancedAccountingRules; + + // Convert lockinPeriodFrequencyType from string to integer + // Backend expects: 0=DAYS, 1=WEEKS, 2=MONTHS, 3=YEARS + if (currentProduct.lockinPeriodFrequencyType) { + const frequencyTypeMapping: { [key: string]: number } = { + DAYS: 0, + WEEKS: 1, + MONTHS: 2, + YEARS: 3 + }; + currentProduct.lockinPeriodFrequencyType = frequencyTypeMapping[currentProduct.lockinPeriodFrequencyType]; + } + + Object.keys(currentProduct).forEach((key) => { + if (currentProduct[key] === '' || currentProduct[key] === null || currentProduct[key] === undefined) { + delete currentProduct[key]; + } + }); + + return currentProduct; + } + + submit() { + const currentProduct = this.prepareCurrentProductPayload(); + console.log('Current product submit payload:', currentProduct); + this.productsService.createCurrentProduct(currentProduct).subscribe((response: any) => { this.router.navigate( [ '../', diff --git a/src/app/products/current-products/current-product-stepper/current-product-accounting-step/current-product-accounting-step.component.html b/src/app/products/current-products/current-product-stepper/current-product-accounting-step/current-product-accounting-step.component.html new file mode 100644 index 000000000..b22ff2e6e --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-accounting-step/current-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/current-products/current-product-stepper/current-product-accounting-step/current-product-accounting-step.component.scss b/src/app/products/current-products/current-product-stepper/current-product-accounting-step/current-product-accounting-step.component.scss new file mode 100644 index 000000000..30125e801 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-accounting-step/current-product-accounting-step.component.scss @@ -0,0 +1,314 @@ +/** + * 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-500: #6b7280; +$gray-700: #334155; +$gray-900: #202124; +$primary: #1353a0; + +.spa-section { + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; + background: #fff; +} + +.spa-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; + } +} + +.spa-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; +} + +.spa-section__body { + padding: 16px 24px 20px; +} + +.spa-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; + } +} + +.spa-add-btn { + border: 0; + background: transparent; + color: #2563eb; + font-size: 12px; + font-weight: 700; + padding: 0; + cursor: pointer; +} + +.spa-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.spa-list + .spa-list { + margin-top: 12px; +} + +.spa-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 10px 12px; + border: 1px solid #e2e8f0; + border-radius: 8px; + background: #fff; + + @media (width <= 900px) { + flex-direction: column; + align-items: stretch; + } +} + +.spa-row:has(mifosx-gl-account-selector) { + align-items: stretch; +} + +.spa-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; + } +} + +.spa-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; + } +} + +.spa-row__meta { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + flex: 1 1 auto; +} + +.spa-row__meta--other { + min-width: 0; + flex: 1 1 auto; + align-self: stretch; +} + +.spa-row__label { + font-size: 14px; + font-weight: 600; + color: $gray-700; +} + +.spa-row__input { + flex: 0 0 22rem; + max-width: 22rem; + min-width: 18rem; + + @media (width <= 900px) { + width: 100%; + max-width: 100%; + min-width: 0; + } +} + +.spa-row__input--other { + flex: 1 1 auto; + max-width: 100%; + min-width: 0; +} + +.spa-row__input :is(mifosx-gl-account-selector) { + width: 100%; + display: block; +} + +.spa-row__input :is(mifosx-gl-account-selector) :is(.layout-column) { + width: 100%; + margin-right: 0 !important; +} + +.spa-row__input :is(mifosx-gl-account-selector) :is(mat-form-field) { + width: 100%; +} + +.spa-row__actions { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.spa-row__actions--other { + justify-content: flex-end; + align-self: stretch; + width: 100%; + + @media (width <= 1100px) { + justify-content: flex-start; + } +} + +.spa-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%; + } +} + +.spa-row__name--code { + max-width: 20rem; +} + +.spa-row__name--other-account { + max-width: 100%; + flex: 1 1 auto; + height: 48px; +} + +.spa-row__other-field { + width: 100%; + min-width: 0; +} + +.spa-row__other-field :is(mat-form-field) { + width: 100%; +} + +.spa-row__other-field :is(.mat-mdc-form-field-required-marker) { + display: none; +} + +.spa-remove-btn { + border: 0; + background: transparent; + color: #ef4444; + font-size: 18px; + line-height: 1; + cursor: pointer; + padding: 0 4px; +} + +.spa-remove-btn--side { + place-self: center end; + margin-right: 4px; + + @media (width <= 1100px) { + justify-self: start; + margin-right: 0; + } +} + +.spa-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; + } +} + +datalist { + display: none; +} + +@media (width <= 900px) { + .spa-row__meta { + margin-bottom: 4px; + } + + .spa-row__input { + flex: 1 1 auto; + max-width: 100%; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-accounting-step/current-product-accounting-step.component.ts b/src/app/products/current-products/current-product-stepper/current-product-accounting-step/current-product-accounting-step.component.ts new file mode 100644 index 000000000..252629bc5 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-accounting-step/current-product-accounting-step.component.ts @@ -0,0 +1,495 @@ +/** + * 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-current-product-accounting-step', + templateUrl: './current-product-accounting-step.component.html', + styleUrls: ['./current-product-accounting-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FormsModule, + ReactiveFormsModule, + MatRadioGroup, + MatRadioButton, + GlAccountSelectorComponent + ] +}) +export class CurrentProductAccountingStepComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private accounting = inject(Accounting); + + @Input() CurrentProductsTemplate: any; + @Input() accountingRuleData: any; + @Input() isDormancyTrackingActive: any; + @Input() CurrentProductFormValid: boolean; + @Input() allowOverdraft: any; + + CurrentProductAccountingForm: UntypedFormGroup; + liabilityAccountOptions: GLAccount[] = []; + expenseAccountOptions: GLAccount[] = []; + incomeAccountOptions: GLAccount[] = []; + assetAccountOptions: GLAccount[] = []; + accountingSectionConfigs: AccountingSectionConfig[] = []; + + constructor() { + this.createCurrentProductAccountingForm(); + this.setConditionalControls(); + } + + ngOnInit() { + const accountingMappings = this.CurrentProductsTemplate?.accountingMappings || {}; + // otherGlMappingsMap is at the root of the template, not inside accountingMappings + const mappings = { + ...accountingMappings, + otherGlMappingsMap: this.CurrentProductsTemplate?.otherGlMappingsMap + }; + const accountingMappingOptions = this.CurrentProductsTemplate?.accountingMappingOptions || {}; + const accountingRuleId = 2; // Always Cash + this.assetAccountOptions = accountingMappingOptions.assetAccountOptions || []; + this.liabilityAccountOptions = accountingMappingOptions.liabilityAccountOptions || []; + this.expenseAccountOptions = accountingMappingOptions.expenseAccountOptions || []; + this.incomeAccountOptions = accountingMappingOptions.incomeAccountOptions || []; + this.buildAccountingSectionConfigs(); + this.CurrentProductAccountingForm.patchValue({ + accountingRule: accountingRuleId + }); + + this.syncAccountingControls(accountingRuleId); + this.patchAccountingValues(mappings); + this.setOverdraftControl(); + this.setDormancyControl(); + } + + createCurrentProductAccountingForm() { + this.CurrentProductAccountingForm = this.formBuilder.group({ + accountingRule: [2], + assetsOtherGlMappings: this.formBuilder.array([]), + liabilitiesOtherGlMappings: this.formBuilder.array([]), + expensesOtherGlMappings: this.formBuilder.array([]), + incomeOtherGlMappings: this.formBuilder.array([]) + }); + } + + private setConditionalControls() { + this.CurrentProductAccountingForm.get('accountingRule')?.valueChanges.subscribe((accountingRule: number) => { + this.syncAccountingControls(accountingRule); + }); + } + + private patchAccountingValues(mappings: any) { + this.CurrentProductAccountingForm.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 || '', + incomeFromInterestId: mappings.incomeFromInterest?.id || '', + overdraftPortfolioControlId: mappings.overdraftPortfolioControl?.id || '', + writeOffAccountId: mappings.writeOffAccount?.id || '', + feesReceivableAccountId: mappings.feeReceivableAccount?.id || '', + penaltiesReceivableAccountId: mappings.penaltyReceivableAccount?.id || '', + interestPayableAccountId: mappings.interestPayableAccount?.id || '', + interestReceivableAccountId: mappings.interestReceivableAccount?.id || '', + escheatLiabilityId: mappings.escheatLiabilityAccount?.id || '' + }); + + if (this.isCashOrAccrualAccounting()) { + // Backend serializes as 'otherGlMappingsMap'; fall back to 'otherGlMappings' for compatibility + 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.CurrentProductAccountingForm.get(controlName) as UntypedFormArray; + } + + private setOtherGlMappings(controlName: string, otherGlMappings: any[], options: GLAccount[]) { + const otherGlMappingsArray = this.getOtherGlMappingsArray(controlName); + otherGlMappingsArray.clear(); + (otherGlMappings || []).forEach((otherGlMapping) => { + otherGlMappingsArray.push( + this.createOtherGlMappingGroup({ + accountName: otherGlMapping.accountName, + glAccountId: + otherGlMapping.glAccountId || options.find((option) => option.glCode === otherGlMapping.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((controlName) => this.getOtherGlMappingsArray(controlName).clear()); + } + + private setOverdraftControl() { + this.allowOverdraft?.valueChanges?.subscribe(() => { + this.syncAccountingControls(this.CurrentProductAccountingForm.value.accountingRule); + }); + } + + private setDormancyControl() { + this.isDormancyTrackingActive?.valueChanges?.subscribe(() => { + this.syncAccountingControls(this.CurrentProductAccountingForm.value.accountingRule); + }); + } + + private getOtherGlMappings() { + const mapOtherGlMappings = (controlName: string, options: GLAccount[]) => + this.getOtherGlMappingsArray(controlName) + .value.filter((item: OtherGlMappingValue) => !!item.accountName || !!item.glAccountId) + .map((item: OtherGlMappingValue) => ({ + accountName: item.accountName, + glCode: options.find((option) => option.id === item.glAccountId)?.glCode || '' + })) + .filter((item: { accountName: string; glCode: string }) => !!item.accountName || !!item.glCode); + + const otherGlMappings = { + assets: mapOtherGlMappings('assetsOtherGlMappings', this.assetAccountOptions), + liabilities: mapOtherGlMappings('liabilitiesOtherGlMappings', this.liabilityAccountOptions), + expenses: mapOtherGlMappings('expensesOtherGlMappings', this.expenseAccountOptions), + income: mapOtherGlMappings('incomeOtherGlMappings', this.incomeAccountOptions) + }; + + return Object.values(otherGlMappings).some((value) => value.length) ? otherGlMappings : null; + } + + isDormancyTrackingEnabled(): boolean { + return this.isDormancyTrackingActive?.value ?? this.isDormancyTrackingActive ?? false; + } + + private syncAccountingControls(accountingRule: number) { + const isDormancyActive = this.isDormancyTrackingEnabled(); + + const addControl = (controlName: string, required = false) => { + if (!this.CurrentProductAccountingForm.contains(controlName)) { + this.CurrentProductAccountingForm.addControl( + controlName, + new UntypedFormControl('', required ? Validators.required : undefined) + ); + } else if (required) { + this.CurrentProductAccountingForm.get(controlName)?.setValidators(Validators.required); + this.CurrentProductAccountingForm.get(controlName)?.updateValueAndValidity({ emitEvent: false }); + } + }; + + const removeControl = (controlName: string) => { + if (this.CurrentProductAccountingForm.contains(controlName)) { + this.CurrentProductAccountingForm.removeControl(controlName); + } + }; + + if (this.accounting.isCashOrAccrualAccountingRuleId(accountingRule)) { + addControl('savingsReferenceAccountId', true); + addControl('savingsControlAccountId', true); + addControl('transfersInSuspenseAccountId', true); + addControl('interestOnSavingsAccountId', true); + addControl('incomeFromFeeAccountId', true); + addControl('incomeFromPenaltyAccountId', true); + addControl('incomeFromInterestId', true); + addControl('writeOffAccountId', true); + addControl('overdraftPortfolioControlId', false); + + if (this.accounting.isAccrualAccountingRuleId(accountingRule)) { + addControl('feesReceivableAccountId', true); + addControl('penaltiesReceivableAccountId', true); + addControl('interestPayableAccountId', true); + addControl('interestReceivableAccountId', false); + } else { + removeControl('feesReceivableAccountId'); + removeControl('penaltiesReceivableAccountId'); + removeControl('interestPayableAccountId'); + removeControl('interestReceivableAccountId'); + } + + if (isDormancyActive) { + addControl('escheatLiabilityId', false); + } else { + removeControl('escheatLiabilityId'); + } + } else { + removeControl('savingsReferenceAccountId'); + removeControl('savingsControlAccountId'); + removeControl('transfersInSuspenseAccountId'); + removeControl('interestOnSavingsAccountId'); + removeControl('incomeFromFeeAccountId'); + removeControl('incomeFromPenaltyAccountId'); + removeControl('incomeFromInterestId'); + removeControl('writeOffAccountId'); + removeControl('overdraftPortfolioControlId'); + removeControl('feesReceivableAccountId'); + removeControl('penaltiesReceivableAccountId'); + removeControl('interestPayableAccountId'); + removeControl('interestReceivableAccountId'); + removeControl('escheatLiabilityId'); + this.clearOtherGlMappings(); + } + } + + get CurrentProductAccounting() { + const { + assetsOtherGlMappings, + liabilitiesOtherGlMappings, + expensesOtherGlMappings, + incomeOtherGlMappings, + ...CurrentProductAccounting + } = this.CurrentProductAccountingForm.value; + const otherGlMappings = this.getOtherGlMappings(); + + return { + ...CurrentProductAccounting, + ...(otherGlMappings ? { otherGlMappings } : {}) + }; + } + + isCashOrAccrualAccounting(): boolean { + return this.accounting.isCashOrAccrualAccountingRuleId(this.CurrentProductAccountingForm.value.accountingRule); + } + + isAccrualAccounting(): boolean { + return this.accounting.isAccrualAccountingRuleId(this.CurrentProductAccountingForm.value.accountingRule); + } + + 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: 'overdraftPortfolioControlId', + label: 'Overdraft Portfolio Control', + inputLabel: 'Overdraft portfolio control', + controlName: 'overdraftPortfolioControlId', + options: this.assetAccountOptions + }, + { + key: 'interestReceivableAccountId', + label: 'Interest Receivable Account', + inputLabel: 'Interest receivable account', + controlName: 'interestReceivableAccountId', + options: this.assetAccountOptions, + accrualOnly: 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 + }, + { + key: 'writeOffAccountId', + label: 'Write Off Account', + inputLabel: 'Write off account', + controlName: 'writeOffAccountId', + 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 + }, + { + key: 'incomeFromInterestId', + label: 'Income From Interest', + inputLabel: 'Income from interest', + controlName: 'incomeFromInterestId', + options: this.incomeAccountOptions, + required: true + } + ], + otherMappingsControlName: 'incomeOtherGlMappings', + otherAccountOptions: this.incomeAccountOptions + } + ]; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-charges-step/current-product-charges-step.component.html b/src/app/products/current-products/current-product-stepper/current-product-charges-step/current-product-charges-step.component.html new file mode 100644 index 000000000..c72d119fe --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-charges-step/current-product-charges-step.component.html @@ -0,0 +1,93 @@ + + +
+
+ +

{{ 'labels.inputs.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 | translateKey: 'catalogs' }} + {{ 'labels.inputs.Amount' | translate }} + {{ charge.amount | formatNumber }} + {{ 'labels.inputs.Collected On' | translate }} + {{ charge.chargeTimeType.value | translateKey: 'catalogs' }} + {{ 'labels.inputs.Actions' | translate }} + +
+
+ } + + @if (chargesDataSource.length === 0) { +
+ + {{ 'labels.text.No charges added yet' | translate }} +
+ } +
+
diff --git a/src/app/products/current-products/current-product-stepper/current-product-charges-step/current-product-charges-step.component.scss b/src/app/products/current-products/current-product-stepper/current-product-charges-step/current-product-charges-step.component.scss new file mode 100644 index 000000000..f272b71f6 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-charges-step/current-product-charges-step.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: #1353a0; +$accent: #0ea5e9; +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-500: #9aa0a6; +$gray-900: #202124; + +.spch-section { + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; + background: #fff; + + &__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; + } + } +} + +.spch-body { + padding: 16px 24px; +} + +.spch-add-row { + display: flex; + align-items: flex-start; + gap: 12px; + + &__select { + flex: 1; + max-width: 400px; + } + + &__btn { + margin-top: 4px; + height: 36px !important; + border-radius: 8px !important; + font-size: 13px !important; + font-weight: 600 !important; + box-shadow: 0 1px 3px rgba($primary, 0.3) !important; + } + + @media (width <= 600px) { + flex-direction: column; + + &__select { + max-width: 100%; + width: 100%; + } + } +} + +.spch-table-wrap { + margin-top: 8px; + border: 1px solid $border; + border-radius: 8px; + overflow: hidden; +} + +.spch-table { + width: 100%; +} + +.spch-empty { + display: flex; + align-items: center; + gap: 8px; + padding: 16px; + margin-top: 8px; + background: $gray-50; + border-radius: 8px; + color: $gray-500; + font-size: 13px; + + i { + font-size: 14px; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-charges-step/current-product-charges-step.component.ts b/src/app/products/current-products/current-product-stepper/current-product-charges-step/current-product-charges-step.component.ts new file mode 100644 index 000000000..d6cafa12d --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-charges-step/current-product-charges-step.component.ts @@ -0,0 +1,108 @@ +/** + * 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 { FormatNumberPipe } from '../../../../pipes/format-number.pipe'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-current-product-charges-step', + templateUrl: './current-product-charges-step.component.html', + styleUrls: ['./current-product-charges-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatIconButton, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, + ChargesFilterPipe, + FormatNumberPipe + ] +}) +export class CurrentProductChargesStepComponent implements OnInit { + dialog = inject(MatDialog); + private translateService = inject(TranslateService); + + @Input() CurrentProductsTemplate: any; + @Input() currencyCode: UntypedFormControl; + + chargeData: any; + + chargesDataSource: {}[]; + displayedColumns: string[] = [ + 'name', + 'chargeCalculationType', + 'amount', + 'chargeTimeType', + 'action' + ]; + + pristine = true; + + ngOnInit() { + this.chargeData = this.CurrentProductsTemplate.chargeOptions; + + this.chargesDataSource = this.CurrentProductsTemplate.charges || []; + this.pristine = true; + + this.currencyCode.valueChanges.subscribe(() => (this.chargesDataSource = [])); + } + + addCharge(charge: any) { + this.chargesDataSource = this.chargesDataSource.concat([charge.value]); + charge.value = ''; + this.pristine = false; + } + + 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([]); + this.pristine = false; + } + }); + } + + get CurrentProductCharges() { + return { + charges: this.chargesDataSource + }; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-currency-step/current-product-currency-step.component.html b/src/app/products/current-products/current-product-stepper/current-product-currency-step/current-product-currency-step.component.html new file mode 100644 index 000000000..1fb165c2d --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-currency-step/current-product-currency-step.component.html @@ -0,0 +1,111 @@ + + +
+
+
+ + 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 + +
+ +
+
+

Balance Swap Option

+

Enable automated transfer of excess funds to linked deposits or vice-versa to maintain liquidity.

+
+ + yes + no + +
+ +
+
+

MSME

+

Micro, Small and Medium Enterprises

+
+ + 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/current-products/current-product-stepper/current-product-currency-step/current-product-currency-step.component.scss b/src/app/products/current-products/current-product-stepper/current-product-currency-step/current-product-currency-step.component.scss new file mode 100644 index 000000000..a287d691e --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-currency-step/current-product-currency-step.component.scss @@ -0,0 +1,114 @@ +/** + * 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; + +.spc-section { + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; + 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; + } + } +} + +.spc-section__badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: #eff6ff; + font-size: 14px; +} + +.spc-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px 16px; + padding: 16px 24px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.spc-body { + background: #fff; +} + +.spc-field { + width: 100%; +} + +.spc-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/current-products/current-product-stepper/current-product-currency-step/current-product-currency-step.component.ts b/src/app/products/current-products/current-product-stepper/current-product-currency-step/current-product-currency-step.component.ts new file mode 100644 index 000000000..37c7b4c56 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-currency-step/current-product-currency-step.component.ts @@ -0,0 +1,94 @@ +/** + * 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 { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-current-product-currency-step', + templateUrl: './current-product-currency-step.component.html', + styleUrls: ['./current-product-currency-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatTooltip, + MatRadioGroup, + MatRadioButton, + FaIconComponent + ] +}) +export class CurrentProductCurrencyStepComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + + @Input() CurrentProductsTemplate: any; + + CurrentProductCurrencyForm: UntypedFormGroup; + + currencyData: any; + decimalPlacesData = [ + 2, + 0, + 3 + ]; + + constructor() { + this.createCurrentProductCurrencyForm(); + } + + ngOnInit() { + this.currencyData = this.CurrentProductsTemplate.currencyOptions; + + this.CurrentProductCurrencyForm.patchValue({ + currencyCode: this.CurrentProductsTemplate.currency?.code || this.currencyData?.[0]?.code || '', + digitsAfterDecimal: + this.CurrentProductsTemplate.digitsAfterDecimal ?? this.CurrentProductsTemplate.currency?.decimalPlaces ?? 2, + dormancyAlert: this.CurrentProductsTemplate.dormancyAlert ?? false, + balanceSwapOption: this.CurrentProductsTemplate.balanceSwapOption ?? false, + msme: this.CurrentProductsTemplate.msme ?? false, + nominationFacility: this.CurrentProductsTemplate.nominationFacility ?? false, + autoDebitFacility: this.CurrentProductsTemplate.autoDebitFacility ?? false + }); + } + + createCurrentProductCurrencyForm() { + this.CurrentProductCurrencyForm = this.formBuilder.group({ + currencyCode: [ + '', + Validators.required + ], + digitsAfterDecimal: [ + 2, + [ + Validators.required, + Validators.min(0) + ] + ], + dormancyAlert: [false], + balanceSwapOption: [false], + msme: [false], + nominationFacility: [false], + autoDebitFacility: [false] + }); + } + + get CurrentProductCurrency() { + const formValue = this.CurrentProductCurrencyForm.value; + return { + currencyCode: formValue.currencyCode, + digitsAfterDecimal: formValue.digitsAfterDecimal, + dormancyAlert: formValue.dormancyAlert, + balanceSwapOption: formValue.balanceSwapOption, + msme: formValue.msme, + nominationFacility: formValue.nominationFacility, + autoDebitFacility: formValue.autoDebitFacility + }; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.html b/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.html new file mode 100644 index 000000000..7b619793d --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.html @@ -0,0 +1,117 @@ + + +
+
+
+ +

{{ 'labels.inputs.Product Name' | translate }} & {{ 'labels.inputs.Description' | translate }}

+
+
+ + {{ 'labels.inputs.Product Name' | translate }} + + + {{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + + {{ 'labels.inputs.Short Name' | translate }} + + + {{ 'labels.inputs.Short Name' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + + {{ 'labels.inputs.Description' | translate }} + + +
+
+ +
+
+ +

Product Classification

+
+ +
+ + Account Type + + @for (accountType of accountTypeData; track accountType) { + {{ accountType }} + } + + Account Type is required + + + + Customer Segment + + @for (customerSegment of customerSegmentData; track customerSegment) { + {{ customerSegment }} + } + + + + + Product Code + + + + + Effective To + + + + + + + End Date + + + + + + + Introducer Required + + @for (option of yesNoOptions; track option.value) { + {{ option.label }} + } + + + +
+ Share Linking Enabled +
+
+
+
diff --git a/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.scss b/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.scss new file mode 100644 index 000000000..7b5c920ef --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.scss @@ -0,0 +1,70 @@ +/** + * 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; + +.spd-section { + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; + background: #fff; + + &__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; + } + } +} + +.spd-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px 16px; + padding: 16px 24px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.spd-grid--classification { + grid-template-columns: repeat(3, 1fr); +} + +.spd-field { + width: 100%; + + &--full { + grid-column: 1 / -1; + } +} + +.spd-checkbox-row { + display: flex; + align-items: center; + min-height: 56px; +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.ts b/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.ts new file mode 100644 index 000000000..1aba2fbac --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.ts @@ -0,0 +1,190 @@ +/** + * 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 } 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 { FaIconComponent } from '@fortawesome/angular-fontawesome'; +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-current-product-details-step', + templateUrl: './current-product-details-step.component.html', + styleUrls: ['./current-product-details-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatTooltip, + CdkTextareaAutosize, + FaIconComponent + ] +}) +export class CurrentProductDetailsStepComponent implements OnInit, OnChanges { + private formBuilder = inject(UntypedFormBuilder); + private dateUtils = inject(Dates); + private settingsService = inject(SettingsService); + + @Input() CurrentProductsTemplate: any; + + CurrentProductDetailsForm: UntypedFormGroup; + accountTypeData: string[] = []; + customerSegmentData: string[] = []; + yesNoOptions: Array<{ + value: boolean; + label: string; + }> = [ + { + value: true, + label: 'Yes' + }, + { + value: false, + label: 'No' + } + ]; + + constructor() { + this.createCurrentProductDetailsForm(); + } + + ngOnInit() { + if (this.CurrentProductsTemplate) { + this.applyTemplate(this.CurrentProductsTemplate); + } + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.CurrentProductsTemplate?.currentValue) { + this.applyTemplate(changes.CurrentProductsTemplate.currentValue); + } + } + + createCurrentProductDetailsForm() { + this.CurrentProductDetailsForm = this.formBuilder.group({ + name: [ + '', + Validators.required + ], + shortName: [ + '', + Validators.required + ], + description: [''], + accountType: [ + null, + Validators.required + ], + customerSegment: [null], + productCode: [''], + effectiveTo: [''], + endDate: [''], + introducerRequired: [false], + shareLinkingEnabled: [false] + }); + } + + 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; + + // Handle array format [year, month, day] + if (Array.isArray(dateValue)) { + return new Date(dateValue[0], dateValue[1] - 1, dateValue[2]); + } + + // Handle Date object + if (dateValue instanceof Date) { + return dateValue; + } + + // Handle string format + return new Date(dateValue); + } + + private applyTemplate(template: any) { + const accountTypeOptions = this.getOptionNames(template.accountTypeOptions); + const customerSegmentOptions = this.getOptionNames(template.customerSegmentOptions); + const yesNoOptions = this.getYesNoOptions(template.yesNoOptions); + + this.accountTypeData = accountTypeOptions; + this.customerSegmentData = customerSegmentOptions; + if (yesNoOptions.length) { + this.yesNoOptions = yesNoOptions; + } + + // Use existing product data if available (when editing), otherwise use template defaults + const isEditing = !!template.id; + this.CurrentProductDetailsForm.patchValue({ + name: isEditing ? template.name : template.name || '', + shortName: isEditing ? template.shortName : template.shortName || '', + description: template.description, + accountType: template.accountType ?? this.accountTypeData[0] ?? null, + customerSegment: template.customerSegment ?? this.customerSegmentData[0] ?? null, + productCode: template.productCode ?? '', + effectiveTo: this.parseDate(template.effectiveTo), + endDate: this.parseDate(template.endDate), + introducerRequired: template.introducerRequired === true || template.introducerRequired === 'Yes', + shareLinkingEnabled: !!template.shareLinkingEnabled + }); + } + + private getYesNoOptions(options: any[]): Array<{ value: boolean; label: string }> { + if (!Array.isArray(options)) { + return []; + } + + return options + .map((option) => { + const rawValue = option?.name ?? option?.value ?? option; + const normalized = typeof rawValue === 'string' ? rawValue.trim().toUpperCase() : rawValue; + + if (normalized === 'YES') { + return { + value: true, + label: 'Yes' + }; + } + + if (normalized === 'NO') { + return { + value: false, + label: 'No' + }; + } + + return null; + }) + .filter((value) => !!value); + } + + get CurrentProductDetails() { + const formValue = { ...this.CurrentProductDetailsForm.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) || ''; + } + + return formValue; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-limits-step/current-product-limits-step.component.html b/src/app/products/current-products/current-product-stepper/current-product-limits-step/current-product-limits-step.component.html new file mode 100644 index 000000000..0a78185a6 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-limits-step/current-product-limits-step.component.html @@ -0,0 +1,351 @@ + +
+
+ + + +
Transaction & Product Limits
+
R&D BASED DEFAULTS
+
+ +
+
+
Account Features (as per RBI guidelines)
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + @for (row of accountFacilityLimits; track row.limitType) { + + + + + + + } + +
Limit TypeGeneral Limit (Daily)Minor Account LimitSmall Saving Limit
{{ row.limitType }} + + + + + +
+
+ +
+

Deposit Limit (KYC)

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Account TypeKYC TypeLimit
General + + + +
Minor + + + +
Small Saving + + + +
+
+ +
+ Note: + Small Saving Account limits are strictly capped at Rs.1,00,000 maximum balance and Rs.10,000 monthly withdrawal + per RBI Master Circular. Minor accounts are restricted from RTGS and high-risk digital products (BEPG/BBPS) by + default. +
+
+
diff --git a/src/app/products/current-products/current-product-stepper/current-product-limits-step/current-product-limits-step.component.scss b/src/app/products/current-products/current-product-stepper/current-product-limits-step/current-product-limits-step.component.scss new file mode 100644 index 000000000..f0f7cad32 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-limits-step/current-product-limits-step.component.scss @@ -0,0 +1,257 @@ +/** + * 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: #d9e2ec; +$border-strong: #c7d2de; +$text: #24364b; +$muted: #64748b; +$header-bg: #f8fafc; +$highlight-bg: #eff6ff; + +.spl-section { + border: 1px solid $border; + border-radius: 12px; + overflow: hidden; + background: #fff; +} + +.spl-section__header { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + background: $header-bg; + border-bottom: 1px solid $border; +} + +.spl-section__icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 8px; + background: #eff6ff; + color: #2563eb; + font-size: 14px; + flex: 0 0 auto; +} + +.spl-section__title { + flex: 1 1 auto; + font-size: 16px; + font-weight: 600; + color: $text; +} + +.spl-section__badge { + flex: 0 0 auto; + padding: 5px 10px; + border-radius: 999px; + background: #dbeafe; + color: #1d4ed8; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.05em; + white-space: nowrap; +} + +.spl-section__body { + padding: 18px 20px 20px; +} + +.spl-subsection { + border: 1px solid $border; + border-radius: 12px; + overflow: hidden; + background: #fff; + + & + & { + margin-top: 16px; + } +} + +.spl-subsection__header { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; + background: $header-bg; + border-bottom: 1px solid $border; + color: $text; + font-size: 14px; + font-weight: 600; +} + +.spl-subsection__icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + border-radius: 8px; + background: #eff6ff; + color: #2563eb; + font-size: 13px; + flex: 0 0 auto; +} + +.spl-subsection__body { + padding: 16px 16px 18px; +} + +.spl-table-wrap { + overflow-x: auto; +} + +.spl-table { + width: 100%; + min-width: 0; + table-layout: fixed; + border-collapse: collapse; + border: 1px solid $border; + + col.spl-table__col--label { + width: 22%; + } + + col.spl-table__col--value { + width: 26%; + } + + th, + td { + border: 1px solid $border; + padding: 6px 8px; + vertical-align: middle; + } + + th { + background: #f8fafc; + color: #64748b; + text-align: center; + font-size: 11px; + font-weight: 700; + line-height: 1.3; + white-space: normal; + word-break: break-word; + } + + td:first-child { + background: #f8fafc; + font-weight: 600; + color: $text; + white-space: nowrap; + } + + td { + font-size: 13px; + overflow: hidden; + } +} + +.spl-table__highlight td:first-child { + color: #1d4ed8; +} + +.spl-input, +.spl-select { + width: 100%; + box-sizing: border-box; + font: inherit; +} + +.spl-input { + height: 30px; + border: 1px solid $border-strong; + border-radius: 2px; + padding: 0 8px; + background: #fff; +} + +.spl-input:disabled, +.spl-select:disabled { + background: #f8fafc; + color: $muted; +} + +.spl-select { + height: 30px; + border: 1px solid $border-strong; + border-radius: 2px; + padding: 0 6px; + background: #fff; +} + +.spl-input--stacked { + margin-top: 6px; +} + +.spl-input--disabled { + background: #f8fafc; + color: $muted; +} + +.spl-table__muted { + font-size: 11px; + color: #94a3b8; + letter-spacing: 0.03em; +} + +.spl-note { + margin-top: 16px; + padding: 12px 14px; + border: 1px solid $border; + border-radius: 8px; + background: #f8fafc; + color: $muted; + font-size: 11px; + line-height: 1.6; +} + +.spl-features { + margin-bottom: 16px; +} + +.spl-features__grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px 16px; + + @media (width <= 900px) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + @media (width <= 640px) { + grid-template-columns: 1fr; + } +} + +.spl-feature { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 13px; + color: $text; +} + +.spl-feature__label { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 18px; + font-weight: 600; + color: #334155; + + i { + color: #2563eb; + font-size: 12px; + width: 14px; + text-align: center; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-limits-step/current-product-limits-step.component.ts b/src/app/products/current-products/current-product-stepper/current-product-limits-step/current-product-limits-step.component.ts new file mode 100644 index 000000000..a210e2ae2 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-limits-step/current-product-limits-step.component.ts @@ -0,0 +1,250 @@ +/** + * 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, OnChanges, SimpleChanges } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +type BooleanFeatureKey = + | 'atmFacility' + | 'atmWithdrawalFacility' + | 'upiFacility' + | 'posTransactionsFacility' + | 'chequeBook' + | 'impsFacility' + | 'smsAlerts' + | 'bbpsFacility' + | 'insuranceCover' + | 'bepgFacility' + | 'neftFacility' + | 'rtgsFacility' + | 'interBankTotalCapFacility' + | 'withdrawalFacility' + | 'depositFacilityKyc' + | 'virtualDebitCard'; + +interface FacilityLimitRow { + limitType: string; + generalLimit: string | number | null; + minorAccountLimit: string | number | null; + smallSavingLimit: string | number | null; +} + +@Component({ + selector: 'mifosx-current-product-limits-step', + templateUrl: './current-product-limits-step.component.html', + styleUrls: ['./current-product-limits-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FormsModule + ] +}) +export class CurrentProductLimitsStepComponent implements OnChanges { + @Input() CurrentProductsTemplate: any; + pristine = true; + + yesNoOptions: Array<{ + value: boolean; + label: string; + }> = [ + { + value: true, + label: 'Yes' + }, + { + value: false, + label: 'No' + } + ]; + + accountFacilityConfig: Record = { + atmFacility: true, + atmWithdrawalFacility: true, + upiFacility: true, + posTransactionsFacility: true, + chequeBook: true, + impsFacility: true, + smsAlerts: true, + bbpsFacility: true, + insuranceCover: false, + bepgFacility: true, + neftFacility: true, + rtgsFacility: true, + interBankTotalCapFacility: true, + withdrawalFacility: true, + depositFacilityKyc: true, + virtualDebitCard: false + }; + + accountFacilityLimits: FacilityLimitRow[] = [ + { + limitType: 'ATM Withdrawal', + generalLimit: 40000, + minorAccountLimit: 5000, + smallSavingLimit: 10000 + }, + { + limitType: 'POS Transactions', + generalLimit: 100000, + minorAccountLimit: 2000, + smallSavingLimit: 10000 + }, + { + limitType: 'UPI Limit', + generalLimit: 50000, + minorAccountLimit: 1000, + smallSavingLimit: 5000 + }, + { + limitType: 'IMPS Limit', + generalLimit: 50000, + minorAccountLimit: 100, + smallSavingLimit: 5000 + }, + { + limitType: 'BBPS', + generalLimit: 25000, + minorAccountLimit: 0, + smallSavingLimit: 2000 + }, + { + limitType: 'BEPG', + generalLimit: 25000, + minorAccountLimit: 0, + smallSavingLimit: 2000 + }, + { + limitType: 'NEFT Limit', + generalLimit: 1000000, + minorAccountLimit: 25000, + smallSavingLimit: 10000 + }, + { + limitType: 'RTGS Limit (Min 2L)', + generalLimit: 5000000, + minorAccountLimit: 'N/A', + smallSavingLimit: 'N/A' + }, + { + limitType: 'Inter-Bank Total Cap', + generalLimit: 5000000, + minorAccountLimit: 25000, + smallSavingLimit: 10000 + }, + { + limitType: 'Withdrawal Limit', + generalLimit: 'UNLIMITED', + minorAccountLimit: 10000, + smallSavingLimit: 10000 + } + ]; + + depositLimitKyc = { + general: { + kycType: 'Full KYC', + limit: 'No Limit' + }, + minor: { + kycType: 'Guardian KYC', + limit: 100000 + }, + smallSaving: { + kycType: 'Min KYC', + limit: 50000 + } + }; + + ngOnChanges(changes: SimpleChanges) { + if (changes.CurrentProductsTemplate?.currentValue) { + const template = changes.CurrentProductsTemplate.currentValue; + this.setYesNoOptions(template); + this.populateFromTemplate(template); + } + } + + markAsDirty() { + this.pristine = false; + } + + private populateFromTemplate(template: any) { + const isEditing = !!template.id; + + // Populate accountFacilityConfig from existing product + if (isEditing && template.accountFacilityConfig) { + this.accountFacilityConfig = { ...this.accountFacilityConfig, ...template.accountFacilityConfig }; + } + + // Populate accountFacilityLimits from existing product + if (isEditing && template.accountFacilityLimits && template.accountFacilityLimits.length > 0) { + this.accountFacilityLimits = template.accountFacilityLimits.map((limit: any) => ({ + limitType: limit.limitType || '', + generalLimit: limit.generalLimit ?? null, + minorAccountLimit: limit.minorAccountLimit ?? null, + smallSavingLimit: limit.smallSavingLimit ?? null + })); + } + + // Populate depositLimitKyc from existing product + if (isEditing && template.depositLimitKyc) { + this.depositLimitKyc = { + general: { + kycType: template.depositLimitKyc.general?.kycType || 'Full KYC', + limit: template.depositLimitKyc.general?.limit || 'No Limit' + }, + minor: { + kycType: template.depositLimitKyc.minor?.kycType || 'Guardian KYC', + limit: template.depositLimitKyc.minor?.limit ?? 100000 + }, + smallSaving: { + kycType: template.depositLimitKyc.smallSaving?.kycType || 'Min KYC', + limit: template.depositLimitKyc.smallSaving?.limit ?? 50000 + } + }; + } + } + + private setYesNoOptions(template: any) { + const options = Array.isArray(template?.yesNoOptions) ? template.yesNoOptions : []; + const mappedOptions = options + .map((option: any) => { + const rawValue = option?.name ?? option?.value ?? option; + const normalized = typeof rawValue === 'string' ? rawValue.trim().toUpperCase() : rawValue; + + if (normalized === 'YES') { + return { + value: true, + label: 'Yes' + }; + } + + if (normalized === 'NO') { + return { + value: false, + label: 'No' + }; + } + + return null; + }) + .filter((value: any) => !!value); + + if (mappedOptions.length) { + this.yesNoOptions = mappedOptions; + } + } + + get CurrentProductLimits() { + return { + accountFacilityConfig: { ...this.accountFacilityConfig }, + accountFacilityLimits: this.accountFacilityLimits.map((row) => ({ ...row })), + depositLimitKyc: { + ...this.depositLimitKyc + } + }; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.html b/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.html new file mode 100644 index 000000000..7e179aef5 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.html @@ -0,0 +1,799 @@ + + +
+

{{ CurrentProduct.name }}

+ + +

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

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

Product Classification

+ + + + @if (CurrentProduct.accountType) { +
+ Account Type: + {{ CurrentProduct.accountType }} +
+ } + + @if (CurrentProduct.customerSegment) { +
+ Customer Segment: + {{ CurrentProduct.customerSegment }} +
+ } + + @if (CurrentProduct.productCode) { +
+ Product Code: + {{ CurrentProduct.productCode }} +
+ } + + @if (CurrentProduct.effectiveTo) { +
+ Effective To: + {{ CurrentProduct.effectiveTo }} +
+ } + + @if (CurrentProduct.endDate) { +
+ End Date: + {{ CurrentProduct.endDate }} +
+ } + + @if (CurrentProduct.introducerRequired !== undefined) { +
+ Introducer Required: + {{ + CurrentProduct.introducerRequired === true || CurrentProduct.introducerRequired === 'Yes' ? 'Yes' : 'No' + }} +
+ } + + @if (CurrentProduct.shareLinkingEnabled !== undefined) { +
+ Share Linking Enabled: + {{ CurrentProduct.shareLinkingEnabled | yesNo }} +
+ } + } + +

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

+ + +
+ {{ 'labels.inputs.Currency' | translate }}: + {{ + CurrentProduct.currency?.name || + CurrentProduct.currency?.displayLabel || + (CurrentProduct.currencyCode | find: CurrentProductsTemplate?.currencyOptions : 'code' : 'name') || + CurrentProduct.currencyCode || + '-' + }} +
+ +
+ {{ 'labels.inputs.Decimal Places' | translate }}: + {{ CurrentProduct.digitsAfterDecimal ?? CurrentProduct.currency?.decimalPlaces ?? 2 }} +
+ + @if (CurrentProduct.currency?.inMultiplesOf !== undefined && CurrentProduct.currency?.inMultiplesOf !== null) { +
+ {{ 'labels.inputs.Currency in multiples of' | translate }}: + {{ CurrentProduct.currency.inMultiplesOf }} +
+ } + + @if (CurrentProduct.dormancyAlert !== undefined) { +
+ Dormancy Alert: + {{ CurrentProduct.dormancyAlert | yesNo }} +
+ } + + @if (CurrentProduct.balanceSwapOption !== undefined) { +
+ Balance Swap Option: + {{ CurrentProduct.balanceSwapOption | yesNo }} +
+ } + + @if (CurrentProduct.msme !== undefined) { +
+ MSME: + {{ CurrentProduct.msme | yesNo }} +
+ } + + @if (CurrentProduct.nominationFacility !== undefined) { +
+ Nomination Facility: + {{ CurrentProduct.nominationFacility | yesNo }} +
+ } + + @if (CurrentProduct.autoDebitFacility !== undefined) { +
+ Auto Debit Facility: + {{ CurrentProduct.autoDebitFacility | yesNo }} +
+ } + +

Interest On OD

+ + +
+ Interest Posting Frequency: + {{ + CurrentProduct.interestPostingPeriodType + | find: CurrentProductsTemplate?.interestPostingPeriodTypeOptions : 'id' : 'value' + | translateKey: 'catalogs' + }} +
+ +
+ Auto Interest Post: + {{ CurrentProduct.autoPost | yesNo }} +
+ + @if ( + CurrentProduct.frequencyFixDay !== undefined && + CurrentProduct.frequencyFixDay !== null && + CurrentProduct.frequencyFixDay !== '' + ) { +
+ Frequency (Fix Day): + {{ CurrentProduct.frequencyFixDay }} Days +
+ } + + @if (CurrentProduct.lastInterestDate) { +
+ Last Interest Date: + {{ CurrentProduct.lastInterestDate }} +
+ } + +

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

+ + + @if (CurrentProduct.minRequiredOpeningBalance) { +
+ {{ 'labels.inputs.Minimum Opening Balance' | translate }}: + {{ CurrentProduct.minRequiredOpeningBalance | formatNumber }} +
+ } + + @if (CurrentProduct.lockinPeriodFrequency) { +
+ {{ 'labels.inputs.Lock-in Period' | translate }}: + {{ + CurrentProduct.lockinPeriodFrequency + + ' ' + + (CurrentProduct.lockinPeriodFrequencyType + | find: CurrentProductsTemplate?.lockinPeriodFrequencyTypeOptions : 'id' : 'value') + | translateKey: 'catalogs' + }} +
+ } + +
+ {{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}: + {{ CurrentProduct.withdrawalFeeForTransfers | yesNo }} +
+ + @if (CurrentProduct.balanceFrequency) { +
+ Balance Frequency: + {{ CurrentProduct.balanceFrequency }} +
+ } + +
+ Enable Lock-in Period: + {{ CurrentProduct.enableLockinPeriod | yesNo }} +
+ + @if (CurrentProduct.enableLockinPeriod && CurrentProduct.lockinPeriodFrequency) { +
+ Lock-in Period: + + {{ CurrentProduct.lockinPeriodFrequency }} + {{ getLockinPeriodFrequencyTypeLabel(CurrentProduct.lockinPeriodFrequencyType) }} + +
+ } + + @if (CurrentProduct.minBalanceForInterestCalculation) { +
+ {{ 'labels.inputs.Balance Required for Interest Calculation' | translate }}: + {{ CurrentProduct.minBalanceForInterestCalculation | formatNumber }} +
+ } + +
+ {{ 'labels.inputs.Enforce Minimum Balance' | translate }}: + {{ CurrentProduct.enforceMinRequiredBalance | yesNo }} +
+ + @if (CurrentProduct.minRequiredBalance) { +
+ {{ 'labels.inputs.Minimum Balance' | translate }}: + {{ CurrentProduct.minRequiredBalance | formatNumber }} +
+ } + +
+ {{ 'labels.inputs.Withhold Tax is Applicable' | translate }}: + {{ CurrentProduct.withHoldTax | yesNo }} +
+ + @if (CurrentProduct.withHoldTax) { +
+ {{ 'labels.inputs.Tax Group' | translate }}: + {{ + CurrentProduct.taxGroupId | find: CurrentProductsTemplate?.taxGroupOptions : 'id' : 'name' + }} +
+ } + +
+ Account Closed with Zero Balance in Day Closing: + {{ CurrentProduct.accountClosedZeroBalanceInDayClosing | yesNo }} +
+ +

Overdraft (OD) Facility

+ + +
+ Is Overdraft Allowed: + {{ CurrentProduct.allowOverdraft | yesNo }} +
+ + @if (CurrentProduct.allowOverdraft) { + @if (CurrentProduct.avgBalanceCalcPeriod) { +
+ Avg Balance Calculation Period: + {{ CurrentProduct.avgBalanceCalcPeriod }} +
+ } + + @if (CurrentProduct.validForPeriod) { +
+ Valid for Period: + {{ CurrentProduct.validForPeriod }} +
+ } + + @if (CurrentProduct.overdraftLimit !== undefined && CurrentProduct.overdraftLimit !== '') { +
+ OD Limit %: + {{ CurrentProduct.overdraftLimit }} +
+ } + + @if ( + CurrentProduct.nominalAnnualInterestRateOverdraft !== undefined && + CurrentProduct.nominalAnnualInterestRateOverdraft !== '' + ) { +
+ Interest on OD %: + {{ CurrentProduct.nominalAnnualInterestRateOverdraft }} +
+ } + +
+ OD Account Closed with Zero Balance in Day Closing: + {{ CurrentProduct.odAccountClosedZeroBalanceInDayClosing | yesNo }} +
+ } + +
+ {{ 'labels.inputs.Enable Dormancy Tracking' | translate }}: + {{ CurrentProduct.isDormancyTrackingActive | yesNo }} +
+ + @if (CurrentProduct.isDormancyTrackingActive) { +
+ {{ 'labels.inputs.Number of Days to Inactive sub-status' | translate }}: + {{ CurrentProduct.daysToInactive }} + {{ 'labels.inputs.Number of Days to Dormant sub-status' | translate }}: + {{ CurrentProduct.daysToDormancy }} + {{ 'labels.inputs.Number of Days to Escheat' | translate }}: + {{ CurrentProduct.daysToEscheat }} +
+ } + +

Account Facilities

+ + + @if (CurrentProduct.accountFacilityConfig) { +
+ @for (feature of accountFacilityFeatureItems; track feature.key) { +
+ {{ feature.label }}: + {{ CurrentProduct.accountFacilityConfig[feature.key] | yesNo }} +
+ } +
+ } + + @if (CurrentProduct.accountFacilityLimits?.length) { +
+

Account Facility Limits

+ + + + + + + + + + + + + + + + + + + +
Limit Type{{ row.limitType }}General Limit{{ row.generalLimit }}Minor Account Limit{{ row.minorAccountLimit }}Small Saving Limit{{ row.smallSavingLimit }}
+
+ } + + @if (CurrentProduct.depositLimitKyc) { +
+

Deposit Limit (KYC)

+
+ General: + {{ CurrentProduct.depositLimitKyc.general.kycType }} - + {{ CurrentProduct.depositLimitKyc.general.limit }} +
+
+ Minor: + {{ CurrentProduct.depositLimitKyc.minor.kycType }} - {{ CurrentProduct.depositLimitKyc.minor.limit }} +
+
+ Small Saving: + + {{ CurrentProduct.depositLimitKyc.smallSaving.kycType }} - + {{ CurrentProduct.depositLimitKyc.smallSaving.limit }} + +
+
+ } + + @if (CurrentProduct.charges.length) { +
+

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

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

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

+ + + @if (isCashOrAccrualAccounting()) { +
+
+

GL Mapping

+
+ +
+
Assets
+
+ Savings reference account + {{ + formatGlAccountById( + CurrentProduct.savingsReferenceAccountId, + CurrentProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }} +
+
+ Overdraft portfolio control + {{ + formatGlAccountById( + CurrentProduct.overdraftPortfolioControlId, + CurrentProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }} +
+
+ Interest receivable account + {{ + formatGlAccountById( + CurrentProduct.interestReceivableAccountId, + CurrentProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }} +
+
+ Fees receivable account + {{ + formatGlAccountById( + CurrentProduct.feesReceivableAccountId, + CurrentProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }} +
+
+ Penalties receivable account + {{ + formatGlAccountById( + CurrentProduct.penaltiesReceivableAccountId, + CurrentProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }} +
+
+ +
+
Liabilities
+
+ Savings control account + {{ + formatGlAccountById( + CurrentProduct.savingsControlAccountId, + CurrentProductsTemplate?.accountingMappingOptions.liabilityAccountOptions + ) + }} +
+
+ Transfers in suspense account + {{ + formatGlAccountById( + CurrentProduct.transfersInSuspenseAccountId, + CurrentProductsTemplate?.accountingMappingOptions.liabilityAccountOptions + ) + }} +
+
+ Interest payable account + {{ + formatGlAccountById( + CurrentProduct.interestPayableAccountId, + CurrentProductsTemplate?.accountingMappingOptions.liabilityAccountOptions + ) + }} +
+ @if (CurrentProduct.isDormancyTrackingActive) { +
+ Escheat liability account + {{ + formatGlAccountById( + CurrentProduct.escheatLiabilityId, + CurrentProductsTemplate?.accountingMappingOptions.liabilityAccountOptions + ) + }} +
+ } +
+ +
+
Expenses
+
+ Interest on savings account + {{ + formatGlAccountById( + CurrentProduct.interestOnSavingsAccountId, + CurrentProductsTemplate?.accountingMappingOptions.expenseAccountOptions + ) + }} +
+
+ Write off account + {{ + formatGlAccountById( + CurrentProduct.writeOffAccountId, + CurrentProductsTemplate?.accountingMappingOptions.expenseAccountOptions + ) + }} +
+
+ +
+
Income
+
+ Income from fees + {{ + formatGlAccountById( + CurrentProduct.incomeFromFeeAccountId, + CurrentProductsTemplate?.accountingMappingOptions.incomeAccountOptions + ) + }} +
+
+ Income from penalties + {{ + formatGlAccountById( + CurrentProduct.incomeFromPenaltyAccountId, + CurrentProductsTemplate?.accountingMappingOptions.incomeAccountOptions + ) + }} +
+
+ Income from interest + {{ + formatGlAccountById( + CurrentProduct.incomeFromInterestId, + CurrentProductsTemplate?.accountingMappingOptions.incomeAccountOptions + ) + }} +
+
+
+ } + @if (CurrentProduct.otherGlMappings) { +
+
+

Other GL Mappings

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

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

+ + @if (CurrentProduct.paymentChannelToFundSourceMappings?.length) { +
+

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

+ + + + + + + + + + + +
{{ 'labels.inputs.Payment Type' | translate }} + {{ paymentFundSource.paymentTypeId | find: CurrentProductsTemplate.paymentTypeOptions : 'id' : 'name' }} + {{ 'labels.inputs.Fund Source' | translate }} + {{ + paymentFundSource.fundSourceAccountId + | find: CurrentProductsTemplate?.accountingMappingOptions.assetAccountOptions : 'id' : 'name' + }} +
+
+ } + @if (CurrentProduct.feeToIncomeAccountMappings?.length) { +
+

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

+ + + + + + + + + + + +
{{ 'labels.inputs.Fees' | translate }} + {{ feesIncome.chargeId | find: CurrentProductsTemplate.chargeOptions : 'id' : 'name' }} + {{ 'labels.inputs.Income Account' | translate }} + {{ + feesIncome.incomeAccountId + | find: CurrentProductsTemplate?.accountingMappingOptions.incomeAccountOptions : 'id' : 'name' + }} +
+
+ } + @if (CurrentProduct.penaltyToIncomeAccountMappings?.length) { +
+

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

+ + + + + + + + + + + +
{{ 'labels.inputs.Penalty' | translate }} + {{ penaltyIncome.chargeId | find: CurrentProductsTemplate.penaltyOptions : 'id' : 'name' }} + {{ 'labels.inputs.Income Account' | translate }} + {{ + penaltyIncome.incomeAccountId + | find: CurrentProductsTemplate?.accountingMappingOptions.incomeAccountOptions : 'id' : 'name' + }} +
+
+ } +
+ } +
diff --git a/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.scss b/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.scss new file mode 100644 index 000000000..f69c8fea0 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.scss @@ -0,0 +1,165 @@ +/** + * 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/helper'; +@use 'assets/styles/colours' as *; + +.tab-container { + padding: 1%; + margin: 1%; +} + +.mat-h3 { + margin-top: 10px !important; + font-weight: 300; + color: $status-approved; +} + +.mat-elevation-z1 { + margin: 1em 0 1.5em; +} + +.interestCarts { + float: right; +} + +h2, +h3, +h4 { + margin: 0; + font-weight: 500; +} + +table { + width: 100%; + + tr.incentives-row { + height: 0; + + > td { + padding: 0; + } + + .incentives { + overflow: hidden; + } + + .mat-card { + border-radius: 0; + + > h4 { + font-weight: 500; + margin: 0.5em 0 0; + } + } + } +} + +span { + margin: 0.5em 0; +} + +mat-divider { + margin: 0 0 0.5em; +} + +.margin-t { + margin-top: 1em; +} + +.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; +} + +.spa-gl-section 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%; +} + +.spa-other-gl-table:first-of-type { + margin-top: 0.25rem; +} + +.spa-other-gl-summary .mat-elevation-z1 { + border-radius: 10px; + overflow: hidden; +} + +.spa-other-gl-table th.mat-mdc-header-cell, +.spa-other-gl-table td.mat-mdc-cell { + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +.spa-other-gl-table th.mat-mdc-header-cell:first-child, +.spa-other-gl-table td.mat-mdc-cell:first-child { + width: 38%; +} + +.spa-other-gl-table th.mat-mdc-header-cell:last-child, +.spa-other-gl-table td.mat-mdc-cell:last-child { + width: 62%; + word-break: break-word; +} + +@media (width <= 768px) { + .spa-gl-row { + grid-template-columns: 1fr; + gap: 0.35rem; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.ts b/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.ts new file mode 100644 index 000000000..5ca6b51dc --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.ts @@ -0,0 +1,247 @@ +/** + * 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 { FormatNumberPipe } from '../../../../pipes/format-number.pipe'; +import { YesnoPipe } from '../../../../pipes/yesno.pipe'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-current-product-preview-step', + templateUrl: './current-product-preview-step.component.html', + styleUrls: ['./current-product-preview-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatDivider, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, + FaIconComponent, + FindPipe, + FormatNumberPipe, + YesnoPipe + ] +}) +export class CurrentProductPreviewStepComponent implements OnInit, OnChanges { + private accounting = inject(Accounting); + + @Input() CurrentProductsTemplate: any; + @Input() accountingRuleData: any; + @Input() CurrentProduct: any; + @Input() taskPermission: string; + @Output() submitEvent = new EventEmitter(); + + chargesDisplayedColumns: string[] = [ + 'name', + 'chargeCalculationType', + 'amount', + 'chargeTimeType' + ]; + paymentFundSourceDisplayedColumns: string[] = [ + 'paymentTypeId', + 'fundSourceAccountId' + ]; + feesPenaltyIncomeDisplayedColumns: string[] = [ + 'chargeId', + 'incomeAccountId' + ]; + + accountingMappings: any = {}; + accountingRule: OptionData; + interestCalculationTypeOptionData: any[] = []; + accountFacilityFeatureItems = [ + { key: 'atmFacility', label: 'ATM Facility' }, + { key: 'atmWithdrawalFacility', label: 'ATM Withdrawal Facility' }, + { key: 'upiFacility', label: 'UPI Facility' }, + { key: 'posTransactionsFacility', label: 'POS Transactions Facility' }, + { key: 'chequeBook', label: 'Cheque Book' }, + { key: 'impsFacility', label: 'IMPS Facility' }, + { key: 'smsAlerts', label: 'SMS Alerts' }, + { key: 'bbpsFacility', label: 'BBPS Facility' }, + { key: 'insuranceCover', label: 'Insurance Cover' }, + { key: 'bepgFacility', label: 'BEPG Facility' }, + { key: 'neftFacility', label: 'NEFT Facility' }, + { key: 'rtgsFacility', label: 'RTGS Facility' }, + { key: 'interBankTotalCapFacility', label: 'Inter-Bank Total Cap Facility' }, + { key: 'withdrawalFacility', label: 'Withdrawal Facility' }, + { key: 'depositFacilityKyc', label: 'Deposit Facility (KYC)' }, + { key: 'virtualDebitCard', label: 'Virtual Debit Card' } + ]; + + ngOnInit() { + this.setCurrentValues(); + } + + ngOnChanges(changes: SimpleChanges): void { + this.setCurrentValues(); + } + + setCurrentValues(): void { + if (!this.CurrentProduct || !this.CurrentProductsTemplate) { + return; + } + + this.interestCalculationTypeOptionData = + this.CurrentProductsTemplate.interestCalculationTypeOption || + this.CurrentProductsTemplate.interestCalculationTypeOptions || + []; + this.accountingRule = this.accounting.getAccountingRuleFrom(this.CurrentProduct.accountingRule); + + if (!this.isCashOrAccrualAccounting()) { + return; + } + + const assetAccountData = this.CurrentProductsTemplate?.accountingMappingOptions?.assetAccountOptions || []; + const incomeAccountData = this.CurrentProductsTemplate?.accountingMappingOptions?.incomeAccountOptions || []; + const expenseAccountData = this.CurrentProductsTemplate?.accountingMappingOptions?.expenseAccountOptions || []; + const liabilityAccountData = this.CurrentProductsTemplate?.accountingMappingOptions?.liabilityAccountOptions || []; + + this.accountingMappings = { + savingsReferenceAccount: this.accounting.glAccountLookUp( + this.CurrentProduct.savingsReferenceAccountId, + assetAccountData + ), + overdraftPortfolioControl: this.accounting.glAccountLookUp( + this.CurrentProduct.overdraftPortfolioControlId, + assetAccountData + ), + savingsControlAccount: this.accounting.glAccountLookUp( + this.CurrentProduct.savingsControlAccountId, + liabilityAccountData + ), + transfersInSuspenseAccount: this.accounting.glAccountLookUp( + this.CurrentProduct.transfersInSuspenseAccountId, + liabilityAccountData + ), + interestOnSavingsAccount: this.accounting.glAccountLookUp( + this.CurrentProduct.interestOnSavingsAccountId, + expenseAccountData + ), + writeOffAccount: this.accounting.glAccountLookUp(this.CurrentProduct.writeOffAccountId, expenseAccountData), + incomeFromFeeAccount: this.accounting.glAccountLookUp( + this.CurrentProduct.incomeFromFeeAccountId, + incomeAccountData + ), + incomeFromPenaltyAccount: this.accounting.glAccountLookUp( + this.CurrentProduct.incomeFromPenaltyAccountId, + incomeAccountData + ), + incomeFromInterest: this.accounting.glAccountLookUp(this.CurrentProduct.incomeFromInterestId, incomeAccountData) + }; + + if (this.isAccrualAccounting()) { + this.accountingMappings = { + ...this.accountingMappings, + feeReceivableAccount: this.accounting.glAccountLookUp( + this.CurrentProduct.feesReceivableAccountId, + assetAccountData + ), + penaltyReceivableAccount: this.accounting.glAccountLookUp( + this.CurrentProduct.penaltiesReceivableAccountId, + assetAccountData + ), + interestPayableAccount: this.accounting.glAccountLookUp( + this.CurrentProduct.interestPayableAccountId, + liabilityAccountData + ), + interestReceivableAccount: this.accounting.glAccountLookUp( + this.CurrentProduct.interestReceivableAccountId, + assetAccountData + ) + }; + } + + if (this.CurrentProduct.isDormancyTrackingActive) { + this.accountingMappings = { + ...this.accountingMappings, + escheatLiabilityAccount: this.accounting.glAccountLookUp( + this.CurrentProduct.escheatLiabilityId, + liabilityAccountData + ) + }; + } + } + + isCashOrAccrualAccounting(): boolean { + return this.accounting.isCashOrAccrualAccounting(this.accountingRule); + } + + isAccrualAccounting(): boolean { + return this.accounting.isAccrualAccounting(this.accountingRule); + } + + getBooleanLabel(value: any): string { + return value === true || value === 'Yes' ? 'Yes' : 'No'; + } + + getLockinPeriodFrequencyTypeLabel(value: any): string { + switch (value) { + case 'DAYS': + return 'Days'; + case 'WEEKS': + return 'Weeks'; + case 'MONTHS': + return 'Months'; + case 'YEARS': + return 'Years'; + default: + return value ?? ''; + } + } + + getOptionLabel(value: any, options: any[], key: string, property: string): string { + if (value === null || value === undefined || !Array.isArray(options)) { + return '-'; + } + + const match = options.find((option) => option?.[key] === value); + return match?.[property] || '-'; + } + + 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 '-'; + } + + const account = options.find((option) => option?.glCode === glCode); + return account ? `(${account.glCode}) ${account.name}` : glCode; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-settings-step/current-product-settings-step.component.html b/src/app/products/current-products/current-product-stepper/current-product-settings-step/current-product-settings-step.component.html new file mode 100644 index 000000000..f79d6f32e --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-settings-step/current-product-settings-step.component.html @@ -0,0 +1,189 @@ + + +
+ +
+
+ + Balance & Minimum Balance +
+
+
+
+ + Minimum Opening Balance + + + +
+ + Enforce Minimum Balance + +
+ + + Minimum Balance (₹) * + + PENAL CHARGE APPLIES ON BREACH + +
+ +
+ + Balance Frequency + + @for (balanceFrequency of periodFrequencyData; track balanceFrequency.id) { + {{ balanceFrequency.value }} + } + + + +
+ + Apply Withdrawal Fee for Transfers + +
+
+
+
+
+ + +
+
+ + Lock-in Period +
+
+
+ Enable Lock-in Period + +
+ + @if (CurrentProductSettingsForm.value.enableLockinPeriod) { +
+ + Frequency * + + + + + Type * + + @for (type of lockinPeriodFrequencyTypeData; track type) { + + {{ type.value | translateKey: 'catalogs' }} + + } + + +
+ } +
+
+ + +
+
+ + Additional Rules +
+
+
+ + Account Closed with Zero Balance in Day Closing + +
+
+ + Withhold Tax is Applicable + +
+
+
+ + +
+
+ + Overdraft (OD) Facility +
+
+
+ +
+
+ + Is Overdraft Allowed + +
+ @if (CurrentProductSettingsForm.value.allowOverdraft) { +
+ + OD Account Closed with Zero Balance in Day Closing + +

+ Automatically closes OD accounts when balance reaches zero during day-end closing. +

+
+ } +
+ + + @if (CurrentProductSettingsForm.value.allowOverdraft) { +
+
+ Average Balance Calculation in Period + + 6M + 1Y + 3Y + +
+ +
+ Valid for Period + + 3M + 1Y + 3Y + +
+ +
+ + OD Limit % + + + + + Interest on OD % + + +
+
+ } +
+
+
+
diff --git a/src/app/products/current-products/current-product-stepper/current-product-settings-step/current-product-settings-step.component.scss b/src/app/products/current-products/current-product-stepper/current-product-settings-step/current-product-settings-step.component.scss new file mode 100644 index 000000000..0e3cd524f --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-settings-step/current-product-settings-step.component.scss @@ -0,0 +1,263 @@ +/** + * 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; + +.sps-section { + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; + 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; + } + } +} + +.sps-section__badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: #eff6ff; + font-size: 14px; +} + +.sps-body { + background: #fff; + padding: 18px 24px 24px; +} + +.sps-field { + width: 100%; + + &--full { + grid-column: 1 / -1; + } + + &--compact { + width: 100%; + } +} + +.sps-layout { + display: grid; + gap: 16px; +} + +.sps-layout--balance { + gap: 22px; +} + +.sps-layout--lockin { + grid-template-columns: repeat(2, minmax(0, 1fr)); + + @media (width <= 640px) { + grid-template-columns: 1fr; + } +} + +.sps-layout--additional { + grid-template-columns: repeat(2, minmax(0, 1fr)); + + @media (width <= 1100px) { + grid-template-columns: 1fr; + } +} + +.sps-row { + display: grid; + gap: 18px; + align-items: start; +} + +.sps-row--three { + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + align-items: center; + + .sps-field--compact:first-child { + min-width: 0; + } + + .sps-toggle--inline { + align-self: center; + min-height: 56px; + } +} + +.sps-row--two { + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + + .sps-field--compact { + min-width: 0; + } +} + +.sps-toggle { + display: flex; + align-items: center; + min-height: 56px; + + mat-checkbox { + font-size: 14px; + color: $gray-900; + } + + &--full { + min-height: auto; + margin-bottom: 8px; + } + + &--inline { + padding-top: 0; + } + + &--right { + justify-content: flex-start; + } +} + +.sps-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; +} + +.sps-lockin-row mat-checkbox { + margin-right: 2px; +} + +.sps-lockin-fields { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + + @media (width <= 640px) { + grid-template-columns: 1fr; + } +} + +// ── Overdraft (OD) Facility ─────────────────────────────────────────────── + +.sps-od-layout { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; + align-items: start; + + @media (width <= 900px) { + grid-template-columns: 1fr; + } +} + +.sps-od-checks { + display: flex; + flex-direction: column; + gap: 12px; +} + +.sps-od-check-row { + padding: 14px 16px; + border: 1px solid $border; + border-radius: 8px; + background: $gray-50; + + mat-checkbox { + font-size: 14px; + font-weight: 500; + color: $gray-900; + } +} + +.sps-od-check-hint { + margin: 6px 0 0 28px; + font-size: 11.5px; + color: #64748b; + line-height: 1.5; +} + +.sps-od-panel { + background: #f0f7ff; + border: 1px solid #bfdbfe; + border-radius: 10px; + padding: 20px 24px; + display: flex; + flex-direction: column; + gap: 20px; +} + +.sps-od-radio-group { + display: flex; + flex-direction: column; + gap: 8px; + padding-bottom: 16px; + border-bottom: 1px solid #dbeafe; + + &:last-of-type { + border-bottom: none; + padding-bottom: 0; + } +} + +.sps-od-radio-label { + font-size: 10.5px; + font-weight: 700; + letter-spacing: 0.07em; + color: #3b82f6; + text-transform: uppercase; +} + +.sps-od-radio-choices { + display: flex; + gap: 20px; + flex-wrap: wrap; +} + +.sps-od-inputs { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + + @media (width <= 640px) { + grid-template-columns: 1fr; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-settings-step/current-product-settings-step.component.ts b/src/app/products/current-products/current-product-stepper/current-product-settings-step/current-product-settings-step.component.ts new file mode 100644 index 000000000..4c464aeae --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-settings-step/current-product-settings-step.component.ts @@ -0,0 +1,172 @@ +/** + * 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 { MatCheckbox } from '@angular/material/checkbox'; +import { MatRadioGroup, MatRadioButton } from '@angular/material/radio'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-current-product-settings-step', + templateUrl: './current-product-settings-step.component.html', + styleUrls: ['./current-product-settings-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatCheckbox, + MatRadioGroup, + MatRadioButton + ] +}) +export class CurrentProductSettingsStepComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + + @Input() CurrentProductsTemplate: any; + + CurrentProductSettingsForm: UntypedFormGroup; + + periodFrequencyData: any[] = []; + + lockinPeriodFrequencyTypeData = [ + { id: 'DAYS', value: 'value.days' }, + { id: 'WEEKS', value: 'value.weeks' }, + { id: 'MONTHS', value: 'value.months' }, + { id: 'YEARS', value: 'value.years' } + ]; + + constructor() { + this.createCurrentProductSettingsForm(); + this.setConditionalControls(); + } + + ngOnInit() { + this.periodFrequencyData = this.CurrentProductsTemplate.periodFrequencyOptions || []; + const defaultPeriod = this.periodFrequencyData[0]?.value || 'Monthly'; + const isEditing = !!this.CurrentProductsTemplate.id; + + this.CurrentProductSettingsForm.patchValue({ + minRequiredOpeningBalance: this.CurrentProductsTemplate.minRequiredOpeningBalance, + withdrawalFeeForTransfers: this.CurrentProductsTemplate.withdrawalFeeForTransfers, + enforceMinRequiredBalance: this.CurrentProductsTemplate.enforceMinRequiredBalance, + minRequiredBalance: this.CurrentProductsTemplate.minRequiredBalance, + withHoldTax: this.CurrentProductsTemplate.withHoldTax ?? false, + balanceFrequency: isEditing ? this.CurrentProductsTemplate.balanceFrequency || defaultPeriod : defaultPeriod, + enableLockinPeriod: !!( + this.CurrentProductsTemplate.lockinPeriodFrequency && this.CurrentProductsTemplate.lockinPeriodFrequency > 0 + ), + accountClosedZeroBalanceInDayClosing: isEditing + ? (this.CurrentProductsTemplate.accountClosedZeroBalanceInDayClosing ?? false) + : false, + isDormancyTrackingActive: isEditing ? (this.CurrentProductsTemplate.isDormancyTrackingActive ?? true) : true, + daysToInactive: isEditing ? this.CurrentProductsTemplate.daysToInactive || 30 : 30, + daysToDormancy: isEditing ? this.CurrentProductsTemplate.daysToDormancy || 60 : 60, + daysToEscheat: isEditing ? this.CurrentProductsTemplate.daysToEscheat || 90 : 90, + // Overdraft fields + allowOverdraft: isEditing ? (this.CurrentProductsTemplate.allowOverdraft ?? false) : false, + overdraftLimit: isEditing ? this.CurrentProductsTemplate.overdraftLimit : '', + nominalAnnualInterestRateOverdraft: isEditing + ? this.CurrentProductsTemplate.nominalAnnualInterestRateOverdraft + : '', + avgBalanceCalcPeriod: isEditing ? this.CurrentProductsTemplate.avgBalanceCalcPeriod || '1Y' : '1Y', + validForPeriod: isEditing ? this.CurrentProductsTemplate.validForPeriod || '1Y' : '1Y', + odAccountClosedZeroBalanceInDayClosing: isEditing + ? (this.CurrentProductsTemplate.odAccountClosedZeroBalanceInDayClosing ?? false) + : false + }); + + if ( + isEditing && + this.CurrentProductsTemplate.lockinPeriodFrequency && + this.CurrentProductsTemplate.lockinPeriodFrequency > 0 + ) { + const lockinTypeIdToKey: { [key: number]: string } = { 0: 'DAYS', 1: 'WEEKS', 2: 'MONTHS', 3: 'YEARS' }; + const rawLockinType = this.CurrentProductsTemplate.lockinPeriodFrequencyType; + // Backend may return an EnumOptionData {id, code, value} or a plain integer + const lockinTypeId = typeof rawLockinType === 'object' ? rawLockinType?.id : rawLockinType; + this.CurrentProductSettingsForm.patchValue({ + lockinPeriodFrequency: this.CurrentProductsTemplate.lockinPeriodFrequency, + lockinPeriodFrequencyType: lockinTypeIdToKey[lockinTypeId] || '' + }); + } + } + + createCurrentProductSettingsForm() { + this.CurrentProductSettingsForm = this.formBuilder.group({ + minRequiredOpeningBalance: [ + '', + [Validators.min(0)] + ], + withdrawalFeeForTransfers: [false], + enforceMinRequiredBalance: [false], + minRequiredBalance: [ + '', + [Validators.min(0)] + ], + withHoldTax: [false], + balanceFrequency: ['Monthly'], + enableLockinPeriod: [false], + lockinPeriodFrequency: [''], + lockinPeriodFrequencyType: [''], + accountClosedZeroBalanceInDayClosing: [false], + // Dormancy (used internally / by accounting step) + isDormancyTrackingActive: [true], + daysToInactive: [30], + daysToDormancy: [60], + daysToEscheat: [90], + // Overdraft (OD) Facility + allowOverdraft: [false], + overdraftLimit: [ + '', + [Validators.min(0)] + ], + nominalAnnualInterestRateOverdraft: [ + '', + [Validators.min(0)] + ], + avgBalanceCalcPeriod: ['1Y'], + validForPeriod: ['1Y'], + odAccountClosedZeroBalanceInDayClosing: [false] + }); + } + + setConditionalControls() { + this.CurrentProductSettingsForm.get('enableLockinPeriod').valueChanges.subscribe((enabled: boolean) => { + const freq = this.CurrentProductSettingsForm.get('lockinPeriodFrequency'); + const type = this.CurrentProductSettingsForm.get('lockinPeriodFrequencyType'); + if (enabled) { + freq.setValidators([ + Validators.required, + Validators.min(1), + Validators.pattern('^[1-9]\\d*$') + ]); + type.setValidators([Validators.required]); + } else { + freq.setValue(''); + type.setValue(''); + freq.clearValidators(); + type.clearValidators(); + } + freq.updateValueAndValidity(); + type.updateValueAndValidity(); + }); + } + + get CurrentProductSettings() { + const formValue = { ...this.CurrentProductSettingsForm.value }; + + if (!formValue.allowOverdraft) { + delete formValue.overdraftLimit; + delete formValue.nominalAnnualInterestRateOverdraft; + delete formValue.avgBalanceCalcPeriod; + delete formValue.validForPeriod; + delete formValue.odAccountClosedZeroBalanceInDayClosing; + } + + return formValue; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-terms-step/current-product-terms-step.component.html b/src/app/products/current-products/current-product-stepper/current-product-terms-step/current-product-terms-step.component.html new file mode 100644 index 000000000..f2350d95a --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-terms-step/current-product-terms-step.component.html @@ -0,0 +1,68 @@ + + +
+
+
+ + Interest On OD +
+
+ + Interest Posting Frequency * + + @for (item of interestPostingPeriodTypeData; track item) { + + {{ item.value | translateKey: 'catalogs' }} + + } + + + Interest Posting Frequency {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + +
+
+
diff --git a/src/app/products/current-products/current-product-stepper/current-product-terms-step/current-product-terms-step.component.scss b/src/app/products/current-products/current-product-stepper/current-product-terms-step/current-product-terms-step.component.scss new file mode 100644 index 000000000..807262b37 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-terms-step/current-product-terms-step.component.scss @@ -0,0 +1,148 @@ +/** + * 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; + +.spt-section { + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; + 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; + } + } +} + +.spt-section__badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: #eff6ff; + font-size: 14px; +} + +.spt-body { + background: #fff; + padding: 16px 24px; +} + +.spt-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px 16px; + padding: 16px 24px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.spt-grid--three { + grid-template-columns: repeat(3, 1fr); +} + +.spt-field { + width: 100%; + + &--full { + grid-column: 1 / -1; + } +} + +.spt-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; + } +} + +.spt-footer { + padding: 0 24px 24px; + border-top: 1px solid #f1f5f9; +} + +.spt-footer__grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + padding-top: 18px; + + @media (width <= 900px) { + grid-template-columns: 1fr; + } +} + +.spt-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; + } +} + +.spt-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/current-products/current-product-stepper/current-product-terms-step/current-product-terms-step.component.ts b/src/app/products/current-products/current-product-stepper/current-product-terms-step/current-product-terms-step.component.ts new file mode 100644 index 000000000..aaab2e0aa --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-terms-step/current-product-terms-step.component.ts @@ -0,0 +1,112 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +import { Component, OnInit, Input, inject } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +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-current-product-terms-step', + templateUrl: './current-product-terms-step.component.html', + styleUrls: ['./current-product-terms-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent + ] +}) +export class CurrentProductTermsStepComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private dateUtils = inject(Dates); + private settingsService = inject(SettingsService); + + @Input() CurrentProductsTemplate: any; + + CurrentProductTermsForm: UntypedFormGroup; + + interestPostingPeriodTypeData: any[] = []; + yesNoOptions: Array<{ value: boolean; label: string }> = [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' } + ]; + + constructor() { + this.createCurrentProductTermsForm(); + } + + ngOnInit() { + this.interestPostingPeriodTypeData = this.CurrentProductsTemplate.interestPostingPeriodTypeOptions || []; + this.setYesNoOptions(this.CurrentProductsTemplate?.yesNoOptions); + + const isEditing = !!this.CurrentProductsTemplate.id; + + this.CurrentProductTermsForm.patchValue({ + interestPostingPeriodType: this.CurrentProductsTemplate.interestPostingPeriodType?.id || '', + autoPost: isEditing ? (this.CurrentProductsTemplate.autoPost ?? true) : true, + frequencyFixDay: isEditing ? (this.CurrentProductsTemplate.frequencyFixDay ?? 30) : 30, + lastInterestDate: this.parseDate(this.CurrentProductsTemplate.lastInterestDate) + }); + } + + 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 mappedOptions = options + .map((option: any) => { + const rawValue = option?.name ?? option?.value ?? option; + const normalized = typeof rawValue === 'string' ? rawValue.trim().toUpperCase() : rawValue; + if (normalized === 'YES') return { value: true, label: 'Yes' }; + if (normalized === 'NO') return { value: false, label: 'No' }; + return null; + }) + .filter((v: any) => !!v); + + if (mappedOptions.length) { + this.yesNoOptions = mappedOptions; + } + } + + createCurrentProductTermsForm() { + this.CurrentProductTermsForm = this.formBuilder.group({ + interestPostingPeriodType: [ + '', + Validators.required + ], + autoPost: [true], + frequencyFixDay: [ + 30, + [Validators.min(0)] + ], + lastInterestDate: [''] + }); + } + + get CurrentProductTerms() { + const formValue = { ...this.CurrentProductTermsForm.value }; + const dateFormat = this.settingsService.dateFormat; + + if (formValue.lastInterestDate instanceof Date) { + formValue.lastInterestDate = this.dateUtils.formatDate(formValue.lastInterestDate, dateFormat) || ''; + } + + return { ...formValue }; + } +} diff --git a/src/app/products/current-products/edit-current-product/edit-current-product.component.html b/src/app/products/current-products/edit-current-product/edit-current-product.component.html index a8ab8c762..699f0bfce 100644 --- a/src/app/products/current-products/edit-current-product/edit-current-product.component.html +++ b/src/app/products/current-products/edit-current-product/edit-current-product.component.html @@ -1,69 +1,154 @@ -
- - - - - - + - - {{ 'labels.inputs.CURRENCY' | translate }} - - +
+
+ +

{{ 'labels.heading.Edit Current Product' | translate }}

+
- - {{ 'labels.inputs.TERMS' | translate }} - - + + + + + {{ 'labels.inputs.DETAILS' | translate }} + +
+ +
+
- - {{ 'labels.inputs.SETTINGS' | translate }} - - + + + + {{ 'labels.inputs.CURRENCY' | translate }} + +
+ +
+
- - {{ 'labels.inputs.CHARGES' | translate }} - - + + + + {{ 'labels.inputs.TERMS' | translate }} + +
+ +
+
- - {{ 'labels.inputs.ACCOUNTING' | translate }} - - + + + + {{ 'labels.inputs.SETTINGS' | translate }} + +
+ +
+
+ + + + + {{ 'labels.inputs.CHARGES' | translate }} + +
+ +
+
+ + + + + Digital + +
+ +
+
+ + + + + {{ 'labels.inputs.ACCOUNTING' | translate }} + +
+ +
+
@if (formValidAndNotPristine) { - - {{ 'labels.inputs.PREVIEW' | translate }} - - + + + + {{ 'labels.inputs.PREVIEW' | translate }} + +
+ +
+
} - +
+ +
+ +
+ @if (selectedTabIndex > 0) { + + } + @if (selectedTabIndex < totalTabs - 1) { + + } + @if (formValidAndNotPristine && selectedTabIndex === totalTabs - 1) { + + } +
+
diff --git a/src/app/products/current-products/edit-current-product/edit-current-product.component.scss b/src/app/products/current-products/edit-current-product/edit-current-product.component.scss index fbc6592ec..98f8a7bee 100644 --- a/src/app/products/current-products/edit-current-product/edit-current-product.component.scss +++ b/src/app/products/current-products/edit-current-product/edit-current-product.component.scss @@ -1,4 +1,90 @@ -fa-icon { - position: relative; - left: 5%; +/** + * 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; + +.cp-container { + max-width: 960px; + margin: 0 auto; + padding: 24px 16px; +} + +.cp-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; + } +} + +.cp-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: 120px; + padding: 0 16px; + 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; + } + } +} + +.cp-tab-icon { + margin-right: 8px; + font-size: 14px; +} + +.cp-tab-content { + padding: 24px 16px; +} + +.cp-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/current-products/edit-current-product/edit-current-product.component.ts b/src/app/products/current-products/edit-current-product/edit-current-product.component.ts index 0083121a2..5d6d2441b 100644 --- a/src/app/products/current-products/edit-current-product/edit-current-product.component.ts +++ b/src/app/products/current-products/edit-current-product/edit-current-product.component.ts @@ -1,17 +1,24 @@ +/** + * 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, ViewChild, inject } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { SavingProductDetailsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component'; -import { SavingProductCurrencyStepComponent } from '../../saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component'; -import { SavingProductTermsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component'; -import { SavingProductSettingsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component'; -import { SavingProductChargesStepComponent } from '../../saving-products/saving-product-stepper/saving-product-charges-step/saving-product-charges-step.component'; -import { SavingProductAccountingStepComponent } from '../../saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component'; -import { SavingProductPreviewStepComponent } from '../../saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component'; +import { CurrentProductDetailsStepComponent } from '../current-product-stepper/current-product-details-step/current-product-details-step.component'; +import { CurrentProductCurrencyStepComponent } from '../current-product-stepper/current-product-currency-step/current-product-currency-step.component'; +import { CurrentProductTermsStepComponent } from '../current-product-stepper/current-product-terms-step/current-product-terms-step.component'; +import { CurrentProductSettingsStepComponent } from '../current-product-stepper/current-product-settings-step/current-product-settings-step.component'; +import { CurrentProductChargesStepComponent } from '../current-product-stepper/current-product-charges-step/current-product-charges-step.component'; +import { CurrentProductLimitsStepComponent } from '../current-product-stepper/current-product-limits-step/current-product-limits-step.component'; +import { CurrentProductAccountingStepComponent } from '../current-product-stepper/current-product-accounting-step/current-product-accounting-step.component'; +import { CurrentProductPreviewStepComponent } from '../current-product-stepper/current-product-preview-step/current-product-preview-step.component'; import { ProductsService } from 'app/products/products.service'; import { SettingsService } from 'app/settings/settings.service'; import { Accounting } from 'app/core/utils/accounting'; -import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper'; -import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { MatTabsModule } from '@angular/material/tabs'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ @@ -20,18 +27,15 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; styleUrls: ['./edit-current-product.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, - MatStepper, - MatStepperIcon, - FaIconComponent, - MatStep, - MatStepLabel, - SavingProductDetailsStepComponent, - SavingProductCurrencyStepComponent, - SavingProductTermsStepComponent, - SavingProductSettingsStepComponent, - SavingProductChargesStepComponent, - SavingProductAccountingStepComponent, - SavingProductPreviewStepComponent + MatTabsModule, + CurrentProductDetailsStepComponent, + CurrentProductCurrencyStepComponent, + CurrentProductTermsStepComponent, + CurrentProductSettingsStepComponent, + CurrentProductChargesStepComponent, + CurrentProductLimitsStepComponent, + CurrentProductAccountingStepComponent, + CurrentProductPreviewStepComponent ] }) export class EditCurrentProductComponent { @@ -41,16 +45,18 @@ export class EditCurrentProductComponent { private settingsService = inject(SettingsService); private accounting = inject(Accounting); - @ViewChild(SavingProductDetailsStepComponent, { static: true }) detailsStep: SavingProductDetailsStepComponent; - @ViewChild(SavingProductCurrencyStepComponent, { static: true }) currencyStep: SavingProductCurrencyStepComponent; - @ViewChild(SavingProductTermsStepComponent, { static: true }) termsStep: SavingProductTermsStepComponent; - @ViewChild(SavingProductSettingsStepComponent, { static: true }) settingsStep: SavingProductSettingsStepComponent; - @ViewChild(SavingProductChargesStepComponent, { static: true }) chargesStep: SavingProductChargesStepComponent; - @ViewChild(SavingProductAccountingStepComponent, { static: true }) - accountingStep: SavingProductAccountingStepComponent; + @ViewChild(CurrentProductDetailsStepComponent, { static: true }) detailsStep: CurrentProductDetailsStepComponent; + @ViewChild(CurrentProductCurrencyStepComponent, { static: true }) currencyStep: CurrentProductCurrencyStepComponent; + @ViewChild(CurrentProductTermsStepComponent, { static: true }) termsStep: CurrentProductTermsStepComponent; + @ViewChild(CurrentProductSettingsStepComponent, { static: true }) settingsStep: CurrentProductSettingsStepComponent; + @ViewChild(CurrentProductChargesStepComponent, { static: true }) chargesStep: CurrentProductChargesStepComponent; + @ViewChild(CurrentProductLimitsStepComponent, { static: true }) limitsStep: CurrentProductLimitsStepComponent; + @ViewChild(CurrentProductAccountingStepComponent, { static: true }) + accountingStep: CurrentProductAccountingStepComponent; currentProductAndTemplate: any; accountingRuleData: string[] = []; + selectedTabIndex = 0; constructor() { this.route.data.subscribe((data: { currentProductAndTemplate: any }) => { @@ -60,19 +66,26 @@ export class EditCurrentProductComponent { } get detailsForm() { - return this.detailsStep.savingProductDetailsForm; + return this.detailsStep.CurrentProductDetailsForm; } get currencyForm() { - return this.currencyStep.savingProductCurrencyForm; + return this.currencyStep.CurrentProductCurrencyForm; } get termsForm() { - return this.termsStep.savingProductTermsForm; + return this.termsStep.CurrentProductTermsForm; } get settingsForm() { - return this.settingsStep.savingProductSettingsForm; + return this.settingsStep.CurrentProductSettingsForm; + } + get limitsForm(): void { + return; // Limits step doesn't have a form, it's data-driven } get accountingForm() { - return this.accountingStep.savingProductAccountingForm; + return this.accountingStep.CurrentProductAccountingForm; + } + + get totalTabs(): number { + return this.formValidAndNotPristine ? 8 : 7; } get formValidAndNotPristine() { @@ -87,36 +100,64 @@ export class EditCurrentProductComponent { !this.termsForm.pristine || !this.settingsForm.pristine || !this.chargesStep.pristine || + !this.limitsStep.pristine || !this.accountingForm.pristine) ); } get currentProduct() { return { - ...this.detailsStep.savingProductDetails, - ...this.currencyStep.savingProductCurrency, - ...this.termsStep.savingProductTerms, - ...this.settingsStep.savingProductSettings, - ...this.chargesStep.savingProductCharges, - ...this.accountingStep.savingProductAccounting + ...this.detailsStep.CurrentProductDetails, + ...this.currencyStep.CurrentProductCurrency, + ...this.termsStep.CurrentProductTerms, + ...this.settingsStep.CurrentProductSettings, + ...this.chargesStep.CurrentProductCharges, + ...this.limitsStep.CurrentProductLimits, + ...this.accountingStep.CurrentProductAccounting }; } - submit() { - const product = { + private prepareCurrentProductPayload() { + const currentProduct = { ...this.currentProduct, charges: this.currentProduct.charges.map((charge: any) => ({ id: charge.id })), - locale: this.settingsService.language.code + locale: this.settingsService.language.code, + dateFormat: 'dd MMMM yyyy' }; - delete product.advancedAccountingRules; - this.productsService.updateCurrentProduct(this.currentProductAndTemplate.id, product).subscribe((response: any) => { - this.router.navigate( - [ - '../../', - response.resourceId - ], - { relativeTo: this.route } - ); + delete currentProduct.advancedAccountingRules; + + if (currentProduct.lockinPeriodFrequencyType) { + const frequencyTypeMapping: { [key: string]: number } = { + DAYS: 0, + WEEKS: 1, + MONTHS: 2, + YEARS: 3 + }; + currentProduct.lockinPeriodFrequencyType = frequencyTypeMapping[currentProduct.lockinPeriodFrequencyType]; + } + + Object.keys(currentProduct).forEach((key) => { + if (currentProduct[key] === '' || currentProduct[key] === null || currentProduct[key] === undefined) { + delete currentProduct[key]; + } }); + + return currentProduct; + } + + submit() { + const currentProduct = this.prepareCurrentProductPayload(); + console.log('Current product update payload:', currentProduct); + this.productsService + .updateCurrentProduct(this.currentProductAndTemplate.id, currentProduct) + .subscribe((response: any) => { + this.router.navigate( + [ + '../../', + response.resourceId + ], + { relativeTo: this.route } + ); + }); } } diff --git a/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.html b/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.html index 18491561c..6dc6b1343 100644 --- a/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.html +++ b/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.html @@ -1,3 +1,11 @@ + +
-
-
-

{{ currentProduct.name }}

- +
+

{{ currentProduct.name }}

-

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

- + +

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

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

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

- - -
- {{ 'labels.inputs.Currency' | translate }}: - {{ currentProduct.currency.name }} -
- -
- {{ 'labels.inputs.Decimal Places' | translate }}: - {{ currentProduct.currency.decimalPlaces }} -
- -

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

- - -
- {{ 'labels.inputs.Nominal Annual Interest' | translate }}: - {{ currentProduct.nominalAnnualInterestRate | formatNumber }} % -
- -
- {{ 'labels.inputs.Interest Compounding Period' | translate }}: - {{ currentProduct.interestCompoundingPeriodType?.value | translateKey: 'catalogs' }} -
- -
- {{ 'labels.inputs.Interest Posting Period' | translate }}: - {{ currentProduct.interestPostingPeriodType?.value | translateKey: 'catalogs' }} -
- - @if (currentProduct.charges?.length) { -
-

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

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

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

- - - + +
+ {{ 'labels.inputs.Short Name' | translate }}: + {{ currentProduct.shortName }}
+ + @if (currentProduct.description) { +
+ {{ 'labels.inputs.Description' | translate }}: + {{ currentProduct.description }} +
+ } + + @if ( + currentProduct.accountType || + currentProduct.customerSegment || + currentProduct.productCode || + currentProduct.effectiveDate || + currentProduct.effectiveTo || + currentProduct.endDate || + currentProduct.introducerRequired !== undefined || + currentProduct.shareLinkingEnabled !== undefined + ) { +

Product Classification

+ + + + @if (currentProduct.accountType) { +
+ Account Type: + {{ currentProduct.accountType }} +
+ } + + @if (currentProduct.customerSegment) { +
+ Customer Segment: + {{ currentProduct.customerSegment }} +
+ } + + @if (currentProduct.productCode) { +
+ Product Code: + {{ currentProduct.productCode }} +
+ } + + @if (currentProduct.effectiveDate) { +
+ Effective Date: + {{ currentProduct.effectiveDate }} +
+ } + + @if (currentProduct.effectiveTo) { +
+ Effective To: + {{ currentProduct.effectiveTo }} +
+ } + + @if (currentProduct.endDate) { +
+ End Date: + {{ currentProduct.endDate }} +
+ } + + @if (currentProduct.introducerRequired !== undefined) { +
+ Introducer Required: + {{ + currentProduct.introducerRequired === true || currentProduct.introducerRequired === 'Yes' ? 'Yes' : 'No' + }} +
+ } + + @if (currentProduct.shareLinkingEnabled !== undefined) { +
+ Share Linking Enabled: + {{ currentProduct.shareLinkingEnabled | yesNo }} +
+ } + } + +

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

+ + + +
+ {{ 'labels.inputs.Currency' | translate }}: + {{ currentProduct.currency.name }} +
+ +
+ {{ 'labels.inputs.Decimal Places' | translate }}: + {{ currentProduct.currency.decimalPlaces }} +
+ + @if (currentProduct.currency.inMultiplesOf !== undefined && currentProduct.currency.inMultiplesOf !== null) { +
+ {{ 'labels.inputs.Currency in multiples of' | translate }}: + {{ currentProduct.currency.inMultiplesOf }} +
+ } + + @if (currentProduct.dormancyAlert !== undefined) { +
+ Dormancy Alert: + {{ currentProduct.dormancyAlert | yesNo }} +
+ } + + @if (currentProduct.nominationFacility !== undefined) { +
+ Nomination Facility: + {{ currentProduct.nominationFacility | yesNo }} +
+ } + + @if (currentProduct.balanceSwapOption !== undefined) { +
+ Balance Swap Option: + {{ currentProduct.balanceSwapOption | yesNo }} +
+ } + + @if (currentProduct.msme !== undefined) { +
+ MSME: + {{ currentProduct.msme | yesNo }} +
+ } + + @if (currentProduct.autoDebitFacility !== undefined) { +
+ Auto Debit Facility: + {{ currentProduct.autoDebitFacility | yesNo }} +
+ } + +

Interest On OD

+ + + + @if (currentProduct.interestPostingPeriodType?.value) { +
+ {{ 'labels.inputs.Interest Posting Period' | translate }}: + {{ currentProduct.interestPostingPeriodType.value | translateKey: 'catalogs' }} +
+ } + + @if (currentProduct.interestCalculateSelector) { +
+ Interest Calculate: + {{ currentProduct.interestCalculateSelector }} +
+ } + + @if (currentProduct.averagePeriod) { +
+ Average Period: + {{ currentProduct.averagePeriod }} +
+ } + + @if (currentProduct.autoPost !== undefined) { +
+ Auto Post: + {{ currentProduct.autoPost | yesNo }} +
+ } + + @if (currentProduct.frequencyFixDay) { +
+ Frequency Fix Day: + {{ currentProduct.frequencyFixDay }} +
+ } + + @if (currentProduct.lastInterestDate) { +
+ Last Interest Date: + {{ currentProduct.lastInterestDate }} +
+ } + +

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

+ + + + @if (currentProduct.minRequiredOpeningBalance) { +
+ {{ 'labels.inputs.Minimum Opening Balance' | translate }}: + {{ currentProduct.minRequiredOpeningBalance | formatNumber }} +
+ } + + @if (currentProduct.lockinPeriodFrequency) { +
+ {{ 'labels.inputs.Lock-in Period' | translate }}: + {{ + currentProduct.lockinPeriodFrequency + + ' ' + + (currentProduct.lockinPeriodFrequencyType?.value || currentProduct.lockinPeriodFrequencyType?.code || '') + }} +
+ } + +
+ {{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}: + {{ currentProduct.withdrawalFeeForTransfers | yesNo }} +
+ + @if (currentProduct.balanceFrequency) { +
+ Balance Frequency: + {{ currentProduct.balanceFrequency }} +
+ } + +
+ Enable Lock-in Period: + {{ currentProduct.enableLockinPeriod | yesNo }} +
+ + @if (currentProduct.minBalanceForInterestCalculation) { +
+ {{ 'labels.inputs.Balance Required for Interest Calculation' | translate }}: + {{ currentProduct.minBalanceForInterestCalculation | formatNumber }} +
+ } + +
+ {{ 'labels.inputs.Enforce Minimum Balance' | translate }}: + {{ currentProduct.enforceMinRequiredBalance | yesNo }} +
+ + @if (currentProduct.minRequiredBalance) { +
+ {{ 'labels.inputs.Minimum Balance' | translate }}: + {{ currentProduct.minRequiredBalance | formatNumber }} +
+ } + +
+ {{ 'labels.inputs.Withhold Tax is Applicable' | translate }}: + {{ currentProduct.withHoldTax | yesNo }} +
+ + @if (currentProduct.withHoldTax) { +
+ {{ 'labels.inputs.Tax Group' | translate }}: + {{ currentProduct.taxGroup.name }} +
+ } + +
+ {{ 'labels.inputs.Is Overdraft Allowed' | translate }}: + {{ currentProduct.allowOverdraft | yesNo }} +
+ +
+ Account Closed Zero Balance in Day Closing: + {{ currentProduct.accountClosedZeroBalanceInDayClosing | yesNo }} +
+ + @if (currentProduct.minOverdraftForInterestCalculation) { +
+ {{ 'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate }}: + {{ currentProduct.minOverdraftForInterestCalculation | formatNumber }} +
+ } + + @if (currentProduct.nominalAnnualInterestRateOverdraft) { +
+ {{ 'labels.inputs.Nominal Annual Interest for Overdraft' | translate }}: + {{ currentProduct.nominalAnnualInterestRateOverdraft | formatNumber }} % +
+ } + + @if (currentProduct.overdraftLimit) { +
+ {{ 'labels.inputs.Maximum Overdraft Amount Limit' | translate }}: + {{ currentProduct.overdraftLimit | formatNumber }} +
+ } + + @if (currentProduct.allowOverdraft) { + @if (currentProduct.avgBalanceCalcPeriod) { +
+ Average Balance Calculation Period: + {{ currentProduct.avgBalanceCalcPeriod }} +
+ } + + @if (currentProduct.validForPeriod) { +
+ Valid for Period: + {{ currentProduct.validForPeriod }} +
+ } + + @if (currentProduct.odAccountClosedZeroBalanceInDayClosing !== undefined) { +
+ OD Account Closed with Zero Balance in Day Closing: + {{ currentProduct.odAccountClosedZeroBalanceInDayClosing | yesNo }} +
+ } + } + +
+ {{ 'labels.inputs.Enable Dormancy Tracking' | translate }}: + {{ currentProduct.isDormancyTrackingActive | yesNo }} +
+ + @if (currentProduct.isDormancyTrackingActive) { +
+ {{ 'labels.inputs.Number of Days to Inactive sub-status' | translate }}: + {{ currentProduct.daysToInactive }} + {{ 'labels.inputs.Number of Days to Dormant sub-status' | translate }}: + {{ currentProduct.daysToDormancy }} + {{ 'labels.inputs.Number of Days to Escheat' | translate }}: + {{ currentProduct.daysToEscheat }} +
+ } + +

Account Facilities

+ + + @if (currentProduct.accountFacilityConfig) { +
+ @for (feature of accountFacilityFeatureItems; track feature.key) { + @if (currentProduct.accountFacilityConfig[feature.key] !== undefined) { +
+ {{ feature.label }}: + {{ currentProduct.accountFacilityConfig[feature.key] | yesNo }} +
+ } + } +
+ } + + @if (currentProduct.accountFacilityLimits?.length) { +
+

Account Facility Limits

+ + + + + + + + + + + + + + + + + + + + +
Limit Type{{ row.limitType }}General Limit{{ row.generalLimit }}Minor Account Limit{{ row.minorAccountLimit }}Small Saving Limit{{ row.smallSavingLimit }}
+
+ } + + @if (currentProduct.depositLimitKyc) { +
+

Deposit Limit (KYC)

+
+ General: + {{ currentProduct.depositLimitKyc.general.kycType }} - + {{ currentProduct.depositLimitKyc.general.limit }} +
+
+ Minor: + {{ currentProduct.depositLimitKyc.minor.kycType }} - {{ currentProduct.depositLimitKyc.minor.limit }} +
+
+ Small Saving: + {{ currentProduct.depositLimitKyc.smallSaving.kycType }} - + {{ currentProduct.depositLimitKyc.smallSaving.limit }} +
+
+ } + + @if (currentProduct.charges.length) { +
+

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

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

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

+ + + + + + + @if (currentProduct.otherGlMappingsMap) { +
+

Other GL Mappings

+ + + @if (currentProduct.otherGlMappingsMap.assets?.length) { +

Assets

+ + + + + + + + + + + +
Account Name{{ row.accountName }}GL Account{{ row.glCode || row.glAccountId }}
+ } + + @if (currentProduct.otherGlMappingsMap.liabilities?.length) { +

Liabilities

+ + + + + + + + + + + +
Account Name{{ row.accountName }}GL Account{{ row.glCode || row.glAccountId }}
+ } + + @if (currentProduct.otherGlMappingsMap.expenses?.length) { +

Expenses

+ + + + + + + + + + + +
Account Name{{ row.accountName }}GL Account{{ row.glCode || row.glAccountId }}
+ } + + @if (currentProduct.otherGlMappingsMap.income?.length) { +

Income

+ + + + + + + + + + + +
Account Name{{ row.accountName }}GL Account{{ row.glCode || row.glAccountId }}
+ } +
+ } + + @if ( + currentProduct.paymentChannelToFundSourceMappings?.length || + currentProduct.feeToIncomeAccountMappings?.length || + currentProduct.penaltyToIncomeAccountMappings?.length + ) { +
+

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

+ + @if (currentProduct.paymentChannelToFundSourceMappings?.length) { +
+

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

+ + + + + + + + + + + +
{{ 'labels.inputs.Payment Type' | translate }} + {{ paymentFundSource.paymentType.name }} + {{ 'labels.inputs.Fund Source' | translate }} + ({{ paymentFundSource.fundSourceAccount.glCode }}) {{ paymentFundSource.fundSourceAccount.name }} +
+
+ } + @if (currentProduct.feeToIncomeAccountMappings?.length) { +
+

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

+ + + + + + + + + + + +
{{ 'labels.inputs.Fee/Charge' | translate }} + {{ fee.charge.name }} + {{ 'labels.inputs.Income Account' | translate }}({{ fee.incomeAccount.glCode }}) {{ fee.incomeAccount.name }}
+
+ } + @if (currentProduct.penaltyToIncomeAccountMappings?.length) { +
+

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

+ + + + + + + + + + + +
{{ 'labels.inputs.Penalty' | translate }} + {{ penalty.charge.name }} + {{ 'labels.inputs.Income Account' | translate }} + ({{ penalty.incomeAccount.glCode }}) {{ penalty.incomeAccount.name }} +
+
+ } +
+ }
diff --git a/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.ts b/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.ts index 54b58f02c..289028614 100644 --- a/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.ts +++ b/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.ts @@ -1,3 +1,10 @@ +/** + * 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 { Accounting } from 'app/core/utils/accounting'; @@ -56,6 +63,34 @@ export class CurrentProductGeneralTabComponent { 'chargeTimeType' ]; + paymentFundSourceDisplayedColumns: string[] = [ + 'paymentTypeId', + 'fundSourceAccountId' + ]; + feesPenaltyIncomeDisplayedColumns: string[] = [ + 'chargeId', + 'incomeAccountId' + ]; + + accountFacilityFeatureItems = [ + { key: 'atmFacility', label: 'ATM Facility' }, + { key: 'atmWithdrawalFacility', label: 'ATM Withdrawal Facility' }, + { key: 'upiFacility', label: 'UPI Facility' }, + { key: 'posTransactionsFacility', label: 'POS Transactions Facility' }, + { key: 'chequeBook', label: 'Cheque Book' }, + { key: 'impsFacility', label: 'IMPS Facility' }, + { key: 'smsAlerts', label: 'SMS Alerts' }, + { key: 'bbpsFacility', label: 'BBPS Facility' }, + { key: 'insuranceCover', label: 'Insurance Cover' }, + { key: 'bepgFacility', label: 'BEPG Facility' }, + { key: 'neftFacility', label: 'NEFT Facility' }, + { key: 'rtgsFacility', label: 'RTGS Facility' }, + { key: 'interBankTotalCapFacility', label: 'Inter-Bank Total Cap Facility' }, + { key: 'withdrawalFacility', label: 'Withdrawal Facility' }, + { key: 'depositFacilityKyc', label: 'Deposit Facility KYC' }, + { key: 'virtualDebitCard', label: 'Virtual Debit Card' } + ]; + constructor() { this.route.data.subscribe((data: { currentProduct: any }) => { this.currentProduct = data.currentProduct; diff --git a/src/app/products/fixed-deposit-products/create-fixed-deposit-product/create-fixed-deposit-product.component.scss b/src/app/products/fixed-deposit-products/create-fixed-deposit-product/create-fixed-deposit-product.component.scss index 38cce13dd..8e2ab2292 100644 --- a/src/app/products/fixed-deposit-products/create-fixed-deposit-product/create-fixed-deposit-product.component.scss +++ b/src/app/products/fixed-deposit-products/create-fixed-deposit-product/create-fixed-deposit-product.component.scss @@ -18,9 +18,10 @@ $primary: #1353a0; .fdp-tabs { background: var(--md-sys-color-surface, #fff); border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + box-shadow: 0 2px 8px rgb(0 0 0 / 8%); + ::ng-deep { - .mat-mdc-tab-header { border-bottom: 1px solid rgba(0, 0, 0, 0.08); } + .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; } @@ -33,7 +34,7 @@ $primary: #1353a0; .fdp-action-bar { display: flex; justify-content: space-between; align-items: center; - margin-top: 20px; padding: 16px 0; border-top: 1px solid rgba(0, 0, 0, 0.08); - &__cancel { color: rgba(0, 0, 0, 0.6); } + 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/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.scss b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.scss index 2f9db0ace..74f18ffcf 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.scss +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.scss @@ -44,7 +44,7 @@ gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { + @media (width <= 600px) { grid-template-columns: 1fr; } } @@ -62,7 +62,7 @@ gap: 24px; flex-wrap: wrap; - @media (max-width: 600px) { + @media (width <= 600px) { flex-direction: column; } } diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-charges-step/fixed-deposit-product-charges-step.component.scss b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-charges-step/fixed-deposit-product-charges-step.component.scss index 1e57eef92..ccfe94921 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-charges-step/fixed-deposit-product-charges-step.component.scss +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-charges-step/fixed-deposit-product-charges-step.component.scss @@ -44,7 +44,7 @@ gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { + @media (width <= 600px) { grid-template-columns: 1fr; } } diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-currency-step/fixed-deposit-product-currency-step.component.scss b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-currency-step/fixed-deposit-product-currency-step.component.scss index a6baa2b82..fe1c47dc5 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-currency-step/fixed-deposit-product-currency-step.component.scss +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-currency-step/fixed-deposit-product-currency-step.component.scss @@ -44,7 +44,7 @@ gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { + @media (width <= 600px) { grid-template-columns: 1fr; } } diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.scss b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.scss index c561e87f5..7747019fc 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.scss +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.scss @@ -44,7 +44,7 @@ gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { + @media (width <= 600px) { grid-template-columns: 1fr; } } diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-interest-rate-chart-step/fixed-deposit-product-interest-rate-chart-step.component.scss b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-interest-rate-chart-step/fixed-deposit-product-interest-rate-chart-step.component.scss index 161bdd25c..e74afdae2 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-interest-rate-chart-step/fixed-deposit-product-interest-rate-chart-step.component.scss +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-interest-rate-chart-step/fixed-deposit-product-interest-rate-chart-step.component.scss @@ -51,7 +51,7 @@ grid-template-columns: repeat(2, 1fr); gap: 4px 16px; - @media (max-width: 600px) { + @media (width <= 600px) { grid-template-columns: 1fr; } } diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-preview-step/fixed-deposit-product-preview-step.component.scss b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-preview-step/fixed-deposit-product-preview-step.component.scss index 5e4dbc4ec..4ae6c504a 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-preview-step/fixed-deposit-product-preview-step.component.scss +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-preview-step/fixed-deposit-product-preview-step.component.scss @@ -57,7 +57,7 @@ gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { + @media (width <= 600px) { grid-template-columns: 1fr; } } diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.scss b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.scss index 41cc7b11f..62b1187f8 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.scss +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.scss @@ -44,7 +44,7 @@ gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { + @media (width <= 600px) { grid-template-columns: 1fr; } } diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.scss b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.scss index 49fd6b91b..ac8a2ddd5 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.scss +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.scss @@ -44,7 +44,7 @@ gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { + @media (width <= 600px) { grid-template-columns: 1fr; } } diff --git a/src/app/products/floating-rates/create-floating-rate/create-floating-rate.component.scss b/src/app/products/floating-rates/create-floating-rate/create-floating-rate.component.scss index e5da63228..9a9eda976 100644 --- a/src/app/products/floating-rates/create-floating-rate/create-floating-rate.component.scss +++ b/src/app/products/floating-rates/create-floating-rate/create-floating-rate.component.scss @@ -152,7 +152,7 @@ padding-top: 8px; } -@media (max-width: 600px) { +@media (width <= 600px) { .cfr-container { padding: 16px 10px; } diff --git a/src/app/products/floating-rates/edit-floating-rate/edit-floating-rate.component.scss b/src/app/products/floating-rates/edit-floating-rate/edit-floating-rate.component.scss index 4cf972d91..2c1864327 100644 --- a/src/app/products/floating-rates/edit-floating-rate/edit-floating-rate.component.scss +++ b/src/app/products/floating-rates/edit-floating-rate/edit-floating-rate.component.scss @@ -152,7 +152,7 @@ padding-top: 8px; } -@media (max-width: 600px) { +@media (width <= 600px) { .efr-container { padding: 16px 10px; } diff --git a/src/app/products/floating-rates/view-floating-rate/view-floating-rate.component.scss b/src/app/products/floating-rates/view-floating-rate/view-floating-rate.component.scss index 3c5fe96f7..9fc868d02 100644 --- a/src/app/products/floating-rates/view-floating-rate/view-floating-rate.component.scss +++ b/src/app/products/floating-rates/view-floating-rate/view-floating-rate.component.scss @@ -154,7 +154,7 @@ margin-right: 10px; } -@media (max-width: 600px) { +@media (width <= 600px) { .vfr-container { padding: 16px 10px; } diff --git a/src/app/products/loan-products/create-loan-product/create-loan-product.component.scss b/src/app/products/loan-products/create-loan-product/create-loan-product.component.scss index 65cff69e8..d05cedb1a 100644 --- a/src/app/products/loan-products/create-loan-product/create-loan-product.component.scss +++ b/src/app/products/loan-products/create-loan-product/create-loan-product.component.scss @@ -35,11 +35,11 @@ $primary: #1353a0; .lp-tabs { background: var(--md-sys-color-surface, #fff); border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + box-shadow: 0 2px 8px rgb(0 0 0 / 8%); ::ng-deep { .mat-mdc-tab-header { - border-bottom: 1px solid rgba(0, 0, 0, 0.08); + border-bottom: 1px solid rgb(0 0 0 / 8%); } .mat-mdc-tab { @@ -77,10 +77,10 @@ $primary: #1353a0; align-items: center; margin-top: 20px; padding: 16px 0; - border-top: 1px solid rgba(0, 0, 0, 0.08); + border-top: 1px solid rgb(0 0 0 / 8%); &__cancel { - color: rgba(0, 0, 0, 0.6); + color: rgb(0 0 0 / 60%); } &__right { diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.scss index de058a346..687d9456d 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.scss +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.scss @@ -112,7 +112,7 @@ } // Responsive -@media (max-width: 600px) { +@media (width <= 600px) { .lpa-grid { grid-template-columns: 1fr; } diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-capitalized-income-step/loan-product-deferred-income-recognition-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-capitalized-income-step/loan-product-deferred-income-recognition-step.component.scss index 1c455c6f9..3b4fc2e89 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-capitalized-income-step/loan-product-deferred-income-recognition-step.component.scss +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-capitalized-income-step/loan-product-deferred-income-recognition-step.component.scss @@ -39,7 +39,7 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { grid-template-columns: 1fr; } } .lpdi-field { diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.scss index e9667a432..a9cfa9a1f 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.scss +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.scss @@ -57,7 +57,7 @@ $gray-900: #202124; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; } - @media (max-width: 600px) { + @media (width <= 600px) { flex-direction: column; &__select { max-width: 100%; width: 100%; } } diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.scss index eb806e2d3..5dbbf06b8 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.scss +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.scss @@ -39,7 +39,7 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { grid-template-columns: 1fr; } } .lpc-field { diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.scss index f50bafc78..01e69962d 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.scss +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.scss @@ -39,7 +39,7 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { grid-template-columns: 1fr; } } .lpd-field { diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-interest-refund-step/loan-product-interest-refund-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-interest-refund-step/loan-product-interest-refund-step.component.scss index 5d6f7a21b..c8dc9d8b9 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-interest-refund-step/loan-product-interest-refund-step.component.scss +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-interest-refund-step/loan-product-interest-refund-step.component.scss @@ -37,7 +37,7 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { grid-template-columns: 1fr; } } .lpir-field { diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.scss index 9fbe0d1ec..ef42e0e74 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.scss +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.scss @@ -53,7 +53,7 @@ gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { + @media (width <= 600px) { grid-template-columns: 1fr; } } @@ -80,7 +80,7 @@ grid-template-columns: repeat(2, 1fr); gap: 4px 16px; - @media (max-width: 600px) { + @media (width <= 600px) { grid-template-columns: 1fr; } } diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.scss index 883ed9f1c..5f40cb5b5 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.scss +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.scss @@ -50,7 +50,7 @@ gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { + @media (width <= 600px) { grid-template-columns: 1fr; } } @@ -58,7 +58,7 @@ .lpt-grid-3 { grid-template-columns: repeat(3, 1fr); - @media (max-width: 600px) { + @media (width <= 600px) { grid-template-columns: 1fr; } } @@ -66,11 +66,11 @@ .lpt-grid-4 { grid-template-columns: repeat(4, 1fr); - @media (max-width: 900px) { + @media (width <= 900px) { grid-template-columns: repeat(2, 1fr); } - @media (max-width: 600px) { + @media (width <= 600px) { grid-template-columns: 1fr; } } diff --git a/src/app/products/manage-delinquency-buckets/delinquency-bucket/create-bucket/create-bucket.component.scss b/src/app/products/manage-delinquency-buckets/delinquency-bucket/create-bucket/create-bucket.component.scss index d043d9455..4ba22656a 100644 --- a/src/app/products/manage-delinquency-buckets/delinquency-bucket/create-bucket/create-bucket.component.scss +++ b/src/app/products/manage-delinquency-buckets/delinquency-bucket/create-bucket/create-bucket.component.scss @@ -92,7 +92,7 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { grid-template-columns: 1fr; } } .db-field { diff --git a/src/app/products/manage-delinquency-buckets/delinquency-bucket/edit-bucket/edit-bucket.component.scss b/src/app/products/manage-delinquency-buckets/delinquency-bucket/edit-bucket/edit-bucket.component.scss index d043d9455..4ba22656a 100644 --- a/src/app/products/manage-delinquency-buckets/delinquency-bucket/edit-bucket/edit-bucket.component.scss +++ b/src/app/products/manage-delinquency-buckets/delinquency-bucket/edit-bucket/edit-bucket.component.scss @@ -92,7 +92,7 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { grid-template-columns: 1fr; } } .db-field { diff --git a/src/app/products/manage-delinquency-buckets/delinquency-range/create-range/create-range.component.scss b/src/app/products/manage-delinquency-buckets/delinquency-range/create-range/create-range.component.scss index baee4a424..c191f95f5 100644 --- a/src/app/products/manage-delinquency-buckets/delinquency-range/create-range/create-range.component.scss +++ b/src/app/products/manage-delinquency-buckets/delinquency-range/create-range/create-range.component.scss @@ -31,9 +31,9 @@ .cr-action-bar { display: flex; justify-content: flex-end; gap: 12px; margin-top: 20px; padding: 16px 0; - border-top: 1px solid rgba(0, 0, 0, 0.08); + border-top: 1px solid rgb(0 0 0 / 8%); } -@media (max-width: 600px) { +@media (width <= 600px) { .cr-grid { grid-template-columns: 1fr; } } diff --git a/src/app/products/manage-delinquency-buckets/delinquency-range/edit-range/edit-range.component.scss b/src/app/products/manage-delinquency-buckets/delinquency-range/edit-range/edit-range.component.scss index e5a3364e0..53d95280c 100644 --- a/src/app/products/manage-delinquency-buckets/delinquency-range/edit-range/edit-range.component.scss +++ b/src/app/products/manage-delinquency-buckets/delinquency-range/edit-range/edit-range.component.scss @@ -31,9 +31,9 @@ .er-action-bar { display: flex; justify-content: flex-end; gap: 12px; margin-top: 20px; padding: 16px 0; - border-top: 1px solid rgba(0, 0, 0, 0.08); + border-top: 1px solid rgb(0 0 0 / 8%); } -@media (max-width: 600px) { +@media (width <= 600px) { .er-grid { grid-template-columns: 1fr; } } diff --git a/src/app/products/manage-delinquency-buckets/manage-delinquency-buckets.component.scss b/src/app/products/manage-delinquency-buckets/manage-delinquency-buckets.component.scss index 8d62191ed..cc835be50 100644 --- a/src/app/products/manage-delinquency-buckets/manage-delinquency-buckets.component.scss +++ b/src/app/products/manage-delinquency-buckets/manage-delinquency-buckets.component.scss @@ -23,7 +23,7 @@ &:hover { border-color: #0ea5e9; - box-shadow: 0 2px 8px rgba(14, 165, 233, 0.12); + box-shadow: 0 2px 8px rgb(14 165 233 / 12%); } &__icon-wrap { diff --git a/src/app/products/manage-tax-components/create-tax-component/create-tax-component.component.scss b/src/app/products/manage-tax-components/create-tax-component/create-tax-component.component.scss index 54e7899d4..ddf54f2c6 100644 --- a/src/app/products/manage-tax-components/create-tax-component/create-tax-component.component.scss +++ b/src/app/products/manage-tax-components/create-tax-component/create-tax-component.component.scss @@ -92,7 +92,7 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { grid-template-columns: 1fr; } } .ctc-field { diff --git a/src/app/products/manage-tax-components/edit-tax-component/edit-tax-component.component.scss b/src/app/products/manage-tax-components/edit-tax-component/edit-tax-component.component.scss index 57173690c..4eddbd8f5 100644 --- a/src/app/products/manage-tax-components/edit-tax-component/edit-tax-component.component.scss +++ b/src/app/products/manage-tax-components/edit-tax-component/edit-tax-component.component.scss @@ -92,7 +92,7 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { grid-template-columns: 1fr; } } .etc-field { diff --git a/src/app/products/manage-tax-components/view-tax-component/view-tax-component.component.scss b/src/app/products/manage-tax-components/view-tax-component/view-tax-component.component.scss index 83d5d9403..5d3396259 100644 --- a/src/app/products/manage-tax-components/view-tax-component/view-tax-component.component.scss +++ b/src/app/products/manage-tax-components/view-tax-component/view-tax-component.component.scss @@ -81,14 +81,14 @@ $gray-900: #202124; padding: 20px 28px 24px; background: #fff; - @media (max-width: 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { grid-template-columns: 1fr; } } .vtc-cell { padding: 14px 16px; background: #f8fafc; border-radius: 8px; - border: 1px solid rgba(0, 0, 0, 0.04); + border: 1px solid rgb(0 0 0 / 4%); margin: 3px; transition: background 0.12s; @@ -114,7 +114,7 @@ $gray-900: #202124; } // Responsive -@media (max-width: 600px) { +@media (width <= 600px) { .vtc-header { flex-direction: column; align-items: flex-start; diff --git a/src/app/products/manage-tax-configurations/manage-tax-configurations.component.scss b/src/app/products/manage-tax-configurations/manage-tax-configurations.component.scss index bc087f023..b107dad99 100644 --- a/src/app/products/manage-tax-configurations/manage-tax-configurations.component.scss +++ b/src/app/products/manage-tax-configurations/manage-tax-configurations.component.scss @@ -62,7 +62,7 @@ $gray-900: #0f172a; grid-template-columns: repeat(2, 1fr); gap: 12px; - @media (max-width: 768px) { + @media (width <= 768px) { grid-template-columns: 1fr; } } @@ -82,7 +82,7 @@ $gray-900: #0f172a; &:hover { border-color: #cbd5e1; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + box-shadow: 0 2px 8px rgb(0 0 0 / 6%); transform: translateY(-1px); } diff --git a/src/app/products/manage-tax-groups/create-tax-group/create-tax-group.component.scss b/src/app/products/manage-tax-groups/create-tax-group/create-tax-group.component.scss index 5fc7c9d76..38a8f6a0a 100644 --- a/src/app/products/manage-tax-groups/create-tax-group/create-tax-group.component.scss +++ b/src/app/products/manage-tax-groups/create-tax-group/create-tax-group.component.scss @@ -102,7 +102,7 @@ $text-muted: #64748b; gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { grid-template-columns: 1fr; } } .ctg-field { diff --git a/src/app/products/manage-tax-groups/edit-tax-group/edit-tax-group.component.scss b/src/app/products/manage-tax-groups/edit-tax-group/edit-tax-group.component.scss index 508e65c38..5558ddbaa 100644 --- a/src/app/products/manage-tax-groups/edit-tax-group/edit-tax-group.component.scss +++ b/src/app/products/manage-tax-groups/edit-tax-group/edit-tax-group.component.scss @@ -102,7 +102,7 @@ $text-muted: #64748b; gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { grid-template-columns: 1fr; } } .etg-field { diff --git a/src/app/products/manage-tax-groups/view-tax-group/view-tax-group.component.scss b/src/app/products/manage-tax-groups/view-tax-group/view-tax-group.component.scss index e17847cf5..107152abd 100644 --- a/src/app/products/manage-tax-groups/view-tax-group/view-tax-group.component.scss +++ b/src/app/products/manage-tax-groups/view-tax-group/view-tax-group.component.scss @@ -109,14 +109,14 @@ $text-muted: #64748b; gap: 1px; padding: 20px 28px 24px; - @media (max-width: 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { grid-template-columns: 1fr; } } .vtg-cell { padding: 14px 16px; background: #f8fafc; border-radius: 8px; - border: 1px solid rgba(0, 0, 0, 0.04); + border: 1px solid rgb(0 0 0 / 4%); margin: 3px; transition: background 0.12s; @@ -195,7 +195,7 @@ $text-muted: #64748b; } // Responsive -@media (max-width: 600px) { +@media (width <= 600px) { .vtg-header { flex-direction: column; align-items: flex-start; diff --git a/src/app/products/products-mix/create-product-mix/create-product-mix.component.scss b/src/app/products/products-mix/create-product-mix/create-product-mix.component.scss index 621e88022..a7e748e41 100644 --- a/src/app/products/products-mix/create-product-mix/create-product-mix.component.scss +++ b/src/app/products/products-mix/create-product-mix/create-product-mix.component.scss @@ -81,7 +81,7 @@ padding-top: 8px; } -@media (max-width: 600px) { +@media (width <= 600px) { .cpm-container { padding: 16px 12px; } diff --git a/src/app/products/products-mix/edit-product-mix/edit-product-mix.component.scss b/src/app/products/products-mix/edit-product-mix/edit-product-mix.component.scss index 2a0403cdf..c8f348e5d 100644 --- a/src/app/products/products-mix/edit-product-mix/edit-product-mix.component.scss +++ b/src/app/products/products-mix/edit-product-mix/edit-product-mix.component.scss @@ -81,7 +81,7 @@ padding-top: 8px; } -@media (max-width: 600px) { +@media (width <= 600px) { .epm-container { padding: 16px 12px; } diff --git a/src/app/products/products-mix/view-product-mix/view-product-mix.component.scss b/src/app/products/products-mix/view-product-mix/view-product-mix.component.scss index 069352a61..7ed4e4fb4 100644 --- a/src/app/products/products-mix/view-product-mix/view-product-mix.component.scss +++ b/src/app/products/products-mix/view-product-mix/view-product-mix.component.scss @@ -156,7 +156,7 @@ background: transparent; } -@media (max-width: 600px) { +@media (width <= 600px) { .vpm-container { padding: 16px 12px; } diff --git a/src/app/products/products.component.scss b/src/app/products/products.component.scss index 985812ad5..698497b5f 100644 --- a/src/app/products/products.component.scss +++ b/src/app/products/products.component.scss @@ -61,7 +61,7 @@ $gray-900: #0f172a; grid-template-columns: repeat(2, 1fr); gap: 12px; - @media (max-width: 768px) { + @media (width <= 768px) { grid-template-columns: 1fr; } } @@ -82,7 +82,7 @@ $gray-900: #0f172a; &:hover { border-color: #cbd5e1; background: $gray-50; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04); + box-shadow: 0 2px 8px rgb(0 0 0 / 4%); .pd-card__arrow { color: $gray-700; @@ -101,62 +101,62 @@ $gray-900: #0f172a; flex-shrink: 0; &--blue { - background: rgba(59, 130, 246, 0.1); + background: rgb(59 130 246 / 10%); color: #2563eb; } &--green { - background: rgba(34, 197, 94, 0.1); + background: rgb(34 197 94 / 10%); color: #16a34a; } &--cyan { - background: rgba(6, 182, 212, 0.1); + background: rgb(6 182 212 / 10%); color: #0891b2; } &--purple { - background: rgba(139, 92, 246, 0.1); + background: rgb(139 92 246 / 10%); color: #7c3aed; } &--amber { - background: rgba(245, 158, 11, 0.1); + background: rgb(245 158 11 / 10%); color: #d97706; } &--teal { - background: rgba(20, 184, 166, 0.1); + background: rgb(20 184 166 / 10%); color: #0d9488; } &--indigo { - background: rgba(99, 102, 241, 0.1); + background: rgb(99 102 241 / 10%); color: #4f46e5; } &--orange { - background: rgba(249, 115, 22, 0.1); + background: rgb(249 115 22 / 10%); color: #ea580c; } &--slate { - background: rgba(100, 116, 139, 0.1); + background: rgb(100 116 139 / 10%); color: #475569; } &--rose { - background: rgba(244, 63, 94, 0.1); + background: rgb(244 63 94 / 10%); color: #e11d48; } &--emerald { - background: rgba(16, 185, 129, 0.1); + background: rgb(16 185 129 / 10%); color: #059669; } &--red { - background: rgba(239, 68, 68, 0.1); + background: rgb(239 68 68 / 10%); color: #dc2626; } } diff --git a/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.html b/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.html index 9395a61dd..b0882ab54 100644 --- a/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.html +++ b/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.html @@ -12,7 +12,12 @@

{{ 'labels.heading.Create Recurring Deposit Product' | translate }}

- + @@ -126,19 +131,19 @@
@if (selectedTabIndex > 0) { } @if (selectedTabIndex < totalTabs - 1) { } @if (recurringDepositProductFormValid && selectedTabIndex === totalTabs - 1) { } diff --git a/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.scss b/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.scss index 6d2fa4996..9891e8ba1 100644 --- a/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.scss +++ b/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.scss @@ -8,32 +8,83 @@ $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-container { + max-width: 960px; + margin: 0 auto; + padding: 24px 16px; } -.rdp-tabs { - background: var(--md-sys-color-surface, #fff); border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); - ::ng-deep { - .mat-mdc-tab-header { border-bottom: 1px solid rgba(0, 0, 0, 0.08); } - .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-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-tab-icon { margin-right: 8px; font-size: 14px; } -.rdp-tab-content { padding: 24px 16px; } +.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; +} .rdp-action-bar { - display: flex; justify-content: space-between; align-items: center; - margin-top: 20px; padding: 16px 0; border-top: 1px solid rgba(0, 0, 0, 0.08); - &__cancel { color: rgba(0, 0, 0, 0.6); } - &__right { display: flex; gap: 12px; } + 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/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.html index 10024514c..6d8eba561 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.html @@ -7,273 +7,113 @@ -->
- -
- -

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

+ + + +

Accounting

+
- - @for (accountingRule of accountingRuleData; track accountingRule; let i = $index) { - - {{ 'labels.accounting.' + accountingRule | translate }} - - } - -
-
- - @if (isCashOrAccrualAccounting()) { - - -
-
- -

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

-
-
- - @if (isAccrualAccounting()) { - - - } -
-
- - -
-
- -

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

-
-
- - - @if (isAccrualAccounting()) { - - } -
-
- - -
-
- -

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

-
-
- -
-
- - -
-
- -

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

-
-
- - -
-
- - -
-
- -

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

- +
+
+

Accounting Rule

+
+
+ Cash +
- @if (recurringDepositProductAccountingForm.value.advancedAccountingRules) { -
- - -
-
-

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

-
- @if (paymentChannelToFundSourceMappings.value.length !== 0) { -
- - - - - - - - - - - - - - - -
{{ 'labels.inputs.Payment Type' | translate }} - {{ paymentFundSource.paymentTypeId | find: paymentTypeData : 'id' : 'name' }} - {{ 'labels.inputs.Fund Source' | translate }} - {{ paymentFundSource.fundSourceAccountId | find: assetAccountData : 'id' : 'name' }} - {{ 'labels.inputs.Actions' | translate }} - - -
-
- } +
+ @for (row of section.rows; track row.key) { + @if (!row.accrualOnly || isAccrualAccounting()) { +
+
+ + {{ 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 }} + + } + + +
+
+ +
+ } +
- - - @if (existCharges()) { -
-
-

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

- -
- @if (feeToIncomeAccountMappings.value.length !== 0) { -
- - - - - - - - - - - - - - - -
{{ 'labels.inputs.Fees' | translate }} - {{ feesIncome.chargeId | find: chargeData : 'id' : 'name' }} - {{ 'labels.inputs.Income Account' | translate }} - {{ feesIncome.incomeAccountId | find: incomeAccountData : 'id' : 'name' }} - {{ 'labels.inputs.Actions' | translate }} - - -
-
- } -
- } - - - @if (existCharges()) { -
-
-

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

- -
- @if (penaltyToIncomeAccountMappings.value.length !== 0) { -
- - - - - - - - - - - - - - - -
{{ 'labels.inputs.Penalty' | translate }} - {{ penaltyIncome.chargeId | find: penaltyData : 'id' : 'name' }} - {{ 'labels.inputs.Income Account' | translate }} - {{ penaltyIncome.incomeAccountId | find: incomeAccountData : 'id' : 'name' }} - {{ 'labels.inputs.Actions' | translate }} - - -
-
- } -
- } - -
+ } }
- } +
diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.scss b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.scss index 250653495..fe6ce3f2b 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.scss +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.scss @@ -6,33 +6,256 @@ * 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 #e2e8f0; + border: 1px solid $border; border-radius: 10px; overflow: hidden; background: #fff; +} - & + & { margin-top: 16px; } +.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; - 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; } + justify-content: space-between; + gap: 16px; + margin: 0 0 12px; } - &__body { padding: 16px 24px; } + h4 { + margin: 0 0 12px; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #94a3b8; + } } -.rdpa-grid { +.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: repeat(2, 1fr); - gap: 4px 16px; + 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 { @@ -41,39 +264,41 @@ flex-wrap: wrap; } -.rdpa-header-checkbox { margin-left: auto; } +.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; -.rdpa-subsection { - & + & { margin-top: 16px; } + &--green { + background: #dcfce7; + color: #16a34a; + } - &__header { - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: wrap; - gap: 8px; - margin-bottom: 8px; + &--amber { + background: #fef3c7; + color: #d97706; + } - h4 { margin: 0; font-size: 13px; font-weight: 600; color: #202124; } + &--blue { + background: #dbeafe; + color: #2563eb; } } -.rdpa-add-btn { - height: 36px; - border-radius: 8px; - font-size: 13px; - font-weight: 600; -} +@media (width <= 900px) { + .rdpa-row__meta { + margin-bottom: 4px; + } -.rdpa-table-wrapper { - border: 1px solid #e2e8f0; - border-radius: 8px; - overflow: hidden; - - table { width: 100%; } -} - -@media (max-width: 600px) { - .rdpa-grid { grid-template-columns: 1fr; } - .rdpa-radio-group { flex-direction: column; } + .rdpa-row__input { + flex: 1 1 auto; + max-width: 100%; + } } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.ts b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.ts index 6dfca1628..9d8843dda 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.ts +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.ts @@ -6,441 +6,393 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, OnInit, Input, inject } from '@angular/core'; +import { Component, Input, OnInit, inject } from '@angular/core'; import { - UntypedFormGroup, - UntypedFormBuilder, + FormsModule, + ReactiveFormsModule, UntypedFormArray, - Validators, + UntypedFormBuilder, UntypedFormControl, - ReactiveFormsModule + UntypedFormGroup, + Validators } from '@angular/forms'; -import { MatDialog } from '@angular/material/dialog'; - -import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component'; -import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; - -import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base'; -import { SelectBase } from 'app/shared/form-dialog/formfield/model/select-base'; +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 { TranslateService } from '@ngx-translate/core'; -import { MatRadioGroup, MatRadioButton } from '@angular/material/radio'; -import { GlAccountSelectorComponent } from '../../../../shared/accounting/gl-account-selector/gl-account-selector.component'; -import { MatCheckbox } from '@angular/material/checkbox'; -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 { FindPipe } from '../../../../pipes/find.pipe'; +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; +}; + +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-recurring-deposit-product-accounting-step', templateUrl: './recurring-deposit-product-accounting-step.component.html', styleUrls: ['./recurring-deposit-product-accounting-step.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, + FormsModule, + ReactiveFormsModule, MatRadioGroup, MatRadioButton, - GlAccountSelectorComponent, - MatCheckbox, - FaIconComponent, - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatIconButton, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow, - FindPipe + GlAccountSelectorComponent ] }) export class RecurringDepositProductAccountingStepComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); - private dialog = inject(MatDialog); private accounting = inject(Accounting); - private translateService = inject(TranslateService); @Input() recurringDepositProductsTemplate: any; @Input() accountingRuleData: any; @Input() recurringDepositProductFormValid: boolean; recurringDepositProductAccountingForm: UntypedFormGroup; - - chargeData: any; - penaltyData: any; - paymentTypeData: any; - assetAccountData: any; - incomeAccountData: any; - expenseAccountData: any; - liabilityAccountData: any; - - paymentFundSourceDisplayedColumns: string[] = [ - 'paymentTypeId', - 'fundSourceAccountId', - 'actions' - ]; - feesPenaltyIncomeDisplayedColumns: string[] = [ - 'chargeId', - 'incomeAccountId', - 'actions' - ]; + assetAccountOptions: GLAccount[] = []; + liabilityAccountOptions: GLAccount[] = []; + expenseAccountOptions: GLAccount[] = []; + incomeAccountOptions: GLAccount[] = []; + accountingSectionConfigs: AccountingSectionConfig[] = []; constructor() { - this.createrecurringDepositProductAccountingForm(); + this.createRecurringDepositProductAccountingForm(); this.setConditionalControls(); } ngOnInit() { - this.chargeData = this.recurringDepositProductsTemplate.chargeOptions || []; - this.penaltyData = this.recurringDepositProductsTemplate.penaltyOptions || []; - this.paymentTypeData = this.recurringDepositProductsTemplate.paymentTypeOptions || []; - this.assetAccountData = this.recurringDepositProductsTemplate.accountingMappingOptions.assetAccountOptions || []; - this.incomeAccountData = this.recurringDepositProductsTemplate.accountingMappingOptions.incomeAccountOptions || []; - this.expenseAccountData = - this.recurringDepositProductsTemplate.accountingMappingOptions.expenseAccountOptions || []; - this.liabilityAccountData = - this.recurringDepositProductsTemplate.accountingMappingOptions.liabilityAccountOptions || []; - if (!(this.recurringDepositProductsTemplate === undefined) && this.recurringDepositProductsTemplate.id) { - this.assignAccountingStepData(); - } + const accountingMappings = this.recurringDepositProductsTemplate?.accountingMappings || {}; + const mappings = { + ...accountingMappings, + otherGlMappingsMap: this.recurringDepositProductsTemplate?.otherGlMappingsMap + }; + const accountingMappingOptions = this.recurringDepositProductsTemplate?.accountingMappingOptions || {}; + const accountingRuleId = 2; // Always Cash + + this.assetAccountOptions = accountingMappingOptions.assetAccountOptions || []; + this.liabilityAccountOptions = accountingMappingOptions.liabilityAccountOptions || []; + this.expenseAccountOptions = accountingMappingOptions.expenseAccountOptions || []; + this.incomeAccountOptions = accountingMappingOptions.incomeAccountOptions || []; + + this.buildAccountingSectionConfigs(); + + this.recurringDepositProductAccountingForm.patchValue({ accountingRule: accountingRuleId }); + this.syncAccountingControls(accountingRuleId); + this.patchAccountingValues(mappings); } - assignAccountingStepData() { - this.recurringDepositProductAccountingForm.patchValue({ - accountingRule: this.recurringDepositProductsTemplate.accountingRule.id - }); - if (this.isCashOrAccrualAccounting()) { - this.recurringDepositProductAccountingForm.patchValue({ - savingsReferenceAccountId: this.recurringDepositProductsTemplate.accountingMappings.savingsReferenceAccount.id, - savingsControlAccountId: this.recurringDepositProductsTemplate.accountingMappings.savingsControlAccount.id, - transfersInSuspenseAccountId: - this.recurringDepositProductsTemplate.accountingMappings.transfersInSuspenseAccount.id, - incomeFromFeeAccountId: this.recurringDepositProductsTemplate.accountingMappings.incomeFromFeeAccount.id, - incomeFromPenaltyAccountId: - this.recurringDepositProductsTemplate.accountingMappings.incomeFromPenaltyAccount.id, - interestOnSavingsAccountId: this.recurringDepositProductsTemplate.accountingMappings.interestOnSavingsAccount.id - }); - - if (this.isAccrualAccounting()) { - this.recurringDepositProductAccountingForm.patchValue({ - feesReceivableAccountId: this.recurringDepositProductsTemplate.accountingMappings.feeReceivableAccount.id, - penaltiesReceivableAccountId: - this.recurringDepositProductsTemplate.accountingMappings.penaltyReceivableAccount.id, - interestPayableAccountId: this.recurringDepositProductsTemplate.accountingMappings.interestPayableAccount.id - }); - } - - if ( - this.recurringDepositProductsTemplate.paymentChannelToFundSourceMappings || - this.recurringDepositProductsTemplate.feeToIncomeAccountMappings || - this.recurringDepositProductsTemplate.penaltyToIncomeAccountMappings - ) { - this.recurringDepositProductAccountingForm.patchValue({ - advancedAccountingRules: true - }); - } - if (this.recurringDepositProductsTemplate.paymentChannelToFundSourceMappings) { - this.recurringDepositProductsTemplate.paymentChannelToFundSourceMappings.forEach( - (paymentChannelToFundSourceMapping: any) => { - const paymentChannelToFundSourceMappingData = this.formBuilder.group({ - paymentTypeId: [ - paymentChannelToFundSourceMapping.paymentType.id, - Validators.required - ], - fundSourceAccountId: [ - paymentChannelToFundSourceMapping.fundSourceAccount.id, - Validators.required - ] - }); - const formArray = this.recurringDepositProductAccountingForm.controls[ - 'paymentChannelToFundSourceMappings' - ] as UntypedFormArray; - formArray.push(paymentChannelToFundSourceMappingData); - } - ); - } - if (this.recurringDepositProductsTemplate.feeToIncomeAccountMappings) { - this.recurringDepositProductsTemplate.feeToIncomeAccountMappings.forEach((feeToIncomeAccountMapping: any) => { - const feeToIncomeAccountMappingData = this.formBuilder.group({ - chargeId: [ - feeToIncomeAccountMapping.charge.id, - Validators.required - ], - incomeAccountId: [ - feeToIncomeAccountMapping.incomeAccount.id, - Validators.required - ] - }); - const formArray = this.recurringDepositProductAccountingForm.controls[ - 'feeToIncomeAccountMappings' - ] as UntypedFormArray; - formArray.push(feeToIncomeAccountMappingData); - }); - } - if (this.recurringDepositProductsTemplate.penaltyToIncomeAccountMappings) { - this.recurringDepositProductsTemplate.penaltyToIncomeAccountMappings.forEach( - (penaltyToIncomeAccountMapping: any) => { - const penaltyToIncomeAccountMappingData = this.formBuilder.group({ - chargeId: [ - penaltyToIncomeAccountMapping.charge.id, - Validators.required - ], - incomeAccountId: [ - penaltyToIncomeAccountMapping.incomeAccount.id, - Validators.required - ] - }); - const formArray = this.recurringDepositProductAccountingForm.controls[ - 'penaltyToIncomeAccountMappings' - ] as UntypedFormArray; - formArray.push(penaltyToIncomeAccountMappingData); - } - ); - } - } - } - - createrecurringDepositProductAccountingForm() { + createRecurringDepositProductAccountingForm() { this.recurringDepositProductAccountingForm = this.formBuilder.group({ - accountingRule: [1] + accountingRule: [2], + assetsOtherGlMappings: this.formBuilder.array([]), + liabilitiesOtherGlMappings: this.formBuilder.array([]), + expensesOtherGlMappings: this.formBuilder.array([]), + incomeOtherGlMappings: this.formBuilder.array([]) }); } - existCharges(): boolean { - return this.chargeData.length > 0; - } - - setConditionalControls() { - this.recurringDepositProductAccountingForm.get('accountingRule').valueChanges.subscribe((accountingRule: any) => { - if (accountingRule === 2 || accountingRule === 3) { - this.recurringDepositProductAccountingForm.addControl( - 'savingsReferenceAccountId', - new UntypedFormControl('', Validators.required) - ); - this.recurringDepositProductAccountingForm.addControl( - 'savingsControlAccountId', - new UntypedFormControl('', Validators.required) - ); - this.recurringDepositProductAccountingForm.addControl( - 'transfersInSuspenseAccountId', - new UntypedFormControl('', Validators.required) - ); - this.recurringDepositProductAccountingForm.addControl( - 'interestOnSavingsAccountId', - new UntypedFormControl('', Validators.required) - ); - this.recurringDepositProductAccountingForm.addControl( - 'incomeFromFeeAccountId', - new UntypedFormControl('', Validators.required) - ); - this.recurringDepositProductAccountingForm.addControl( - 'incomeFromPenaltyAccountId', - new UntypedFormControl('', Validators.required) - ); - this.recurringDepositProductAccountingForm.addControl('advancedAccountingRules', new UntypedFormControl(false)); - - if (accountingRule === 3) { - this.recurringDepositProductAccountingForm.addControl( - 'feesReceivableAccountId', - new UntypedFormControl('', Validators.required) - ); - this.recurringDepositProductAccountingForm.addControl( - 'penaltiesReceivableAccountId', - new UntypedFormControl('', Validators.required) - ); - this.recurringDepositProductAccountingForm.addControl( - 'interestPayableAccountId', - new UntypedFormControl('', Validators.required) - ); - } - - this.recurringDepositProductAccountingForm - .get('advancedAccountingRules') - .valueChanges.subscribe((advancedAccountingRules: boolean) => { - if (advancedAccountingRules) { - this.recurringDepositProductAccountingForm.addControl( - 'paymentChannelToFundSourceMappings', - this.formBuilder.array([]) - ); - this.recurringDepositProductAccountingForm.addControl( - 'feeToIncomeAccountMappings', - this.formBuilder.array([]) - ); - this.recurringDepositProductAccountingForm.addControl( - 'penaltyToIncomeAccountMappings', - this.formBuilder.array([]) - ); - } else { - this.recurringDepositProductAccountingForm.removeControl('paymentChannelToFundSourceMappings'); - this.recurringDepositProductAccountingForm.removeControl('feeToIncomeAccountMappings'); - this.recurringDepositProductAccountingForm.removeControl('penaltyToIncomeAccountMappings'); - } - }); - } else { - this.recurringDepositProductAccountingForm.removeControl('savingsReferenceAccountId'); - this.recurringDepositProductAccountingForm.removeControl('overdraftPortfolioControlId'); - this.recurringDepositProductAccountingForm.removeControl('savingsControlAccountId'); - this.recurringDepositProductAccountingForm.removeControl('transfersInSuspenseAccountId'); - this.recurringDepositProductAccountingForm.removeControl('interestOnSavingsAccountId'); - this.recurringDepositProductAccountingForm.removeControl('writeOffAccountId'); - this.recurringDepositProductAccountingForm.removeControl('incomeFromFeeAccountId'); - this.recurringDepositProductAccountingForm.removeControl('incomeFromPenaltyAccountId'); - this.recurringDepositProductAccountingForm.removeControl('incomeFromInterestId'); - this.recurringDepositProductAccountingForm.removeControl('advancedAccountingRules'); - this.recurringDepositProductAccountingForm.removeControl('escheatLiabilityId'); - this.recurringDepositProductAccountingForm.removeControl('feesReceivableAccountId'); - this.recurringDepositProductAccountingForm.removeControl('penaltiesReceivableAccountId'); - this.recurringDepositProductAccountingForm.removeControl('interestPayableAccountId'); - } + private setConditionalControls() { + this.recurringDepositProductAccountingForm.get('accountingRule')?.valueChanges.subscribe((rule: number) => { + this.syncAccountingControls(rule); }); } - get paymentChannelToFundSourceMappings(): UntypedFormArray { - return this.recurringDepositProductAccountingForm.get('paymentChannelToFundSourceMappings') as UntypedFormArray; - } - - get feeToIncomeAccountMappings(): UntypedFormArray { - return this.recurringDepositProductAccountingForm.get('feeToIncomeAccountMappings') as UntypedFormArray; - } - - get penaltyToIncomeAccountMappings(): UntypedFormArray { - return this.recurringDepositProductAccountingForm.get('penaltyToIncomeAccountMappings') as UntypedFormArray; - } - - add(formType: string, formArray: UntypedFormArray) { - const data = { ...this.getData(formType), pristine: false }; - const dialogRef = this.dialog.open(FormDialogComponent, { data }); - dialogRef.afterClosed().subscribe((response: any) => { - if (response.data) { - formArray.push(response.data); - } + private patchAccountingValues(mappings: any) { + this.recurringDepositProductAccountingForm.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 || '' }); - } - edit(formType: string, formArray: UntypedFormArray, index: number) { - const data = { ...this.getData(formType, formArray.at(index).value), layout: { addButtonText: 'Edit' } }; - const dialogRef = this.dialog.open(FormDialogComponent, { data }); - dialogRef.afterClosed().subscribe((response: any) => { - if (response.data) { - formArray.at(index).patchValue(response.data.value); - } - }); - } - - delete(formArray: UntypedFormArray, index: number) { - const dialogRef = this.dialog.open(DeleteDialogComponent, { - data: { deleteContext: `this` } - }); - dialogRef.afterClosed().subscribe((response: any) => { - if (response.delete) { - formArray.removeAt(index); - } - }); - } - - getData(formType: string, values?: any) { - switch (formType) { - case 'PaymentFundSource': - return { - title: this.translateService.instant('labels.heading.Configure Fund Sources for Payment Channels'), - formfields: this.getPaymentFundSourceFormfields(values) - }; - case 'FeesIncome': - return { - title: this.translateService.instant('labels.heading.Map Fees to Specific Income Accounts'), - formfields: this.getFeesIncomeFormfields(values) - }; - case 'PenaltyIncome': - return { - title: this.translateService.instant('labels.heading.Map Penalties to Specific Income Accounts'), - formfields: this.getPenaltyIncomeFormfields(values) - }; + 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); } } - getPaymentFundSourceFormfields(values?: any) { - const formfields: FormfieldBase[] = [ - new SelectBase({ - controlName: 'paymentTypeId', - label: this.translateService.instant('labels.inputs.Payment Type'), - value: values ? values.paymentTypeId : this.paymentTypeData[0].id, - options: { label: 'name', value: 'id', data: this.paymentTypeData }, - required: true, - order: 1 - }), - new SelectBase({ - controlName: 'fundSourceAccountId', - label: this.translateService.instant('labels.inputs.Fund Source'), - value: values ? values.fundSourceAccountId : this.assetAccountData[0].id, - options: { label: 'name', value: 'id', data: this.assetAccountData }, - required: true, - order: 2 - }) - ]; - return formfields; + private createOtherGlMappingGroup(otherGlMapping: Partial = {}) { + return this.formBuilder.group({ + accountName: [ + otherGlMapping.accountName || '', + Validators.required + ], + glAccountId: [ + otherGlMapping.glAccountId || '', + Validators.required + ] + }); } - getFeesIncomeFormfields(values?: any) { - const formfields: FormfieldBase[] = [ - new SelectBase({ - controlName: 'chargeId', - label: this.translateService.instant('labels.inputs.Fees'), - value: values ? values.chargeId : this.chargeData[0].id, - options: { label: 'name', value: 'id', data: this.chargeData }, - required: true, - order: 1 - }), - new SelectBase({ - controlName: 'incomeAccountId', - label: this.translateService.instant('labels.inputs.Income Account'), - value: values ? values.incomeAccountId : this.incomeAccountData[0].id, - options: { label: 'name', value: 'id', data: this.incomeAccountData }, - required: true, - order: 2 - }) - ]; - return formfields; + getOtherGlMappingsArray(controlName: string): UntypedFormArray { + return this.recurringDepositProductAccountingForm.get(controlName) as UntypedFormArray; } - getPenaltyIncomeFormfields(values?: any) { - const formfields: FormfieldBase[] = [ - new SelectBase({ - controlName: 'chargeId', - label: 'Penalty', - value: values ? values.chargeId : this.penaltyData[0].id, - options: { label: 'name', value: 'id', data: this.penaltyData }, - required: true, - order: 1 - }), - new SelectBase({ - controlName: 'incomeAccountId', - label: 'Income Account', - value: values ? values.incomeAccountId : this.incomeAccountData[0].id, - options: { label: 'name', value: 'id', data: this.incomeAccountData }, - required: true, - order: 2 - }) + 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 syncAccountingControls(accountingRule: number) { + const addControl = (name: string, required = false) => { + if (!this.recurringDepositProductAccountingForm.contains(name)) { + this.recurringDepositProductAccountingForm.addControl( + name, + new UntypedFormControl('', required ? Validators.required : undefined) + ); + } else if (required) { + this.recurringDepositProductAccountingForm.get(name)?.setValidators(Validators.required); + this.recurringDepositProductAccountingForm.get(name)?.updateValueAndValidity({ emitEvent: false }); + } + }; + const removeControl = (name: string) => { + if (this.recurringDepositProductAccountingForm.contains(name)) { + this.recurringDepositProductAccountingForm.removeControl(name); + } + }; + + if (this.accounting.isCashOrAccrualAccountingRuleId(accountingRule)) { + addControl('savingsReferenceAccountId', true); + addControl('savingsControlAccountId', true); + addControl('transfersInSuspenseAccountId', true); + addControl('interestOnSavingsAccountId', true); + addControl('incomeFromFeeAccountId', true); + addControl('incomeFromPenaltyAccountId', true); + + if (this.accounting.isAccrualAccountingRuleId(accountingRule)) { + addControl('feesReceivableAccountId', true); + addControl('penaltiesReceivableAccountId', true); + addControl('interestPayableAccountId', true); + } else { + removeControl('feesReceivableAccountId'); + removeControl('penaltiesReceivableAccountId'); + removeControl('interestPayableAccountId'); + } + } else { + [ + 'savingsReferenceAccountId', + 'savingsControlAccountId', + 'transfersInSuspenseAccountId', + 'interestOnSavingsAccountId', + 'incomeFromFeeAccountId', + 'incomeFromPenaltyAccountId', + 'feesReceivableAccountId', + 'penaltiesReceivableAccountId', + 'interestPayableAccountId' + ].forEach(removeControl); + 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 + } + ], + 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 + } ]; - return formfields; } get recurringDepositProductAccounting() { - return this.recurringDepositProductAccountingForm.value; + const { + assetsOtherGlMappings, + liabilitiesOtherGlMappings, + expensesOtherGlMappings, + incomeOtherGlMappings, + ...accounting + } = this.recurringDepositProductAccountingForm.value; + const otherGlMappings = this.getOtherGlMappings(); + return { ...accounting, ...(otherGlMappings ? { otherGlMappings } : {}) }; } isCashOrAccrualAccounting(): boolean { diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component.html index dd7563ee5..b2c701878 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component.html @@ -23,7 +23,14 @@ } - diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component.scss b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component.scss index 84a9cf676..661815e4c 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component.scss +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component.scss @@ -20,8 +20,17 @@ background: #f8f9fa; border-bottom: 1px solid #e8eaed; - i { font-size: 14px; color: #0ea5e9; } - h3 { margin: 0; font-size: 14px; font-weight: 600; color: #202124; } + i { + font-size: 14px; + color: #0ea5e9; + } + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: #202124; + } } &__body { @@ -35,7 +44,10 @@ gap: 16px; flex-wrap: wrap; - &__select { flex: 1; min-width: 200px; } + &__select { + flex: 1; + min-width: 200px; + } } .rdpch-add-btn { @@ -51,5 +63,7 @@ overflow: hidden; margin-top: 12px; - table { width: 100%; } + table { + width: 100%; + } } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.html index baeea3de9..d21e17094 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.html @@ -9,71 +9,103 @@
- -

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

+ + Currency & Account Status Configuration
-
- - {{ 'labels.inputs.Currency' | translate }} - - @for (currency of currencyData; track currency) { - - {{ currency.name }} - - } - - - {{ 'labels.inputs.Currency' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.Decimal Places' | translate }} - - - {{ 'labels.inputs.Decimal Places' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - {{ 'labels.inputs.Decimal Places' | translate }} {{ 'labels.commons.Minimum Value must be' | translate }} - 0 - - - -
- - {{ 'labels.inputs.Set the recurring deposit installment in multiples of' | translate }} - -
- - @if (recurringDepositProductCurrencyForm.value.setMultiples) { +
+
- {{ 'labels.inputs.Currency in multiples of' | translate }} - + {{ 'labels.inputs.Currency' | translate }} + + @for (currency of currencyData; track currency) { + + {{ currency.name }} + + } + + Default: Indian Rupee - auto-set for domestic products - {{ 'labels.inputs.Currency in multiples of' | translate }} {{ 'labels.commons.is' | translate }} + {{ '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 + +
+ +
+
+

Balance Swap Option

+

Enable automated transfer of excess funds to linked deposits or vice-versa to maintain liquidity.

+
+ + yes + no + +
+ +
+
+

MSME

+

Micro, Small and Medium Enterprises

+
+ + 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/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.scss b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.scss index c0b63cb17..c2bcdef6b 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.scss +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.scss @@ -6,8 +6,14 @@ * 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 #e2e8f0; + border: 1px solid $border; border-radius: 10px; overflow: hidden; background: #fff; @@ -21,46 +27,77 @@ align-items: center; gap: 10px; padding: 12px 24px; - background: #f8f9fa; - border-bottom: 1px solid #e8eaed; + background: $gray-50; + border-bottom: 1px solid $gray-200; i { font-size: 14px; - color: #0ea5e9; + color: $accent; } - h3 { + span { margin: 0; font-size: 14px; font-weight: 600; - color: #202124; + 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%; - - &--full { - grid-column: 1 / -1; - } } -.rdpc-checkbox-row { +.rdpc-alert { display: flex; align-items: center; - padding: 4px 0; -} + justify-content: space-between; + gap: 24px; + padding: 18px 24px 24px; + border-top: 1px solid $border; + background: #f8fafc; -@media (max-width: 600px) { - .rdpc-grid { - grid-template-columns: 1fr; + @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/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.ts b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.ts index 50412b965..7beb38296 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.ts +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.ts @@ -6,12 +6,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, OnInit, Input, inject, DestroyRef } from '@angular/core'; +import { Component, OnInit, Input, inject } from '@angular/core'; import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; import { MatTooltip } from '@angular/material/tooltip'; -import { MatCheckbox } from '@angular/material/checkbox'; +import { MatRadioGroup, MatRadioButton } from '@angular/material/radio'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'mifosx-recurring-deposit-product-currency-step', @@ -20,18 +19,23 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; imports: [ ...STANDALONE_SHARED_IMPORTS, MatTooltip, - MatCheckbox + MatRadioGroup, + MatRadioButton ] }) export class RecurringDepositProductCurrencyStepComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); - private destroyRef = inject(DestroyRef); @Input() recurringDepositProductsTemplate: any; recurringDepositProductCurrencyForm: UntypedFormGroup; currencyData: any; + decimalPlacesData = [ + 2, + 0, + 3 + ]; constructor() { this.createrecurringDepositProductCurrencyForm(); @@ -41,24 +45,17 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit { this.currencyData = this.recurringDepositProductsTemplate.currencyOptions; this.recurringDepositProductCurrencyForm.patchValue({ - currencyCode: this.recurringDepositProductsTemplate.currency?.code || this.currencyData[0].code, - digitsAfterDecimal: this.recurringDepositProductsTemplate.digitsAfterDecimal ?? '', - setMultiples: !!this.recurringDepositProductsTemplate.inMultiplesOf, - inMultiplesOf: this.recurringDepositProductsTemplate.inMultiplesOf ?? '' + currencyCode: this.recurringDepositProductsTemplate.currency?.code || this.currencyData?.[0]?.code || '', + digitsAfterDecimal: + this.recurringDepositProductsTemplate.digitsAfterDecimal ?? + this.recurringDepositProductsTemplate.currency?.decimalPlaces ?? + 2, + dormancyAlert: this.recurringDepositProductsTemplate.dormancyAlert ?? false, + balanceSwapOption: this.recurringDepositProductsTemplate.balanceSwapOption ?? false, + msme: this.recurringDepositProductsTemplate.msme ?? false, + nominationFacility: this.recurringDepositProductsTemplate.nominationFacility ?? false, + autoDebitFacility: this.recurringDepositProductsTemplate.autoDebitFacility ?? false }); - - this.setupConditionalValidation(); - - // Apply initial validators based on the patched setMultiples value - const inMultiplesOfControl = this.recurringDepositProductCurrencyForm.get('inMultiplesOf'); - const setMultiplesControl = this.recurringDepositProductCurrencyForm.get('setMultiples'); - if (setMultiplesControl?.value) { - inMultiplesOfControl?.setValidators([ - Validators.required, - Validators.min(1) - ]); - inMultiplesOfControl?.updateValueAndValidity(); - } } createrecurringDepositProductCurrencyForm() { @@ -68,46 +65,30 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit { Validators.required ], digitsAfterDecimal: [ - '', + 2, [ Validators.required, Validators.min(0) ] ], - setMultiples: [false], - inMultiplesOf: [''] - }); - } - - setupConditionalValidation() { - const inMultiplesOfControl = this.recurringDepositProductCurrencyForm.get('inMultiplesOf'); - const setMultiplesControl = this.recurringDepositProductCurrencyForm.get('setMultiples'); - - setMultiplesControl?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((checked) => { - if (checked) { - inMultiplesOfControl?.setValidators([ - Validators.required, - Validators.min(1) - ]); - } else { - inMultiplesOfControl?.clearValidators(); - inMultiplesOfControl?.setValue(''); - } - inMultiplesOfControl?.updateValueAndValidity(); + dormancyAlert: [false], + balanceSwapOption: [false], + msme: [false], + nominationFacility: [false], + autoDebitFacility: [false] }); } get recurringDepositProductCurrency() { const formValue = this.recurringDepositProductCurrencyForm.value; - const result: any = { + return { currencyCode: formValue.currencyCode, - digitsAfterDecimal: formValue.digitsAfterDecimal + digitsAfterDecimal: formValue.digitsAfterDecimal, + dormancyAlert: formValue.dormancyAlert, + balanceSwapOption: formValue.balanceSwapOption, + msme: formValue.msme, + nominationFacility: formValue.nominationFacility, + autoDebitFacility: formValue.autoDebitFacility }; - - if (formValue.inMultiplesOf !== '' && formValue.inMultiplesOf !== null && formValue.inMultiplesOf !== undefined) { - result.inMultiplesOf = formValue.inMultiplesOf; - } - - return result; } } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.html index d2d651a6e..7e5f20a46 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.html @@ -10,7 +10,7 @@
-

{{ 'labels.inputs.Product Name' | translate }}

+

{{ 'labels.inputs.Product Name' | translate }} & {{ 'labels.inputs.Description' | translate }}

@@ -57,13 +57,67 @@ cdkTextareaAutosize cdkAutosizeMinRows="2" > - @if (recurringDepositProductDetailsForm.controls.description.hasError('required')) { - - {{ 'labels.inputs.Description' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - }
+ +
+
+ +

Product Classification

+
+ +
+ + Account Type + + @for (accountType of accountTypeData; track accountType) { + {{ accountType }} + } + + Account Type is required + + + + Customer Segment + + @for (customerSegment of customerSegmentData; track customerSegment) { + {{ customerSegment }} + } + + + + + Product Code + + + + + Effective To + + + + + + + End Date + + + + + + + Introducer Required + + @for (option of yesNoOptions; track option.value) { + {{ option.label }} + } + + + +
+ Share Linking Enabled +
+
+
diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.scss b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.scss index 543151363..fd6cd1cb3 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.scss +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.scss @@ -6,8 +6,14 @@ * 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 #e2e8f0; + border: 1px solid $border; border-radius: 10px; overflow: hidden; background: #fff; @@ -21,19 +27,19 @@ align-items: center; gap: 10px; padding: 12px 24px; - background: #f8f9fa; - border-bottom: 1px solid #e8eaed; + background: $gray-50; + border-bottom: 1px solid $gray-200; i { font-size: 14px; - color: #0ea5e9; + color: $accent; } h3 { margin: 0; font-size: 14px; font-weight: 600; - color: #202124; + color: $gray-900; } } } @@ -43,6 +49,22 @@ 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 { @@ -53,8 +75,8 @@ } } -@media (max-width: 600px) { - .rdpd-grid { - grid-template-columns: 1fr; - } +.rdpd-checkbox-row { + display: flex; + align-items: center; + min-height: 56px; } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.ts b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.ts index 1aaa3dac7..3f2cd558c 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.ts +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.ts @@ -6,10 +6,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, OnInit, Input, inject } from '@angular/core'; +import { Component, OnInit, OnChanges, Input, SimpleChanges, inject } 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({ @@ -22,24 +24,34 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; CdkTextareaAutosize ] }) -export class RecurringDepositProductDetailsStepComponent implements OnInit { +export class RecurringDepositProductDetailsStepComponent implements OnInit, OnChanges { private formBuilder = inject(UntypedFormBuilder); + private dateUtils = inject(Dates); + private settingsService = inject(SettingsService); @Input() recurringDepositProductsTemplate: any; recurringDepositProductDetailsForm: UntypedFormGroup; + accountTypeData: string[] = []; + customerSegmentData: string[] = []; + yesNoOptions: Array<{ value: boolean; label: string }> = [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' } + ]; constructor() { this.createrecurringDepositProductDetailsForm(); } ngOnInit() { - if (!(this.recurringDepositProductsTemplate === undefined) && this.recurringDepositProductsTemplate.id) { - this.recurringDepositProductDetailsForm.patchValue({ - name: this.recurringDepositProductsTemplate.name, - shortName: this.recurringDepositProductsTemplate.shortName, - description: this.recurringDepositProductsTemplate.description - }); + if (this.recurringDepositProductsTemplate) { + this.applyTemplate(this.recurringDepositProductsTemplate); + } + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.recurringDepositProductsTemplate?.currentValue) { + this.applyTemplate(changes.recurringDepositProductsTemplate.currentValue); } } @@ -53,14 +65,70 @@ export class RecurringDepositProductDetailsStepComponent implements OnInit { '', Validators.required ], - description: [ - '', + description: [''], + accountType: [ + null, Validators.required - ] + ], + customerSegment: [null], + productCode: [''], + effectiveTo: [''], + endDate: [''], + introducerRequired: [false], + shareLinkingEnabled: [false] + }); + } + + 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); + } + + private applyTemplate(template: any) { + this.accountTypeData = this.getOptionNames(template.accountTypeOptions); + this.customerSegmentData = this.getOptionNames(template.customerSegmentOptions); + + const isEditing = !!template.id; + this.recurringDepositProductDetailsForm.patchValue({ + name: isEditing ? template.name : template.name || '', + shortName: isEditing ? template.shortName : template.shortName || '', + description: template.description || '', + accountType: template.accountType ?? this.accountTypeData[0] ?? null, + customerSegment: template.customerSegment ?? this.customerSegmentData[0] ?? null, + productCode: template.productCode ?? '', + effectiveTo: this.parseDate(template.effectiveTo), + endDate: this.parseDate(template.endDate), + introducerRequired: template.introducerRequired === true || template.introducerRequired === 'Yes', + shareLinkingEnabled: !!template.shareLinkingEnabled }); } get recurringDepositProductDetails() { - return this.recurringDepositProductDetailsForm.value; + const formValue = { ...this.recurringDepositProductDetailsForm.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) || ''; + } + + return formValue; } } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.html index 1f75f427d..df813724b 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.html @@ -78,7 +78,13 @@ {{ 'labels.inputs.End Date' | translate }} - + @@ -91,7 +97,13 @@
- @@ -105,11 +117,7 @@ @if (chart.value.chartSlabs.length !== 0) {
- +
@@ -211,7 +212,9 @@ @@ -233,6 +236,89 @@ } + + @if (currentAccounts?.length > 0) { +
{{ 'labels.inputs.Period' | translate }} @@ -184,7 +192,9 @@ >

@@ -205,25 +215,33 @@
- + - + - + - + - + diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.scss b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.scss index 09b447b0c..e72e64e10 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.scss +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.scss @@ -143,7 +143,7 @@ margin-bottom: 12px; } -@media (max-width: 600px) { +@media (width <= 600px) { .rdpi-grid { grid-template-columns: 1fr; } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.html index b8d5f706e..3c9a72fa5 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.html @@ -300,7 +300,9 @@ >

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

@@ -319,7 +321,9 @@ -
+ - + - + - + - - - - - - v.isActive) ?? this._rdInterestRates[0] ?? null; @@ -80,6 +84,18 @@ export class RecurringDepositsAccountPreviewStepComponent implements OnChanges { /** Output the submit action */ @Output() submitEvent = new EventEmitter(); + get currencyName(): string { + return this.recurringDepositsAccountProductTemplate?.currency?.name || '—'; + } + + get decimalPlaces(): string | number { + return this.recurringDepositsAccountProductTemplate?.currency?.decimalPlaces ?? '—'; + } + + get inMultiplesOf(): string | number { + return this.recurringDepositsAccountProductTemplate?.currency?.inMultiplesOf ?? '—'; + } + _rdInterestRates: any[] = []; _activeRdVersion: any = null; diff --git a/src/app/savings/create-savings-account/create-savings-account.component.html b/src/app/savings/create-savings-account/create-savings-account.component.html index 23f9f93aa..edd2aea77 100644 --- a/src/app/savings/create-savings-account/create-savings-account.component.html +++ b/src/app/savings/create-savings-account/create-savings-account.component.html @@ -92,6 +92,8 @@ [savingsAccountTemplate]="savingsAccountTemplate" [savingsAccountTermsForm]="savingsAccountTermsForm" [savingsAccount]="savingsAccount" + [savingsAccountDigital]="savingsAccountDigital" + [savingsAccountNominee]="savingsAccountNominee" (submitEvent)="submit()" > diff --git a/src/app/savings/create-savings-account/create-savings-account.component.ts b/src/app/savings/create-savings-account/create-savings-account.component.ts index fd733bbae..a84dd5dcb 100644 --- a/src/app/savings/create-savings-account/create-savings-account.component.ts +++ b/src/app/savings/create-savings-account/create-savings-account.component.ts @@ -15,6 +15,7 @@ import { FormGroup } from '@angular/forms'; import { SavingsAccountDetailsStepComponent } from '../savings-account-stepper/savings-account-details-step/savings-account-details-step.component'; import { SavingsAccountCurrencyStepComponent } from '../savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component'; import { SavingsAccountDigitalStepComponent } from '../savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component'; +import { SavingsAccountSettingsStepComponent } from '../savings-account-stepper/savings-account-settings-step/savings-account-settings-step.component'; /** Custom Services */ import { SavingsService } from '../savings.service'; @@ -69,6 +70,12 @@ export class CreateSavingsAccountComponent { @ViewChild(SavingsAccountDetailsStepComponent, { static: true }) savingsAccountDetailsStep: SavingsAccountDetailsStepComponent; + @ViewChild(SavingsAccountDigitalStepComponent) + savingsAccountDigitalStep: SavingsAccountDigitalStepComponent; + + @ViewChild(SavingsAccountCurrencyStepComponent) + savingsAccountCurrencyStep: SavingsAccountCurrencyStepComponent; + constructor() { this.route.data.subscribe((data: { savingsAccountTemplate: any }) => { this.savingsAccountTemplate = data.savingsAccountTemplate; @@ -94,6 +101,14 @@ export class CreateSavingsAccountComponent { }; } + get savingsAccountDigital() { + return this.savingsAccountDigitalStep?.savingsAccountDigital ?? null; + } + + get savingsAccountNominee() { + return this.savingsAccountCurrencyStep?.nomineeData ?? null; + } + submit() { const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; diff --git a/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.html b/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.html index 9dd007828..29231e681 100644 --- a/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.html +++ b/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.html @@ -35,8 +35,9 @@ {{ savingsAccountProductTemplate?.dormancyAlert ? 'Yes' : 'No' }} - - + @if (savingsAccountProductTemplate?.dormancyAlert) { + + }
@@ -47,7 +48,6 @@ {{ savingsAccountProductTemplate?.nominationFacility ? 'Yes' : 'No' }}
- @if (savingsAccountProductTemplate?.nominationFacility) { } diff --git a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts index 8585f3dac..7bdaefa68 100644 --- a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts +++ b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts @@ -59,6 +59,8 @@ export class SavingsAccountDetailsStepComponent implements OnInit { savingsProductSelected = false; introducerId: number | null = null; + introducerName: string | null = null; + introducerCif: string | null = null; /** Savings Account Template with product data */ @Output() savingsAccountProductTemplate = new EventEmitter(); @@ -168,14 +170,18 @@ export class SavingsAccountDetailsStepComponent implements OnInit { /** * Returns savings account form value. */ - onIntroducerChange(event: { clientId: number; displayName: string } | null): void { + onIntroducerChange(event: { clientId: number; displayName: string; cifNumber?: string } | null): void { this.introducerId = event?.clientId ?? null; + this.introducerName = event?.displayName ?? null; + this.introducerCif = event?.cifNumber ?? null; } get savingsAccountDetails() { const value = this.savingsAccountDetailsForm.getRawValue(); if (this.introducerId) { value.introducerId = this.introducerId; + value.introducerName = this.introducerName; + value.introducerCif = this.introducerCif; } return value; } diff --git a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.html b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.html index 70337bb64..f523e4bd4 100644 --- a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.html +++ b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.html @@ -7,9 +7,11 @@ -->
+
- {{ 'labels.heading.Details' | translate }} + + Account Details
@@ -37,25 +39,27 @@ } @else { - {{ 'labels.inputs.Unassigned' | translate }} + Unassigned }
+ @if ( savingsAccountProductTemplate?.accountType || savingsAccountProductTemplate?.customerSegment || savingsAccountProductTemplate?.productCode || + savingsAccountProductTemplate?.previewAccountNo || savingsAccountProductTemplate?.effectiveDate || - savingsAccountProductTemplate?.endDate || - savingsAccountProductTemplate?.introducerRequired !== undefined || - savingsAccountProductTemplate?.shareLinkingEnabled !== undefined + savingsAccountProductTemplate?.endDate ) {
+ Product Classification + From Product
@@ -74,9 +78,15 @@ @if (savingsAccountProductTemplate?.productCode) {
Product Code - {{ savingsAccountProductTemplate?.productCode }} + {{ savingsAccountProductTemplate?.productCode }}
} +
+ Account No + {{ + savingsAccountProductTemplate?.previewAccountNo || savingsAccountTemplate?.accountNo || 'Auto-generated' + }} +
@if (savingsAccountProductTemplate?.effectiveDate) {
Effective Date @@ -89,145 +99,285 @@ {{ savingsAccountProductTemplate?.endDate | dateFormat }}
} - @if ( - savingsAccountProductTemplate?.introducerRequired !== undefined && - savingsAccountProductTemplate?.introducerRequired !== null - ) { -
- Introducer Required - - {{ savingsAccountProductTemplate?.introducerRequired | yesNo }} - -
- } - @if ( - savingsAccountProductTemplate?.shareLinkingEnabled !== undefined && - savingsAccountProductTemplate?.shareLinkingEnabled !== null - ) { -
- Share Linking Enabled - - {{ savingsAccountProductTemplate?.shareLinkingEnabled | yesNo }} - -
- } +
+ Introducer Required + + {{ savingsAccountProductTemplate?.introducerRequired | yesNo }} + +
+
+ Share Linking Enabled + + {{ savingsAccountProductTemplate?.shareLinkingEnabled | yesNo }} + +
} + + @if (savingsAccount?.introducerId) { +
+
+ + Introducer Details +
+
+
+
+ Name + {{ savingsAccount?.introducerName || '—' }} +
+ @if (savingsAccount?.introducerCif) { +
+ CIF No + {{ savingsAccount.introducerCif }} +
+ } +
+ Client ID + {{ savingsAccount.introducerId }} +
+
+
+
+ } + +
- {{ 'labels.heading.Currency' | translate }} + + Currency & Account Flags
- {{ 'labels.inputs.Currency' | translate }} - {{ savingsAccountTermsForm?.get('currencyCode')?.value }} + Currency + {{ currencyName }}
- {{ 'labels.inputs.Decimal Places' | translate }} - {{ savingsAccountProductTemplate?.currency?.decimalPlaces }} + Decimal Places + {{ decimalPlaces }}
- {{ 'labels.inputs.Currency In Multiples Of' | translate }} - {{ savingsAccountProductTemplate?.currency?.inMultiplesOf }} + Currency In Multiples Of + {{ inMultiplesOf }} +
+
+ Dormancy Alert + + {{ savingsAccountProductTemplate?.dormancyAlert | yesNo }} + +
+
+ Nomination Facility + + {{ savingsAccountProductTemplate?.nominationFacility | yesNo }} +
- - @if ( - savingsAccountProductTemplate?.dormancyAlert !== undefined && - savingsAccountProductTemplate?.dormancyAlert !== null - ) { -
- Dormancy Alert - - {{ savingsAccountProductTemplate?.dormancyAlert | yesNo }} - -
- } - - @if ( - savingsAccountProductTemplate?.balanceSwapOption !== undefined && - savingsAccountProductTemplate?.balanceSwapOption !== null - ) { -
- Balance Swap Option - - {{ savingsAccountProductTemplate?.balanceSwapOption | yesNo }} - -
- } - - @if (savingsAccountProductTemplate?.msme !== undefined && savingsAccountProductTemplate?.msme !== null) { -
- MSME - - {{ savingsAccountProductTemplate?.msme | yesNo }} - -
- } - - @if ( - savingsAccountProductTemplate?.nominationFacility !== undefined && - savingsAccountProductTemplate?.nominationFacility !== null - ) { -
- Nomination Facility - - {{ savingsAccountProductTemplate?.nominationFacility | yesNo }} - -
- } - - @if ( - savingsAccountProductTemplate?.autoDebitFacility !== undefined && - savingsAccountProductTemplate?.autoDebitFacility !== null - ) { -
- Auto Debit Facility - - {{ savingsAccountProductTemplate?.autoDebitFacility | yesNo }} - -
- }
- @if (hasDigitalSection) { + + @if ( + savingsAccountProductTemplate?.dormancyAlert || savingsAccountProductTemplate?.isDormancyTrackingActiveFromProduct + ) {
- Digital + + Dormancy Monitoring + + {{ savingsAccountProductTemplate?.isDormancyTrackingActiveFromProduct ? 'Active' : 'Inactive' }} + +
+
+
+
+
+
+ Inactive + + @if (savingsAccountProductTemplate?.daysToInactiveFromProduct != null) { + After {{ savingsAccountProductTemplate.daysToInactiveFromProduct }} days of no transaction + } @else { + Not configured + } + +
+
+
+
+
+
+ Dormant + + @if (savingsAccountProductTemplate?.daysToDormancyFromProduct != null) { + {{ savingsAccountProductTemplate.daysToDormancyFromProduct }} days after inactive + } @else { + Not configured + } + +
+
+
+
+
+
+ Escheat + + @if (savingsAccountProductTemplate?.daysToEscheatFromProduct != null) { + {{ savingsAccountProductTemplate.daysToEscheatFromProduct }} days after dormant + } @else { + Not configured + } + +
+
+
+
+ + Alert + {{ savingsAccountProductTemplate?.dormancyAlert ? 'Enabled' : 'Disabled' }} + + + Tracking + {{ savingsAccountProductTemplate?.isDormancyTrackingActiveFromProduct ? 'Active' : 'Inactive' }} + +
+
+
+ } + + + @if (savingsAccountNominee) { +
+
+ + Nominee Details + + {{ savingsAccountNominee?.mode === 'linked' ? 'Linked Client' : 'Manual Entry' }} +
- @for (facility of digitalFacilityLabels; track facility.key) { + +
+ Name + {{ + savingsAccountNominee?.fullName || savingsAccountNominee?.displayName || '—' + }} +
+ @if (savingsAccountNominee?.cifNumber) {
- {{ facility.label }} + CIF No + {{ savingsAccountNominee.cifNumber }} +
+ } +
+ Relationship + {{ savingsAccountNominee?.relationship || '—' }} +
+
+ Share % + {{ savingsAccountNominee?.nomineeShare ?? '—' }}% +
+ @if (savingsAccountNominee?.dateOfBirth) { +
+ Date of Birth + {{ savingsAccountNominee?.dateOfBirth | dateFormat }} +
+ } + @if (savingsAccountNominee?.nationality) { +
+ Nationality + {{ savingsAccountNominee?.nationality }} +
+ } + @if (savingsAccountNominee?.mobileNo) { +
+ Mobile No + {{ savingsAccountNominee?.mobileNo }} +
+ } + @if (savingsAccountNominee?.aadhaarNumber) { +
+ Aadhaar No + {{ savingsAccountNominee?.aadhaarNumber }} +
+ } + @if (savingsAccountNominee?.panNumber) { +
+ PAN No + {{ savingsAccountNominee?.panNumber }} +
+ } + @if (savingsAccountNominee?.isDependent) { +
+ Dependent + Yes +
+ } + @if (savingsAccountNominee?.guardianName) { +
+ Guardian Name + {{ savingsAccountNominee?.guardianName }} +
+ } + @if (savingsAccountNominee?.guardianRelationship) { +
+ Guardian Relationship + {{ savingsAccountNominee?.guardianRelationship }} +
+ } + @if (savingsAccountNominee?.remarks) { +
+ Remarks + {{ savingsAccountNominee?.remarks }} +
+ } +
+
+
+ } + + + @if (hasDigitalSection) { +
+
+ + Digital Facilities + User Configured +
+
+
+ @for (facility of digitalFacilityLabels; track facility.key) { +
+ {{ facility.label }} - {{ digitalPreview?.accountFacilityConfig?.[facility.key] | yesNo }} + {{ digitalFacilityConfig?.[facility.key] ? 'Enabled' : 'Disabled' }}
} @@ -236,9 +386,11 @@
} + @if (activeClientMembers) {
+ {{ 'labels.heading.Client Members' | translate }}
diff --git a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.scss b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.scss index bec5645fb..25e0a4e09 100644 --- a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.scss +++ b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.scss @@ -9,29 +9,59 @@ .preview-wrapper { display: flex; flex-direction: column; - gap: 1.25rem; + gap: 1rem; padding: 0.5rem 0; } .preview-section { - border: 1px solid #e0e0e0; - border-radius: 8px; + border: 1px solid #e2e8f0; + border-radius: 10px; overflow: hidden; background: #fff; - box-shadow: 0 1px 3px rgb(0 0 0 / 6%); + box-shadow: 0 1px 4px rgb(0 0 0 / 5%); &__header { - background: #f5f5f5; - border-bottom: 1px solid #e0e0e0; + display: flex; + align-items: center; + gap: 8px; + background: #f8fafc; + border-bottom: 1px solid #e2e8f0; padding: 0.6rem 1rem; } + &__icon { + font-size: 13px; + color: #0ea5e9; + width: 16px; + text-align: center; + } + &__title { - font-size: 0.95rem; - font-weight: 600; - color: #333; + font-size: 0.82rem; + font-weight: 700; + color: #1e293b; text-transform: uppercase; - letter-spacing: 0.04em; + letter-spacing: 0.06em; + flex: 1; + } + + &__badge { + display: inline-flex; + align-items: center; + padding: 2px 10px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + background: #dbeafe; + color: #1d4ed8; + border: 1px solid #bfdbfe; + text-transform: uppercase; + + &--digital { + background: #fef3c7; + color: #92400e; + border-color: #fde68a; + } } } @@ -41,7 +71,7 @@ .preview-grid { display: grid; - gap: 0.75rem 1.5rem; + gap: 0.85rem 1.5rem; &--three { grid-template-columns: repeat(3, 1fr); @@ -59,36 +89,229 @@ .preview-field { display: flex; flex-direction: column; - gap: 0.15rem; + gap: 0.2rem; } .preview-label { - font-size: 0.72rem; - color: #888; - font-weight: 500; + font-size: 0.68rem; + color: #94a3b8; + font-weight: 600; text-transform: uppercase; - letter-spacing: 0.04em; + letter-spacing: 0.05em; } .preview-value { - font-size: 0.92rem; - color: #222; + font-size: 0.9rem; + color: #1e293b; font-weight: 500; } +.preview-muted { + color: #94a3b8; + font-style: italic; +} + +.preview-mono { + font-family: monospace; + font-size: 0.88rem; + color: #334155; +} + .preview-badge { display: inline-block; - padding: 0.1rem 0.55rem; + padding: 0.15rem 0.6rem; border-radius: 12px; - font-size: 0.78rem; + font-size: 0.75rem; font-weight: 600; - background: #ffebee; - color: #c62828; + background: #fef2f2; + color: #dc2626; width: fit-content; &--yes { - background: #e8f5e9; - color: #2e7d32; + background: #f0fdf4; + color: #15803d; + } + + &--warn { + background: #fef3c7; + color: #92400e; + } +} + +// Digital facilities grid +.preview-digital-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; + + @media (width <= 900px) { + grid-template-columns: repeat(3, 1fr); + } + + @media (width <= 600px) { + grid-template-columns: repeat(2, 1fr); + } +} + +.preview-digital-card { + display: flex; + flex-direction: column; + gap: 4px; + padding: 10px 12px; + border: 1.5px solid #e2e8f0; + border-radius: 8px; + background: #f8fafc; + transition: all 0.12s; + + &--on { + border-color: #86efac; + background: #f0fdf4; + } +} + +.preview-digital-label { + font-size: 11px; + font-weight: 600; + color: #475569; +} + +.preview-digital-status { + font-size: 11px; + font-weight: 700; + color: #dc2626; + + &--on { + color: #15803d; + } +} + +// Section badge variants +.preview-section__badge--active { + background: #dcfce7 !important; + color: #15803d !important; + border-color: #bbf7d0 !important; +} + +// ── Dormancy Monitoring ─────────────────────────────────────────── +.preview-dormancy-timeline { + display: flex; + align-items: flex-start; + gap: 0; + margin-bottom: 14px; +} + +.preview-dorm-step { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + flex: 1; + + &__dot { + width: 36px; + height: 36px; + border-radius: 50%; + border: 3px solid; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + &--inactive { + border-color: #f59e0b; + background: #fffbeb; + + &::after { + content: ''; + width: 10px; + height: 10px; + border-radius: 50%; + background: #f59e0b; + } + } + + &--dormant { + border-color: #ef4444; + background: #fef2f2; + + &::after { + content: ''; + width: 10px; + height: 10px; + border-radius: 50%; + background: #ef4444; + } + } + + &--escheat { + border-color: #7c3aed; + background: #f5f3ff; + + &::after { + content: ''; + width: 10px; + height: 10px; + border-radius: 50%; + background: #7c3aed; + } + } + } + + &__body { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 2px; + } + + &__label { + font-size: 12px; + font-weight: 700; + color: #1e293b; + } + + &__days { + font-size: 10.5px; + color: #64748b; + line-height: 1.3; + } +} + +.preview-dorm-connector { + flex: 0 0 40px; + height: 3px; + background: linear-gradient(90deg, #e2e8f0, #cbd5e1); + margin-top: 16px; + border-radius: 2px; +} + +.preview-dorm-flags { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.preview-dorm-flag { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 4px 12px; + border-radius: 20px; + font-size: 11.5px; + font-weight: 600; + background: #f1f5f9; + color: #94a3b8; + border: 1px solid #e2e8f0; + + i { + font-size: 11px; + } + + &--on { + background: #f0fdf4; + color: #15803d; + border-color: #bbf7d0; } } diff --git a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.ts index aae881819..f29663102 100644 --- a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.ts +++ b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.ts @@ -88,6 +88,10 @@ export class SavingsAccountPreviewStepComponent implements OnChanges { @Input() savingsAccountTermsForm: any; /** Savings Account */ @Input() savingsAccount: any; + /** Digital facilities — user-modified values from digital step */ + @Input() savingsAccountDigital: any; + /** Nominee data from currency step */ + @Input() savingsAccountNominee: any; /** active Client Members in case of GSIM Account */ @Input() activeClientMembers?: any; @@ -118,15 +122,33 @@ export class SavingsAccountPreviewStepComponent implements OnChanges { } } - get digitalPreview(): any { - return this.savingsAccountProductTemplate || {}; + /** Returns user-modified digital config (from digital step), falling back to product template */ + get digitalFacilityConfig(): any { + return ( + this.savingsAccountDigital?.accountFacilityConfig ?? + this.savingsAccountProductTemplate?.accountFacilityConfig ?? + null + ); } get hasDigitalSection(): boolean { - return !!( - this.digitalPreview?.accountFacilityConfig || - this.digitalPreview?.accountFacilityLimits?.length || - this.digitalPreview?.depositLimitKyc + return !!this.digitalFacilityConfig; + } + + /** Currency info from product template */ + get currencyName(): string { + return ( + this.savingsAccountProductTemplate?.currency?.name || + this.savingsAccountTermsForm?.get?.('currencyCode')?.value || + '—' ); } + + get decimalPlaces(): string | number { + return this.savingsAccountProductTemplate?.currency?.decimalPlaces ?? '—'; + } + + get inMultiplesOf(): string | number { + return this.savingsAccountProductTemplate?.currency?.inMultiplesOf ?? '—'; + } } diff --git a/src/app/shared/introducer-search/introducer-search.component.ts b/src/app/shared/introducer-search/introducer-search.component.ts index fa0410d78..703b691fa 100644 --- a/src/app/shared/introducer-search/introducer-search.component.ts +++ b/src/app/shared/introducer-search/introducer-search.component.ts @@ -31,7 +31,7 @@ export class IntroducerSearchComponent { private clientsService = inject(ClientsService); private sanitizer = inject(DomSanitizer); - @Output() introducerChange = new EventEmitter<{ clientId: number; displayName: string } | null>(); + @Output() introducerChange = new EventEmitter<{ clientId: number; displayName: string; cifNumber?: string } | null>(); entryMode: 'linked' = 'linked'; searchText = ''; @@ -81,7 +81,11 @@ export class IntroducerSearchComponent { this.linkedProfileImage = null; this.linkedKycStatus = null; this.fetchLinkedClientData(client.id); - this.introducerChange.emit({ clientId: client.id, displayName: client.displayName }); + this.introducerChange.emit({ + clientId: client.id, + displayName: client.displayName, + cifNumber: client.cifNumber || client.accountNumber || '' + }); } removeClient(): void { diff --git a/src/app/shared/nominee-form/nominee-form.component.ts b/src/app/shared/nominee-form/nominee-form.component.ts index 594692183..d016d8a6b 100644 --- a/src/app/shared/nominee-form/nominee-form.component.ts +++ b/src/app/shared/nominee-form/nominee-form.component.ts @@ -36,6 +36,7 @@ export interface NomineeData { // Linked client fields clientId?: number; displayName?: string; + cifNumber?: string; } const RELATIONSHIPS = [ @@ -98,6 +99,7 @@ export class NomineeFormComponent implements OnInit { clientDob = ''; clientAadhaar = ''; clientPan = ''; + clientCifNumber = ''; linkedRelationship = ''; linkedShare = 100; @@ -177,7 +179,12 @@ export class NomineeFormComponent implements OnInit { this.searchResults = []; this.searchText = ''; this.fetchLinkedClientData(client.id); - this.nomineeChange.emit({ mode: 'linked', clientId: client.id, displayName: client.displayName }); + this.nomineeChange.emit({ + mode: 'linked', + clientId: client.id, + displayName: client.displayName, + cifNumber: client.accountNo || client.cifNumber || '' + }); } removeLinked(): void { @@ -191,6 +198,7 @@ export class NomineeFormComponent implements OnInit { mode: 'linked', clientId: this.selectedClient.id, displayName: this.selectedClient.displayName, + cifNumber: this.selectedClient.accountNo || this.selectedClient.cifNumber || '', relationship: rel, nomineeShare: this.linkedShare }); @@ -204,6 +212,7 @@ export class NomineeFormComponent implements OnInit { this.clientDob = ''; this.clientAadhaar = ''; this.clientPan = ''; + this.clientCifNumber = ''; this.searchText = ''; this.searchResults = []; this.linkedRelationship = ''; diff --git a/src/environments/.env.ts b/src/environments/.env.ts index d1086eaaa..77fd662d8 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -5,12 +5,13 @@ * 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/. */ + // IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN! /* tslint:disable */ export default { 'mifos_x': { - 'version': '260425', - 'hash': 'fbaec185' + 'version': '260427', + 'hash': '5fb25edc' }, 'allow_switching_backend_instance': true }; diff --git a/src/main.scss b/src/main.scss index 9fda56e50..c10b82bd2 100644 --- a/src/main.scss +++ b/src/main.scss @@ -957,3 +957,51 @@ mat-card { border-radius: 16px !important; } } + +/* ─── Add Beneficiary Dialog Panel ─── */ +.abd-dialog-panel { + .mat-mdc-dialog-container { + padding: 0; + border-radius: 14px; + overflow: hidden; + } + + .mat-mdc-dialog-surface { + padding: 0 !important; + border-radius: 14px !important; + } + + .mat-mdc-dialog-content { + padding: 0 20px !important; + max-height: 72vh; + } + + .mat-mdc-dialog-actions { + padding: 0 !important; + margin: 0 !important; + } +} + +/* ─── Add Related Client Dialog Panel ─── */ +.arc-dialog-panel { + .mat-mdc-dialog-container { + padding: 0; + border-radius: 14px; + overflow: hidden; + } + + .mat-mdc-dialog-surface { + padding: 0 !important; + border-radius: 14px !important; + } + + .mat-mdc-dialog-content { + padding: 0 20px !important; + max-height: 70vh; + } + + .mat-mdc-dialog-actions { + padding: 0 !important; + margin: 0 !important; + } +} From f917467b68abb2ba9643e19e69cf91343095fa01 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Mon, 27 Apr 2026 15:48:05 +0530 Subject: [PATCH 09/12] add editable Account Type and Customer Segment dropdowns in product classification Co-Authored-By: Claude Sonnet 4.6 --- ...urrent-account-details-step.component.html | 12 +++++++++-- .../current-account-details-step.component.ts | 15 ++++++++++++++ ...posits-account-details-step.component.html | 12 +++++++++-- ...deposits-account-details-step.component.ts | 20 +++++++++++++++++-- ...avings-account-details-step.component.html | 12 +++++++++-- .../savings-account-details-step.component.ts | 20 +++++++++++++++++-- src/environments/.env.ts | 3 +-- 7 files changed, 82 insertions(+), 12 deletions(-) diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html index 68adf30a0..cc480140b 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html @@ -74,12 +74,20 @@
Account Type - + + @for (opt of accountTypeOptions; track opt) { + {{ opt }} + } + Customer Segment - + + @for (opt of customerSegmentOptions; track opt) { + {{ opt }} + } + diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts index 8f1523d0e..205a267cc 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts @@ -116,6 +116,10 @@ export class CurrentAccountDetailsStepComponent implements OnInit { /** Local copy of product template for displaying classification info */ productTemplate: any = null; + accountTypeOptions: string[] = []; + customerSegmentOptions: string[] = []; + selectedAccountType: string | null = null; + selectedCustomerSegment: string | null = null; constructor() { this.createCurrentAccountDetailsForm(); @@ -192,6 +196,10 @@ export class CurrentAccountDetailsStepComponent implements OnInit { } } this.productTemplate = response; + this.accountTypeOptions = this.extractOptions(response.accountTypeOptions, response.accountType); + this.customerSegmentOptions = this.extractOptions(response.customerSegmentOptions, response.customerSegment); + this.selectedAccountType = response.accountType ?? null; + this.selectedCustomerSegment = response.customerSegment ?? null; this.currentAccountProductTemplate.emit(response); this.fieldOfficerData = response.fieldOfficerOptions; this.currentAccountProductSelected = true; @@ -215,6 +223,13 @@ export class CurrentAccountDetailsStepComponent implements OnInit { /** * Formats template dates into the current CBS date format. */ + private extractOptions(options: any[], fallback?: string): string[] { + if (Array.isArray(options) && options.length > 0) { + return options.map((o: any) => o.value ?? o.name ?? o); + } + return fallback ? [fallback] : []; + } + formatTemplateDate(value: any): string { if (!value) { return ''; diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html index 186f2f076..30b71d413 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html @@ -74,12 +74,20 @@
Account Type - + + @for (opt of accountTypeOptions; track opt) { + {{ opt }} + } + Customer Segment - + + @for (opt of customerSegmentOptions; track opt) { + {{ opt }} + } + diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts index 50fb89362..6c484b099 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts @@ -8,7 +8,7 @@ /** Angular Imports */ import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorRef } from '@angular/core'; -import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; +import { UntypedFormGroup, UntypedFormBuilder, Validators, FormsModule } from '@angular/forms'; import { SettingsService } from 'app/settings/settings.service'; import { Dates } from 'app/core/utils/dates'; @@ -33,7 +33,8 @@ import { IntroducerSearchComponent } from 'app/shared/introducer-search/introduc MatStepperPrevious, FaIconComponent, MatStepperNext, - IntroducerSearchComponent + IntroducerSearchComponent, + FormsModule ] }) export class RecurringDepositsAccountDetailsStepComponent implements OnInit { @@ -69,6 +70,10 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { /** Local copy of product template for displaying classification info */ productTemplate: any = null; + accountTypeOptions: string[] = []; + customerSegmentOptions: string[] = []; + selectedAccountType: string | null = null; + selectedCustomerSegment: string | null = null; constructor() { this.createRecurringDepositsAccountDetailsForm(); @@ -140,6 +145,10 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { } } this.productTemplate = response; + this.accountTypeOptions = this.extractOptions(response.accountTypeOptions, response.accountType); + this.customerSegmentOptions = this.extractOptions(response.customerSegmentOptions, response.customerSegment); + this.selectedAccountType = response.accountType ?? null; + this.selectedCustomerSegment = response.customerSegment ?? null; this.recurringDepositsAccountProductTemplate.emit(response); this.fieldOfficerData = response.fieldOfficerOptions; this.isProductSelected = true; @@ -173,6 +182,13 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { * Formats template dates into the current CBS date format. * Handles backend date arrays like [2026, 4, 20] as well as strings and Date objects. */ + private extractOptions(options: any[], fallback?: string): string[] { + if (Array.isArray(options) && options.length > 0) { + return options.map((o: any) => o.value ?? o.name ?? o); + } + return fallback ? [fallback] : []; + } + formatTemplateDate(value: any): string { if (!value) { return ''; diff --git a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html index 4bce9f33d..8bf0e988a 100644 --- a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html +++ b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html @@ -78,12 +78,20 @@
Account Type - + + @for (opt of accountTypeOptions; track opt) { + {{ opt }} + } + Customer Segment - + + @for (opt of customerSegmentOptions; track opt) { + {{ opt }} + } + diff --git a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts index 7bdaefa68..f4af5679d 100644 --- a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts +++ b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts @@ -8,7 +8,7 @@ /** Angular Imports */ import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorRef } from '@angular/core'; -import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule, FormsModule } from '@angular/forms'; import { Dates } from 'app/core/utils/dates'; import { SettingsService } from 'app/settings/settings.service'; @@ -31,7 +31,8 @@ import { IntroducerSearchComponent } from 'app/shared/introducer-search/introduc MatStepperPrevious, FaIconComponent, MatStepperNext, - IntroducerSearchComponent + IntroducerSearchComponent, + FormsModule ] }) export class SavingsAccountDetailsStepComponent implements OnInit { @@ -67,6 +68,10 @@ export class SavingsAccountDetailsStepComponent implements OnInit { /** Local copy of product template for displaying classification info */ productTemplate: any = null; + accountTypeOptions: string[] = []; + customerSegmentOptions: string[] = []; + selectedAccountType: string | null = null; + selectedCustomerSegment: string | null = null; /** * Sets share account details form. @@ -127,6 +132,10 @@ export class SavingsAccountDetailsStepComponent implements OnInit { .getSavingsAccountTemplate(entityId, productId, this.savingsAccountTemplate.groupId ? true : false) .subscribe((response: any) => { this.productTemplate = response; + this.accountTypeOptions = this.extractOptions(response.accountTypeOptions, response.accountType); + this.customerSegmentOptions = this.extractOptions(response.customerSegmentOptions, response.customerSegment); + this.selectedAccountType = response.accountType ?? null; + this.selectedCustomerSegment = response.customerSegment ?? null; this.savingsAccountProductTemplate.emit(response); this.fieldOfficerData = response.fieldOfficerOptions; this.savingsProductSelected = true; @@ -145,6 +154,13 @@ export class SavingsAccountDetailsStepComponent implements OnInit { * Formats template dates into the current CBS date format. * Handles backend date arrays like [2026, 4, 20] as well as strings and Date objects. */ + private extractOptions(options: any[], fallback?: string): string[] { + if (Array.isArray(options) && options.length > 0) { + return options.map((o: any) => o.value ?? o.name ?? o); + } + return fallback ? [fallback] : []; + } + formatTemplateDate(value: any): string { if (!value) { return ''; diff --git a/src/environments/.env.ts b/src/environments/.env.ts index 77fd662d8..6dc70b65f 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -5,13 +5,12 @@ * 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/. */ - // IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN! /* tslint:disable */ export default { 'mifos_x': { 'version': '260427', - 'hash': '5fb25edc' + 'hash': '65345f99' }, 'allow_switching_backend_instance': true }; From 31b2490f09553b3fd7906508e68495bedddd5c57 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Tue, 28 Apr 2026 13:16:07 +0530 Subject: [PATCH 10/12] feat: update savings/current/rd account UI flows --- .../add-related-client-dialog.component.html | 1 + .../add-related-client-dialog.component.ts | 22 +- ...urrent-account-details-step.component.html | 546 +++++++ ...urrent-account-details-step.component.scss | 935 ++++++++++++ .../current-account-details-step.component.ts | 312 +++- .../current-accounts.service.ts | 5 + ...posits-account-details-step.component.html | 655 ++++++++- ...posits-account-details-step.component.scss | 1283 +++++++++++++++++ ...deposits-account-details-step.component.ts | 384 ++++- ...posits-account-preview-step.component.html | 2 +- ...eposit-product-details-step.component.html | 2 +- ...eposit-product-preview-step.component.html | 2 +- ...curring-deposit-general-tab.component.html | 2 +- .../create-savings-account.component.ts | 13 +- ...avings-account-details-step.component.html | 557 +++++++ ...avings-account-details-step.component.scss | 1043 ++++++++++++++ .../savings-account-details-step.component.ts | 322 ++++- ...avings-account-preview-step.component.html | 162 ++- ...avings-account-preview-step.component.scss | 72 + src/app/savings/savings.service.ts | 5 + .../introducer-search.component.ts | 2 +- .../nominee-form/nominee-form.component.ts | 2 +- 22 files changed, 6150 insertions(+), 179 deletions(-) diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.html b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.html index 29ffc269e..7325a2cca 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.html @@ -116,6 +116,7 @@ }
+ diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.ts b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.ts index 60c96d69a..bba5fd60d 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.ts +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.ts @@ -42,9 +42,23 @@ export class AddRelatedClientDialogComponent implements OnDestroy { Validators.min(1), Validators.max(100) ] - ] + ], + signingAuthority: [null], + role: [null] }); + readonly signingAuthorityOptions = [ + { value: 'SINGLE', label: 'Single' }, + { value: 'JOINTLY', label: 'Jointly' }, + { value: 'EITHER_OR_SURVIVOR', label: 'Either or Survivor' } + ]; + + readonly roleOptions = [ + { value: 'PRIMARY', label: 'Primary' }, + { value: 'JOINT', label: 'Joint' }, + { value: 'NOMINEE', label: 'Nominee' } + ]; + searchResults: any[] = []; selectedClient: any = null; isSearching = false; @@ -89,8 +103,10 @@ export class AddRelatedClientDialogComponent implements OnDestroy { this.dialogRef.close({ clientId: this.selectedClient.id, clientName: this.selectedClient.displayName, - clientCif: this.selectedClient.cifNumber || this.selectedClient.accountNumber || '', - sharePercentage: this.form.get('sharePercentage').value + clientCif: this.selectedClient.cifNumber || this.selectedClient.cifNo || '', + sharePercentage: this.form.get('sharePercentage').value, + signingAuthority: this.form.get('signingAuthority').value || null, + role: this.form.get('role').value || null }); } diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html index cc480140b..e2bd70e26 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html @@ -59,6 +59,22 @@ {{ 'labels.inputs.External ID' | translate }}
+ + + Establishment Date + + + + Date of entity / business establishment + + + + Start of Operation Date + + + + Date when operations commenced + }
@@ -139,6 +155,536 @@ } } + + @if (currentAccountProductSelected) { +
+
+
+
+ +
+
+

Status of Account

+ As per RBI guidelines — select account holder type +
+
+ RBI Compliant +
+ + + @if (clientDob) { +
+
+ cake + Date of Birth: {{ formatTemplateDate(clientDob) }} +
+
+ person_pin + Age: {{ clientAge }} years +
+ @if (clientAge !== null && clientAge < 18) { + + child_care Auto-detected: Minor + + } @else if (clientAge !== null && clientAge >= 60) { + + elderly Auto-detected: Senior Citizen + + } @else { + check_circle Age Verified + } +
+ } + + +
+ @for (opt of accountStatusOptions; track opt.value) { + + } +
+ + + @if (selectedAccountStatus === 'JOINT') { +
+
+
+ A +
+ Joint Account Holders + Add all co-applicants linked to this joint current account +
+
+ +
+ +
+
{{ 'labels.inputs.Entity Type' | translate }} + {{ 'labels.inputs.Entity Type' | translate }} + {{ incentive.entityType | find: entityTypeData : 'id' : 'value' }} {{ 'labels.inputs.Attribute Name' | translate }} + {{ 'labels.inputs.Attribute Name' | translate }} + {{ incentive.attributeName | find: attributeNameData : 'id' : 'value' }} {{ 'labels.inputs.Condition Type' | translate }} + {{ 'labels.inputs.Condition Type' | translate }} + {{ incentive.conditionType | find: conditionTypeData : 'id' : 'value' }} {{ 'labels.inputs.Attribute Value' | translate }} + {{ 'labels.inputs.Attribute Value' | translate }} + @switch (incentive.attributeName) { @case (2) { @@ -233,7 +251,9 @@ {{ incentive.attributeValue }} } @case (4) { - {{ incentive.attributeValue | find: clientTypeData : 'id' : 'name' }} + {{ + incentive.attributeValue | find: clientTypeData : 'id' : 'name' + }} } @case (5) { {{ @@ -244,7 +264,9 @@ {{ 'labels.inputs.Incentive Type' | translate }} + {{ 'labels.inputs.Incentive Type' | translate }} + {{ incentive.incentiveType | find: incentiveTypeData : 'id' : 'value' }} {{ 'labels.inputs.Attribute Name' | translate }} + {{ 'labels.inputs.Attribute Name' | translate }} + {{ incentive.attributeName @@ -331,7 +335,9 @@ {{ 'labels.inputs.Condition Type' | translate }} + {{ 'labels.inputs.Condition Type' | translate }} + {{ incentive.conditionType @@ -343,13 +349,18 @@ {{ 'labels.inputs.Attribute Value' | translate }} + {{ 'labels.inputs.Attribute Value' | translate }} + @switch (incentive.attributeName) { @case (2) { {{ incentive.attributeValue - | find: recurringDepositProductsTemplate.chartTemplate.genderOptions : 'id' : 'name' + | find + : recurringDepositProductsTemplate.chartTemplate.genderOptions + : 'id' + : 'name' }} } @case (3) { @@ -377,7 +388,9 @@ {{ 'labels.inputs.Incentive Type' | translate }} + {{ 'labels.inputs.Incentive Type' | translate }} + {{ incentive.incentiveType @@ -468,7 +481,10 @@
@if (!isNoneAccounting()) { - + } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.scss b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.scss index 375a82861..b00fc9c5c 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.scss +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.scss @@ -12,7 +12,9 @@ overflow: hidden; background: #fff; - & + & { margin-top: 16px; } + & + & { + margin-top: 16px; + } &__header { display: flex; @@ -22,11 +24,22 @@ background: #f8f9fa; border-bottom: 1px solid #e8eaed; - i { font-size: 14px; color: #0ea5e9; } - h3 { margin: 0; font-size: 14px; font-weight: 600; color: #202124; } + i { + font-size: 14px; + color: #0ea5e9; + } + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: #202124; + } } - &__body { padding: 16px 24px; } + &__body { + padding: 16px 24px; + } } .rdpp-product-name { @@ -41,7 +54,9 @@ padding: 6px 0; border-bottom: 1px solid #f1f3f4; - &:last-child { border-bottom: none; } + &:last-child { + border-bottom: none; + } } .rdpp-label { @@ -63,9 +78,13 @@ overflow: hidden; margin-top: 8px; - table { width: 100%; } + table { + width: 100%; + } - & + & { margin-top: 12px; } + & + & { + margin-top: 12px; + } } .rdpp-sub-heading { @@ -108,6 +127,9 @@ table { tr.incentives-row { height: 0; - > td { padding: 0; } + + > td { + padding: 0; + } } } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.html index ff0f2d5d5..ca817dbd2 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.html @@ -7,215 +7,117 @@ -->
+
- -

{{ 'labels.inputs.Deposit Options' | translate }}

+ + Premature option
-
-
- - {{ 'labels.inputs.Is Mandatory Deposit' | translate }} - -
- -
- - {{ 'labels.inputs.Adjust advance payments toward future installments' | translate }} - -
- -
- - {{ 'labels.inputs.Allow Withdrawals' | translate }} - +
+
+ Loan Facility + Premature Withdrawal option + Tenure Flexibility + Lien Marked + Apply Withdrawal Fee for Transfers
+
- -

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

+ + {{ 'labels.inputs.Lock-in Period' | translate }}
-
- - {{ 'labels.inputs.Frequency' | translate }} - - - - - {{ 'labels.inputs.Type' | translate }} - - @for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) { - - {{ lockinPeriodFrequencyType.value }} - - } - - -
-
- -
-
- -

{{ 'labels.inputs.Minimum Deposit Term' | translate }}

-
-
- - {{ 'labels.inputs.Frequency' | translate }} - - - {{ 'labels.inputs.Minimum Deposit Term Frequency' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.Frequency Type' | translate }} - - @for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) { - - {{ periodFrequencyType.value }} - - } - - - {{ 'labels.inputs.Minimum Deposit Term Frequency Type' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - -
-
- -
-
- -

{{ 'labels.heading.And thereafter, in Multiples of' | translate }}

-
-
- - {{ 'labels.inputs.Frequency' | translate }} - - - - - {{ 'labels.inputs.Type' | translate }} - - @for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) { - - {{ periodFrequencyType.value }} - - } - - -
-
- -
-
- -

{{ 'labels.inputs.Maximum Deposit Term' | translate }}

-
-
- - {{ 'labels.inputs.Frequency' | translate }} - - - - - {{ 'labels.inputs.Type' | translate }} - - @for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) { - - {{ periodFrequencyType.value }} - - } - - -
-
- -
-
- -

{{ 'labels.heading.For Pre-mature closure' | translate }}

-
-
-
- - {{ 'labels.inputs.Apply Penal Interest (less)' | translate }} - +
+
+ Enable Lock-in Period +
- - {{ 'labels.inputs.Penal Interest' | translate }} (%) - - + @if (recurringDepositProductSettingsForm.value.enableLockinPeriod) { +
+ + Frequency* + + - - {{ 'labels.inputs.Period' | translate }} - - @for (preClosurePenalInterestOnType of preClosurePenalInterestOnTypeData; track preClosurePenalInterestOnType) { - - {{ preClosurePenalInterestOnType.value }} - - } - - -
-
- -
-
- -

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

-
-
-
- - {{ 'labels.inputs.Is Withhold Tax Applicable' | translate }} - -
- - @if (recurringDepositProductSettingsForm.value.withHoldTax) { - - {{ 'labels.inputs.Tax Group' | translate }} - - @for (taxGroup of taxGroupData; track taxGroup) { - - {{ taxGroup.name }} - - } - - - {{ 'labels.inputs.Tax Group' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - + + 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/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.scss b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.scss index d94100dfe..53e82e31e 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.scss +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.scss @@ -6,8 +6,14 @@ * 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 #e2e8f0; + border: 1px solid $border; border-radius: 10px; overflow: hidden; background: #fff; @@ -21,28 +27,37 @@ align-items: center; gap: 10px; padding: 12px 24px; - background: #f8f9fa; - border-bottom: 1px solid #e8eaed; + background: $gray-50; + border-bottom: 1px solid $gray-200; i { font-size: 14px; - color: #0ea5e9; + color: $accent; } - h3 { + span:not(.rdps-section__badge) { margin: 0; font-size: 14px; font-weight: 600; - color: #202124; + color: $gray-900; } } } -.rdps-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 4px 16px; - padding: 16px 24px; +.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 { @@ -53,14 +68,98 @@ } } -.rdps-checkbox-row { +.rdps-premature-row { display: flex; align-items: center; - padding: 4px 0; + gap: 24px; + flex-wrap: wrap; + + mat-checkbox { + font-size: 14px; + color: $gray-900; + } } -@media (max-width: 600px) { - .rdps-grid { +.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/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.ts b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.ts index a9b559218..74c8ca844 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.ts +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.ts @@ -7,15 +7,8 @@ */ import { Component, OnInit, Input, inject } from '@angular/core'; -import { - UntypedFormGroup, - UntypedFormBuilder, - UntypedFormControl, - Validators, - ReactiveFormsModule -} from '@angular/forms'; +import { UntypedFormGroup, UntypedFormBuilder, ReactiveFormsModule } from '@angular/forms'; import { MatCheckbox } from '@angular/material/checkbox'; -import { MatTooltip } from '@angular/material/tooltip'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ @@ -24,8 +17,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; styleUrls: ['./recurring-deposit-product-settings-step.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, - MatCheckbox, - MatTooltip + MatCheckbox ] }) export class RecurringDepositProductSettingsStepComponent implements OnInit { @@ -36,106 +28,68 @@ export class RecurringDepositProductSettingsStepComponent implements OnInit { recurringDepositProductSettingsForm: UntypedFormGroup; lockinPeriodFrequencyTypeData: any; - periodFrequencyTypeData: any; - preClosurePenalInterestOnTypeData: any; - taxGroupData: any; constructor() { this.createrecurringDepositProductSettingsForm(); - this.setConditionalControls(); } ngOnInit() { this.lockinPeriodFrequencyTypeData = this.recurringDepositProductsTemplate.lockinPeriodFrequencyTypeOptions; - this.periodFrequencyTypeData = this.recurringDepositProductsTemplate.periodFrequencyTypeOptions.slice(0, -1); - this.preClosurePenalInterestOnTypeData = this.recurringDepositProductsTemplate.preClosurePenalInterestOnTypeOptions; - this.taxGroupData = this.recurringDepositProductsTemplate.taxGroupOptions; - if (!(this.recurringDepositProductsTemplate === undefined) && this.recurringDepositProductsTemplate.id) { + if (this.recurringDepositProductsTemplate?.id) { this.recurringDepositProductSettingsForm.patchValue({ - isMandatoryDeposit: this.recurringDepositProductsTemplate.isMandatoryDeposit, - adjustAdvanceTowardsFuturePayments: this.recurringDepositProductsTemplate.adjustAdvanceTowardsFuturePayments, - allowWithdrawal: this.recurringDepositProductsTemplate.allowWithdrawal, - lockinPeriodFrequency: this.recurringDepositProductsTemplate.lockinPeriodFrequency, - lockinPeriodFrequencyType: this.recurringDepositProductsTemplate.lockinPeriodFrequencyType - ? this.recurringDepositProductsTemplate.lockinPeriodFrequencyType.id - : '', - minDepositTerm: this.recurringDepositProductsTemplate.minDepositTerm, - minDepositTermTypeId: this.recurringDepositProductsTemplate.minDepositTermType - ? this.recurringDepositProductsTemplate.minDepositTermType.id - : '', - inMultiplesOfDepositTerm: this.recurringDepositProductsTemplate.inMultiplesOfDepositTerm, - inMultiplesOfDepositTermTypeId: this.recurringDepositProductsTemplate.inMultiplesOfDepositTermType - ? this.recurringDepositProductsTemplate.inMultiplesOfDepositTerm.id - : '', - maxDepositTerm: this.recurringDepositProductsTemplate.maxDepositTerm, - maxDepositTermTypeId: this.recurringDepositProductsTemplate.maxDepositTermType - ? this.recurringDepositProductsTemplate.minDepositTermType.id - : '', - preClosurePenalApplicable: this.recurringDepositProductsTemplate.preClosurePenalApplicable, - preClosurePenalInterest: this.recurringDepositProductsTemplate.preClosurePenalInterest, - preClosurePenalInterestOnTypeId: this.recurringDepositProductsTemplate.preClosurePenalInterestOnType - ? this.recurringDepositProductsTemplate.preClosurePenalInterestOnType.id - : '', - withHoldTax: this.recurringDepositProductsTemplate.withHoldTax - }); - } - - if (this.recurringDepositProductsTemplate.withHoldTax) { - this.recurringDepositProductSettingsForm.patchValue({ - taxGroupId: this.recurringDepositProductsTemplate.taxGroup - ? this.recurringDepositProductsTemplate.taxGroup.id - : '' + loanFacility: this.recurringDepositProductsTemplate.loanFacility ?? false, + prematureWithdrawalOption: this.recurringDepositProductsTemplate.prematureWithdrawalOption ?? false, + tenureFlexibility: this.recurringDepositProductsTemplate.tenureFlexibility ?? false, + lienMarked: this.recurringDepositProductsTemplate.lienMarked ?? false, + withdrawalFeeForTransfers: this.recurringDepositProductsTemplate.withdrawalFeeForTransfers ?? false, + enableLockinPeriod: this.recurringDepositProductsTemplate.enableLockinPeriod ?? false, + lockinPeriodFrequency: this.recurringDepositProductsTemplate.lockinPeriodFrequency ?? '', + lockinPeriodFrequencyType: this.recurringDepositProductsTemplate.lockinPeriodFrequencyType?.id ?? '', + maturityInstructions: this.recurringDepositProductsTemplate.maturityInstructions ?? false, + creditMaturityProceeds: this.recurringDepositProductsTemplate.creditMaturityProceeds ?? false, + penaltyGracePeriodMissedInstallments: + this.recurringDepositProductsTemplate.penaltyGracePeriodMissedInstallments ?? false, + panelRequestApplicable: this.recurringDepositProductsTemplate.panelRequestApplicable ?? false, + noPenaltyNomineeHeirClosure: this.recurringDepositProductsTemplate.noPenaltyNomineeHeirClosure ?? false, + deceasedSettlement: this.recurringDepositProductsTemplate.deceasedSettlement ?? false, + allowDebitTxnAfterDueDate: this.recurringDepositProductsTemplate.allowDebitTxnAfterDueDate ?? false, + accountClosedZeroBalanceInDayClosing: + this.recurringDepositProductsTemplate.accountClosedZeroBalanceInDayClosing ?? false, + minBalanceForInterestCalculation: this.recurringDepositProductsTemplate.minBalanceForInterestCalculation ?? '' }); } } createrecurringDepositProductSettingsForm() { this.recurringDepositProductSettingsForm = this.formBuilder.group({ - isMandatoryDeposit: [false], - adjustAdvanceTowardsFuturePayments: [false], - allowWithdrawal: [false], + loanFacility: [false], + prematureWithdrawalOption: [false], + tenureFlexibility: [false], + lienMarked: [false], + withdrawalFeeForTransfers: [false], + enableLockinPeriod: [false], lockinPeriodFrequency: [''], lockinPeriodFrequencyType: [''], - minDepositTerm: [ - '', - Validators.required - ], - minDepositTermTypeId: [ - '', - Validators.required - ], - inMultiplesOfDepositTerm: [''], - inMultiplesOfDepositTermTypeId: [''], - maxDepositTerm: [''], - maxDepositTermTypeId: [''], - preClosurePenalApplicable: [false], - preClosurePenalInterest: [''], - preClosurePenalInterestOnTypeId: [''], - withHoldTax: [false] - }); - } - - setConditionalControls() { - this.recurringDepositProductSettingsForm.get('withHoldTax').valueChanges.subscribe((withHoldTax: any) => { - if (withHoldTax) { - this.recurringDepositProductSettingsForm.addControl( - 'taxGroupId', - new UntypedFormControl('', Validators.required) - ); - } else { - this.recurringDepositProductSettingsForm.removeControl('taxGroupId'); - } + maturityInstructions: [false], + creditMaturityProceeds: [false], + penaltyGracePeriodMissedInstallments: [false], + panelRequestApplicable: [false], + noPenaltyNomineeHeirClosure: [false], + deceasedSettlement: [false], + allowDebitTxnAfterDueDate: [false], + accountClosedZeroBalanceInDayClosing: [false], + minBalanceForInterestCalculation: [''] }); } get recurringDepositProductSettings() { - const recurringDepositProductSettings = this.recurringDepositProductSettingsForm.value; - for (const key in recurringDepositProductSettings) { - if (recurringDepositProductSettings[key] === '') { - delete recurringDepositProductSettings[key]; + const settings = { ...this.recurringDepositProductSettingsForm.value }; + for (const key in settings) { + if (settings[key] === '') { + delete settings[key]; } } - return recurringDepositProductSettings; + return settings; } } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.html index b5a55f78e..65358d314 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.html @@ -9,99 +9,148 @@
- -

{{ 'labels.inputs.Deposit Amount' | translate }}

+ + Interest Rate & Posting Terms
-
- - {{ 'labels.inputs.Minimum' | translate }} - - +
+
+ ℹ️ + + Recurring Deposit base rate: + {{ recurringDepositProductTermsForm.get('nominalAnnualInterestRate').value }}% + +
- - {{ 'labels.inputs.Default' | translate }} - - - {{ 'labels.inputs.Default' | translate }} {{ 'labels.inputs.Deposit Amount' | translate }} - {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.Maximum' | translate }} - - -
-
- -
-
- -

{{ 'labels.inputs.Interest Compounding Period' | translate }}

-
-
- - {{ 'labels.inputs.Interest Compounding Period' | translate }} - - @for (interestCompoundingPeriodType of interestCompoundingPeriodTypeData; track interestCompoundingPeriodType) { - - {{ interestCompoundingPeriodType.value }} - +
+ + {{ 'labels.inputs.Nominal Annual Interest' | translate }} + + @if (recurringDepositProductTermsForm.get('nominalAnnualInterestRate').hasError('required')) { + + {{ 'labels.inputs.Nominal Annual Interest' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + } - - - {{ 'labels.inputs.Interest Compounding Period' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.Interest Posting Period' | translate }} - - @for (interestPostingPeriodType of interestPostingPeriodTypeData; track interestPostingPeriodType) { - - {{ interestPostingPeriodType.value }} - + @if (recurringDepositProductTermsForm.get('nominalAnnualInterestRate').hasError('min')) { + {{ 'labels.inputs.Nominal Annual Interest' | translate }} must be zero or greater } - - - {{ 'labels.inputs.Interest Posting Period' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - + - - {{ 'labels.inputs.Interest Calculated using' | translate }} - - @for (interestCalculationType of interestCalculationTypeData; track interestCalculationType) { - - {{ interestCalculationType.value }} - - } - - - {{ 'labels.inputs.Interest Calculated using' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - + + Effective Date + + + + - - {{ 'labels.inputs.Days in Year' | translate }} - - @for ( - interestCalculationDaysInYearType of interestCalculationDaysInYearTypeData; - track interestCalculationDaysInYearType - ) { - - {{ interestCalculationDaysInYearType.value }} - - } - - - {{ 'labels.inputs.Days in Year' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - + + {{ 'labels.inputs.Interest Calculation Type' | translate }} + + @for (interestCalculationType of interestCalculationTypeOptionData; track interestCalculationType.id) { + + {{ interestCalculationType.value | translateKey: 'catalogs' }} + + } + + + {{ 'labels.inputs.Interest Calculation Type' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + +
+ +
+ + Interest Calculate + + @for (mode of interestCalculationTypeData; track mode.id) { + {{ mode.value }} + } + + + + @if (recurringDepositProductTermsForm.value.interestCalculateSelector === 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/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.scss b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.scss index a520130f8..e5e2608bc 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.scss +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.scss @@ -6,8 +6,14 @@ * 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 #e2e8f0; + border: 1px solid $border; border-radius: 10px; overflow: hidden; background: #fff; @@ -21,23 +27,39 @@ align-items: center; gap: 10px; padding: 12px 24px; - background: #f8f9fa; - border-bottom: 1px solid #e8eaed; + background: $gray-50; + border-bottom: 1px solid $gray-200; i { font-size: 14px; - color: #0ea5e9; + color: $accent; } - h3 { + h3, + span:not(.rdpt-section__badge) { margin: 0; font-size: 14px; font-weight: 600; - color: #202124; + 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); @@ -47,6 +69,14 @@ &--three { grid-template-columns: repeat(3, 1fr); } + + @media (width <= 600px) { + grid-template-columns: 1fr; + + &--three { + grid-template-columns: 1fr; + } + } } .rdpt-field { @@ -57,12 +87,66 @@ } } -@media (max-width: 600px) { - .rdpt-grid { - grid-template-columns: 1fr; +.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; - &--three { - grid-template-columns: 1fr; - } + &__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/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.ts b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.ts index 81b8af313..ccef15fff 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.ts +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.ts @@ -7,7 +7,10 @@ */ import { Component, OnInit, Input, inject } from '@angular/core'; -import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +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({ @@ -15,11 +18,14 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; templateUrl: './recurring-deposit-product-terms-step.component.html', styleUrls: ['./recurring-deposit-product-terms-step.component.scss'], imports: [ - ...STANDALONE_SHARED_IMPORTS + ...STANDALONE_SHARED_IMPORTS, + MatTooltip ] }) export class RecurringDepositProductTermsStepComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); + private dateUtils = inject(Dates); + private settingsService = inject(SettingsService); @Input() recurringDepositProductsTemplate: any; @@ -28,7 +34,13 @@ export class RecurringDepositProductTermsStepComponent implements OnInit { 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.createrecurringDepositProductTermsForm(); @@ -38,40 +50,72 @@ export class RecurringDepositProductTermsStepComponent implements OnInit { this.interestCompoundingPeriodTypeData = this.recurringDepositProductsTemplate.interestCompoundingPeriodTypeOptions; this.interestPostingPeriodTypeData = this.recurringDepositProductsTemplate.interestPostingPeriodTypeOptions; this.interestCalculationTypeData = this.recurringDepositProductsTemplate.interestCalculationTypeOptions; + this.interestCalculationTypeOptionData = + this.recurringDepositProductsTemplate.interestCalculationTypeOption || + this.recurringDepositProductsTemplate.interestCalculationTypeOptions; this.interestCalculationDaysInYearTypeData = this.recurringDepositProductsTemplate.interestCalculationDaysInYearTypeOptions; - if (!(this.recurringDepositProductsTemplate === undefined) && this.recurringDepositProductsTemplate.id) { - this.recurringDepositProductTermsForm.patchValue({ - minDepositAmount: this.recurringDepositProductsTemplate.minDepositAmount, - depositAmount: this.recurringDepositProductsTemplate.depositAmount, - maxDepositAmount: this.recurringDepositProductsTemplate.maxDepositAmount - }); - } + this.periodFrequencyData = this.recurringDepositProductsTemplate.periodFrequencyOptions || []; + this.setYesNoOptions(this.recurringDepositProductsTemplate?.yesNoOptions); + + const isEditing = !!this.recurringDepositProductsTemplate.id; + const defaultPeriod = this.periodFrequencyData[0]?.value || 'Monthly'; + this.recurringDepositProductTermsForm.patchValue({ - interestCompoundingPeriodType: this.recurringDepositProductsTemplate.interestCompoundingPeriodType.id, - interestPostingPeriodType: this.recurringDepositProductsTemplate.interestPostingPeriodType.id, - interestCalculationType: this.recurringDepositProductsTemplate.interestCalculationType.id, - interestCalculationDaysInYearType: this.recurringDepositProductsTemplate.interestCalculationDaysInYearType.id + nominalAnnualInterestRate: this.recurringDepositProductsTemplate.nominalAnnualInterestRate ?? 0, + interestCompoundingPeriodType: this.recurringDepositProductsTemplate.interestCompoundingPeriodType?.id, + interestPostingPeriodType: this.recurringDepositProductsTemplate.interestPostingPeriodType?.id, + interestCalculationType: isEditing + ? this.recurringDepositProductsTemplate.interestCalculationTypeOption?.id || + this.interestCalculationTypeOptionData?.[0]?.id || + 1 + : this.interestCalculationTypeOptionData?.[0]?.id || 1, + interestCalculationDaysInYearType: this.recurringDepositProductsTemplate.interestCalculationDaysInYearType?.id, + effectiveDate: this.recurringDepositProductsTemplate.effectiveDate + ? new Date(this.recurringDepositProductsTemplate.effectiveDate) + : '', + interestCalculateSelector: isEditing + ? this.recurringDepositProductsTemplate.interestCalculateSelector || 1 + : this.recurringDepositProductsTemplate.interestCalculationType?.id || + this.interestCalculationTypeData?.[0]?.id || + 1, + averagePeriod: isEditing ? this.recurringDepositProductsTemplate.averagePeriod || defaultPeriod : defaultPeriod, + autoPost: isEditing ? (this.recurringDepositProductsTemplate.autoPost ?? true) : true, + frequencyFixDay: isEditing ? this.recurringDepositProductsTemplate.frequencyFixDay || 30 : 30, + lastInterestDate: this.recurringDepositProductsTemplate.lastInterestDate + ? new Date(this.recurringDepositProductsTemplate.lastInterestDate) + : '' }); + + const currentInterestCalcType = this.recurringDepositProductTermsForm.get('interestCalculationType').value; + if (currentInterestCalcType && currentInterestCalcType > 2) { + this.recurringDepositProductTermsForm.patchValue({ interestCalculationType: 1 }); + } + } + + 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; } createrecurringDepositProductTermsForm() { this.recurringDepositProductTermsForm = this.formBuilder.group({ - minDepositAmount: [ - '', - Validators.min(0) - ], - depositAmount: [ + nominalAnnualInterestRate: [ '', [ Validators.required, Validators.min(0) ] ], - maxDepositAmount: [ - '', - Validators.min(0) - ], interestCompoundingPeriodType: [ '', Validators.required @@ -87,17 +131,38 @@ export class RecurringDepositProductTermsStepComponent implements OnInit { interestCalculationDaysInYearType: [ '', Validators.required - ] + ], + effectiveDate: [''], + interestCalculateSelector: [1], + averagePeriod: ['Monthly'], + autoPost: [true], + frequencyFixDay: [ + 30, + [Validators.min(0)] + ], + lastInterestDate: [''] }); } get recurringDepositProductTerms() { - const recurringDepositProductTerms = this.recurringDepositProductTermsForm.value; - for (const key in recurringDepositProductTerms) { - if (recurringDepositProductTerms[key] === '') { - delete recurringDepositProductTerms[key]; + const formValue = { ...this.recurringDepositProductTermsForm.value }; + const dateFormat = this.settingsService.dateFormat; + + if (formValue.effectiveDate instanceof Date) { + formValue.effectiveDate = this.dateUtils.formatDate(formValue.effectiveDate, dateFormat) || ''; + } + if (formValue.lastInterestDate instanceof Date) { + formValue.lastInterestDate = this.dateUtils.formatDate(formValue.lastInterestDate, dateFormat) || ''; + } + if (formValue.interestCalculationType && formValue.interestCalculationType > 2) { + formValue.interestCalculationType = 1; + } + + for (const key in formValue) { + if (formValue[key] === '') { + delete formValue[key]; } } - return recurringDepositProductTerms; + return formValue; } } diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.html b/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.html index 64b0a162f..d2db28c86 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.html +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.html @@ -20,21 +20,10 @@

Accounting Rule

- - @for (accountingRule of accountingRuleData; track accountingRule; let i = $index) { - - {{ 'labels.accounting.' + accountingRule | translate }} - - } - -
- - @if (!isCashOrAccrualAccounting()) { -
- Select a cash or accrual accounting rule to configure GL mappings. The default "None" rule does not require - any accounting mappings. +
+ Cash
- } +
@if (isCashOrAccrualAccounting()) { @for (section of accountingSectionConfigs; track section.key) { diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.ts b/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.ts index d148f29c3..88e88a51d 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.ts +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.ts @@ -86,9 +86,14 @@ export class SavingProductAccountingStepComponent implements OnInit { } ngOnInit() { - const mappings = this.savingProductsTemplate?.accountingMappings || {}; + const accountingMappings = this.savingProductsTemplate?.accountingMappings || {}; + // otherGlMappingsMap is at the root of the template, not inside accountingMappings + const mappings = { + ...accountingMappings, + otherGlMappingsMap: this.savingProductsTemplate?.otherGlMappingsMap + }; const accountingMappingOptions = this.savingProductsTemplate?.accountingMappingOptions || {}; - const accountingRuleId = this.savingProductsTemplate?.accountingRule?.id || 1; + const accountingRuleId = 2; // Always Cash this.assetAccountOptions = accountingMappingOptions.assetAccountOptions || []; this.liabilityAccountOptions = accountingMappingOptions.liabilityAccountOptions || []; this.expenseAccountOptions = accountingMappingOptions.expenseAccountOptions || []; @@ -106,7 +111,7 @@ export class SavingProductAccountingStepComponent implements OnInit { createSavingProductAccountingForm() { this.savingProductAccountingForm = this.formBuilder.group({ - accountingRule: [1], + accountingRule: [2], assetsOtherGlMappings: this.formBuilder.array([]), liabilitiesOtherGlMappings: this.formBuilder.array([]), expensesOtherGlMappings: this.formBuilder.array([]), @@ -135,30 +140,20 @@ export class SavingProductAccountingStepComponent implements OnInit { penaltiesReceivableAccountId: mappings.penaltyReceivableAccount?.id || '', interestPayableAccountId: mappings.interestPayableAccount?.id || '', interestReceivableAccountId: mappings.interestReceivableAccount?.id || '', - escheatLiabilityId: mappings.escheatLiabilityAccount?.id || this.liabilityAccountOptions[0]?.id || '' + escheatLiabilityId: mappings.escheatLiabilityAccount?.id || '' }); if (this.isCashOrAccrualAccounting()) { - this.setOtherGlMappings( - 'assetsOtherGlMappings', - mappings.otherGlMappings?.assets || [], - this.assetAccountOptions - ); + // Backend serializes as 'otherGlMappingsMap'; fall back to 'otherGlMappings' for compatibility + const glMappings = mappings.otherGlMappingsMap || mappings.otherGlMappings; + this.setOtherGlMappings('assetsOtherGlMappings', glMappings?.assets || [], this.assetAccountOptions); this.setOtherGlMappings( 'liabilitiesOtherGlMappings', - mappings.otherGlMappings?.liabilities || [], + glMappings?.liabilities || [], this.liabilityAccountOptions ); - this.setOtherGlMappings( - 'expensesOtherGlMappings', - mappings.otherGlMappings?.expenses || [], - this.expenseAccountOptions - ); - this.setOtherGlMappings( - 'incomeOtherGlMappings', - mappings.otherGlMappings?.income || [], - this.incomeAccountOptions - ); + this.setOtherGlMappings('expensesOtherGlMappings', glMappings?.expenses || [], this.expenseAccountOptions); + this.setOtherGlMappings('incomeOtherGlMappings', glMappings?.income || [], this.incomeAccountOptions); } } diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.html b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.html index 8b101463b..93ae5be7c 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.html +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.html @@ -83,7 +83,7 @@ Product Code - + diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.ts b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.ts index 01a118a8a..2805dc866 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.ts +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.ts @@ -82,7 +82,7 @@ export class SavingProductDetailsStepComponent implements OnInit, OnChanges { Validators.required ], customerSegment: [null], - productCode: ['SB-IND-001'], + productCode: [''], effectiveDate: [''], effectiveTo: [''], endDate: [''], @@ -122,7 +122,7 @@ export class SavingProductDetailsStepComponent implements OnInit, OnChanges { description: template.description, accountType: template.accountType ?? this.accountTypeData[0] ?? null, customerSegment: template.customerSegment ?? this.customerSegmentData[0] ?? null, - productCode: template.productCode ?? 'SB-IND-001', + productCode: template.productCode ?? '', effectiveDate: template.effectiveDate ? new Date(template.effectiveDate) : '', effectiveTo: template.effectiveTo ? new Date(template.effectiveTo) : '', endDate: template.endDate ? new Date(template.endDate) : '', diff --git a/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.html b/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.html index c1a5138fa..ad48a1b1d 100644 --- a/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.html +++ b/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.html @@ -449,6 +449,77 @@ > + @if (savingProduct.otherGlMappingsMap) { +
+

Other GL Mappings

+ + + @if (savingProduct.otherGlMappingsMap.assets?.length) { +

Assets

+ + + + + + + + + + + +
Account Name{{ row.accountName }}GL Account{{ row.glCode || row.glAccountId }}
+ } + + @if (savingProduct.otherGlMappingsMap.liabilities?.length) { +

Liabilities

+ + + + + + + + + + + +
Account Name{{ row.accountName }}GL Account{{ row.glCode || row.glAccountId }}
+ } + + @if (savingProduct.otherGlMappingsMap.expenses?.length) { +

Expenses

+ + + + + + + + + + + +
Account Name{{ row.accountName }}GL Account{{ row.glCode || row.glAccountId }}
+ } + + @if (savingProduct.otherGlMappingsMap.income?.length) { +

Income

+ + + + + + + + + + + +
Account Name{{ row.accountName }}GL Account{{ row.glCode || row.glAccountId }}
+ } +
+ } + @if ( savingProduct.paymentChannelToFundSourceMappings?.length || savingProduct.feeToIncomeAccountMappings?.length || diff --git a/src/app/products/share-products/create-share-product/create-share-product.component.scss b/src/app/products/share-products/create-share-product/create-share-product.component.scss index 95c13fd11..6f7b6c969 100644 --- a/src/app/products/share-products/create-share-product/create-share-product.component.scss +++ b/src/app/products/share-products/create-share-product/create-share-product.component.scss @@ -27,10 +27,10 @@ $primary: #1353a0; .shp-tabs { background: var(--md-sys-color-surface, #fff); border-radius: 8px; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08); + box-shadow: 0 2px 8px rgb(0 0 0 / 8%); ::ng-deep { - .mat-mdc-tab-header { border-bottom: 1px solid rgba(0, 0, 0, 0.08); } + .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; } @@ -47,8 +47,8 @@ $primary: #1353a0; align-items: center; margin-top: 20px; padding: 16px 0; - border-top: 1px solid rgba(0, 0, 0, 0.08); + border-top: 1px solid rgb(0 0 0 / 8%); - &__cancel { color: rgba(0, 0, 0, 0.6); } + &__cancel { color: rgb(0 0 0 / 60%); } &__right { display: flex; gap: 12px; } } diff --git a/src/app/products/share-products/share-product-stepper/share-product-accounting-step/share-product-accounting-step.component.scss b/src/app/products/share-products/share-product-stepper/share-product-accounting-step/share-product-accounting-step.component.scss index 17f1d4ab4..fa8e0ab44 100644 --- a/src/app/products/share-products/share-product-stepper/share-product-accounting-step/share-product-accounting-step.component.scss +++ b/src/app/products/share-products/share-product-stepper/share-product-accounting-step/share-product-accounting-step.component.scss @@ -44,7 +44,7 @@ gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { + @media (width <= 600px) { grid-template-columns: 1fr; } } diff --git a/src/app/products/share-products/share-product-stepper/share-product-charges-step/share-product-charges-step.component.scss b/src/app/products/share-products/share-product-stepper/share-product-charges-step/share-product-charges-step.component.scss index e055abfed..b1b3a5d6c 100644 --- a/src/app/products/share-products/share-product-stepper/share-product-charges-step/share-product-charges-step.component.scss +++ b/src/app/products/share-products/share-product-stepper/share-product-charges-step/share-product-charges-step.component.scss @@ -44,7 +44,7 @@ gap: 16px; margin-bottom: 16px; - @media (max-width: 600px) { + @media (width <= 600px) { flex-direction: column; align-items: stretch; } @@ -54,7 +54,7 @@ flex: 1; max-width: 400px; - @media (max-width: 600px) { + @media (width <= 600px) { max-width: 100%; } } diff --git a/src/app/products/share-products/share-product-stepper/share-product-currency-step/share-product-currency-step.component.scss b/src/app/products/share-products/share-product-stepper/share-product-currency-step/share-product-currency-step.component.scss index 4418949a8..2b4edfb6d 100644 --- a/src/app/products/share-products/share-product-stepper/share-product-currency-step/share-product-currency-step.component.scss +++ b/src/app/products/share-products/share-product-stepper/share-product-currency-step/share-product-currency-step.component.scss @@ -31,7 +31,8 @@ $gray-900: #202124; .shpcr-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { grid-template-columns: 1fr; } + + @media (width <= 600px) { grid-template-columns: 1fr; } } .shpcr-field { diff --git a/src/app/products/share-products/share-product-stepper/share-product-details-step/share-product-details-step.component.scss b/src/app/products/share-products/share-product-stepper/share-product-details-step/share-product-details-step.component.scss index b521d4162..a8e05a519 100644 --- a/src/app/products/share-products/share-product-stepper/share-product-details-step/share-product-details-step.component.scss +++ b/src/app/products/share-products/share-product-stepper/share-product-details-step/share-product-details-step.component.scss @@ -29,7 +29,8 @@ $gray-900: #202124; .shpd-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { grid-template-columns: 1fr; } + + @media (width <= 600px) { grid-template-columns: 1fr; } } .shpd-field { diff --git a/src/app/products/share-products/share-product-stepper/share-product-preview-step/share-product-preview-step.component.scss b/src/app/products/share-products/share-product-stepper/share-product-preview-step/share-product-preview-step.component.scss index e0921d1c9..427ce59ca 100644 --- a/src/app/products/share-products/share-product-stepper/share-product-preview-step/share-product-preview-step.component.scss +++ b/src/app/products/share-products/share-product-stepper/share-product-preview-step/share-product-preview-step.component.scss @@ -61,7 +61,7 @@ font-size: 13px; line-height: 1.5; - @media (max-width: 600px) { + @media (width <= 600px) { flex-direction: column; } } diff --git a/src/app/products/share-products/share-product-stepper/share-product-settings-step/share-product-settings-step.component.scss b/src/app/products/share-products/share-product-stepper/share-product-settings-step/share-product-settings-step.component.scss index 5122b92b7..5b661cd01 100644 --- a/src/app/products/share-products/share-product-stepper/share-product-settings-step/share-product-settings-step.component.scss +++ b/src/app/products/share-products/share-product-stepper/share-product-settings-step/share-product-settings-step.component.scss @@ -44,7 +44,7 @@ gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { + @media (width <= 600px) { grid-template-columns: 1fr; } } diff --git a/src/app/products/share-products/share-product-stepper/share-product-terms-step/share-product-terms-step.component.scss b/src/app/products/share-products/share-product-stepper/share-product-terms-step/share-product-terms-step.component.scss index f5c638d7c..34b0f20d8 100644 --- a/src/app/products/share-products/share-product-stepper/share-product-terms-step/share-product-terms-step.component.scss +++ b/src/app/products/share-products/share-product-stepper/share-product-terms-step/share-product-terms-step.component.scss @@ -29,7 +29,8 @@ $gray-900: #202124; .shpt-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 4px 16px; padding: 16px 24px; - @media (max-width: 600px) { grid-template-columns: 1fr; } + + @media (width <= 600px) { grid-template-columns: 1fr; } } .shpt-field { diff --git a/src/environments/.env.ts b/src/environments/.env.ts index a46de5b84..acdf1e8dc 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -9,8 +9,8 @@ /* tslint:disable */ export default { 'mifos_x': { - 'version': '260413', - 'hash': '79e455c4' + 'version': '260415', + 'hash': '4679c6f7' }, 'allow_switching_backend_instance': true }; From e212bf7d79db37e9f871c12ba5c751f388daa405 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Mon, 20 Apr 2026 13:16:03 +0530 Subject: [PATCH 03/12] update RD prodcut code --- ...-recurring-deposits-account.component.html | 1 + ...te-recurring-deposits-account.component.ts | 15 +- ...t-recurring-deposit-account.component.html | 1 + ...dit-recurring-deposit-account.component.ts | 12 + ...nt-interest-rate-chart-step.component.html | 96 +- ...ount-interest-rate-chart-step.component.ts | 27 +- src/app/products/products-routing.module.ts | 19 + src/app/products/products.module.ts | 2 + src/app/products/products.service.ts | 20 + ...deposit-interest-rate-chart.component.html | 119 ++ ...deposit-interest-rate-chart.component.scss | 95 ++ ...g-deposit-interest-rate-chart.component.ts | 121 ++ ...e-recurring-deposit-product.component.html | 58 +- ...e-recurring-deposit-product.component.scss | 48 + ...ate-recurring-deposit-product.component.ts | 63 +- ...t-recurring-deposit-product.component.html | 275 ++-- ...t-recurring-deposit-product.component.scss | 131 ++ ...dit-recurring-deposit-product.component.ts | 84 +- ...ct-interest-rate-chart-step.component.html | 417 ++---- ...ct-interest-rate-chart-step.component.scss | 65 + ...duct-interest-rate-chart-step.component.ts | 217 +++- ...eposit-product-preview-step.component.html | 1144 +++++++++-------- ...eposit-product-preview-step.component.scss | 226 ++-- ...-deposit-product-preview-step.component.ts | 110 +- ...ng-deposit-product-terms-step.component.ts | 13 +- .../recurring-deposit-products.component.html | 12 + .../rd-interest-rates-tab.component.html | 68 + .../rd-interest-rates-tab.component.scss | 40 + .../rd-interest-rates-tab.component.ts | 66 + .../rd-interest-rates.resolver.ts | 16 + ...curring-deposit-general-tab.component.html | 1131 ++++++++-------- ...recurring-deposit-general-tab.component.ts | 1 + ...w-recurring-deposit-product.component.html | 9 + ...iew-recurring-deposit-product.component.ts | 1 + src/environments/.env.ts | 5 +- 35 files changed, 3000 insertions(+), 1728 deletions(-) create mode 100644 src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.html create mode 100644 src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.scss create mode 100644 src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.ts create mode 100644 src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates-tab.component.html create mode 100644 src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates-tab.component.scss create mode 100644 src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates-tab.component.ts create mode 100644 src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates.resolver.ts diff --git a/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.html b/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.html index 8b257b561..95803a7d0 100644 --- a/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.html +++ b/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.html @@ -70,6 +70,7 @@ diff --git a/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts b/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts index 4d459251f..0b5649391 100644 --- a/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts +++ b/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts @@ -13,6 +13,7 @@ import { ActivatedRoute, Router } from '@angular/router'; /** Custom Services */ import { RecurringDepositsService } from '../recurring-deposits.service'; import { SettingsService } from 'app/settings/settings.service'; +import { ProductsService } from 'app/products/products.service'; /** Step Components */ import { RecurringDepositsAccountDetailsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component'; @@ -54,6 +55,7 @@ export class CreateRecurringDepositsAccountComponent { private dateUtils = inject(Dates); private recurringDepositsService = inject(RecurringDepositsService); private settingsService = inject(SettingsService); + private productsService = inject(ProductsService); /** Imports all the step component */ @ViewChild(RecurringDepositsAccountDetailsStepComponent, { static: true }) @@ -69,6 +71,8 @@ export class CreateRecurringDepositsAccountComponent { recurringDepositsAccountTemplate: any; /** Recurring Deposit Account Product Template */ recurringDepositsAccountProductTemplate: any; + /** Active CBS base interest rate chart */ + rdInterestRates: any[] = []; constructor() { this.route.data.subscribe((data: { recurringDepositsAccountTemplate: any }) => { @@ -78,10 +82,19 @@ export class CreateRecurringDepositsAccountComponent { /** * Sets recurring deposits account product template. - * @param {any} $event API response + * Also fetches the active CBS base interest rate chart. + * @param {any} $event API response (includes productId) */ setTemplate($event: any) { this.recurringDepositsAccountProductTemplate = $event; + this.productsService.getRdBaseInterestRates().subscribe({ + next: (rates: any[]) => { + this.rdInterestRates = rates || []; + }, + error: () => { + this.rdInterestRates = []; + } + }); } /** Get Recurring Deposit Account Details Form Data */ diff --git a/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.html b/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.html index 0ac77d4cb..e44e07f33 100644 --- a/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.html +++ b/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.html @@ -71,6 +71,7 @@ diff --git a/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.ts b/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.ts index ffb6c9821..278e8e501 100644 --- a/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.ts +++ b/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.ts @@ -13,6 +13,7 @@ import { ActivatedRoute, Router } from '@angular/router'; /** Custom Services */ import { RecurringDepositsService } from '../recurring-deposits.service'; import { SettingsService } from 'app/settings/settings.service'; +import { ProductsService } from 'app/products/products.service'; /** Step Components */ import { RecurringDepositsAccountDetailsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component'; @@ -54,6 +55,7 @@ export class EditRecurringDepositAccountComponent { private dateUtils = inject(Dates); private recurringDepositsService = inject(RecurringDepositsService); private settingsService = inject(SettingsService); + private productsService = inject(ProductsService); /** Imports all the step component */ @ViewChild(RecurringDepositsAccountDetailsStepComponent, { static: true }) @@ -69,6 +71,8 @@ export class EditRecurringDepositAccountComponent { recurringDepositsAccountAndTemplate: any; /** Recurring Deposit Account Product Template */ recurringDepositsAccountProductTemplate: any; + /** Active CBS base interest rate chart */ + rdInterestRates: any[] = []; constructor() { this.route.data.subscribe((data: { recurringDepositsAccountAndTemplate: any }) => { @@ -82,6 +86,14 @@ export class EditRecurringDepositAccountComponent { */ setTemplate($event: any) { this.recurringDepositsAccountProductTemplate = $event; + this.productsService.getRdBaseInterestRates().subscribe({ + next: (rates: any[]) => { + this.rdInterestRates = rates || []; + }, + error: () => { + this.rdInterestRates = []; + } + }); } /** Get Recurring Deposit Account Details Form Data */ diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.html index 8c5d75118..b4d0cf3a0 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.html @@ -7,34 +7,84 @@ -->
-
- {{ 'labels.inputs.name' | translate }}: - {{ recurringDepositsAccountProductTemplate.accountChart.name }} -
+ + @if (_activeRdVersion) { +
+ {{ _activeRdVersion.versionLabel }} + Active + Effective from: {{ _activeRdVersion.effectiveFromDate }} +
-
- {{ 'labels.inputs.Valid from Date' | translate }}: - {{ recurringDepositsAccountProductTemplate?.accountChart.fromDate | dateFormat }} -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Sr.{{ slab.srNo }}Particular{{ slab.description }}From{{ slab.fromPeriod }}To{{ slab.toPeriod ?? '—' }}Period Type{{ slab.periodTypeLabel }}Annual Rate (%) + {{ slab.annualInterestRate | number: '1.2-2' }} +
+ } -
- {{ 'labels.inputs.End Date' | translate }}: - {{ recurringDepositsAccountProductTemplate?.accountChart.endDate | dateFormat }} -
+ + @if (!_activeRdVersion) { +
+ {{ 'labels.inputs.name' | translate }}: + {{ recurringDepositsAccountProductTemplate.accountChart.name }} +
-
- {{ 'labels.inputs.Description' | translate }}: - {{ recurringDepositsAccountProductTemplate?.accountChart.description }} -
+
+ {{ 'labels.inputs.Valid from Date' | translate }}: + {{ recurringDepositsAccountProductTemplate?.accountChart.fromDate | dateFormat }} +
-
- {{ 'labels.inputs.Primary Grouping by Amount' | translate }}: - {{ - recurringDepositsAccountProductTemplate?.accountChart.isPrimaryGroupingByAmount | yesNo - }} -
+
+ {{ 'labels.inputs.End Date' | translate }}: + {{ recurringDepositsAccountProductTemplate?.accountChart.endDate | dateFormat }} +
- +
+ {{ 'labels.inputs.Description' | translate }}: + {{ recurringDepositsAccountProductTemplate?.accountChart.description }} +
+ +
+ {{ 'labels.inputs.Primary Grouping by Amount' | translate }}: + {{ + recurringDepositsAccountProductTemplate?.accountChart.isPrimaryGroupingByAmount | yesNo + }} +
+ } + +
{{ 'labels.inputs.Period' | translate }} diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.ts index fdf29b5f3..d194eeb9a 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.ts @@ -21,7 +21,7 @@ import { MatRow } from '@angular/material/table'; import { trigger, state, style, transition, animate } from '@angular/animations'; -import { NgIf, NgSwitch, TitleCasePipe } from '@angular/common'; +import { NgIf, NgSwitch, TitleCasePipe, DecimalPipe } from '@angular/common'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper'; import { DateFormatPipe } from '../../../../pipes/date-format.pipe'; @@ -69,7 +69,28 @@ export class RecurringDepositsAccountInterestRateChartStepComponent implements O @Input() recurringDepositsAccountTemplate: any; @Input() recurringDepositsAccountProductTemplate: any; - /** Interest Rate Chart Data */ + /** + * CBS base rate chart loaded from the active base-interest-rate service. + * When set, this table is shown instead of the legacy accountChart.chartSlabs. + */ + @Input() set rdInterestRates(rates: any[]) { + this._rdInterestRates = rates || []; + this._activeRdVersion = this._rdInterestRates.find((v: any) => v.isActive) ?? this._rdInterestRates[0] ?? null; + } + + _rdInterestRates: any[] = []; + _activeRdVersion: any = null; + + readonly rdSlabColumns = [ + 'srNo', + 'description', + 'fromPeriod', + 'toPeriod', + 'periodTypeLabel', + 'annualInterestRate' + ]; + + /** Interest Rate Chart Data (legacy – used when no custom rate chart is available) */ interestRateChartData: any = []; /** Columns to be displayed in interest rate chart table. */ chartSlabsDisplayedColumns: any[] = [ @@ -88,7 +109,7 @@ export class RecurringDepositsAccountInterestRateChartStepComponent implements O 'incentiveType', 'amount' ]; - /** Additional Column to disblac incentives table */ + /** Additional Column to display incentives table */ chartSlabsIncentivesDisplayedColumns: string[] = ['incentives']; /** Expand Chart Slab Index used in the view */ expandChartSlabIndex: number; diff --git a/src/app/products/products-routing.module.ts b/src/app/products/products-routing.module.ts index 0c49649fd..c6cb43e70 100644 --- a/src/app/products/products-routing.module.ts +++ b/src/app/products/products-routing.module.ts @@ -49,6 +49,7 @@ import { EditTaxComponentComponent } from './manage-tax-components/edit-tax-comp import { ViewTaxGroupComponent } from './manage-tax-groups/view-tax-group/view-tax-group.component'; import { ShareProductsDividendsComponent } from './share-products/dividends-share-product/dividends.components'; import { CreateRecurringDepositProductComponent } from './recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component'; +import { BaseRecurringDepositInterestRateChartComponent } from './recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component'; import { CreateDividendComponent } from './share-products/create-dividend/create-dividend.component'; import { ViewFixedDepositProductComponent } from './fixed-deposit-products/view-fixed-deposit-product/view-fixed-deposit-product.component'; import { ViewDividendComponent } from './share-products/view-dividend/view-dividend.component'; @@ -129,6 +130,8 @@ import { FixedDepositGeneralTabComponent } from './fixed-deposit-products/view-f import { FixedDepositDatatableTabComponent } from './fixed-deposit-products/view-fixed-deposit-product/fixed-deposit-datatable-tab/fixed-deposit-datatable-tab.component'; import { RecurringDepositGeneralTabComponent } from './recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component'; import { RecurringDepositDatatableTabComponent } from './recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-datatable-tab/recurring-deposit-datatable-tab.component'; +import { RdInterestRatesTabComponent } from './recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates-tab.component'; +import { rdInterestRatesResolver } from './recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates.resolver'; import { ShareProductGeneralTabComponent } from './share-products/view-share-product/share-product-general-tab/share-product-general-tab.component'; import { ShareProductDatatableTabComponent } from './share-products/view-share-product/share-product-datatable-tab/share-product-datatable-tab.component'; import { ShareProductDatatablesResolver } from './share-products/share-product-datatables.resolver'; @@ -652,6 +655,14 @@ const routes: Routes = [ path: 'recurring-deposit-products', data: { title: 'Recurring Deposit Products', breadcrumb: 'Recurring Deposit Products' }, children: [ + { + path: 'base-interest-rates', + component: BaseRecurringDepositInterestRateChartComponent, + data: { title: 'CBS Base Interest Rates', breadcrumb: 'Base Interest Rates' }, + resolve: { + recurringDepositProductsTemplate: RecurringDepositProductsTemplateResolver + } + }, { path: 'create', component: CreateRecurringDepositProductComponent, @@ -704,6 +715,14 @@ const routes: Routes = [ } ] }, + { + path: 'interest-rates', + component: RdInterestRatesTabComponent, + data: { title: 'CBS Base Chart', breadcrumb: 'CBS Base Chart', routeParamBreadcrumb: false }, + resolve: { + rdInterestRates: rdInterestRatesResolver + } + }, { path: 'edit', data: { title: 'Edit Recurring Deposit Product', breadcrumb: 'edit', routeParamBreadcrumb: false }, diff --git a/src/app/products/products.module.ts b/src/app/products/products.module.ts index 27675d51e..d98b4d3a9 100644 --- a/src/app/products/products.module.ts +++ b/src/app/products/products.module.ts @@ -84,6 +84,7 @@ import { ViewTaxGroupComponent } from './manage-tax-groups/view-tax-group/view-t import { ShareProductsDividendsComponent } from './share-products/dividends-share-product/dividends.components'; import { ViewRecurringDepositProductComponent } from './recurring-deposit-products/view-recurring-deposit-product/view-recurring-deposit-product.component'; import { CreateRecurringDepositProductComponent } from './recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component'; +import { BaseRecurringDepositInterestRateChartComponent } from './recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component'; import { RecurringDepositProductAccountingStepComponent } from './recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component'; import { RecurringDepositProductChargesStepComponent } from './recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component'; import { RecurringDepositProductCurrencyStepComponent } from './recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component'; @@ -213,6 +214,7 @@ import { AdvancedAccountingMappingRuleComponent } from './loan-products/loan-pro DepositProductIncentiveFormDialogComponent, ViewRecurringDepositProductComponent, CreateRecurringDepositProductComponent, + BaseRecurringDepositInterestRateChartComponent, RecurringDepositProductAccountingStepComponent, RecurringDepositProductChargesStepComponent, RecurringDepositProductCurrencyStepComponent, diff --git a/src/app/products/products.service.ts b/src/app/products/products.service.ts index 97daf2e91..82e0f2035 100644 --- a/src/app/products/products.service.ts +++ b/src/app/products/products.service.ts @@ -526,6 +526,26 @@ export class ProductsService { return this.http.put(`/recurringdepositproducts/${recurringDepositProductId}`, recurringDepositProduct); } + /** Returns the active CBS base interest-rate version(s). */ + getAllRdInterestRates(): Observable { + return this.getRdBaseInterestRates(); + } + + /** Returns the active CBS base interest-rate version(s). */ + getRdBaseInterestRates(): Observable { + return this.http.get('/recurringdepositproducts/baseinterestrates'); + } + + /** Returns the full CBS base chart history. */ + getRdBaseInterestRateHistory(): Observable { + return this.http.get('/recurringdepositproducts/baseinterestrates/history'); + } + + /** Saves a new CBS base interest-rate version. */ + saveRdBaseInterestRates(payload: any): Observable { + return this.http.post('/recurringdepositproducts/baseinterestrates', payload); + } + /** * @returns {Observable} Collateral data. */ diff --git a/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.html b/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.html new file mode 100644 index 000000000..f8cdf923e --- /dev/null +++ b/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.html @@ -0,0 +1,119 @@ + + +
+ + + @if (recurringDepositProductsTemplate?.chartTemplate) { + + + +
+ + @if (saveMessage) { + {{ saveMessage }} + } +
+ } @else { +

Loading CBS base chart...

+ } + +
+
+
+

Chart History

+

Previous CBS base chart versions are listed here for comparison and audit.

+
+
+ + @if (isHistoryLoading) { +

Loading history...

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

No history found for the CBS base chart.

+ } @else { + + @for (version of rdInterestRateHistory; track version.id) { + + + + + {{ version.versionLabel }} + + {{ version.isActive ? 'Active' : 'Inactive' }} + + + + + From {{ version.effectiveFromDate }} + To {{ version.effectiveToDate || 'Current' }} + + + + @if (version.remarks) { +

{{ version.remarks }}

+ } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Sr.{{ slab.srNo }}Description{{ slab.description }}From{{ slab.fromPeriod }}To + {{ slab.toPeriod !== null && slab.toPeriod !== undefined ? slab.toPeriod : 'N/A' }} + Period Type{{ slab.periodTypeLabel }}Annual Rate (%){{ slab.annualInterestRate | number: '1.2-2' }}
+
+ } +
+ } +
+
diff --git a/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.scss b/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.scss new file mode 100644 index 000000000..00de402dc --- /dev/null +++ b/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.scss @@ -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/. + */ + +.page-layout { + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.page-header { + display: flex; + align-items: end; + justify-content: space-between; +} + +.page-subtitle { + margin: 0.25rem 0 0; + color: #6b7280; +} + +.actions-row { + display: flex; + align-items: center; + gap: 1rem; +} + +.save-message { + color: #2563eb; + font-weight: 600; +} + +.history-section { + margin-top: 1rem; + padding-top: 0.5rem; + border-top: 1px solid #e5e7eb; +} + +.section-header { + display: flex; + align-items: end; + justify-content: space-between; + margin-bottom: 0.75rem; +} + +.section-subtitle { + margin: 0.25rem 0 0; + color: #6b7280; +} + +.version-header { + display: inline-flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; +} + +.meta-text { + margin-left: 0.75rem; + color: #6b7280; + font-size: 0.85rem; +} + +.active-badge, +.inactive-badge { + display: inline-flex; + align-items: center; + padding: 0.2rem 0.5rem; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 600; +} + +.active-badge { + background: #dcfce7; + color: #166534; +} + +.inactive-badge { + background: #f3f4f6; + color: #4b5563; +} + +.remarks-text { + margin: 0 0 0.75rem; + color: #374151; +} + +.history-table { + width: 100%; +} diff --git a/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.ts b/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.ts new file mode 100644 index 000000000..811810cfc --- /dev/null +++ b/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.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, OnInit, inject } from '@angular/core'; +import { ProductsService } from 'app/products/products.service'; +import { RecurringDepositProductInterestRateChartStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component'; +import { MatButton } from '@angular/material/button'; +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-base-recurring-deposit-interest-rate-chart', + templateUrl: './base-recurring-deposit-interest-rate-chart.component.html', + styleUrls: ['./base-recurring-deposit-interest-rate-chart.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatButton, + MatExpansionModule, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, + DecimalPipe, + RecurringDepositProductInterestRateChartStepComponent + ] +}) +export class BaseRecurringDepositInterestRateChartComponent implements OnInit { + private productsService = inject(ProductsService); + + recurringDepositProductsTemplate: any; + rdInterestRates: any[] = []; + rdInterestRateHistory: any[] = []; + readonly historySlabColumns = [ + 'srNo', + 'description', + 'fromPeriod', + 'toPeriod', + 'periodTypeLabel', + 'annualInterestRate' + ]; + isSaving = false; + isHistoryLoading = false; + saveMessage = ''; + + ngOnInit(): void { + this.productsService.getRecurringDepositProductsTemplate().subscribe({ + next: (template: any) => (this.recurringDepositProductsTemplate = template), + error: () => (this.recurringDepositProductsTemplate = null) + }); + + this.loadBaseRates(); + this.loadBaseRateHistory(); + } + + get chartStep(): RecurringDepositProductInterestRateChartStepComponent | null { + return null; + } + + save(stepComponent: RecurringDepositProductInterestRateChartStepComponent): void { + this.isSaving = true; + this.saveMessage = ''; + const payload = stepComponent.modifiedRdInterestRatePayload; + this.productsService.saveRdBaseInterestRates(payload).subscribe({ + next: () => { + this.saveMessage = 'CBS base chart saved successfully.'; + this.loadBaseRates(); + this.loadBaseRateHistory(); + this.isSaving = false; + }, + error: () => { + this.saveMessage = 'Unable to save CBS base chart.'; + this.isSaving = false; + } + }); + } + + private loadBaseRates(): void { + this.productsService.getRdBaseInterestRates().subscribe({ + next: (rates: any[]) => (this.rdInterestRates = rates || []), + error: () => (this.rdInterestRates = []) + }); + } + + private loadBaseRateHistory(): void { + this.isHistoryLoading = true; + this.productsService.getRdBaseInterestRateHistory().subscribe({ + next: (history: any[]) => { + this.rdInterestRateHistory = history || []; + this.isHistoryLoading = false; + }, + error: () => { + this.rdInterestRateHistory = []; + this.isHistoryLoading = false; + } + }); + } +} diff --git a/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.html b/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.html index b0882ab54..3b01d9081 100644 --- a/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.html +++ b/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.html @@ -69,12 +69,60 @@ - {{ 'labels.inputs.INTEREST RATE CHART' | 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...

+ }
@@ -114,7 +162,7 @@
{ + this.rdInterestRates = rates || []; + }, + error: () => { + this.rdInterestRates = []; + } + }); } get recurringDepositProductDetailsForm() { @@ -102,10 +138,6 @@ export class CreateRecurringDepositProductComponent { return this.recurringDepositProductSettingsStep.recurringDepositProductSettingsForm; } - get recurringDepositProductInterestRateChartForm() { - return this.recurringDepositProductInterestRateChartStep.recurringDepositProductInterestRateChartForm; - } - get recurringDepositProductAccountingForm() { return this.recurringDepositProductAccountingStep.recurringDepositProductAccountingForm; } @@ -120,20 +152,27 @@ export class CreateRecurringDepositProductComponent { this.recurringDepositProductCurrencyForm.valid && this.recurringDepositProductTermsForm.valid && this.recurringDepositProductSettingsForm.valid && - this.recurringDepositProductInterestRateChartForm.valid && this.recurringDepositProductAccountingForm.valid ); } + get activeBaseVersion() { + return this.rdInterestRates.find((version: any) => version.isActive) ?? this.rdInterestRates[0] ?? null; + } + + get activeBaseSlabs() { + return this.activeBaseVersion?.slabs || []; + } + get recurringDepositProduct() { return { ...this.recurringDepositProductDetailsStep.recurringDepositProductDetails, ...this.recurringDepositProductCurrencyStep.recurringDepositProductCurrency, ...this.recurringDepositProductTermsStep.recurringDepositProductTerms, ...this.recurringDepositProductSettingsStep.recurringDepositProductSettings, - ...this.recurringDepositProductInterestRateChartStep.recurringDepositProductInterestRateChart, ...this.recurringDepositProductChargesStep.recurringDepositProductCharges, - ...this.recurringDepositProductAccountingStep.recurringDepositProductAccounting + ...this.recurringDepositProductAccountingStep.recurringDepositProductAccounting, + rdSlabs: this.activeBaseSlabs }; } diff --git a/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.html b/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.html index 606a08fb4..975cc9065 100644 --- a/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.html +++ b/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.html @@ -6,112 +6,195 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
- +
+ +

{{ 'labels.heading.Edit Recurring Deposit 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 +
- - {{ 'labels.inputs.DETAILS' | translate }} + @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.CURRENCY' | translate }} + + + + {{ 'labels.inputs.CHARGES' | translate }} + +
+ +
+
- - -
- - - {{ 'labels.inputs.TERMS' | translate }} - - - - - - - {{ 'labels.inputs.SETTINGS' | translate }} - - - - - - - {{ 'labels.inputs.INTEREST RATE CHART' | translate }} - - - - - - - {{ 'labels.inputs.CHARGES' | translate }} - - - - - - - {{ 'labels.inputs.ACCOUNTING' | translate }} - - - - + + + + {{ 'labels.inputs.ACCOUNTING' | translate }} + +
+ +
+
@if (recurringDepositProductFormValidAndNotPrinstine) { - - {{ 'labels.inputs.PREVIEW' | translate }} - - - + + + + {{ 'labels.inputs.PREVIEW' | translate }} + +
+ +
+
} -
+ + +
+ +
+ @if (selectedTabIndex > 0) { + + } + @if (selectedTabIndex < totalTabs - 1) { + + } + @if (recurringDepositProductFormValidAndNotPrinstine && selectedTabIndex === totalTabs - 1) { + + } +
+
diff --git a/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.scss b/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.scss index af9f4c075..c04c383f8 100644 --- a/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.scss +++ b/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.scss @@ -5,3 +5,134 @@ * 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/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.ts b/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.ts index 2c39bef1d..aa5fb1ff0 100644 --- a/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.ts +++ b/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.ts @@ -15,16 +15,26 @@ import { RecurringDepositProductDetailsStepComponent } from '../recurring-deposi import { RecurringDepositProductCurrencyStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component'; import { RecurringDepositProductTermsStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component'; import { RecurringDepositProductSettingsStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component'; -import { RecurringDepositProductInterestRateChartStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component'; import { RecurringDepositProductChargesStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component'; import { RecurringDepositProductAccountingStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-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 { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper'; -import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { MatTabsModule } from '@angular/material/tabs'; import { RecurringDepositProductPreviewStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @@ -34,19 +44,24 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; styleUrls: ['./edit-recurring-deposit-product.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, - MatStepper, - MatStepperIcon, - FaIconComponent, - MatStep, - MatStepLabel, + MatTabsModule, RecurringDepositProductDetailsStepComponent, RecurringDepositProductCurrencyStepComponent, RecurringDepositProductTermsStepComponent, RecurringDepositProductSettingsStepComponent, - RecurringDepositProductInterestRateChartStepComponent, RecurringDepositProductChargesStepComponent, RecurringDepositProductAccountingStepComponent, - RecurringDepositProductPreviewStepComponent + RecurringDepositProductPreviewStepComponent, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow ] }) export class EditRecurringDepositProductComponent { @@ -64,8 +79,6 @@ export class EditRecurringDepositProductComponent { recurringDepositProductTermsStep: RecurringDepositProductTermsStepComponent; @ViewChild(RecurringDepositProductSettingsStepComponent, { static: true }) recurringDepositProductSettingsStep: RecurringDepositProductSettingsStepComponent; - @ViewChild(RecurringDepositProductInterestRateChartStepComponent, { static: true }) - recurringDepositProductInterestRateChartStep: RecurringDepositProductInterestRateChartStepComponent; @ViewChild(RecurringDepositProductChargesStepComponent, { static: true }) recurringDepositProductChargesStep: RecurringDepositProductChargesStepComponent; @ViewChild(RecurringDepositProductAccountingStepComponent, { static: true }) @@ -73,6 +86,16 @@ export class EditRecurringDepositProductComponent { recurringDepositProductsTemplate: any; accountingRuleData: string[] = []; + rdInterestRates: any[] = []; + readonly rdSlabColumns = [ + 'srNo', + 'description', + 'fromPeriod', + 'toPeriod', + 'periodTypeLabel', + 'annualInterestRate' + ]; + selectedTabIndex = 0; /** * @param {ActivatedRoute} route Activated Route. @@ -84,6 +107,14 @@ export class EditRecurringDepositProductComponent { constructor() { this.route.data.subscribe((data: { recurringDepositProductAndTemplate: any }) => { this.recurringDepositProductsTemplate = data.recurringDepositProductAndTemplate; + this.productsService.getRdBaseInterestRates().subscribe({ + next: (rates: any[]) => { + this.rdInterestRates = rates || []; + }, + error: () => { + this.rdInterestRates = []; + } + }); }); this.accountingRuleData = this.accounting.getAccountingRulesForSavings(); } @@ -104,43 +135,55 @@ export class EditRecurringDepositProductComponent { return this.recurringDepositProductSettingsStep.recurringDepositProductSettingsForm; } - get recurringDepositProductInterestRateChartForm() { - return this.recurringDepositProductInterestRateChartStep.recurringDepositProductInterestRateChartForm; - } - get recurringDepositProductAccountingForm() { return this.recurringDepositProductAccountingStep.recurringDepositProductAccountingForm; } + get totalTabs(): number { + return this.recurringDepositProductFormValidAndNotPrinstine ? 7 : 6; + } + get recurringDepositProductFormValidAndNotPrinstine() { return ( this.recurringDepositProductDetailsForm.valid && this.recurringDepositProductCurrencyForm.valid && this.recurringDepositProductTermsForm.valid && this.recurringDepositProductSettingsForm.valid && - this.recurringDepositProductInterestRateChartForm.valid && this.recurringDepositProductAccountingForm.valid && (this.recurringDepositProductDetailsForm.pristine || this.recurringDepositProductCurrencyForm.pristine || this.recurringDepositProductTermsForm.pristine || this.recurringDepositProductSettingsForm.pristine || - this.recurringDepositProductInterestRateChartForm.pristine || this.recurringDepositProductAccountingForm.pristine) ); } + get activeBaseVersion() { + return this.rdInterestRates.find((version: any) => version.isActive) ?? this.rdInterestRates[0] ?? null; + } + + get activeBaseSlabs() { + return this.activeBaseVersion?.slabs || []; + } + get recurringDepositProduct() { return { ...this.recurringDepositProductDetailsStep.recurringDepositProductDetails, ...this.recurringDepositProductCurrencyStep.recurringDepositProductCurrency, ...this.recurringDepositProductTermsStep.recurringDepositProductTerms, ...this.recurringDepositProductSettingsStep.recurringDepositProductSettings, - ...this.recurringDepositProductInterestRateChartStep.recurringDepositProductInterestRateChart, ...this.recurringDepositProductChargesStep.recurringDepositProductCharges, ...this.recurringDepositProductAccountingStep.recurringDepositProductAccounting }; } + get recurringDepositProductPreview() { + return { + ...this.recurringDepositProduct, + rdSlabs: this.activeBaseSlabs + }; + } + submit() { // TODO: Update once language and date settings are setup const recurringDepositProduct = { @@ -152,8 +195,9 @@ export class EditRecurringDepositProductComponent { recurringDepositProduct.description = ''; } delete recurringDepositProduct.advancedAccountingRules; + const productId = this.recurringDepositProductsTemplate.id; this.productsService - .updateRecurringDepositProduct(this.recurringDepositProductsTemplate.id, recurringDepositProduct) + .updateRecurringDepositProduct(productId, recurringDepositProduct) .subscribe((response: any) => { this.router.navigate(['../'], { relativeTo: this.route }); }); diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.html index df813724b..7b799e488 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.html @@ -6,325 +6,104 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
-
-
- -

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

- - -
+ +
+
+ +

Bank Interest Rate Chart  Reference / Editable

+ + +
-
- @for (chart of charts.controls; track chart; let chartIndex = $index) { -
-
-
- -
- -
- - {{ 'labels.inputs.Name' | translate }} - - @if (chart.get('name')?.touched && chart.get('name')?.hasError('required')) { - - {{ 'labels.inputs.Name' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - - - - {{ 'labels.inputs.Description' | translate }} - - @if (chart.get('description')?.touched && chart.get('description')?.hasError('required')) { - - {{ 'labels.inputs.Description' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - - - - {{ 'labels.inputs.From Date' | translate }} - - - - - {{ 'labels.inputs.From Date' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.End Date' | translate }} - - - - - -
- - {{ 'labels.inputs.Is primary grouping by amount?' | translate }} - -
-
- -
- -
- - @if (chart.value.chartSlabs.length === 0) { -
-

{{ 'labels.heading.It is required to add at least one Slab' | translate }}

-
- } - - @if (chart.value.chartSlabs.length !== 0) { -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Period' | translate }} - {{ - chartSlab.fromPeriod + - ' - ' + - chartSlab.toPeriod + - ' ' + - (chartSlab.periodType | find: periodTypeData : 'id' : 'value') - }} - {{ 'labels.inputs.Amount Range' | translate }} - {{ chartSlab.amountRangeFrom + ' - ' + chartSlab.amountRangeTo }} - {{ 'labels.inputs.Interest' | translate }} - {{ chartSlab.annualInterestRate }} - {{ 'labels.inputs.Description' | translate }} - {{ chartSlab.description }} - {{ 'labels.inputs.Actions' | translate }} - - - - -
- -

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

-
- -
- @if (chartSlab.incentives.length) { -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- {{ 'labels.inputs.Entity Type' | translate }} - - {{ incentive.entityType | find: entityTypeData : 'id' : 'value' }} - - {{ 'labels.inputs.Attribute Name' | translate }} - - {{ incentive.attributeName | find: attributeNameData : 'id' : 'value' }} - - {{ 'labels.inputs.Condition Type' | translate }} - - {{ incentive.conditionType | find: conditionTypeData : 'id' : 'value' }} - - {{ 'labels.inputs.Attribute Value' | translate }} - - @switch (incentive.attributeName) { - @case (2) { - {{ incentive.attributeValue | find: genderData : 'id' : 'name' }} - } - @case (3) { - {{ incentive.attributeValue }} - } - @case (4) { - {{ - incentive.attributeValue | find: clientTypeData : 'id' : 'name' - }} - } - @case (5) { - {{ - incentive.attributeValue | find: clientClassificationData : 'id' : 'name' - }} - } - } - - {{ 'labels.inputs.Incentive Type' | translate }} - - {{ incentive.incentiveType | find: incentiveTypeData : 'id' : 'value' }} - {{ 'labels.inputs.Interest' | translate }} - {{ incentive.amount }} - {{ 'labels.inputs.Actions' | translate }} - - -
-
- } -
-
-
-
- } -
-
+ @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/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.scss b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.scss index e72e64e10..21f95779c 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.scss +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.scss @@ -148,3 +148,68 @@ 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/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.ts b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.ts index 8f6778ce0..ddb15f739 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.ts +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.ts @@ -7,7 +7,7 @@ */ /** Angular Imports */ -import { Component, OnInit, Input, inject } from '@angular/core'; +import { Component, OnInit, OnChanges, Input, inject } from '@angular/core'; import { UntypedFormGroup, UntypedFormBuilder, @@ -21,7 +21,6 @@ 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'; -import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; /** Dialog Components */ import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base'; @@ -31,11 +30,10 @@ 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, MatIconButton } from '@angular/material/button'; +import { MatButton } from '@angular/material/button'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatDivider } from '@angular/material/divider'; -import { MatTooltip } from '@angular/material/tooltip'; import { MatCheckbox } from '@angular/material/checkbox'; import { MatTable, @@ -68,8 +66,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; ...STANDALONE_SHARED_IMPORTS, FaIconComponent, MatDivider, - MatIconButton, - MatTooltip, + MatButton, MatCheckbox, MatTable, MatColumnDef, @@ -81,8 +78,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; MatHeaderRow, MatRowDef, MatRow, - MatStepperPrevious, - MatStepperNext, FindPipe ] }) @@ -94,6 +89,190 @@ export class RecurringDepositProductInterestRateChartStepComponent implements On private translateService = inject(TranslateService); @Input() recurringDepositProductsTemplate: 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; + } + } + } recurringDepositProductInterestRateChartForm: UntypedFormGroup; @@ -146,9 +325,9 @@ export class RecurringDepositProductInterestRateChartStepComponent implements On this.clientClassificationData = this.recurringDepositProductsTemplate.chartTemplate.clientClassificationOptions; this.incentiveTypeData = this.recurringDepositProductsTemplate.chartTemplate.incentiveTypeOptions; - if (!(this.recurringDepositProductsTemplate === undefined)) { - this.assignFormData(); - } + // assignFormData() intentionally not called — old chart form is removed from UI. + // The recurringDepositProductInterestRateChartForm is kept only for stepControl + // validity; it remains empty (valid) so the preview tab becomes available. } // Assign form the values for edit recurring Depoosit @@ -429,17 +608,6 @@ export class RecurringDepositProductInterestRateChartStepComponent implements On }); } - delete(formArray: UntypedFormArray, index: number) { - const dialogRef = this.dialog.open(DeleteDialogComponent, { - data: { deleteContext: `this` } - }); - dialogRef.afterClosed().subscribe((response: any) => { - if (response.delete) { - formArray.removeAt(index); - } - }); - } - getData(formType: string, values?: any) { switch (formType) { case 'Slab': @@ -541,6 +709,9 @@ export class RecurringDepositProductInterestRateChartStepComponent implements On delete chart.id; } } - return recurringDepositProductInterestRateChart; + return { + ...recurringDepositProductInterestRateChart, + rdSlabs: this._editableSlabs + }; } } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.html index 3c9a72fa5..acad7041a 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.html @@ -6,516 +6,638 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - -
-
- -

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

-
-
-

{{ recurringDepositProduct.name }}

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

{{ recurringDepositProduct.name }}

- -
-
- -

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

-
-
-
- {{ 'labels.inputs.Currency' | translate }}: - {{ - recurringDepositProduct.currencyCode | find: recurringDepositProductsTemplate.currencyOptions : 'code' : 'name' - }} -
-
- {{ 'labels.inputs.Decimal Places' | translate }}: - {{ - recurringDepositProduct.digitsAfterDecimal ?? recurringDepositProduct.currency?.decimalPlaces - }} -
- @if (recurringDepositProduct.inMultiplesOf) { -
- {{ 'labels.inputs.Currency in multiples of' | translate }}: - {{ recurringDepositProduct.inMultiplesOf }} -
- } -
-
+ + +

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

+ - -
-
- -

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

+
+ {{ 'labels.inputs.Short Name' | translate }}: + {{ recurringDepositProduct.shortName }}
-
-
- {{ 'labels.inputs.Deposit Amount' | translate }}: - {{ - recurringDepositProduct.depositAmount + - ' (Min: ' + - (recurringDepositProduct.minDepositAmount - ? recurringDepositProduct.minDepositAmount - : recurringDepositProduct.depositAmount) + - ', Max: ' + - (recurringDepositProduct.maxDepositAmount - ? recurringDepositProduct.maxDepositAmount - : recurringDepositProduct.depositAmount) + - ')' - }} -
-
- {{ 'labels.inputs.Interest Compounding Period' | translate }}: - {{ - recurringDepositProduct.interestCompoundingPeriodType - | find: recurringDepositProductsTemplate.interestCompoundingPeriodTypeOptions : 'id' : 'value' - }} -
-
- {{ 'labels.inputs.Interest Posting Period' | translate }}: - {{ - recurringDepositProduct.interestPostingPeriodType - | find: recurringDepositProductsTemplate.interestPostingPeriodTypeOptions : 'id' : 'value' - }} -
-
- {{ 'labels.inputs.Interest Calculated using' | translate }}: - {{ - recurringDepositProduct.interestCalculationType - | find: recurringDepositProductsTemplate.interestCalculationTypeOptions : 'id' : 'value' - }} -
-
- {{ 'labels.inputs.Days in Year' | translate }}: - {{ - recurringDepositProduct.interestCalculationDaysInYearType - | find: recurringDepositProductsTemplate.interestCalculationDaysInYearTypeOptions : 'id' : 'value' - }} -
-
-
- -
-
- -

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

-
-
- @if (recurringDepositProduct.lockinPeriodFrequency) { -
- {{ 'labels.inputs.Lock-in Period' | translate }}: - {{ - recurringDepositProduct.lockinPeriodFrequency + - ' ' + - (recurringDepositProduct.lockinPeriodFrequencyType - | find: recurringDepositProductsTemplate.lockinPeriodFrequencyTypeOptions : 'id' : 'value') - }} -
- } -
- {{ 'labels.inputs.Minimum Deposit Term' | translate }}: - {{ - recurringDepositProduct.minDepositTerm + - ' ' + - (recurringDepositProduct.minDepositTermTypeId - | find: recurringDepositProductsTemplate.periodFrequencyTypeOptions : 'id' : 'value') - }} + @if (recurringDepositProduct.description) { +
+ {{ 'labels.inputs.Description' | translate }}: + {{ recurringDepositProduct.description }}
- @if (recurringDepositProduct.inMultiplesOfDepositTerm) { -
- {{ 'labels.inputs.And thereafter, in Multiples of' | translate }}: - {{ - recurringDepositProduct.inMultiplesOfDepositTerm + - ' ' + - (recurringDepositProduct.inMultiplesOfDepositTermTypeId - | find: recurringDepositProductsTemplate.periodFrequencyTypeOptions : 'id' : 'value') - }} -
- } - @if (recurringDepositProduct.maxDepositTerm) { -
- {{ 'labels.inputs.Maximum Deposit Term' | translate }}: - {{ - recurringDepositProduct.maxDepositTerm + - ' ' + - (recurringDepositProduct.maxDepositTermTypeId - | find: recurringDepositProductsTemplate.periodFrequencyTypeOptions : 'id' : 'value') - }} -
- } -
- {{ 'labels.inputs.Apply Penal Interest (less)' | translate }}: - {{ recurringDepositProduct.preClosurePenalApplicable | yesNo }} -
- @if (recurringDepositProduct.preClosurePenalApplicable) { -
- {{ 'labels.inputs.Penal Interest' | translate }} (%): - {{ - recurringDepositProduct.preClosurePenalInterest + - '% (' + - (recurringDepositProduct.preClosurePenalInterestOnTypeId - | find: recurringDepositProductsTemplate.preClosurePenalInterestOnTypeOptions : 'id' : 'value') + - ')' - }} -
- } - @if (recurringDepositProduct.isMandatoryDeposit !== undefined) { -
- {{ 'labels.inputs.Recurring Deposit Type' | translate }}: - @if (recurringDepositProduct.isMandatoryDeposit) { - {{ 'labels.text.Mandatory' | translate }} - } - @if (!recurringDepositProduct.isMandatoryDeposit) { - {{ 'labels.text.Voluntary' | translate }} - } -
- } -
- {{ 'labels.inputs.Withhold Tax is Applicable' | translate }}: - {{ recurringDepositProduct.withHoldTax | yesNo }} -
- @if (recurringDepositProduct.withHoldTax) { -
- {{ 'labels.inputs.Tax Group' | translate }}: - {{ - recurringDepositProduct.taxGroupId | find: recurringDepositProductsTemplate.taxGroupOptions : 'id' : 'name' - }} -
- } -
-
+ } - -@if (recurringDepositProduct.charts.length !== 0) { -
-
- -

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

+ + @if ( + recurringDepositProduct.accountType || + recurringDepositProduct.customerSegment || + recurringDepositProduct.productCode || + recurringDepositProduct.effectiveTo || + recurringDepositProduct.introducerRequired !== undefined || + recurringDepositProduct.shareLinkingEnabled !== undefined + ) { +

Product Classification

+ + + @if (recurringDepositProduct.accountType) { +
+ Account Type: + {{ recurringDepositProduct.accountType }} +
+ } + + @if (recurringDepositProduct.customerSegment) { +
+ Customer Segment: + {{ recurringDepositProduct.customerSegment }} +
+ } + + @if (recurringDepositProduct.productCode) { +
+ Product Code: + {{ recurringDepositProduct.productCode }} +
+ } + + @if (recurringDepositProduct.effectiveTo) { +
+ Effective To: + {{ recurringDepositProduct.effectiveTo }} +
+ } + + @if (recurringDepositProduct.endDate) { +
+ End Date: + {{ recurringDepositProduct.endDate }} +
+ } + + @if (recurringDepositProduct.introducerRequired !== undefined) { +
+ Introducer Required: + {{ + recurringDepositProduct.introducerRequired === true || recurringDepositProduct.introducerRequired === 'Yes' + ? 'Yes' + : 'No' + }} +
+ } + + @if (recurringDepositProduct.shareLinkingEnabled !== undefined) { +
+ Share Linking Enabled: + {{ recurringDepositProduct.shareLinkingEnabled | yesNo }} +
+ } + } + + +

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

+ + +
+ {{ 'labels.inputs.Currency' | translate }}: + {{ + recurringDepositProduct.currencyCode | find: recurringDepositProductsTemplate.currencyOptions : 'code' : 'name' + }} +
+ +
+ {{ 'labels.inputs.Decimal Places' | translate }}: + {{ recurringDepositProduct.digitsAfterDecimal }} +
+ + @if (recurringDepositProduct.dormancyAlert !== undefined) { +
+ Dormancy Alert: + {{ recurringDepositProduct.dormancyAlert | yesNo }}
-
- @for (chart of recurringDepositProduct.charts; track chart; let chartIndex = $index) { -
- @if (chart.name) { -
- {{ 'labels.inputs.Name' | translate }}: - {{ chart.name }} -
- } -
- {{ 'labels.inputs.From Date' | translate }}: - {{ chart.fromDate | dateFormat }} -
- @if (chart.endDate) { -
- {{ 'labels.inputs.End Date' | translate }}: - {{ chart.endDate | dateFormat }} -
- } - @if (chart.description) { -
- {{ 'labels.inputs.Description' | translate }}: - {{ chart.description }} -
- } -
- {{ 'labels.inputs.Primary Grouping by Amount' | translate }}: - {{ recurringDepositProduct.isPrimaryGroupingByAmount | yesNo }} -
- @if (chart.chartSlabs.length !== 0) { -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Period' | translate }} - {{ - chartSlab.fromPeriod + - ' - ' + - chartSlab.toPeriod + - ' ' + - (chartSlab.periodType - | find: recurringDepositProductsTemplate.chartTemplate.periodTypes : 'id' : 'value') - }} - {{ 'labels.inputs.Amount Range' | translate }} - {{ chartSlab.amountRangeFrom + ' - ' + chartSlab.amountRangeTo }} - {{ 'labels.inputs.Interest' | translate }}{{ chartSlab.annualInterestRate | formatNumber }} %{{ 'labels.inputs.Description' | translate }}{{ chartSlab.description }}{{ 'labels.inputs.Actions' | translate }} - - -
- -

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

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Entity Type' | translate }} - {{ - incentive.entityType - | find - : recurringDepositProductsTemplate.chartTemplate.entityTypeOptions - : 'id' - : 'value' - }} - - {{ 'labels.inputs.Attribute Name' | translate }} - - {{ - incentive.attributeName - | find - : recurringDepositProductsTemplate.chartTemplate.attributeNameOptions - : 'id' - : 'value' - }} - - {{ 'labels.inputs.Condition Type' | translate }} - - {{ - incentive.conditionType - | find - : recurringDepositProductsTemplate.chartTemplate.conditionTypeOptions - : 'id' - : 'value' - }} - - {{ 'labels.inputs.Attribute Value' | translate }} - - @switch (incentive.attributeName) { - @case (2) { - {{ - incentive.attributeValue - | find - : recurringDepositProductsTemplate.chartTemplate.genderOptions - : 'id' - : 'name' - }} - } - @case (3) { - {{ incentive.attributeValue }} - } - @case (4) { - {{ - incentive.attributeValue - | find - : recurringDepositProductsTemplate.chartTemplate.clientTypeOptions - : 'id' - : 'name' - }} - } - @case (5) { - {{ - incentive.attributeValue - | find - : recurringDepositProductsTemplate.chartTemplate.clientClassificationOptions - : 'id' - : 'name' - }} - } - } - - {{ 'labels.inputs.Incentive Type' | translate }} - - {{ - incentive.incentiveType - | find - : recurringDepositProductsTemplate.chartTemplate.incentiveTypeOptions - : 'id' - : 'value' - }} - {{ 'labels.inputs.Amount' | translate }} - {{ incentive.amount | formatNumber }} -
-
-
-
-
-
- } + } + + @if (recurringDepositProduct.balanceSwapOption !== undefined) { +
+ Balance Swap Option: + {{ recurringDepositProduct.balanceSwapOption | yesNo }} +
+ } + + @if (recurringDepositProduct.msme !== undefined) { +
+ MSME: + {{ recurringDepositProduct.msme | yesNo }} +
+ } + + @if (recurringDepositProduct.nominationFacility !== undefined) { +
+ Nomination Facility: + {{ recurringDepositProduct.nominationFacility | yesNo }} +
+ } + + @if (recurringDepositProduct.autoDebitFacility !== undefined) { +
+ Auto Debit Facility: + {{ recurringDepositProduct.autoDebitFacility | yesNo }} +
+ } + + +

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

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

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

+ + +
+ Loan Facility: + {{ recurringDepositProduct.loanFacility | yesNo }} +
+ +
+ Premature Withdrawal: + {{ recurringDepositProduct.prematureWithdrawalOption | yesNo }} +
+ +
+ Tenure Flexibility: + {{ recurringDepositProduct.tenureFlexibility | yesNo }} +
+ +
+ Lien Marked: + {{ recurringDepositProduct.lienMarked | yesNo }} +
+ +
+ {{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}: + {{ recurringDepositProduct.withdrawalFeeForTransfers | yesNo }} +
+ +
+ Enable Lock-in Period: + {{ recurringDepositProduct.enableLockinPeriod | yesNo }} +
+ + @if (recurringDepositProduct.enableLockinPeriod && recurringDepositProduct.lockinPeriodFrequency) { +
+ Lock-in Period: + + {{ recurringDepositProduct.lockinPeriodFrequency }} + {{ getLockinPeriodFrequencyTypeLabel(recurringDepositProduct.lockinPeriodFrequencyType) }} + +
+ } + +
+ Maturity Instructions: + {{ recurringDepositProduct.maturityInstructions | yesNo }} +
+ +
+ Credit Maturity Proceeds: + {{ recurringDepositProduct.creditMaturityProceeds | yesNo }} +
+ +
+ Penalty Grace Period (Missed Installments): + {{ recurringDepositProduct.penaltyGracePeriodMissedInstallments | yesNo }} +
+ +
+ Panel Request Applicable: + {{ recurringDepositProduct.panelRequestApplicable | yesNo }} +
+ +
+ No Penalty (Nominee / Heir Closure): + {{ recurringDepositProduct.noPenaltyNomineeHeirClosure | yesNo }} +
+ +
+ Deceased Settlement: + {{ recurringDepositProduct.deceasedSettlement | yesNo }} +
+ +
+ Allow Debit Txn After Due Date: + {{ recurringDepositProduct.allowDebitTxnAfterDueDate | yesNo }} +
+ +
+ Account Closed with Zero Balance in Day Closing: + {{ recurringDepositProduct.accountClosedZeroBalanceInDayClosing | yesNo }} +
+ + @if (recurringDepositProduct.minBalanceForInterestCalculation) { +
+ Min Balance for Interest Calculation: + {{ recurringDepositProduct.minBalanceForInterestCalculation | formatNumber }} +
+ } + + + @if (recurringDepositProduct.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 (recurringDepositProduct.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( + recurringDepositProduct.savingsReferenceAccountId, + recurringDepositProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }}
+ @if (isAccrualAccounting()) { +
+ Fees Receivable Account + {{ + formatGlAccountById( + recurringDepositProduct.feesReceivableAccountId, + recurringDepositProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }} +
+
+ Penalties Receivable Account + {{ + formatGlAccountById( + recurringDepositProduct.penaltiesReceivableAccountId, + recurringDepositProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }} +
+ } +
+ +
+
Liabilities
+
+ Savings Control Account + {{ + formatGlAccountById( + recurringDepositProduct.savingsControlAccountId, + recurringDepositProductsTemplate?.accountingMappingOptions.liabilityAccountOptions + ) + }} +
+
+ Transfers In Suspense Account + {{ + formatGlAccountById( + recurringDepositProduct.transfersInSuspenseAccountId, + recurringDepositProductsTemplate?.accountingMappingOptions.liabilityAccountOptions + ) + }} +
+ @if (isAccrualAccounting()) { +
+ Interest Payable Account + {{ + formatGlAccountById( + recurringDepositProduct.interestPayableAccountId, + recurringDepositProductsTemplate?.accountingMappingOptions.liabilityAccountOptions + ) + }} +
+ } +
+ +
+
Expenses
+
+ Interest on Savings Account + {{ + formatGlAccountById( + recurringDepositProduct.interestOnSavingsAccountId, + recurringDepositProductsTemplate?.accountingMappingOptions.expenseAccountOptions + ) + }} +
+
+ +
+
Income
+
+ Income from Fees + {{ + formatGlAccountById( + recurringDepositProduct.incomeFromFeeAccountId, + recurringDepositProductsTemplate?.accountingMappingOptions.incomeAccountOptions + ) + }} +
+
+ Income from Penalties + {{ + formatGlAccountById( + recurringDepositProduct.incomeFromPenaltyAccountId, + recurringDepositProductsTemplate?.accountingMappingOptions.incomeAccountOptions + ) + }} +
+
+
+ } + + + @if (recurringDepositProduct.otherGlMappings) { +
+
+

Other GL Mappings

+
+ + @if (recurringDepositProduct.otherGlMappings.assets?.length) { + + + + + + + + + + + +
Asset Account{{ row.accountName }}GL Account + {{ + formatGlAccountByCode( + row.glCode, + recurringDepositProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }} +
+ } + + @if (recurringDepositProduct.otherGlMappings.liabilities?.length) { + + + + + + + + + + + +
Liability Account{{ row.accountName }}GL Account + {{ + formatGlAccountByCode( + row.glCode, + recurringDepositProductsTemplate?.accountingMappingOptions.liabilityAccountOptions + ) + }} +
+ } + + @if (recurringDepositProduct.otherGlMappings.expenses?.length) { + + + + + + + + + + + +
Expense Account{{ row.accountName }}GL Account + {{ + formatGlAccountByCode( + row.glCode, + recurringDepositProductsTemplate?.accountingMappingOptions.expenseAccountOptions + ) + }} +
+ } + + @if (recurringDepositProduct.otherGlMappings.income?.length) { + + + + + + + + + + + +
Income Account{{ row.accountName }}GL Account + {{ + formatGlAccountByCode( + row.glCode, + recurringDepositProductsTemplate?.accountingMappingOptions.incomeAccountOptions + ) + }} +
}
-
-} + } - -@if (recurringDepositProduct.charges.length) { -
-
- -

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

-
-
-
- - - - - - - - - - - - - - - - - - - -
{{ '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.heading.Accounting' | translate }}

-
-
- @if (!isNoneAccounting()) { - - - } - - @if ( - recurringDepositProduct.advancedAccountingRules && - (recurringDepositProduct.paymentChannelToFundSourceMappings?.length || - recurringDepositProduct.feeToIncomeAccountMappings?.length || - recurringDepositProduct.penaltyToIncomeAccountMappings?.length) - ) { -

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

+ + @if ( + recurringDepositProduct.advancedAccountingRules && + (recurringDepositProduct.paymentChannelToFundSourceMappings?.length || + recurringDepositProduct.feeToIncomeAccountMappings?.length || + recurringDepositProduct.penaltyToIncomeAccountMappings?.length) + ) { +
+

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

+ @if (recurringDepositProduct.paymentChannelToFundSourceMappings?.length) { -
{{ 'labels.heading.Fund Sources for Payment Channels' | translate }}
-
- +
+

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

+
- - - + @@ -352,7 +363,7 @@ } - + @if (recurringDepositProduct.charges?.length) {

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

@@ -380,7 +391,7 @@
} - +

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

diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.html index 65358d314..df3d7e0f7 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.html @@ -54,14 +54,22 @@ {{ 'labels.inputs.Interest Calculation Type' | translate }} - @for (interestCalculationType of interestCalculationTypeOptionData; track interestCalculationType.id) { - - {{ interestCalculationType.value | translateKey: 'catalogs' }} - + @for (opt of interestCalculationTypeOptionData; track opt.id) { + {{ opt.value }} + } + + + + +
+ + Interest Calculate + + @for (mode of interestCalculationTypeData; track mode.id) { + {{ mode.value | translateKey: 'catalogs' }} } @@ -69,19 +77,8 @@ {{ 'labels.commons.required' | translate }} -
-
- - Interest Calculate - - @for (mode of interestCalculationTypeData; track mode.id) { - {{ mode.value }} - } - - - - @if (recurringDepositProductTermsForm.value.interestCalculateSelector === 2) { + @if (recurringDepositProductTermsForm.value.interestCalculationType === 2) { Average Period @@ -142,7 +139,7 @@
- @if (recurringDepositProductTermsForm.value.interestCalculateSelector === 1) { + @if (recurringDepositProductTermsForm.value.interestCalculationType === 1) {
⚠️ 2) { - this.recurringDepositProductTermsForm.patchValue({ interestCalculationType: 1 }); - } } private parseDate(dateValue: any): Date | null { @@ -163,10 +159,6 @@ export class RecurringDepositProductTermsStepComponent implements OnInit { if (formValue.lastInterestDate instanceof Date) { formValue.lastInterestDate = this.dateUtils.formatDate(formValue.lastInterestDate, dateFormat) || ''; } - if (formValue.interestCalculationType && formValue.interestCalculationType > 2) { - formValue.interestCalculationType = 1; - } - for (const key in formValue) { if (formValue[key] === '') { delete formValue[key]; diff --git a/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.html b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.html index 241b30d61..09319e1dc 100644 --- a/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.html +++ b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.html @@ -141,16 +141,16 @@ {{ recurringDepositProduct.effectiveDate | dateFormat }}
} -
- {{ 'labels.inputs.Interest Calculation Type' | translate }}: - {{ recurringDepositProduct.interestCalculationType.value }} -
- @if (recurringDepositProduct.interestCalculateSelector !== undefined) { + @if (recurringDepositProduct.interestCalculationTypeName) {
- Interest Calculate: - {{ recurringDepositProduct.interestCalculateSelector }} + {{ 'labels.inputs.Interest Calculation Type' | translate }}: + {{ recurringDepositProduct.interestCalculationTypeName }}
} +
+ Interest Calculate: + {{ recurringDepositProduct.interestCalculationType?.value }} +
@if (recurringDepositProduct.averagePeriod) {
Average Period: diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.html b/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.html index c75a6a993..1fbae2866 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.html +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.html @@ -1,4 +1,4 @@ - - {{ 'labels.inputs.DETAILS' | translate }} + Product Classification - - {{ 'labels.inputs.TERMS' | translate }} + + + Currency - + + + +
+ + +
+ + + Term + + + + +
+ + +
+
+ + + + {{ 'labels.inputs.SETTINGS' | translate }} + + + + +
+ + +
+
+ + + + Digital + + + + +
+ + +
+
+ + {{ 'labels.inputs.CHARGES' | translate }} @@ -63,6 +133,7 @@ + @if (savingsAccountFormValid) { {{ 'labels.inputs.PREVIEW' | translate }} diff --git a/src/app/savings/create-savings-account/create-savings-account.component.ts b/src/app/savings/create-savings-account/create-savings-account.component.ts index 8ee38716a..134b23abe 100644 --- a/src/app/savings/create-savings-account/create-savings-account.component.ts +++ b/src/app/savings/create-savings-account/create-savings-account.component.ts @@ -12,8 +12,11 @@ import { Router, ActivatedRoute } from '@angular/router'; /** Custom Components */ import { SavingsAccountDetailsStepComponent } from '../savings-account-stepper/savings-account-details-step/savings-account-details-step.component'; -import { SavingsAccountTermsStepComponent } from '../savings-account-stepper/savings-account-terms-step/savings-account-terms-step.component'; +import { SavingsAccountTermStepComponent } from '../savings-account-stepper/savings-account-term-step/savings-account-term-step.component'; +import { SavingsAccountSettingsStepComponent } from '../savings-account-stepper/savings-account-settings-step/savings-account-settings-step.component'; import { SavingsAccountChargesStepComponent } from '../savings-account-stepper/savings-account-charges-step/savings-account-charges-step.component'; +import { SavingsAccountCurrencyStepComponent } from '../savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component'; +import { SavingsAccountDigitalStepComponent } from '../savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component'; /** Custom Services */ import { SavingsService } from '../savings.service'; @@ -39,8 +42,11 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; MatStep, MatStepLabel, SavingsAccountDetailsStepComponent, - SavingsAccountTermsStepComponent, + SavingsAccountCurrencyStepComponent, + SavingsAccountTermStepComponent, + SavingsAccountSettingsStepComponent, SavingsAccountChargesStepComponent, + SavingsAccountDigitalStepComponent, SavingsAccountPreviewStepComponent ] }) @@ -56,75 +62,58 @@ export class CreateSavingsAccountComponent { /** Savings Account Product Template */ savingsAccountProductTemplate: any; - /** Savings Account Details Step */ @ViewChild(SavingsAccountDetailsStepComponent, { static: true }) savingsAccountDetailsStep: SavingsAccountDetailsStepComponent; - /** Savings Account Terms Step */ - @ViewChild(SavingsAccountTermsStepComponent, { static: true }) - savingsAccountTermsStep: SavingsAccountTermsStepComponent; - /** Savings Account Charges Step */ + + @ViewChild(SavingsAccountTermStepComponent, { static: true }) + savingsAccountTermStep: SavingsAccountTermStepComponent; + + @ViewChild(SavingsAccountSettingsStepComponent, { static: true }) + savingsAccountSettingsStep: SavingsAccountSettingsStepComponent; + @ViewChild(SavingsAccountChargesStepComponent, { static: true }) savingsAccountChargesStep: SavingsAccountChargesStepComponent; - /** - * Fetches savings account template from `resolve` - * @param {ActivatedRoute} route Activated Route - * @param {Router} router Router - * @param {Dates} dateUtils Date Utils - * @param {SavingsService} savingsService Savings Service - * @param {SettingsService} settingsService Settings Service - */ constructor() { this.route.data.subscribe((data: { savingsAccountTemplate: any }) => { this.savingsAccountTemplate = data.savingsAccountTemplate; }); } - /** - * Sets savings account product template. - * @param {any} $event API response - */ setTemplate($event: any) { this.savingsAccountProductTemplate = $event; } - /** - * Retrieves savings account details form. - */ get savingsAccountDetailsForm() { return this.savingsAccountDetailsStep.savingsAccountDetailsForm; } - /** - * Retrieves savings account terms form. - */ get savingsAccountTermsForm() { - return this.savingsAccountTermsStep.savingsAccountTermsForm; + return this.savingsAccountTermStep.savingsAccountTermForm; + } + + get savingsAccountSettingsForm() { + return this.savingsAccountSettingsStep.savingsAccountSettingsForm; } - /** - * Checks validity of overall savings account form. - */ get savingsAccountFormValid() { - return this.savingsAccountDetailsForm.valid && this.savingsAccountTermsForm.valid; + return ( + this.savingsAccountDetailsForm.valid && + this.savingsAccountTermsForm.valid && + this.savingsAccountSettingsForm.valid + ); } - /** - * Retrieves savings account object. - */ get savingsAccount() { return { ...this.savingsAccountDetailsStep.savingsAccountDetails, - ...this.savingsAccountTermsStep.savingsAccountTerms, + ...this.savingsAccountTermStep.savingsAccountTerms, + ...this.savingsAccountSettingsStep.savingsAccountSettings, ...this.savingsAccountChargesStep.savingsAccountCharges }; } - /** - * Creates a new share account. - */ submit() { - // TODO: Update once language and date settings are setup const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; const monthDayFormat = 'dd MMMM'; diff --git a/src/app/savings/savings-account-stepper/savings-account-charges-step/savings-account-charges-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-charges-step/savings-account-charges-step.component.ts index 78ac98e4b..fc6e7b6b5 100644 --- a/src/app/savings/savings-account-stepper/savings-account-charges-step/savings-account-charges-step.component.ts +++ b/src/app/savings/savings-account-stepper/savings-account-charges-step/savings-account-charges-step.component.ts @@ -7,7 +7,7 @@ */ /** Angular Imports */ -import { Component, OnInit, OnChanges, Input, inject } from '@angular/core'; +import { Component, OnInit, Input, inject } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { UntypedFormControl } from '@angular/forms'; @@ -69,13 +69,25 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; FormatNumberPipe ] }) -export class SavingsAccountChargesStepComponent implements OnInit, OnChanges { +export class SavingsAccountChargesStepComponent implements OnInit { private dialog = inject(MatDialog); private dateUtils = inject(Dates); private translateService = inject(TranslateService); + private _savingsAccountProductTemplate: any; + /** Savings Account Product Template */ - @Input() savingsAccountProductTemplate: any; + @Input() + set savingsAccountProductTemplate(value: any) { + this._savingsAccountProductTemplate = value; + if (value) { + this.chargeData = value.chargeOptions || []; + this.chargesDataSource = (value.charges || []).map((charge: any) => ({ ...charge, id: charge.chargeId })); + } + } + get savingsAccountProductTemplate(): any { + return this._savingsAccountProductTemplate; + } /** Savings Account Template */ @Input() savingsAccountTemplate: any; /** Currency Code */ @@ -126,14 +138,6 @@ export class SavingsAccountChargesStepComponent implements OnInit, OnChanges { } } - ngOnChanges() { - if (this.savingsAccountProductTemplate) { - this.chargeData = this.savingsAccountProductTemplate.chargeOptions; - this.chargesDataSource = - this.savingsAccountProductTemplate.charges.map((charge: any) => ({ ...charge, id: charge.chargeId })) || []; - } - } - /** * Adds the charge to charges table * @param {any} charge Charge diff --git a/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.html b/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.html new file mode 100644 index 000000000..10c787094 --- /dev/null +++ b/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.html @@ -0,0 +1,49 @@ + + +
+
+ + Currency & Account Status Configuration +
+
+
+ + Currency + + Inherited from selected product + + + + Decimal Places + + Inherited from selected product + +
+ +
+
+

Dormancy Alert

+

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

+
+ + {{ savingsAccountProductTemplate?.dormancyAlert ? 'Yes' : 'No' }} + +
+ +
+
+

Nomination Facility

+

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

+
+ + {{ savingsAccountProductTemplate?.nominationFacility ? 'Yes' : 'No' }} + +
+
+
diff --git a/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.scss b/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.scss new file mode 100644 index 000000000..17712a898 --- /dev/null +++ b/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.scss @@ -0,0 +1,116 @@ +/** + * 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; + +.sac-section { + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; + 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; + } + } +} + +.sac-body { + background: #fff; +} + +.sac-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px 16px; + padding: 16px 24px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.sac-field { + width: 100%; +} + +.sac-alert { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + padding: 18px 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; + } + } +} + +.sac-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 48px; + padding: 4px 14px; + border-radius: 999px; + font-size: 12px; + font-weight: 700; + background: #fee2e2; + color: #b91c1c; + border: 1px solid #fecaca; + + &--yes { + background: #dcfce7; + color: #15803d; + border-color: #bbf7d0; + } +} diff --git a/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.ts new file mode 100644 index 000000000..553256ce0 --- /dev/null +++ b/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.ts @@ -0,0 +1,20 @@ +/** + * 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 } from '@angular/core'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-savings-account-currency-step', + templateUrl: './savings-account-currency-step.component.html', + styleUrls: ['./savings-account-currency-step.component.scss'], + imports: [...STANDALONE_SHARED_IMPORTS] +}) +export class SavingsAccountCurrencyStepComponent { + @Input() savingsAccountProductTemplate: any; +} diff --git a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html index 6f492fa09..df5db2234 100644 --- a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html +++ b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html @@ -1,4 +1,4 @@ -
-
- - {{ 'labels.inputs.Product Name' | translate }} - - @for (product of productData; track product) { - - {{ product.name }} - - } - - - {{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - @if (savingsProductSelected) { - - {{ 'labels.inputs.Submitted On' | translate }} - - - + +
+
+ +

Account Details

+
+
+ + {{ 'labels.inputs.Product Name' | translate }} + + @for (product of productData; track product) { + {{ product.name }} + } + - {{ 'labels.inputs.Submitted On' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} - } - @if (savingsProductSelected) { - - {{ 'labels.inputs.Field Officer' | translate }} - - @for (officer of fieldOfficerData; track officer) { - - {{ officer.displayName }} - - } - - - } + @if (savingsProductSelected) { + + {{ 'labels.inputs.Submitted On' | translate }} + + + + + {{ 'labels.inputs.Submitted On' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + } - @if (savingsProductSelected) { - - {{ 'labels.inputs.External ID' | translate }} - - - } + @if (savingsProductSelected) { + + {{ 'labels.inputs.Field Officer' | translate }} + + @for (officer of fieldOfficerData; track officer) { + {{ officer.displayName }} + } + + + } + + @if (savingsProductSelected) { + + {{ 'labels.inputs.External ID' | translate }} + + + } +
+ + @if (savingsProductSelected) { +
+
+ +

Product Classification

+ From Product +
+
+ + Account Type + + + + + Customer Segment + + + + + Product Code + + + + @if (productTemplate.effectiveDate) { + + Effective Date + + + } + + @if (productTemplate.endDate) { + + End Date + + + } + + + Introducer Required + + + +
+ Share Linking Enabled + + {{ productTemplate.shareLinkingEnabled ? 'Yes' : 'No' }} + +
+
+
+ } +
{{ 'labels.inputs.Payment Type' | translate }} - {{ - paymentFundSource.paymentTypeId - | find: recurringDepositProductsTemplate.paymentTypeOptions : 'id' : 'name' - }} + + {{ item.paymentTypeId | find: recurringDepositProductsTemplate.paymentTypeOptions : 'id' : 'name' }} {{ 'labels.inputs.Fund Source' | translate }} + {{ - paymentFundSource.fundSourceAccountId + item.fundSourceAccountId | find - : recurringDepositProductsTemplate.accountingMappingOptions.assetAccountOptions + : recurringDepositProductsTemplate?.accountingMappingOptions.assetAccountOptions : 'id' : 'name' }} @@ -528,22 +650,26 @@ } @if (recurringDepositProduct.feeToIncomeAccountMappings?.length) { -
{{ 'labels.heading.Fees to Specific Income Accounts' | translate }}
-
- +
+

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

+
- - + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.Fees' | translate }} - {{ feesIncome.chargeId | find: recurringDepositProductsTemplate.chargeOptions : 'id' : 'name' }} + + {{ item.chargeId | find: recurringDepositProductsTemplate.chargeOptions : 'id' : 'name' }} {{ 'labels.inputs.Income Account' | translate }} + {{ - feesIncome.incomeAccountId + item.incomeAccountId | find - : recurringDepositProductsTemplate.accountingMappingOptions.incomeAccountOptions + : recurringDepositProductsTemplate?.accountingMappingOptions.incomeAccountOptions : 'id' : 'name' }} @@ -556,22 +682,26 @@ } @if (recurringDepositProduct.penaltyToIncomeAccountMappings?.length) { -
{{ 'labels.heading.Penalties to Specific Income Accounts' | translate }}
-
- +
+

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

+
- -
{{ 'labels.inputs.Penalty' | translate }} - {{ penaltyIncome.chargeId | find: recurringDepositProductsTemplate.penaltyOptions : 'id' : 'name' }} + + {{ item.chargeId | find: recurringDepositProductsTemplate.penaltyOptions : 'id' : 'name' }} {{ 'labels.inputs.Income Account' | translate }} + {{ - penaltyIncome.incomeAccountId + item.incomeAccountId | find - : recurringDepositProductsTemplate.accountingMappingOptions.incomeAccountOptions + : recurringDepositProductsTemplate?.accountingMappingOptions.incomeAccountOptions : 'id' : 'name' }} @@ -582,6 +712,6 @@
} - } - + + } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.scss b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.scss index b00fc9c5c..33a0e1852 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.scss +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.scss @@ -6,130 +6,126 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -.rdpp-section { - border: 1px solid #e2e8f0; - border-radius: 10px; - overflow: hidden; - background: #fff; +@use 'assets/styles/colours' as *; - & + & { - 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; - } - } - - &__body { - padding: 16px 24px; - } +.mat-h3 { + margin-top: 10px !important; + font-weight: 300; + color: $status-approved; } -.rdpp-product-name { - margin: 0 0 12px; - font-size: 18px; - font-weight: 600; - color: #202124; +.mat-elevation-z1 { + margin: 1em 0 1.5em; } -.rdpp-row { - display: flex; - padding: 6px 0; - border-bottom: 1px solid #f1f3f4; - - &:last-child { - border-bottom: none; - } -} - -.rdpp-label { - flex: 0 0 40%; - font-size: 13px; +h2, +h3, +h4 { + margin: 0; font-weight: 500; - color: #5f6368; -} - -.rdpp-value { - flex: 1; - font-size: 13px; - color: #202124; -} - -.rdpp-table-wrapper { - border: 1px solid #e2e8f0; - border-radius: 8px; - overflow: hidden; - margin-top: 8px; - - table { - width: 100%; - } - - & + & { - margin-top: 12px; - } -} - -.rdpp-sub-heading { - margin: 16px 0 8px; - font-size: 14px; - font-weight: 600; - color: #0ea5e9; -} - -.rdpp-sub-heading-sm { - margin: 12px 0 4px; - font-size: 13px; - font-weight: 600; - color: #202124; -} - -.rdpp-chart-block { - & + & { - margin-top: 16px; - padding-top: 16px; - border-top: 1px solid #e2e8f0; - } -} - -.rdpp-incentives { - overflow: hidden; - - &__card { - border-radius: 0; - padding: 12px 16px; - - h4 { - margin: 0 0 8px; - font-size: 13px; - font-weight: 600; - } - } } table { - tr.incentives-row { - height: 0; + width: 100%; +} - > td { - padding: 0; - } +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/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.ts b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.ts index d259e2e75..dadf5d011 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.ts +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.ts @@ -7,9 +7,9 @@ */ import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges, inject } from '@angular/core'; -import { trigger, state, transition, animate, style } from '@angular/animations'; -import { OptionData } from 'app/shared/models/option-data.model'; 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, @@ -23,7 +23,6 @@ import { MatRow } from '@angular/material/table'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; -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'; @@ -34,15 +33,9 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; selector: 'mifosx-recurring-deposit-product-preview-step', templateUrl: './recurring-deposit-product-preview-step.component.html', styleUrls: ['./recurring-deposit-product-preview-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, + MatDivider, MatTable, MatColumnDef, MatHeaderCellDef, @@ -54,7 +47,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; MatHeaderRow, MatRowDef, MatRow, - ViewSavingsAccountingDetailsComponent, FindPipe, DateFormatPipe, FormatNumberPipe, @@ -65,20 +57,11 @@ export class RecurringDepositProductPreviewStepComponent implements OnInit, OnCh private accounting = inject(Accounting); @Input() recurringDepositProductsTemplate: any; - @Input() chartSlabsDisplayedColumns: any[]; + @Input() chartSlabsDisplayedColumns: any[] = []; @Input() accountingRuleData: any; @Input() recurringDepositProduct: any; @Output() submitEvent = new EventEmitter(); - chartSlabsIncentivesDisplayedColumns: string[] = ['incentives']; - incentivesDisplayedColumns: string[] = [ - 'entityType', - 'attributeName', - 'conditionType', - 'attributeValue', - 'incentiveType', - 'amount' - ]; chargesDisplayedColumns: string[] = [ 'name', 'chargeCalculationType', @@ -94,72 +77,28 @@ export class RecurringDepositProductPreviewStepComponent implements OnInit, OnCh 'incomeAccountId' ]; - expandChartSlabIndex: number[] = []; + readonly rdSlabColumns = [ + 'srNo', + 'description', + 'fromPeriod', + 'toPeriod', + 'periodTypeLabel', + 'annualInterestRate' + ]; - accountingMappings: any = {}; - accountingRule: OptionData; + accountingRule: OptionData | undefined; ngOnInit() { this.setCurrentValues(); } - ngOnChanges(changes: SimpleChanges): void { + ngOnChanges(_changes: SimpleChanges): void { this.setCurrentValues(); } setCurrentValues(): void { - if (this.isCashOrAccrualAccounting()) { + if (this.recurringDepositProduct) { this.accountingRule = this.accounting.getAccountingRuleFrom(this.recurringDepositProduct.accountingRule); - - const assetAccountData = this.recurringDepositProductsTemplate.accountingMappingOptions.assetAccountOptions || []; - const incomeAccountData = - this.recurringDepositProductsTemplate.accountingMappingOptions.incomeAccountOptions || []; - const expenseAccountData = - this.recurringDepositProductsTemplate.accountingMappingOptions.expenseAccountOptions || []; - const liabilityAccountData = - this.recurringDepositProductsTemplate.accountingMappingOptions.liabilityAccountOptions || []; - - this.accountingMappings = { - savingsReferenceAccount: this.accounting.glAccountLookUp( - this.recurringDepositProduct.savingsReferenceAccountId, - assetAccountData - ), - savingsControlAccount: this.accounting.glAccountLookUp( - this.recurringDepositProduct.savingsControlAccountId, - liabilityAccountData - ), - transfersInSuspenseAccount: this.accounting.glAccountLookUp( - this.recurringDepositProduct.transfersInSuspenseAccountId, - liabilityAccountData - ), - interestOnSavingsAccount: this.accounting.glAccountLookUp( - this.recurringDepositProduct.interestOnSavingsAccountId, - expenseAccountData - ), - incomeFromFeeAccount: this.accounting.glAccountLookUp( - this.recurringDepositProduct.incomeFromFeeAccountId, - incomeAccountData - ), - incomeFromPenaltyAccount: this.accounting.glAccountLookUp( - this.recurringDepositProduct.incomeFromPenaltyAccountId, - incomeAccountData - ) - }; - - if (this.isAccrualAccounting()) { - this.accountingMappings['feeReceivableAccount'] = this.accounting.glAccountLookUp( - this.recurringDepositProduct.feesReceivableAccountId, - assetAccountData - ); - this.accountingMappings['penaltyReceivableAccount'] = this.accounting.glAccountLookUp( - this.recurringDepositProduct.penaltiesReceivableAccountId, - assetAccountData - ); - this.accountingMappings['interestPayableAccount'] = this.accounting.glAccountLookUp( - this.recurringDepositProduct.interestPayableAccountId, - liabilityAccountData - ); - } } } @@ -174,4 +113,23 @@ export class RecurringDepositProductPreviewStepComponent implements OnInit, OnCh isAccrualAccounting(): boolean { return this.accounting.isAccrualAccountingRuleId(this.recurringDepositProduct.accountingRule); } + + getLockinPeriodFrequencyTypeLabel(value: any): string { + if (!value && value !== 0) return ''; + const options = this.recurringDepositProductsTemplate?.lockinPeriodFrequencyTypeOptions || []; + const found = options.find((o: any) => o.id === value); + return found?.value || String(value); + } + + 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/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.ts b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.ts index ccef15fff..f19d25d85 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.ts +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.ts @@ -72,7 +72,7 @@ export class RecurringDepositProductTermsStepComponent implements OnInit { : this.interestCalculationTypeOptionData?.[0]?.id || 1, interestCalculationDaysInYearType: this.recurringDepositProductsTemplate.interestCalculationDaysInYearType?.id, effectiveDate: this.recurringDepositProductsTemplate.effectiveDate - ? new Date(this.recurringDepositProductsTemplate.effectiveDate) + ? this.parseDate(this.recurringDepositProductsTemplate.effectiveDate) : '', interestCalculateSelector: isEditing ? this.recurringDepositProductsTemplate.interestCalculateSelector || 1 @@ -83,7 +83,7 @@ export class RecurringDepositProductTermsStepComponent implements OnInit { autoPost: isEditing ? (this.recurringDepositProductsTemplate.autoPost ?? true) : true, frequencyFixDay: isEditing ? this.recurringDepositProductsTemplate.frequencyFixDay || 30 : 30, lastInterestDate: this.recurringDepositProductsTemplate.lastInterestDate - ? new Date(this.recurringDepositProductsTemplate.lastInterestDate) + ? this.parseDate(this.recurringDepositProductsTemplate.lastInterestDate) : '' }); @@ -93,6 +93,15 @@ export class RecurringDepositProductTermsStepComponent implements OnInit { } } + 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 diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-products.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-products.component.html index 5523e15cc..a2e463e62 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-products.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-products.component.html @@ -20,6 +20,18 @@ {{ 'labels.buttons.Create Recurring Deposit Product' | translate }} +
+ +
diff --git a/src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates-tab.component.html b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates-tab.component.html new file mode 100644 index 000000000..2295a36d2 --- /dev/null +++ b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-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/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates-tab.component.scss b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates-tab.component.scss new file mode 100644 index 000000000..1728940c9 --- /dev/null +++ b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-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/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates-tab.component.ts b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates-tab.component.ts new file mode 100644 index 000000000..79c7ac552 --- /dev/null +++ b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-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 RdInterestRatesTabComponent { + 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/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates.resolver.ts b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates.resolver.ts new file mode 100644 index 000000000..68e82a666 --- /dev/null +++ b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-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/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.html b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.html index 004fb3daa..241b30d61 100644 --- a/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.html +++ b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.html @@ -6,546 +6,629 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
-
-
- - -
-

{{ recurringDepositProduct.name }}

+
+

{{ recurringDepositProduct.name }}

- + + +

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

+ -

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

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

Product Classification

+ + + @if (recurringDepositProduct.accountType) { +
+ Account Type: + {{ recurringDepositProduct.accountType }} +
+ } + @if (recurringDepositProduct.customerSegment) { +
+ Customer Segment: + {{ recurringDepositProduct.customerSegment }} +
+ } + @if (recurringDepositProduct.productCode) { +
+ Product Code: + {{ recurringDepositProduct.productCode }} +
+ } + @if (recurringDepositProduct.effectiveTo) { +
+ Effective To: + {{ recurringDepositProduct.effectiveTo | dateFormat }} +
+ } + @if (recurringDepositProduct.endDate) { +
+ End Date: + {{ recurringDepositProduct.endDate | dateFormat }} +
+ } +
+ Introducer Required: + {{ recurringDepositProduct.introducerRequired | yesNo }} +
+
+ Share Linking Enabled: + {{ recurringDepositProduct.shareLinkingEnabled | yesNo }} +
+ + +

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

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

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

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

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

+ + + +

Premature Option

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

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

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

Maturity Instructions

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

Additional Rules

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

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

+ +
+ @if (recurringDepositProduct.activeChart.name) { +
+ {{ 'labels.inputs.Name' | translate }}: + {{ recurringDepositProduct.activeChart.name }} +
+ }
- {{ 'labels.inputs.Short Name' | translate }}: - {{ recurringDepositProduct.shortName }} + {{ 'labels.inputs.Valid from Date' | translate }}: + {{ recurringDepositProduct.activeChart.fromDate | dateFormat }}
- - @if (recurringDepositProduct.description) { + @if (recurringDepositProduct.activeChart.endDate) { +
+ {{ 'labels.inputs.End Date' | translate }}: + {{ recurringDepositProduct.activeChart.endDate | dateFormat }} +
+ } + @if (recurringDepositProduct.activeChart.description) {
{{ 'labels.inputs.Description' | translate }}: - {{ recurringDepositProduct.description }} + {{ recurringDepositProduct.activeChart.description }}
} - -

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

- - -
- {{ 'labels.heading.Currency' | translate }}: - ({{ recurringDepositProduct.currency.code }}) {{ recurringDepositProduct.currency.name }} + {{ 'labels.inputs.Primary Grouping by Amount' | translate }}: + {{ recurringDepositProduct.activeChart.isPrimaryGroupingByAmount | yesNo }}
- -
- {{ 'labels.inputs.Decimal Places' | translate }}: - {{ recurringDepositProduct.currency.decimalPlaces }} -
- -
- {{ 'labels.inputs.Currency in multiples of' | translate }}: - {{ recurringDepositProduct.currency.inMultiplesOf | formatNumber }} -
- -

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

- - - -
- {{ 'labels.inputs.Deposit Amount' | translate }}: - {{ - recurringDepositProduct.depositAmount + - ' (Min: ' + - (recurringDepositProduct.minDepositAmount - ? recurringDepositProduct.minDepositAmount - : recurringDepositProduct.depositAmount) + - ', Max: ' + - (recurringDepositProduct.maxDepositAmount - ? recurringDepositProduct.maxDepositAmount - : recurringDepositProduct.depositAmount) + - ')' - }} -
- -
- {{ 'labels.inputs.Interest Compounding Period' | translate }}: - {{ recurringDepositProduct.interestCompoundingPeriodType.value }} -
- -
- {{ 'labels.inputs.Interest Posting Period' | translate }}: - {{ recurringDepositProduct.interestPostingPeriodType.value }} -
- -
- {{ 'labels.inputs.Interest Calculated using' | translate }}: - {{ recurringDepositProduct.interestCalculationType.value }} -
- -
- {{ 'labels.inputs.Days in Year' | translate }}: - {{ recurringDepositProduct.interestCalculationDaysInYearType.value }} -
- -

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

- - - - @if (recurringDepositProduct.lockinPeriodFrequency) { -
- {{ 'labels.inputs.Lock-in Period' | translate }}: - @if (recurringDepositProduct.lockinPeriodFrequency !== undefined) { - - {{ recurringDepositProduct.lockinPeriodFrequency }} - {{ recurringDepositProduct.lockinPeriodFrequencyType.value }} - } -
- } - - @if (recurringDepositProduct.minDepositTerm) { -
- {{ 'labels.inputs.Minimum Deposit Term' | translate }}: - @if (recurringDepositProduct.minDepositTerm !== undefined) { - {{ recurringDepositProduct.minDepositTerm }} -   - {{ recurringDepositProduct.minDepositTermType.value }} - } -
- } - - @if (recurringDepositProduct.maxDepositTerm) { -
- {{ 'labels.inputs.Maximum Deposit Term' | translate }}: - @if (recurringDepositProduct.maxDepositTerm !== undefined) { - {{ recurringDepositProduct.maxDepositTerm }} -   - {{ recurringDepositProduct.maxDepositTermType.value }} - } -
- } - - @if (recurringDepositProduct.inMultipleOfDepositTerm) { -
- {{ 'labels.inputs.Deposit Term In multiple of' | translate }}: - @if (recurringDepositProduct.inMultipleOfDepositTerm !== undefined) { - {{ recurringDepositProduct.inMultipleOfDepositTerm }} - - - {{ recurringDepositProduct.inMultipleOfDepositTermType.value }} - } -
- } - - @if (recurringDepositProduct.isMandatoryDeposit !== undefined) { -
- {{ 'labels.inputs.Recurring Deposit Type' | translate }}: - @if (recurringDepositProduct.isMandatoryDeposit) { - - {{ 'labels.text.Mandatory' | translate }} - - } - @if (!recurringDepositProduct.isMandatoryDeposit) { - - {{ 'labels.text.Voluntary' | translate }} - - } -
- } - - @if (recurringDepositProduct.preClosurePenalInterest) { -
- {{ 'labels.inputs.Apply penal interest (less)' | translate }}: - {{ recurringDepositProduct.preClosurePenalInterest }}%On - {{ recurringDepositProduct.preClosurePenalInterestOnType.value }} - -
- } - -
- {{ 'labels.inputs.Withhold Tax is Applicable' | translate }}: - {{ recurringDepositProduct.withHoldTax | yesNo }} -
- - @if (recurringDepositProduct.withHoldTax) { -
- {{ 'labels.inputs.Withhold Tax group' | translate }}: - {{ recurringDepositProduct.taxGroup.name }} -
- } - - @if (recurringDepositProduct.activeChart) { -
-

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

- -
- @if (recurringDepositProduct.activeChart.name) { -
- {{ 'labels.inputs.Name' | translate }}: - {{ recurringDepositProduct.activeChart.name }} -
- } -
- {{ 'labels.inputs.Valid from Date' | translate }}: - {{ recurringDepositProduct.activeChart.fromDate | dateFormat }} -
- @if (recurringDepositProduct.activeChart.endDate) { -
- {{ 'labels.inputs.End Date' | translate }}: - {{ recurringDepositProduct.activeChart.endDate | dateFormat }} -
- } - @if (recurringDepositProduct.activeChart.description) { -
- {{ 'labels.inputs.Description' | translate }}: - {{ recurringDepositProduct.activeChart.description }} -
- } -
- {{ 'labels.inputs.Primary Grouping by Amount' | translate }}: - {{ recurringDepositProduct.activeChart.isPrimaryGroupingByAmount | yesNo }} -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Period' | translate }} - @if (chartSlab.fromPeriod !== undefined) { -
{{ chartSlab.fromPeriod }} - {{ chartSlab.toPeriod }} {{ chartSlab.periodType.value }}
- } - @if (chartSlab.fromPeriod === undefined) { -
 
- } -
{{ 'labels.inputs.Amount Range' | translate }} - @if (chartSlab.amountRangeFrom !== undefined) { -
{{ chartSlab.amountRangeFrom }} - {{ chartSlab.amountRangeTo }}
- } - @if (chartSlab.amountRangeFrom === undefined) { -
 
- } -
{{ 'labels.inputs.Interest' | translate }} - {{ chartSlab.annualInterestRate }} - {{ 'labels.inputs.Description' | translate }} - {{ chartSlab.description }} - {{ 'labels.inputs.Actions' | translate }} - - -
- -

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

- - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Entity Type' | translate }} - {{ - incentive.entityType - | find - : recurringDepositProductTemplate.chartTemplate.entityTypeOptions - : 'id' - : 'value' - }} - {{ 'labels.inputs.Attribute Name' | translate }} - {{ - incentive.attributeName - | find - : recurringDepositProductTemplate.chartTemplate.attributeNameOptions - : 'id' - : 'value' - }} - {{ 'labels.inputs.Condition Type' | translate }} - {{ - incentive.conditionType - | find - : recurringDepositProductTemplate.chartTemplate.conditionTypeOptions - : 'id' - : 'value' - }} - {{ 'labels.inputs.Attribute Value' | translate }} - @switch (incentive.attributeName) { - @case (2) { - {{ - incentive.attributeValue - | find - : recurringDepositProductTemplate.chartTemplate.genderOptions - : 'id' - : 'name' - }} - } - @case (3) { - {{ incentive.attributeValue }} - } - @case (4) { - {{ - incentive.attributeValue - | find - : recurringDepositProductTemplate.chartTemplate.clientTypeOptions - : 'id' - : 'name' - }} - } - @case (5) { - {{ - incentive.attributeValue - | find - : recurringDepositProductTemplate.chartTemplate.clientClassificationOptions - : 'id' - : 'name' - }} - } - } - {{ 'labels.inputs.Incentive Type' | translate }} - {{ - incentive.incentiveType - | find - : recurringDepositProductTemplate.chartTemplate.incentiveTypeOptions - : 'id' - : 'value' - }} - {{ 'labels.inputs.Amount' | translate }} - {{ incentive.amount }} -
-
-
-
- -
-
- } - - @if (recurringDepositProduct.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 ( - recurringDepositProduct.paymentChannelToFundSourceMappings?.length || - recurringDepositProduct.feeToIncomeAccountMappings?.length || - recurringDepositProduct.penaltyToIncomeAccountMappings?.length - ) { -
-

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

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

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

- - - - - - - - - - - -
{{ 'labels.inputs.Payment Type' | translate }} - {{ paymentFundSource.paymentType.name }} - {{ 'labels.inputs.Fund Source' | translate }} - {{ paymentFundSource.fundSourceAccount.name }} -
+ +
{{ 'labels.inputs.Period' | translate }} + @if (chartSlab.fromPeriod !== undefined) { +
{{ chartSlab.fromPeriod }} - {{ chartSlab.toPeriod }} {{ chartSlab.periodType.value }}
+ } + @if (chartSlab.fromPeriod === undefined) { +
 
+ } +
{{ 'labels.inputs.Amount Range' | translate }} + @if (chartSlab.amountRangeFrom !== undefined) { +
{{ chartSlab.amountRangeFrom }} - {{ chartSlab.amountRangeTo }}
+ } + @if (chartSlab.amountRangeFrom === undefined) { +
 
+ } +
{{ 'labels.inputs.Interest' | translate }}{{ chartSlab.annualInterestRate }}{{ 'labels.inputs.Description' | translate }}{{ chartSlab.description }}{{ 'labels.inputs.Actions' | translate }} + + +
+ +

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

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

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

- - - - - - - - - - - -
{{ 'labels.inputs.Fees' | translate }} - {{ feesIncome.charge.name }} - {{ 'labels.inputs.Income Account' | translate }} - {{ feesIncome.incomeAccount.name }} -
-
- } - @if (recurringDepositProduct.penaltyToIncomeAccountMappings?.length) { -
-

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

- - - - - - - - - - - -
{{ 'labels.inputs.Penalty' | translate }} - {{ penaltyIncome.charge.name }} - {{ 'labels.inputs.Income Account' | translate }} - {{ penaltyIncome.incomeAccount.name }} -
-
- } - - } +
+
- - + + } + + + @if (recurringDepositProduct.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 ( + recurringDepositProduct.paymentChannelToFundSourceMappings?.length || + recurringDepositProduct.feeToIncomeAccountMappings?.length || + recurringDepositProduct.penaltyToIncomeAccountMappings?.length + ) { +
+

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

+ + @if (recurringDepositProduct.paymentChannelToFundSourceMappings?.length) { +
+

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

+ + + + + + + + + + + +
{{ 'labels.inputs.Payment Type' | translate }}{{ paymentFundSource.paymentType.name }}{{ 'labels.inputs.Fund Source' | translate }}{{ paymentFundSource.fundSourceAccount.name }}
+
+ } + @if (recurringDepositProduct.feeToIncomeAccountMappings?.length) { +
+

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

+ + + + + + + + + + + +
{{ 'labels.inputs.Fee/Charge' | translate }}{{ feesIncome.charge.name }}{{ 'labels.inputs.Income Account' | translate }}{{ feesIncome.incomeAccount.name }}
+
+ } + @if (recurringDepositProduct.penaltyToIncomeAccountMappings?.length) { +
+

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

+ + + + + + + + + + + +
{{ 'labels.inputs.Penalty' | translate }}{{ penaltyIncome.charge.name }}{{ 'labels.inputs.Income Account' | translate }}{{ penaltyIncome.incomeAccount.name }}
+
+ } +
+ } diff --git a/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.ts b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.ts index 88331fcaf..4ba8ac022 100644 --- a/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.ts +++ b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.ts @@ -68,6 +68,7 @@ export class RecurringDepositGeneralTabComponent { recurringDepositProduct: any; recurringDepositProductTemplate: any; + expandChartSlabIndex: number | null = null; chartSlabsIncentivesDisplayedColumns: string[] = ['incentives']; chartSlabsDisplayedColumns: string[] = [ diff --git a/src/app/products/recurring-deposit-products/view-recurring-deposit-product/view-recurring-deposit-product.component.html b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/view-recurring-deposit-product.component.html index 041cff962..4bde6da1a 100644 --- a/src/app/products/recurring-deposit-products/view-recurring-deposit-product/view-recurring-deposit-product.component.html +++ b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/view-recurring-deposit-product.component.html @@ -18,6 +18,15 @@ > {{ 'labels.heading.General' | translate }} + + CBS Base Chart + @for (recurringDepositDatatable of recurringDepositDatatables; track recurringDepositDatatable) { Date: Tue, 21 Apr 2026 17:38:07 +0530 Subject: [PATCH 04/12] update some logic show option Co-Authored-By: Claude Sonnet 4.6 --- ...urrent-product-preview-step.component.html | 8 +- .../current-product-preview-step.component.ts | 4 +- ...rrent-product-settings-step.component.html | 5 - ...current-product-settings-step.component.ts | 4 +- ...current-product-general-tab.component.html | 10 +- .../current-product-general-tab.component.ts | 4 +- ...dit-recurring-deposit-product.component.ts | 10 +- ...eposit-product-preview-step.component.html | 41 +-- ...-deposit-product-terms-step.component.html | 35 ++- ...ng-deposit-product-terms-step.component.ts | 38 ++- ...curring-deposit-general-tab.component.html | 14 +- ...saving-product-preview-step.component.html | 17 +- .../saving-product-preview-step.component.ts | 4 +- .../saving-product-settings-step.component.ts | 10 +- .../saving-product-terms-step.component.html | 35 ++- .../saving-product-terms-step.component.ts | 60 ++--- .../saving-product-general-tab.component.html | 21 +- .../saving-product-general-tab.component.ts | 4 +- .../create-savings-account.component.html | 83 ++++++- .../create-savings-account.component.ts | 65 ++--- .../savings-account-charges-step.component.ts | 26 +- ...vings-account-currency-step.component.html | 49 ++++ ...vings-account-currency-step.component.scss | 116 +++++++++ ...savings-account-currency-step.component.ts | 20 ++ ...avings-account-details-step.component.html | 156 ++++++++---- ...avings-account-details-step.component.scss | 98 ++++++++ .../savings-account-details-step.component.ts | 40 ++- ...avings-account-digital-step.component.html | 234 ++++++++++++++++++ ...avings-account-digital-step.component.scss | 199 +++++++++++++++ .../savings-account-digital-step.component.ts | 69 ++++++ ...avings-account-preview-step.component.html | 87 ++++++- ...vings-account-settings-step.component.html | 113 +++++++++ ...vings-account-settings-step.component.scss | 169 +++++++++++++ ...savings-account-settings-step.component.ts | 152 ++++++++++++ .../savings-account-term-step.component.html | 170 +++++++++++++ .../savings-account-term-step.component.scss | 166 +++++++++++++ .../savings-account-term-step.component.ts | 154 ++++++++++++ .../savings-account-terms-step.component.html | 7 - src/environments/.env.ts | 5 +- 39 files changed, 2215 insertions(+), 287 deletions(-) create mode 100644 src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.html create mode 100644 src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.scss create mode 100644 src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.ts create mode 100644 src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.html create mode 100644 src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.scss create mode 100644 src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.ts create mode 100644 src/app/savings/savings-account-stepper/savings-account-settings-step/savings-account-settings-step.component.html create mode 100644 src/app/savings/savings-account-stepper/savings-account-settings-step/savings-account-settings-step.component.scss create mode 100644 src/app/savings/savings-account-stepper/savings-account-settings-step/savings-account-settings-step.component.ts create mode 100644 src/app/savings/savings-account-stepper/savings-account-term-step/savings-account-term-step.component.html create mode 100644 src/app/savings/savings-account-stepper/savings-account-term-step/savings-account-term-step.component.scss create mode 100644 src/app/savings/savings-account-stepper/savings-account-term-step/savings-account-term-step.component.ts diff --git a/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.html b/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.html index 7e179aef5..68e5bdde8 100644 --- a/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.html +++ b/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.html @@ -1,4 +1,4 @@ -
- +

{{ recurringDepositProduct.name }}

- +

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

@@ -27,7 +27,7 @@
} - + @if ( recurringDepositProduct.accountType || recurringDepositProduct.customerSegment || @@ -63,14 +63,14 @@ @if (recurringDepositProduct.effectiveTo) {
Effective To: - {{ recurringDepositProduct.effectiveTo }} + {{ recurringDepositProduct.effectiveTo | dateFormat }}
} @if (recurringDepositProduct.endDate) {
End Date: - {{ recurringDepositProduct.endDate }} + {{ recurringDepositProduct.endDate | dateFormat }}
} @@ -93,7 +93,7 @@ } } - +

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

@@ -144,7 +144,7 @@ } - +

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

@@ -169,11 +169,22 @@ }} + @if (recurringDepositProduct.interestCalculateSelector) { +
+ {{ 'labels.inputs.Interest Calculation Type' | translate }}: + {{ + recurringDepositProduct.interestCalculateSelector + | find: recurringDepositProductsTemplate.interestCalculationTypeOption : 'id' : 'value' + }} +
+ } +
{{ 'labels.inputs.Interest Calculated using' | translate }}: {{ recurringDepositProduct.interestCalculationType | find: recurringDepositProductsTemplate.interestCalculationTypeOptions : 'id' : 'value' + | translateKey: 'catalogs' }}
@@ -188,7 +199,7 @@ @if (recurringDepositProduct.effectiveDate) {
Effective Date: - {{ recurringDepositProduct.effectiveDate }} + {{ recurringDepositProduct.effectiveDate | dateFormat }}
} @@ -218,11 +229,11 @@ @if (recurringDepositProduct.lastInterestDate) {
Last Interest Date: - {{ recurringDepositProduct.lastInterestDate }} + {{ recurringDepositProduct.lastInterestDate | dateFormat }}
} - +

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

@@ -313,7 +324,7 @@ } - + @if (recurringDepositProduct.rdSlabs?.length) {

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

@@ -334,7 +345,7 @@
To{{ slab.toPeriod ?? '—' }}{{ slab.toPeriod ?? '—' }} Period
+ + + + + + + + + + + + + + + + @for (row of accountFacilityLimits; track row.limitType) { + + + + + + + } + +
Limit TypeGeneral Limit (Daily)Minor Account LimitSmall Saving Limit
{{ row.limitType }}
+
+ } + + @if (depositLimitKyc?.general) { +
+

Deposit Limit (KYC)

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Account TypeKYC TypeLimit
General + +
Minor + +
Small Saving + +
+
+ } + +
+ Note: + These facilities are defined at product level and applied to this account. Small Saving Account limits are + strictly capped at Rs.1,00,000 maximum balance per RBI Master Circular. +
+
+
diff --git a/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.scss b/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.scss new file mode 100644 index 000000000..7b58a1abb --- /dev/null +++ b/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.scss @@ -0,0 +1,199 @@ +/** + * 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: #d9e2ec; +$border-strong: #c7d2de; +$text: #24364b; +$muted: #64748b; +$header-bg: #f8fafc; + +.spl-section { + border: 1px solid $border; + border-radius: 12px; + overflow: hidden; + background: #fff; +} + +.spl-section__header { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + background: $header-bg; + border-bottom: 1px solid $border; +} + +.spl-section__icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 8px; + background: #eff6ff; + color: #2563eb; + font-size: 14px; + flex: 0 0 auto; +} + +.spl-section__title { + flex: 1 1 auto; + font-size: 16px; + font-weight: 600; + color: $text; +} + +.spl-section__badge { + flex: 0 0 auto; + padding: 5px 10px; + border-radius: 999px; + background: #dbeafe; + color: #1d4ed8; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.05em; + white-space: nowrap; +} + +.spl-section__body { + padding: 18px 20px 20px; +} + +.spl-table-wrap { + overflow-x: auto; + margin-top: 16px; +} + +.spl-table { + width: 100%; + min-width: 0; + table-layout: fixed; + border-collapse: collapse; + border: 1px solid $border; + + col.spl-table__col--label { + width: 22%; + } + + col.spl-table__col--value { + width: 26%; + } + + th, + td { + border: 1px solid $border; + padding: 6px 8px; + vertical-align: middle; + } + + th { + background: #f8fafc; + color: #64748b; + text-align: center; + font-size: 11px; + font-weight: 700; + } + + td:first-child { + background: #f8fafc; + font-weight: 600; + color: $text; + white-space: nowrap; + } + + td { + font-size: 13px; + overflow: hidden; + } +} + +.spl-input, +.spl-select { + width: 100%; + box-sizing: border-box; + font: inherit; +} + +.spl-input { + height: 30px; + border: 1px solid $border-strong; + border-radius: 2px; + padding: 0 8px; + background: #f8fafc; + color: $muted; +} + +.spl-select { + height: 30px; + border: 1px solid $border-strong; + border-radius: 2px; + padding: 0 6px; + background: #f8fafc; + color: $muted; +} + +.spl-features { + margin-bottom: 16px; +} + +.spl-features__header { + font-size: 13px; + font-weight: 600; + color: $muted; + margin-bottom: 12px; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.spl-features__grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px 16px; + + @media (width <= 900px) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + @media (width <= 640px) { + grid-template-columns: 1fr; + } +} + +.spl-feature { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 13px; + color: $text; +} + +.spl-feature__label { + display: inline-flex; + align-items: center; + gap: 8px; + font-weight: 600; + color: #334155; + + i { + color: #2563eb; + font-size: 12px; + width: 14px; + text-align: center; + } +} + +.spl-note { + margin-top: 16px; + padding: 12px 14px; + border: 1px solid $border; + border-radius: 8px; + background: #f8fafc; + color: $muted; + font-size: 11px; + line-height: 1.6; +} diff --git a/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.ts new file mode 100644 index 000000000..ae9f82d8e --- /dev/null +++ b/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.ts @@ -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/. + */ + +import { Component, Input } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +const DEFAULT_FACILITY_CONFIG = { + atmFacility: false, + atmWithdrawalFacility: false, + upiFacility: false, + posTransactionsFacility: false, + chequeBook: false, + impsFacility: false, + smsAlerts: false, + bbpsFacility: false, + insuranceCover: false, + bepgFacility: false, + neftFacility: false, + rtgsFacility: false, + interBankTotalCapFacility: false, + withdrawalFacility: false, + depositFacilityKyc: false, + virtualDebitCard: false +}; + +@Component({ + selector: 'mifosx-savings-account-digital-step', + templateUrl: './savings-account-digital-step.component.html', + styleUrls: ['./savings-account-digital-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FormsModule + ] +}) +export class SavingsAccountDigitalStepComponent { + private _savingsAccountProductTemplate: any; + + @Input() + set savingsAccountProductTemplate(value: any) { + this._savingsAccountProductTemplate = value; + } + get savingsAccountProductTemplate(): any { + return this._savingsAccountProductTemplate; + } + + get accountFacilityConfig() { + return this._savingsAccountProductTemplate?.accountFacilityConfig || DEFAULT_FACILITY_CONFIG; + } + + get accountFacilityLimits(): any[] { + return this._savingsAccountProductTemplate?.accountFacilityLimits || []; + } + + get depositLimitKyc() { + return ( + this._savingsAccountProductTemplate?.depositLimitKyc || { + general: { kycType: '', limit: '' }, + minor: { kycType: '', limit: '' }, + smallSaving: { kycType: '', limit: '' } + } + ); + } +} diff --git a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.html b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.html index 10ee18062..7625ea53d 100644 --- a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.html +++ b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.html @@ -22,6 +22,27 @@ {{ savingsAccount.submittedOnDate | dateFormat }}
+ @if (savingsAccountProductTemplate?.effectiveDate) { +
+ Effective Date + {{ savingsAccountProductTemplate.effectiveDate | dateFormat }} +
+ } + + @if (savingsAccountProductTemplate?.effectiveTo) { +
+ Effective To + {{ savingsAccountProductTemplate.effectiveTo | dateFormat }} +
+ } + + @if (savingsAccountProductTemplate?.endDate) { +
+ End Date + {{ savingsAccountProductTemplate.endDate | dateFormat }} +
+ } +
{{ 'labels.inputs.External Id' | translate }} @if (savingsAccount.externalId) { @@ -74,7 +95,7 @@
{{ 'labels.inputs.Decimal Places' | translate }} - {{ savingsAccountTermsForm.get('decimal').value }} + {{ savingsAccountProductTemplate?.currency?.decimalPlaces }}
@@ -82,6 +103,49 @@ {{ savingsAccount.nominalAnnualInterestRate | formatNumber }} %
+ @if (savingsAccountProductTemplate?.interestCalculationTypeName) { +
+ Interest Calculation Mode + {{ savingsAccountProductTemplate.interestCalculationTypeName }} +
+ } + + @if (savingsAccountProductTemplate?.lastInterestDate) { +
+ Last Interest Date + {{ savingsAccountProductTemplate.lastInterestDate | dateFormat }} +
+ } + +
+ Interest Calculation Selector + {{ + getCatalogTranslation( + savingsAccount.interestCalculateSelector + | find: savingsAccountProductTemplate.interestCalculationTypeOption : 'id' : 'value' + ) + }} +
+ + @if (savingsAccount.averagePeriod) { +
+ Average Period + {{ savingsAccount.averagePeriod }} +
+ } + +
+ Auto Post + {{ savingsAccount.autoPost | yesNo }} +
+ + @if (savingsAccount.frequencyFixDay !== undefined && savingsAccount.frequencyFixDay !== null) { +
+ Frequency Fix Day + {{ savingsAccount.frequencyFixDay }} +
+ } +
{{ 'labels.inputs.Interest Compounding Period' | translate }}: {{ @@ -181,6 +245,27 @@ {{ savingsAccount.enforceMinRequiredBalance | yesNo }}
+ @if (savingsAccountProductTemplate?.enableLockinPeriod !== undefined) { +
+ Enable Lock-in Period + {{ savingsAccountProductTemplate.enableLockinPeriod | yesNo }} +
+ } + + @if (savingsAccountProductTemplate?.balanceFrequency) { +
+ Balance Frequency + {{ savingsAccountProductTemplate.balanceFrequency }} +
+ } + + @if (savingsAccountProductTemplate?.accountClosedZeroBalanceInDayClosing !== undefined) { +
+ Account Closed Zero Balance In Day Closing + {{ savingsAccountProductTemplate.accountClosedZeroBalanceInDayClosing | yesNo }} +
+ } + @if (savingsAccount.minRequiredBalance) {
{{ 'labels.inputs.Minimum Balance' | translate }}: diff --git a/src/app/savings/savings-account-stepper/savings-account-settings-step/savings-account-settings-step.component.html b/src/app/savings/savings-account-stepper/savings-account-settings-step/savings-account-settings-step.component.html new file mode 100644 index 000000000..0cfb16fb7 --- /dev/null +++ b/src/app/savings/savings-account-stepper/savings-account-settings-step/savings-account-settings-step.component.html @@ -0,0 +1,113 @@ + + + +
+
+ + Balance & Minimum Balance +
+
+
+
+ + {{ 'labels.inputs.Minimum Opening Balance' | translate }} + + + +
+ + Enforce Minimum Balance + +
+ + + Minimum Balance (₹) + + Penal charge applies on breach + +
+ +
+ + Balance Frequency + + @for (freq of periodFrequencyData; track freq.value) { + {{ freq.value }} + } + + + +
+ + {{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }} + +
+
+
+
+
+ +
+
+ + Lock-in Period +
+
+
+ Enable Lock-in Period + +
+ + @if (savingsAccountSettingsForm.value.enableLockinPeriod) { +
+ + {{ 'labels.inputs.Frequency' | translate }}* + + + + + {{ 'labels.inputs.Type' | translate }}* + + @for (type of lockinPeriodFrequencyTypeData; track type) { + {{ type.value | translateKey: 'catalogs' }} + } + + +
+ } +
+
+ +
+
+ + Additional Rules +
+
+
+
+ + Account Closed with Zero balance in Day Closing + +
+ + @if (savingsAccountProductTemplate?.minBalanceForInterestCalculation != null) { + + {{ 'labels.inputs.Balance Required for Interest Calculation' | translate }} + + + } +
+
+
+ diff --git a/src/app/savings/savings-account-stepper/savings-account-settings-step/savings-account-settings-step.component.scss b/src/app/savings/savings-account-stepper/savings-account-settings-step/savings-account-settings-step.component.scss new file mode 100644 index 000000000..4356c409f --- /dev/null +++ b/src/app/savings/savings-account-stepper/savings-account-settings-step/savings-account-settings-step.component.scss @@ -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/. + */ + +$accent: #0ea5e9; +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-900: #202124; + +.sps-section { + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; + 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 { + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + } +} + +.sps-section__badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: #eff6ff; + font-size: 14px; +} + +.sps-body { + background: #fff; + padding: 18px 24px 24px; +} + +.sps-field { + width: 100%; + + &--full { + grid-column: 1 / -1; + } + + &--compact { + width: 100%; + } +} + +.sps-layout { + display: grid; + gap: 16px; +} + +.sps-layout--balance { + gap: 22px; +} + +.sps-layout--additional { + grid-template-columns: repeat(2, minmax(0, 1fr)); + align-items: start; + + @media (width <= 1100px) { + grid-template-columns: 1fr; + } +} + +.sps-row { + display: grid; + gap: 18px; + align-items: start; +} + +.sps-row--three { + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + align-items: center; + + .sps-field--compact:first-child { + min-width: 0; + } + + .sps-toggle--inline { + align-self: center; + min-height: 56px; + } +} + +.sps-row--two { + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + + .sps-field--compact { + min-width: 0; + } +} + +.sps-toggle { + display: flex; + align-items: center; + min-height: 56px; + + mat-checkbox { + font-size: 14px; + color: $gray-900; + } + + &--full { + min-height: auto; + margin-bottom: 8px; + } + + &--inline { + padding-top: 0; + } + + &--right { + justify-content: flex-start; + } +} + +.sps-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; +} + +.sps-lockin-row mat-checkbox { + margin-right: 2px; +} + +.sps-lockin-fields { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + + @media (width <= 640px) { + grid-template-columns: 1fr; + } +} diff --git a/src/app/savings/savings-account-stepper/savings-account-settings-step/savings-account-settings-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-settings-step/savings-account-settings-step.component.ts new file mode 100644 index 000000000..d3683e753 --- /dev/null +++ b/src/app/savings/savings-account-stepper/savings-account-settings-step/savings-account-settings-step.component.ts @@ -0,0 +1,152 @@ +/** + * 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 { UntypedFormGroup, UntypedFormBuilder, Validators, UntypedFormControl } from '@angular/forms'; +import { MatCheckbox } from '@angular/material/checkbox'; +import { FormatNumberPipe } from '../../../pipes/format-number.pipe'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-savings-account-settings-step', + templateUrl: './savings-account-settings-step.component.html', + styleUrls: ['./savings-account-settings-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatCheckbox, + FormatNumberPipe + ] +}) +export class SavingsAccountSettingsStepComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + + private _savingsAccountProductTemplate: any; + + @Input() + set savingsAccountProductTemplate(value: any) { + this._savingsAccountProductTemplate = value; + if (value && this.savingsAccountSettingsForm) { + this.applyTemplate(value); + } + } + get savingsAccountProductTemplate(): any { + return this._savingsAccountProductTemplate; + } + + savingsAccountSettingsForm: UntypedFormGroup; + + lockinPeriodFrequencyTypeData: any[] = []; + + periodFrequencyData: any[] = [ + { value: 'Daily' }, + { value: 'Weekly' }, + { value: 'Monthly' }, + { value: 'Quarterly' }, + { value: 'Half Yearly' }, + { value: 'Yearly' }]; + + constructor() { + this.createForm(); + this.buildDependencies(); + } + + createForm() { + this.savingsAccountSettingsForm = this.formBuilder.group({ + minRequiredOpeningBalance: [''], + withdrawalFeeForTransfers: [false], + enforceMinRequiredBalance: [false], + minRequiredBalance: [''], + balanceFrequency: ['Monthly'], + enableLockinPeriod: [false], + lockinPeriodFrequency: [''], + lockinPeriodFrequencyType: [''], + accountClosedZeroBalanceInDayClosing: [false], + allowOverdraft: [false] + }); + } + + ngOnInit() { + if (this._savingsAccountProductTemplate) { + this.applyTemplate(this._savingsAccountProductTemplate); + } + } + + private applyTemplate(t: any) { + this.lockinPeriodFrequencyTypeData = t.lockinPeriodFrequencyTypeOptions || []; + const hasLockin = !!(t.lockinPeriodFrequency && t.lockinPeriodFrequency > 0); + + this.savingsAccountSettingsForm.patchValue({ + minRequiredOpeningBalance: t.minRequiredOpeningBalance, + withdrawalFeeForTransfers: t.withdrawalFeeForTransfers || false, + enforceMinRequiredBalance: t.enforceMinRequiredBalance || false, + minRequiredBalance: t.minRequiredBalance, + balanceFrequency: t.balanceFrequency || 'Monthly', + enableLockinPeriod: hasLockin, + lockinPeriodFrequency: hasLockin ? t.lockinPeriodFrequency : '', + lockinPeriodFrequencyType: hasLockin ? t.lockinPeriodFrequencyType?.id || '' : '', + accountClosedZeroBalanceInDayClosing: t.accountClosedZeroBalanceInDayClosing || false, + allowOverdraft: t.allowOverdraft || false + }); + + if (t.allowOverdraft) { + if (!this.savingsAccountSettingsForm.contains('minOverdraftForInterestCalculation')) { + this.savingsAccountSettingsForm.addControl('minOverdraftForInterestCalculation', new UntypedFormControl('')); + this.savingsAccountSettingsForm.addControl('nominalAnnualInterestRateOverdraft', new UntypedFormControl('')); + this.savingsAccountSettingsForm.addControl('overdraftLimit', new UntypedFormControl('')); + } + this.savingsAccountSettingsForm.patchValue({ + minOverdraftForInterestCalculation: t.minOverdraftForInterestCalculation, + nominalAnnualInterestRateOverdraft: t.nominalAnnualInterestRateOverdraft, + overdraftLimit: t.overdraftLimit + }); + } + } + + buildDependencies() { + this.savingsAccountSettingsForm.get('enableLockinPeriod').valueChanges.subscribe((enabled: boolean) => { + if (enabled) { + this.savingsAccountSettingsForm.get('lockinPeriodFrequency').setValidators([ + Validators.required, + Validators.min(1) + ]); + this.savingsAccountSettingsForm.get('lockinPeriodFrequencyType').setValidators([Validators.required]); + } else { + this.savingsAccountSettingsForm.get('lockinPeriodFrequency').setValue(''); + this.savingsAccountSettingsForm.get('lockinPeriodFrequencyType').setValue(''); + this.savingsAccountSettingsForm.get('lockinPeriodFrequency').clearValidators(); + this.savingsAccountSettingsForm.get('lockinPeriodFrequencyType').clearValidators(); + } + this.savingsAccountSettingsForm.get('lockinPeriodFrequency').updateValueAndValidity(); + this.savingsAccountSettingsForm.get('lockinPeriodFrequencyType').updateValueAndValidity(); + }); + + this.savingsAccountSettingsForm.get('allowOverdraft').valueChanges.subscribe((allowed: boolean) => { + if (allowed) { + this.savingsAccountSettingsForm.addControl('minOverdraftForInterestCalculation', new UntypedFormControl('')); + this.savingsAccountSettingsForm.addControl('nominalAnnualInterestRateOverdraft', new UntypedFormControl('')); + this.savingsAccountSettingsForm.addControl('overdraftLimit', new UntypedFormControl('')); + } else { + this.savingsAccountSettingsForm.removeControl('minOverdraftForInterestCalculation'); + this.savingsAccountSettingsForm.removeControl('nominalAnnualInterestRateOverdraft'); + this.savingsAccountSettingsForm.removeControl('overdraftLimit'); + } + }); + } + + get savingsAccountSettings() { + const value = { ...this.savingsAccountSettingsForm.getRawValue() }; + if (!value.enableLockinPeriod) { + value.lockinPeriodFrequency = null; + value.lockinPeriodFrequencyType = null; + } + delete value.enableLockinPeriod; + delete value.balanceFrequency; + delete value.accountClosedZeroBalanceInDayClosing; + return value; + } +} diff --git a/src/app/savings/savings-account-stepper/savings-account-term-step/savings-account-term-step.component.html b/src/app/savings/savings-account-stepper/savings-account-term-step/savings-account-term-step.component.html new file mode 100644 index 000000000..a90dfeba3 --- /dev/null +++ b/src/app/savings/savings-account-stepper/savings-account-term-step/savings-account-term-step.component.html @@ -0,0 +1,170 @@ + + +
+
+
+ + Interest Rate & Posting Terms +
+
+ +
+ ℹ️ + + Savings Account base rate: + {{ savingsAccountTermForm.get('nominalAnnualInterestRate').value || 0 }}% + +
+ + +
+ + {{ 'labels.inputs.Nominal Annual Interest' | translate }} + + @if (savingsAccountTermForm.get('nominalAnnualInterestRate').hasError('required')) { + Nominal Annual Interest is required + } + @if (savingsAccountTermForm.get('nominalAnnualInterestRate').hasError('min')) { + Must be zero or greater + } + + + + Effective Date + + + + + + + {{ 'labels.inputs.Interest Calculation Type' | translate }} + + @for (opt of interestCalculateSelectorData; track opt.id) { + {{ opt.value }} + } + + +
+ + +
+ + Interest Calculate + + @for (opt of interestCalculationTypeData; track opt.id) { + {{ opt.value | translateKey: 'catalogs' }} + } + + {{ 'labels.inputs.Interest Calculation Type' | translate }} is required + + + @if (savingsAccountTermForm.value.interestCalculationType === 2) { + + Average Period + + @for (period of periodFrequencyData; track period) { + {{ period.value }} + } + + + } + + + {{ 'labels.inputs.Interest Posting Period' | translate }} + + @for (opt of interestPostingPeriodTypeData; track opt) { + {{ opt.value | translateKey: 'catalogs' }} + } + + {{ 'labels.inputs.Interest Posting Period' | translate }} is required + +
+ + +
+ + {{ 'labels.inputs.Interest Compounding Period' | translate }} + + @for (opt of interestCompoundingPeriodTypeData; track opt) { + {{ opt.value | translateKey: 'catalogs' }} + } + + {{ 'labels.inputs.Interest Compounding Period' | translate }} is required + + + + {{ 'labels.inputs.Days in Year' | translate }} + + @for (opt of interestCalculationDaysInYearTypeData; track opt) { + {{ opt.value | translateKey: 'catalogs' }} + } + + {{ 'labels.inputs.Days in Year' | translate }} is required + +
+ + + +
+
+
diff --git a/src/app/savings/savings-account-stepper/savings-account-term-step/savings-account-term-step.component.scss b/src/app/savings/savings-account-stepper/savings-account-term-step/savings-account-term-step.component.scss new file mode 100644 index 000000000..038977265 --- /dev/null +++ b/src/app/savings/savings-account-stepper/savings-account-term-step/savings-account-term-step.component.scss @@ -0,0 +1,166 @@ +/** + * 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; + +.sat-section { + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; + 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; + + span { + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + } +} + +.sat-section__badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: #eff6ff; + font-size: 14px; +} + +.sat-body { + background: #fff; + padding: 18px 24px 24px; +} + +.sat-banner { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 14px; + margin-bottom: 18px; + border-radius: 8px; + background: #eff6ff; + border: 1px solid #bfdbfe; + font-size: 13px; + color: $gray-900; + + &__icon { + font-size: 16px; + } +} + +.sat-grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 4px 16px; + + @media (width <= 900px) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.sat-field { + width: 100%; +} + +.sat-footer { + margin-top: 16px; + padding-top: 16px; + border-top: 1px solid $border; + + &__grid { + display: grid; + grid-template-columns: auto 1fr 1fr; + gap: 4px 16px; + align-items: start; + + @media (width <= 900px) { + grid-template-columns: 1fr 1fr; + } + + @media (width <= 600px) { + grid-template-columns: 1fr; + } + } +} + +.sat-toggle-card { + display: flex; + flex-direction: column; + gap: 8px; + padding: 10px 14px; + border: 1px solid $border; + border-radius: 8px; + background: $gray-50; + min-height: 56px; + + h4 { + margin: 0; + font-size: 12px; + font-weight: 600; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.04em; + } + + &__choices { + display: flex; + gap: 16px; + align-items: center; + } +} + +.sat-radio-label { + display: flex; + align-items: center; + gap: 6px; + font-size: 14px; + color: $gray-900; + cursor: pointer; + + [type='radio'] { + accent-color: $accent; + width: 16px; + height: 16px; + cursor: pointer; + } +} + +.sat-note { + display: flex; + align-items: flex-start; + gap: 8px; + margin-top: 12px; + padding: 10px 14px; + border-radius: 8px; + background: #fffbeb; + border: 1px solid #fde68a; + font-size: 13px; + color: #92400e; +} diff --git a/src/app/savings/savings-account-stepper/savings-account-term-step/savings-account-term-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-term-step/savings-account-term-step.component.ts new file mode 100644 index 000000000..fbf388008 --- /dev/null +++ b/src/app/savings/savings-account-stepper/savings-account-term-step/savings-account-term-step.component.ts @@ -0,0 +1,154 @@ +/** + * 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 { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; +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-savings-account-term-step', + templateUrl: './savings-account-term-step.component.html', + styleUrls: ['./savings-account-term-step.component.scss'], + imports: [...STANDALONE_SHARED_IMPORTS] +}) +export class SavingsAccountTermStepComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private dateUtils = inject(Dates); + private settingsService = inject(SettingsService); + + private _savingsAccountProductTemplate: any; + + @Input() + set savingsAccountProductTemplate(value: any) { + this._savingsAccountProductTemplate = value; + if (value && this.savingsAccountTermForm) { + this.applyTemplate(value); + } + } + get savingsAccountProductTemplate(): any { + return this._savingsAccountProductTemplate; + } + + savingsAccountTermForm: UntypedFormGroup; + + interestCompoundingPeriodTypeData: any[] = []; + interestPostingPeriodTypeData: any[] = []; + interestCalculationTypeData: any[] = []; + interestCalculationDaysInYearTypeData: any[] = []; + interestCalculateSelectorData: any[] = []; + periodFrequencyData: any[] = []; + + yesNoOptions = [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' } + ]; + + minDate = new Date(2000, 0, 1); + maxDate = new Date(); + + constructor() { + this.createForm(); + } + + createForm() { + this.savingsAccountTermForm = this.formBuilder.group({ + currencyCode: [{ value: '', disabled: true }], + 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: [''] + }); + } + + ngOnInit() { + this.maxDate = this.settingsService.businessDate; + // Apply template if it was set before the form was ready + if (this._savingsAccountProductTemplate) { + this.applyTemplate(this._savingsAccountProductTemplate); + } + } + + private applyTemplate(t: any) { + this.maxDate = this.settingsService.businessDate; + + this.interestCompoundingPeriodTypeData = t.interestCompoundingPeriodTypeOptions || []; + this.interestPostingPeriodTypeData = t.interestPostingPeriodTypeOptions || []; + this.interestCalculationTypeData = t.interestCalculationTypeOptions || []; + this.interestCalculateSelectorData = t.interestCalculationTypeOption || []; + this.interestCalculationDaysInYearTypeData = t.interestCalculationDaysInYearTypeOptions || []; + this.periodFrequencyData = t.periodFrequencyOptions?.length ? t.periodFrequencyOptions : [ + { value: 'Monthly' }, + { value: 'Quarterly' }, + { value: 'Half Yearly' }, + { value: 'Yearly' }]; + + const defaultPeriod = this.periodFrequencyData[0]?.value || 'Monthly'; + const savedEnumTypeId = t.interestCalculationType?.id || this.interestCalculationTypeData?.[0]?.id || 1; + const savedSelectorId = t.interestCalculateSelector ?? this.interestCalculateSelectorData?.[0]?.id ?? null; + + this.savingsAccountTermForm.patchValue({ + currencyCode: t.currency?.code, + nominalAnnualInterestRate: t.nominalAnnualInterestRate, + interestCompoundingPeriodType: t.interestCompoundingPeriodType?.id, + interestPostingPeriodType: t.interestPostingPeriodType?.id, + interestCalculationType: savedEnumTypeId, + interestCalculationDaysInYearType: t.interestCalculationDaysInYearType?.id, + effectiveDate: t.effectiveDate ? new Date(t.effectiveDate) : '', + interestCalculateSelector: savedSelectorId, + averagePeriod: t.averagePeriod || defaultPeriod, + autoPost: t.autoPost ?? true, + frequencyFixDay: t.frequencyFixDay ?? 30, + lastInterestDate: t.lastInterestDate ? new Date(t.lastInterestDate) : '' + }); + } + + get savingsAccountTerms() { + const value = { ...this.savingsAccountTermForm.getRawValue() }; + const dateFormat = this.settingsService.dateFormat; + + if (value.effectiveDate instanceof Date) { + value.effectiveDate = this.dateUtils.formatDate(value.effectiveDate, dateFormat) || ''; + } + if (value.lastInterestDate instanceof Date) { + value.lastInterestDate = this.dateUtils.formatDate(value.lastInterestDate, dateFormat) || ''; + } + + delete value.currencyCode; + return value; + } +} diff --git a/src/app/savings/savings-account-stepper/savings-account-terms-step/savings-account-terms-step.component.html b/src/app/savings/savings-account-stepper/savings-account-terms-step/savings-account-terms-step.component.html index 2f6b97985..8d2053bc9 100644 --- a/src/app/savings/savings-account-stepper/savings-account-terms-step/savings-account-terms-step.component.html +++ b/src/app/savings/savings-account-stepper/savings-account-terms-step/savings-account-terms-step.component.html @@ -152,13 +152,6 @@ {{ 'labels.inputs.Minimum Balance' | translate }} - - @if (savingsAccountTermsForm.controls.minBalanceForInterestCalculation.value) { - - {{ 'labels.inputs.Balance Required for Interest Calculation' | translate }} - - - }
diff --git a/src/environments/.env.ts b/src/environments/.env.ts index c92cec15e..e32239a6a 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -5,13 +5,12 @@ * 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/. */ - // IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN! /* tslint:disable */ export default { 'mifos_x': { - 'version': '260420', - 'hash': 'fe61c925' + 'version': '260421', + 'hash': 'e212bf7d' }, 'allow_switching_backend_instance': true }; From fc1c5126fc2405c278589c186a7697270d493822 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Wed, 22 Apr 2026 18:28:14 +0530 Subject: [PATCH 05/12] saving , current and RD account code update --- .../create-current-account.component.html | 49 +- .../create-current-account.component.ts | 49 +- .../current-account-charges-step.component.ts | 40 +- ...rrent-account-currency-step.component.html | 79 ++ ...rrent-account-currency-step.component.scss | 116 ++ ...current-account-currency-step.component.ts | 20 + ...urrent-account-details-step.component.html | 161 ++- ...urrent-account-details-step.component.scss | 98 ++ .../current-account-details-step.component.ts | 51 +- ...urrent-account-digital-step.component.html | 234 ++++ ...urrent-account-digital-step.component.scss | 199 +++ .../current-account-digital-step.component.ts | 69 + ...urrent-account-preview-step.component.html | 738 ++++++---- ...urrent-account-preview-step.component.scss | 92 +- .../current-account-preview-step.component.ts | 45 +- ...rrent-account-settings-step.component.html | 174 +++ ...rrent-account-settings-step.component.scss | 255 ++++ ...current-account-settings-step.component.ts | 163 +++ .../current-account-term-step.component.html | 71 + .../current-account-term-step.component.scss | 166 +++ .../current-account-term-step.component.ts | 102 ++ ...-recurring-deposits-account.component.html | 1 + ...t-recurring-deposit-account.component.html | 1 + ...deposits-account-charges-step.component.ts | 15 +- ...posits-account-details-step.component.html | 249 +++- ...posits-account-details-step.component.scss | 178 ++- ...deposits-account-details-step.component.ts | 77 +- ...nt-interest-rate-chart-step.component.html | 16 +- ...ount-interest-rate-chart-step.component.ts | 2 +- ...posits-account-preview-step.component.html | 1238 +++++++++++------ ...posits-account-preview-step.component.scss | 200 ++- ...deposits-account-preview-step.component.ts | 18 +- ...osits-account-settings-step.component.html | 348 ++--- ...osits-account-settings-step.component.scss | 177 ++- ...eposits-account-settings-step.component.ts | 112 +- ...deposits-account-terms-step.component.html | 220 ++- ...deposits-account-terms-step.component.scss | 164 +++ ...g-deposits-account-terms-step.component.ts | 86 +- ...current-product-general-tab.component.html | 10 +- .../current-product-general-tab.component.ts | 4 +- ...avings-account-preview-step.component.html | 850 ++++++----- ...avings-account-preview-step.component.scss | 92 +- .../savings-account-preview-step.component.ts | 34 +- src/environments/.env.ts | 5 +- 44 files changed, 5516 insertions(+), 1552 deletions(-) create mode 100644 src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html create mode 100644 src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.scss create mode 100644 src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts create mode 100644 src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.html create mode 100644 src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.scss create mode 100644 src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.ts create mode 100644 src/app/current-accounts/current-account-stepper/current-account-settings-step/current-account-settings-step.component.html create mode 100644 src/app/current-accounts/current-account-stepper/current-account-settings-step/current-account-settings-step.component.scss create mode 100644 src/app/current-accounts/current-account-stepper/current-account-settings-step/current-account-settings-step.component.ts create mode 100644 src/app/current-accounts/current-account-stepper/current-account-term-step/current-account-term-step.component.html create mode 100644 src/app/current-accounts/current-account-stepper/current-account-term-step/current-account-term-step.component.scss create mode 100644 src/app/current-accounts/current-account-stepper/current-account-term-step/current-account-term-step.component.ts diff --git a/src/app/current-accounts/create-current-account/create-current-account.component.html b/src/app/current-accounts/create-current-account/create-current-account.component.html index e6ffe3794..9b5c5211b 100644 --- a/src/app/current-accounts/create-current-account/create-current-account.component.html +++ b/src/app/current-accounts/create-current-account/create-current-account.component.html @@ -34,6 +34,7 @@ + {{ 'labels.inputs.DETAILS' | translate }} @@ -44,14 +45,43 @@ - - {{ 'labels.inputs.TERMS' | translate }} + + + CURRENCY - + > + + + {{ 'labels.inputs.TERMS' | translate }} + + + + + + + SETTINGS + + + + + + + DIGITAL + + + + + {{ 'labels.inputs.CHARGES' | translate }} @@ -61,14 +91,25 @@ [currencyCode]="currentAccountTermsForm.get('currencyCode')" > + + +
+ +
+ @if (currentAccountFormValid) { {{ 'labels.inputs.PREVIEW' | translate }} { this.currentAccountTemplate = data.currentAccountTemplate; @@ -82,7 +88,6 @@ export class CreateCurrentAccountComponent { /** * Sets current account product template. - * @param {any} $event API response */ setTemplate($event: any) { this.currentAccountProductTemplate = $event; @@ -96,7 +101,21 @@ export class CreateCurrentAccountComponent { } /** - * Retrieves current account terms form. + * Retrieves new term form (interest rate). + */ + get currentAccountTermForm() { + return this.currentAccountTermStep.currentAccountTermForm; + } + + /** + * Retrieves settings form. + */ + get currentAccountSettingsForm() { + return this.currentAccountSettingsStep.currentAccountSettingsForm; + } + + /** + * Retrieves old terms form — still used for currencyCode by charges step. */ get currentAccountTermsForm() { return this.currentAccountTermsStep.currentAccountTermsForm; @@ -106,7 +125,7 @@ export class CreateCurrentAccountComponent { * Checks validity of overall current account form. */ get currentAccountFormValid() { - return this.currentAccountDetailsForm.valid && this.currentAccountTermsForm.valid; + return this.currentAccountDetailsForm.valid && this.currentAccountTermForm.valid; } /** @@ -115,16 +134,16 @@ export class CreateCurrentAccountComponent { get currentAccount() { return { ...this.currentAccountDetailsStep.currentAccountDetails, - ...this.currentAccountTermsStep.currentAccountTerms, + ...this.currentAccountTermStep.currentAccountTerms, + ...this.currentAccountSettingsStep.currentAccountSettings, ...this.currentAccountChargesStep.currentAccountCharges }; } /** - * Creates a new share account. + * Creates a new current account. */ submit() { - // TODO: Update once language and date settings are setup const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; const monthDayFormat = 'dd MMMM'; diff --git a/src/app/current-accounts/current-account-stepper/current-account-charges-step/current-account-charges-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-charges-step/current-account-charges-step.component.ts index 8ac930f21..c9c5a906d 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-charges-step/current-account-charges-step.component.ts +++ b/src/app/current-accounts/current-account-stepper/current-account-charges-step/current-account-charges-step.component.ts @@ -7,7 +7,7 @@ */ /** Angular Imports */ -import { Component, OnInit, OnChanges, Input, inject } from '@angular/core'; +import { Component, OnInit, Input, inject } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { UntypedFormControl } from '@angular/forms'; @@ -69,13 +69,27 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; FormatNumberPipe ] }) -export class CurrentAccountChargesStepComponent implements OnInit, OnChanges { +export class CurrentAccountChargesStepComponent implements OnInit { private dialog = inject(MatDialog); private dateUtils = inject(Dates); private translateService = inject(TranslateService); /** Current Account Product Template */ - @Input() currentAccountProductTemplate: any; + private _currentAccountProductTemplate: any; + @Input() + set currentAccountProductTemplate(value: any) { + this._currentAccountProductTemplate = value; + if (value) { + this.chargeData = value.chargeOptions || []; + // Only pre-populate from product template for new accounts + if (!this.currentAccountTemplate?.id) { + this.chargesDataSource = (value.charges || []).map((charge: any) => ({ ...charge, id: charge.chargeId })); + } + } + } + get currentAccountProductTemplate(): any { + return this._currentAccountProductTemplate; + } /** Current Account Template */ @Input() currentAccountTemplate: any; /** Currency Code */ @@ -88,8 +102,6 @@ export class CurrentAccountChargesStepComponent implements OnInit, OnChanges { chargesDataSource: {}[] = []; /** Component is pristine if there has been no changes by user interaction */ pristine = true; - /** For Edit Current Account Form */ - isChargesPatched = false; /** Display columns for charges table */ displayedColumns: string[] = [ 'name', @@ -114,22 +126,10 @@ export class CurrentAccountChargesStepComponent implements OnInit, OnChanges { ]; ngOnInit() { - if (this.currentAccountTemplate) { - if (!this.isChargesPatched && this.currentAccountTemplate.charges) { - this.chargesDataSource = - this.currentAccountProductTemplate.charges.map((charge: any) => ({ ...charge, id: charge.chargeId })) || []; - this.isChargesPatched = true; - } else { - this.chargesDataSource = []; - } - } - } - - ngOnChanges() { - if (this.currentAccountProductTemplate) { - this.chargeData = this.currentAccountProductTemplate.chargeOptions; + if (this.currentAccountTemplate?.id && this.currentAccountTemplate?.charges) { + // Edit mode: load saved charges from existing account this.chargesDataSource = - this.currentAccountProductTemplate.charges.map((charge: any) => ({ ...charge, id: charge.chargeId })) || []; + this.currentAccountTemplate.charges.map((charge: any) => ({ ...charge, id: charge.chargeId })) || []; } } diff --git a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html new file mode 100644 index 000000000..e9ba49bac --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html @@ -0,0 +1,79 @@ + + +
+
+ + Currency & Account Status Configuration +
+
+
+ + Currency + + Inherited from selected product + + + + Decimal Places + + Inherited from selected product + +
+ +
+
+

Dormancy Alert

+

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

+
+ + {{ currentAccountProductTemplate?.dormancyAlert ? 'Yes' : 'No' }} + +
+ +
+
+

Balance Swap Option

+

Allows balance to be swapped or transferred between linked accounts as per product configuration.

+
+ + {{ currentAccountProductTemplate?.balanceSwapOption ? 'Yes' : 'No' }} + +
+ +
+
+

MSME

+

Account is categorized under the Micro, Small and Medium Enterprises segment.

+
+ + {{ currentAccountProductTemplate?.msme ? 'Yes' : 'No' }} + +
+ +
+
+

Nomination Facility

+

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

+
+ + {{ currentAccountProductTemplate?.nominationFacility ? 'Yes' : 'No' }} + +
+ +
+
+

Auto Debit Facility

+

Enable automatic debit instructions for recurring payments linked to this account.

+
+ + {{ currentAccountProductTemplate?.autoDebitFacility ? 'Yes' : 'No' }} + +
+
+
diff --git a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.scss b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.scss new file mode 100644 index 000000000..17712a898 --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.scss @@ -0,0 +1,116 @@ +/** + * 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; + +.sac-section { + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; + 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; + } + } +} + +.sac-body { + background: #fff; +} + +.sac-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px 16px; + padding: 16px 24px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.sac-field { + width: 100%; +} + +.sac-alert { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + padding: 18px 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; + } + } +} + +.sac-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 48px; + padding: 4px 14px; + border-radius: 999px; + font-size: 12px; + font-weight: 700; + background: #fee2e2; + color: #b91c1c; + border: 1px solid #fecaca; + + &--yes { + background: #dcfce7; + color: #15803d; + border-color: #bbf7d0; + } +} diff --git a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts new file mode 100644 index 000000000..805c4336b --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts @@ -0,0 +1,20 @@ +/** + * 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 } from '@angular/core'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-current-account-currency-step', + templateUrl: './current-account-currency-step.component.html', + styleUrls: ['./current-account-currency-step.component.scss'], + imports: [...STANDALONE_SHARED_IMPORTS] +}) +export class CurrentAccountCurrencyStepComponent { + @Input() currentAccountProductTemplate: any; +} diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html index 52c79fcb1..e5914fe18 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html @@ -7,63 +7,126 @@ -->
-
- - {{ 'labels.inputs.Product Name' | translate }} - - @for (product of productData; track product) { - - {{ product.name }} - - } - - - {{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - @if (currentAccountProductSelected) { - - {{ 'labels.inputs.Submitted On' | translate }} - - - + +
+
+ +

Account Details

+
+
+ + {{ 'labels.inputs.Product Name' | translate }} + + @for (product of productData; track product) { + {{ product.name }} + } + - {{ 'labels.inputs.Submitted On' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} - } - @if (currentAccountProductSelected) { - - {{ 'labels.inputs.Field Officer' | translate }} - - @for (officer of fieldOfficerData; track officer) { - - {{ officer.displayName }} - - } - - - } + @if (currentAccountProductSelected) { + + {{ 'labels.inputs.Submitted On' | translate }} + + + + + {{ 'labels.inputs.Submitted On' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + } - @if (currentAccountProductSelected) { - - {{ 'labels.inputs.External ID' | translate }} - - - } + @if (currentAccountProductSelected) { + + {{ 'labels.inputs.Field Officer' | translate }} + + @for (officer of fieldOfficerData; track officer) { + {{ officer.displayName }} + } + + + } + + @if (currentAccountProductSelected) { + + {{ 'labels.inputs.External ID' | translate }} + + + } +
+ + @if (currentAccountProductSelected && productTemplate) { +
+
+ +

Product Classification

+ From Product +
+
+ + Account Type + + + + + Customer Segment + + + + + Product Code + + + + @if (productTemplate.effectiveDate) { + + Effective Date + + + } + + @if (productTemplate.effectiveTo) { + + Effective To + + + } + + @if (productTemplate.endDate) { + + End Date + + + } + + + Introducer Required + + + +
+ Share Linking Enabled + + {{ productTemplate.shareLinkingEnabled ? 'Yes' : 'No' }} + +
+
+
+ } +
- diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.scss b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.scss index d59bc82ee..45ea9fcab 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.scss +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.scss @@ -6,6 +6,180 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -.margin-t { - margin-top: 1em; +$accent: #0ea5e9; +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-900: #202124; + +// ── Section card ─────────────────────────────────────────────── +.rad-section { + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; + background: #fff; + + &--mt { + 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; + flex: 1; + } + } +} + +// ── 2-col grid (Account Details inputs) ─────────────────────── +.rad-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px 16px; + padding: 16px 24px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +// ── 3-col grid (Product Classification) ─────────────────────── +.rad-grid--classification { + grid-template-columns: repeat(3, 1fr); + + @media (width <= 900px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.rad-field { + width: 100%; +} + +// ── Share Linking badge row ──────────────────────────────────── +.rad-info-row { + display: flex; + align-items: center; + gap: 12px; + min-height: 56px; +} + +.rad-info-label { + font-size: 14px; + color: $gray-900; + font-weight: 500; +} + +.rad-badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 3px 12px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + background: #dbeafe; + color: #1d4ed8; + border: 1px solid #bfdbfe; + + &--yes { + background: #dcfce7; + color: #15803d; + border-color: #bbf7d0; + } +} + +// ── Currency section body ────────────────────────────────────── +.rad-body { + background: #fff; +} + +// ── Yes / No alert rows (same as rdpc-alert) ────────────────── +.rad-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; + } +} + +// ── yes / no indicator pills ─────────────────────────────────── +.rad-yn { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 14px; + color: #94a3b8; + cursor: default; + + &::before { + content: ''; + display: inline-block; + width: 18px; + height: 18px; + border-radius: 50%; + border: 2px solid #cbd5e1; + background: #fff; + flex-shrink: 0; + } + + &--active { + color: $gray-900; + font-weight: 500; + + &::before { + border-color: #22c55e; + background: radial-gradient(circle, #22c55e 45%, #fff 46%); + } + } } diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts index 5deb072dc..a399f42da 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts @@ -7,9 +7,10 @@ */ /** Angular Imports */ -import { Component, OnInit, Input, Output, EventEmitter, inject } from '@angular/core'; -import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorRef } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; import { SettingsService } from 'app/settings/settings.service'; +import { Dates } from 'app/core/utils/dates'; /** Custom Services */ import { RecurringDepositsService } from '../../recurring-deposits.service'; @@ -37,6 +38,8 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); private recurringDepositsService = inject(RecurringDepositsService); private settingsService = inject(SettingsService); + private dateUtils = inject(Dates); + private cdr = inject(ChangeDetectorRef); /** Recurring Deposits Account Template */ @Input() recurringDepositsAccountTemplate: any; @@ -52,19 +55,16 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { /** For edit recurring deposits form */ isFieldOfficerPatched = false; /** Recurring Deposits Account Details Form */ - recurringDepositAccountDetailsForm: UntypedFormGroup; + recurringDepositAccountDetailsForm!: UntypedFormGroup; isProductSelected = false; /** Recurring Deposits Account Template with product data */ @Output() recurringDepositsAccountProductTemplate = new EventEmitter(); - /** - * Sets recurring deposits account details form. - * @param {FormBuilder} formBuilder Form Builder. - * @param {RecurringDepositsService} recurringDepositsService Recurring Deposits Service. - * @param {SettingsService} settingsService Settings Service - */ + /** Local copy of product template for displaying classification info */ + productTemplate: any = null; + constructor() { this.createRecurringDepositsAccountDetailsForm(); } @@ -75,6 +75,7 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { if (this.recurringDepositsAccountTemplate) { this.productData = this.recurringDepositsAccountTemplate.productOptions; if (this.recurringDepositsAccountTemplate.depositProductId) { + // Edit mode: patch from saved data this.recurringDepositAccountDetailsForm.patchValue({ productId: this.recurringDepositsAccountTemplate.depositProductId, submittedOnDate: @@ -82,6 +83,11 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { new Date(this.recurringDepositsAccountTemplate.timeline.submittedOnDate), externalId: this.recurringDepositsAccountTemplate.externalId }); + } else { + // New account: default submittedOnDate to today's business date + this.recurringDepositAccountDetailsForm.patchValue({ + submittedOnDate: this.settingsService.businessDate + }); } } } @@ -109,25 +115,72 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { */ buildDependencies() { const clientId = this.recurringDepositsAccountTemplate.clientId; - this.recurringDepositAccountDetailsForm.get('productId').valueChanges.subscribe((productId: string) => { + this.recurringDepositAccountDetailsForm.get('productId')!.valueChanges.subscribe((productId: string) => { this.recurringDepositsService .getRecurringDepositsAccountTemplate(clientId, productId) .subscribe((response: any) => { + // Ensure currency is included in the product template + if (!response.currency) { + // Try to get currency from the selected product + const selectedProduct = this.recurringDepositsAccountTemplate.productOptions?.find( + (product: any) => product.id == productId + ); + if (selectedProduct?.currency) { + response.currency = selectedProduct.currency; + } else if (this.recurringDepositsAccountTemplate.currency) { + response.currency = this.recurringDepositsAccountTemplate.currency; + } + } + this.productTemplate = response; this.recurringDepositsAccountProductTemplate.emit(response); this.fieldOfficerData = response.fieldOfficerOptions; this.isProductSelected = true; + this.cdr.detectChanges(); if (!this.isFieldOfficerPatched && this.recurringDepositsAccountTemplate.fieldOfficerId) { this.recurringDepositAccountDetailsForm - .get('fieldOfficerId') + .get('fieldOfficerId')! .patchValue(this.recurringDepositsAccountTemplate.fieldOfficerId); this.isFieldOfficerPatched = true; } else { - this.recurringDepositAccountDetailsForm.get('fieldOfficerId').patchValue(''); + this.recurringDepositAccountDetailsForm.get('fieldOfficerId')!.patchValue(''); } }); }); } + /** + * Returns true if a value is not null, undefined, or empty string. + */ + hasValue(val: any): boolean { + return val !== null && val !== undefined && val !== ''; + } + + /** + * Formats template dates into the current CBS date format. + * Handles backend date arrays like [2026, 4, 20] as well as strings and Date objects. + */ + formatTemplateDate(value: any): string { + if (!value) { + return ''; + } + + let dateValue: Date | null = null; + + if (value instanceof Date) { + dateValue = value; + } else if (Array.isArray(value) && value.length >= 3) { + dateValue = new Date(value[0], value[1] - 1, value[2]); + } else { + dateValue = new Date(value); + } + + if (!dateValue || Number.isNaN(dateValue.getTime())) { + return Array.isArray(value) ? value.join('/') : String(value); + } + + return this.dateUtils.formatDate(dateValue, 'M/d/yyyy') || ''; + } + /** * Returns recurring deposits account details form value. */ diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.html index b4d0cf3a0..c38bb2cc0 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.html @@ -49,30 +49,30 @@ @if (!_activeRdVersion) { -
+
{{ 'labels.inputs.name' | translate }}: - {{ recurringDepositsAccountProductTemplate.accountChart.name }} + {{ recurringDepositsAccountProductTemplate?.accountChart?.name }}
{{ 'labels.inputs.Valid from Date' | translate }}: - {{ recurringDepositsAccountProductTemplate?.accountChart.fromDate | dateFormat }} + {{ recurringDepositsAccountProductTemplate?.accountChart?.fromDate | dateFormat }}
-
+
{{ 'labels.inputs.End Date' | translate }}: - {{ recurringDepositsAccountProductTemplate?.accountChart.endDate | dateFormat }} + {{ recurringDepositsAccountProductTemplate?.accountChart?.endDate | dateFormat }}
-
+
{{ 'labels.inputs.Description' | translate }}: - {{ recurringDepositsAccountProductTemplate?.accountChart.description }} + {{ recurringDepositsAccountProductTemplate?.accountChart?.description }}
{{ 'labels.inputs.Primary Grouping by Amount' | translate }}: {{ - recurringDepositsAccountProductTemplate?.accountChart.isPrimaryGroupingByAmount | yesNo + recurringDepositsAccountProductTemplate?.accountChart?.isPrimaryGroupingByAmount | yesNo }}
} diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.ts index d194eeb9a..0836f8502 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.ts @@ -120,7 +120,7 @@ export class RecurringDepositsAccountInterestRateChartStepComponent implements O ngOnChanges() { if (this.recurringDepositsAccountProductTemplate) { - this.interestRateChartData = this.recurringDepositsAccountProductTemplate.accountChart.chartSlabs; + this.interestRateChartData = this.recurringDepositsAccountProductTemplate.accountChart?.chartSlabs ?? []; } } diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html index ed080c025..7b64f8833 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html @@ -6,416 +6,846 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
-

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

- - -
- {{ 'labels.inputs.Product' | translate }} - {{ - recurringDepositAccountData.productId | find: recurringDepositsAccountTemplate.productOptions : 'id' : 'name' - }} -
- -
- {{ 'labels.inputs.Submitted On' | translate }} - {{ recurringDepositAccountData.submittedOnDate | dateFormat }} -
- -
- {{ 'labels.inputs.Field Officer' | translate }} - {{ - recurringDepositAccountData.fieldOfficerId - | find: recurringDepositsAccountProductTemplate.fieldOfficerOptions : 'id' : 'displayName' - }} -
- -

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

- - -
- {{ 'labels.inputs.Currency' | translate }} - {{ recurringDepositsAccountProductTemplate.currency.code }} -
- -
- {{ 'labels.inputs.Decimal Places' | translate }} - {{ recurringDepositsAccountProductTemplate.currency.decimalPlaces }} -
- -
- {{ 'labels.inputs.Currency In Multiples Of' | translate }} - {{ recurringDepositsAccountProductTemplate.currency.inMultiplesOf }} -
- -

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

- -
- {{ 'labels.inputs.Interest Compounding Period' | translate }}: - {{ - recurringDepositAccountData.interestCompoundingPeriodType - | find: recurringDepositsAccountProductTemplate.interestCompoundingPeriodTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
- -
- {{ 'labels.inputs.Interest Posting Period' | translate }}: - {{ - recurringDepositAccountData.interestPostingPeriodType - | find: recurringDepositsAccountProductTemplate.interestPostingPeriodTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
- -
- {{ 'labels.inputs.Interest Calculated using' | translate }}: - {{ - recurringDepositAccountData.interestCalculationType - | find: recurringDepositsAccountProductTemplate.interestCalculationTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
- -
- {{ 'labels.inputs.Days in Year' | translate }}: - {{ - recurringDepositAccountData.interestCalculationDaysInYearType - | find: recurringDepositsAccountProductTemplate.interestCalculationDaysInYearTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
- -

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

- - -
- {{ 'labels.inputs.Lock-in Period' | translate }}: - {{ recurringDepositAccountData.lockinPeriodFrequency }} - {{ - recurringDepositAccountData.lockinPeriodFrequencyType - | find: recurringDepositsAccountProductTemplate.lockinPeriodFrequencyTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
- -
- {{ 'labels.inputs.Recurring Deposit Amount' | translate }}: - {{ recurringDepositAccountData.mandatoryRecommendedDepositAmount | formatNumber }} -
- -
- {{ 'labels.inputs.Deposit Period' | translate }}: - {{ recurringDepositAccountData.depositPeriod }} - {{ - recurringDepositAccountData.depositPeriodFrequencyId - | find: recurringDepositsAccountProductTemplate.periodFrequencyTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
- -
- {{ 'labels.inputs.Deposit Frequency Same as Group/Center meeting' | translate }}: - {{ recurringDepositsAccountProductTemplate.isCalendarInherited | yesNo }} -
- - -
- {{ 'labels.inputs.Deposit Start Date' | translate }}: - {{ recurringDepositAccountData.expectedFirstDepositOnDate | dateFormat }} +
+ +
+
+ {{ 'labels.heading.Details' | translate }}
- -
- {{ 'labels.inputs.Deposit Frequency' | translate }}: - {{ recurringDepositAccountData.recurringFrequency }} - {{ - recurringDepositAccountData.recurringFrequencyType - | find: recurringDepositsAccountProductTemplate.periodFrequencyTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
- - -
- {{ 'labels.inputs.Minimum Deposit Term' | translate }}: - {{ - recurringDepositsAccountProductTemplate.minDepositTerm - ? recurringDepositsAccountProductTemplate.minDepositTerm - : '' - }} - {{ - (this.recurringDepositsAccountProductTemplate.minDepositTermType - ? this.recurringDepositsAccountProductTemplate.minDepositTermType.value - : '' - ) | translateKey: 'catalogs' - }} -
- -
- {{ 'labels.inputs.In Multiples Of' | translate }}: - {{ - recurringDepositsAccountProductTemplate.inMultiplesOfDepositTerm - ? recurringDepositsAccountProductTemplate.inMultiplesOfDepositTerm - : '' - }} - {{ - (this.recurringDepositsAccountProductTemplate.inMultiplesOfDepositTermType - ? this.recurringDepositsAccountProductTemplate.inMultiplesOfDepositTermType.value - : '' - ) | translateKey: 'catalogs' - }} -
- -
- {{ 'labels.inputs.Maximum Deposit Term' | translate }} - {{ - recurringDepositsAccountProductTemplate.maxDepositTerm - ? recurringDepositsAccountProductTemplate.maxDepositTerm - : '' - }} - {{ - (this.recurringDepositsAccountProductTemplate.maxDepositTermType - ? this.recurringDepositsAccountProductTemplate.maxDepositTermType.value - : '' - ) | translateKey: 'catalogs' - }} -
- -

{{ 'labels.inputs.Interest Rate Chart' | translate }}

- - -
- {{ 'labels.inputs.name' | translate }}: - {{ recurringDepositsAccountProductTemplate?.accountChart.name }} -
- -
- {{ 'labels.inputs.Valid from Date' | translate }}: - {{ recurringDepositsAccountProductTemplate?.accountChart.fromDate | dateFormat }} -
- -
- {{ 'labels.inputs.End Date' | translate }}: - {{ recurringDepositsAccountProductTemplate?.accountChart.endDate | dateFormat }} -
- -
- {{ 'labels.inputs.Description' | translate }}: - {{ recurringDepositsAccountProductTemplate?.accountChart.description }} -
- -
- {{ 'labels.inputs.Primary Grouping by Amount' | translate }}: - {{ - recurringDepositsAccountProductTemplate?.accountChart.isPrimaryGroupingByAmount | yesNo - }} -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Period' | translate }} - {{ chartSlab.fromPeriod }} - {{ chartSlab.toPeriod }} {{ - chartSlab.periodType.value | translateKey: 'catalogs' - }} - {{ 'labels.inputs.Amount Range' | translate }} - {{ chartSlab.amountRangeFrom | formatNumber }} - {{ chartSlab.amountRangeTo | formatNumber }} - {{ 'labels.inputs.Interest' | translate }}{{ chartSlab.annualInterestRate | formatNumber }} %{{ 'labels.inputs.Description' | translate }}{{ chartSlab.description }}{{ 'labels.inputs.Actions' | translate }} - - -
- -

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

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Entity Type' | translate }} - {{ incentive.entityType.value }} - {{ 'labels.inputs.Attribute Name' | translate }} - {{ incentive.attributeName.value }} - {{ 'labels.inputs.Condition Type' | translate }} - {{ incentive.conditionType.value | titlecase }} - {{ 'labels.inputs.Attribute Value' | translate }} - {{ incentive.attributeValueDesc }} - {{ 'labels.inputs.Incentive Type' | translate }} - {{ incentive.incentiveType.value }} - {{ 'labels.inputs.Interest' | translate }} - {{ incentive.amount }} -
-
+
+
+
+ {{ 'labels.inputs.Product' | translate }} + {{ + recurringDepositAccountData?.productId + | find: recurringDepositsAccountTemplate?.productOptions : 'id' : 'name' + }}
-
+
+ {{ 'labels.inputs.Submitted On' | translate }} + {{ recurringDepositAccountData?.submittedOnDate | dateFormat }} +
-
-

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

+
+ {{ 'labels.inputs.Field Officer' | translate }} + {{ + recurringDepositAccountData?.fieldOfficerId + | find: recurringDepositsAccountProductTemplate?.fieldOfficerOptions : 'id' : 'displayName' + }} +
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.name' | translate }} - {{ charge.name + ', ' + charge.currency.displaySymbol }} - {{ 'labels.inputs.Type' | translate }} - {{ charge.chargeCalculationType.value | translateKey: 'catalogs' }} - {{ 'labels.inputs.Amount' | translate }} - {{ charge.amount | formatNumber }} - {{ 'labels.inputs.Date' | translate }} - - {{ (charge.dueDate | dateFormat) || 'Unassigned' }} - - - {{ (charge.feeOnMonthDay | dateFormat) || 'Unassigned' }} - - - {{ 'labels.inputs.N/A' | translate }} - - {{ 'labels.inputs.Repayments Every' | translate }} - {{ charge.feeInterval || 'Not Provided' }} - {{ 'labels.inputs.Collected On' | translate }} - {{ charge.chargeTimeType.value }} -
+ @if (recurringDepositAccountData?.externalId) { +
+ {{ 'labels.inputs.External Id' | translate }} + {{ recurringDepositAccountData?.externalId }} +
+ } +
+
+ + + @if ( + recurringDepositsAccountProductTemplate?.accountType || + recurringDepositsAccountProductTemplate?.customerSegment || + recurringDepositsAccountProductTemplate?.productCode || + recurringDepositsAccountProductTemplate?.effectiveDate || + recurringDepositsAccountProductTemplate?.endDate || + recurringDepositsAccountProductTemplate?.introducerRequired !== undefined || + recurringDepositsAccountProductTemplate?.shareLinkingEnabled !== undefined + ) { +
+
+ Product Classification +
+
+
+ @if (recurringDepositsAccountProductTemplate?.accountType) { +
+ Account Type + {{ recurringDepositsAccountProductTemplate?.accountType }} +
+ } + @if (recurringDepositsAccountProductTemplate?.customerSegment) { +
+ Customer Segment + {{ recurringDepositsAccountProductTemplate?.customerSegment }} +
+ } + @if (recurringDepositsAccountProductTemplate?.productCode) { +
+ Product Code + {{ recurringDepositsAccountProductTemplate?.productCode }} +
+ } + @if (recurringDepositsAccountProductTemplate?.effectiveDate) { +
+ Effective Date + {{ + recurringDepositsAccountProductTemplate?.effectiveDate | dateFormat + }} +
+ } + @if (recurringDepositsAccountProductTemplate?.endDate) { +
+ End Date + {{ recurringDepositsAccountProductTemplate?.endDate | dateFormat }} +
+ } + @if ( + recurringDepositsAccountProductTemplate?.introducerRequired !== undefined && + recurringDepositsAccountProductTemplate?.introducerRequired !== null + ) { +
+ Introducer Required + + {{ recurringDepositsAccountProductTemplate?.introducerRequired | yesNo }} + +
+ } + @if ( + recurringDepositsAccountProductTemplate?.shareLinkingEnabled !== undefined && + recurringDepositsAccountProductTemplate?.shareLinkingEnabled !== null + ) { +
+ Share Linking Enabled + + {{ recurringDepositsAccountProductTemplate?.shareLinkingEnabled | yesNo }} + +
+ } +
+
+
+ } + + +
+
+ {{ 'labels.heading.Currency' | translate }} +
+
+
+
+ {{ 'labels.inputs.Currency' | translate }} + {{ + recurringDepositsAccountProductTemplate?.currency?.code || + recurringDepositsAccountProductTemplate?.currency?.name || + '—' + }} +
+ +
+ {{ 'labels.inputs.Decimal Places' | translate }} + {{ recurringDepositsAccountProductTemplate?.currency?.decimalPlaces }} +
+ +
+ {{ 'labels.inputs.Currency In Multiples Of' | translate }} + {{ recurringDepositsAccountProductTemplate?.currency?.inMultiplesOf }} +
+ + @if (recurringDepositsAccountProductTemplate?.withHoldTax) { +
+ {{ 'labels.inputs.Is Withhold Tax Applicable' | translate }} + + {{ recurringDepositAccountData?.withHoldTax | yesNo }} + +
+ } + + @if (recurringDepositAccountData?.withHoldTax && recurringDepositsAccountProductTemplate?.taxGroup?.name) { +
+ {{ 'labels.inputs.Tax Group' | translate }} + {{ recurringDepositsAccountProductTemplate?.taxGroup?.name }} +
+ } + + @if ( + recurringDepositsAccountProductTemplate?.dormancyAlert !== undefined && + recurringDepositsAccountProductTemplate?.dormancyAlert !== null + ) { +
+ Dormancy Alert + + {{ recurringDepositsAccountProductTemplate?.dormancyAlert | yesNo }} + +
+ } + + @if ( + recurringDepositsAccountProductTemplate?.balanceSwapOption !== undefined && + recurringDepositsAccountProductTemplate?.balanceSwapOption !== null + ) { +
+ Balance Swap Option + + {{ recurringDepositsAccountProductTemplate?.balanceSwapOption | yesNo }} + +
+ } + + @if ( + recurringDepositsAccountProductTemplate?.msme !== undefined && + recurringDepositsAccountProductTemplate?.msme !== null + ) { +
+ MSME + + {{ recurringDepositsAccountProductTemplate?.msme | yesNo }} + +
+ } + + @if ( + recurringDepositsAccountProductTemplate?.nominationFacility !== undefined && + recurringDepositsAccountProductTemplate?.nominationFacility !== null + ) { +
+ Nomination Facility + + {{ recurringDepositsAccountProductTemplate?.nominationFacility | yesNo }} + +
+ } + + @if ( + recurringDepositsAccountProductTemplate?.autoDebitFacility !== undefined && + recurringDepositsAccountProductTemplate?.autoDebitFacility !== null + ) { +
+ Auto Debit Facility + + {{ recurringDepositsAccountProductTemplate?.autoDebitFacility | yesNo }} + +
+ } +
+
+
+ + +
+
+ {{ 'labels.heading.Terms' | translate }} +
+
+
+
+ {{ 'labels.inputs.Nominal Annual Interest' | translate }} + + {{ recurringDepositsAccountProductTemplate?.nominalAnnualInterestRate ?? '—' }} + @if (recurringDepositsAccountProductTemplate?.nominalAnnualInterestRate != null) { + % + } + +
+ + @if (recurringDepositsAccountProductTemplate?.effectiveDate) { +
+ Effective Date + {{ recurringDepositsAccountProductTemplate?.effectiveDate | dateFormat }} +
+ } + +
+ {{ 'labels.inputs.Interest Compounding Period' | translate }} + {{ + recurringDepositAccountData?.interestCompoundingPeriodType + | find: recurringDepositsAccountProductTemplate?.interestCompoundingPeriodTypeOptions : 'id' : 'value' + | translateKey: 'catalogs' + }} +
+ +
+ {{ 'labels.inputs.Interest Posting Period' | translate }} + {{ + recurringDepositAccountData?.interestPostingPeriodType + | find: recurringDepositsAccountProductTemplate?.interestPostingPeriodTypeOptions : 'id' : 'value' + | translateKey: 'catalogs' + }} +
+ + @if (recurringDepositsAccountProductTemplate?.averagePeriod) { +
+ Average Period + {{ recurringDepositsAccountProductTemplate?.averagePeriod }} +
+ } + +
+ {{ 'labels.inputs.Interest Calculated using' | translate }} + {{ + recurringDepositAccountData?.interestCalculationType + | find: recurringDepositsAccountProductTemplate?.interestCalculationTypeOptions : 'id' : 'value' + | translateKey: 'catalogs' + }} +
+ +
+ {{ 'labels.inputs.Days in Year' | translate }} + {{ + recurringDepositAccountData?.interestCalculationDaysInYearType + | find: recurringDepositsAccountProductTemplate?.interestCalculationDaysInYearTypeOptions : 'id' : 'value' + | translateKey: 'catalogs' + }} +
+ + @if ( + recurringDepositsAccountProductTemplate?.autoPost !== undefined && + recurringDepositsAccountProductTemplate?.autoPost !== null + ) { +
+ Auto Interest Post + + {{ recurringDepositsAccountProductTemplate?.autoPost | yesNo }} + +
+ } + + @if ( + recurringDepositsAccountProductTemplate?.frequencyFixDay !== undefined && + recurringDepositsAccountProductTemplate?.frequencyFixDay !== null + ) { +
+ Frequency (Fix Day) + {{ recurringDepositsAccountProductTemplate?.frequencyFixDay }} +
+ } + + @if (recurringDepositsAccountProductTemplate?.lastInterestDate) { +
+ Last Interest Date + {{ + recurringDepositsAccountProductTemplate?.lastInterestDate | dateFormat + }} +
+ } + + @if (recurringDepositsAccountProductTemplate?.interestCalculationTypeName) { +
+ Interest Calculation Type + {{ + recurringDepositsAccountProductTemplate?.interestCalculationTypeName + }} +
+ } +
+
+
+ + +
+
+ {{ 'labels.heading.Settings' | translate }} +
+
+ +
+
Premature Option
+
+
+ Loan Facility + + {{ recurringDepositAccountData?.loanFacility | yesNo }} + +
+
+ Premature Withdrawal Option + + {{ recurringDepositAccountData?.prematureWithdrawalOption | yesNo }} + +
+
+ Tenure Flexibility + + {{ recurringDepositAccountData?.tenureFlexibility | yesNo }} + +
+
+ Lien Marked + + {{ recurringDepositAccountData?.lienMarked | yesNo }} + +
+
+ Withdrawal Fee for Transfers + + {{ recurringDepositAccountData?.withdrawalFeeForTransfers | yesNo }} + +
+
+
+ + + + +
+
{{ 'labels.inputs.Lock-in Period' | translate }}
+
+
+ Enable Lock-in Period + + {{ recurringDepositAccountData?.enableLockinPeriod | yesNo }} + +
+ @if (recurringDepositAccountData?.enableLockinPeriod) { +
+ Lock-in Frequency + + {{ recurringDepositAccountData?.lockinPeriodFrequency }} + {{ + recurringDepositAccountData?.lockinPeriodFrequencyType + | find: recurringDepositsAccountProductTemplate?.lockinPeriodFrequencyTypeOptions : 'id' : 'value' + | translateKey: 'catalogs' + }} + +
+ } +
+
+ + + + +
+
Maturity Instructions
+
+
+ Maturity Instructions + + {{ recurringDepositAccountData?.maturityInstructions | yesNo }} + +
+
+ Credit Maturity Proceeds + + {{ recurringDepositAccountData?.creditMaturityProceeds | yesNo }} + +
+
+ Penalty / Grace Period for Missed Installments + + {{ recurringDepositAccountData?.penaltyGracePeriodMissedInstallments | yesNo }} + +
+
+ Panel Request Applicable + + {{ recurringDepositAccountData?.panelRequestApplicable | yesNo }} + +
+
+ No Penalty for Nominee / Legal Heir Closure + + {{ recurringDepositAccountData?.noPenaltyNomineeHeirClosure | yesNo }} + +
+
+ Deceased Settlement + + {{ recurringDepositAccountData?.deceasedSettlement | yesNo }} + +
+
+ Allow Debit Txn after Due Date + + {{ recurringDepositAccountData?.allowDebitTxnAfterDueDate | yesNo }} + +
+
+
+ + + + +
+
Additional Rules
+
+
+ Account Closed with Zero Balance in Day Closing + + {{ recurringDepositAccountData?.accountClosedZeroBalanceInDayClosing | yesNo }} + +
+ @if (recurringDepositAccountData?.minBalanceForInterestCalculation) { +
+ Min. Balance for Interest Calculation + {{ + recurringDepositAccountData?.minBalanceForInterestCalculation | formatNumber + }} +
+ } + @if (recurringDepositAccountData?.mandatoryRecommendedDepositAmount) { +
+ {{ 'labels.inputs.Recurring Deposit Amount' | translate }} + {{ + recurringDepositAccountData?.mandatoryRecommendedDepositAmount | formatNumber + }} +
+ } +
+
+
+
+ + + @if (_activeRdVersion || recurringDepositsAccountProductTemplate?.accountChart) { +
+
+ {{ 'labels.inputs.Interest Rate Chart' | translate }} +
+
+ @if (_activeRdVersion) { +
+
+ Version + {{ _activeRdVersion.versionLabel }} +
+
+ Active + Yes +
+
+ Effective From + {{ _activeRdVersion.effectiveFromDate }} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Sr.{{ slab.srNo }}Particular{{ slab.description }}From{{ slab.fromPeriod }}To{{ slab.toPeriod ?? 'Unassigned' }}Period Type{{ slab.periodTypeLabel }}Annual Rate (%){{ slab.annualInterestRate | formatNumber }}
+ } @else { +
+ @if (recurringDepositsAccountProductTemplate?.accountChart?.name) { +
+ {{ 'labels.inputs.name' | translate }} + {{ recurringDepositsAccountProductTemplate?.accountChart?.name }} +
+ } + + @if (recurringDepositsAccountProductTemplate?.accountChart?.fromDate) { +
+ {{ 'labels.inputs.Valid from Date' | translate }} + {{ + recurringDepositsAccountProductTemplate?.accountChart?.fromDate | dateFormat + }} +
+ } + + @if (recurringDepositsAccountProductTemplate?.accountChart?.endDate) { +
+ {{ 'labels.inputs.End Date' | translate }} + {{ + recurringDepositsAccountProductTemplate?.accountChart?.endDate | dateFormat + }} +
+ } + + @if (recurringDepositsAccountProductTemplate?.accountChart?.description) { +
+ {{ 'labels.inputs.Description' | translate }} + {{ + recurringDepositsAccountProductTemplate?.accountChart?.description + }} +
+ } + +
+ {{ 'labels.inputs.Primary Grouping by Amount' | translate }} + {{ + recurringDepositsAccountProductTemplate?.accountChart?.isPrimaryGroupingByAmount | yesNo + }} +
+
+ + @if (interestRateChartData?.length) { + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.Period' | translate }} + {{ chartSlab.fromPeriod }} - {{ chartSlab.toPeriod }} {{ + chartSlab.periodType?.value | translateKey: 'catalogs' + }} + {{ 'labels.inputs.Amount Range' | translate }} + {{ chartSlab.amountRangeFrom | formatNumber }} - {{ chartSlab.amountRangeTo | formatNumber }} + {{ 'labels.inputs.Interest' | translate }}{{ chartSlab.annualInterestRate | formatNumber }} %{{ 'labels.inputs.Description' | translate }}{{ chartSlab.description }}{{ 'labels.inputs.Actions' | translate }} + + +
+ +

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

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.Entity Type' | translate }}{{ incentive.entityType?.value }}{{ 'labels.inputs.Attribute Name' | translate }}{{ incentive.attributeName?.value }}{{ 'labels.inputs.Condition Type' | translate }}{{ incentive.conditionType?.value | titlecase }}{{ 'labels.inputs.Attribute Value' | translate }}{{ incentive.attributeValueDesc }}{{ 'labels.inputs.Incentive Type' | translate }}{{ incentive.incentiveType?.value }}{{ 'labels.inputs.Interest' | translate }}{{ incentive.amount }}
+
+
+
+ } + } +
+
+ } + + + @if (recurringDepositAccountData?.charges?.length) { +
+
+ {{ 'labels.heading.Charges' | translate }} +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.name' | translate }}{{ charge.name + ', ' + charge.currency?.displaySymbol }}{{ 'labels.inputs.Type' | translate }} + {{ charge.chargeCalculationType?.value | translateKey: 'catalogs' }} + {{ 'labels.inputs.Amount' | translate }}{{ charge.amount | formatNumber }}{{ 'labels.inputs.Collected On' | translate }}{{ charge.chargeTimeType?.value }}{{ 'labels.inputs.Date' | translate }} + + {{ (charge.dueDate | dateFormat) || 'Unassigned' }} + + + {{ (charge.feeOnMonthDay | dateFormat) || 'Unassigned' }} + + + {{ 'labels.inputs.N/A' | translate }} + + {{ 'labels.inputs.Repayments Every' | translate }}{{ charge.feeInterval || 'Not Provided' }}
+
+
+ }
diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.scss b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.scss index 4c9f978a6..433db21ac 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.scss +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.scss @@ -6,35 +6,120 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -table { - width: 100%; +// ── Wrapper ────────────────────────────────────────────────────────────────── +.preview-wrapper { + display: flex; + flex-direction: column; + gap: 1.25rem; + padding: 0.5rem 0; } -.mat-elevation-z1 { - margin: 1em 0 1.5em; +// ── Section card ───────────────────────────────────────────────────────────── +.preview-section { + border: 1px solid #e0e0e0; + border-radius: 8px; + overflow: hidden; + background: #fff; + box-shadow: 0 1px 3px rgb(0 0 0 / 6%); + + &__header { + background: #f5f5f5; + border-bottom: 1px solid #e0e0e0; + padding: 0.6rem 1rem; + } + + &__title { + font-size: 0.95rem; + font-weight: 600; + color: #333; + text-transform: uppercase; + letter-spacing: 0.04em; + } } -h2, -h3, -h4 { - margin: 0; +// ── Body ───────────────────────────────────────────────────────────────────── +.preview-body { + padding: 1rem 1.25rem; +} + +// ── Subsection (inside Settings) ───────────────────────────────────────────── +.preview-subsection { + margin-bottom: 1rem; + + &__title { + font-size: 0.82rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #666; + margin-bottom: 0.6rem; + } +} + +.preview-divider { + margin: 0.75rem 0 1rem; +} + +// ── Grid ───────────────────────────────────────────────────────────────────── +.preview-grid { + display: grid; + gap: 0.75rem 1.5rem; + + &--three { + grid-template-columns: repeat(3, 1fr); + + @media (width <= 768px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (width <= 480px) { + grid-template-columns: 1fr; + } + } +} + +// ── Field ───────────────────────────────────────────────────────────────────── +.preview-field { + display: flex; + flex-direction: column; + gap: 0.15rem; +} + +.preview-label { + font-size: 0.72rem; + color: #888; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.preview-value { + font-size: 0.92rem; + color: #222; font-weight: 500; } -span { - margin: 0.5em 0; +// ── Yes/No badge ───────────────────────────────────────────────────────────── +.preview-badge { + display: inline-block; + padding: 0.1rem 0.55rem; + border-radius: 12px; + font-size: 0.78rem; + font-weight: 600; + background: #ffebee; + color: #c62828; + width: fit-content; + + &--yes { + background: #e8f5e9; + color: #2e7d32; + } } -mat-divider { - margin: 0 0 0.5em; -} - -.margin-t { - margin-top: 1em; -} - -.margin-b { - margin-bottom: 0.25em; +// ── Tables ─────────────────────────────────────────────────────────────────── +.preview-table { + width: 100%; + margin-top: 1rem; } .irc-table { @@ -56,3 +141,78 @@ mat-divider { } } } + +.margin-t { + margin-top: 1em; +} + +// ── Product feature flag rows (same style as details step) ──────────────────── +.preview-feature-list { + margin-top: 1rem; + border-top: 1px solid #e0e0e0; +} + +.preview-feature-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 0; + border-bottom: 1px solid #f0f0f0; + gap: 1rem; +} + +.preview-feature-info { + display: flex; + flex-direction: column; + gap: 0.15rem; + + strong { + font-size: 0.88rem; + font-weight: 600; + color: #222; + } + + span { + font-size: 0.78rem; + color: #26a69a; + margin: 0; + } +} + +.preview-feature-choices { + display: flex; + gap: 0.5rem; + flex-shrink: 0; +} + +.rad-yn { + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.2rem 0.75rem; + border-radius: 20px; + border: 2px solid #ccc; + font-size: 0.82rem; + color: #999; + cursor: default; + + &::before { + content: ''; + width: 12px; + height: 12px; + border-radius: 50%; + border: 2px solid #ccc; + display: inline-block; + } + + &--active { + border-color: #4caf50; + color: #2e7d32; + font-weight: 600; + + &::before { + border-color: #4caf50; + background: #4caf50; + } + } +} diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.ts index d3d3a1a9f..d3186524f 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.ts @@ -73,9 +73,25 @@ export class RecurringDepositsAccountPreviewStepComponent implements OnChanges { @Input() recurringDepositsAccountTemplate: any; @Input() recurringDepositsAccountProductTemplate: any; @Input() recurringDepositAccountData: any; + @Input() set rdInterestRates(rates: any[]) { + this._rdInterestRates = rates || []; + this._activeRdVersion = this._rdInterestRates.find((v: any) => v.isActive) ?? this._rdInterestRates[0] ?? null; + } /** Output the submit action */ @Output() submitEvent = new EventEmitter(); + _rdInterestRates: any[] = []; + _activeRdVersion: any = null; + + readonly rdSlabColumns = [ + 'srNo', + 'description', + 'fromPeriod', + 'toPeriod', + 'periodTypeLabel', + 'annualInterestRate' + ]; + /** Charges Displayed Columns */ chargesDisplayedColumns: string[] = [ 'name', @@ -113,7 +129,7 @@ export class RecurringDepositsAccountPreviewStepComponent implements OnChanges { ngOnChanges() { if (this.recurringDepositsAccountProductTemplate) { - this.interestRateChartData = this.recurringDepositsAccountProductTemplate.accountChart.chartSlabs; + this.interestRateChartData = this.recurringDepositsAccountProductTemplate.accountChart?.chartSlabs ?? []; } } } diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-settings-step/recurring-deposits-account-settings-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-settings-step/recurring-deposits-account-settings-step.component.html index 083d8f2bb..696f8780a 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-settings-step/recurring-deposits-account-settings-step.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-settings-step/recurring-deposits-account-settings-step.component.html @@ -7,219 +7,145 @@ --> -
- - {{ 'labels.inputs.Is Mandatory Deposit' | translate }}? - - - - {{ 'labels.inputs.Adjust advance payments toward future installments' | translate }}? - - - - {{ 'labels.inputs.Allow Withdrawals' | translate }}? - - -

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

- - - {{ 'labels.inputs.Frequency' | translate }} - - - - - {{ 'labels.inputs.Type' | translate }} - - @for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) { - - {{ lockinPeriodFrequencyType.value | translateKey: 'catalogs' }} - - } - - - -

{{ 'labels.heading.Recurring Deposit Details' | translate }}

- @if (currency) { - - - } - -

{{ 'labels.heading.Deposit Period' | translate }}

- - - {{ 'labels.inputs.Deposit Period' | translate }} - - - - - {{ 'labels.inputs.Type' | translate }} - - @for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) { - - {{ periodFrequencyType.value | translateKey: 'catalogs' }} - - } - - - - - {{ 'labels.inputs.Deposit Frequency Same as Group/Center meeting' | translate }} - - - @if (!recurringDepositAccountSettingsForm.value.isCalendarInherited) { - - {{ 'labels.inputs.Deposit Start Date' | translate }} - - - - -

{{ 'labels.heading.Deposit Frequency' | translate }}

- - {{ 'labels.inputs.Deposit Frequency' | translate }} - - - {{ 'labels.inputs.Deposit Frequency' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - {{ 'labels.inputs.Type' | translate }} - - @for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) { - - {{ periodFrequencyType.value | translateKey: 'catalogs' }} - - } - - - {{ 'labels.inputs.Deposit Frequency Type' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - } - - - -

{{ 'labels.heading.Minimum Deposit Term' | translate }}

- - - {{ 'labels.inputs.Frequency' | translate }} - - - {{ 'labels.inputs.Minimum Deposit Term Frequency' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.Frequency Type' | translate }} - - @for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) { - - {{ periodFrequencyType.value | translateKey: 'catalogs' }} - - } - - - {{ 'labels.inputs.Minimum Deposit Term Frequency Type' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - -

{{ 'labels.heading.And thereafter, in Multiples of' | translate }}

- - - {{ 'labels.inputs.Frequency' | translate }} - - - - - {{ 'labels.inputs.Type' | translate }} - - @for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) { - - {{ periodFrequencyType.value | translateKey: 'catalogs' }} - - } - - - -

{{ 'labels.heading.Maximum Deposit Term' | translate }}

- - - {{ 'labels.inputs.Frequency' | translate }} - - - - - {{ 'labels.inputs.Type' | translate }} - - @for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) { - - {{ periodFrequencyType.value | translateKey: 'catalogs' }} - - } - - - - - -

{{ 'labels.heading.For Pre-mature closure' | translate }}

- - - {{ 'labels.inputs.Apply Penal Interest (less)' | translate }} - - - - {{ 'labels.inputs.Penal Interest' | translate }} (%) - - - - - {{ 'labels.inputs.Period' | translate }} - - @for (preClosurePenalInterestOnType of preClosurePenalInterestOnTypeData; track preClosurePenalInterestOnType) { - - {{ preClosurePenalInterestOnType.value | translateKey: 'catalogs' }} - - } - - - - - - - {{ 'labels.inputs.Minimum Balance For Interest Calculation' | translate }} - - - - @if (taxGroup) { -

{{ 'labels.heading.Tax Group' | translate }}

- - {{ 'labels.inputs.Is Withhold Tax Applicable' | translate }}? - - @if (recurringDepositAccountSettingsForm.controls.taxGroupId) { - - {{ 'labels.inputs.Tax Group' | translate }} - - - } - } + +
+
+ + 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 (recurringDepositAccountSettingsForm.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 + + +
+
+
+ + + @if (recurringDepositAccountSettingsForm.contains('withHoldTax')) { +
+
+ + {{ 'labels.inputs.Is Withhold Tax Applicable' | translate }} +
+
+
+ {{ 'labels.inputs.Is Withhold Tax Applicable' | translate }} + +
+ @if ( + recurringDepositAccountSettingsForm.value.withHoldTax && + recurringDepositAccountSettingsForm.contains('taxGroupId') + ) { + + {{ 'labels.inputs.Tax Group' | translate }} + + + } +
+
+ } +
+ +
+ + + + + DIGITAL + + + +
+ + +
+
+ + {{ 'labels.inputs.PREVIEW' | translate }} { @@ -100,32 +94,17 @@ export class CreateCurrentAccountComponent { return this.currentAccountDetailsStep.currentAccountDetailsForm; } - /** - * Retrieves new term form (interest rate). - */ - get currentAccountTermForm() { - return this.currentAccountTermStep.currentAccountTermForm; + /** Stub forms needed by preview step inputs */ + get currentAccountTermForm(): FormGroup { + return new FormGroup({}); } - /** - * Retrieves settings form. - */ - get currentAccountSettingsForm() { - return this.currentAccountSettingsStep.currentAccountSettingsForm; + get currentAccountSettingsForm(): FormGroup { + return new FormGroup({}); } - /** - * Retrieves old terms form — still used for currencyCode by charges step. - */ - get currentAccountTermsForm() { - return this.currentAccountTermsStep.currentAccountTermsForm; - } - - /** - * Checks validity of overall current account form. - */ - get currentAccountFormValid() { - return this.currentAccountDetailsForm.valid && this.currentAccountTermForm.valid; + get currentAccountTermsForm(): FormGroup { + return new FormGroup({}); } /** @@ -133,10 +112,7 @@ export class CreateCurrentAccountComponent { */ get currentAccount() { return { - ...this.currentAccountDetailsStep.currentAccountDetails, - ...this.currentAccountTermStep.currentAccountTerms, - ...this.currentAccountSettingsStep.currentAccountSettings, - ...this.currentAccountChargesStep.currentAccountCharges + ...this.currentAccountDetailsStep.currentAccountDetails }; } @@ -149,15 +125,8 @@ export class CreateCurrentAccountComponent { const monthDayFormat = 'dd MMMM'; const currentAccount = { ...this.currentAccount, - charges: this.currentAccount.charges.map((charge: any) => ({ - chargeId: charge.id, - amount: charge.amount, - dueDate: charge.dueDate ? this.dateUtils.formatDate(charge.dueDate, dateFormat) : charge.dueDate, - feeOnMonthDay: charge.feeOnMonthDay - ? this.dateUtils.formatDate(charge.feeOnMonthDay, monthDayFormat) - : charge.feeOnMonthDay, - feeInterval: charge.feeInterval - })), + ...this.currentAccountCurrencyStep.currencyStepData, + charges: [], submittedOnDate: this.dateUtils.formatDate(this.currentAccount.submittedOnDate, dateFormat), dateFormat, monthDayFormat, diff --git a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html index e9ba49bac..e790fd695 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html @@ -26,6 +26,30 @@
+
+
+

Balance Swap Option

+

Allows balance to be swapped or transferred between linked accounts as per product configuration.

+
+ +
+ +
+
+

MSME

+

Account is categorized under the Micro, Small and Medium Enterprises segment.

+
+ +
+ +
+
+

Auto Debit Facility

+

Enable automatic debit instructions for recurring payments linked to this account.

+
+ +
+

Dormancy Alert

@@ -36,25 +60,7 @@
-
-
-

Balance Swap Option

-

Allows balance to be swapped or transferred between linked accounts as per product configuration.

-
- - {{ currentAccountProductTemplate?.balanceSwapOption ? 'Yes' : 'No' }} - -
- -
-
-

MSME

-

Account is categorized under the Micro, Small and Medium Enterprises segment.

-
- - {{ currentAccountProductTemplate?.msme ? 'Yes' : 'No' }} - -
+
@@ -66,14 +72,8 @@
-
-
-

Auto Debit Facility

-

Enable automatic debit instructions for recurring payments linked to this account.

-
- - {{ currentAccountProductTemplate?.autoDebitFacility ? 'Yes' : 'No' }} - -
+ @if (currentAccountProductTemplate?.nominationFacility) { + + }
diff --git a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.scss b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.scss index 17712a898..68c9a94a2 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.scss +++ b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.scss @@ -114,3 +114,17 @@ $gray-900: #202124; border-color: #bbf7d0; } } + +.sac-alert--editable { + border-color: #bae6fd; + background: #f0f9ff; + + &:hover { + border-color: #7dd3fc; + box-shadow: 0 2px 8px rgb(2 132 199 / 10%); + } + + h4 { + color: #0369a1; + } +} diff --git a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts index 805c4336b..c3cae7dce 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts +++ b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts @@ -6,15 +6,55 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnChanges, SimpleChanges } from '@angular/core'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { NomineeFormComponent } from 'app/shared/nominee-form/nominee-form.component'; +import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dormancy-alert-panel.component'; +import { MatSlideToggle } from '@angular/material/slide-toggle'; +import { FormsModule } from '@angular/forms'; @Component({ selector: 'mifosx-current-account-currency-step', templateUrl: './current-account-currency-step.component.html', styleUrls: ['./current-account-currency-step.component.scss'], - imports: [...STANDALONE_SHARED_IMPORTS] + imports: [ + ...STANDALONE_SHARED_IMPORTS, + NomineeFormComponent, + DormancyAlertPanelComponent, + MatSlideToggle, + FormsModule + ] }) -export class CurrentAccountCurrencyStepComponent { +export class CurrentAccountCurrencyStepComponent implements OnChanges { @Input() currentAccountProductTemplate: any; + + nomineeData: any = null; + + // Editable feature toggles (pre-filled from product, overridable by user) + balanceSwapOption = false; + msme = false; + autoDebitFacility = false; + + ngOnChanges(changes: SimpleChanges): void { + if (changes['currentAccountProductTemplate']?.currentValue) { + const pt = changes['currentAccountProductTemplate'].currentValue; + this.balanceSwapOption = !!pt.balanceSwapOption; + this.msme = !!pt.msme; + this.autoDebitFacility = !!pt.autoDebitFacility; + } + } + + onNomineeChange(event: any): void { + this.nomineeData = event; + } + + /** Called by parent to collect currency step values */ + get currencyStepData() { + return { + balanceSwapOption: this.balanceSwapOption, + msme: this.msme, + autoDebitFacility: this.autoDebitFacility, + ...(this.nomineeData ? { nominee: this.nomineeData } : {}) + }; + } } diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html index e5914fe18..440bf7b4f 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html @@ -7,7 +7,7 @@ --> - +
@@ -45,9 +45,7 @@ {{ 'labels.commons.required' | translate }} - } - @if (currentAccountProductSelected) { {{ 'labels.inputs.Field Officer' | translate }} @@ -56,9 +54,7 @@ } - } - @if (currentAccountProductSelected) { {{ 'labels.inputs.External ID' | translate }} @@ -67,8 +63,8 @@
- @if (currentAccountProductSelected && productTemplate) { +
@@ -125,6 +121,10 @@
+ + @if (productTemplate?.introducerRequired) { + + } }
diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.scss b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.scss index 7c9847ae8..cd7492271 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.scss +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.scss @@ -15,7 +15,7 @@ $gray-900: #202124; .sad-section { border: 1px solid $border; border-radius: 10px; - overflow: hidden; + overflow: visible; background: #fff; &--mt { @@ -103,3 +103,61 @@ $gray-900: #202124; border-color: #bbf7d0; } } + +// ── Account Feature Toggles ──────────────────────────────────────────────── +.sad-toggles { + display: flex; + flex-direction: column; + gap: 10px; + padding: 4px 0; +} + +.sad-toggle-card { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 18px; + border: 1.5px solid #e2e8f0; + border-radius: 10px; + background: #fff; + transition: + border-color 0.2s, + box-shadow 0.2s; + + &:hover { + border-color: #93c5fd; + box-shadow: 0 1px 6px rgb(59 130 246 / 10%); + } + + &__left { + display: flex; + align-items: center; + gap: 12px; + + > i { + width: 36px; + height: 36px; + border-radius: 8px; + background: #f0f9ff; + border: 1px solid #bae6fd; + color: #0284c7; + display: flex; + align-items: center; + justify-content: center; + font-size: 15px; + flex: 0 0 auto; + } + } + + &__label { + font-size: 13.5px; + font-weight: 600; + color: #1e293b; + } + + &__hint { + font-size: 11.5px; + color: #64748b; + margin-top: 2px; + } +} diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts index c547fedcf..bacad5cdb 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts @@ -17,6 +17,7 @@ import { SettingsService } from 'app/settings/settings.service'; import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component'; /** * Current Account Details Step @@ -29,7 +30,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; ...STANDALONE_SHARED_IMPORTS, MatStepperPrevious, FaIconComponent, - MatStepperNext + MatStepperNext, + IntroducerSearchComponent ] }) export class CurrentAccountDetailsStepComponent implements OnInit { @@ -56,6 +58,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit { currentAccountDetailsForm: UntypedFormGroup; currentAccountProductSelected = false; + introducerId: number | null = null; /** Current Account Template with product data */ @Output() currentAccountProductTemplate = new EventEmitter(); @@ -102,7 +105,10 @@ export class CurrentAccountDetailsStepComponent implements OnInit { Validators.required ], fieldOfficerId: [''], - externalId: [''] + externalId: [''], + balanceSwapOption: [false], + msme: [false], + autoDebitFacility: [false] }); } @@ -131,6 +137,12 @@ export class CurrentAccountDetailsStepComponent implements OnInit { this.currentAccountProductTemplate.emit(response); this.fieldOfficerData = response.fieldOfficerOptions; this.currentAccountProductSelected = true; + // Pre-populate toggles from product defaults (user can override) + this.currentAccountDetailsForm.patchValue({ + balanceSwapOption: !!response.balanceSwapOption, + msme: !!response.msme, + autoDebitFacility: !!response.autoDebitFacility + }); this.cdr.detectChanges(); if (!this.isFieldOfficerPatched && this.currentAccountTemplate.fieldOfficerId) { this.currentAccountDetailsForm.get('fieldOfficerId').patchValue(this.currentAccountTemplate.fieldOfficerId); @@ -166,7 +178,15 @@ export class CurrentAccountDetailsStepComponent implements OnInit { /** * Returns current account form value. */ + onIntroducerChange(event: { clientId: number; displayName: string } | null): void { + this.introducerId = event?.clientId ?? null; + } + get currentAccountDetails() { - return this.currentAccountDetailsForm.getRawValue(); + const value = this.currentAccountDetailsForm.getRawValue(); + if (this.introducerId) { + value.introducerId = this.introducerId; + } + return value; } } diff --git a/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.html index 096e08a7b..5b0970996 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.html @@ -6,229 +6,209 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
-
- - - -
Transaction & Product Limits
-
FROM PRODUCT
+
+
+
+ +
+
+

Digital & Account Features

+

Toggle the facilities available for this account (as per RBI guidelines)

+
+ FROM PRODUCT
-
-
-
Account Features (as per RBI guidelines)
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+
+
+
+ ATM Facility + {{ facilityConfig.atmFacility ? 'Enabled' : 'Disabled' }}
+
- @if (accountFacilityLimits.length > 0) { -
- - - - - - - - - - - - - - - - - @for (row of accountFacilityLimits; track row.limitType) { - - - - - - - } - -
Limit TypeGeneral Limit (Daily)Minor Account LimitSmall Saving Limit
{{ row.limitType }}
+
+
+
+ ATM Withdrawal + {{ facilityConfig.atmWithdrawalFacility ? 'Enabled' : 'Disabled' }}
- } + +
- @if (depositLimitKyc?.general) { -
-

Deposit Limit (KYC)

- - - - - - - - - - - - - - - - - - - - - - - - - -
Account TypeKYC TypeLimit
General - -
Minor - -
Small Saving - -
+
+
+
+ UPI Facility + {{ facilityConfig.upiFacility ? 'Enabled' : 'Disabled' }}
- } + +
-
- Note: - These facilities are defined at product level and applied to this account. Small Saving Account limits are - strictly capped at Rs.1,00,000 maximum balance per RBI Master Circular. +
+
+
+ POS Transactions + {{ facilityConfig.posTransactionsFacility ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ Cheque Book + {{ facilityConfig.chequeBook ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ IMPS Facility + {{ facilityConfig.impsFacility ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ SMS Alerts + {{ facilityConfig.smsAlerts ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ BBPS Facility + {{ facilityConfig.bbpsFacility ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ Insurance Cover + {{ facilityConfig.insuranceCover ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ BEPG Facility + {{ facilityConfig.bepgFacility ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ NEFT Facility + {{ facilityConfig.neftFacility ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ RTGS Facility + {{ facilityConfig.rtgsFacility ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ Inter-Bank Total Cap + {{ facilityConfig.interBankTotalCapFacility ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ Withdrawal Facility + {{ facilityConfig.withdrawalFacility ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ Deposit Facility (KYC) + {{ facilityConfig.depositFacilityKyc ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ Virtual Debit Card + {{ facilityConfig.virtualDebitCard ? 'Enabled' : 'Disabled' }} +
+
diff --git a/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.scss b/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.scss index 7b58a1abb..038aa7fc7 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.scss +++ b/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.scss @@ -6,194 +6,205 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -$border: #d9e2ec; -$border-strong: #c7d2de; -$text: #24364b; +$blue: #2563eb; +$blue-light: #eff6ff; +$green: #16a34a; +$green-light: #f0fdf4; +$green-border: #bbf7d0; +$border: #e2e8f0; +$text: #1e293b; $muted: #64748b; -$header-bg: #f8fafc; +$gray-bg: #f8fafc; -.spl-section { +// ── Wrapper ────────────────────────────────────────────── +.dig-wrapper { border: 1px solid $border; - border-radius: 12px; + border-radius: 14px; overflow: hidden; background: #fff; + box-shadow: 0 1px 4px rgb(0 0 0 / 6%); } -.spl-section__header { +// ── Header ─────────────────────────────────────────────── +.dig-header { display: flex; align-items: center; - gap: 12px; - padding: 16px 20px; - background: $header-bg; + gap: 14px; + padding: 18px 24px; + background: $gray-bg; border-bottom: 1px solid $border; -} -.spl-section__icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border-radius: 8px; - background: #eff6ff; - color: #2563eb; - font-size: 14px; - flex: 0 0 auto; -} - -.spl-section__title { - flex: 1 1 auto; - font-size: 16px; - font-weight: 600; - color: $text; -} - -.spl-section__badge { - flex: 0 0 auto; - padding: 5px 10px; - border-radius: 999px; - background: #dbeafe; - color: #1d4ed8; - font-size: 10px; - font-weight: 700; - letter-spacing: 0.05em; - white-space: nowrap; -} - -.spl-section__body { - padding: 18px 20px 20px; -} - -.spl-table-wrap { - overflow-x: auto; - margin-top: 16px; -} - -.spl-table { - width: 100%; - min-width: 0; - table-layout: fixed; - border-collapse: collapse; - border: 1px solid $border; - - col.spl-table__col--label { - width: 22%; + &__icon { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 10px; + background: $blue-light; + color: $blue; + font-size: 16px; + flex: 0 0 auto; } - col.spl-table__col--value { - width: 26%; + &__text { + flex: 1; } - th, - td { - border: 1px solid $border; - padding: 6px 8px; - vertical-align: middle; - } - - th { - background: #f8fafc; - color: #64748b; - text-align: center; - font-size: 11px; + &__title { + margin: 0 0 2px; + font-size: 15px; font-weight: 700; - } - - td:first-child { - background: #f8fafc; - font-weight: 600; color: $text; - white-space: nowrap; } - td { - font-size: 13px; - overflow: hidden; + &__sub { + margin: 0; + font-size: 12px; + color: $muted; + } + + &__badge { + flex: 0 0 auto; + padding: 4px 12px; + border-radius: 999px; + background: #dbeafe; + color: $blue; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.06em; } } -.spl-input, -.spl-select { - width: 100%; - box-sizing: border-box; - font: inherit; -} - -.spl-input { - height: 30px; - border: 1px solid $border-strong; - border-radius: 2px; - padding: 0 8px; - background: #f8fafc; - color: $muted; -} - -.spl-select { - height: 30px; - border: 1px solid $border-strong; - border-radius: 2px; - padding: 0 6px; - background: #f8fafc; - color: $muted; -} - -.spl-features { - margin-bottom: 16px; -} - -.spl-features__header { - font-size: 13px; - font-weight: 600; - color: $muted; - margin-bottom: 12px; - text-transform: uppercase; - letter-spacing: 0.04em; -} - -.spl-features__grid { +// ── Grid ───────────────────────────────────────────────── +.dig-grid { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px 16px; + grid-template-columns: repeat(3, 1fr); + gap: 0; - @media (width <= 900px) { - grid-template-columns: repeat(2, minmax(0, 1fr)); + @media (width <= 960px) { + grid-template-columns: repeat(2, 1fr); } - @media (width <= 640px) { + @media (width <= 580px) { grid-template-columns: 1fr; } } -.spl-feature { +// ── Card ───────────────────────────────────────────────── +.dig-card { display: flex; - flex-direction: column; - gap: 6px; - font-size: 13px; - color: $text; -} - -.spl-feature__label { - display: inline-flex; align-items: center; - gap: 8px; - font-weight: 600; - color: #334155; + gap: 12px; + padding: 14px 18px; + border-right: 1px solid $border; + border-bottom: 1px solid $border; + background: #fff; + transition: background 0.18s ease; + cursor: pointer; - i { - color: #2563eb; - font-size: 12px; - width: 14px; - text-align: center; + &:nth-child(3n) { + border-right: none; + } + + &--on { + background: $green-light; + + .dig-card__icon { + background: $green-border; + color: $green; + } + + .dig-card__status { + color: $green; + font-weight: 600; + } + } + + &__icon { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 9px; + background: $blue-light; + color: $blue; + font-size: 14px; + flex: 0 0 auto; + transition: + background 0.18s, + color 0.18s; + } + + &__body { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; + } + + &__label { + font-size: 13px; + font-weight: 600; + color: $text; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__status { + font-size: 11px; + color: $muted; + transition: color 0.18s; } } -.spl-note { - margin-top: 16px; - padding: 12px 14px; - border: 1px solid $border; - border-radius: 8px; - background: #f8fafc; - color: $muted; - font-size: 11px; - line-height: 1.6; +// ── Toggle Switch ───────────────────────────────────────── +.dig-toggle { + position: relative; + flex: 0 0 auto; + cursor: pointer; + + input { + position: absolute; + opacity: 0; + width: 0; + height: 0; + + &:checked + .dig-toggle__track { + background: $green; + border-color: $green; + } + + &:checked + .dig-toggle__track .dig-toggle__thumb { + transform: translateX(18px); + } + } + + &__track { + display: block; + width: 40px; + height: 22px; + border-radius: 999px; + background: #cbd5e1; + border: 1.5px solid #94a3b8; + position: relative; + transition: + background 0.2s, + border-color 0.2s; + } + + &__thumb { + position: absolute; + top: 2px; + left: 2px; + width: 16px; + height: 16px; + border-radius: 50%; + background: #fff; + box-shadow: 0 1px 3px rgb(0 0 0 / 20%); + transition: transform 0.2s ease; + } } diff --git a/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.ts index 9ffb1d278..321d9984d 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.ts +++ b/src/app/current-accounts/current-account-stepper/current-account-digital-step/current-account-digital-step.component.ts @@ -39,31 +39,16 @@ const DEFAULT_FACILITY_CONFIG = { ] }) export class CurrentAccountDigitalStepComponent { - private _currentAccountProductTemplate: any; + facilityConfig = { ...DEFAULT_FACILITY_CONFIG }; @Input() set currentAccountProductTemplate(value: any) { - this._currentAccountProductTemplate = value; - } - get currentAccountProductTemplate(): any { - return this._currentAccountProductTemplate; + if (value?.accountFacilityConfig) { + this.facilityConfig = { ...DEFAULT_FACILITY_CONFIG, ...value.accountFacilityConfig }; + } } - get accountFacilityConfig() { - return this._currentAccountProductTemplate?.accountFacilityConfig || DEFAULT_FACILITY_CONFIG; - } - - get accountFacilityLimits(): any[] { - return this._currentAccountProductTemplate?.accountFacilityLimits || []; - } - - get depositLimitKyc() { - return ( - this._currentAccountProductTemplate?.depositLimitKyc || { - general: { kycType: '', limit: '' }, - minor: { kycType: '', limit: '' }, - smallSaving: { kycType: '', limit: '' } - } - ); + get currentAccountDigital() { + return { accountFacilityConfig: { ...this.facilityConfig } }; } } diff --git a/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.html index 5cf37b020..3a893481d 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.html @@ -218,174 +218,6 @@
- -
-
- {{ 'labels.heading.Terms' | translate }} -
-
-
-
- Interest Posting Frequency - {{ - termPreview?.interestPostingPeriodType - | find: currentAccountProductTemplate?.interestPostingPeriodTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
-
- Auto Interest Post - - {{ termPreview?.autoPost | yesNo }} - -
- @if (termPreview?.frequencyFixDay !== undefined && termPreview?.frequencyFixDay !== null) { -
- Frequency Fix Day - {{ termPreview?.frequencyFixDay }} Days -
- } - @if (termPreview?.lastInterestDate) { -
- Last Interest Date - {{ termPreview?.lastInterestDate | dateFormat }} -
- } -
-
-
- - -
-
- {{ 'labels.heading.Settings' | translate }} -
-
-
- @if ( - settingsPreview?.minRequiredOpeningBalance !== undefined && - settingsPreview?.minRequiredOpeningBalance !== null && - settingsPreview?.minRequiredOpeningBalance !== '' - ) { -
- {{ 'labels.inputs.Minimum Opening Balance' | translate }} - {{ settingsPreview?.minRequiredOpeningBalance | formatNumber }} -
- } -
- {{ 'labels.inputs.Enforce Minimum Balance' | translate }} - - {{ settingsPreview?.enforceMinRequiredBalance | yesNo }} - -
- @if ( - settingsPreview?.minRequiredBalance !== undefined && - settingsPreview?.minRequiredBalance !== null && - settingsPreview?.minRequiredBalance !== '' - ) { -
- {{ 'labels.inputs.Minimum Balance' | translate }} - {{ settingsPreview?.minRequiredBalance | formatNumber }} -
- } -
- Balance Frequency - {{ settingsPreview?.balanceFrequency || 'Unassigned' }} -
-
- {{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }} - - {{ settingsPreview?.withdrawalFeeForTransfers | yesNo }} - -
-
- Enable Lock-in Period - - {{ settingsPreview?.enableLockinPeriod || !!settingsPreview?.lockinPeriodFrequency | yesNo }} - -
- @if (settingsPreview?.lockinPeriodFrequency) { -
- {{ 'labels.inputs.Lock-in Period' | translate }} - - {{ settingsPreview?.lockinPeriodFrequency }} - {{ - settingsPreview?.lockinPeriodFrequencyType - | find: currentAccountProductTemplate?.lockinPeriodFrequencyTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} - -
- } -
- Account Closed Zero Balance In Day Closing - - {{ settingsPreview?.accountClosedZeroBalanceInDayClosing | yesNo }} - -
-
- {{ 'labels.inputs.Is Overdraft Allowed' | translate }} - - {{ settingsPreview?.allowOverdraft | yesNo }} - -
- @if (settingsPreview?.allowOverdraft) { -
- Avg Balance Calc Period - {{ settingsPreview?.avgBalanceCalcPeriod || 'Unassigned' }} -
-
- Valid For Period - {{ settingsPreview?.validForPeriod || 'Unassigned' }} -
- @if ( - settingsPreview?.overdraftLimit !== undefined && - settingsPreview?.overdraftLimit !== null && - settingsPreview?.overdraftLimit !== '' - ) { -
- OD Limit % - {{ settingsPreview?.overdraftLimit | formatNumber }} -
- } - @if ( - settingsPreview?.nominalAnnualInterestRateOverdraft !== undefined && - settingsPreview?.nominalAnnualInterestRateOverdraft !== null && - settingsPreview?.nominalAnnualInterestRateOverdraft !== '' - ) { -
- Interest on OD % - {{ - settingsPreview?.nominalAnnualInterestRateOverdraft | formatNumber - }} -
- } -
- OD Account Closed Zero Balance In Day Closing - - {{ settingsPreview?.odAccountClosedZeroBalanceInDayClosing | yesNo }} - -
- } -
-
-
- @if (hasDigitalSection) {
@@ -405,111 +237,6 @@
}
- - @if (digitalPreview?.accountFacilityLimits?.length) { - - - - - - - - - - - @for (row of digitalPreview.accountFacilityLimits; track row.limitType) { - - - - - - - } - -
Limit TypeGeneral LimitMinor Account LimitSmall Saving Limit
{{ row.limitType }}{{ row.generalLimit || 'Unassigned' }}{{ row.minorAccountLimit || 'Unassigned' }}{{ row.smallSavingLimit || 'Unassigned' }}
- } - - @if (digitalPreview?.depositLimitKyc?.general) { - - - - - - - - - - - - - - - - - - - - - - - - - -
Account TypeKYC TypeLimit
General{{ digitalPreview.depositLimitKyc.general.kycType || 'Unassigned' }}{{ digitalPreview.depositLimitKyc.general.limit || 'Unassigned' }}
Minor{{ digitalPreview.depositLimitKyc.minor?.kycType || 'Unassigned' }}{{ digitalPreview.depositLimitKyc.minor?.limit || 'Unassigned' }}
Small Saving{{ digitalPreview.depositLimitKyc.smallSaving?.kycType || 'Unassigned' }}{{ digitalPreview.depositLimitKyc.smallSaving?.limit || 'Unassigned' }}
- } -
-
- } - - - @if (currentAccount?.charges?.length) { -
-
- {{ 'labels.inputs.Charges' | translate }} -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Name' | translate }}{{ charge.name + ', ' + charge.currency?.displaySymbol }}{{ 'labels.inputs.Type' | translate }} - {{ charge.chargeCalculationType?.value | translateKey: 'catalogs' }} - {{ 'labels.inputs.Amount' | translate }}{{ charge.amount | formatNumber }}{{ 'labels.inputs.Collected On' | translate }}{{ charge.chargeTimeType?.value }}{{ 'labels.inputs.Date' | translate }} - @if ( - charge.chargeTimeType?.value === 'Specified due date' || charge.chargeTimeType?.value === 'Weekly Fee' - ) { - {{ (charge.dueDate | dateFormat) || 'Unassigned' }} - } @else if ( - charge.chargeTimeType?.value === 'Monthly Fee' || charge.chargeTimeType?.value === 'Annual Fee' - ) { - {{ (charge.feeOnMonthDay | dateFormat) || 'Unassigned' }} - } @else { - N/A - } - {{ 'labels.inputs.Repayments Every' | translate }}{{ charge.feeInterval || 'Not Provided' }}
} diff --git a/src/app/current-accounts/current-account-stepper/current-account-term-step/current-account-term-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-term-step/current-account-term-step.component.html index 3dd3b12b0..b98fbacaf 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-term-step/current-account-term-step.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-term-step/current-account-term-step.component.html @@ -49,13 +49,7 @@ Last Interest Date - + diff --git a/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.html b/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.html index 889528426..7431eddc8 100644 --- a/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.html +++ b/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.html @@ -34,7 +34,8 @@ - + + {{ 'labels.inputs.DETAILS' | translate }} - - {{ 'labels.inputs.TERMS' | translate }} + + @if (recurringDepositsAccountProductTemplate) { + + Currency - - - + + - - {{ 'labels.inputs.SETTINGS' | translate }} +
+ + +
+
- - -
+ + + {{ 'labels.inputs.INTEREST RATE CHART' | translate }} - - {{ 'labels.inputs.INTEREST RATE CHART' | translate }} + + - - - +
+ + +
+
- - {{ 'labels.inputs.CHARGES' | translate }} - - - - - - @if (recurringDepositAccountFormValid) { + {{ 'labels.inputs.PREVIEW' | translate }} ({ - chargeId: charge.id, - amount: charge.amount, - dueDate: charge.dueDate && this.dateUtils.formatDate(charge.dueDate, dateFormat), - feeOnMonthDay: charge.feeOnMonthDay, - feeInterval: charge.feeInterval - })), - isCalendarInherited: this.recurringDepositAccount.recurringDepositAccount - ? this.recurringDepositAccount.recurringDepositAccount - : false, + charges: [], + isCalendarInherited: false, submittedOnDate: this.dateUtils.formatDate(this.recurringDepositAccount.submittedOnDate, dateFormat), expectedFirstDepositOnDate: this.dateUtils.formatDate( this.recurringDepositAccount.expectedFirstDepositOnDate, diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.html new file mode 100644 index 000000000..568ebc0c2 --- /dev/null +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.html @@ -0,0 +1,82 @@ + + +
+
+ + Currency & Account Status Configuration + From Product +
+
+
+ + Currency + + Inherited from selected product + + + + Decimal Places + + Inherited from selected product + +
+ +
+
+

Balance Swap Option

+

Enable automated transfer of excess funds to linked deposits or vice-versa to maintain liquidity.

+
+ +
+ +
+
+

MSME

+

Micro, Small and Medium Enterprises — mark this account under MSME category.

+
+ +
+ +
+
+

Auto Debit Facility

+

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

+
+ +
+ +
+
+

Dormancy Alert

+

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

+
+ + {{ recurringDepositsAccountProductTemplate?.dormancyAlert ? 'Yes' : 'No' }} + +
+ + + +
+
+

Nomination Facility

+

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

+
+ + {{ recurringDepositsAccountProductTemplate?.nominationFacility ? 'Yes' : 'No' }} + +
+ + @if (recurringDepositsAccountProductTemplate?.nominationFacility) { + + } +
+
diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.scss b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.scss new file mode 100644 index 000000000..3ed604577 --- /dev/null +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.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/. + */ + +$accent: #0ea5e9; +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-900: #202124; + +.racc-section { + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; + background: #fff; + + &__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; + } + } +} + +.racc-body { + background: #fff; +} + +.racc-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px 16px; + padding: 16px 24px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.racc-field { + width: 100%; +} + +.racc-alert { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + padding: 18px 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; + } + } +} + +.racc-yn { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 48px; + padding: 4px 14px; + border-radius: 999px; + font-size: 12px; + font-weight: 700; + background: #fee2e2; + color: #b91c1c; + border: 1px solid #fecaca; + + &--yes { + background: #dcfce7; + color: #15803d; + border-color: #bbf7d0; + } +} + +.racc-badge { + display: inline-flex; + align-items: center; + padding: 2px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; + + &--from { + background: #eff6ff; + color: #1d4ed8; + border: 1px solid #bfdbfe; + } +} + +.racc-alert--editable { + border-color: #bae6fd; + background: #f0f9ff; + + &:hover { + border-color: #7dd3fc; + box-shadow: 0 2px 8px rgb(2 132 199 / 10%); + } + + h4 { + color: #0369a1; + } +} diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.ts new file mode 100644 index 000000000..d793ae8fe --- /dev/null +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.ts @@ -0,0 +1,60 @@ +/** + * 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, OnChanges, SimpleChanges } from '@angular/core'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { NomineeFormComponent } from 'app/shared/nominee-form/nominee-form.component'; +import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dormancy-alert-panel.component'; +import { MatSlideToggle } from '@angular/material/slide-toggle'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'mifosx-recurring-deposits-account-currency-step', + templateUrl: './recurring-deposits-account-currency-step.component.html', + styleUrls: ['./recurring-deposits-account-currency-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + NomineeFormComponent, + DormancyAlertPanelComponent, + MatSlideToggle, + FormsModule + ] +}) +export class RecurringDepositsAccountCurrencyStepComponent implements OnChanges { + @Input() recurringDepositsAccountProductTemplate: any; + + nomineeData: any = null; + + // Editable feature toggles (pre-filled from product, overridable by user) + balanceSwapOption = false; + msme = false; + autoDebitFacility = false; + + ngOnChanges(changes: SimpleChanges): void { + if (changes['recurringDepositsAccountProductTemplate']?.currentValue) { + const pt = changes['recurringDepositsAccountProductTemplate'].currentValue; + this.balanceSwapOption = !!pt.balanceSwapOption; + this.msme = !!pt.msme; + this.autoDebitFacility = !!pt.autoDebitFacility; + } + } + + onNomineeChange(event: any): void { + this.nomineeData = event; + } + + /** Called by parent to collect currency step values */ + get currencyStepData() { + return { + balanceSwapOption: this.balanceSwapOption, + msme: this.msme, + autoDebitFacility: this.autoDebitFacility, + ...(this.nomineeData ? { nominee: this.nomineeData } : {}) + }; + } +} diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html index 194eeefec..1b01888e6 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html @@ -7,7 +7,7 @@ --> - +
@@ -64,7 +64,7 @@
@if (isProductSelected && productTemplate) { - +
@@ -115,101 +115,8 @@
- -
-
- -

Currency & Account Status Configuration

- From Product -
-
- @if (productTemplate.currency) { -
- - Currency - - Default: Indian Rupee - auto-set for domestic products - - - @if ( - productTemplate.currency.decimalPlaces !== undefined && productTemplate.currency.decimalPlaces !== null - ) { - - Decimal Places - - - } -
- } - -
-
-

Dormancy Alert

-

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

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

Balance Swap Option

-

Enable automated transfer of excess funds to linked deposits or vice-versa to maintain liquidity.

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

MSME

-

Micro, Small and Medium Enterprises

-
-
- 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 -
-
-
-
+ @if (productTemplate?.introducerRequired) { + + } } - -
- - -
diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.scss b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.scss index 45ea9fcab..cd6bfa982 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.scss +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.scss @@ -16,7 +16,7 @@ $gray-900: #202124; .rad-section { border: 1px solid $border; border-radius: 10px; - overflow: hidden; + overflow: visible; background: #fff; &--mt { @@ -183,3 +183,61 @@ $gray-900: #202124; } } } + +// ── Account Feature Toggles ──────────────────────────────────────────────── +.rad-toggles { + display: flex; + flex-direction: column; + gap: 10px; + padding: 4px 0; +} + +.rad-toggle-card { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 18px; + border: 1.5px solid #e2e8f0; + border-radius: 10px; + background: #fff; + transition: + border-color 0.2s, + box-shadow 0.2s; + + &:hover { + border-color: #93c5fd; + box-shadow: 0 1px 6px rgb(59 130 246 / 10%); + } + + &__left { + display: flex; + align-items: center; + gap: 12px; + + > i { + width: 36px; + height: 36px; + border-radius: 8px; + background: #f0f9ff; + border: 1px solid #bae6fd; + color: #0284c7; + display: flex; + align-items: center; + justify-content: center; + font-size: 15px; + flex: 0 0 auto; + } + } + + &__label { + font-size: 13.5px; + font-weight: 600; + color: #1e293b; + } + + &__hint { + font-size: 11.5px; + color: #64748b; + margin-top: 2px; + } +} diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts index a399f42da..8caea5ac7 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts @@ -18,6 +18,7 @@ import { MatTooltip } from '@angular/material/tooltip'; import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component'; /** * Recurring Deposits Account Details Step @@ -31,7 +32,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; MatTooltip, MatStepperPrevious, FaIconComponent, - MatStepperNext + MatStepperNext, + IntroducerSearchComponent ] }) export class RecurringDepositsAccountDetailsStepComponent implements OnInit { @@ -58,6 +60,7 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { recurringDepositAccountDetailsForm!: UntypedFormGroup; isProductSelected = false; + introducerId: number | null = null; /** Recurring Deposits Account Template with product data */ @Output() recurringDepositsAccountProductTemplate = new EventEmitter(); @@ -106,7 +109,10 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { Validators.required ], fieldOfficerId: [''], - externalId: [''] + externalId: [''], + balanceSwapOption: [false], + msme: [false], + autoDebitFacility: [false] }); } @@ -135,6 +141,12 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { this.recurringDepositsAccountProductTemplate.emit(response); this.fieldOfficerData = response.fieldOfficerOptions; this.isProductSelected = true; + // Pre-populate toggles from product defaults (user can override) + this.recurringDepositAccountDetailsForm.patchValue({ + balanceSwapOption: !!response.balanceSwapOption, + msme: !!response.msme, + autoDebitFacility: !!response.autoDebitFacility + }); this.cdr.detectChanges(); if (!this.isFieldOfficerPatched && this.recurringDepositsAccountTemplate.fieldOfficerId) { this.recurringDepositAccountDetailsForm @@ -184,7 +196,15 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { /** * Returns recurring deposits account details form value. */ + onIntroducerChange(event: { clientId: number; displayName: string } | null): void { + this.introducerId = event?.clientId ?? null; + } + get recurringDepositAccountDetails() { - return this.recurringDepositAccountDetailsForm.value; + const value = this.recurringDepositAccountDetailsForm.value; + if (this.introducerId) { + value.introducerId = this.introducerId; + } + return value; } } diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.html index c38bb2cc0..28268e478 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.html @@ -207,14 +207,3 @@
- -
- - -
diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html index 7b64f8833..e8058af74 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html @@ -249,316 +249,6 @@
- -
-
- {{ 'labels.heading.Terms' | translate }} -
-
-
-
- {{ 'labels.inputs.Nominal Annual Interest' | translate }} - - {{ recurringDepositsAccountProductTemplate?.nominalAnnualInterestRate ?? '—' }} - @if (recurringDepositsAccountProductTemplate?.nominalAnnualInterestRate != null) { - % - } - -
- - @if (recurringDepositsAccountProductTemplate?.effectiveDate) { -
- Effective Date - {{ recurringDepositsAccountProductTemplate?.effectiveDate | dateFormat }} -
- } - -
- {{ 'labels.inputs.Interest Compounding Period' | translate }} - {{ - recurringDepositAccountData?.interestCompoundingPeriodType - | find: recurringDepositsAccountProductTemplate?.interestCompoundingPeriodTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
- -
- {{ 'labels.inputs.Interest Posting Period' | translate }} - {{ - recurringDepositAccountData?.interestPostingPeriodType - | find: recurringDepositsAccountProductTemplate?.interestPostingPeriodTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
- - @if (recurringDepositsAccountProductTemplate?.averagePeriod) { -
- Average Period - {{ recurringDepositsAccountProductTemplate?.averagePeriod }} -
- } - -
- {{ 'labels.inputs.Interest Calculated using' | translate }} - {{ - recurringDepositAccountData?.interestCalculationType - | find: recurringDepositsAccountProductTemplate?.interestCalculationTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
- -
- {{ 'labels.inputs.Days in Year' | translate }} - {{ - recurringDepositAccountData?.interestCalculationDaysInYearType - | find: recurringDepositsAccountProductTemplate?.interestCalculationDaysInYearTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
- - @if ( - recurringDepositsAccountProductTemplate?.autoPost !== undefined && - recurringDepositsAccountProductTemplate?.autoPost !== null - ) { -
- Auto Interest Post - - {{ recurringDepositsAccountProductTemplate?.autoPost | yesNo }} - -
- } - - @if ( - recurringDepositsAccountProductTemplate?.frequencyFixDay !== undefined && - recurringDepositsAccountProductTemplate?.frequencyFixDay !== null - ) { -
- Frequency (Fix Day) - {{ recurringDepositsAccountProductTemplate?.frequencyFixDay }} -
- } - - @if (recurringDepositsAccountProductTemplate?.lastInterestDate) { -
- Last Interest Date - {{ - recurringDepositsAccountProductTemplate?.lastInterestDate | dateFormat - }} -
- } - - @if (recurringDepositsAccountProductTemplate?.interestCalculationTypeName) { -
- Interest Calculation Type - {{ - recurringDepositsAccountProductTemplate?.interestCalculationTypeName - }} -
- } -
-
-
- - -
-
- {{ 'labels.heading.Settings' | translate }} -
-
- -
-
Premature Option
-
-
- Loan Facility - - {{ recurringDepositAccountData?.loanFacility | yesNo }} - -
-
- Premature Withdrawal Option - - {{ recurringDepositAccountData?.prematureWithdrawalOption | yesNo }} - -
-
- Tenure Flexibility - - {{ recurringDepositAccountData?.tenureFlexibility | yesNo }} - -
-
- Lien Marked - - {{ recurringDepositAccountData?.lienMarked | yesNo }} - -
-
- Withdrawal Fee for Transfers - - {{ recurringDepositAccountData?.withdrawalFeeForTransfers | yesNo }} - -
-
-
- - - - -
-
{{ 'labels.inputs.Lock-in Period' | translate }}
-
-
- Enable Lock-in Period - - {{ recurringDepositAccountData?.enableLockinPeriod | yesNo }} - -
- @if (recurringDepositAccountData?.enableLockinPeriod) { -
- Lock-in Frequency - - {{ recurringDepositAccountData?.lockinPeriodFrequency }} - {{ - recurringDepositAccountData?.lockinPeriodFrequencyType - | find: recurringDepositsAccountProductTemplate?.lockinPeriodFrequencyTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} - -
- } -
-
- - - - -
-
Maturity Instructions
-
-
- Maturity Instructions - - {{ recurringDepositAccountData?.maturityInstructions | yesNo }} - -
-
- Credit Maturity Proceeds - - {{ recurringDepositAccountData?.creditMaturityProceeds | yesNo }} - -
-
- Penalty / Grace Period for Missed Installments - - {{ recurringDepositAccountData?.penaltyGracePeriodMissedInstallments | yesNo }} - -
-
- Panel Request Applicable - - {{ recurringDepositAccountData?.panelRequestApplicable | yesNo }} - -
-
- No Penalty for Nominee / Legal Heir Closure - - {{ recurringDepositAccountData?.noPenaltyNomineeHeirClosure | yesNo }} - -
-
- Deceased Settlement - - {{ recurringDepositAccountData?.deceasedSettlement | yesNo }} - -
-
- Allow Debit Txn after Due Date - - {{ recurringDepositAccountData?.allowDebitTxnAfterDueDate | yesNo }} - -
-
-
- - - - -
-
Additional Rules
-
-
- Account Closed with Zero Balance in Day Closing - - {{ recurringDepositAccountData?.accountClosedZeroBalanceInDayClosing | yesNo }} - -
- @if (recurringDepositAccountData?.minBalanceForInterestCalculation) { -
- Min. Balance for Interest Calculation - {{ - recurringDepositAccountData?.minBalanceForInterestCalculation | formatNumber - }} -
- } - @if (recurringDepositAccountData?.mandatoryRecommendedDepositAmount) { -
- {{ 'labels.inputs.Recurring Deposit Amount' | translate }} - {{ - recurringDepositAccountData?.mandatoryRecommendedDepositAmount | formatNumber - }} -
- } -
-
-
-
- @if (_activeRdVersion || recurringDepositsAccountProductTemplate?.accountChart) {
@@ -770,82 +460,6 @@
} - - - @if (recurringDepositAccountData?.charges?.length) { -
-
- {{ 'labels.heading.Charges' | translate }} -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.name' | translate }}{{ charge.name + ', ' + charge.currency?.displaySymbol }}{{ 'labels.inputs.Type' | translate }} - {{ charge.chargeCalculationType?.value | translateKey: 'catalogs' }} - {{ 'labels.inputs.Amount' | translate }}{{ charge.amount | formatNumber }}{{ 'labels.inputs.Collected On' | translate }}{{ charge.chargeTimeType?.value }}{{ 'labels.inputs.Date' | translate }} - - {{ (charge.dueDate | dateFormat) || 'Unassigned' }} - - - {{ (charge.feeOnMonthDay | dateFormat) || 'Unassigned' }} - - - {{ 'labels.inputs.N/A' | translate }} - - {{ 'labels.inputs.Repayments Every' | translate }}{{ charge.feeInterval || 'Not Provided' }}
-
-
- }
diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.ts b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.ts index 7beb38296..25e2a9c41 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.ts +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.ts @@ -51,6 +51,10 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit { this.recurringDepositProductsTemplate.currency?.decimalPlaces ?? 2, dormancyAlert: this.recurringDepositProductsTemplate.dormancyAlert ?? false, + isDormancyTrackingActive: this.recurringDepositProductsTemplate.isDormancyTrackingActive ?? false, + daysToInactive: this.recurringDepositProductsTemplate.daysToInactive ?? 30, + daysToDormancy: this.recurringDepositProductsTemplate.daysToDormancy ?? 60, + daysToEscheat: this.recurringDepositProductsTemplate.daysToEscheat ?? 90, balanceSwapOption: this.recurringDepositProductsTemplate.balanceSwapOption ?? false, msme: this.recurringDepositProductsTemplate.msme ?? false, nominationFacility: this.recurringDepositProductsTemplate.nominationFacility ?? false, @@ -72,6 +76,10 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit { ] ], dormancyAlert: [false], + isDormancyTrackingActive: [false], + daysToInactive: [30], + daysToDormancy: [60], + daysToEscheat: [90], balanceSwapOption: [false], msme: [false], nominationFacility: [false], @@ -81,7 +89,7 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit { get recurringDepositProductCurrency() { const formValue = this.recurringDepositProductCurrencyForm.value; - return { + const result: any = { currencyCode: formValue.currencyCode, digitsAfterDecimal: formValue.digitsAfterDecimal, dormancyAlert: formValue.dormancyAlert, @@ -90,5 +98,11 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit { 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/savings/create-savings-account/create-savings-account.component.html b/src/app/savings/create-savings-account/create-savings-account.component.html index ffe983360..23f9f93aa 100644 --- a/src/app/savings/create-savings-account/create-savings-account.component.html +++ b/src/app/savings/create-savings-account/create-savings-account.component.html @@ -35,7 +35,7 @@ - + Product Classification - - - Currency + @if (savingsAccountProductTemplate) { + + + Currency - - + + -
- - -
-
+
+ + +
+
- - - Term + + + Digital - - + + -
- - -
-
+
+ + +
+
- - - {{ 'labels.inputs.SETTINGS' | translate }} - - - - -
- - -
-
- - - - Digital - - - - -
- - -
-
- - - - {{ 'labels.inputs.CHARGES' | translate }} - - - - - - - @if (savingsAccountFormValid) { + {{ 'labels.inputs.PREVIEW' | translate }} { this.savingsAccountTemplate = data.savingsAccountTemplate; @@ -88,28 +83,14 @@ export class CreateSavingsAccountComponent { return this.savingsAccountDetailsStep.savingsAccountDetailsForm; } - get savingsAccountTermsForm() { - return this.savingsAccountTermStep.savingsAccountTermForm; - } - - get savingsAccountSettingsForm() { - return this.savingsAccountSettingsStep.savingsAccountSettingsForm; - } - - get savingsAccountFormValid() { - return ( - this.savingsAccountDetailsForm.valid && - this.savingsAccountTermsForm.valid && - this.savingsAccountSettingsForm.valid - ); + /** Stub — no term step in this flow */ + get savingsAccountTermsForm(): FormGroup { + return new FormGroup({}); } get savingsAccount() { return { - ...this.savingsAccountDetailsStep.savingsAccountDetails, - ...this.savingsAccountTermStep.savingsAccountTerms, - ...this.savingsAccountSettingsStep.savingsAccountSettings, - ...this.savingsAccountChargesStep.savingsAccountCharges + ...this.savingsAccountDetailsStep.savingsAccountDetails }; } @@ -119,15 +100,7 @@ export class CreateSavingsAccountComponent { const monthDayFormat = 'dd MMMM'; const savingsAccount = { ...this.savingsAccount, - charges: this.savingsAccount.charges.map((charge: any) => ({ - chargeId: charge.id, - amount: charge.amount, - dueDate: charge.dueDate ? this.dateUtils.formatDate(charge.dueDate, dateFormat) : charge.dueDate, - feeOnMonthDay: charge.feeOnMonthDay - ? this.dateUtils.formatDate(charge.feeOnMonthDay, monthDayFormat) - : charge.feeOnMonthDay, - feeInterval: charge.feeInterval - })), + charges: [], submittedOnDate: this.dateUtils.formatDate(this.savingsAccount.submittedOnDate, dateFormat), dateFormat, monthDayFormat, diff --git a/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.html b/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.html index 10c787094..9dd007828 100644 --- a/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.html +++ b/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.html @@ -36,6 +36,8 @@
+ +

Nomination Facility

@@ -45,5 +47,9 @@ {{ savingsAccountProductTemplate?.nominationFacility ? 'Yes' : 'No' }}
+ + @if (savingsAccountProductTemplate?.nominationFacility) { + + }
diff --git a/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.ts index 553256ce0..31e029241 100644 --- a/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.ts +++ b/src/app/savings/savings-account-stepper/savings-account-currency-step/savings-account-currency-step.component.ts @@ -8,13 +8,25 @@ import { Component, Input } from '@angular/core'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { NomineeFormComponent } from 'app/shared/nominee-form/nominee-form.component'; +import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dormancy-alert-panel.component'; @Component({ selector: 'mifosx-savings-account-currency-step', templateUrl: './savings-account-currency-step.component.html', styleUrls: ['./savings-account-currency-step.component.scss'], - imports: [...STANDALONE_SHARED_IMPORTS] + imports: [ + ...STANDALONE_SHARED_IMPORTS, + NomineeFormComponent, + DormancyAlertPanelComponent + ] }) export class SavingsAccountCurrencyStepComponent { @Input() savingsAccountProductTemplate: any; + + nomineeData: any = null; + + onNomineeChange(event: any): void { + this.nomineeData = event; + } } diff --git a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html index df5db2234..60304a70d 100644 --- a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html +++ b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html @@ -67,7 +67,7 @@ - + @if (savingsProductSelected) {
@@ -118,16 +118,9 @@
- } -
- - -
+ @if (productTemplate?.introducerRequired) { + + } + } diff --git a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.scss b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.scss index 7c9847ae8..5744c0823 100644 --- a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.scss +++ b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.scss @@ -15,7 +15,7 @@ $gray-900: #202124; .sad-section { border: 1px solid $border; border-radius: 10px; - overflow: hidden; + overflow: visible; background: #fff; &--mt { diff --git a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts index e30469624..8585f3dac 100644 --- a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts +++ b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts @@ -17,6 +17,7 @@ import { SavingsService } from 'app/savings/savings.service'; import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component'; /** * Savings Account Details Step @@ -29,7 +30,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; ...STANDALONE_SHARED_IMPORTS, MatStepperPrevious, FaIconComponent, - MatStepperNext + MatStepperNext, + IntroducerSearchComponent ] }) export class SavingsAccountDetailsStepComponent implements OnInit { @@ -56,6 +58,7 @@ export class SavingsAccountDetailsStepComponent implements OnInit { savingsAccountDetailsForm: UntypedFormGroup; savingsProductSelected = false; + introducerId: number | null = null; /** Savings Account Template with product data */ @Output() savingsAccountProductTemplate = new EventEmitter(); @@ -165,7 +168,15 @@ export class SavingsAccountDetailsStepComponent implements OnInit { /** * Returns savings account form value. */ + onIntroducerChange(event: { clientId: number; displayName: string } | null): void { + this.introducerId = event?.clientId ?? null; + } + get savingsAccountDetails() { - return this.savingsAccountDetailsForm.getRawValue(); + const value = this.savingsAccountDetailsForm.getRawValue(); + if (this.introducerId) { + value.introducerId = this.introducerId; + } + return value; } } diff --git a/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.html b/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.html index c8bddd8d6..5b0970996 100644 --- a/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.html +++ b/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.html @@ -6,229 +6,209 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
-
- - - -
Transaction & Product Limits
-
FROM PRODUCT
+
+
+
+ +
+
+

Digital & Account Features

+

Toggle the facilities available for this account (as per RBI guidelines)

+
+ FROM PRODUCT
-
-
-
Account Features (as per RBI guidelines)
- -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - +
+
+
+
+ ATM Facility + {{ facilityConfig.atmFacility ? 'Enabled' : 'Disabled' }}
+
- @if (accountFacilityLimits.length > 0) { -
- - - - - - - - - - - - - - - - - @for (row of accountFacilityLimits; track row.limitType) { - - - - - - - } - -
Limit TypeGeneral Limit (Daily)Minor Account LimitSmall Saving Limit
{{ row.limitType }}
+
+
+
+ ATM Withdrawal + {{ facilityConfig.atmWithdrawalFacility ? 'Enabled' : 'Disabled' }}
- } + +
- @if (depositLimitKyc?.general) { -
-

Deposit Limit (KYC)

- - - - - - - - - - - - - - - - - - - - - - - - - -
Account TypeKYC TypeLimit
General - -
Minor - -
Small Saving - -
+
+
+
+ UPI Facility + {{ facilityConfig.upiFacility ? 'Enabled' : 'Disabled' }}
- } + +
-
- Note: - These facilities are defined at product level and applied to this account. Small Saving Account limits are - strictly capped at Rs.1,00,000 maximum balance per RBI Master Circular. +
+
+
+ POS Transactions + {{ facilityConfig.posTransactionsFacility ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ Cheque Book + {{ facilityConfig.chequeBook ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ IMPS Facility + {{ facilityConfig.impsFacility ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ SMS Alerts + {{ facilityConfig.smsAlerts ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ BBPS Facility + {{ facilityConfig.bbpsFacility ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ Insurance Cover + {{ facilityConfig.insuranceCover ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ BEPG Facility + {{ facilityConfig.bepgFacility ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ NEFT Facility + {{ facilityConfig.neftFacility ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ RTGS Facility + {{ facilityConfig.rtgsFacility ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ Inter-Bank Total Cap + {{ facilityConfig.interBankTotalCapFacility ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ Withdrawal Facility + {{ facilityConfig.withdrawalFacility ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ Deposit Facility (KYC) + {{ facilityConfig.depositFacilityKyc ? 'Enabled' : 'Disabled' }} +
+ +
+ +
+
+
+ Virtual Debit Card + {{ facilityConfig.virtualDebitCard ? 'Enabled' : 'Disabled' }} +
+
diff --git a/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.scss b/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.scss index 7b58a1abb..038aa7fc7 100644 --- a/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.scss +++ b/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.scss @@ -6,194 +6,205 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -$border: #d9e2ec; -$border-strong: #c7d2de; -$text: #24364b; +$blue: #2563eb; +$blue-light: #eff6ff; +$green: #16a34a; +$green-light: #f0fdf4; +$green-border: #bbf7d0; +$border: #e2e8f0; +$text: #1e293b; $muted: #64748b; -$header-bg: #f8fafc; +$gray-bg: #f8fafc; -.spl-section { +// ── Wrapper ────────────────────────────────────────────── +.dig-wrapper { border: 1px solid $border; - border-radius: 12px; + border-radius: 14px; overflow: hidden; background: #fff; + box-shadow: 0 1px 4px rgb(0 0 0 / 6%); } -.spl-section__header { +// ── Header ─────────────────────────────────────────────── +.dig-header { display: flex; align-items: center; - gap: 12px; - padding: 16px 20px; - background: $header-bg; + gap: 14px; + padding: 18px 24px; + background: $gray-bg; border-bottom: 1px solid $border; -} -.spl-section__icon { - display: inline-flex; - align-items: center; - justify-content: center; - width: 28px; - height: 28px; - border-radius: 8px; - background: #eff6ff; - color: #2563eb; - font-size: 14px; - flex: 0 0 auto; -} - -.spl-section__title { - flex: 1 1 auto; - font-size: 16px; - font-weight: 600; - color: $text; -} - -.spl-section__badge { - flex: 0 0 auto; - padding: 5px 10px; - border-radius: 999px; - background: #dbeafe; - color: #1d4ed8; - font-size: 10px; - font-weight: 700; - letter-spacing: 0.05em; - white-space: nowrap; -} - -.spl-section__body { - padding: 18px 20px 20px; -} - -.spl-table-wrap { - overflow-x: auto; - margin-top: 16px; -} - -.spl-table { - width: 100%; - min-width: 0; - table-layout: fixed; - border-collapse: collapse; - border: 1px solid $border; - - col.spl-table__col--label { - width: 22%; + &__icon { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 10px; + background: $blue-light; + color: $blue; + font-size: 16px; + flex: 0 0 auto; } - col.spl-table__col--value { - width: 26%; + &__text { + flex: 1; } - th, - td { - border: 1px solid $border; - padding: 6px 8px; - vertical-align: middle; - } - - th { - background: #f8fafc; - color: #64748b; - text-align: center; - font-size: 11px; + &__title { + margin: 0 0 2px; + font-size: 15px; font-weight: 700; - } - - td:first-child { - background: #f8fafc; - font-weight: 600; color: $text; - white-space: nowrap; } - td { - font-size: 13px; - overflow: hidden; + &__sub { + margin: 0; + font-size: 12px; + color: $muted; + } + + &__badge { + flex: 0 0 auto; + padding: 4px 12px; + border-radius: 999px; + background: #dbeafe; + color: $blue; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.06em; } } -.spl-input, -.spl-select { - width: 100%; - box-sizing: border-box; - font: inherit; -} - -.spl-input { - height: 30px; - border: 1px solid $border-strong; - border-radius: 2px; - padding: 0 8px; - background: #f8fafc; - color: $muted; -} - -.spl-select { - height: 30px; - border: 1px solid $border-strong; - border-radius: 2px; - padding: 0 6px; - background: #f8fafc; - color: $muted; -} - -.spl-features { - margin-bottom: 16px; -} - -.spl-features__header { - font-size: 13px; - font-weight: 600; - color: $muted; - margin-bottom: 12px; - text-transform: uppercase; - letter-spacing: 0.04em; -} - -.spl-features__grid { +// ── Grid ───────────────────────────────────────────────── +.dig-grid { display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 12px 16px; + grid-template-columns: repeat(3, 1fr); + gap: 0; - @media (width <= 900px) { - grid-template-columns: repeat(2, minmax(0, 1fr)); + @media (width <= 960px) { + grid-template-columns: repeat(2, 1fr); } - @media (width <= 640px) { + @media (width <= 580px) { grid-template-columns: 1fr; } } -.spl-feature { +// ── Card ───────────────────────────────────────────────── +.dig-card { display: flex; - flex-direction: column; - gap: 6px; - font-size: 13px; - color: $text; -} - -.spl-feature__label { - display: inline-flex; align-items: center; - gap: 8px; - font-weight: 600; - color: #334155; + gap: 12px; + padding: 14px 18px; + border-right: 1px solid $border; + border-bottom: 1px solid $border; + background: #fff; + transition: background 0.18s ease; + cursor: pointer; - i { - color: #2563eb; - font-size: 12px; - width: 14px; - text-align: center; + &:nth-child(3n) { + border-right: none; + } + + &--on { + background: $green-light; + + .dig-card__icon { + background: $green-border; + color: $green; + } + + .dig-card__status { + color: $green; + font-weight: 600; + } + } + + &__icon { + display: flex; + align-items: center; + justify-content: center; + width: 36px; + height: 36px; + border-radius: 9px; + background: $blue-light; + color: $blue; + font-size: 14px; + flex: 0 0 auto; + transition: + background 0.18s, + color 0.18s; + } + + &__body { + flex: 1; + min-width: 0; + display: flex; + flex-direction: column; + gap: 2px; + } + + &__label { + font-size: 13px; + font-weight: 600; + color: $text; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + &__status { + font-size: 11px; + color: $muted; + transition: color 0.18s; } } -.spl-note { - margin-top: 16px; - padding: 12px 14px; - border: 1px solid $border; - border-radius: 8px; - background: #f8fafc; - color: $muted; - font-size: 11px; - line-height: 1.6; +// ── Toggle Switch ───────────────────────────────────────── +.dig-toggle { + position: relative; + flex: 0 0 auto; + cursor: pointer; + + input { + position: absolute; + opacity: 0; + width: 0; + height: 0; + + &:checked + .dig-toggle__track { + background: $green; + border-color: $green; + } + + &:checked + .dig-toggle__track .dig-toggle__thumb { + transform: translateX(18px); + } + } + + &__track { + display: block; + width: 40px; + height: 22px; + border-radius: 999px; + background: #cbd5e1; + border: 1.5px solid #94a3b8; + position: relative; + transition: + background 0.2s, + border-color 0.2s; + } + + &__thumb { + position: absolute; + top: 2px; + left: 2px; + width: 16px; + height: 16px; + border-radius: 50%; + background: #fff; + box-shadow: 0 1px 3px rgb(0 0 0 / 20%); + transition: transform 0.2s ease; + } } diff --git a/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.ts index ae9f82d8e..5c747b548 100644 --- a/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.ts +++ b/src/app/savings/savings-account-stepper/savings-account-digital-step/savings-account-digital-step.component.ts @@ -39,31 +39,16 @@ const DEFAULT_FACILITY_CONFIG = { ] }) export class SavingsAccountDigitalStepComponent { - private _savingsAccountProductTemplate: any; + facilityConfig = { ...DEFAULT_FACILITY_CONFIG }; @Input() set savingsAccountProductTemplate(value: any) { - this._savingsAccountProductTemplate = value; - } - get savingsAccountProductTemplate(): any { - return this._savingsAccountProductTemplate; + if (value?.accountFacilityConfig) { + this.facilityConfig = { ...DEFAULT_FACILITY_CONFIG, ...value.accountFacilityConfig }; + } } - get accountFacilityConfig() { - return this._savingsAccountProductTemplate?.accountFacilityConfig || DEFAULT_FACILITY_CONFIG; - } - - get accountFacilityLimits(): any[] { - return this._savingsAccountProductTemplate?.accountFacilityLimits || []; - } - - get depositLimitKyc() { - return ( - this._savingsAccountProductTemplate?.depositLimitKyc || { - general: { kycType: '', limit: '' }, - minor: { kycType: '', limit: '' }, - smallSaving: { kycType: '', limit: '' } - } - ); + get savingsAccountDigital() { + return { accountFacilityConfig: { ...this.facilityConfig } }; } } diff --git a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.html b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.html index 0a595f67f..70337bb64 100644 --- a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.html +++ b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.html @@ -213,198 +213,6 @@
-
-
- {{ 'labels.heading.Terms' | translate }} -
-
-
-
- {{ 'labels.inputs.Nominal Annual Interest' | translate }} - {{ savingsAccount?.nominalAnnualInterestRate | formatNumber }} % -
- @if (savingsAccountProductTemplate?.interestCalculationTypeName) { -
- Interest Calculation Mode - {{ savingsAccountProductTemplate?.interestCalculationTypeName }} -
- } -
- {{ 'labels.inputs.Interest Compounding Period' | translate }} - {{ - savingsAccount?.interestCompoundingPeriodType - | find: savingsAccountProductTemplate?.interestCompoundingPeriodTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
-
- {{ 'labels.inputs.Interest Posting Period' | translate }} - {{ - savingsAccount?.interestPostingPeriodType - | find: savingsAccountProductTemplate?.interestPostingPeriodTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
-
- {{ 'labels.inputs.Interest Calculated using' | translate }} - {{ - savingsAccount?.interestCalculationType - | find: savingsAccountProductTemplate?.interestCalculationTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
-
- {{ 'labels.inputs.Days in Year' | translate }} - {{ - savingsAccount?.interestCalculationDaysInYearType - | find: savingsAccountProductTemplate?.interestCalculationDaysInYearTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
- -
- Interest Calculation Selector - {{ - savingsAccount?.interestCalculateSelector - | find: savingsAccountProductTemplate?.interestCalculationTypeOption : 'id' : 'value' - | translateKey: 'catalogs' - }} -
- @if (savingsAccount?.averagePeriod) { -
- Average Period - {{ savingsAccount?.averagePeriod }} -
- } -
- Auto Post - - {{ savingsAccount?.autoPost | yesNo }} - -
- @if (savingsAccountProductTemplate?.lastInterestDate) { -
- Last Interest Date - {{ savingsAccountProductTemplate?.lastInterestDate | dateFormat }} -
- } - @if (savingsAccount?.frequencyFixDay !== undefined && savingsAccount?.frequencyFixDay !== null) { -
- Frequency Fix Day - {{ savingsAccount?.frequencyFixDay }} -
- } -
-
-
- -
-
- {{ 'labels.heading.Settings' | translate }} -
-
-
- @if (savingsAccount?.minRequiredOpeningBalance) { -
- {{ 'labels.inputs.Minimum Opening Balance' | translate }} - {{ savingsAccount?.minRequiredOpeningBalance | formatNumber }} -
- } -
- {{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }} - - {{ savingsAccount?.withdrawalFeeForTransfers | yesNo }} - -
- @if (savingsAccount?.lockinPeriodFrequency) { -
- {{ 'labels.inputs.Lock-in Period' | translate }} - - {{ savingsAccount?.lockinPeriodFrequency }} - {{ - savingsAccount?.lockinPeriodFrequencyType - | find: savingsAccountProductTemplate?.lockinPeriodFrequencyTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} - -
- } -
- {{ 'labels.inputs.Is Overdraft Allowed' | translate }} - - {{ savingsAccount?.allowOverdraft | yesNo }} - -
- @if (savingsAccount?.minOverdraftForInterestCalculation) { -
- {{ - 'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate - }} - {{ savingsAccount?.minOverdraftForInterestCalculation | formatNumber }} -
- } - @if (savingsAccount?.nominalAnnualInterestRateOverdraft) { -
- {{ 'labels.inputs.Nominal Annual Interest for Overdraft' | translate }} - {{ savingsAccount?.nominalAnnualInterestRateOverdraft | formatNumber }} % -
- } - @if (savingsAccount?.overdraftLimit) { -
- {{ 'labels.inputs.Maximum Overdraft Amount Limit' | translate }} - {{ savingsAccount?.overdraftLimit | formatNumber }} -
- } -
- {{ 'labels.inputs.Enforce Minimum Balance' | translate }} - - {{ savingsAccount?.enforceMinRequiredBalance | yesNo }} - -
- @if (savingsAccountProductTemplate?.enableLockinPeriod !== undefined) { -
- Enable Lock-in Period - - {{ savingsAccountProductTemplate?.enableLockinPeriod | yesNo }} - -
- } - @if (savingsAccountProductTemplate?.balanceFrequency) { -
- Balance Frequency - {{ savingsAccountProductTemplate?.balanceFrequency }} -
- } - @if (savingsAccountProductTemplate?.accountClosedZeroBalanceInDayClosing !== undefined) { -
- Account Closed Zero Balance In Day Closing - - {{ savingsAccountProductTemplate?.accountClosedZeroBalanceInDayClosing | yesNo }} - -
- } - @if (savingsAccount?.minRequiredBalance) { -
- {{ 'labels.inputs.Minimum Balance' | translate }} - {{ savingsAccount?.minRequiredBalance }} -
- } -
-
-
- @if (hasDigitalSection) {
@@ -424,58 +232,6 @@
}
- - @if (digitalPreview?.accountFacilityLimits?.length) { - - - - - - - - - - - @for (row of digitalPreview.accountFacilityLimits; track row.limitType) { - - - - - - - } - -
Limit TypeGeneral LimitMinor Account LimitSmall Saving Limit
{{ row.limitType }}{{ row.generalLimit || 'Unassigned' }}{{ row.minorAccountLimit || 'Unassigned' }}{{ row.smallSavingLimit || 'Unassigned' }}
- } - - @if (digitalPreview?.depositLimitKyc?.general) { - - - - - - - - - - - - - - - - - - - - - - - - - -
Account TypeKYC TypeLimit
General{{ digitalPreview.depositLimitKyc.general.kycType || 'Unassigned' }}{{ digitalPreview.depositLimitKyc.general.limit || 'Unassigned' }}
Minor{{ digitalPreview.depositLimitKyc.minor?.kycType || 'Unassigned' }}{{ digitalPreview.depositLimitKyc.minor?.limit || 'Unassigned' }}
Small Saving{{ digitalPreview.depositLimitKyc.smallSaving?.kycType || 'Unassigned' }}{{ digitalPreview.depositLimitKyc.smallSaving?.limit || 'Unassigned' }}
- }
} @@ -501,65 +257,6 @@
} - - @if (savingsAccount?.charges?.length) { -
-
- {{ 'labels.inputs.Charges' | translate }} -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Name' | translate }}{{ charge.name + ', ' + charge.currency.displaySymbol }}{{ 'labels.inputs.Type' | translate }} - {{ charge.chargeCalculationType.value | translateKey: 'catalogs' }} - {{ 'labels.inputs.Amount' | translate }}{{ charge.amount | formatNumber }}{{ 'labels.inputs.Date' | translate }} - @if ( - charge.chargeTimeType.value === 'Specified due date' || charge.chargeTimeType.value === 'Weekly Fee' - ) { - {{ (charge.dueDate | dateFormat) || 'Unassigned' }} - } - @if (charge.chargeTimeType.value === 'Monthly Fee' || charge.chargeTimeType.value === 'Annual Fee') { - {{ (charge.feeOnMonthDay | dateFormat) || 'Unassigned' }} - } - @if ( - !( - charge.chargeTimeType.value === 'Monthly Fee' || - charge.chargeTimeType.value === 'Annual Fee' || - charge.chargeTimeType.value === 'Specified due date' || - charge.chargeTimeType.value === 'Weekly Fee' - ) - ) { - N/A - } - {{ 'labels.inputs.Repayments Every' | translate }}{{ charge.feeInterval || 'Not Provided' }}{{ 'labels.inputs.Collected On' | translate }}{{ charge.chargeTimeType.value }}
-
-
- }
diff --git a/src/app/savings/savings-account-stepper/savings-account-term-step/savings-account-term-step.component.html b/src/app/savings/savings-account-stepper/savings-account-term-step/savings-account-term-step.component.html index a90dfeba3..b7592f93b 100644 --- a/src/app/savings/savings-account-stepper/savings-account-term-step/savings-account-term-step.component.html +++ b/src/app/savings/savings-account-stepper/savings-account-term-step/savings-account-term-step.component.html @@ -144,13 +144,7 @@ Last Interest Date - + diff --git a/src/app/shared/dormancy-alert-panel/dormancy-alert-panel.component.html b/src/app/shared/dormancy-alert-panel/dormancy-alert-panel.component.html new file mode 100644 index 000000000..5aba5ae30 --- /dev/null +++ b/src/app/shared/dormancy-alert-panel/dormancy-alert-panel.component.html @@ -0,0 +1,308 @@ + + +@if (shouldShow) { +
+ +
+
+
+ +
+
+
+ {{ isProductMode ? 'Dormancy Monitoring Configuration' : 'Dormancy Risk Monitor' }} +
+
+ @if (isProductMode) { + Product-level dormancy thresholds applied to this account + } @else { + Real-time dormancy tracking · Based on last customer transaction + } +
+
+
+ + +
+ + + @if (isProductMode) { + Monitoring: {{ riskSeverity }} + } @else { + Dormancy Risk: {{ riskSeverity }} + } + + @if (!isProductMode && isMonitoringActive) { + Monitoring Active + } +
+
+ + +
+ @if (isProductMode) { + + + +
+ Account Status + New Account + No transactions yet +
+ + +
+ + Dormancy Monitoring + + + + + {{ isMonitoringActive ? 'Enabled' : 'Disabled' }} + + {{ + isMonitoringActive ? 'Transaction activity tracked' : 'Not configured' + }} +
+ + +
+ + Alert Notifications + + + + + {{ isAlertEnabled ? 'Enabled' : 'Disabled' }} + + {{ isAlertEnabled ? 'Branch will be notified' : 'No alerts configured' }} +
+ + +
+ + Inactive Threshold + + + @if (daysToInactive !== null) { + {{ daysToInactive }}days + No transaction for {{ daysToInactive }} days + } @else { + Not configured + } +
+ + +
+ + Dormancy Threshold + + + @if (daysToDormancy !== null) { + {{ daysToDormancy }}days + {{ daysToDormancy }} days after inactive + } @else { + Not configured + } +
+ + +
+ + Escheat Threshold + + + @if (daysToEscheat !== null) { + {{ daysToEscheat }}days + {{ daysToEscheat }} days after dormant + } @else { + Not configured + } +
+ } @else { + + + +
+ Account Status + + {{ statusLabel }} + + + @if (statusId === 0) { + Currently active + } + @if (statusId === 100) { + No recent transactions + } + @if (statusId === 200) { + Classified as dormant + } + @if (statusId === 300) { + Pending regulatory transfer + } + +
+ + +
+ Last Transaction + @if (lastTxnDate) { + {{ lastTxnDate }} + Most recent customer activity + } @else if (accountOpenedDate) { + {{ accountOpenedDate }} + Account opened · No transactions yet + } @else { + No transactions yet + } +
+ + +
+ + Days to Inactive + + + @if (statusId >= 100) { + Threshold Reached + Account is inactive + } @else if (daysToInactive !== null) { + + {{ daysToInactive }}days + + {{ inactiveSubtext() }} + } @else { + — + } +
+ + +
+ + Days to Dormant + + + @if (statusId >= 200) { + Threshold Reached + Account is dormant + } @else if (daysToDormancy !== null) { + + {{ daysToDormancy }}days + + {{ dormancySubtext() }} + } @else { + — + } +
+ + +
+ + Days to Escheat + + + @if (statusId >= 300) { + Threshold Reached + Pending regulatory transfer + } @else if (daysToEscheat !== null) { + + {{ daysToEscheat }}days + + {{ escheatSubtext() }} + } @else { + — + } +
+ } +
+ + +
+
+ Dormancy Progression + @if (!isProductMode) { + Timeline based on last customer transaction + } +
+ +
+ +
+
+ +
+ Active + Normal +
+ + @for (step of timelineSteps; track step.label) { +
+
+
+ @if (step.reached && !step.current) { + + } @else if (step.current) { + + } @else { + + } +
+ + {{ step.label }} + + + @if (!step.reached && step.days !== null) { + {{ step.days }}d + } @else if (step.reached) { + Reached + } +
+ } +
+
+
+} diff --git a/src/app/shared/dormancy-alert-panel/dormancy-alert-panel.component.scss b/src/app/shared/dormancy-alert-panel/dormancy-alert-panel.component.scss new file mode 100644 index 000000000..4cb26e9bc --- /dev/null +++ b/src/app/shared/dormancy-alert-panel/dormancy-alert-panel.component.scss @@ -0,0 +1,684 @@ +/** + * 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 'sass:color'; + +// ── Design Tokens ────────────────────────────────────────────────────────── +// Risk colours +$c-safe: #16a34a; +$bg-safe: #f0fdf4; +$bd-safe: #bbf7d0; +$c-low: #0284c7; +$bg-low: #f0f9ff; +$bd-low: #bae6fd; +$c-medium: #d97706; +$bg-medium: #fffbeb; +$bd-medium: #fde68a; +$c-high: #dc2626; +$bg-high: #fff5f5; +$bd-high: #fecaca; +$c-critical: #b91c1c; +$bg-critical: #fef2f2; +$bd-critical: #fca5a5; +$c-escheat: #7c3aed; +$bg-escheat: #f5f3ff; +$bd-escheat: #ddd6fe; + +// Neutral +$text: #1e293b; +$muted: #64748b; +$subtle: #94a3b8; +$border: #e2e8f0; +$surface: #f8fafc; + +// ── Host ─────────────────────────────────────────────────────────────────── +:host { + display: block; + margin: 14px 0 18px; +} + +// ── Root wrapper ─────────────────────────────────────────────────────────── +.dap { + border-radius: 14px; + border: 1.5px solid $border; + background: #fff; + overflow: hidden; + box-shadow: 0 1px 4px rgb(0 0 0 / 6%); + transition: box-shadow 0.2s; + + &:hover { + box-shadow: 0 3px 12px rgb(0 0 0 / 10%); + } + + // Risk-tinted border + &--safe { + border-color: $bd-safe; + } + + &--low { + border-color: $bd-low; + } + + &--medium { + border-color: $bd-medium; + } + + &--high { + border-color: $bd-high; + } + + &--critical { + border-color: $bd-critical; + } + + &--escheat { + border-color: $bd-escheat; + } + + // ── HEADER ─────────────────────────────────────────────────────── + &__header { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; + padding: 14px 18px; + background: $surface; + border-bottom: 1px solid $border; + } + + &__header-left { + display: flex; + align-items: center; + gap: 12px; + } + + &__icon-wrap { + width: 40px; + height: 40px; + border-radius: 10px; + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + border: 1px solid $border; + + i { + font-size: 17px; + } + + .dap--safe & { + background: $bg-safe; + border-color: $bd-safe; + + i { + color: $c-safe; + } + } + + .dap--low & { + background: $bg-low; + border-color: $bd-low; + + i { + color: $c-low; + } + } + + .dap--medium & { + background: $bg-medium; + border-color: $bd-medium; + + i { + color: $c-medium; + } + } + + .dap--high & { + background: $bg-high; + border-color: $bd-high; + + i { + color: $c-high; + } + } + + .dap--critical & { + background: $bg-critical; + border-color: $bd-critical; + + i { + color: $c-critical; + } + } + + .dap--escheat & { + background: $bg-escheat; + border-color: $bd-escheat; + + i { + color: $c-escheat; + } + } + } + + &__title { + font-size: 13.5px; + font-weight: 700; + color: $text; + letter-spacing: -0.01em; + line-height: 1.3; + } + + &__subtitle { + font-size: 11.5px; + color: $muted; + margin-top: 2px; + line-height: 1.4; + } + + // ── Badge group ────────────────────────────────────────────────── + &__badges { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + } + + &__risk-badge { + display: inline-flex; + align-items: center; + gap: 7px; + padding: 5px 13px; + border-radius: 999px; + font-size: 11.5px; + font-weight: 700; + letter-spacing: 0.01em; + cursor: default; + + .dap--safe & { + background: $bg-safe; + color: $c-safe; + border: 1.5px solid $bd-safe; + } + + .dap--low & { + background: $bg-low; + color: $c-low; + border: 1.5px solid $bd-low; + } + + .dap--medium & { + background: $bg-medium; + color: $c-medium; + border: 1.5px solid $bd-medium; + } + + .dap--high & { + background: $bg-high; + color: $c-high; + border: 1.5px solid $bd-high; + } + + .dap--critical & { + background: $bg-critical; + color: $c-critical; + border: 1.5px solid $bd-critical; + } + + .dap--escheat & { + background: $bg-escheat; + color: $c-escheat; + border: 1.5px solid $bd-escheat; + } + } + + &__risk-dot { + width: 7px; + height: 7px; + border-radius: 50%; + display: inline-block; + animation: pulse-dot 2s ease-in-out infinite; + + .dap--safe & { + background: $c-safe; + } + + .dap--low & { + background: $c-low; + } + + .dap--medium & { + background: $c-medium; + } + + .dap--high & { + background: $c-high; + } + + .dap--critical & { + background: $c-critical; + } + + .dap--escheat & { + background: $c-escheat; + } + } + + &__monitor-badge { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 5px 11px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; + background: $bg-low; + color: $c-low; + border: 1.5px solid $bd-low; + + i { + font-size: 10px; + } + } + + // ── Metric Cards ───────────────────────────────────────────────── + &__cards { + display: grid; + grid-template-columns: repeat(5, 1fr); + border-bottom: 1px solid $border; + + &--product { + grid-template-columns: repeat(6, 1fr); + } + + @media (width <= 1024px) { + grid-template-columns: repeat(3, 1fr) !important; + + .dap__card:nth-child(3n) { + border-right: none; + } + } + + @media (width <= 600px) { + grid-template-columns: repeat(2, 1fr) !important; + + .dap__card:nth-child(2n) { + border-right: none; + } + } + } + + &__card { + padding: 14px 16px; + border-right: 1px solid $border; + display: flex; + flex-direction: column; + gap: 5px; + transition: background 0.15s; + + &:last-child { + border-right: none; + } + + &:hover { + background: $surface; + } + } + + &__card-label { + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + color: $muted; + display: flex; + align-items: center; + gap: 4px; + } + + &__card-hint { + font-size: 10.5px; + color: $subtle; + margin-top: 2px; + line-height: 1.3; + } + + &__help { + font-size: 9px; + color: $subtle; + cursor: help; + + &:hover { + color: $c-low; + } + } + + // Status pill + &__pill { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 4px 11px; + border-radius: 999px; + font-size: 12px; + font-weight: 600; + width: fit-content; + + i { + font-size: 11px; + } + + &--green { + background: $bg-safe; + color: $c-safe; + border: 1px solid $bd-safe; + } + + &--gray { + background: #f1f5f9; + color: $muted; + border: 1px solid #e2e8f0; + } + + &--safe { + background: $bg-safe; + color: $c-safe; + border: 1px solid $bd-safe; + } + + &--low { + background: $bg-low; + color: $c-low; + border: 1px solid $bd-low; + } + + &--medium { + background: $bg-medium; + color: $c-medium; + border: 1px solid $bd-medium; + } + + &--high { + background: $bg-high; + color: $c-high; + border: 1px solid $bd-high; + } + + &--critical { + background: $bg-critical; + color: $c-critical; + border: 1px solid $bd-critical; + } + + &--escheat { + background: $bg-escheat; + color: $c-escheat; + border: 1px solid $bd-escheat; + } + } + + // Number display + &__number { + font-size: 22px; + font-weight: 800; + color: $text; + display: flex; + align-items: baseline; + gap: 4px; + font-variant-numeric: tabular-nums; + + small { + font-size: 11px; + font-weight: 500; + color: $muted; + } + + &--warn { + color: $c-high; + + small { + color: color.adjust($c-high, $lightness: 10%); + } + } + } + + &__date { + font-size: 12.5px; + font-weight: 600; + color: $text; + display: flex; + align-items: center; + gap: 6px; + + i { + color: $muted; + font-size: 12px; + } + + &--muted { + color: $muted; + } + } + + &__reached { + font-size: 12px; + font-weight: 600; + color: $c-high; + display: flex; + align-items: center; + gap: 6px; + + i { + font-size: 11px; + } + } + + &__na { + font-size: 12px; + color: $subtle; + font-style: italic; + } + + // ── Timeline ───────────────────────────────────────────────────── + &__timeline { + padding: 16px 18px 18px; + background: $surface; + border-top: 1px solid $border; + } + + &__tl-header { + display: flex; + align-items: baseline; + justify-content: space-between; + flex-wrap: wrap; + gap: 4px; + margin-bottom: 16px; + } + + &__tl-label { + font-size: 10px; + font-weight: 800; + text-transform: uppercase; + letter-spacing: 0.07em; + color: $muted; + } + + &__tl-note { + font-size: 10px; + color: $subtle; + font-style: italic; + } + + &__tl-track { + display: flex; + align-items: flex-start; + overflow-x: auto; + padding-bottom: 4px; + } + + &__tl-node { + display: flex; + flex-direction: column; + align-items: center; + gap: 5px; + flex: 0 0 auto; + min-width: 68px; + } + + &__tl-circle { + width: 30px; + height: 30px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + border: 2px solid $border; + background: #fff; + transition: all 0.2s; + + i { + font-size: 12px; + color: #cbd5e1; + } + + &--done { + background: $c-safe; + border-color: $c-safe; + box-shadow: 0 0 0 3px rgba($c-safe, 0.15); + + i { + color: #fff; + } + } + + &--current { + background: #fff; + box-shadow: 0 0 0 3px rgb(0 0 0 / 8%); + + .dap--safe &, + .dap--low & { + border-color: $c-safe; + box-shadow: 0 0 0 3px rgba($c-safe, 0.15); + + i { + color: $c-safe; + } + } + + .dap--medium & { + border-color: $c-medium; + box-shadow: 0 0 0 3px rgba($c-medium, 0.15); + + i { + color: $c-medium; + } + } + + .dap--high &, + .dap--critical & { + border-color: $c-high; + box-shadow: 0 0 0 3px rgba($c-high, 0.15); + + i { + color: $c-high; + } + } + + .dap--escheat & { + border-color: $c-escheat; + box-shadow: 0 0 0 3px rgba($c-escheat, 0.15); + + i { + color: $c-escheat; + } + } + } + + &--pending { + background: $surface; + + i { + color: #cbd5e1; + } + } + } + + &__tl-name { + font-size: 10px; + font-weight: 700; + color: $muted; + text-transform: uppercase; + letter-spacing: 0.04em; + text-align: center; + white-space: nowrap; + cursor: help; + display: flex; + align-items: center; + gap: 3px; + } + + &__tl-info { + font-size: 8px; + color: $subtle; + text-transform: none; + } + + &__tl-sub { + font-size: 9px; + color: $subtle; + text-align: center; + } + + &__tl-days { + font-size: 10px; + color: $muted; + background: #e9eef5; + border-radius: 999px; + padding: 2px 7px; + font-weight: 600; + white-space: nowrap; + } + + &__tl-reached-tag { + font-size: 9px; + color: $c-high; + background: $bg-high; + border-radius: 999px; + padding: 2px 7px; + font-weight: 700; + border: 1px solid $bd-high; + white-space: nowrap; + } + + &__tl-line { + flex: 1; + height: 2px; + background: $border; + margin: 14px 2px 0; + min-width: 20px; + border-radius: 2px; + transition: background 0.3s; + + &--done { + background: $c-safe; + } + } +} + +// ── Pulse animation for risk dot ─────────────────────────────────────────── +@keyframes pulse-dot { + 0%, + 100% { + opacity: 1; + } + + 50% { + opacity: 0.45; + } +} diff --git a/src/app/shared/dormancy-alert-panel/dormancy-alert-panel.component.ts b/src/app/shared/dormancy-alert-panel/dormancy-alert-panel.component.ts new file mode 100644 index 000000000..2797494ad --- /dev/null +++ b/src/app/shared/dormancy-alert-panel/dormancy-alert-panel.component.ts @@ -0,0 +1,195 @@ +/** + * 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 } from '@angular/core'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +export type DormancyRisk = 'safe' | 'low' | 'medium' | 'high' | 'critical' | 'escheat'; + +@Component({ + selector: 'mifosx-dormancy-alert-panel', + templateUrl: './dormancy-alert-panel.component.html', + styleUrls: ['./dormancy-alert-panel.component.scss'], + imports: [...STANDALONE_SHARED_IMPORTS] +}) +export class DormancyAlertPanelComponent implements OnInit { + /** Pass full account data — for ACCOUNT VIEW (general tab) */ + @Input() accountData: any; + /** Pass product template — for ACCOUNT CREATION (currency step) */ + @Input() productTemplate: any; + + risk: DormancyRisk = 'safe'; + riskLabel = ''; + riskSeverity = ''; // e.g. "Low", "Medium", "High" + + statusLabel = ''; + statusId = 0; + + lastTxnDate: string | null = null; + accountOpenedDate: string | null = null; + + daysToInactive: number | null = null; + daysToDormancy: number | null = null; + daysToEscheat: number | null = null; + + isMonitoringActive = false; // isDormancyTrackingActive + isAlertEnabled = false; // dormancyAlert + + shouldShow = false; + + /** true = product template mode (creation stepper), false = account view mode */ + get isProductMode(): boolean { + return !!this.productTemplate && !this.accountData; + } + + ngOnInit(): void { + if (this.productTemplate && !this.accountData) { + this.initFromProductTemplate(); + } else if (this.accountData) { + this.initFromAccountData(); + } + } + + // ── Product Template Mode (Currency Step) ───────────────────── + private initFromProductTemplate(): void { + const pt = this.productTemplate; + + // Savings: fields come with "FromProduct" suffix + // RD/Current: fields come directly (isDormancyTrackingActive, daysToInactive, etc.) + this.isMonitoringActive = !!(pt.isDormancyTrackingActiveFromProduct ?? pt.isDormancyTrackingActive ?? false); + this.isAlertEnabled = !!pt.dormancyAlert; + + this.daysToInactive = pt.daysToInactiveFromProduct ?? pt.daysToInactive ?? null; + this.daysToDormancy = pt.daysToDormancyFromProduct ?? pt.daysToDormancy ?? null; + this.daysToEscheat = pt.daysToEscheatFromProduct ?? pt.daysToEscheat ?? null; + + this.statusId = 0; + this.statusLabel = 'New Account'; + + this.computeRiskFromProduct(); + this.shouldShow = this.isMonitoringActive || this.isAlertEnabled; + } + + private computeRiskFromProduct(): void { + // Product mode — always show safe/neutral, no risk colouring + this.risk = 'safe'; + this.riskLabel = 'Configured'; + this.riskSeverity = this.isMonitoringActive ? 'Active' : 'Inactive'; + } + + // ── Account Data Mode (General Tab) ─────────────────────────── + private initFromAccountData(): void { + const a = this.accountData; + + this.statusId = a.subStatus?.id ?? 0; + this.statusLabel = a.subStatus?.value ?? 'Active'; + this.isAlertEnabled = !!a.dormancyAlert; + this.isMonitoringActive = !!a.isDormancyTrackingActive; + + this.daysToInactive = a.daysToInactive ?? null; + this.daysToDormancy = a.daysToDormancy ?? null; + this.daysToEscheat = a.daysToEscheat ?? null; + + // Last active transaction date + const raw = a.lastActiveTransactionDate; + if (raw) { + const d = Array.isArray(raw) && raw.length >= 3 ? new Date(raw[0], raw[1] - 1, raw[2]) : new Date(raw); + this.lastTxnDate = d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }); + } + + // Account opened / activated date + const tl = a.timeline; + if (tl?.activatedOnDate) { + const ad = + Array.isArray(tl.activatedOnDate) && tl.activatedOnDate.length >= 3 + ? new Date(tl.activatedOnDate[0], tl.activatedOnDate[1] - 1, tl.activatedOnDate[2]) + : new Date(tl.activatedOnDate); + this.accountOpenedDate = ad.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }); + } + + this.computeRiskFromAccount(); + this.shouldShow = + this.isAlertEnabled || + this.isMonitoringActive || + this.statusId > 0 || + (this.daysToInactive !== null && this.daysToInactive <= 90) || + !!this.lastTxnDate; + } + + private computeRiskFromAccount(): void { + if (this.statusId === 300) { + this.risk = 'escheat'; + this.riskLabel = 'Escheat'; + this.riskSeverity = 'Critical'; + } else if (this.statusId === 200) { + this.risk = 'critical'; + this.riskLabel = 'Dormant'; + this.riskSeverity = 'Critical'; + } else if (this.statusId === 100) { + this.risk = 'high'; + this.riskLabel = 'Inactive'; + this.riskSeverity = 'High'; + } else if (this.daysToInactive !== null && this.daysToInactive <= 30) { + this.risk = 'high'; + this.riskLabel = 'High'; + this.riskSeverity = 'High'; + } else if (this.daysToInactive !== null && this.daysToInactive <= 60) { + this.risk = 'medium'; + this.riskLabel = 'Medium'; + this.riskSeverity = 'Medium'; + } else if (this.daysToInactive !== null && this.daysToInactive <= 90) { + this.risk = 'low'; + this.riskLabel = 'Low'; + this.riskSeverity = 'Low'; + } else { + this.risk = 'safe'; + this.riskLabel = 'Active'; + this.riskSeverity = 'Safe'; + } + } + + get timelineSteps() { + return [ + { + label: 'Inactive', + tooltip: 'Account flagged as inactive after no customer-initiated transaction', + days: this.daysToInactive, + reached: this.statusId >= 100, + current: this.statusId === 100 + }, + { + label: 'Dormant', + tooltip: 'Account classified as dormant per RBI/regulatory guidelines', + days: this.daysToDormancy, + reached: this.statusId >= 200, + current: this.statusId === 200 + }, + { + label: 'Escheat', + tooltip: 'Unclaimed funds transferred to government/regulator as per law', + days: this.daysToEscheat, + reached: this.statusId >= 300, + current: this.statusId === 300 + } + ]; + } + + /** e.g. "Inactive after 30 days of no transaction" */ + inactiveSubtext(): string { + return this.daysToInactive !== null ? `No transaction for ${this.daysToInactive} days` : 'Threshold not configured'; + } + + dormancySubtext(): string { + const total = (this.daysToInactive ?? 0) + (this.daysToDormancy ?? 0); + return this.daysToDormancy !== null ? `${this.daysToDormancy} days after inactive` : 'Threshold not configured'; + } + + escheatSubtext(): string { + return this.daysToEscheat !== null ? `${this.daysToEscheat} days after dormant` : 'Threshold not configured'; + } +} diff --git a/src/app/shared/introducer-search/introducer-search.component.html b/src/app/shared/introducer-search/introducer-search.component.html new file mode 100644 index 000000000..d7663ef85 --- /dev/null +++ b/src/app/shared/introducer-search/introducer-search.component.html @@ -0,0 +1,130 @@ + + +
+
+ + Introducer + Required +
+ +
+ @if (!selectedClient) { + +
+ + Search by Name or Client ID + + search + + + @if (searching) { +
+
+ + Searching... +
+
+ } + + @if (!searching && searchResults.length > 0) { +
+ @for (r of searchResults; track r.id) { +
+ person +
+ {{ r.displayName }} + + ID: {{ r.id }} + @if (r.officeName) { + · {{ r.officeName }} + } + @if (r.mobileNo) { + · {{ r.mobileNo }} + } + +
+
+ } +
+ } + + @if (!searching && searchText.length >= 2 && searchResults.length === 0) { +
No clients found
+ } +
+ } + + @if (selectedClient) { + +
+
+ @if (linkedProfileImage) { + Profile + } @else { + person + } +
+
+ {{ selectedClient.displayName }} + + Client ID: {{ selectedClient.id }} + @if (selectedClient.accountNumber) { + · A/C: {{ selectedClient.accountNumber }} + } + +
+ +
+ + @if (fetchingLinkedData) { +
+ + Fetching client details... +
+ } + + @if (!fetchingLinkedData) { +
+
+ FULL NAME + {{ selectedClient.displayName }} +
+
+ DATE OF BIRTH + {{ clientDob || '—' }} +
+ @if (clientAadhaar) { +
+ AADHAAR + + {{ clientAadhaar }} + @if (linkedKycStatus?.aadhaarVerified) { + verified Verified + } + +
+ } + @if (clientPan) { +
+ PAN + + {{ clientPan }} + @if (linkedKycStatus?.panVerified) { + verified Verified + } + +
+ } +
+ } + } +
+
diff --git a/src/app/shared/introducer-search/introducer-search.component.scss b/src/app/shared/introducer-search/introducer-search.component.scss new file mode 100644 index 000000000..4b882d3cd --- /dev/null +++ b/src/app/shared/introducer-search/introducer-search.component.scss @@ -0,0 +1,316 @@ +/** + * 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/. + */ + +$blue: #2563eb; +$blue-light: #eff6ff; +$green: #16a34a; +$green-light: #dcfce7; +$border: #e2e8f0; +$text: #1e293b; +$muted: #64748b; +$gray-bg: #f8fafc; + +:host { + overflow: visible; +} + +.intro-section { + border: 1px solid $border; + border-radius: 12px; + overflow: visible; + background: #fff; + margin-top: 16px; + position: relative; + z-index: 10; + + &__header { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 20px; + background: $gray-bg; + border-bottom: 1px solid $border; + border-radius: 12px 12px 0 0; + + i { + color: $blue; + font-size: 14px; + } + + span:nth-child(2) { + font-size: 14px; + font-weight: 600; + color: $text; + flex: 1; + } + } +} + +.intro-badge { + display: inline-flex; + align-items: center; + padding: 2px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; + + &--required { + background: #fef3c7; + color: #92400e; + border: 1px solid #fde68a; + } +} + +.intro-body { + padding: 16px 20px; + overflow: visible; + position: relative; + z-index: 1000; +} + +// Search +.intro-search-wrap { + position: relative; + z-index: 1000; + overflow: visible !important; +} + +.intro-search-field { + width: 100%; +} + +.intro-results-inline { + margin-top: 4px; + border: 1px solid $border; + border-radius: 8px; + background: #fff; + box-shadow: 0 4px 16px rgb(0 0 0 / 8%); + overflow: hidden; + max-height: 260px; + overflow-y: auto; +} + +.intro-results { + position: absolute !important; + z-index: 99999 !important; + top: calc(100% + 4px); + left: 0; + right: 0; + background: #fff; + border: 1px solid $border; + border-radius: 8px; + box-shadow: 0 4px 16px rgb(0 0 0 / 10%); + max-height: 260px; + overflow: hidden auto; + + &__loading { + display: flex; + align-items: center; + gap: 10px; + padding: 14px 16px; + font-size: 13px; + color: $muted; + } +} + +.intro-result-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + cursor: pointer; + transition: background 0.15s; + border-bottom: 1px solid $border; + min-width: 0; + + &:last-child { + border-bottom: none; + } + + &:hover { + background: $blue-light; + } + + &__icon { + color: $muted; + font-size: 20px; + width: 20px; + height: 20px; + flex: 0 0 auto; + } + + &__info { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + flex: 1; + } + + &__name { + font-size: 13px; + font-weight: 600; + color: $text; + white-space: normal; + word-break: break-word; + overflow-wrap: break-word; + } + + &__meta { + font-size: 11px; + color: $muted; + white-space: normal; + word-break: break-word; + overflow-wrap: break-word; + } +} + +.intro-no-results { + padding: 10px 0; + font-size: 13px; + color: $muted; + text-align: center; +} + +// Selected client card +.intro-card { + display: flex; + align-items: center; + gap: 14px; + padding: 14px 16px; + border: 1px solid $border; + border-radius: 10px; + background: $gray-bg; + margin-bottom: 14px; + min-width: 0; + + &__avatar { + width: 44px; + height: 44px; + border-radius: 50%; + background: $blue-light; + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + overflow: hidden; + } + + &__photo { + width: 100%; + height: 100%; + object-fit: cover; + } + + &__avatar-icon { + color: $blue; + font-size: 24px; + width: 24px; + height: 24px; + } + + &__info { + flex: 1; + display: flex; + flex-direction: column; + gap: 3px; + min-width: 0; + } + + &__name { + font-size: 14px; + font-weight: 600; + color: $text; + white-space: normal; + word-break: break-word; + overflow-wrap: break-word; + } + + &__meta { + font-size: 12px; + color: $muted; + white-space: normal; + word-break: break-word; + overflow-wrap: break-word; + } + + &__remove { + flex: 0 0 auto; + color: $muted; + } +} + +.intro-fetching { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: $muted; + padding: 6px 0; +} + +// Details grid +.intro-details-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 14px 24px; + padding: 14px 16px; + border: 1px solid $border; + border-radius: 10px; + background: #fff; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.intro-detail { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; + + &__label { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.06em; + color: $muted; + text-transform: uppercase; + } + + &__value { + font-size: 13px; + font-weight: 600; + color: $text; + display: flex; + align-items: center; + gap: 6px; + white-space: normal; + word-break: break-word; + overflow-wrap: break-word; + flex-wrap: wrap; + } +} + +.intro-verified { + display: inline-flex; + align-items: center; + gap: 3px; + font-size: 11px; + font-weight: 600; + color: $green; + background: $green-light; + border-radius: 999px; + padding: 2px 8px; + + mat-icon { + font-size: 12px; + width: 12px; + height: 12px; + } +} diff --git a/src/app/shared/introducer-search/introducer-search.component.ts b/src/app/shared/introducer-search/introducer-search.component.ts new file mode 100644 index 000000000..fa0410d78 --- /dev/null +++ b/src/app/shared/introducer-search/introducer-search.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/. + */ + +import { Component, Output, EventEmitter, inject } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { ClientsService } from 'app/clients/clients.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { MatIconButton } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'mifosx-introducer-search', + templateUrl: './introducer-search.component.html', + styleUrls: ['./introducer-search.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FormsModule, + MatProgressSpinner, + MatIconButton, + MatIcon + ] +}) +export class IntroducerSearchComponent { + private clientsService = inject(ClientsService); + private sanitizer = inject(DomSanitizer); + + @Output() introducerChange = new EventEmitter<{ clientId: number; displayName: string } | null>(); + + entryMode: 'linked' = 'linked'; + searchText = ''; + searching = false; + searchResults: any[] = []; + selectedClient: any = null; + fetchingLinkedData = false; + linkedProfileImage: any = null; + linkedKycStatus: any = null; + + // Fetched client details + clientDob = ''; + clientAadhaar = ''; + clientPan = ''; + + private searchTimer: any; + + onSearch(text: string): void { + this.searchText = text; + clearTimeout(this.searchTimer); + if (!text || text.length < 2) { + this.searchResults = []; + return; + } + this.searching = true; + this.searchTimer = setTimeout(() => { + this.clientsService.searchByText(text, 0, 8).subscribe({ + next: (data: any) => { + this.searchResults = data?.content || []; + this.searching = false; + }, + error: () => { + this.searchResults = []; + this.searching = false; + } + }); + }, 400); + } + + selectClient(client: any): void { + this.selectedClient = client; + this.searchResults = []; + this.searchText = ''; + this.clientDob = ''; + this.clientAadhaar = ''; + this.clientPan = ''; + this.linkedProfileImage = null; + this.linkedKycStatus = null; + this.fetchLinkedClientData(client.id); + this.introducerChange.emit({ clientId: client.id, displayName: client.displayName }); + } + + removeClient(): void { + this.selectedClient = null; + this.linkedProfileImage = null; + this.linkedKycStatus = null; + this.clientDob = ''; + this.clientAadhaar = ''; + this.clientPan = ''; + this.introducerChange.emit(null); + } + + private fetchLinkedClientData(clientId: string): void { + this.fetchingLinkedData = true; + + this.clientsService.getClientData(clientId).subscribe({ + next: (data: any) => { + if (data.dateOfBirth) { + const d = Array.isArray(data.dateOfBirth) + ? new Date(data.dateOfBirth[0], data.dateOfBirth[1] - 1, data.dateOfBirth[2]) + : new Date(data.dateOfBirth); + this.clientDob = d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }); + } + this.fetchingLinkedData = false; + }, + error: () => { + this.fetchingLinkedData = false; + } + }); + + this.clientsService.getClientKyc(clientId).subscribe({ + next: (kyc: any) => { + if (kyc) { + this.clientAadhaar = kyc.aadhaarNumberMasked || ''; + this.clientPan = kyc.panNumber || ''; + this.linkedKycStatus = { + panVerified: kyc.panVerified || false, + aadhaarVerified: kyc.aadhaarVerified || false + }; + } + }, + error: () => {} + }); + + this.clientsService.getClientProfileImage(clientId).subscribe({ + next: (imageData: any) => { + if (imageData) { + this.linkedProfileImage = this.sanitizer.bypassSecurityTrustResourceUrl(imageData); + } + }, + error: () => {} + }); + } +} diff --git a/src/app/shared/nominee-form/nominee-form.component.html b/src/app/shared/nominee-form/nominee-form.component.html new file mode 100644 index 000000000..49bd32d8a --- /dev/null +++ b/src/app/shared/nominee-form/nominee-form.component.html @@ -0,0 +1,282 @@ + + +
+ +
+
+ + Nomination Facility +
+
+ + +
+
+ +
+ + @if (mode === 'manual') { +
+ +
+ + Full Name * + + Full name is required + + + + Relationship * + + @for (rel of relationships; track rel) { + {{ rel }} + } + + Relationship is required + +
+ +
+ + Date of Birth + + + + + + + Nationality + + Indian + NRI + Foreign National + + +
+ + + + +
+ + Aadhaar Number + + + + + PAN Number + + +
+ +
+ + Mobile Number + + + + + Nominee Share (%) + + +
+ + + + +
+ + Guardian Name + + + + + Guardian Relationship + + @for (rel of relationships; track rel) { + {{ rel }} + } + + +
+ + +
+ Is Alive + Is Dependent +
+ + + + Remarks + + +
+ } + + + @if (mode === 'linked') { + @if (!selectedClient) { + +
+ + Search by Name or Client ID + + search + + + @if (searching) { +
+
+ + Searching... +
+
+ } + + @if (!searching && searchResults.length > 0) { +
+ @for (r of searchResults; track r.id) { +
+ person +
+ {{ r.displayName }} + + ID: {{ r.id }} + @if (r.officeName) { + · {{ r.officeName }} + } + @if (r.mobileNo) { + · {{ r.mobileNo }} + } + +
+
+ } +
+ } + + @if (!searching && searchText.length >= 2 && searchResults.length === 0) { +
No clients found
+ } +
+ } + + @if (selectedClient) { + +
+
+ @if (linkedProfileImage) { + Profile + } @else { + person + } +
+
+ {{ selectedClient.displayName }} + + Client ID: {{ selectedClient.id }} + @if (selectedClient.accountNumber) { + · A/C: {{ selectedClient.accountNumber }} + } + +
+ +
+ + @if (fetchingLinkedData) { +
+ + Fetching nominee details... +
+ } + + @if (!fetchingLinkedData) { +
+
+ Full Name + {{ selectedClient.displayName }} +
+
+ Date of Birth + {{ clientDob || '—' }} +
+ @if (clientAadhaar) { +
+ Aadhaar + + {{ clientAadhaar }} + @if (linkedKycStatus?.aadhaarVerified) { + verified Verified + } + +
+ } + @if (clientPan) { +
+ PAN + + {{ clientPan }} + @if (linkedKycStatus?.panVerified) { + verified Verified + } + +
+ } +
+ + +
+ + Relationship * + + @for (rel of relationships; track rel) { + {{ rel }} + } + + + + + Nominee Share (%) + + +
+ } + } + } +
+
diff --git a/src/app/shared/nominee-form/nominee-form.component.scss b/src/app/shared/nominee-form/nominee-form.component.scss new file mode 100644 index 000000000..edfa9a96f --- /dev/null +++ b/src/app/shared/nominee-form/nominee-form.component.scss @@ -0,0 +1,387 @@ +/** + * 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 'sass:color'; + +$blue: #2563eb; +$blue-light: #eff6ff; +$green: #16a34a; +$green-light: #dcfce7; +$border: #e2e8f0; +$text: #1e293b; +$muted: #64748b; +$gray-bg: #f8fafc; +$purple: #7c3aed; +$purple-light: #f5f3ff; + +:host { + display: block; +} + +// ─── Wrapper ─────────────────────────────────────────────────── +.nom-section { + border: 1px solid $border; + border-radius: 14px; + background: #fff; + margin-top: 20px; + overflow: visible; + position: relative; + z-index: 10; +} + +// ─── Header ──────────────────────────────────────────────────── +.nom-header { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; + padding: 14px 20px; + background: $gray-bg; + border-bottom: 1px solid $border; + border-radius: 14px 14px 0 0; + + &__left { + display: flex; + align-items: center; + gap: 10px; + + i { + color: $purple; + font-size: 15px; + } + } + + &__title { + font-size: 14px; + font-weight: 700; + color: $text; + } +} + +// ─── Mode toggle tabs ────────────────────────────────────────── +.nom-mode-tabs { + display: flex; + gap: 8px; +} + +.nom-tab { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 14px; + border-radius: 8px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + border: 1.5px solid $border; + background: #fff; + color: $muted; + transition: all 0.18s; + + i { + font-size: 11px; + } + + &:hover { + border-color: $blue; + color: $blue; + background: $blue-light; + } + + &--active { + border-color: $blue; + background: $blue; + color: #fff; + + &:hover { + background: color.adjust(#2563eb, $lightness: -5%); + } + } +} + +// ─── Body ────────────────────────────────────────────────────── +.nom-body { + padding: 20px; + overflow: visible; + position: relative; + z-index: 10; +} + +// ─── Manual form grid ────────────────────────────────────────── +.nom-form { + display: flex; + flex-direction: column; + gap: 0; +} + +.nom-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.nom-field { + width: 100%; + + &--full { + width: 100%; + display: block; + } +} + +.nom-section-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.07em; + text-transform: uppercase; + color: $blue; + margin: 8px 0 4px; + + i { + font-size: 12px; + } +} + +.nom-hint { + font-size: 10px; + font-weight: 500; + text-transform: none; + letter-spacing: 0; + color: $muted; + margin-left: 2px; +} + +.nom-flags { + display: flex; + align-items: center; + gap: 24px; + margin: 8px 0 12px; +} + +// ─── Search (link mode) ──────────────────────────────────────── +.nom-search-wrap { + position: relative; + z-index: 1000; + overflow: visible; +} + +.nom-results { + margin-top: 4px; + border: 1px solid $border; + border-radius: 8px; + background: #fff; + box-shadow: 0 4px 20px rgb(0 0 0 / 10%); + overflow: hidden; + max-height: 260px; + overflow-y: auto; + + &__loading { + display: flex; + align-items: center; + gap: 10px; + padding: 14px 16px; + font-size: 13px; + color: $muted; + } +} + +.nom-result-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + cursor: pointer; + transition: background 0.15s; + border-bottom: 1px solid $border; + + &:last-child { + border-bottom: none; + } + + &:hover { + background: $blue-light; + } + + &__icon { + color: $muted; + font-size: 20px; + width: 20px; + height: 20px; + flex: 0 0 auto; + } + + &__info { + display: flex; + flex-direction: column; + gap: 2px; + } + + &__name { + font-size: 13px; + font-weight: 600; + color: $text; + } + + &__meta { + font-size: 11px; + color: $muted; + } +} + +.nom-no-results { + padding: 12px 0; + font-size: 13px; + color: $muted; + text-align: center; +} + +// ─── Selected client card ────────────────────────────────────── +.nom-client-card { + display: flex; + align-items: center; + gap: 14px; + padding: 14px 16px; + border: 1px solid $border; + border-radius: 10px; + background: $gray-bg; + margin-bottom: 14px; + + &__avatar { + width: 46px; + height: 46px; + border-radius: 50%; + background: $blue-light; + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + overflow: hidden; + } + + &__photo { + width: 100%; + height: 100%; + object-fit: cover; + } + + &__avatar-icon { + color: $blue; + font-size: 26px; + width: 26px; + height: 26px; + } + + &__info { + flex: 1; + display: flex; + flex-direction: column; + gap: 3px; + } + + &__name { + font-size: 14px; + font-weight: 700; + color: $text; + } + + &__meta { + font-size: 12px; + color: $muted; + } + + &__remove { + flex: 0 0 auto; + color: $muted; + } +} + +.nom-fetching { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: $muted; + padding: 6px 0; +} + +// ─── Linked client details grid ──────────────────────────────── +.nom-details-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 14px 24px; + padding: 14px 16px; + border: 1px solid $border; + border-radius: 10px; + background: #fff; + margin-bottom: 14px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.nom-detail { + display: flex; + flex-direction: column; + gap: 4px; + + &__label { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.06em; + color: $muted; + text-transform: uppercase; + } + + &__value { + font-size: 13px; + font-weight: 600; + color: $text; + display: flex; + align-items: center; + gap: 6px; + } +} + +.nom-verified { + display: inline-flex; + align-items: center; + gap: 3px; + font-size: 11px; + font-weight: 600; + color: $green; + background: $green-light; + border-radius: 999px; + padding: 2px 8px; + + mat-icon { + font-size: 12px; + width: 12px; + height: 12px; + } +} + +// ─── Extras for linked (relationship + share) ────────────────── +.nom-linked-extras { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + margin-top: 4px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} diff --git a/src/app/shared/nominee-form/nominee-form.component.ts b/src/app/shared/nominee-form/nominee-form.component.ts new file mode 100644 index 000000000..594692183 --- /dev/null +++ b/src/app/shared/nominee-form/nominee-form.component.ts @@ -0,0 +1,254 @@ +/** + * 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, Output, EventEmitter, inject, OnInit } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators, FormsModule } from '@angular/forms'; +import { DomSanitizer } from '@angular/platform-browser'; +import { ClientsService } from 'app/clients/clients.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { MatIconButton } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; +import { MatCheckbox } from '@angular/material/checkbox'; +import { MatDatepickerModule } from '@angular/material/datepicker'; + +export interface NomineeData { + mode: 'manual' | 'linked'; + // Manual fields + fullName?: string; + relationship?: string; + dateOfBirth?: string; + nationality?: string; + aadhaarNumber?: string; + panNumber?: string; + mobileNo?: string; + nomineeShare?: number; + isAlive?: boolean; + isDependent?: boolean; + guardianName?: string; + guardianRelationship?: string; + remarks?: string; + // Linked client fields + clientId?: number; + displayName?: string; +} + +const RELATIONSHIPS = [ + 'Spouse', + 'Son', + 'Daughter', + 'Father', + 'Mother', + 'Brother', + 'Sister', + 'Grandfather', + 'Grandmother', + 'Grandson', + 'Granddaughter', + 'Uncle', + 'Aunt', + 'Nephew', + 'Niece', + 'Friend', + 'Other' +]; + +@Component({ + selector: 'mifosx-nominee-form', + templateUrl: './nominee-form.component.html', + styleUrls: ['./nominee-form.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FormsModule, + MatProgressSpinner, + MatIconButton, + MatIcon, + MatCheckbox, + MatDatepickerModule + ] +}) +export class NomineeFormComponent implements OnInit { + private fb = inject(UntypedFormBuilder); + private clientsService = inject(ClientsService); + private sanitizer = inject(DomSanitizer); + + @Output() nomineeChange = new EventEmitter(); + + mode: 'manual' | 'linked' = 'manual'; + relationships = RELATIONSHIPS; + + // Manual form + manualForm!: UntypedFormGroup; + minDate = new Date(1900, 0, 1); + maxDate = new Date(); + + // Link mode + searchText = ''; + searching = false; + searchResults: any[] = []; + selectedClient: any = null; + fetchingLinkedData = false; + linkedProfileImage: any = null; + linkedKycStatus: any = null; + clientDob = ''; + clientAadhaar = ''; + clientPan = ''; + linkedRelationship = ''; + linkedShare = 100; + + private searchTimer: any; + + ngOnInit(): void { + this.manualForm = this.fb.group({ + fullName: [ + '', + Validators.required + ], + relationship: [ + '', + Validators.required + ], + dateOfBirth: [''], + nationality: ['Indian'], + aadhaarNumber: [''], + panNumber: [''], + mobileNo: [''], + nomineeShare: [100], + isAlive: [true], + isDependent: [false], + guardianName: [''], + guardianRelationship: [''], + remarks: [''] + }); + + this.manualForm.valueChanges.subscribe(() => { + if (this.mode === 'manual' && this.manualForm.valid) { + this.emitManual(); + } + }); + } + + switchMode(m: 'manual' | 'linked'): void { + this.mode = m; + if (m === 'manual') { + this.clearLinked(); + } else { + this.manualForm.reset({ nationality: 'Indian', isAlive: true, isDependent: false, nomineeShare: 100 }); + this.nomineeChange.emit(null); + } + } + + // ─── Manual ────────────────────────────────────── + private emitManual(): void { + const v = this.manualForm.value; + this.nomineeChange.emit({ mode: 'manual', ...v }); + } + + // ─── Link existing client ───────────────────────── + onSearch(text: string): void { + this.searchText = text; + clearTimeout(this.searchTimer); + if (!text || text.length < 2) { + this.searchResults = []; + return; + } + this.searching = true; + this.searchTimer = setTimeout(() => { + this.clientsService.searchByText(text, 0, 8).subscribe({ + next: (data: any) => { + this.searchResults = data?.content || []; + this.searching = false; + }, + error: () => { + this.searchResults = []; + this.searching = false; + } + }); + }, 400); + } + + selectClient(client: any): void { + this.selectedClient = client; + this.searchResults = []; + this.searchText = ''; + this.fetchLinkedClientData(client.id); + this.nomineeChange.emit({ mode: 'linked', clientId: client.id, displayName: client.displayName }); + } + + removeLinked(): void { + this.clearLinked(); + this.nomineeChange.emit(null); + } + + onLinkedRelationshipChange(rel: string): void { + if (this.selectedClient) { + this.nomineeChange.emit({ + mode: 'linked', + clientId: this.selectedClient.id, + displayName: this.selectedClient.displayName, + relationship: rel, + nomineeShare: this.linkedShare + }); + } + } + + private clearLinked(): void { + this.selectedClient = null; + this.linkedProfileImage = null; + this.linkedKycStatus = null; + this.clientDob = ''; + this.clientAadhaar = ''; + this.clientPan = ''; + this.searchText = ''; + this.searchResults = []; + this.linkedRelationship = ''; + this.linkedShare = 100; + } + + private fetchLinkedClientData(clientId: string): void { + this.fetchingLinkedData = true; + + this.clientsService.getClientData(clientId).subscribe({ + next: (data: any) => { + if (data.dateOfBirth) { + const d = Array.isArray(data.dateOfBirth) + ? new Date(data.dateOfBirth[0], data.dateOfBirth[1] - 1, data.dateOfBirth[2]) + : new Date(data.dateOfBirth); + this.clientDob = d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }); + } + this.fetchingLinkedData = false; + }, + error: () => { + this.fetchingLinkedData = false; + } + }); + + this.clientsService.getClientKyc(clientId).subscribe({ + next: (kyc: any) => { + if (kyc) { + this.clientAadhaar = kyc.aadhaarNumberMasked || ''; + this.clientPan = kyc.panNumber || ''; + this.linkedKycStatus = { + panVerified: kyc.panVerified || false, + aadhaarVerified: kyc.aadhaarVerified || false + }; + } + }, + error: () => {} + }); + + this.clientsService.getClientProfileImage(clientId).subscribe({ + next: (imageData: any) => { + if (imageData) { + this.linkedProfileImage = this.sanitizer.bypassSecurityTrustResourceUrl(imageData); + } + }, + error: () => {} + }); + } +} diff --git a/src/environments/.env.ts b/src/environments/.env.ts index c31d3d90c..9feeaaf07 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -5,13 +5,12 @@ * 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/. */ - // IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN! /* tslint:disable */ export default { 'mifos_x': { - 'version': '260422', - 'hash': 'e9a93771' + 'version': '260424', + 'hash': 'fc1c5126' }, 'allow_switching_backend_instance': true }; From 5fb25edca5294da71e922b96d50580b4cbd98793 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Sun, 26 Apr 2026 02:11:28 +0530 Subject: [PATCH 07/12] add gl mapping in the saving product,current product,rd product and also add genrate account no all three --- .../current-account-details-step.component.html | 8 ++++++-- ...ng-deposits-account-details-step.component.html | 10 ++++++++-- .../current-product-details-step.component.html | 8 ++++++-- .../current-product-details-step.component.ts | 14 +++++++++++--- .../current-product-preview-step.component.html | 8 ++++---- .../current-product-preview-step.component.ts | 7 +++++++ .../current-product-general-tab.component.html | 8 ++++---- ...ing-deposit-product-details-step.component.html | 8 ++++++-- ...rring-deposit-product-details-step.component.ts | 14 +++++++++++--- ...ing-deposit-product-preview-step.component.html | 8 ++++---- ...rring-deposit-product-preview-step.component.ts | 7 +++++++ .../recurring-deposit-general-tab.component.html | 6 +++--- .../saving-product-details-step.component.html | 8 ++++++-- .../saving-product-details-step.component.ts | 14 +++++++++++--- .../saving-product-preview-step.component.html | 8 ++++---- .../saving-product-preview-step.component.ts | 7 +++++++ .../saving-product-general-tab.component.html | 8 ++++---- .../savings-account-details-step.component.html | 8 ++++++-- src/environments/.env.ts | 4 ++-- 19 files changed, 117 insertions(+), 46 deletions(-) diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html index 440bf7b4f..46882eb22 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html @@ -83,8 +83,12 @@ - Product Code - + Account No + @if (productTemplate.effectiveDate) { diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html index 1b01888e6..186f2f076 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html @@ -83,8 +83,14 @@ - Product Code - + Account No + @if (productTemplate.effectiveDate) { diff --git a/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.html b/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.html index 7b619793d..65c350380 100644 --- a/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.html +++ b/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.html @@ -82,8 +82,12 @@ - Product Code - + GL Account (Liability) + + @for (account of liabilityAccountOptions; track account.id) { + {{ account.name }} ({{ account.glCode }}) + } + diff --git a/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.ts b/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.ts index 1aba2fbac..0c7125ca5 100644 --- a/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.ts +++ b/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.ts @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, OnInit, OnChanges, Input, SimpleChanges, inject } from '@angular/core'; +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'; @@ -30,12 +30,14 @@ export class CurrentProductDetailsStepComponent implements OnInit, OnChanges { private formBuilder = inject(UntypedFormBuilder); private dateUtils = inject(Dates); private settingsService = inject(SettingsService); + private cdr = inject(ChangeDetectorRef); @Input() CurrentProductsTemplate: any; CurrentProductDetailsForm: UntypedFormGroup; accountTypeData: string[] = []; customerSegmentData: string[] = []; + liabilityAccountOptions: any[] = []; yesNoOptions: Array<{ value: boolean; label: string; @@ -82,7 +84,7 @@ export class CurrentProductDetailsStepComponent implements OnInit, OnChanges { Validators.required ], customerSegment: [null], - productCode: [''], + glAccountId: [null], effectiveTo: [''], endDate: [''], introducerRequired: [false], @@ -124,6 +126,8 @@ export class CurrentProductDetailsStepComponent implements OnInit, OnChanges { this.accountTypeData = accountTypeOptions; this.customerSegmentData = customerSegmentOptions; + this.liabilityAccountOptions = template.accountingMappingOptions?.liabilityAccountOptions ?? []; + this.cdr.detectChanges(); if (yesNoOptions.length) { this.yesNoOptions = yesNoOptions; } @@ -136,7 +140,7 @@ export class CurrentProductDetailsStepComponent implements OnInit, OnChanges { description: template.description, accountType: template.accountType ?? this.accountTypeData[0] ?? null, customerSegment: template.customerSegment ?? this.customerSegmentData[0] ?? null, - productCode: template.productCode ?? '', + glAccountId: template.glAccountId ?? null, effectiveTo: this.parseDate(template.effectiveTo), endDate: this.parseDate(template.endDate), introducerRequired: template.introducerRequired === true || template.introducerRequired === 'Yes', @@ -173,6 +177,10 @@ export class CurrentProductDetailsStepComponent implements OnInit, OnChanges { .filter((value) => !!value); } + compareById(o1: any, o2: any): boolean { + return o1 != null && o2 != null && Number(o1) === Number(o2); + } + get CurrentProductDetails() { const formValue = { ...this.CurrentProductDetailsForm.value }; const dateFormat = this.settingsService.dateFormat; diff --git a/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.html b/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.html index 68e5bdde8..c74008227 100644 --- a/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.html +++ b/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.html @@ -28,7 +28,7 @@ @if ( CurrentProduct.accountType || CurrentProduct.customerSegment || - CurrentProduct.productCode || + CurrentProduct.glAccountId || CurrentProduct.effectiveTo || CurrentProduct.introducerRequired !== undefined || CurrentProduct.shareLinkingEnabled !== undefined @@ -51,10 +51,10 @@
} - @if (CurrentProduct.productCode) { + @if (CurrentProduct.glAccountId) {
- Product Code: - {{ CurrentProduct.productCode }} + GL Account (Liability): + {{ getGlAccountName(CurrentProduct.glAccountId) }}
} diff --git a/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.ts b/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.ts index 2ff2bd27c..8800b5b92 100644 --- a/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.ts +++ b/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.ts @@ -205,6 +205,13 @@ export class CurrentProductPreviewStepComponent implements OnInit, OnChanges { return value === true || value === 'Yes' ? 'Yes' : 'No'; } + getGlAccountName(id: any): string { + if (!id) return ''; + const accounts = this.CurrentProductsTemplate?.accountingMappingOptions?.liabilityAccountOptions ?? []; + const found = accounts.find((a: any) => a.id === id || a.id === +id); + return found ? `${found.name} (${found.glCode})` : String(id); + } + getLockinPeriodFrequencyTypeLabel(value: any): string { switch (value) { case 'DAYS': diff --git a/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.html b/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.html index c316d2003..0a68dd9d7 100644 --- a/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.html +++ b/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.html @@ -40,7 +40,7 @@ @if ( currentProduct.accountType || currentProduct.customerSegment || - currentProduct.productCode || + currentProduct.glAccountId || currentProduct.effectiveDate || currentProduct.effectiveTo || currentProduct.endDate || @@ -65,10 +65,10 @@
} - @if (currentProduct.productCode) { + @if (currentProduct.glAccountId) {
- Product Code: - {{ currentProduct.productCode }} + GL Account (Liability): + {{ currentProduct.glAccountName || currentProduct.glAccountId }}
} diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.html index 7e5f20a46..35a3129ac 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.html @@ -88,8 +88,12 @@ - Product Code - + GL Account (Liability) + + @for (account of liabilityAccountOptions; track account.id) { + {{ account.name }} ({{ account.glCode }}) + } + diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.ts b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.ts index 3f2cd558c..749ac8a3b 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.ts +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.ts @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, OnInit, OnChanges, Input, SimpleChanges, inject } from '@angular/core'; +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'; @@ -28,12 +28,14 @@ export class RecurringDepositProductDetailsStepComponent implements OnInit, OnCh private formBuilder = inject(UntypedFormBuilder); private dateUtils = inject(Dates); private settingsService = inject(SettingsService); + private cdr = inject(ChangeDetectorRef); @Input() recurringDepositProductsTemplate: any; recurringDepositProductDetailsForm: UntypedFormGroup; accountTypeData: string[] = []; customerSegmentData: string[] = []; + liabilityAccountOptions: any[] = []; yesNoOptions: Array<{ value: boolean; label: string }> = [ { value: true, label: 'Yes' }, { value: false, label: 'No' } @@ -71,7 +73,7 @@ export class RecurringDepositProductDetailsStepComponent implements OnInit, OnCh Validators.required ], customerSegment: [null], - productCode: [''], + glAccountId: [null], effectiveTo: [''], endDate: [''], introducerRequired: [false], @@ -99,9 +101,15 @@ export class RecurringDepositProductDetailsStepComponent implements OnInit, OnCh 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.customerSegmentData = this.getOptionNames(template.customerSegmentOptions); + this.liabilityAccountOptions = template.accountingMappingOptions?.liabilityAccountOptions ?? []; + this.cdr.detectChanges(); const isEditing = !!template.id; this.recurringDepositProductDetailsForm.patchValue({ @@ -110,7 +118,7 @@ export class RecurringDepositProductDetailsStepComponent implements OnInit, OnCh description: template.description || '', accountType: template.accountType ?? this.accountTypeData[0] ?? null, customerSegment: template.customerSegment ?? this.customerSegmentData[0] ?? null, - productCode: template.productCode ?? '', + glAccountId: template.glAccountId ?? null, effectiveTo: this.parseDate(template.effectiveTo), endDate: this.parseDate(template.endDate), introducerRequired: template.introducerRequired === true || template.introducerRequired === 'Yes', diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.html index a421e9a2e..c32f8fbd1 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.html @@ -31,7 +31,7 @@ @if ( recurringDepositProduct.accountType || recurringDepositProduct.customerSegment || - recurringDepositProduct.productCode || + recurringDepositProduct.glAccountId || recurringDepositProduct.effectiveTo || recurringDepositProduct.introducerRequired !== undefined || recurringDepositProduct.shareLinkingEnabled !== undefined @@ -53,10 +53,10 @@ } - @if (recurringDepositProduct.productCode) { + @if (recurringDepositProduct.glAccountId) {
- Product Code: - {{ recurringDepositProduct.productCode }} + GL Account (Liability): + {{ getGlAccountName(recurringDepositProduct.glAccountId) }}
} diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.ts b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.ts index dadf5d011..9aa3f0276 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.ts +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.ts @@ -121,6 +121,13 @@ export class RecurringDepositProductPreviewStepComponent implements OnInit, OnCh return found?.value || String(value); } + getGlAccountName(id: any): string { + if (!id) return ''; + const accounts = this.recurringDepositProductsTemplate?.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); diff --git a/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.html b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.html index 09319e1dc..f673989be 100644 --- a/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.html +++ b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.html @@ -54,10 +54,10 @@ {{ recurringDepositProduct.customerSegment }} } - @if (recurringDepositProduct.productCode) { + @if (recurringDepositProduct.glAccountId) {
- Product Code: - {{ recurringDepositProduct.productCode }} + GL Account (Liability): + {{ recurringDepositProduct.glAccountName || recurringDepositProduct.glAccountId }}
} @if (recurringDepositProduct.effectiveTo) { diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.html b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.html index 93ae5be7c..7a8c5d3dc 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.html +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.html @@ -82,8 +82,12 @@
- Product Code - + GL Account (Liability) + + @for (account of liabilityAccountOptions; track account.id) { + {{ account.name }} ({{ account.glCode }}) + } + diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.ts b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.ts index 2805dc866..c9d2ba566 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.ts +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.ts @@ -6,7 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, OnInit, OnChanges, Input, SimpleChanges, inject } from '@angular/core'; +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'; @@ -30,12 +30,14 @@ export class SavingProductDetailsStepComponent implements OnInit, OnChanges { private formBuilder = inject(UntypedFormBuilder); private dateUtils = inject(Dates); private settingsService = inject(SettingsService); + private cdr = inject(ChangeDetectorRef); @Input() savingProductsTemplate: any; savingProductDetailsForm: UntypedFormGroup; accountTypeData: string[] = []; customerSegmentData: string[] = []; + liabilityAccountOptions: any[] = []; yesNoOptions: Array<{ value: boolean; label: string; @@ -82,7 +84,7 @@ export class SavingProductDetailsStepComponent implements OnInit, OnChanges { Validators.required ], customerSegment: [null], - productCode: [''], + glAccountId: [null], effectiveDate: [''], effectiveTo: [''], endDate: [''], @@ -110,6 +112,8 @@ export class SavingProductDetailsStepComponent implements OnInit, OnChanges { this.accountTypeData = accountTypeOptions; this.customerSegmentData = customerSegmentOptions; + this.liabilityAccountOptions = template.accountingMappingOptions?.liabilityAccountOptions ?? []; + this.cdr.detectChanges(); if (yesNoOptions.length) { this.yesNoOptions = yesNoOptions; } @@ -122,7 +126,7 @@ export class SavingProductDetailsStepComponent implements OnInit, OnChanges { description: template.description, accountType: template.accountType ?? this.accountTypeData[0] ?? null, customerSegment: template.customerSegment ?? this.customerSegmentData[0] ?? null, - productCode: template.productCode ?? '', + glAccountId: template.glAccountId ?? null, effectiveDate: template.effectiveDate ? new Date(template.effectiveDate) : '', effectiveTo: template.effectiveTo ? new Date(template.effectiveTo) : '', endDate: template.endDate ? new Date(template.endDate) : '', @@ -162,6 +166,10 @@ export class SavingProductDetailsStepComponent implements OnInit, OnChanges { .filter((value) => !!value); } + compareById(o1: any, o2: any): boolean { + return o1 != null && o2 != null && Number(o1) === Number(o2); + } + get savingProductDetails() { const formValue = { ...this.savingProductDetailsForm.value }; const dateFormat = this.settingsService.dateFormat; diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.html b/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.html index 1fbae2866..8710a144e 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.html +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.html @@ -28,7 +28,7 @@ @if ( savingProduct.accountType || savingProduct.customerSegment || - savingProduct.productCode || + savingProduct.glAccountId || savingProduct.effectiveTo || savingProduct.introducerRequired !== undefined || savingProduct.shareLinkingEnabled !== undefined @@ -51,10 +51,10 @@ } - @if (savingProduct.productCode) { + @if (savingProduct.glAccountId) {
- Product Code: - {{ savingProduct.productCode }} + GL Account (Liability): + {{ getGlAccountName(savingProduct.glAccountId) }}
} diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.ts b/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.ts index e1b544103..decfc645d 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.ts +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.ts @@ -201,6 +201,13 @@ export class SavingProductPreviewStepComponent implements OnInit, OnChanges { return this.accounting.isAccrualAccounting(this.accountingRule); } + getGlAccountName(id: any): string { + if (!id) return ''; + const accounts = this.savingProductsTemplate?.accountingMappingOptions?.liabilityAccountOptions ?? []; + const found = accounts.find((a: any) => a.id === id || a.id === +id); + return found ? `${found.name} (${found.glCode})` : String(id); + } + getBooleanLabel(value: any): string { return value === true || value === 'Yes' ? 'Yes' : 'No'; } diff --git a/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.html b/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.html index cde61d82f..a36665ab5 100644 --- a/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.html +++ b/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.html @@ -40,7 +40,7 @@ @if ( savingProduct.accountType || savingProduct.customerSegment || - savingProduct.productCode || + savingProduct.glAccountId || savingProduct.effectiveDate || savingProduct.effectiveTo || savingProduct.endDate || @@ -65,10 +65,10 @@ } - @if (savingProduct.productCode) { + @if (savingProduct.glAccountId) {
- Product Code: - {{ savingProduct.productCode }} + GL Account (Liability): + {{ savingProduct.glAccountName || savingProduct.glAccountId }}
} diff --git a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html index 60304a70d..4bce9f33d 100644 --- a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html +++ b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html @@ -87,8 +87,12 @@
- Product Code - + Account No + @if (productTemplate.effectiveDate) { diff --git a/src/environments/.env.ts b/src/environments/.env.ts index 9feeaaf07..d1086eaaa 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -9,8 +9,8 @@ /* tslint:disable */ export default { 'mifos_x': { - 'version': '260424', - 'hash': 'fc1c5126' + 'version': '260425', + 'hash': 'fbaec185' }, 'allow_switching_backend_instance': true }; From e4516934a7d3f4eb7485e4a91d98f681cf2d0b00 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Mon, 27 Apr 2026 10:55:03 +0530 Subject: [PATCH 08/12] saving account,current account and rd account modify based on new update --- .../create-current-account.component.html | 3 + .../create-current-account.component.ts | 26 +- ...rrent-account-currency-step.component.html | 6 +- .../add-beneficiary-dialog.component.html | 190 ++++++ .../add-beneficiary-dialog.component.scss | 415 ++++++++++++ .../add-beneficiary-dialog.component.ts | 167 +++++ .../add-related-client-dialog.component.html | 135 ++++ .../add-related-client-dialog.component.scss | 359 +++++++++++ .../add-related-client-dialog.component.ts | 104 +++ ...urrent-account-details-step.component.html | 202 ++++++ ...urrent-account-details-step.component.scss | 608 ++++++++++++++++++ .../current-account-details-step.component.ts | 132 +++- ...urrent-account-preview-step.component.html | 451 +++++++++---- ...urrent-account-preview-step.component.scss | 324 +++++++++- .../current-account-preview-step.component.ts | 37 +- ...-recurring-deposits-account.component.html | 4 + ...te-recurring-deposits-account.component.ts | 25 +- ...osits-account-currency-step.component.html | 10 +- ...deposits-account-details-step.component.ts | 8 +- ...nt-interest-rate-chart-step.component.html | 235 ++----- ...nt-interest-rate-chart-step.component.scss | 99 +++ ...ount-interest-rate-chart-step.component.ts | 20 +- ...posits-account-preview-step.component.html | 402 ++++++++---- ...posits-account-preview-step.component.scss | 193 ++++++ ...deposits-account-preview-step.component.ts | 16 + .../create-savings-account.component.html | 2 + .../create-savings-account.component.ts | 15 + ...vings-account-currency-step.component.html | 6 +- .../savings-account-details-step.component.ts | 8 +- ...avings-account-preview-step.component.html | 386 +++++++---- ...avings-account-preview-step.component.scss | 271 +++++++- .../savings-account-preview-step.component.ts | 34 +- .../introducer-search.component.ts | 8 +- .../nominee-form/nominee-form.component.ts | 11 +- src/environments/.env.ts | 5 +- src/main.scss | 48 ++ 36 files changed, 4302 insertions(+), 663 deletions(-) create mode 100644 src/app/current-accounts/current-account-stepper/current-account-details-step/add-beneficiary-dialog/add-beneficiary-dialog.component.html create mode 100644 src/app/current-accounts/current-account-stepper/current-account-details-step/add-beneficiary-dialog/add-beneficiary-dialog.component.scss create mode 100644 src/app/current-accounts/current-account-stepper/current-account-details-step/add-beneficiary-dialog/add-beneficiary-dialog.component.ts create mode 100644 src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.html create mode 100644 src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.scss create mode 100644 src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.ts diff --git a/src/app/current-accounts/create-current-account/create-current-account.component.html b/src/app/current-accounts/create-current-account/create-current-account.component.html index 1f07c596b..6a81d16b6 100644 --- a/src/app/current-accounts/create-current-account/create-current-account.component.html +++ b/src/app/current-accounts/create-current-account/create-current-account.component.html @@ -96,6 +96,9 @@ [currentAccountSettingsForm]="currentAccountSettingsForm" [currentAccountTermsForm]="currentAccountTermsForm" [currentAccount]="currentAccount" + [currentAccountDigital]="currentAccountDigital" + [currentAccountNominee]="currentAccountNominee" + [currentAccountCurrencyFlags]="currentAccountCurrencyFlags" (submitEvent)="submit()" > diff --git a/src/app/current-accounts/create-current-account/create-current-account.component.ts b/src/app/current-accounts/create-current-account/create-current-account.component.ts index 9e0cad657..1329e5312 100644 --- a/src/app/current-accounts/create-current-account/create-current-account.component.ts +++ b/src/app/current-accounts/create-current-account/create-current-account.component.ts @@ -66,14 +66,15 @@ export class CreateCurrentAccountComponent { /** Current Account Product Template */ currentAccountProductTemplate: any; - /** Current Account Details Step */ @ViewChild(CurrentAccountDetailsStepComponent, { static: true }) currentAccountDetailsStep: CurrentAccountDetailsStepComponent; - /** Current Account Currency Step */ - @ViewChild(CurrentAccountCurrencyStepComponent, { static: true }) + @ViewChild(CurrentAccountCurrencyStepComponent) currentAccountCurrencyStep: CurrentAccountCurrencyStepComponent; + @ViewChild(CurrentAccountDigitalStepComponent) + currentAccountDigitalStep: CurrentAccountDigitalStepComponent; + constructor() { this.route.data.subscribe((data: { currentAccountTemplate: any }) => { this.currentAccountTemplate = data.currentAccountTemplate; @@ -107,12 +108,23 @@ export class CreateCurrentAccountComponent { return new FormGroup({}); } - /** - * Retrieves current account object. - */ get currentAccount() { + return { ...this.currentAccountDetailsStep.currentAccountDetails }; + } + + get currentAccountDigital() { + return this.currentAccountDigitalStep?.currentAccountDigital ?? null; + } + + get currentAccountNominee() { + return this.currentAccountCurrencyStep?.nomineeData ?? null; + } + + get currentAccountCurrencyFlags() { return { - ...this.currentAccountDetailsStep.currentAccountDetails + balanceSwapOption: this.currentAccountCurrencyStep?.balanceSwapOption ?? false, + msme: this.currentAccountCurrencyStep?.msme ?? false, + autoDebitFacility: this.currentAccountCurrencyStep?.autoDebitFacility ?? false }; } diff --git a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html index e790fd695..935e312dd 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html @@ -59,8 +59,9 @@ {{ currentAccountProductTemplate?.dormancyAlert ? 'Yes' : 'No' }} - - + @if (currentAccountProductTemplate?.dormancyAlert) { + + }
@@ -71,7 +72,6 @@ {{ currentAccountProductTemplate?.nominationFacility ? 'Yes' : 'No' }}
- @if (currentAccountProductTemplate?.nominationFacility) { } diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-beneficiary-dialog/add-beneficiary-dialog.component.html b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-beneficiary-dialog/add-beneficiary-dialog.component.html new file mode 100644 index 000000000..1b0c61fc7 --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-beneficiary-dialog/add-beneficiary-dialog.component.html @@ -0,0 +1,190 @@ + + + +
+
+ person_pin +
+
+

Add Beneficiary / Nominee

+

Register a nominee for this account as per Indian banking norms

+
+ +
+ + +
+ +
+ + +
+ + + @if (mode === 'search') { +
+ + Search Client by Name / CIF + search + + @if (isSearching) { + + } + @if (selectedClient) { + + } + + + @if (searchResults.length > 0) { +
+ @for (client of searchResults; track client) { +
+
{{ (client.displayName || '?')[0].toUpperCase() }}
+
+ {{ client.displayName }} + + @if (client.cifNumber) { + {{ client.cifNumber }} + } + @if (client.officeName) { + {{ client.officeName }} + } + +
+ chevron_right +
+ } +
+ } + + @if (selectedClient) { +
+
{{ (selectedClient.displayName || '?')[0].toUpperCase() }}
+
+ {{ selectedClient.displayName }} + {{ selectedClient.cifNumber || selectedClient.accountNumber }} +
+ Linked +
+ } +
+ } + + + + Nominee / Beneficiary Name * + + Name is required + + + +
+ + Relationship * + + @for (r of relationships; track r) { + {{ r }} + } + + Relationship is required + + + + Date of Birth + + + + +
+ + +
+ + Share % * + + % + Enter 1–100 + +
+ @for (p of [25, 33, 50, 100]; track p) { + + } +
+
+ + +
+ + Nominee is a Minor (below 18 years) + +
+ + + @if (isMinor) { +
+
+ supervisor_account + Guardian Details +
+
+ + Guardian Name + + + + Guardian Relationship + + @for (r of guardianRelationships; track r) { + {{ r }} + } + + +
+
+ } +
+
+ + + + + diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-beneficiary-dialog/add-beneficiary-dialog.component.scss b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-beneficiary-dialog/add-beneficiary-dialog.component.scss new file mode 100644 index 000000000..4c9e71b0c --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-beneficiary-dialog/add-beneficiary-dialog.component.scss @@ -0,0 +1,415 @@ +/** + * 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: #1a56db; +$primary-light: #eff6ff; +$primary-border: #bfdbfe; +$success: #057a55; +$success-light: #f3faf7; +$success-border: #84e1bc; +$orange: #d03801; +$orange-light: #fff8f1; +$orange-border: #fcd9bd; +$border: #e5e7eb; +$text: #111827; +$muted: #6b7280; +$radius: 10px; + +mifosx-add-beneficiary-dialog { + display: block; +} + +// Header +.abd-header { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 18px 20px 14px; + border-bottom: 1px solid $border; + background: linear-gradient(135deg, #f0f7ff 0%, #fff 60%); + + &__icon { + width: 42px; + height: 42px; + border-radius: 10px; + background: $primary; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + mat-icon { + color: #fff; + font-size: 22px; + width: 22px; + height: 22px; + } + } + + &__text { + flex: 1; + } + + &__title { + margin: 0; + font-size: 15px; + font-weight: 700; + color: $text; + } + + &__sub { + margin: 3px 0 0; + font-size: 11.5px; + color: $muted; + } + + &__close { + color: $muted; + margin: -4px -8px 0 0; + } +} + +// Form +.abd-form { + display: flex; + flex-direction: column; + gap: 0; + width: 500px; + max-width: 100%; + padding: 4px 0; +} + +.abd-full-width { + width: 100%; +} + +.abd-mt { + margin-top: 8px; +} + +// Mode toggle +.abd-mode-toggle { + display: flex; + gap: 8px; + margin: 14px 0 10px; + background: #f9fafb; + border: 1px solid $border; + border-radius: 10px; + padding: 5px; +} + +.abd-mode-btn { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 14px; + border-radius: 8px; + border: 1.5px solid transparent; + background: transparent; + font-size: 13px; + font-weight: 600; + color: $muted; + cursor: pointer; + transition: all 0.15s; + + mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + } + + &--active { + background: #fff; + border-color: $primary-border; + color: $primary; + box-shadow: 0 1px 4px rgb(0 0 0 / 8%); + } +} + +// Search +.abd-search-section { + position: relative; +} + +.abd-prefix-icon { + font-size: 18px; + width: 18px; + height: 18px; + color: $muted; + margin-right: 4px; +} + +.abd-search-results { + position: absolute; + z-index: 200; + top: calc(100% - 8px); + left: 0; + right: 0; + background: #fff; + border: 1px solid $border; + border-radius: $radius; + box-shadow: 0 8px 24px rgb(0 0 0 / 12%); + max-height: 220px; + overflow-y: auto; +} + +.abd-result-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + cursor: pointer; + border-bottom: 1px solid #f3f4f6; + transition: background 0.1s; + + &:last-child { + border-bottom: none; + } + + &:hover { + background: $primary-light; + } +} + +.abd-result-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background: linear-gradient(135deg, #6366f1, $primary); + color: #fff; + font-size: 13px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.abd-result-body { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; +} + +.abd-result-name { + font-size: 13px; + font-weight: 600; + color: $text; +} + +.abd-result-meta { + display: flex; + gap: 5px; + flex-wrap: wrap; +} + +.abd-chip { + font-size: 10px; + padding: 1px 7px; + border-radius: 999px; + background: #f3f4f6; + color: $muted; + font-weight: 500; +} + +.abd-result-arrow { + font-size: 18px; + width: 18px; + height: 18px; + color: $primary; + opacity: 0.4; + transition: all 0.1s; + + .abd-result-item:hover & { + opacity: 1; + transform: translateX(2px); + } +} + +// Selected card +.abd-selected-card { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + background: $success-light; + border: 1.5px solid $success-border; + border-radius: $radius; + margin-top: 6px; +} + +.abd-selected-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + background: linear-gradient(135deg, #10b981, $success); + color: #fff; + font-size: 14px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.abd-selected-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; +} + +.abd-selected-name { + font-size: 13px; + font-weight: 700; + color: #065f46; +} + +.abd-selected-sub { + font-size: 11px; + color: #047857; +} + +.abd-badge { + font-size: 10px; + font-weight: 700; + padding: 3px 10px; + border-radius: 999px; + text-transform: uppercase; + letter-spacing: 0.04em; + + &--linked { + background: $success; + color: #fff; + } +} + +// Row layout +.abd-row { + display: flex; + gap: 12px; + margin-top: 8px; + + @media (width <= 500px) { + flex-direction: column; + gap: 0; + } +} + +.abd-flex { + flex: 1; +} + +// Share % +.abd-share-row { + display: flex; + align-items: flex-start; + gap: 12px; + margin-top: 8px; +} + +.abd-share-field { + flex: 0 0 150px; +} + +.abd-pct { + font-size: 15px; + font-weight: 600; + color: $muted; + margin-right: 4px; +} + +.abd-quick-pct { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding-top: 10px; +} + +.abd-pct-chip { + padding: 5px 12px; + border-radius: 8px; + border: 1.5px solid $border; + background: #fff; + font-size: 12px; + font-weight: 600; + color: $muted; + cursor: pointer; + transition: all 0.12s; + + &:hover { + border-color: $primary; + background: $primary-light; + color: $primary; + } +} + +// Required star +.abd-required { + color: #e02424; +} + +// Minor toggle +.abd-minor-toggle { + margin-top: 14px; + padding: 12px 14px; + background: #fffbeb; + border: 1px solid #fde68a; + border-radius: 8px; +} + +// Guardian box +.abd-guardian-box { + margin-top: 10px; + padding: 14px; + background: $orange-light; + border: 1.5px solid $orange-border; + border-radius: $radius; +} + +.abd-guardian-label { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + font-weight: 700; + color: $orange; + text-transform: uppercase; + letter-spacing: 0.04em; + margin-bottom: 10px; + + mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + color: $orange; + } +} + +// Actions +.abd-actions { + padding: 12px 20px 16px !important; + border-top: 1px solid $border; + gap: 10px; + justify-content: flex-end !important; +} + +.abd-btn-add { + border-radius: 8px !important; + padding: 0 18px !important; + gap: 4px; + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + } +} diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-beneficiary-dialog/add-beneficiary-dialog.component.ts b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-beneficiary-dialog/add-beneficiary-dialog.component.ts new file mode 100644 index 000000000..bd691bbdf --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-beneficiary-dialog/add-beneficiary-dialog.component.ts @@ -0,0 +1,167 @@ +/** + * 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, OnDestroy, ViewEncapsulation } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; +import { MatDialogRef, MatDialogContent, MatDialogActions } from '@angular/material/dialog'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { MatIcon } from '@angular/material/icon'; +import { MatSlideToggle } from '@angular/material/slide-toggle'; +import { ClientsService } from 'app/clients/clients.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-add-beneficiary-dialog', + templateUrl: './add-beneficiary-dialog.component.html', + styleUrl: './add-beneficiary-dialog.component.scss', + encapsulation: ViewEncapsulation.None, + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatDialogContent, + MatDialogActions, + MatProgressSpinner, + MatIcon, + MatSlideToggle, + ReactiveFormsModule + ] +}) +export class AddBeneficiaryDialogComponent implements OnDestroy { + private fb = inject(UntypedFormBuilder); + private clientsService = inject(ClientsService); + dialogRef = inject(MatDialogRef); + + /** Toggle between linked client and manual entry */ + mode: 'search' | 'manual' = 'search'; + + form: UntypedFormGroup = this.fb.group({ + searchText: [''], + name: [ + '', + Validators.required + ], + relationship: [ + '', + Validators.required + ], + dateOfBirth: [null], + sharePercentage: [ + null, + [ + Validators.required, + Validators.min(1), + Validators.max(100) + ] + ], + isMinor: [false], + guardianName: [''], + guardianRelationship: [''] + }); + + searchResults: any[] = []; + selectedClient: any = null; + isSearching = false; + private searchTimer: any = null; + + readonly relationships = [ + 'Spouse', + 'Son', + 'Daughter', + 'Father', + 'Mother', + 'Brother', + 'Sister', + 'Grandson', + 'Granddaughter', + 'Father-in-law', + 'Mother-in-law', + 'Other' + ]; + + readonly guardianRelationships = [ + 'Father', + 'Mother', + 'Guardian', + 'Natural Guardian' + ]; + + get isMinor(): boolean { + return this.form.get('isMinor').value; + } + + switchMode(m: 'search' | 'manual'): void { + this.mode = m; + this.selectedClient = null; + this.searchResults = []; + this.form.patchValue({ searchText: '', name: '' }); + } + + onSearchInput(value: string): void { + if (this.searchTimer) clearTimeout(this.searchTimer); + this.selectedClient = null; + if (!value || value.trim().length < 2) { + this.searchResults = []; + return; + } + this.isSearching = true; + this.searchTimer = setTimeout(() => { + this.clientsService.searchByText(value.trim(), 0, 10).subscribe({ + next: (res: any) => { + this.searchResults = res?.content || []; + this.isSearching = false; + }, + error: () => { + this.searchResults = []; + this.isSearching = false; + } + }); + }, 400); + } + + selectClient(client: any): void { + this.selectedClient = client; + this.form.patchValue({ + searchText: client.displayName, + name: client.displayName + }); + this.searchResults = []; + } + + clearSelection(): void { + this.selectedClient = null; + this.form.patchValue({ searchText: '', name: '' }); + this.searchResults = []; + } + + confirm(): void { + if ( + this.form.get('name').invalid || + this.form.get('relationship').invalid || + this.form.get('sharePercentage').invalid + ) + return; + const v = this.form.getRawValue(); + this.dialogRef.close({ + clientId: this.selectedClient?.id || null, + name: v.name, + relationship: v.relationship, + dateOfBirth: v.dateOfBirth, + sharePercentage: v.sharePercentage, + isMinor: v.isMinor, + guardianName: v.isMinor ? v.guardianName : null, + guardianRelationship: v.isMinor ? v.guardianRelationship : null + }); + } + + cancel(): void { + this.dialogRef.close(null); + } + + ngOnDestroy(): void { + if (this.searchTimer) clearTimeout(this.searchTimer); + } +} diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.html b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.html new file mode 100644 index 000000000..29ffc269e --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.html @@ -0,0 +1,135 @@ + + + +
+
+ person_add +
+
+

Add Related Client / Partner

+

Search and link an existing client with their ownership share

+
+ +
+ + +
+ +
+ 1 + Select Client +
+ +
+ + Search by Name / CIF / Account No + search + + @if (isSearching) { + + } + @if (selectedClient) { + + } + + + + @if (searchResults.length > 0) { +
+ @for (client of searchResults; track client) { +
+
+ {{ (client.displayName || '?')[0].toUpperCase() }} +
+
+ {{ client.displayName }} + + @if (client.cifNumber) { + {{ client.cifNumber }} + } + @if (client.officeName) { + {{ client.officeName }} + } + @if (client.status?.value) { + + {{ client.status.value }} + + } + +
+ chevron_right +
+ } +
+ } +
+ + + @if (selectedClient) { +
+
+ {{ (selectedClient.displayName || '?')[0].toUpperCase() }} +
+
+ {{ selectedClient.displayName }} + {{ selectedClient.cifNumber || selectedClient.accountNumber || 'N/A' }} +
+ Selected +
+ } + + +
+ 2 + Ownership Share +
+ + +
+
+ + + + + diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.scss b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.scss new file mode 100644 index 000000000..3e9fcdc7e --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.scss @@ -0,0 +1,359 @@ +/** + * 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/. + */ + +// ── Tokens ─────────────────────────────────────────────────────────────────── +$primary: #2563eb; +$primary-light: #eff6ff; +$primary-border: #bfdbfe; +$success: #16a34a; +$success-light: #f0fdf4; +$success-border: #bbf7d0; +$border: #e5e7eb; +$text: #111827; +$text-muted: #6b7280; +$radius: 10px; + +// ── Dialog wrapper ──────────────────────────────────────────────────────────── +mifosx-add-related-client-dialog { + display: block; +} + +// ── Header ──────────────────────────────────────────────────────────────────── +.arc-header { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 20px 20px 14px; + border-bottom: 1px solid $border; + + &__icon { + width: 40px; + height: 40px; + border-radius: 10px; + background: $primary-light; + border: 1px solid $primary-border; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + mat-icon { + color: $primary; + font-size: 20px; + width: 20px; + height: 20px; + } + } + + &__text { + flex: 1; + } + + &__title { + margin: 0; + font-size: 15px; + font-weight: 700; + color: $text; + line-height: 1.3; + } + + &__sub { + margin: 3px 0 0; + font-size: 12px; + color: $text-muted; + } + + &__close { + margin-top: -4px; + margin-right: -8px; + color: $text-muted; + } +} + +// ── Dialog content ──────────────────────────────────────────────────────────── +.arc-dialog-form { + display: flex; + flex-direction: column; + gap: 0; + width: 440px; + max-width: 100%; + padding-top: 4px; +} + +// ── Step labels ─────────────────────────────────────────────────────────────── +.arc-step-label { + display: flex; + align-items: center; + gap: 8px; + margin: 16px 0 10px; + font-size: 12px; + font-weight: 700; + color: $text-muted; + text-transform: uppercase; + letter-spacing: 0.04em; + + &--mt { + margin-top: 20px; + } +} + +.arc-step-badge { + width: 20px; + height: 20px; + border-radius: 50%; + background: $primary; + color: #fff; + font-size: 11px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +// ── Search ──────────────────────────────────────────────────────────────────── +.arc-full-width { + width: 100%; +} + +.arc-search-section { + position: relative; +} + +.arc-search-icon { + font-size: 18px; + width: 18px; + height: 18px; + color: $text-muted; + margin-right: 4px; +} + +.arc-search-results { + position: absolute; + z-index: 200; + top: calc(100% - 8px); + left: 0; + right: 0; + background: #fff; + border: 1px solid $border; + border-radius: $radius; + box-shadow: 0 8px 24px rgb(0 0 0 / 12%); + max-height: 240px; + overflow-y: auto; +} + +.arc-search-result-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + cursor: pointer; + border-bottom: 1px solid #f3f4f6; + transition: background 0.12s; + + &:last-child { + border-bottom: none; + } + + &:hover { + background: $primary-light; + + .arc-result-arrow { + opacity: 1; + transform: translateX(2px); + } + } +} + +.arc-result-avatar { + width: 34px; + height: 34px; + border-radius: 50%; + background: linear-gradient(135deg, #6366f1, #2563eb); + color: #fff; + font-size: 13px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.arc-result-body { + flex: 1; + display: flex; + flex-direction: column; + gap: 3px; +} + +.arc-result-name { + font-size: 13px; + font-weight: 600; + color: $text; +} + +.arc-result-meta { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.arc-chip { + font-size: 10px; + padding: 1px 7px; + border-radius: 999px; + background: #f3f4f6; + color: $text-muted; + font-weight: 500; + + &--status { + background: #fef3c7; + color: #92400e; + } + + &--active { + background: #dcfce7; + color: #166534; + } +} + +.arc-result-arrow { + font-size: 18px; + width: 18px; + height: 18px; + color: $primary; + opacity: 0; + transition: + opacity 0.12s, + transform 0.12s; +} + +// ── Selected client ─────────────────────────────────────────────────────────── +.arc-selected-client { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 14px; + background: $success-light; + border: 1.5px solid $success-border; + border-radius: $radius; + margin-top: 6px; +} + +.arc-selected-avatar { + width: 38px; + height: 38px; + border-radius: 50%; + background: linear-gradient(135deg, #22c55e, #16a34a); + color: #fff; + font-size: 14px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.arc-client-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; +} + +.arc-client-name { + font-size: 13px; + font-weight: 700; + color: #166534; +} + +.arc-client-sub { + font-size: 11px; + color: #15803d; +} + +.arc-selected-badge { + font-size: 10px; + font-weight: 700; + padding: 3px 10px; + border-radius: 999px; + background: $success; + color: #fff; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +// ── Share percentage ────────────────────────────────────────────────────────── +.arc-share-row { + display: flex; + align-items: flex-start; + gap: 12px; +} + +.arc-share-field { + flex: 0 0 160px; +} + +.arc-percent-suffix { + font-size: 15px; + font-weight: 600; + color: $text-muted; + margin-right: 4px; +} + +.arc-share-hints { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding-top: 10px; +} + +.arc-hint-chip { + padding: 5px 12px; + border-radius: 8px; + border: 1.5px solid $border; + background: #fff; + font-size: 12px; + font-weight: 600; + color: $text-muted; + cursor: pointer; + transition: + border-color 0.12s, + background 0.12s, + color 0.12s; + + &:hover { + border-color: $primary; + background: $primary-light; + color: $primary; + } +} + +// ── Actions ─────────────────────────────────────────────────────────────────── +.arc-actions { + padding: 12px 20px 16px !important; + border-top: 1px solid $border; + gap: 10px; + justify-content: flex-end !important; +} + +.arc-btn-cancel { + border-radius: 8px !important; +} + +.arc-btn-add { + border-radius: 8px !important; + padding: 0 18px !important; + gap: 4px; + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + } +} diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.ts b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.ts new file mode 100644 index 000000000..60c96d69a --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.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, inject, OnDestroy, ViewEncapsulation } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; +import { MatDialogRef, MatDialogContent, MatDialogActions } from '@angular/material/dialog'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { MatIcon } from '@angular/material/icon'; +import { ClientsService } from 'app/clients/clients.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-add-related-client-dialog', + templateUrl: './add-related-client-dialog.component.html', + styleUrl: './add-related-client-dialog.component.scss', + encapsulation: ViewEncapsulation.None, + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatDialogContent, + MatDialogActions, + MatProgressSpinner, + MatIcon, + ReactiveFormsModule + ] +}) +export class AddRelatedClientDialogComponent implements OnDestroy { + private fb = inject(UntypedFormBuilder); + private clientsService = inject(ClientsService); + dialogRef = inject(MatDialogRef); + + form: UntypedFormGroup = this.fb.group({ + searchText: [''], + sharePercentage: [ + null, + [ + Validators.required, + Validators.min(1), + Validators.max(100) + ] + ] + }); + + searchResults: any[] = []; + selectedClient: any = null; + isSearching = false; + private searchTimer: any = null; + + onSearchInput(value: string): void { + if (this.searchTimer) clearTimeout(this.searchTimer); + this.selectedClient = null; + if (!value || value.trim().length < 2) { + this.searchResults = []; + return; + } + this.isSearching = true; + this.searchTimer = setTimeout(() => { + this.clientsService.searchByText(value.trim(), 0, 10).subscribe({ + next: (res: any) => { + this.searchResults = res?.content || []; + this.isSearching = false; + }, + error: () => { + this.searchResults = []; + this.isSearching = false; + } + }); + }, 400); + } + + selectClient(client: any): void { + this.selectedClient = client; + this.form.patchValue({ searchText: client.displayName }); + this.searchResults = []; + } + + clearSelection(): void { + this.selectedClient = null; + this.form.patchValue({ searchText: '' }); + this.searchResults = []; + } + + confirm(): void { + if (!this.selectedClient || this.form.get('sharePercentage').invalid) return; + this.dialogRef.close({ + clientId: this.selectedClient.id, + clientName: this.selectedClient.displayName, + clientCif: this.selectedClient.cifNumber || this.selectedClient.accountNumber || '', + sharePercentage: this.form.get('sharePercentage').value + }); + } + + cancel(): void { + this.dialogRef.close(null); + } + + ngOnDestroy(): void { + if (this.searchTimer) clearTimeout(this.searchTimer); + } +} diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html index 46882eb22..68adf30a0 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html @@ -131,6 +131,208 @@ } } + + @if (isNonIndividualClient && currentAccountProductSelected) { +
+ +
+
+
+ +
+
+

Non-Individual Account Details

+ As per RBI / Indian banking norms for non-individual entities +
+
+ Entity Account +
+ + +
+
+
+ A +
+ Partners / Members + Authorised persons linked to this entity account +
+
+ +
+ + @if (relatedClients.length > 0) { +
+ + + + + + + + + + + + + @for (c of relatedClients; track c; let i = $index) { + + + + + + + + + } + + + + + + + + + +
Sr.Partner NameCIF No.Ownership %Signing AuthorityAction
{{ i + 1 }} +
+
+ {{ c.clientName[0].toUpperCase() }} +
+
+ {{ c.clientName }} + @if (c.isMandatory) { + Mandatory Signatory + } +
+
+
+ {{ c.clientCif || '—' }} + + + {{ c.sharePercentage }}% + + + + + +
Total Ownership + + {{ totalSharePercentage }}% + + + + lock {{ mandatoryCount }} Mandatory + +
+
+ @if (totalSharePercentage !== 100) { +
+ warning_amber + Total ownership is {{ totalSharePercentage }}%. It must equal 100%. +
+ } @else { +
+ check_circle + Ownership allocation is complete (100%). +
+ } + } @else { +
+ group_add + No partners added yet. Click Add Partner to begin. +
+ } +
+ + +
+
+
+ B +
+ Mode of Operation / Signatory Rights + Auto-computed from Mandatory partners — you can override manually +
+
+ @if (signatoryRights) { + auto_awesome Auto + } +
+ + @if (relatedClients.length === 0) { +
+ how_to_vote + Add partners in Section A and set Mandatory/Optional to auto-compute the mode. +
+ } @else { +
+ @for (opt of signatoryRightsOptions; track opt.value) { + + } +
+ +
+ @if (signatoryRights) { + how_to_vote + Mode of Operation: {{ selectedSignatoryLabel }} + + ({{ mandatoryCount }} Mandatory + {{ relatedClients.length - mandatoryCount }} Optional partner(s)) + + } @else { + info + Set Mandatory/Optional for partners above to auto-select mode. + } +
+ } +
+
+ } +
{{ 'labels.inputs.Amount Range' | translate }} {{ chartSlab.amountRangeFrom | formatNumber }} - {{ chartSlab.amountRangeTo | formatNumber }} {{ 'labels.inputs.Interest' | translate }} {{ chartSlab.annualInterestRate | formatNumber }} % {{ 'labels.inputs.Description' | translate }} {{ chartSlab.description }} {{ 'labels.inputs.Actions' | translate }} @@ -396,7 +526,6 @@
+ + + + + + + + + + + + + + + + + + + + + + + + + @for (h of jointHolders; track h; let i = $index) { + + + + + + + + + + } + + + + + + + + + + +
Sr.Holder NameCIF No.Ownership %Signing AuthorityRoleAction
1 +
+
+ {{ primaryClientName ? primaryClientName[0].toUpperCase() : '?' }} +
+
+ {{ primaryClientName || 'Primary Holder' }} + Primary Account Holder +
+
+
{{ primaryClientCif || '—' }} + + % + + + lock Mandatory + + Primary Account Holder—
{{ i + 2 }} +
+
+ {{ h.clientName[0].toUpperCase() }} +
+
+ {{ h.clientName }} + @if (h.isMandatory) { + Mandatory Signatory + } +
+
+
{{ h.clientCif || '—' }} + + {{ h.sharePercentage }}% + + + + Co-Holder + +
Total Ownership + + {{ totalJointSharePercentage }}% + + + + lock {{ mandatoryJointCount + 1 }} Mandatory + +
+
+ + @if (jointHolders.length === 0) { +
+ group_add + Click Add Holder to add joint account holders. +
+ } + + @if (totalJointSharePercentage !== 100) { +
+ warning_amber + Total ownership is {{ totalJointSharePercentage }}%. It must equal 100%. +
+ } @else { +
+ check_circle + Ownership allocation is complete (100%). +
+ } +

+ + +
+
+
+ B +
+ Mode of Operation + Auto-set from mandatory holders — override manually if required +
+
+ @if (modeOfOperation) { + auto_awesome Auto + } +
+ + @if (jointHolders.length === 0) { +
+ how_to_vote + Add joint holders in Section A first. +
+ } @else { +
+ @for (opt of modeOfOperationOptions; track opt.value) { + + } +
+ +
+ @if (modeOfOperation) { + how_to_vote + Mode of Operation: {{ selectedModeLabel }} + + ({{ mandatoryJointCount }} Mandatory + {{ jointHolders.length - mandatoryJointCount }} Optional) + + } @else { + info + Set Mandatory/Optional for holders above to auto-select mode. + } +
+ } +
+ } + + + @if (selectedAccountStatus === 'MINOR') { +
+
+
+ + child_care + +
+ Minor Account Details + As per RBI — guardian must be an existing registered client +
+
+ Guardian Required +
+ +
+ + Minor's Date of Birth + + + + @if (clientDob) { + Auto-filled from client profile + } + + + + Age + + Must be below 18 years + + + + Date of Majority (Auto) + + Auto-calculated: DOB + 18 years + + + + Guardian Relationship + + @for (rel of guardianRelationshipOptions; track rel) { + {{ rel }} + } + + +
+ + + +
+ info +
    +
  • Minor below 10 years — Guardian must operate the account.
  • +
  • Minor above 10 years — Can operate independently (bank discretion).
  • +
  • Account must be converted to regular savings on attaining majority.
  • +
  • Fresh KYC of the minor must be done on majority.
  • +
+
+
+ } + + + @if (selectedAccountStatus === 'SENIOR_CITIZEN') { +
+
+
+ + elderly + +
+ Senior Citizen Account + Age ≥ 60 years — eligible for preferential banking services +
+
+ Age ≥ 60 +
+ +
+ + Date of Birth + + From client profile + + + + Age + + Age ≥ 60 required for Senior Citizen + +
+ +
+
+ priority_high +
+ Priority Service + Dedicated counters / assisted banking at branches +
+
+
+ percent +
+ Preferential Interest Rate + Additional 0.25%–0.50% on deposits (as per product) +
+
+
+ health_and_safety +
+ Doorstep Banking + Eligible for home banking services (RBI circular) +
+
+
+ gavel +
+ Nomination Mandatory + RBI mandate — ensure nomination is registered +
+
+
+ +
+ info +
    +
  • Age proof (DOB document) mandatory at time of account opening.
  • +
  • Nomination registration is compulsory (Form DA-1).
  • +
  • Bank must offer dedicated helpline and priority grievance redressal.
  • +
  • Door-step banking services must be offered (RBI Master Direction 2023).
  • +
+
+
+ } + + + @if (selectedAccountStatus === 'INDIVIDUAL_MALE') { +
+
+
+ + person + +
+ Individual Account (Male) + Select special category if applicable — governs applicable schemes & benefits +
+
+ Special Category +
+ +
+ @for (opt of specialCategoryOptions; track opt.value) { + + } +
+ +
+ info +
    +
  • Valid KYC documents required (Aadhaar / PAN / Passport).
  • +
  • Nomination registration is mandatory (Form DA-1).
  • +
  • Defence / Ex-Serviceman — eligible for special savings schemes (DSSP).
  • +
  • NRI — FCNR / NRE / NRO account rules apply as per FEMA guidelines.
  • +
+
+
+ } + + + @if (selectedAccountStatus === 'INDIVIDUAL_FEMALE') { +
+
+
+ + person_outline + +
+ Individual Account (Female) + Select weaker section category if applicable — governs PMJDY & priority schemes +
+
+ Weaker Section +
+ +
+ @for (opt of weakerSectionOptions; track opt.value) { + + } +
+ +
+ info +
    +
  • Valid KYC documents required (Aadhaar / PAN / Passport).
  • +
  • Nomination registration is mandatory (Form DA-1).
  • +
  • BPL / SC-ST / Minority — eligible for zero-balance accounts (RBI Priority Sector norms).
  • +
  • PMJDY beneficiary — ₹10,000 overdraft facility & accident insurance cover applicable.
  • +
  • SHG members — group linkage lending & special deposit rates applicable.
  • +
+
+
+ } + + } + @if (isNonIndividualClient && currentAccountProductSelected) {
diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.scss b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.scss index bcad57a8c..859832e06 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.scss +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.scss @@ -712,6 +712,941 @@ $ni-bg: #f9fafb; } } +// ── Status of Account (RBI) ─────────────────────────────────────────────── + +.soa-card { + margin-top: 16px; + border: 1px solid #e2e8f0; + border-radius: 10px; + background: #fff; + overflow: visible; + + &__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 24px; + background: #f8f9fa; + border-bottom: 1px solid #e8eaed; + border-radius: 10px 10px 0 0; + } + + &__header-left { + display: flex; + align-items: center; + gap: 12px; + } + + &__icon { + width: 36px; + height: 36px; + border-radius: 8px; + background: #ede9fe; + display: flex; + align-items: center; + justify-content: center; + + i { + font-size: 16px; + color: #7c3aed; + } + } + + &__title { + margin: 0; + font-size: 14px; + font-weight: 600; + color: #202124; + } + + &__sub { + font-size: 11px; + color: #6b7280; + display: block; + } +} + +.soa-pill { + display: inline-flex; + align-items: center; + padding: 3px 12px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + background: #ede9fe; + color: #7c3aed; + border: 1px solid #ddd6fe; + + &--minor { + background: #fef3c7; + color: #b45309; + border-color: #fde68a; + } + + &--senior { + background: #d1fae5; + color: #065f46; + border-color: #a7f3d0; + } + + &--ok { + background: #dcfce7; + color: #15803d; + border-color: #bbf7d0; + } +} + +.soa-client-info-bar { + display: flex; + align-items: center; + gap: 20px; + padding: 10px 24px; + background: #f0f9ff; + border-bottom: 1px solid #bae6fd; + flex-wrap: wrap; + + &__item { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + color: #0369a1; + + mat-icon { + font-size: 16px; + height: 16px; + width: 16px; + } + + strong { + font-weight: 700; + } + } +} + +.soa-auto-pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + margin-left: auto; + + mat-icon { + font-size: 13px; + height: 13px; + width: 13px; + } + + &--minor { + background: #fef3c7; + color: #b45309; + border: 1px solid #fde68a; + } + + &--senior { + background: #d1fae5; + color: #065f46; + border: 1px solid #a7f3d0; + } + + &--ok { + background: #dcfce7; + color: #15803d; + border: 1px solid #bbf7d0; + } +} + +.soa-tiles { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 12px; + padding: 16px 24px; + border-bottom: 1px solid #f1f5f9; + + @media (width <= 900px) { + grid-template-columns: repeat(3, 1fr); + } + + @media (width <= 600px) { + grid-template-columns: repeat(2, 1fr); + } +} + +.soa-tile { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 14px 10px; + border: 2px solid #e2e8f0; + border-radius: 10px; + background: #fff; + cursor: pointer; + transition: + border-color 0.15s, + box-shadow 0.15s; + position: relative; + + &:hover { + border-color: #a5b4fc; + box-shadow: 0 2px 8px rgb(99 102 241 / 12%); + } + + &--selected { + border-color: #6366f1; + background: #eef2ff; + box-shadow: 0 2px 12px rgb(99 102 241 / 20%); + } + + &__icon { + font-size: 28px; + height: 28px; + width: 28px; + } + + &__label { + font-size: 12px; + font-weight: 600; + color: #374151; + text-align: center; + } + + &__check { + position: absolute; + top: 6px; + right: 6px; + font-size: 16px; + height: 16px; + width: 16px; + color: #6366f1; + } + + &__lock { + position: absolute; + top: 4px; + right: 4px; + display: flex; + align-items: center; + + mat-icon { + font-size: 13px; + height: 13px; + width: 13px; + color: #94a3b8; + } + } + + &--disabled { + opacity: 0.45; + cursor: not-allowed; + background: #f8fafc; + border-color: #e2e8f0; + box-shadow: none; + + &:hover { + border-color: #e2e8f0; + box-shadow: none; + } + } +} + +.soa-block { + padding: 16px 24px; + + &--sep { + border-top: 1px dashed #e2e8f0; + } + + &__head { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 14px; + } + + &__head-left { + display: flex; + align-items: flex-start; + gap: 12px; + } + + &__letter { + width: 28px; + height: 28px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + font-weight: 700; + color: #fff; + flex-shrink: 0; + margin-top: 2px; + } + + &__title { + font-size: 13px; + font-weight: 600; + color: #1f2937; + display: block; + } + + &__hint { + font-size: 11px; + color: #6b7280; + display: block; + } +} + +.soa-table-wrap { + overflow-x: auto; + border-radius: 8px; + border: 1px solid #e2e8f0; +} + +.soa-tbl { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.soa-th { + padding: 10px 14px; + background: #f8fafc; + text-align: left; + font-weight: 600; + font-size: 11px; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.04em; + border-bottom: 1px solid #e2e8f0; + + &--sr { + width: 44px; + } + + &--center { + text-align: center; + } +} + +.soa-td { + padding: 10px 14px; + border-bottom: 1px solid #f1f5f9; + vertical-align: middle; + + &--sr { + text-align: center; + font-size: 12px; + color: #94a3b8; + font-weight: 600; + } + + &--center { + text-align: center; + } + + &--total-label { + font-size: 12px; + font-weight: 600; + color: #475569; + text-align: right; + } +} + +.soa-tr { + transition: background 0.1s; + + &:hover { + background: #f8fafc; + } + + &--mandatory { + background: #fdf4ff; + } + + &--primary { + background: #f0f9ff; + border-left: 3px solid #0891b2; + } +} + +.soa-avatar--primary { + background: #cffafe; + color: #0e7490; +} + +.soa-primary-tag { + font-size: 10px; + color: #0e7490; + font-weight: 600; + background: #cffafe; + padding: 1px 7px; + border-radius: 999px; + display: inline-block; +} + +.soa-pct-input { + width: 56px; + padding: 4px 6px; + border: 1px solid #cbd5e1; + border-radius: 6px; + font-size: 13px; + font-weight: 600; + text-align: center; + color: #1f2937; + + &:focus { + outline: none; + border-color: #0891b2; + box-shadow: 0 0 0 2px rgb(8 145 178 / 15%); + } + + &::-webkit-inner-spin-button, + &::-webkit-outer-spin-button { + opacity: 1; + } +} + +.soa-pct-unit { + font-size: 13px; + font-weight: 600; + color: #64748b; + margin-left: 2px; +} + +.soa-no-action { + color: #cbd5e1; + font-size: 16px; +} + +.soa-tr-total { + background: #f8fafc; +} + +.soa-client-cell { + display: flex; + align-items: center; + gap: 10px; +} + +.soa-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background: #dbeafe; + color: #1d4ed8; + font-weight: 700; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + &--mandatory { + background: #ede9fe; + color: #7c3aed; + } +} + +.soa-client-name { + font-weight: 600; + font-size: 13px; + color: #1f2937; + display: block; +} + +.soa-mandatory-tag { + font-size: 10px; + color: #7c3aed; + font-weight: 600; + background: #ede9fe; + padding: 1px 7px; + border-radius: 999px; +} + +.soa-mono { + font-family: 'Roboto Mono', monospace; + font-size: 12px; + color: #64748b; +} + +.soa-pct-badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 2px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 700; + background: #f1f5f9; + color: #475569; + + &--full, + &--ok { + background: #dcfce7; + color: #15803d; + } + + &--warn { + background: #fef3c7; + color: #b45309; + } +} + +.soa-sign-toggle { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + border: 1px solid #e2e8f0; + cursor: pointer; + transition: all 0.15s; + + mat-icon { + font-size: 14px; + height: 14px; + width: 14px; + } + + &--mandatory { + background: #ede9fe; + color: #7c3aed; + border-color: #ddd6fe; + } + + &--optional { + background: #f8fafc; + color: #94a3b8; + } +} + +.soa-mandatory-count { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 12px; + font-weight: 600; + color: #7c3aed; + + mat-icon { + font-size: 14px; + height: 14px; + width: 14px; + } +} + +.soa-alert { + display: flex; + align-items: center; + gap: 8px; + margin-top: 10px; + padding: 8px 14px; + border-radius: 8px; + font-size: 13px; + font-weight: 500; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + } + + &--warn { + background: #fef3c7; + color: #b45309; + } + + &--ok { + background: #dcfce7; + color: #15803d; + } +} + +.soa-empty { + display: flex; + align-items: center; + gap: 10px; + padding: 20px; + background: #f8fafc; + border-radius: 8px; + font-size: 13px; + color: #64748b; + + mat-icon { + color: #94a3b8; + font-size: 22px; + height: 22px; + width: 22px; + } +} + +.soa-add-btn { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; +} + +.soa-mode-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px; + + @media (width <= 700px) { + grid-template-columns: repeat(2, 1fr); + } +} + +.soa-mode-card { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 12px 8px; + border: 2px solid #e2e8f0; + border-radius: 10px; + background: #fff; + cursor: pointer; + transition: all 0.15s; + + &:hover { + border-color: #a5b4fc; + } + + &--selected { + border-color: #7c3aed; + background: #faf5ff; + } + + &__icon { + font-size: 24px; + height: 24px; + width: 24px; + color: #7c3aed; + } + + &__code { + font-size: 13px; + font-weight: 700; + color: #1f2937; + } + + &__desc { + font-size: 11px; + color: #6b7280; + text-align: center; + } + + &__check { + position: absolute; + top: 6px; + right: 6px; + font-size: 16px; + height: 16px; + width: 16px; + color: #7c3aed; + } +} + +.soa-mode-summary { + display: flex; + align-items: center; + gap: 8px; + margin-top: 12px; + padding: 8px 14px; + border-radius: 8px; + font-size: 13px; + background: #f8fafc; + color: #64748b; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + } + + &--set { + background: #faf5ff; + color: #7c3aed; + } + + &__from { + font-size: 11px; + color: #94a3b8; + } +} + +.soa-auto-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + background: #fef9c3; + color: #854d0e; + border: 1px solid #fde68a; + + mat-icon { + font-size: 13px; + height: 13px; + width: 13px; + } +} + +.soa-minor-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 4px 16px; + margin-bottom: 16px; + + @media (width <= 700px) { + grid-template-columns: 1fr; + } +} + +.soa-field { + width: 100%; +} + +.soa-guardian-search { + margin-bottom: 12px; + + &__label { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 600; + color: #374151; + margin-bottom: 8px; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + color: #7c3aed; + } + } +} + +.soa-guardian-selected { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 14px; + background: #f0fdf4; + border: 1px solid #bbf7d0; + border-radius: 8px; + margin-top: 10px; + + &__icon { + font-size: 24px; + height: 24px; + width: 24px; + color: #16a34a; + } + + &__name { + font-size: 13px; + font-weight: 600; + color: #1f2937; + } + + &__cif { + font-size: 11px; + color: #6b7280; + } +} + +.soa-sc-benefits { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; + margin-bottom: 14px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.soa-sc-benefit { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px; + background: #f0fdf4; + border: 1px solid #bbf7d0; + border-radius: 8px; + + mat-icon { + color: #16a34a; + font-size: 20px; + height: 20px; + width: 20px; + flex-shrink: 0; + margin-top: 2px; + } + + strong { + display: block; + font-size: 13px; + color: #1f2937; + font-weight: 600; + } + + span { + font-size: 11px; + color: #6b7280; + } +} + +.soa-category-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; + margin-bottom: 16px; + + @media (width <= 700px) { + grid-template-columns: repeat(2, 1fr); + } +} + +.soa-cat-tile { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 14px 10px; + border: 2px solid #e2e8f0; + border-radius: 10px; + background: #fff; + cursor: pointer; + transition: all 0.15s; + + &:hover { + border-color: #a5b4fc; + box-shadow: 0 2px 8px rgb(99 102 241 / 12%); + } + + &--selected { + &.soa-cat-tile--male { + border-color: #3b82f6; + background: #eff6ff; + box-shadow: 0 2px 12px rgb(59 130 246 / 20%); + } + + &.soa-cat-tile--female { + border-color: #ec4899; + background: #fdf2f8; + box-shadow: 0 2px 12px rgb(236 72 153 / 20%); + } + } + + &__icon { + font-size: 26px; + height: 26px; + width: 26px; + color: #64748b; + } + + &--selected.soa-cat-tile--male &__icon { + color: #3b82f6; + } + + &--selected.soa-cat-tile--female &__icon { + color: #ec4899; + } + + &__label { + font-size: 11px; + font-weight: 600; + color: #374151; + text-align: center; + line-height: 1.3; + } + + &__check { + position: absolute; + top: 6px; + right: 6px; + font-size: 16px; + height: 16px; + width: 16px; + + .soa-cat-tile--male & { + color: #3b82f6; + } + + .soa-cat-tile--female & { + color: #ec4899; + } + } +} + +.soa-info-banner { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px 14px; + border-radius: 8px; + font-size: 12px; + background: #eff6ff; + border: 1px solid #bfdbfe; + color: #1e40af; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + flex-shrink: 0; + margin-top: 2px; + } + + ul { + margin: 4px 0 0; + padding-left: 18px; + + li { + margin-bottom: 3px; + } + } + + &--minor { + background: #fefce8; + border-color: #fde68a; + color: #92400e; + } + + &--senior { + background: #f0fdf4; + border-color: #bbf7d0; + color: #065f46; + } + + &--male { + background: #eff6ff; + border-color: #bfdbfe; + color: #1e40af; + } + + &--female { + background: #fdf2f8; + border-color: #f9a8d4; + color: #9d174d; + } +} + // ── Account Feature Toggles ──────────────────────────────────────────────── .sad-toggles { display: flex; diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts index 205a267cc..3cc5083de 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts @@ -8,7 +8,7 @@ /** Angular Imports */ import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorRef } from '@angular/core'; -import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule, FormsModule } from '@angular/forms'; import { Dates } from 'app/core/utils/dates'; /** Custom Services */ @@ -17,20 +17,7 @@ import { SettingsService } from 'app/settings/settings.service'; import { ClientsService } from 'app/clients/clients.service'; import { MatDialog } from '@angular/material/dialog'; import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper'; -import { - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow -} from '@angular/material/table'; import { MatIcon } from '@angular/material/icon'; -import { FormsModule } from '@angular/forms'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component'; @@ -49,16 +36,6 @@ import { AddRelatedClientDialogComponent } from './add-related-client-dialog/add FaIconComponent, MatStepperNext, IntroducerSearchComponent, - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow, MatIcon, FormsModule ] @@ -72,20 +49,13 @@ export class CurrentAccountDetailsStepComponent implements OnInit { private dateUtils = inject(Dates); private cdr = inject(ChangeDetectorRef); - /** Current Account Template */ @Input() currentAccountTemplate: any; - /** Minimum date allowed. */ minDate = new Date(2000, 0, 1); - /** Maximum date allowed. */ maxDate = new Date(); - /** Product Data */ productData: any; - /** Field Officer Data */ fieldOfficerData: any; - /** For edit current account form */ isFieldOfficerPatched = false; - /** Current Account Details Form */ currentAccountDetailsForm: UntypedFormGroup; currentAccountProductSelected = false; @@ -93,7 +63,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit { introducerName: string | null = null; introducerCif: string | null = null; - /** Non-Individual section */ + // ── Non-Individual (existing) ────────────────────────────────────────── isNonIndividualClient = false; relatedClients: Array<{ clientId: number; @@ -111,16 +81,86 @@ export class CurrentAccountDetailsStepComponent implements OnInit { { value: 'ONLY_ONE', label: 'Single', sublabel: 'Only 1 Person', icon: 'person' } ]; - /** Current Account Template with product data */ @Output() currentAccountProductTemplate = new EventEmitter(); - /** Local copy of product template for displaying classification info */ productTemplate: any = null; accountTypeOptions: string[] = []; customerSegmentOptions: string[] = []; selectedAccountType: string | null = null; selectedCustomerSegment: string | null = null; + // ── Client info (auto-fetched) ───────────────────────────────────────── + clientDob: Date | null = null; + clientAge: number | null = null; + clientGender: string | null = null; + primaryClientName = ''; + primaryClientCif = ''; + primaryClientId: number | null = null; + primaryHolderShare = 0; + + // ── Status of Account (RBI) ──────────────────────────────────────────── + readonly accountStatusOptions = [ + { value: 'INDIVIDUAL_MALE', label: 'Individual (Male)', icon: 'person', color: '#3b82f6' }, + { value: 'INDIVIDUAL_FEMALE', label: 'Individual (Female)', icon: 'person_outline', color: '#ec4899' }, + { value: 'SINGLE', label: 'Single Account', icon: 'account_circle', color: '#0891b2' }, + { value: 'JOINT', label: 'Joint Account', icon: 'group', color: '#8b5cf6' }, + { value: 'MINOR', label: 'Minor Account', icon: 'child_care', color: '#f59e0b' }, + { value: 'SENIOR_CITIZEN', label: 'Senior Citizen', icon: 'elderly', color: '#10b981' } + ]; + + selectedAccountStatus = ''; + + // ── Joint Account (Status of Account) ───────────────────────────────── + jointHolders: Array<{ + clientId: number; + clientName: string; + clientCif: string; + sharePercentage: number; + isMandatory: boolean; + }> = []; + + modeOfOperation = ''; + + readonly modeOfOperationOptions = [ + { value: 'EITHER_OR_SURVIVOR', label: 'E or S', sublabel: 'Either or Survivor', icon: 'how_to_vote' }, + { value: 'FORMER_OR_SURVIVOR', label: 'F or S', sublabel: 'Former or Survivor', icon: 'person' }, + { value: 'JOINTLY', label: 'Jointly', sublabel: 'All Must Sign', icon: 'groups' }, + { value: 'ANYONE_OR_SURVIVOR', label: 'Anyone', sublabel: 'Anyone or Survivor', icon: 'group' } + ]; + + // ── Individual Male — Special Category ──────────────────────────────── + selectedSpecialCategory = ''; + + readonly specialCategoryOptions = [ + { value: 'DEFENCE', label: 'Defence Personnel', icon: 'military_tech' }, + { value: 'EX_SERVICEMAN', label: 'Ex-Serviceman', icon: 'shield' }, + { value: 'GOVT_EMPLOYEE', label: 'Government Employee', icon: 'account_balance' }, + { value: 'PHYSICALLY_CHALLENGED', label: 'Physically Challenged', icon: 'accessible' }, + { value: 'NRI', label: 'NRI (Non-Resident Indian)', icon: 'flight' }, + { value: 'NONE', label: 'None / General', icon: 'person' } + ]; + + // ── Individual Female — Weaker Section ──────────────────────────────── + selectedWeakerSection = ''; + + readonly weakerSectionOptions = [ + { value: 'BPL', label: 'BPL (Below Poverty Line)', icon: 'volunteer_activism' }, + { value: 'SC_ST', label: 'SC / ST Category', icon: 'groups' }, + { value: 'MINORITY', label: 'Minority Community', icon: 'diversity_3' }, + { value: 'SHG', label: 'Self Help Group Member', icon: 'handshake' }, + { value: 'PMJDY', label: 'PMJDY Beneficiary', icon: 'savings' }, + { value: 'NONE', label: 'None / General', icon: 'person_outline' } + ]; + + // ── Minor Account ────────────────────────────────────────────────────── + guardianClientId: number | null = null; + guardianName = ''; + guardianCif = ''; + guardianRelationship = ''; + minorDob: Date | null = null; + + readonly guardianRelationshipOptions = ['Father', 'Mother', 'Legal Guardian', 'Court Appointed Guardian']; + constructor() { this.createCurrentAccountDetailsForm(); } @@ -128,9 +168,23 @@ export class CurrentAccountDetailsStepComponent implements OnInit { ngOnInit() { this.maxDate = this.settingsService.businessDate; this.buildDependencies(); - if (this.currentAccountTemplate?.clientId) { - this.clientsService.getClientData(String(this.currentAccountTemplate.clientId)).subscribe((client: any) => { + + const clientId = this.currentAccountTemplate?.clientId; + if (clientId) { + this.primaryClientId = clientId; + this.clientsService.getClientData(String(clientId)).subscribe((client: any) => { this.isNonIndividualClient = client?.legalForm?.id === 2; + this.primaryClientName = client?.displayName ?? client?.fullname ?? ''; + this.primaryClientCif = client?.cifNumber ?? client?.cifNo ?? ''; + if (client?.dateOfBirth) { + const dob = this.parseClientDob(client.dateOfBirth); + if (dob) { + this.clientDob = dob; + this.clientAge = this.calculateAge(dob); + this.clientGender = client.gender?.name?.toLowerCase() ?? null; + this.autoDetectAccountStatus(); + } + } this.cdr.detectChanges(); }); } @@ -153,39 +207,27 @@ export class CurrentAccountDetailsStepComponent implements OnInit { } } - /** - * Creates current account details form. - */ createCurrentAccountDetailsForm() { this.currentAccountDetailsForm = this.formBuilder.group({ - productId: [ - '', - Validators.required - ], - submittedOnDate: [ - '', - Validators.required - ], + productId: ['', Validators.required], + submittedOnDate: ['', Validators.required], fieldOfficerId: [''], externalId: [''], balanceSwapOption: [false], msme: [false], - autoDebitFacility: [false] + autoDebitFacility: [false], + establishmentDate: [''], + startOperationDate: [''] }); } - /** - * Fetches current account product template on productId value changes - */ buildDependencies() { const entityId = this.currentAccountTemplate.groupId || this.currentAccountTemplate.clientId; this.currentAccountDetailsForm.get('productId').valueChanges.subscribe((productId: string) => { this.currentAccountsService .getCurrentAccountTemplate(entityId, productId, this.currentAccountTemplate.groupId ? true : false) .subscribe((response: any) => { - // Ensure currency is included in the product template if (!response.currency) { - // Try to get currency from the selected product const selectedProduct = this.currentAccountTemplate.productOptions?.find( (product: any) => product.id == productId ); @@ -203,7 +245,6 @@ export class CurrentAccountDetailsStepComponent implements OnInit { this.currentAccountProductTemplate.emit(response); this.fieldOfficerData = response.fieldOfficerOptions; this.currentAccountProductSelected = true; - // Pre-populate toggles from product defaults (user can override) this.currentAccountDetailsForm.patchValue({ balanceSwapOption: !!response.balanceSwapOption, msme: !!response.msme, @@ -220,9 +261,6 @@ export class CurrentAccountDetailsStepComponent implements OnInit { }); } - /** - * Formats template dates into the current CBS date format. - */ private extractOptions(options: any[], fallback?: string): string[] { if (Array.isArray(options) && options.length > 0) { return options.map((o: any) => o.value ?? o.name ?? o); @@ -231,9 +269,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit { } formatTemplateDate(value: any): string { - if (!value) { - return ''; - } + if (!value) return ''; let dateValue: Date | null = null; if (value instanceof Date) { dateValue = value; @@ -248,15 +284,123 @@ export class CurrentAccountDetailsStepComponent implements OnInit { return this.dateUtils.formatDate(dateValue, 'M/d/yyyy') || ''; } - /** - * Returns current account form value. - */ + // ── Client DOB helpers ───────────────────────────────────────────────── + private parseClientDob(dob: any): Date | null { + if (!dob) return null; + if (Array.isArray(dob) && dob.length >= 3) return new Date(dob[0], dob[1] - 1, dob[2]); + const d = new Date(dob); + return isNaN(d.getTime()) ? null : d; + } + + private calculateAge(dob: Date): number { + const today = this.settingsService.businessDate ?? new Date(); + let age = today.getFullYear() - dob.getFullYear(); + const m = today.getMonth() - dob.getMonth(); + if (m < 0 || (m === 0 && today.getDate() < dob.getDate())) age--; + return age; + } + + private autoDetectAccountStatus(): void { + if (this.clientAge === null) return; + if (this.clientAge < 18) { + this.selectedAccountStatus = 'MINOR'; + this.minorDob = this.clientDob; + } else if (this.clientAge >= 60) { + this.selectedAccountStatus = 'SENIOR_CITIZEN'; + } else if (this.clientGender === 'female') { + this.selectedAccountStatus = 'INDIVIDUAL_FEMALE'; + } else { + this.selectedAccountStatus = 'INDIVIDUAL_MALE'; + } + } + + isTileDisabled(value: string): boolean { + if (value === 'MINOR') return this.clientAge === null || this.clientAge >= 18; + if (value === 'SENIOR_CITIZEN') return this.clientAge === null || this.clientAge < 60; + return false; + } + + getTileTooltip(value: string): string { + if (value === 'MINOR' && this.clientAge !== null && this.clientAge >= 18) + return `Age ${this.clientAge} yrs — Minor only allowed below 18 years`; + if (value === 'SENIOR_CITIZEN' && this.clientAge !== null && this.clientAge < 60) + return `Age ${this.clientAge} yrs — Senior Citizen only allowed at 60+ years`; + if (value === 'MINOR' && this.clientAge === null) return 'DOB not available — cannot verify age'; + if (value === 'SENIOR_CITIZEN' && this.clientAge === null) return 'DOB not available — cannot verify age'; + return ''; + } + + // ── Introducer ───────────────────────────────────────────────────────── onIntroducerChange(event: { clientId: number; displayName: string; cifNumber?: string } | null): void { this.introducerId = event?.clientId ?? null; this.introducerName = event?.displayName ?? null; this.introducerCif = event?.cifNumber ?? null; } + onGuardianChange(event: { clientId: number; displayName: string; cifNumber?: string } | null): void { + this.guardianClientId = event?.clientId ?? null; + this.guardianName = event?.displayName ?? ''; + this.guardianCif = event?.cifNumber ?? ''; + } + + // ── Joint Account (Status of Account) ───────────────────────────────── + openAddJointHolderDialog(): void { + const ref = this.dialog.open(AddRelatedClientDialogComponent, { + width: '500px', + panelClass: 'arc-dialog-panel', + disableClose: true + }); + ref.afterClosed().subscribe((result) => { + if (result) { + const duplicate = this.jointHolders.some((h) => h.clientCif && h.clientCif === result.clientCif); + if (duplicate) { + alert(`Account holder with CIF ${result.clientCif} is already added.`); + return; + } + this.jointHolders = [...this.jointHolders, { ...result, isMandatory: false }]; + this.autoSetModeOfOperation(); + } + }); + } + + removeJointHolder(index: number): void { + this.jointHolders = this.jointHolders.filter((_, i) => i !== index); + this.autoSetModeOfOperation(); + } + + toggleJointHolderMandatory(holder: any): void { + holder.isMandatory = !holder.isMandatory; + this.autoSetModeOfOperation(); + } + + autoSetModeOfOperation(): void { + const mandatory = this.jointHolders.filter((h) => h.isMandatory).length; + if (mandatory === 0) this.modeOfOperation = 'EITHER_OR_SURVIVOR'; + else if (mandatory === 1) this.modeOfOperation = 'FORMER_OR_SURVIVOR'; + else this.modeOfOperation = 'JOINTLY'; + } + + get totalJointSharePercentage(): number { + return (this.primaryHolderShare || 0) + this.jointHolders.reduce((sum, h) => sum + (h.sharePercentage || 0), 0); + } + + get mandatoryJointCount(): number { + return this.jointHolders.filter((h) => h.isMandatory).length; + } + + get selectedModeLabel(): string { + return this.modeOfOperationOptions.find((o) => o.value === this.modeOfOperation)?.sublabel ?? ''; + } + + get majorityDate(): string { + const dob = this.minorDob ?? this.clientDob; + if (!dob) return ''; + const d = new Date(dob); + d.setFullYear(d.getFullYear() + 18); + return this.dateUtils.formatDate(d, 'M/d/yyyy') || ''; + } + + // ── Non-Individual (existing) helpers ────────────────────────────────── openAddRelatedClientDialog(): void { const ref = this.dialog.open(AddRelatedClientDialogComponent, { width: '500px', @@ -270,9 +414,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit { alert(`Partner with CIF ${result.clientCif} is already added.`); return; } - this.relatedClients = [ - ...this.relatedClients, - { ...result, isMandatory: false }]; + this.relatedClients = [...this.relatedClients, { ...result, isMandatory: false }]; this.autoSetSignatoryRights(); } }); @@ -288,16 +430,11 @@ export class CurrentAccountDetailsStepComponent implements OnInit { this.autoSetSignatoryRights(); } - /** Auto-compute Mode of Operation based on mandatory count */ autoSetSignatoryRights(): void { const mandatory = this.relatedClients.filter((c) => c.isMandatory).length; - if (mandatory === 0) { - this.signatoryRights = 'ANYONE'; - } else if (mandatory === 1) { - this.signatoryRights = 'ONE_MANDATORY'; - } else { - this.signatoryRights = 'TWO_MANDATORY'; - } + if (mandatory === 0) this.signatoryRights = 'ANYONE'; + else if (mandatory === 1) this.signatoryRights = 'ONE_MANDATORY'; + else this.signatoryRights = 'TWO_MANDATORY'; } get totalSharePercentage(): number { @@ -329,6 +466,33 @@ export class CurrentAccountDetailsStepComponent implements OnInit { sharePercentage: c.sharePercentage, isMandatory: c.isMandatory })); + } else { + if (this.selectedAccountStatus) value.accountStatus = this.selectedAccountStatus; + if (this.selectedAccountStatus === 'INDIVIDUAL_MALE' && this.selectedSpecialCategory) + value.specialCategory = this.selectedSpecialCategory; + if (this.selectedAccountStatus === 'INDIVIDUAL_FEMALE' && this.selectedWeakerSection) + value.weakerSection = this.selectedWeakerSection; + if (this.selectedAccountStatus === 'JOINT') { + value.modeOfOperation = this.modeOfOperation || null; + value.jointHolders = [ + { + clientId: this.primaryClientId, + clientName: this.primaryClientName, + clientCif: this.primaryClientCif, + sharePercentage: this.primaryHolderShare, + isMandatory: true, + isPrimary: true + }, + ...this.jointHolders.map((h) => ({ ...h, isPrimary: false })) + ]; + } + if (this.selectedAccountStatus === 'MINOR') { + value.guardianClientId = this.guardianClientId; + value.guardianName = this.guardianName; + value.guardianCif = this.guardianCif; + value.guardianRelationship = this.guardianRelationship; + value.minorDob = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'yyyy-MM-dd') : null; + } } return value; } diff --git a/src/app/current-accounts/current-accounts.service.ts b/src/app/current-accounts/current-accounts.service.ts index 6267da6b3..b9721be8f 100644 --- a/src/app/current-accounts/current-accounts.service.ts +++ b/src/app/current-accounts/current-accounts.service.ts @@ -68,6 +68,11 @@ export class CurrentAccountsService { return this.http.get(`/currentaccounts/${accountId}`, { params: httpParams }); } + getClientCurrentAccounts(clientId: string): Observable { + const httpParams = new HttpParams().set('clientId', clientId); + return this.http.get('/currentaccounts', { params: httpParams }); + } + /** * @param accountId Current Account Id of account to get data for. * @returns {Observable} Current account and template. diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html index 30b71d413..57e6ae181 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html @@ -115,13 +115,8 @@ } - - Introducer Required - - -
- Share Linking Enabled + Deposit Account Link {{ productTemplate.shareLinkingEnabled ? 'Yes' : 'No' }} @@ -129,8 +124,652 @@
- @if (productTemplate?.introducerRequired) { - + + @if (productTemplate?.shareLinkingEnabled) { +
+
+
+
+ link +
+
+

Deposit Account Link

+ Link an existing Savings or Current account to this RD +
+
+
+ +
+ + @if (dalLoading) { +
+ hourglass_top + Loading accounts… +
+ } @else if (selectedLinkedAccount) { + + +
+
+ + {{ selectedLinkedAccount.accountType === 'SAVINGS' ? 'savings' : 'account_balance' }} + {{ selectedLinkedAccount.accountType === 'SAVINGS' ? 'Savings' : 'Current' }} + +
+ {{ selectedLinkedAccount.accountNo }} + {{ selectedLinkedAccount.productName }} +
+
+ Balance + {{ selectedLinkedAccount.currencyCode }} {{ selectedLinkedAccount.balance | number:'1.2-2' }} +
+
+ +
+ + } @else { + + + @if (!dalSelectedType) { +
+

Select account type to link:

+
+ + +
+
+ } @else { + + +
+
+ + + {{ dalSelectedType === 'SAVINGS' ? 'savings' : 'account_balance' }} + {{ dalSelectedType === 'SAVINGS' ? 'Savings Accounts' : 'Current Accounts' }} + +
+ + + Search by Account No. or Product + search + + @if (dalSearch) { + + } + + + @if (dalFilteredAccounts.length === 0) { +
+ search_off + No {{ dalSelectedType === 'SAVINGS' ? 'Savings' : 'Current' }} accounts found. +
+ } @else { +
+ @for (account of dalFilteredAccounts; track account.id) { +
+
+ {{ account.accountNo }} + {{ account.productName }} +
+
+ {{ account.status }} + {{ account.currencyCode }} {{ account.balance | number:'1.2-2' }} +
+ chevron_right +
+ } +
+ } +
+ } + } +
+
} } + + + @if (isProductSelected) { +
+
+
+
+ +
+
+

Status of Account

+ As per RBI guidelines — select account holder type +
+
+ RBI Compliant +
+ + + @if (clientDob) { +
+
+ cake + Date of Birth: {{ formatTemplateDate(clientDob) }} +
+
+ person_pin + Age: {{ clientAge }} years +
+ @if (clientAge !== null && clientAge < 18) { + + child_care Auto-detected: Minor + + } @else if (clientAge !== null && clientAge >= 60) { + + elderly Auto-detected: Senior Citizen + + } @else { + check_circle Age Verified + } +
+ } + + +
+ @for (opt of accountStatusOptions; track opt.value) { + + } +
+ + + @if (selectedAccountStatus === 'JOINT') { +
+
+
+ A +
+ Joint Account Holders + Add all co-applicants linked to this joint RD account +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + @for (h of jointHolders; track h; let i = $index) { + + + + + + + + + + } + + + + + + + + + + +
Sr.Holder NameCIF No.Ownership %Signing AuthorityRoleAction
1 +
+
+ {{ primaryClientName ? primaryClientName[0].toUpperCase() : '?' }} +
+
+ {{ primaryClientName || 'Primary Holder' }} + Primary Account Holder +
+
+
{{ primaryClientCif || '—' }} + + % + + + lock Mandatory + + Primary Account Holder—
{{ i + 2 }} +
+
+ {{ h.clientName[0].toUpperCase() }} +
+
+ {{ h.clientName }} + @if (h.isMandatory) { + Mandatory Signatory + } +
+
+
{{ h.clientCif || '—' }} + + {{ h.sharePercentage }}% + + + + Co-Holder + +
Total Ownership + + {{ totalJointSharePercentage }}% + + + + lock {{ mandatoryJointCount + 1 }} Mandatory + +
+
+ + @if (jointHolders.length === 0) { +
+ group_add + Click Add Holder to add joint account holders. +
+ } + + @if (totalJointSharePercentage !== 100) { +
+ warning_amber + Total ownership is {{ totalJointSharePercentage }}%. It must equal 100%. +
+ } @else { +
+ check_circle + Ownership allocation is complete (100%). +
+ } +
+ +
+
+
+ B +
+ Mode of Operation + Auto-set from mandatory holders — override manually if required +
+
+ @if (modeOfOperation) { + auto_awesome Auto + } +
+ + @if (jointHolders.length === 0) { +
+ how_to_vote + Add joint holders in Section A first. +
+ } @else { +
+ @for (opt of modeOfOperationOptions; track opt.value) { + + } +
+ +
+ @if (modeOfOperation) { + how_to_vote + Mode of Operation: {{ selectedModeLabel }} + + ({{ mandatoryJointCount }} Mandatory + {{ jointHolders.length - mandatoryJointCount }} Optional) + + } @else { + info + Set Mandatory/Optional for holders above to auto-select mode. + } +
+ } +
+ } + + + @if (selectedAccountStatus === 'MINOR') { +
+
+
+ + child_care + +
+ Minor Account Details + As per RBI — guardian must be an existing registered client +
+
+ Guardian Required +
+ +
+ + Minor's Date of Birth + + + + @if (clientDob) { + Auto-filled from client profile + } + + + + Age + + Must be below 18 years + + + + Date of Majority (Auto) + + Auto-calculated: DOB + 18 years + + + + Guardian Relationship + + @for (rel of guardianRelationshipOptions; track rel) { + {{ rel }} + } + + +
+ + + +
+ info +
    +
  • Minor below 10 years — Guardian must operate the account.
  • +
  • Minor above 10 years — Can operate independently (bank discretion).
  • +
  • Account must be converted to regular savings on attaining majority.
  • +
  • Fresh KYC of the minor must be done on majority.
  • +
+
+
+ } + + + @if (selectedAccountStatus === 'SENIOR_CITIZEN') { +
+
+
+ + elderly + +
+ Senior Citizen Account + Age ≥ 60 years — eligible for preferential banking services +
+
+ Age ≥ 60 +
+ +
+ + Date of Birth + + From client profile + + + + Age + + Age ≥ 60 required for Senior Citizen + +
+ +
+
+ priority_high +
+ Priority Service + Dedicated counters / assisted banking at branches +
+
+
+ percent +
+ Preferential Interest Rate + Additional 0.25%–0.50% on deposits (as per product) +
+
+
+ health_and_safety +
+ Doorstep Banking + Eligible for home banking services (RBI circular) +
+
+
+ gavel +
+ Nomination Mandatory + RBI mandate — ensure nomination is registered +
+
+
+ +
+ info +
    +
  • Age proof (DOB document) mandatory at time of account opening.
  • +
  • Nomination registration is compulsory (Form DA-1).
  • +
  • Bank must offer dedicated helpline and priority grievance redressal.
  • +
  • Door-step banking services must be offered (RBI Master Direction 2023).
  • +
+
+
+ } + + + @if (selectedAccountStatus === 'INDIVIDUAL_MALE') { +
+
+
+ + person + +
+ Individual Account (Male) + Select special category if applicable — governs applicable schemes & benefits +
+
+ Special Category +
+ +
+ @for (opt of specialCategoryOptions; track opt.value) { + + } +
+ +
+ info +
    +
  • Valid KYC documents required (Aadhaar / PAN / Passport).
  • +
  • Nomination registration is mandatory (Form DA-1).
  • +
  • Defence / Ex-Serviceman — eligible for special savings schemes (DSSP).
  • +
  • NRI — FCNR / NRE / NRO account rules apply as per FEMA guidelines.
  • +
+
+
+ } + + + @if (selectedAccountStatus === 'INDIVIDUAL_FEMALE') { +
+
+
+ + person_outline + +
+ Individual Account (Female) + Select weaker section category if applicable — governs PMJDY & priority schemes +
+
+ Weaker Section +
+ +
+ @for (opt of weakerSectionOptions; track opt.value) { + + } +
+ +
+ info +
    +
  • Valid KYC documents required (Aadhaar / PAN / Passport).
  • +
  • Nomination registration is mandatory (Form DA-1).
  • +
  • BPL / SC-ST / Minority — eligible for zero-balance accounts (RBI Priority Sector norms).
  • +
  • PMJDY beneficiary — ₹10,000 overdraft facility & accident insurance cover applicable.
  • +
  • SHG members — group linkage lending & special deposit rates applicable.
  • +
+
+
+ } +
+ } + + diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.scss b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.scss index cd6bfa982..6afa07d9b 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.scss +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.scss @@ -184,6 +184,1289 @@ $gray-900: #202124; } } +// ── Deposit Account Link ────────────────────────────────────────────────── + +.dal-card { + margin-top: 16px; + border: 1px solid #e2e8f0; + border-radius: 12px; + background: #fff; + overflow: hidden; + + &__header { + display: flex; + align-items: center; + gap: 14px; + padding: 16px 24px; + border-bottom: 1px solid #f1f5f9; + background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); + } + + &__header-left { + display: flex; + align-items: center; + gap: 14px; + } + + &__icon { + width: 44px; + height: 44px; + border-radius: 10px; + background: #0ea5e9; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + } + + &__title { + margin: 0; + font-size: 15px; + font-weight: 700; + color: #0c4a6e; + } + + &__sub { + display: block; + font-size: 12px; + color: #64748b; + margin-top: 2px; + } + + &__body { + padding: 20px 24px; + } +} + +// ── Loading ─────────────────────────────────────────────────────────────── +.dal-loading { + display: flex; + align-items: center; + gap: 10px; + padding: 28px; + color: #64748b; + font-size: 14px; + justify-content: center; + + &__icon { + animation: dal-spin 1.2s linear infinite; + color: #0ea5e9; + } +} + +@keyframes dal-spin { + from { transform: rotate(0deg); } + to { transform: rotate(360deg); } +} + +// ── Step 1 — Type selection ─────────────────────────────────────────────── +.dal-type-step { + &__label { + font-size: 13px; + color: #64748b; + margin: 0 0 14px; + } +} + +.dal-type-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 14px; +} + +.dal-type-btn { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + padding: 24px 16px; + border-radius: 12px; + border: 2px solid #e2e8f0; + background: #f8fafc; + cursor: pointer; + transition: all 0.18s; + + mat-icon { + font-size: 32px; + width: 32px; + height: 32px; + } + + &__name { + font-size: 14px; + font-weight: 700; + color: #0f172a; + } + + &__count { + font-size: 11px; + color: #94a3b8; + } + + &--savings { + &:hover { + border-color: #3b82f6; + background: #eff6ff; + box-shadow: 0 4px 12px rgba(59, 130, 246, 0.15); + mat-icon { color: #2563eb; } + .dal-type-btn__name { color: #1d4ed8; } + } + } + + &--current { + &:hover { + border-color: #16a34a; + background: #f0fdf4; + box-shadow: 0 4px 12px rgba(22, 163, 74, 0.15); + mat-icon { color: #15803d; } + .dal-type-btn__name { color: #15803d; } + } + } +} + +// ── Step 2 — Picker ─────────────────────────────────────────────────────── +.dal-picker { + &__topbar { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 14px; + } + + &__title { + display: flex; + align-items: center; + gap: 6px; + font-size: 14px; + font-weight: 700; + color: #0f172a; + + mat-icon { font-size: 18px; width: 18px; height: 18px; color: #64748b; } + } +} + +.dal-back-btn { + color: #64748b; +} + +.dal-search-field { + width: 100%; + margin-bottom: 8px; +} + +.dal-empty { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 32px 16px; + color: #94a3b8; + font-size: 13px; + text-align: center; + + mat-icon { font-size: 36px; width: 36px; height: 36px; } +} + +.dal-list { + display: flex; + flex-direction: column; + gap: 8px; + max-height: 300px; + overflow-y: auto; +} + +.dal-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + border: 1px solid #e2e8f0; + border-radius: 10px; + cursor: pointer; + transition: border-color 0.15s, background 0.15s, box-shadow 0.15s; + + &:hover { + border-color: #0ea5e9; + background: #f0f9ff; + box-shadow: 0 2px 8px rgba(14, 165, 233, 0.12); + } + + &__details { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; + } + + &__accno { + font-size: 14px; + font-weight: 600; + color: #0f172a; + font-family: monospace; + } + + &__product { + font-size: 12px; + color: #64748b; + } + + &__right { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 3px; + } + + &__status { + font-size: 11px; + font-weight: 600; + color: #94a3b8; + + &--active { color: #16a34a; } + } + + &__balance { + font-size: 13px; + font-weight: 600; + color: #0f172a; + } + + &__arrow { + color: #cbd5e1; + font-size: 20px; + width: 20px; + height: 20px; + } +} + +// ── Selected Account ────────────────────────────────────────────────────── +.dal-selected { + display: flex; + align-items: center; + gap: 16px; + padding: 16px 20px; + border: 2px solid #0ea5e9; + border-radius: 12px; + background: #f0f9ff; + + &__info { + display: flex; + align-items: center; + gap: 14px; + flex: 1; + flex-wrap: wrap; + } + + &__details { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1; + } + + &__accno { + font-size: 15px; + font-weight: 700; + color: #0c4a6e; + font-family: monospace; + } + + &__product { + font-size: 12px; + color: #64748b; + } + + &__balance { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; + } + + &__bal-label { + font-size: 10px; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + &__bal-amount { + font-size: 15px; + font-weight: 700; + color: #0c4a6e; + } +} + +.dal-change-btn { + white-space: nowrap; +} + +.dal-type-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 12px; + border-radius: 999px; + font-size: 12px; + font-weight: 700; + white-space: nowrap; + background: #f1f5f9; + color: #475569; + border: 1px solid #e2e8f0; + + mat-icon { font-size: 14px; width: 14px; height: 14px; } + + &--savings { + background: #eff6ff; + color: #1d4ed8; + border-color: #bfdbfe; + mat-icon { color: #2563eb; } + } + + &--current { + background: #f0fdf4; + color: #15803d; + border-color: #bbf7d0; + mat-icon { color: #16a34a; } + } +} + +// ── Status of Account (RBI) ─────────────────────────────────────────────── + +.soa-card { + margin-top: 16px; + border: 1px solid #e2e8f0; + border-radius: 10px; + background: #fff; + overflow: visible; + + &__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 24px; + background: #f8f9fa; + border-bottom: 1px solid #e8eaed; + border-radius: 10px 10px 0 0; + } + + &__header-left { + display: flex; + align-items: center; + gap: 12px; + } + + &__icon { + width: 36px; + height: 36px; + border-radius: 8px; + background: #ede9fe; + display: flex; + align-items: center; + justify-content: center; + + i { + font-size: 16px; + color: #7c3aed; + } + } + + &__title { + margin: 0; + font-size: 14px; + font-weight: 600; + color: #202124; + } + + &__sub { + font-size: 11px; + color: #6b7280; + display: block; + } +} + +.soa-pill { + display: inline-flex; + align-items: center; + padding: 3px 12px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + background: #ede9fe; + color: #7c3aed; + border: 1px solid #ddd6fe; + + &--minor { + background: #fef3c7; + color: #b45309; + border-color: #fde68a; + } + + &--senior { + background: #d1fae5; + color: #065f46; + border-color: #a7f3d0; + } + + &--ok { + background: #dcfce7; + color: #15803d; + border-color: #bbf7d0; + } +} + +.soa-client-info-bar { + display: flex; + align-items: center; + gap: 20px; + padding: 10px 24px; + background: #f0f9ff; + border-bottom: 1px solid #bae6fd; + flex-wrap: wrap; + + &__item { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + color: #0369a1; + + mat-icon { + font-size: 16px; + height: 16px; + width: 16px; + } + + strong { + font-weight: 700; + } + } +} + +.soa-auto-pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + margin-left: auto; + + mat-icon { + font-size: 13px; + height: 13px; + width: 13px; + } + + &--minor { + background: #fef3c7; + color: #b45309; + border: 1px solid #fde68a; + } + + &--senior { + background: #d1fae5; + color: #065f46; + border: 1px solid #a7f3d0; + } + + &--ok { + background: #dcfce7; + color: #15803d; + border: 1px solid #bbf7d0; + } +} + +.soa-tiles { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 12px; + padding: 16px 24px; + border-bottom: 1px solid #f1f5f9; + + @media (width <= 900px) { + grid-template-columns: repeat(3, 1fr); + } + + @media (width <= 600px) { + grid-template-columns: repeat(2, 1fr); + } +} + +.soa-tile { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 14px 10px; + border: 2px solid #e2e8f0; + border-radius: 10px; + background: #fff; + cursor: pointer; + transition: + border-color 0.15s, + box-shadow 0.15s; + position: relative; + + &:hover { + border-color: #a5b4fc; + box-shadow: 0 2px 8px rgb(99 102 241 / 12%); + } + + &--selected { + border-color: #6366f1; + background: #eef2ff; + box-shadow: 0 2px 12px rgb(99 102 241 / 20%); + } + + &__icon { + font-size: 28px; + height: 28px; + width: 28px; + } + + &__label { + font-size: 12px; + font-weight: 600; + color: #374151; + text-align: center; + } + + &__check { + position: absolute; + top: 6px; + right: 6px; + font-size: 16px; + height: 16px; + width: 16px; + color: #6366f1; + } + + &__lock { + position: absolute; + top: 4px; + right: 4px; + display: flex; + align-items: center; + + mat-icon { + font-size: 13px; + height: 13px; + width: 13px; + color: #94a3b8; + } + } + + &--disabled { + opacity: 0.45; + cursor: not-allowed; + background: #f8fafc; + border-color: #e2e8f0; + box-shadow: none; + + &:hover { + border-color: #e2e8f0; + box-shadow: none; + } + } +} + +.soa-block { + padding: 16px 24px; + + &--sep { + border-top: 1px dashed #e2e8f0; + } + + &__head { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 14px; + } + + &__head-left { + display: flex; + align-items: flex-start; + gap: 12px; + } + + &__letter { + width: 28px; + height: 28px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + font-weight: 700; + color: #fff; + flex-shrink: 0; + margin-top: 2px; + } + + &__title { + font-size: 13px; + font-weight: 600; + color: #1f2937; + display: block; + } + + &__hint { + font-size: 11px; + color: #6b7280; + display: block; + } +} + +.soa-table-wrap { + overflow-x: auto; + border-radius: 8px; + border: 1px solid #e2e8f0; +} + +.soa-tbl { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.soa-th { + padding: 10px 14px; + background: #f8fafc; + text-align: left; + font-weight: 600; + font-size: 11px; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.04em; + border-bottom: 1px solid #e2e8f0; + + &--sr { + width: 44px; + } + + &--center { + text-align: center; + } +} + +.soa-td { + padding: 10px 14px; + border-bottom: 1px solid #f1f5f9; + vertical-align: middle; + + &--sr { + text-align: center; + font-size: 12px; + color: #94a3b8; + font-weight: 600; + } + + &--center { + text-align: center; + } + + &--total-label { + font-size: 12px; + font-weight: 600; + color: #475569; + text-align: right; + } +} + +.soa-tr { + transition: background 0.1s; + + &:hover { + background: #f8fafc; + } + + &--mandatory { + background: #fdf4ff; + } + + &--primary { + background: #f0f9ff; + border-left: 3px solid #0891b2; + } +} + +.soa-avatar--primary { + background: #cffafe; + color: #0e7490; +} + +.soa-primary-tag { + font-size: 10px; + color: #0e7490; + font-weight: 600; + background: #cffafe; + padding: 1px 7px; + border-radius: 999px; + display: inline-block; +} + +.soa-pct-input { + width: 56px; + padding: 4px 6px; + border: 1px solid #cbd5e1; + border-radius: 6px; + font-size: 13px; + font-weight: 600; + text-align: center; + color: #1f2937; + + &:focus { + outline: none; + border-color: #0891b2; + box-shadow: 0 0 0 2px rgb(8 145 178 / 15%); + } + + &::-webkit-inner-spin-button, + &::-webkit-outer-spin-button { + opacity: 1; + } +} + +.soa-pct-unit { + font-size: 13px; + font-weight: 600; + color: #64748b; + margin-left: 2px; +} + +.soa-no-action { + color: #cbd5e1; + font-size: 16px; +} + +.soa-tr-total { + background: #f8fafc; +} + +.soa-client-cell { + display: flex; + align-items: center; + gap: 10px; +} + +.soa-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background: #dbeafe; + color: #1d4ed8; + font-weight: 700; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + &--mandatory { + background: #ede9fe; + color: #7c3aed; + } +} + +.soa-client-name { + font-weight: 600; + font-size: 13px; + color: #1f2937; + display: block; +} + +.soa-mandatory-tag { + font-size: 10px; + color: #7c3aed; + font-weight: 600; + background: #ede9fe; + padding: 1px 7px; + border-radius: 999px; +} + +.soa-mono { + font-family: 'Roboto Mono', monospace; + font-size: 12px; + color: #64748b; +} + +.soa-pct-badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 2px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 700; + background: #f1f5f9; + color: #475569; + + &--full, + &--ok { + background: #dcfce7; + color: #15803d; + } + + &--warn { + background: #fef3c7; + color: #b45309; + } +} + +.soa-sign-toggle { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + border: 1px solid #e2e8f0; + cursor: pointer; + transition: all 0.15s; + + mat-icon { + font-size: 14px; + height: 14px; + width: 14px; + } + + &--mandatory { + background: #ede9fe; + color: #7c3aed; + border-color: #ddd6fe; + } + + &--optional { + background: #f8fafc; + color: #94a3b8; + } +} + +.soa-mandatory-count { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 12px; + font-weight: 600; + color: #7c3aed; + + mat-icon { + font-size: 14px; + height: 14px; + width: 14px; + } +} + +.soa-alert { + display: flex; + align-items: center; + gap: 8px; + margin-top: 10px; + padding: 8px 14px; + border-radius: 8px; + font-size: 13px; + font-weight: 500; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + } + + &--warn { + background: #fef3c7; + color: #b45309; + } + + &--ok { + background: #dcfce7; + color: #15803d; + } +} + +.soa-empty { + display: flex; + align-items: center; + gap: 10px; + padding: 20px; + background: #f8fafc; + border-radius: 8px; + font-size: 13px; + color: #64748b; + + mat-icon { + color: #94a3b8; + font-size: 22px; + height: 22px; + width: 22px; + } +} + +.soa-add-btn { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; +} + +.soa-mode-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px; + + @media (width <= 700px) { + grid-template-columns: repeat(2, 1fr); + } +} + +.soa-mode-card { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 12px 8px; + border: 2px solid #e2e8f0; + border-radius: 10px; + background: #fff; + cursor: pointer; + transition: all 0.15s; + + &:hover { + border-color: #a5b4fc; + } + + &--selected { + border-color: #7c3aed; + background: #faf5ff; + } + + &__icon { + font-size: 24px; + height: 24px; + width: 24px; + color: #7c3aed; + } + + &__code { + font-size: 13px; + font-weight: 700; + color: #1f2937; + } + + &__desc { + font-size: 11px; + color: #6b7280; + text-align: center; + } + + &__check { + position: absolute; + top: 6px; + right: 6px; + font-size: 16px; + height: 16px; + width: 16px; + color: #7c3aed; + } +} + +.soa-mode-summary { + display: flex; + align-items: center; + gap: 8px; + margin-top: 12px; + padding: 8px 14px; + border-radius: 8px; + font-size: 13px; + background: #f8fafc; + color: #64748b; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + } + + &--set { + background: #faf5ff; + color: #7c3aed; + } + + &__from { + font-size: 11px; + color: #94a3b8; + } +} + +.soa-auto-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + background: #fef9c3; + color: #854d0e; + border: 1px solid #fde68a; + + mat-icon { + font-size: 13px; + height: 13px; + width: 13px; + } +} + +.soa-minor-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 4px 16px; + margin-bottom: 16px; + + @media (width <= 700px) { + grid-template-columns: 1fr; + } +} + +.soa-field { + width: 100%; +} + +.soa-guardian-search { + margin-bottom: 12px; + + &__label { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 600; + color: #374151; + margin-bottom: 8px; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + color: #7c3aed; + } + } +} + +.soa-guardian-selected { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 14px; + background: #f0fdf4; + border: 1px solid #bbf7d0; + border-radius: 8px; + margin-top: 10px; + + &__icon { + font-size: 24px; + height: 24px; + width: 24px; + color: #16a34a; + } + + &__name { + font-size: 13px; + font-weight: 600; + color: #1f2937; + } + + &__cif { + font-size: 11px; + color: #6b7280; + } +} + +.soa-sc-benefits { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; + margin-bottom: 14px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.soa-sc-benefit { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px; + background: #f0fdf4; + border: 1px solid #bbf7d0; + border-radius: 8px; + + mat-icon { + color: #16a34a; + font-size: 20px; + height: 20px; + width: 20px; + flex-shrink: 0; + margin-top: 2px; + } + + strong { + display: block; + font-size: 13px; + color: #1f2937; + font-weight: 600; + } + + span { + font-size: 11px; + color: #6b7280; + } +} + +.soa-category-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; + margin-bottom: 16px; + + @media (width <= 700px) { + grid-template-columns: repeat(2, 1fr); + } +} + +.soa-cat-tile { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 14px 10px; + border: 2px solid #e2e8f0; + border-radius: 10px; + background: #fff; + cursor: pointer; + transition: all 0.15s; + + &:hover { + border-color: #a5b4fc; + box-shadow: 0 2px 8px rgb(99 102 241 / 12%); + } + + &--selected { + &.soa-cat-tile--male { + border-color: #3b82f6; + background: #eff6ff; + box-shadow: 0 2px 12px rgb(59 130 246 / 20%); + } + + &.soa-cat-tile--female { + border-color: #ec4899; + background: #fdf2f8; + box-shadow: 0 2px 12px rgb(236 72 153 / 20%); + } + } + + &__icon { + font-size: 26px; + height: 26px; + width: 26px; + color: #64748b; + } + + &--selected.soa-cat-tile--male &__icon { + color: #3b82f6; + } + + &--selected.soa-cat-tile--female &__icon { + color: #ec4899; + } + + &__label { + font-size: 11px; + font-weight: 600; + color: #374151; + text-align: center; + line-height: 1.3; + } + + &__check { + position: absolute; + top: 6px; + right: 6px; + font-size: 16px; + height: 16px; + width: 16px; + + .soa-cat-tile--male & { + color: #3b82f6; + } + + .soa-cat-tile--female & { + color: #ec4899; + } + } +} + +.soa-info-banner { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px 14px; + border-radius: 8px; + font-size: 12px; + background: #eff6ff; + border: 1px solid #bfdbfe; + color: #1e40af; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + flex-shrink: 0; + margin-top: 2px; + } + + ul { + margin: 4px 0 0; + padding-left: 18px; + + li { + margin-bottom: 3px; + } + } + + &--minor { + background: #fefce8; + border-color: #fde68a; + color: #92400e; + } + + &--senior { + background: #f0fdf4; + border-color: #bbf7d0; + color: #065f46; + } + + &--male { + background: #eff6ff; + border-color: #bfdbfe; + color: #1e40af; + } + + &--female { + background: #fdf2f8; + border-color: #f9a8d4; + color: #9d174d; + } +} + // ── Account Feature Toggles ──────────────────────────────────────────────── .rad-toggles { display: flex; diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts index 6c484b099..5f8e0063b 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts @@ -11,14 +11,17 @@ import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorR import { UntypedFormGroup, UntypedFormBuilder, Validators, FormsModule } from '@angular/forms'; import { SettingsService } from 'app/settings/settings.service'; import { Dates } from 'app/core/utils/dates'; +import { MatDialog } from '@angular/material/dialog'; /** Custom Services */ import { RecurringDepositsService } from '../../recurring-deposits.service'; +import { ClientsService } from 'app/clients/clients.service'; import { MatTooltip } from '@angular/material/tooltip'; import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; -import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component'; +import { MatIcon } from '@angular/material/icon'; +import { AddRelatedClientDialogComponent } from 'app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component'; /** * Recurring Deposits Account Details Step @@ -33,8 +36,8 @@ import { IntroducerSearchComponent } from 'app/shared/introducer-search/introduc MatStepperPrevious, FaIconComponent, MatStepperNext, - IntroducerSearchComponent, - FormsModule + FormsModule, + MatIcon ] }) export class RecurringDepositsAccountDetailsStepComponent implements OnInit { @@ -43,6 +46,8 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { private settingsService = inject(SettingsService); private dateUtils = inject(Dates); private cdr = inject(ChangeDetectorRef); + private clientsService = inject(ClientsService); + private dialog = inject(MatDialog); /** Recurring Deposits Account Template */ @Input() recurringDepositsAccountTemplate: any; @@ -61,9 +66,6 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { recurringDepositAccountDetailsForm!: UntypedFormGroup; isProductSelected = false; - introducerId: number | null = null; - introducerName: string | null = null; - introducerCif: string | null = null; /** Recurring Deposits Account Template with product data */ @Output() recurringDepositsAccountProductTemplate = new EventEmitter(); @@ -75,6 +77,145 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { selectedAccountType: string | null = null; selectedCustomerSegment: string | null = null; + // ── Client info (auto-fetched) ───────────────────────────────────────── + clientDob: Date | null = null; + clientAge: number | null = null; + clientGender: string | null = null; + primaryClientName = ''; + primaryClientCif = ''; + primaryClientId: number | null = null; + primaryHolderShare = 0; + + // ── Status of Account (RBI) ──────────────────────────────────────────── + readonly accountStatusOptions = [ + { value: 'INDIVIDUAL_MALE', label: 'Individual (Male)', icon: 'person', color: '#3b82f6' }, + { value: 'INDIVIDUAL_FEMALE', label: 'Individual (Female)', icon: 'person_outline', color: '#ec4899' }, + { value: 'SINGLE', label: 'Single Account', icon: 'account_circle', color: '#0891b2' }, + { value: 'JOINT', label: 'Joint Account', icon: 'group', color: '#8b5cf6' }, + { value: 'MINOR', label: 'Minor Account', icon: 'child_care', color: '#f59e0b' }, + { value: 'SENIOR_CITIZEN', label: 'Senior Citizen', icon: 'elderly', color: '#10b981' } + ]; + + selectedAccountStatus = ''; + + // ── Joint Account ────────────────────────────────────────────────────── + jointHolders: Array<{ + clientId: number; + clientName: string; + clientCif: string; + sharePercentage: number; + isMandatory: boolean; + }> = []; + + modeOfOperation = ''; + + readonly modeOfOperationOptions = [ + { value: 'EITHER_OR_SURVIVOR', label: 'E or S', sublabel: 'Either or Survivor', icon: 'how_to_vote' }, + { value: 'FORMER_OR_SURVIVOR', label: 'F or S', sublabel: 'Former or Survivor', icon: 'person' }, + { value: 'JOINTLY', label: 'Jointly', sublabel: 'All Must Sign', icon: 'groups' }, + { value: 'ANYONE_OR_SURVIVOR', label: 'Anyone', sublabel: 'Anyone or Survivor', icon: 'group' } + ]; + + // ── Individual Male — Special Category ──────────────────────────────── + selectedSpecialCategory = ''; + + readonly specialCategoryOptions = [ + { value: 'DEFENCE', label: 'Defence Personnel', icon: 'military_tech' }, + { value: 'EX_SERVICEMAN', label: 'Ex-Serviceman', icon: 'shield' }, + { value: 'GOVT_EMPLOYEE', label: 'Government Employee', icon: 'account_balance' }, + { value: 'PHYSICALLY_CHALLENGED', label: 'Physically Challenged', icon: 'accessible' }, + { value: 'NRI', label: 'NRI (Non-Resident Indian)', icon: 'flight' }, + { value: 'NONE', label: 'None / General', icon: 'person' } + ]; + + // ── Individual Female — Weaker Section ──────────────────────────────── + selectedWeakerSection = ''; + + readonly weakerSectionOptions = [ + { value: 'BPL', label: 'BPL (Below Poverty Line)', icon: 'volunteer_activism' }, + { value: 'SC_ST', label: 'SC / ST Category', icon: 'groups' }, + { value: 'MINORITY', label: 'Minority Community', icon: 'diversity_3' }, + { value: 'SHG', label: 'Self Help Group Member', icon: 'handshake' }, + { value: 'PMJDY', label: 'PMJDY Beneficiary', icon: 'savings' }, + { value: 'NONE', label: 'None / General', icon: 'person_outline' } + ]; + + // ── Minor Account ────────────────────────────────────────────────────── + guardianClientId: number | null = null; + guardianName = ''; + guardianCif = ''; + guardianRelationship = ''; + minorDob: Date | null = null; + + readonly guardianRelationshipOptions = ['Father', 'Mother', 'Legal Guardian', 'Court Appointed Guardian']; + + // ── Deposit Account Link ─────────────────────────────────────────────── + dalSelectedType: 'SAVINGS' | 'CURRENT' | null = null; + dalAllSavings: any[] = []; + dalAllCurrent: any[] = []; + dalSearch = ''; + dalLoading = false; + selectedLinkedAccount: any = null; + + get dalAccounts(): any[] { + return this.dalSelectedType === 'SAVINGS' ? this.dalAllSavings : this.dalAllCurrent; + } + + get dalFilteredAccounts(): any[] { + const q = this.dalSearch.toLowerCase().trim(); + if (!q) return this.dalAccounts; + return this.dalAccounts.filter( + (a) => a.accountNo.toLowerCase().includes(q) || a.productName.toLowerCase().includes(q) + ); + } + + selectDalType(type: 'SAVINGS' | 'CURRENT'): void { + this.dalSelectedType = type; + this.dalSearch = ''; + this.selectedLinkedAccount = null; + } + + selectLinkedAccount(account: any): void { + this.selectedLinkedAccount = account; + } + + clearLinkedAccount(): void { + this.selectedLinkedAccount = null; + this.dalSelectedType = null; + this.dalSearch = ''; + } + + private loadClientAccounts(): void { + const clientId = String(this.recurringDepositsAccountTemplate?.clientId); + if (!clientId) return; + this.dalLoading = true; + this.clientsService.getClientAccountData(clientId).subscribe({ + next: (data: any) => { + this.dalAllSavings = (data?.savingsAccounts ?? []).map((a: any) => ({ + id: a.id, + accountNo: a.accountNo, + productName: a.productName ?? '', + accountType: 'SAVINGS' as const, + status: a.status?.value ?? '', + balance: a.accountBalance ?? 0, + currencyCode: a.currency?.code ?? '' + })); + this.dalAllCurrent = (data?.currentAccounts ?? []).map((a: any) => ({ + id: a.id, + accountNo: a.accountNo, + productName: a.productName ?? '', + accountType: 'CURRENT' as const, + status: a.status?.value ?? '', + balance: a.accountBalance ?? 0, + currencyCode: a.currency?.code ?? '' + })); + this.dalLoading = false; + this.cdr.detectChanges(); + }, + error: () => { this.dalLoading = false; } + }); + } + constructor() { this.createRecurringDepositsAccountDetailsForm(); } @@ -85,7 +226,6 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { if (this.recurringDepositsAccountTemplate) { this.productData = this.recurringDepositsAccountTemplate.productOptions; if (this.recurringDepositsAccountTemplate.depositProductId) { - // Edit mode: patch from saved data this.recurringDepositAccountDetailsForm.patchValue({ productId: this.recurringDepositsAccountTemplate.depositProductId, submittedOnDate: @@ -94,12 +234,84 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { externalId: this.recurringDepositsAccountTemplate.externalId }); } else { - // New account: default submittedOnDate to today's business date this.recurringDepositAccountDetailsForm.patchValue({ submittedOnDate: this.settingsService.businessDate }); } } + + // Auto-detect account status from client DOB + capture primary holder info + const clientId = this.recurringDepositsAccountTemplate?.clientId; + if (clientId) { + this.primaryClientId = clientId; + this.clientsService.getClientData(String(clientId)).subscribe((client: any) => { + this.primaryClientName = client?.displayName ?? client?.fullname ?? ''; + this.primaryClientCif = client?.cifNumber ?? client?.cifNo ?? ''; + if (client?.dateOfBirth) { + const dob = this.parseClientDob(client.dateOfBirth); + if (dob) { + this.clientDob = dob; + this.clientAge = this.calculateAge(dob); + this.clientGender = client.gender?.name?.toLowerCase() ?? null; + this.autoDetectAccountStatus(); + } + } + this.cdr.detectChanges(); + }); + } + } + + /** Parse DOB from backend — handles array [yyyy, mm, dd] or string */ + private parseClientDob(dob: any): Date | null { + if (!dob) return null; + if (Array.isArray(dob) && dob.length >= 3) { + return new Date(dob[0], dob[1] - 1, dob[2]); + } + const d = new Date(dob); + return isNaN(d.getTime()) ? null : d; + } + + /** Calculate full years from a date to today */ + private calculateAge(dob: Date): number { + const today = this.settingsService.businessDate ?? new Date(); + let age = today.getFullYear() - dob.getFullYear(); + const monthDiff = today.getMonth() - dob.getMonth(); + if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < dob.getDate())) { + age--; + } + return age; + } + + isTileDisabled(value: string): boolean { + if (value === 'MINOR') return this.clientAge === null || this.clientAge >= 18; + if (value === 'SENIOR_CITIZEN') return this.clientAge === null || this.clientAge < 60; + return false; + } + + getTileTooltip(value: string): string { + if (value === 'MINOR' && this.clientAge !== null && this.clientAge >= 18) { + return `Age ${this.clientAge} yrs — Minor only allowed below 18 years`; + } + if (value === 'SENIOR_CITIZEN' && this.clientAge !== null && this.clientAge < 60) { + return `Age ${this.clientAge} yrs — Senior Citizen only allowed at 60+ years`; + } + if (value === 'MINOR' && this.clientAge === null) return 'DOB not available — cannot verify age'; + if (value === 'SENIOR_CITIZEN' && this.clientAge === null) return 'DOB not available — cannot verify age'; + return ''; + } + + private autoDetectAccountStatus(): void { + if (this.clientAge === null) return; + if (this.clientAge < 18) { + this.selectedAccountStatus = 'MINOR'; + this.minorDob = this.clientDob; + } else if (this.clientAge >= 60) { + this.selectedAccountStatus = 'SENIOR_CITIZEN'; + } else if (this.clientGender === 'female') { + this.selectedAccountStatus = 'INDIVIDUAL_FEMALE'; + } else { + this.selectedAccountStatus = 'INDIVIDUAL_MALE'; + } } /** @@ -107,14 +319,8 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { */ createRecurringDepositsAccountDetailsForm() { this.recurringDepositAccountDetailsForm = this.formBuilder.group({ - productId: [ - '', - Validators.required - ], - submittedOnDate: [ - '', - Validators.required - ], + productId: ['', Validators.required], + submittedOnDate: ['', Validators.required], fieldOfficerId: [''], externalId: [''], balanceSwapOption: [false], @@ -132,9 +338,7 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { this.recurringDepositsService .getRecurringDepositsAccountTemplate(clientId, productId) .subscribe((response: any) => { - // Ensure currency is included in the product template if (!response.currency) { - // Try to get currency from the selected product const selectedProduct = this.recurringDepositsAccountTemplate.productOptions?.find( (product: any) => product.id == productId ); @@ -152,7 +356,9 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { this.recurringDepositsAccountProductTemplate.emit(response); this.fieldOfficerData = response.fieldOfficerOptions; this.isProductSelected = true; - // Pre-populate toggles from product defaults (user can override) + if (response.shareLinkingEnabled) { + this.loadClientAccounts(); + } this.recurringDepositAccountDetailsForm.patchValue({ balanceSwapOption: !!response.balanceSwapOption, msme: !!response.msme, @@ -171,17 +377,10 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { }); } - /** - * Returns true if a value is not null, undefined, or empty string. - */ hasValue(val: any): boolean { return val !== null && val !== undefined && val !== ''; } - /** - * Formats template dates into the current CBS date format. - * Handles backend date arrays like [2026, 4, 20] as well as strings and Date objects. - */ private extractOptions(options: any[], fallback?: string): string[] { if (Array.isArray(options) && options.length > 0) { return options.map((o: any) => o.value ?? o.name ?? o); @@ -190,12 +389,8 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { } formatTemplateDate(value: any): string { - if (!value) { - return ''; - } - + if (!value) return ''; let dateValue: Date | null = null; - if (value instanceof Date) { dateValue = value; } else if (Array.isArray(value) && value.length >= 3) { @@ -203,30 +398,133 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { } else { dateValue = new Date(value); } - if (!dateValue || Number.isNaN(dateValue.getTime())) { return Array.isArray(value) ? value.join('/') : String(value); } - return this.dateUtils.formatDate(dateValue, 'M/d/yyyy') || ''; } + onGuardianChange(event: { clientId: number; displayName: string; cifNumber?: string } | null): void { + this.guardianClientId = event?.clientId ?? null; + this.guardianName = event?.displayName ?? ''; + this.guardianCif = event?.cifNumber ?? ''; + } + + // ── Joint Account helpers ────────────────────────────────────────────── + openAddJointHolderDialog(): void { + const ref = this.dialog.open(AddRelatedClientDialogComponent, { + width: '500px', + panelClass: 'arc-dialog-panel', + disableClose: true + }); + ref.afterClosed().subscribe((result) => { + if (result) { + const duplicate = this.jointHolders.some((h) => h.clientCif && h.clientCif === result.clientCif); + if (duplicate) { + alert(`Account holder with CIF ${result.clientCif} is already added.`); + return; + } + this.jointHolders = [...this.jointHolders, { ...result, isMandatory: false }]; + this.autoSetModeOfOperation(); + } + }); + } + + removeJointHolder(index: number): void { + this.jointHolders = this.jointHolders.filter((_, i) => i !== index); + this.autoSetModeOfOperation(); + } + + toggleJointHolderMandatory(holder: any): void { + holder.isMandatory = !holder.isMandatory; + this.autoSetModeOfOperation(); + } + + autoSetModeOfOperation(): void { + const mandatory = this.jointHolders.filter((h) => h.isMandatory).length; + if (mandatory === 0) { + this.modeOfOperation = 'EITHER_OR_SURVIVOR'; + } else if (mandatory === 1) { + this.modeOfOperation = 'FORMER_OR_SURVIVOR'; + } else { + this.modeOfOperation = 'JOINTLY'; + } + } + + get totalJointSharePercentage(): number { + return (this.primaryHolderShare || 0) + this.jointHolders.reduce((sum, h) => sum + (h.sharePercentage || 0), 0); + } + + get mandatoryJointCount(): number { + return this.jointHolders.filter((h) => h.isMandatory).length; + } + + get selectedModeLabel(): string { + return this.modeOfOperationOptions.find((o) => o.value === this.modeOfOperation)?.sublabel ?? ''; + } + + get majorityDate(): string { + const dob = this.minorDob ?? this.clientDob; + if (!dob) return ''; + const d = new Date(dob); + d.setFullYear(d.getFullYear() + 18); + return this.dateUtils.formatDate(d, 'M/d/yyyy') || ''; + } + /** * Returns recurring deposits account details form value. */ - onIntroducerChange(event: { clientId: number; displayName: string; cifNumber?: string } | null): void { - this.introducerId = event?.clientId ?? null; - this.introducerName = event?.displayName ?? null; - this.introducerCif = event?.cifNumber ?? null; - } - get recurringDepositAccountDetails() { const value = this.recurringDepositAccountDetailsForm.value; - if (this.introducerId) { - value.introducerId = this.introducerId; - value.introducerName = this.introducerName; - value.introducerCif = this.introducerCif; + + if (this.selectedAccountStatus) { + value.accountStatus = this.selectedAccountStatus; } + + if (this.selectedAccountStatus === 'JOINT') { + value.modeOfOperation = this.modeOfOperation || null; + value.jointHolders = [ + { + clientId: this.primaryClientId, + clientName: this.primaryClientName, + clientCif: this.primaryClientCif, + sharePercentage: this.primaryHolderShare, + isMandatory: true, + isPrimary: true + }, + ...this.jointHolders.map((h) => ({ + clientId: h.clientId, + clientName: h.clientName, + clientCif: h.clientCif, + sharePercentage: h.sharePercentage, + isMandatory: h.isMandatory, + isPrimary: false + })) + ]; + } + + if (this.selectedAccountStatus === 'INDIVIDUAL_MALE' && this.selectedSpecialCategory) { + value.specialCategory = this.selectedSpecialCategory; + } + + if (this.selectedAccountStatus === 'INDIVIDUAL_FEMALE' && this.selectedWeakerSection) { + value.weakerSection = this.selectedWeakerSection; + } + + if (this.selectedAccountStatus === 'MINOR') { + value.guardianClientId = this.guardianClientId; + value.guardianName = this.guardianName; + value.guardianCif = this.guardianCif; + value.guardianRelationship = this.guardianRelationship; + value.minorDob = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'yyyy-MM-dd') : null; + } + + if (this.selectedLinkedAccount) { + value.linkedAccountId = this.selectedLinkedAccount.id; + value.linkedAccountType = this.selectedLinkedAccount.accountType; + value.linkedAccountNo = this.selectedLinkedAccount.accountNo; + } + return value; } } diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html index 60ca14d64..d3699f0a1 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html @@ -112,7 +112,7 @@ recurringDepositsAccountProductTemplate?.shareLinkingEnabled !== null ) {
- Share Linking Enabled + Deposit Account Link
- Share Linking Enabled + Deposit Account Link
diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.html index c32f8fbd1..a946f557e 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.html @@ -87,7 +87,7 @@ @if (recurringDepositProduct.shareLinkingEnabled !== undefined) {
- Share Linking Enabled: + Deposit Account Link: {{ recurringDepositProduct.shareLinkingEnabled | yesNo }}
} diff --git a/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.html b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.html index f673989be..1105b7f68 100644 --- a/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.html +++ b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.html @@ -77,7 +77,7 @@ {{ recurringDepositProduct.introducerRequired | yesNo }}
- Share Linking Enabled: + Deposit Account Link: {{ recurringDepositProduct.shareLinkingEnabled | yesNo }}
diff --git a/src/app/savings/create-savings-account/create-savings-account.component.ts b/src/app/savings/create-savings-account/create-savings-account.component.ts index a84dd5dcb..8cefe0623 100644 --- a/src/app/savings/create-savings-account/create-savings-account.component.ts +++ b/src/app/savings/create-savings-account/create-savings-account.component.ts @@ -113,7 +113,7 @@ export class CreateSavingsAccountComponent { const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; const monthDayFormat = 'dd MMMM'; - const savingsAccount = { + const savingsAccount: any = { ...this.savingsAccount, charges: [], submittedOnDate: this.dateUtils.formatDate(this.savingsAccount.submittedOnDate, dateFormat), @@ -121,6 +121,17 @@ export class CreateSavingsAccountComponent { monthDayFormat, locale }; + + // Digital Facilities + if (this.savingsAccountDigital?.accountFacilityConfig) { + savingsAccount.accountFacilityConfig = this.savingsAccountDigital.accountFacilityConfig; + } + + // Nominee + if (this.savingsAccountNominee) { + savingsAccount.nominee = this.savingsAccountNominee; + } + if (this.savingsAccountTemplate.clientId) { savingsAccount.clientId = this.savingsAccountTemplate.clientId; } else { diff --git a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html index 8bf0e988a..1fd029dc5 100644 --- a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html +++ b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.html @@ -135,4 +135,561 @@ } } + + + @if (savingsProductSelected) { +
+
+
+
+ +
+
+

Status of Account

+ As per RBI guidelines — select account holder type +
+
+ RBI Compliant +
+ + + @if (clientDob) { +
+
+ cake + Date of Birth: {{ formatTemplateDate(clientDob) }} +
+
+ person_pin + Age: {{ clientAge }} years +
+ @if (clientAge !== null && clientAge < 18) { + + child_care Auto-detected: Minor + + } @else if (clientAge !== null && clientAge >= 60) { + + elderly Auto-detected: Senior Citizen + + } @else { + check_circle Age Verified + } +
+ } + + +
+ @for (opt of accountStatusOptions; track opt.value) { + + } +
+ + + @if (selectedAccountStatus === 'JOINT') { +
+
+
+ A +
+ Joint Account Holders + Add all co-applicants linked to this joint savings account +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + @for (h of jointHolders; track h; let i = $index) { + + + + + + + + + + } + + + + + + + + + + +
Sr.Holder NameCIF No.Ownership %Signing AuthorityRoleAction
1 +
+
+ {{ primaryClientName ? primaryClientName[0].toUpperCase() : '?' }} +
+
+ {{ primaryClientName || 'Primary Holder' }} + Primary Account Holder +
+
+
+ {{ primaryClientCif || '—' }} + + + % + + + lock Mandatory + + Primary Account Holder + — +
{{ i + 2 }} +
+
+ {{ h.clientName[0].toUpperCase() }} +
+
+ {{ h.clientName }} + @if (h.isMandatory) { + Mandatory Signatory + } +
+
+
+ {{ h.clientCif || '—' }} + + + {{ h.sharePercentage }}% + + + + Co-Holder + +
Total Ownership + + {{ totalJointSharePercentage }}% + + + + lock {{ mandatoryJointCount + 1 }} Mandatory + +
+
+ + @if (jointHolders.length === 0) { +
+ group_add + Click Add Holder to add joint account holders. +
+ } + + @if (totalJointSharePercentage !== 100) { +
+ warning_amber + Total ownership is {{ totalJointSharePercentage }}%. It must equal 100%. +
+ } @else { +
+ check_circle + Ownership allocation is complete (100%). +
+ } +
+ + +
+
+
+ B +
+ Mode of Operation + Auto-set from mandatory holders — override manually if required +
+
+ @if (modeOfOperation) { + auto_awesome Auto + } +
+ + @if (jointHolders.length === 0) { +
+ how_to_vote + Add joint holders in Section A first. +
+ } @else { +
+ @for (opt of modeOfOperationOptions; track opt.value) { + + } +
+ +
+ @if (modeOfOperation) { + how_to_vote + Mode of Operation: {{ selectedModeLabel }} + + ({{ mandatoryJointCount }} Mandatory + {{ jointHolders.length - mandatoryJointCount }} Optional) + + } @else { + info + Set Mandatory/Optional for holders above to auto-select mode. + } +
+ } +
+ } + + + @if (selectedAccountStatus === 'MINOR') { +
+
+
+ + child_care + +
+ Minor Account Details + As per RBI — guardian must be an existing registered client +
+
+ Guardian Required +
+ +
+ + Minor's Date of Birth + + + + @if (clientDob) { + Auto-filled from client profile + } + + + + Age + + Must be below 18 years + + + + Date of Majority (Auto) + + Auto-calculated: DOB + 18 years + + + + Guardian Relationship + + @for (rel of guardianRelationshipOptions; track rel) { + {{ rel }} + } + + +
+ + + +
+ info +
    +
  • Minor below 10 years — Guardian must operate the account.
  • +
  • Minor above 10 years — Can operate independently (bank discretion).
  • +
  • Account must be converted to regular savings on attaining majority.
  • +
  • Fresh KYC of the minor must be done on majority.
  • +
+
+
+ } + + + @if (selectedAccountStatus === 'SENIOR_CITIZEN') { +
+
+
+ + elderly + +
+ Senior Citizen Account + Age ≥ 60 years — eligible for preferential banking services +
+
+ Age ≥ 60 +
+ + +
+ + Date of Birth + + From client profile + + + + Age + + Age ≥ 60 required for Senior Citizen + +
+ +
+
+ priority_high +
+ Priority Service + Dedicated counters / assisted banking at branches +
+
+
+ percent +
+ Preferential Interest Rate + Additional 0.25%–0.50% on deposits (as per product) +
+
+
+ health_and_safety +
+ Doorstep Banking + Eligible for home banking services (RBI circular) +
+
+
+ gavel +
+ Nomination Mandatory + RBI mandate — ensure nomination is registered +
+
+
+ +
+ info +
    +
  • Age proof (DOB document) mandatory at time of account opening.
  • +
  • Nomination registration is compulsory (Form DA-1).
  • +
  • Bank must offer dedicated helpline and priority grievance redressal.
  • +
  • Door-step banking services must be offered (RBI Master Direction 2023).
  • +
+
+
+ } + + + @if (selectedAccountStatus === 'INDIVIDUAL_MALE') { +
+
+
+ + person + +
+ Individual Account (Male) + Select special category if applicable — governs applicable schemes & benefits +
+
+ + Special Category + +
+ +
+ @for (opt of specialCategoryOptions; track opt.value) { + + } +
+ +
+ info +
    +
  • Valid KYC documents required (Aadhaar / PAN / Passport).
  • +
  • Nomination registration is mandatory (Form DA-1).
  • +
  • Defence / Ex-Serviceman — eligible for special savings schemes (DSSP).
  • +
  • NRI — FCNR / NRE / NRO account rules apply as per FEMA guidelines.
  • +
+
+
+ } + + + @if (selectedAccountStatus === 'INDIVIDUAL_FEMALE') { +
+
+
+ + person_outline + +
+ Individual Account (Female) + Select weaker section category if applicable — governs PMJDY & priority schemes +
+
+ + Weaker Section + +
+ +
+ @for (opt of weakerSectionOptions; track opt.value) { + + } +
+ +
+ info +
    +
  • Valid KYC documents required (Aadhaar / PAN / Passport).
  • +
  • Nomination registration is mandatory (Form DA-1).
  • +
  • BPL / SC-ST / Minority — eligible for zero-balance accounts (RBI Priority Sector norms).
  • +
  • PMJDY beneficiary — ₹10,000 overdraft facility & accident insurance cover applicable.
  • +
  • SHG members — group linkage lending & special deposit rates applicable.
  • +
+
+
+ } +
+ } diff --git a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.scss b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.scss index 5744c0823..ff42ba281 100644 --- a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.scss +++ b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.scss @@ -103,3 +103,1046 @@ $gray-900: #202124; border-color: #bbf7d0; } } + +// ── Status of Account card ──────────────────────────────────────────── + +.soa-card { + margin-top: 16px; + border: 1px solid #e2e8f0; + border-radius: 10px; + background: #fff; + overflow: visible; + + &__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 24px; + background: #f8f9fa; + border-bottom: 1px solid #e8eaed; + border-radius: 10px 10px 0 0; + } + + &__header-left { + display: flex; + align-items: center; + gap: 12px; + } + + &__icon { + width: 36px; + height: 36px; + border-radius: 8px; + background: #ede9fe; + display: flex; + align-items: center; + justify-content: center; + + i { + font-size: 16px; + color: #7c3aed; + } + } + + &__title { + margin: 0; + font-size: 14px; + font-weight: 600; + color: #202124; + } + + &__sub { + font-size: 11px; + color: #6b7280; + display: block; + } +} + +.soa-pill { + display: inline-flex; + align-items: center; + padding: 3px 12px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + background: #ede9fe; + color: #7c3aed; + border: 1px solid #ddd6fe; + + &--minor { + background: #fef3c7; + color: #b45309; + border-color: #fde68a; + } + + &--senior { + background: #d1fae5; + color: #065f46; + border-color: #a7f3d0; + } + + &--ok { + background: #dcfce7; + color: #15803d; + border-color: #bbf7d0; + } +} + +// Client info bar (auto-detected DOB + Age) +.soa-client-info-bar { + display: flex; + align-items: center; + gap: 20px; + padding: 10px 24px; + background: #f0f9ff; + border-bottom: 1px solid #bae6fd; + flex-wrap: wrap; + + &__item { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + color: #0369a1; + + mat-icon { + font-size: 16px; + height: 16px; + width: 16px; + } + + strong { + font-weight: 700; + } + } +} + +.soa-auto-pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + margin-left: auto; + + mat-icon { + font-size: 13px; + height: 13px; + width: 13px; + } + + &--minor { + background: #fef3c7; + color: #b45309; + border: 1px solid #fde68a; + } + + &--senior { + background: #d1fae5; + color: #065f46; + border: 1px solid #a7f3d0; + } + + &--ok { + background: #dcfce7; + color: #15803d; + border: 1px solid #bbf7d0; + } +} + +// Status tiles +.soa-tiles { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 12px; + padding: 16px 24px; + border-bottom: 1px solid #f1f5f9; + + @media (width <= 900px) { + grid-template-columns: repeat(3, 1fr); + } + + @media (width <= 600px) { + grid-template-columns: repeat(2, 1fr); + } +} + +.soa-tile { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 14px 10px; + border: 2px solid #e2e8f0; + border-radius: 10px; + background: #fff; + cursor: pointer; + transition: + border-color 0.15s, + box-shadow 0.15s; + position: relative; + + &:hover { + border-color: #a5b4fc; + box-shadow: 0 2px 8px rgb(99 102 241 / 12%); + } + + &--selected { + border-color: #6366f1; + background: #eef2ff; + box-shadow: 0 2px 12px rgb(99 102 241 / 20%); + } + + &__icon { + font-size: 28px; + height: 28px; + width: 28px; + } + + &__label { + font-size: 12px; + font-weight: 600; + color: #374151; + text-align: center; + } + + &__check { + position: absolute; + top: 6px; + right: 6px; + font-size: 16px; + height: 16px; + width: 16px; + color: #6366f1; + } + + &__lock { + position: absolute; + top: 4px; + right: 4px; + display: flex; + align-items: center; + + mat-icon { + font-size: 13px; + height: 13px; + width: 13px; + color: #94a3b8; + } + } + + &--disabled { + opacity: 0.45; + cursor: not-allowed; + background: #f8fafc; + border-color: #e2e8f0; + box-shadow: none; + + &:hover { + border-color: #e2e8f0; + box-shadow: none; + } + } +} + +// Block inside SOA card +.soa-block { + padding: 16px 24px; + + &--sep { + border-top: 1px dashed #e2e8f0; + } + + &__head { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 14px; + } + + &__head-left { + display: flex; + align-items: flex-start; + gap: 12px; + } + + &__letter { + width: 28px; + height: 28px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + font-weight: 700; + color: #fff; + flex-shrink: 0; + margin-top: 2px; + } + + &__title { + font-size: 13px; + font-weight: 600; + color: #1f2937; + display: block; + } + + &__hint { + font-size: 11px; + color: #6b7280; + display: block; + } +} + +// Joint holders table +.soa-table-wrap { + overflow-x: auto; + border-radius: 8px; + border: 1px solid #e2e8f0; +} + +.soa-tbl { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.soa-th { + padding: 10px 14px; + background: #f8fafc; + text-align: left; + font-weight: 600; + font-size: 11px; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.04em; + border-bottom: 1px solid #e2e8f0; + + &--sr { + width: 44px; + } + + &--center { + text-align: center; + } +} + +.soa-td { + padding: 10px 14px; + border-bottom: 1px solid #f1f5f9; + vertical-align: middle; + + &--sr { + text-align: center; + font-size: 12px; + color: #94a3b8; + font-weight: 600; + } + + &--center { + text-align: center; + } + + &--total-label { + font-size: 12px; + font-weight: 600; + color: #475569; + text-align: right; + } +} + +.soa-tr { + transition: background 0.1s; + + &:hover { + background: #f8fafc; + } + + &--mandatory { + background: #fdf4ff; + } + + &--primary { + background: #f0f9ff; + border-left: 3px solid #0891b2; + } +} + +.soa-avatar--primary { + background: #cffafe; + color: #0e7490; +} + +.soa-primary-tag { + font-size: 10px; + color: #0e7490; + font-weight: 600; + background: #cffafe; + padding: 1px 7px; + border-radius: 999px; + display: inline-block; +} + +.soa-pct-input { + width: 56px; + padding: 4px 6px; + border: 1px solid #cbd5e1; + border-radius: 6px; + font-size: 13px; + font-weight: 600; + text-align: center; + color: #1f2937; + + &:focus { + outline: none; + border-color: #0891b2; + box-shadow: 0 0 0 2px rgb(8 145 178 / 15%); + } + + &::-webkit-inner-spin-button, + &::-webkit-outer-spin-button { + opacity: 1; + } +} + +.soa-pct-unit { + font-size: 13px; + font-weight: 600; + color: #64748b; + margin-left: 2px; +} + +.soa-no-action { + color: #cbd5e1; + font-size: 16px; +} + +.soa-tr-total { + background: #f8fafc; +} + +.soa-client-cell { + display: flex; + align-items: center; + gap: 10px; +} + +.soa-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background: #dbeafe; + color: #1d4ed8; + font-weight: 700; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + &--mandatory { + background: #ede9fe; + color: #7c3aed; + } +} + +.soa-client-name { + font-weight: 600; + font-size: 13px; + color: #1f2937; + display: block; +} + +.soa-mandatory-tag { + font-size: 10px; + color: #7c3aed; + font-weight: 600; + background: #ede9fe; + padding: 1px 7px; + border-radius: 999px; +} + +.soa-mono { + font-family: 'Roboto Mono', monospace; + font-size: 12px; + color: #64748b; +} + +.soa-pct-badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 2px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 700; + background: #f1f5f9; + color: #475569; + + &--full, + &--ok { + background: #dcfce7; + color: #15803d; + } + + &--warn { + background: #fef3c7; + color: #b45309; + } +} + +.soa-sign-toggle { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + border: 1px solid #e2e8f0; + cursor: pointer; + transition: all 0.15s; + + mat-icon { + font-size: 14px; + height: 14px; + width: 14px; + } + + &--mandatory { + background: #ede9fe; + color: #7c3aed; + border-color: #ddd6fe; + } + + &--optional { + background: #f8fafc; + color: #94a3b8; + } +} + +.soa-mandatory-count { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 12px; + font-weight: 600; + color: #7c3aed; + + mat-icon { + font-size: 14px; + height: 14px; + width: 14px; + } +} + +.soa-alert { + display: flex; + align-items: center; + gap: 8px; + margin-top: 10px; + padding: 8px 14px; + border-radius: 8px; + font-size: 13px; + font-weight: 500; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + } + + &--warn { + background: #fef3c7; + color: #b45309; + } + + &--ok { + background: #dcfce7; + color: #15803d; + } +} + +.soa-empty { + display: flex; + align-items: center; + gap: 10px; + padding: 20px; + background: #f8fafc; + border-radius: 8px; + font-size: 13px; + color: #64748b; + + mat-icon { + color: #94a3b8; + font-size: 22px; + height: 22px; + width: 22px; + } +} + +.soa-add-btn { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; +} + +// Mode of operation grid +.soa-mode-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px; + + @media (width <= 700px) { + grid-template-columns: repeat(2, 1fr); + } +} + +.soa-mode-card { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 12px 8px; + border: 2px solid #e2e8f0; + border-radius: 10px; + background: #fff; + cursor: pointer; + transition: all 0.15s; + + &:hover { + border-color: #a5b4fc; + } + + &--selected { + border-color: #7c3aed; + background: #faf5ff; + } + + &__icon { + font-size: 24px; + height: 24px; + width: 24px; + color: #7c3aed; + } + + &__code { + font-size: 13px; + font-weight: 700; + color: #1f2937; + } + + &__desc { + font-size: 11px; + color: #6b7280; + text-align: center; + } + + &__check { + position: absolute; + top: 6px; + right: 6px; + font-size: 16px; + height: 16px; + width: 16px; + color: #7c3aed; + } +} + +.soa-mode-summary { + display: flex; + align-items: center; + gap: 8px; + margin-top: 12px; + padding: 8px 14px; + border-radius: 8px; + font-size: 13px; + background: #f8fafc; + color: #64748b; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + } + + &--set { + background: #faf5ff; + color: #7c3aed; + } + + &__from { + font-size: 11px; + color: #94a3b8; + } +} + +.soa-auto-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + background: #fef9c3; + color: #854d0e; + border: 1px solid #fde68a; + + mat-icon { + font-size: 13px; + height: 13px; + width: 13px; + } +} + +// Minor account +.soa-minor-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 4px 16px; + margin-bottom: 16px; + + @media (width <= 700px) { + grid-template-columns: 1fr; + } +} + +.soa-field { + width: 100%; +} + +.soa-guardian-search { + margin-bottom: 12px; + + &__label { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 600; + color: #374151; + margin-bottom: 8px; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + color: #7c3aed; + } + } +} + +.soa-guardian-selected { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 14px; + background: #f0fdf4; + border: 1px solid #bbf7d0; + border-radius: 8px; + margin-top: 10px; + + &__icon { + font-size: 24px; + height: 24px; + width: 24px; + color: #16a34a; + } + + &__name { + font-size: 13px; + font-weight: 600; + color: #1f2937; + } + + &__cif { + font-size: 11px; + color: #6b7280; + } +} + +// Senior citizen benefits +.soa-sc-benefits { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; + margin-bottom: 14px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.soa-sc-benefit { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px; + background: #f0fdf4; + border: 1px solid #bbf7d0; + border-radius: 8px; + + mat-icon { + color: #16a34a; + font-size: 20px; + height: 20px; + width: 20px; + flex-shrink: 0; + margin-top: 2px; + } + + strong { + display: block; + font-size: 13px; + color: #1f2937; + font-weight: 600; + } + + span { + font-size: 11px; + color: #6b7280; + } +} + +// Individual Male / Female — Category grid +.soa-category-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; + margin-bottom: 16px; + + @media (width <= 700px) { + grid-template-columns: repeat(2, 1fr); + } +} + +.soa-cat-tile { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 14px 10px; + border: 2px solid #e2e8f0; + border-radius: 10px; + background: #fff; + cursor: pointer; + transition: all 0.15s; + + &:hover { + border-color: #a5b4fc; + box-shadow: 0 2px 8px rgb(99 102 241 / 12%); + } + + &--selected { + &.soa-cat-tile--male { + border-color: #3b82f6; + background: #eff6ff; + box-shadow: 0 2px 12px rgb(59 130 246 / 20%); + } + + &.soa-cat-tile--female { + border-color: #ec4899; + background: #fdf2f8; + box-shadow: 0 2px 12px rgb(236 72 153 / 20%); + } + } + + &__icon { + font-size: 26px; + height: 26px; + width: 26px; + color: #64748b; + } + + &--selected.soa-cat-tile--male &__icon { + color: #3b82f6; + } + + &--selected.soa-cat-tile--female &__icon { + color: #ec4899; + } + + &__label { + font-size: 11px; + font-weight: 600; + color: #374151; + text-align: center; + line-height: 1.3; + } + + &__check { + position: absolute; + top: 6px; + right: 6px; + font-size: 16px; + height: 16px; + width: 16px; + + .soa-cat-tile--male & { + color: #3b82f6; + } + + .soa-cat-tile--female & { + color: #ec4899; + } + } +} + +// Single Account — Nominee section +.soa-nominee-section { + margin-bottom: 16px; + + &__header { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + font-weight: 600; + color: #0e7490; + margin-bottom: 12px; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + } + } +} + +.soa-field--full { + grid-column: 1 / -1; +} + +// PoA section +.soa-poa-section { + margin-bottom: 14px; + padding: 12px 14px; + background: #faf5ff; + border: 1px dashed #ddd6fe; + border-radius: 8px; + + &__header { + display: flex; + align-items: center; + gap: 10px; + font-size: 13px; + font-weight: 600; + color: #6d28d9; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + } + } +} + +// Toggle switch +.soa-toggle { + position: relative; + display: inline-block; + width: 40px; + height: 22px; + flex-shrink: 0; + + input { + opacity: 0; + width: 0; + height: 0; + } + + &__slider { + position: absolute; + cursor: pointer; + inset: 0; + background: #cbd5e1; + border-radius: 22px; + transition: background 0.2s; + + &::before { + content: ''; + position: absolute; + height: 16px; + width: 16px; + left: 3px; + bottom: 3px; + background: #fff; + border-radius: 50%; + transition: transform 0.2s; + } + } + + input:checked + &__slider { + background: #7c3aed; + + &::before { + transform: translateX(18px); + } + } + + &__label { + font-size: 12px; + color: #6b7280; + font-weight: 500; + } +} + +// Info banners +.soa-info-banner { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px 14px; + border-radius: 8px; + font-size: 12px; + background: #eff6ff; + border: 1px solid #bfdbfe; + color: #1e40af; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + flex-shrink: 0; + margin-top: 2px; + } + + ul { + margin: 4px 0 0; + padding-left: 18px; + + li { + margin-bottom: 3px; + } + } + + &--minor { + background: #fefce8; + border-color: #fde68a; + color: #92400e; + } + + &--senior { + background: #f0fdf4; + border-color: #bbf7d0; + color: #065f46; + } + + &--male { + background: #eff6ff; + border-color: #bfdbfe; + color: #1e40af; + } + + &--female { + background: #fdf2f8; + border-color: #f9a8d4; + color: #9d174d; + } +} diff --git a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts index f4af5679d..d9d9558a3 100644 --- a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts +++ b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts @@ -1,4 +1,4 @@ -/** +/** * Copyright since 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public @@ -11,13 +11,17 @@ import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorR import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule, FormsModule } from '@angular/forms'; import { Dates } from 'app/core/utils/dates'; import { SettingsService } from 'app/settings/settings.service'; +import { MatDialog } from '@angular/material/dialog'; /** Custom Services */ import { SavingsService } from 'app/savings/savings.service'; +import { ClientsService } from 'app/clients/clients.service'; import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component'; +import { MatIcon } from '@angular/material/icon'; +import { AddRelatedClientDialogComponent } from 'app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component'; /** * Savings Account Details Step @@ -32,30 +36,27 @@ import { IntroducerSearchComponent } from 'app/shared/introducer-search/introduc FaIconComponent, MatStepperNext, IntroducerSearchComponent, - FormsModule + FormsModule, + MatIcon ] }) export class SavingsAccountDetailsStepComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); private savingsService = inject(SavingsService); + private clientsService = inject(ClientsService); private settingsService = inject(SettingsService); private dateUtils = inject(Dates); private cdr = inject(ChangeDetectorRef); + private dialog = inject(MatDialog); /** Savings Account Template */ @Input() savingsAccountTemplate: any; - /** Minimum date allowed. */ minDate = new Date(2000, 0, 1); - /** Maximum date allowed. */ maxDate = new Date(); - /** Product Data */ productData: any; - /** Field Officer Data */ fieldOfficerData: any; - /** For edit savings form */ isFieldOfficerPatched = false; - /** Savings Account Details Form */ savingsAccountDetailsForm: UntypedFormGroup; savingsProductSelected = false; @@ -63,22 +64,93 @@ export class SavingsAccountDetailsStepComponent implements OnInit { introducerName: string | null = null; introducerCif: string | null = null; - /** Savings Account Template with product data */ @Output() savingsAccountProductTemplate = new EventEmitter(); - /** Local copy of product template for displaying classification info */ productTemplate: any = null; accountTypeOptions: string[] = []; customerSegmentOptions: string[] = []; selectedAccountType: string | null = null; selectedCustomerSegment: string | null = null; - /** - * Sets share account details form. - * @param {FormBuilder} formBuilder Form Builder. - * @param {SavingsService} savingsService Savings Service. - * @param {SettingsService} settingsService Setting service - */ + // ── Client info (auto-fetched) ───────────────────────────────────────── + clientDob: Date | null = null; + clientAge: number | null = null; + clientGender: string | null = null; + primaryClientName = ''; + primaryClientCif = ''; + primaryClientId: number | null = null; + primaryHolderShare = 0; + + // ── Status of Account (RBI) ──────────────────────────────────────────── + readonly accountStatusOptions = [ + { value: 'INDIVIDUAL_MALE', label: 'Individual (Male)', icon: 'person', color: '#3b82f6' }, + { value: 'INDIVIDUAL_FEMALE', label: 'Individual (Female)', icon: 'person_outline', color: '#ec4899' }, + { value: 'SINGLE', label: 'Single Account', icon: 'account_circle', color: '#0891b2' }, + { value: 'JOINT', label: 'Joint Account', icon: 'group', color: '#8b5cf6' }, + { value: 'MINOR', label: 'Minor Account', icon: 'child_care', color: '#f59e0b' }, + { value: 'SENIOR_CITIZEN', label: 'Senior Citizen', icon: 'elderly', color: '#10b981' } + ]; + + selectedAccountStatus = ''; + + // ── Joint Account ────────────────────────────────────────────────────── + jointHolders: Array<{ + clientId: number; + clientName: string; + clientCif: string; + sharePercentage: number; + isMandatory: boolean; + signingAuthority?: string; + role?: string; + }> = []; + + modeOfOperation = ''; + + readonly modeOfOperationOptions = [ + { value: 'EITHER_OR_SURVIVOR', label: 'E or S', sublabel: 'Either or Survivor', icon: 'how_to_vote' }, + { value: 'FORMER_OR_SURVIVOR', label: 'F or S', sublabel: 'Former or Survivor', icon: 'person' }, + { value: 'JOINTLY', label: 'Jointly', sublabel: 'All Must Sign', icon: 'groups' }, + { value: 'ANYONE_OR_SURVIVOR', label: 'Anyone', sublabel: 'Anyone or Survivor', icon: 'group' } + ]; + + // ── Individual Male — Special Category ──────────────────────────────── + selectedSpecialCategory = ''; + + readonly specialCategoryOptions = [ + { value: 'DEFENCE', label: 'Defence Personnel', icon: 'military_tech' }, + { value: 'EX_SERVICEMAN', label: 'Ex-Serviceman', icon: 'shield' }, + { value: 'GOVT_EMPLOYEE', label: 'Government Employee', icon: 'account_balance' }, + { value: 'PHYSICALLY_CHALLENGED', label: 'Physically Challenged', icon: 'accessible' }, + { value: 'NRI', label: 'NRI (Non-Resident Indian)', icon: 'flight' }, + { value: 'NONE', label: 'None / General', icon: 'person' } + ]; + + // ── Individual Female — Weaker Section ──────────────────────────────── + selectedWeakerSection = ''; + + readonly weakerSectionOptions = [ + { value: 'BPL', label: 'BPL (Below Poverty Line)', icon: 'volunteer_activism' }, + { value: 'SC_ST', label: 'SC / ST Category', icon: 'groups' }, + { value: 'MINORITY', label: 'Minority Community', icon: 'diversity_3' }, + { value: 'SHG', label: 'Self Help Group Member', icon: 'handshake' }, + { value: 'PMJDY', label: 'PMJDY Beneficiary', icon: 'savings' }, + { value: 'NONE', label: 'None / General', icon: 'person_outline' } + ]; + + // ── Minor Account ────────────────────────────────────────────────────── + guardianClientId: number | null = null; + guardianName = ''; + guardianCif = ''; + guardianRelationship = ''; + minorDob: Date | null = null; + + readonly guardianRelationshipOptions = [ + 'Father', + 'Mother', + 'Legal Guardian', + 'Court Appointed Guardian' + ]; + constructor() { this.createSavingsAccountDetailsForm(); } @@ -102,11 +174,83 @@ export class SavingsAccountDetailsStepComponent implements OnInit { }); } } + + // Auto-detect account status from client DOB + capture primary holder info + const clientId = this.savingsAccountTemplate?.clientId; + if (clientId) { + this.primaryClientId = clientId; + this.clientsService.getClientData(String(clientId)).subscribe((client: any) => { + this.primaryClientName = client?.displayName ?? client?.fullname ?? ''; + this.primaryClientCif = client?.cifNumber ?? client?.cifNo ?? ''; + if (client?.dateOfBirth) { + const dob = this.parseClientDob(client.dateOfBirth); + if (dob) { + this.clientDob = dob; + this.clientAge = this.calculateAge(dob); + this.clientGender = client.gender?.name?.toLowerCase() ?? null; + this.autoDetectAccountStatus(); + } + } + this.cdr.detectChanges(); + }); + } + } + + /** Parse DOB from backend — handles array [yyyy, mm, dd] or string */ + private parseClientDob(dob: any): Date | null { + if (!dob) return null; + if (Array.isArray(dob) && dob.length >= 3) { + return new Date(dob[0], dob[1] - 1, dob[2]); + } + const d = new Date(dob); + return isNaN(d.getTime()) ? null : d; + } + + /** Calculate full years from a date to today */ + private calculateAge(dob: Date): number { + const today = this.settingsService.businessDate ?? new Date(); + let age = today.getFullYear() - dob.getFullYear(); + const monthDiff = today.getMonth() - dob.getMonth(); + if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < dob.getDate())) { + age--; + } + return age; + } + + isTileDisabled(value: string): boolean { + if (value === 'MINOR') return this.clientAge === null || this.clientAge >= 18; + if (value === 'SENIOR_CITIZEN') return this.clientAge === null || this.clientAge < 60; + return false; + } + + getTileTooltip(value: string): string { + if (value === 'MINOR' && this.clientAge !== null && this.clientAge >= 18) { + return `Age ${this.clientAge} yrs — Minor only allowed below 18 years`; + } + if (value === 'SENIOR_CITIZEN' && this.clientAge !== null && this.clientAge < 60) { + return `Age ${this.clientAge} yrs — Senior Citizen only allowed at 60+ years`; + } + if (value === 'MINOR' && this.clientAge === null) return 'DOB not available — cannot verify age'; + if (value === 'SENIOR_CITIZEN' && this.clientAge === null) return 'DOB not available — cannot verify age'; + return ''; + } + + /** Auto-set status tile based on age; pre-fill minorDob if minor */ + private autoDetectAccountStatus(): void { + if (this.clientAge === null) return; + + if (this.clientAge < 18) { + this.selectedAccountStatus = 'MINOR'; + this.minorDob = this.clientDob; + } else if (this.clientAge >= 60) { + this.selectedAccountStatus = 'SENIOR_CITIZEN'; + } else if (this.clientGender === 'female') { + this.selectedAccountStatus = 'INDIVIDUAL_FEMALE'; + } else { + this.selectedAccountStatus = 'INDIVIDUAL_MALE'; + } } - /** - * Creates savings account details form. - */ createSavingsAccountDetailsForm() { this.savingsAccountDetailsForm = this.formBuilder.group({ productId: [ @@ -122,9 +266,6 @@ export class SavingsAccountDetailsStepComponent implements OnInit { }); } - /** - * Fetches savings account product template on productId value changes - */ buildDependencies() { const entityId = this.savingsAccountTemplate.groupId || this.savingsAccountTemplate.clientId; this.savingsAccountDetailsForm.get('productId').valueChanges.subscribe((productId: string) => { @@ -150,10 +291,6 @@ export class SavingsAccountDetailsStepComponent implements OnInit { }); } - /** - * Formats template dates into the current CBS date format. - * Handles backend date arrays like [2026, 4, 20] as well as strings and Date objects. - */ private extractOptions(options: any[], fallback?: string): string[] { if (Array.isArray(options) && options.length > 0) { return options.map((o: any) => o.value ?? o.name ?? o); @@ -162,12 +299,8 @@ export class SavingsAccountDetailsStepComponent implements OnInit { } formatTemplateDate(value: any): string { - if (!value) { - return ''; - } - + if (!value) return ''; let dateValue: Date | null = null; - if (value instanceof Date) { dateValue = value; } else if (Array.isArray(value) && value.length >= 3) { @@ -175,30 +308,145 @@ export class SavingsAccountDetailsStepComponent implements OnInit { } else { dateValue = new Date(value); } - if (!dateValue || Number.isNaN(dateValue.getTime())) { return Array.isArray(value) ? value.join('/') : String(value); } - return this.dateUtils.formatDate(dateValue, 'M/d/yyyy') || ''; } - /** - * Returns savings account form value. - */ onIntroducerChange(event: { clientId: number; displayName: string; cifNumber?: string } | null): void { this.introducerId = event?.clientId ?? null; this.introducerName = event?.displayName ?? null; this.introducerCif = event?.cifNumber ?? null; } + onGuardianChange(event: { clientId: number; displayName: string; cifNumber?: string } | null): void { + this.guardianClientId = event?.clientId ?? null; + this.guardianName = event?.displayName ?? ''; + this.guardianCif = event?.cifNumber ?? ''; + } + + // ── Joint Account helpers ────────────────────────────────────────────── + openAddJointHolderDialog(): void { + const ref = this.dialog.open(AddRelatedClientDialogComponent, { + width: '500px', + panelClass: 'arc-dialog-panel', + disableClose: true + }); + ref.afterClosed().subscribe((result) => { + if (result) { + const duplicate = this.jointHolders.some((h) => h.clientCif && h.clientCif === result.clientCif); + if (duplicate) { + alert(`Account holder with CIF ${result.clientCif} is already added.`); + return; + } + this.jointHolders = [ + ...this.jointHolders, + { ...result, isMandatory: false }]; + this.autoSetModeOfOperation(); + } + }); + } + + removeJointHolder(index: number): void { + this.jointHolders = this.jointHolders.filter((_, i) => i !== index); + this.autoSetModeOfOperation(); + } + + toggleJointHolderMandatory(holder: any): void { + holder.isMandatory = !holder.isMandatory; + this.autoSetModeOfOperation(); + } + + autoSetModeOfOperation(): void { + const mandatory = this.jointHolders.filter((h) => h.isMandatory).length; + if (mandatory === 0) { + this.modeOfOperation = 'EITHER_OR_SURVIVOR'; + } else if (mandatory === 1) { + this.modeOfOperation = 'FORMER_OR_SURVIVOR'; + } else { + this.modeOfOperation = 'JOINTLY'; + } + } + + get totalJointSharePercentage(): number { + return (this.primaryHolderShare || 0) + this.jointHolders.reduce((sum, h) => sum + (h.sharePercentage || 0), 0); + } + + get mandatoryJointCount(): number { + return this.jointHolders.filter((h) => h.isMandatory).length; + } + + get selectedModeLabel(): string { + return this.modeOfOperationOptions.find((o) => o.value === this.modeOfOperation)?.sublabel ?? ''; + } + + // ── Minor Account helpers ────────────────────────────────────────────── + get majorityDate(): string { + const dob = this.minorDob ?? this.clientDob; + if (!dob) return ''; + const d = new Date(dob); + d.setFullYear(d.getFullYear() + 18); + return this.dateUtils.formatDate(d, 'M/d/yyyy') || ''; + } + get savingsAccountDetails() { const value = this.savingsAccountDetailsForm.getRawValue(); + + // Product Classification — user-selected values + if (this.selectedAccountType) value.accountType = this.selectedAccountType; + if (this.selectedCustomerSegment) value.customerSegment = this.selectedCustomerSegment; + if (this.introducerId) { value.introducerId = this.introducerId; value.introducerName = this.introducerName; - value.introducerCif = this.introducerCif; + value.introducerCifNumber = this.introducerCif; } + + if (this.selectedAccountStatus) { + value.accountStatus = this.selectedAccountStatus; + } + + if (this.selectedAccountStatus === 'JOINT') { + value.modeOfOperation = this.modeOfOperation || null; + value.jointHolders = [ + { + clientId: this.primaryClientId, + displayName: this.primaryClientName, + cifNumber: this.primaryClientCif, + sharePercentage: this.primaryHolderShare, + isMandatory: true, + isPrimary: true + }, + ...this.jointHolders.map((h) => ({ + clientId: h.clientId, + displayName: h.clientName, + cifNumber: h.clientCif, + sharePercentage: h.sharePercentage, + isPrimary: false, + isMandatory: h.isMandatory, + signingAuthority: h.signingAuthority || null, + role: h.role || null + })) + ]; + } + + if (this.selectedAccountStatus === 'INDIVIDUAL_MALE' && this.selectedSpecialCategory) { + value.specialCategory = this.selectedSpecialCategory; + } + + if (this.selectedAccountStatus === 'INDIVIDUAL_FEMALE' && this.selectedWeakerSection) { + value.weakerSection = this.selectedWeakerSection; + } + + if (this.selectedAccountStatus === 'MINOR') { + value.guardianClientId = this.guardianClientId; + value.guardianName = this.guardianName; + value.guardianCifNumber = this.guardianCif; + value.guardianRelationship = this.guardianRelationship; + value.minorDateOfBirth = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'yyyy-MM-dd') : null; + } + return value; } } diff --git a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.html b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.html index f523e4bd4..0b42b59a5 100644 --- a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.html +++ b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.html @@ -63,16 +63,16 @@
- @if (savingsAccountProductTemplate?.accountType) { + @if (savingsAccount?.accountType || savingsAccountProductTemplate?.accountType) {
Account Type - {{ savingsAccountProductTemplate?.accountType }} + {{ savingsAccount?.accountType || savingsAccountProductTemplate?.accountType }}
} - @if (savingsAccountProductTemplate?.customerSegment) { + @if (savingsAccount?.customerSegment || savingsAccountProductTemplate?.customerSegment) {
Customer Segment - {{ savingsAccountProductTemplate?.customerSegment }} + {{ savingsAccount?.customerSegment || savingsAccountProductTemplate?.customerSegment }}
} @if (savingsAccountProductTemplate?.productCode) { @@ -135,10 +135,10 @@ Name {{ savingsAccount?.introducerName || '—' }}
- @if (savingsAccount?.introducerCif) { + @if (savingsAccount?.introducerCifNumber) {
CIF No - {{ savingsAccount.introducerCif }} + {{ savingsAccount.introducerCifNumber }}
}
@@ -150,7 +150,155 @@
} - + + @if (savingsAccount?.accountStatus) { +
+
+ + Status of Account + RBI Guidelines +
+
+ + +
+ + @switch (savingsAccount.accountStatus) { + @case ('INDIVIDUAL_MALE') { Individual (Male) } + @case ('INDIVIDUAL_FEMALE') { Individual (Female) } + @case ('SINGLE') { Single Account } + @case ('JOINT') { Joint Account } + @case ('MINOR') { Minor Account } + @case ('SENIOR_CITIZEN') { Senior Citizen } + @default { {{ savingsAccount.accountStatus }} } + } + +
+ + + @if (savingsAccount.accountStatus === 'JOINT') { +
+
+ Mode of Operation + + @switch (savingsAccount.modeOfOperation) { + @case ('EITHER_OR_SURVIVOR') { Either or Survivor } + @case ('FORMER_OR_SURVIVOR') { Former or Survivor } + @case ('JOINTLY') { Jointly } + @case ('ANYONE_OR_SURVIVOR') { Anyone or Survivor } + @default { {{ savingsAccount.modeOfOperation || '—' }} } + } + +
+
+ @if (savingsAccount.jointHolders?.length) { +
+ + + + + + + + + + + + + @for (holder of savingsAccount.jointHolders; track holder.clientId; let i = $index) { + + + + + + + + + } + +
Sr.Holder NameCIF No.Ownership %SigningRole
{{ i + 1 }} + {{ holder.displayName || '—' }} + @if (holder.isPrimary) { Primary } + {{ holder.cifNumber || '—' }}{{ holder.sharePercentage ?? '—' }}%{{ holder.isMandatory ? 'Mandatory' : 'Optional' }}{{ holder.isPrimary ? 'Primary Account Holder' : 'Co-Holder' }}
+
+ } + } + + + @if (savingsAccount.accountStatus === 'INDIVIDUAL_MALE' && savingsAccount.specialCategory) { +
+
+ Special Category + + @switch (savingsAccount.specialCategory) { + @case ('DEFENCE') { Defence Personnel } + @case ('EX_SERVICEMAN') { Ex-Serviceman } + @case ('GOVT_EMPLOYEE') { Government Employee } + @case ('PHYSICALLY_CHALLENGED') { Physically Challenged } + @case ('NRI') { NRI (Non-Resident Indian) } + @case ('NONE') { None / General } + @default { {{ savingsAccount.specialCategory }} } + } + +
+
+ } + + + @if (savingsAccount.accountStatus === 'INDIVIDUAL_FEMALE' && savingsAccount.weakerSection) { +
+
+ Weaker Section + + @switch (savingsAccount.weakerSection) { + @case ('BPL') { BPL (Below Poverty Line) } + @case ('SC_ST') { SC / ST Category } + @case ('MINORITY') { Minority Community } + @case ('SHG') { Self Help Group Member } + @case ('PMJDY') { PMJDY Beneficiary } + @case ('NONE') { None / General } + @default { {{ savingsAccount.weakerSection }} } + } + +
+
+ } + + + @if (savingsAccount.accountStatus === 'MINOR') { +
+ @if (savingsAccount.guardianName) { +
+ Guardian Name + {{ savingsAccount.guardianName }} +
+ } + @if (savingsAccount.guardianCifNumber) { +
+ Guardian CIF + {{ savingsAccount.guardianCifNumber }} +
+ } + @if (savingsAccount.guardianRelationship) { +
+ Relationship + {{ savingsAccount.guardianRelationship }} +
+ } + @if (savingsAccount.minorDateOfBirth) { +
+ Minor DOB + {{ savingsAccount.minorDateOfBirth }} +
+ } +
+ } + +
+
+ } + +
diff --git a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.scss b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.scss index 25e0a4e09..f97eaaa4a 100644 --- a/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.scss +++ b/src/app/savings/savings-account-stepper/savings-account-preview-step/savings-account-preview-step.component.scss @@ -323,3 +323,75 @@ .margin-t { margin-top: 1em; } + +// ── Status of Account preview ───────────────────────────────────────────── + +.preview-soa-status-row { + display: flex; + align-items: center; + gap: 10px; +} + +.preview-soa-tile { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 20px; + border-radius: 999px; + font-size: 14px; + font-weight: 700; + background: #eff6ff; + color: #1d4ed8; + border: 2px solid #bfdbfe; + + i { font-size: 16px; } +} + +.preview-soa-table-wrap { + margin-top: 14px; + overflow-x: auto; + border-radius: 8px; + border: 1px solid #e2e8f0; +} + +.preview-soa-tbl { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.preview-soa-th { + background: #f8fafc; + padding: 8px 12px; + text-align: left; + font-weight: 600; + color: #475569; + border-bottom: 1px solid #e2e8f0; + white-space: nowrap; +} + +.preview-soa-td { + padding: 10px 12px; + border-bottom: 1px solid #f1f5f9; + color: #0f172a; + vertical-align: middle; +} + +.preview-soa-tr--primary { + background: #f0f9ff; + + .preview-soa-td { font-weight: 600; } +} + +.preview-soa-primary-tag { + display: inline-block; + margin-left: 6px; + padding: 1px 8px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + background: #dbeafe; + color: #1d4ed8; + border: 1px solid #bfdbfe; + vertical-align: middle; +} diff --git a/src/app/savings/savings.service.ts b/src/app/savings/savings.service.ts index bd5078871..52480fc4c 100644 --- a/src/app/savings/savings.service.ts +++ b/src/app/savings/savings.service.ts @@ -64,6 +64,11 @@ export class SavingsService { return this.http.get(`/savingsaccounts/${accountId}`, { params: httpParams }); } + getClientSavingsAccounts(clientId: string): Observable { + const httpParams = new HttpParams().set('clientId', clientId); + return this.http.get('/savingsaccounts', { params: httpParams }); + } + /** * @param accountId Savings Account Id of account to get data for. * @returns {Observable} Savings account and template. diff --git a/src/app/shared/introducer-search/introducer-search.component.ts b/src/app/shared/introducer-search/introducer-search.component.ts index 703b691fa..adf14cc46 100644 --- a/src/app/shared/introducer-search/introducer-search.component.ts +++ b/src/app/shared/introducer-search/introducer-search.component.ts @@ -84,7 +84,7 @@ export class IntroducerSearchComponent { this.introducerChange.emit({ clientId: client.id, displayName: client.displayName, - cifNumber: client.cifNumber || client.accountNumber || '' + cifNumber: client.cifNumber || client.cifNo || '' }); } diff --git a/src/app/shared/nominee-form/nominee-form.component.ts b/src/app/shared/nominee-form/nominee-form.component.ts index d016d8a6b..8d0e05652 100644 --- a/src/app/shared/nominee-form/nominee-form.component.ts +++ b/src/app/shared/nominee-form/nominee-form.component.ts @@ -183,7 +183,7 @@ export class NomineeFormComponent implements OnInit { mode: 'linked', clientId: client.id, displayName: client.displayName, - cifNumber: client.accountNo || client.cifNumber || '' + cifNumber: client.cifNumber || client.cifNo || '' }); } From 0d3d83799b28b7d9cb9fa43fa482ddc844fa54dc Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Tue, 28 Apr 2026 16:22:26 +0530 Subject: [PATCH 11/12] save saving ui update and current account update priview section --- .../create-current-account.component.ts | 23 +- .../current-account-details-step.component.ts | 96 ++++--- ...urrent-account-preview-step.component.html | 269 +++++++++++++++++- ...urrent-account-preview-step.component.scss | 93 +++++- ...deposits-account-details-step.component.ts | 35 ++- .../savings-account-details-step.component.ts | 8 +- src/environments/.env.ts | 5 +- 7 files changed, 476 insertions(+), 53 deletions(-) diff --git a/src/app/current-accounts/create-current-account/create-current-account.component.ts b/src/app/current-accounts/create-current-account/create-current-account.component.ts index 1329e5312..4c4c17a11 100644 --- a/src/app/current-accounts/create-current-account/create-current-account.component.ts +++ b/src/app/current-accounts/create-current-account/create-current-account.component.ts @@ -135,20 +135,37 @@ export class CreateCurrentAccountComponent { const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; const monthDayFormat = 'dd MMMM'; - const currentAccount = { - ...this.currentAccount, + const details = this.currentAccount; + + const currentAccount: any = { + ...details, ...this.currentAccountCurrencyStep.currencyStepData, + ...(this.currentAccountDigitalStep?.currentAccountDigital ?? {}), charges: [], - submittedOnDate: this.dateUtils.formatDate(this.currentAccount.submittedOnDate, dateFormat), + submittedOnDate: this.dateUtils.formatDate(details.submittedOnDate, dateFormat), dateFormat, monthDayFormat, locale }; + + if (details.establishmentDate) { + currentAccount.establishmentDate = this.dateUtils.formatDate(details.establishmentDate, dateFormat); + } + if (details.startOperationDate) { + currentAccount.startOperationDate = this.dateUtils.formatDate(details.startOperationDate, dateFormat); + } + + // strip UI-only flags not needed by backend + delete currentAccount.isNonIndividualClient; + if (this.currentAccountTemplate.clientId) { currentAccount.clientId = this.currentAccountTemplate.clientId; } else { currentAccount.groupId = this.currentAccountTemplate.groupId; } + + console.log('Current Account Payload:', JSON.stringify(currentAccount, null, 2)); + this.currentAccountsService.createCurrentAccount(currentAccount).subscribe((response: any) => { this.router.navigate( [ diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts index 3cc5083de..cd2f8d1e4 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts @@ -117,6 +117,8 @@ export class CurrentAccountDetailsStepComponent implements OnInit { clientCif: string; sharePercentage: number; isMandatory: boolean; + signingAuthority?: string; + role?: string; }> = []; modeOfOperation = ''; @@ -159,7 +161,12 @@ export class CurrentAccountDetailsStepComponent implements OnInit { guardianRelationship = ''; minorDob: Date | null = null; - readonly guardianRelationshipOptions = ['Father', 'Mother', 'Legal Guardian', 'Court Appointed Guardian']; + readonly guardianRelationshipOptions = [ + 'Father', + 'Mother', + 'Legal Guardian', + 'Court Appointed Guardian' + ]; constructor() { this.createCurrentAccountDetailsForm(); @@ -209,8 +216,14 @@ export class CurrentAccountDetailsStepComponent implements OnInit { createCurrentAccountDetailsForm() { this.currentAccountDetailsForm = this.formBuilder.group({ - productId: ['', Validators.required], - submittedOnDate: ['', Validators.required], + productId: [ + '', + Validators.required + ], + submittedOnDate: [ + '', + Validators.required + ], fieldOfficerId: [''], externalId: [''], balanceSwapOption: [false], @@ -357,7 +370,9 @@ export class CurrentAccountDetailsStepComponent implements OnInit { alert(`Account holder with CIF ${result.clientCif} is already added.`); return; } - this.jointHolders = [...this.jointHolders, { ...result, isMandatory: false }]; + this.jointHolders = [ + ...this.jointHolders, + { ...result, isMandatory: false }]; this.autoSetModeOfOperation(); } }); @@ -414,7 +429,9 @@ export class CurrentAccountDetailsStepComponent implements OnInit { alert(`Partner with CIF ${result.clientCif} is already added.`); return; } - this.relatedClients = [...this.relatedClients, { ...result, isMandatory: false }]; + this.relatedClients = [ + ...this.relatedClients, + { ...result, isMandatory: false }]; this.autoSetSignatoryRights(); } }); @@ -457,6 +474,48 @@ export class CurrentAccountDetailsStepComponent implements OnInit { value.introducerName = this.introducerName; value.introducerCif = this.introducerCif; } + value.isNonIndividualClient = this.isNonIndividualClient; + + // accountStatus applies to all client types + value.accountStatus = this.selectedAccountStatus || null; + if (this.selectedAccountStatus === 'INDIVIDUAL_MALE' && this.selectedSpecialCategory) + value.specialCategory = this.selectedSpecialCategory; + if (this.selectedAccountStatus === 'INDIVIDUAL_FEMALE' && this.selectedWeakerSection) + value.weakerSection = this.selectedWeakerSection; + if (this.selectedAccountStatus === 'JOINT') { + value.modeOfOperation = this.modeOfOperation || null; + value.jointHolders = [ + { + clientId: this.primaryClientId, + clientName: this.primaryClientName, + clientCif: this.primaryClientCif, + sharePercentage: this.primaryHolderShare, + isMandatory: true, + isPrimary: true, + signingAuthority: 'Mandatory', + role: 'Primary Account Holder' + }, + ...this.jointHolders.map((h) => ({ + clientId: h.clientId, + clientName: h.clientName, + clientCif: h.clientCif, + sharePercentage: h.sharePercentage, + isMandatory: h.isMandatory, + isPrimary: false, + signingAuthority: h.signingAuthority || (h.isMandatory ? 'Mandatory' : 'Optional'), + role: h.role || 'Co-Holder' + })) + ]; + } + if (this.selectedAccountStatus === 'MINOR') { + value.guardianClientId = this.guardianClientId; + value.guardianName = this.guardianName; + value.guardianCif = this.guardianCif; + value.guardianRelationship = this.guardianRelationship; + value.minorDob = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'yyyy-MM-dd') : null; + } + + // non-individual: partners are a separate dataset if (this.isNonIndividualClient) { value.signatoryRights = this.signatoryRights || null; value.partners = this.relatedClients.map((c) => ({ @@ -466,33 +525,6 @@ export class CurrentAccountDetailsStepComponent implements OnInit { sharePercentage: c.sharePercentage, isMandatory: c.isMandatory })); - } else { - if (this.selectedAccountStatus) value.accountStatus = this.selectedAccountStatus; - if (this.selectedAccountStatus === 'INDIVIDUAL_MALE' && this.selectedSpecialCategory) - value.specialCategory = this.selectedSpecialCategory; - if (this.selectedAccountStatus === 'INDIVIDUAL_FEMALE' && this.selectedWeakerSection) - value.weakerSection = this.selectedWeakerSection; - if (this.selectedAccountStatus === 'JOINT') { - value.modeOfOperation = this.modeOfOperation || null; - value.jointHolders = [ - { - clientId: this.primaryClientId, - clientName: this.primaryClientName, - clientCif: this.primaryClientCif, - sharePercentage: this.primaryHolderShare, - isMandatory: true, - isPrimary: true - }, - ...this.jointHolders.map((h) => ({ ...h, isPrimary: false })) - ]; - } - if (this.selectedAccountStatus === 'MINOR') { - value.guardianClientId = this.guardianClientId; - value.guardianName = this.guardianName; - value.guardianCif = this.guardianCif; - value.guardianRelationship = this.guardianRelationship; - value.minorDob = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'yyyy-MM-dd') : null; - } } return value; } diff --git a/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.html index 222c0c142..04797de4c 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.html @@ -42,6 +42,18 @@ Unassigned }
+ @if (currentAccount?.establishmentDate) { +
+ Establishment Date + {{ currentAccount.establishmentDate | dateFormat }} +
+ } + @if (currentAccount?.startOperationDate) { +
+ Start of Operation Date + {{ currentAccount.startOperationDate | dateFormat }} +
+ }
@@ -157,7 +169,262 @@ } - + + @if (currentAccount?.accountStatus) { +
+
+ + Status of Account + RBI Guidelines +
+
+ +
+ + @switch (currentAccount.accountStatus) { + @case ('INDIVIDUAL_MALE') { + Individual (Male) + } + @case ('INDIVIDUAL_FEMALE') { + Individual (Female) + } + @case ('SINGLE') { + Single Account + } + @case ('JOINT') { + Joint Account + } + @case ('MINOR') { + Minor Account + } + @case ('SENIOR_CITIZEN') { + Senior Citizen + } + @default { + {{ currentAccount.accountStatus }} + } + } + +
+ + + @if (currentAccount.accountStatus === 'JOINT') { +
+
+ Mode of Operation + + @switch (currentAccount.modeOfOperation) { + @case ('EITHER_OR_SURVIVOR') { + Either or Survivor + } + @case ('FORMER_OR_SURVIVOR') { + Former or Survivor + } + @case ('JOINTLY') { + Jointly + } + @case ('ANYONE_OR_SURVIVOR') { + Anyone or Survivor + } + @default { + {{ currentAccount.modeOfOperation || '—' }} + } + } + +
+
+ @if (currentAccount.jointHolders?.length) { +
+ + + + + + + + + + + + + @for (holder of currentAccount.jointHolders; track holder.clientId; let i = $index) { + + + + + + + + + } + +
Sr.Holder NameCIF No.Ownership %SigningRole
{{ i + 1 }} + {{ holder.clientName || holder.displayName || '—' }} + @if (holder.isPrimary) { + Primary + } + {{ holder.clientCif || holder.cifNumber || '—' }}{{ holder.sharePercentage ?? '—' }}% + + {{ holder.signingAuthority || (holder.isMandatory ? 'Mandatory' : 'Optional') }} + + + {{ holder.role || (holder.isPrimary ? 'Primary Account Holder' : 'Co-Holder') }} +
+
+ } + } + + + @if (currentAccount.accountStatus === 'INDIVIDUAL_MALE' && currentAccount.specialCategory) { +
+
+ Special Category + + @switch (currentAccount.specialCategory) { + @case ('DEFENCE') { + Defence Personnel + } + @case ('EX_SERVICEMAN') { + Ex-Serviceman + } + @case ('GOVT_EMPLOYEE') { + Government Employee + } + @case ('PHYSICALLY_CHALLENGED') { + Physically Challenged + } + @case ('NRI') { + NRI (Non-Resident Indian) + } + @case ('NONE') { + None / General + } + @default { + {{ currentAccount.specialCategory }} + } + } + +
+
+ } + + + @if (currentAccount.accountStatus === 'INDIVIDUAL_FEMALE' && currentAccount.weakerSection) { +
+
+ Weaker Section + + @switch (currentAccount.weakerSection) { + @case ('BPL') { + BPL (Below Poverty Line) + } + @case ('SC_ST') { + SC / ST Category + } + @case ('MINORITY') { + Minority Community + } + @case ('SHG') { + Self Help Group Member + } + @case ('PMJDY') { + PMJDY Beneficiary + } + @case ('NONE') { + None / General + } + @default { + {{ currentAccount.weakerSection }} + } + } + +
+
+ } + + + @if (currentAccount.accountStatus === 'MINOR') { +
+ @if (currentAccount.guardianName) { +
+ Guardian Name + {{ currentAccount.guardianName }} +
+ } + @if (currentAccount.guardianCif) { +
+ Guardian CIF + {{ currentAccount.guardianCif }} +
+ } + @if (currentAccount.guardianRelationship) { +
+ Relationship + {{ currentAccount.guardianRelationship }} +
+ } + @if (currentAccount.minorDob) { +
+ Minor DOB + {{ currentAccount.minorDob | dateFormat }} +
+ } +
+ } +
+
+ } + + + @if (currentAccount?.isNonIndividualClient) { +
+
+ + Partners / Related Clients + @if (currentAccount?.signatoryRights) { + {{ + currentAccount.signatoryRights + }} + } +
+
+ @if (currentAccount?.partners?.length) { +
+ + + + + + + + + + + + @for (p of currentAccount.partners; track p.clientId; let i = $index) { + + + + + + + + } + +
Sr.Partner / Client NameCIF No.Ownership %Signing
{{ i + 1 }}{{ p.clientName || '—' }}{{ p.clientCif || '—' }}{{ p.sharePercentage ?? '—' }}% + + {{ p.isMandatory ? 'Mandatory' : 'Optional' }} + +
+
+ } @else { +

No related clients / partners added.

+ } +
+
+ } + +
diff --git a/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.scss b/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.scss index fcfa95163..b65af385a 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.scss +++ b/src/app/current-accounts/current-account-stepper/current-account-preview-step/current-account-preview-step.component.scss @@ -112,11 +112,6 @@ font-weight: 500; } -.preview-muted { - color: #94a3b8; - font-style: italic; -} - .preview-mono { font-family: monospace; font-size: 0.88rem; @@ -376,3 +371,91 @@ .margin-t { margin-top: 1em; } + +// ── Status of Account ───────────────────────────────────────────── +.preview-soa-status-row { + display: flex; + align-items: center; + gap: 10px; +} + +.preview-soa-tile { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 20px; + border-radius: 999px; + font-size: 14px; + font-weight: 700; + background: #eff6ff; + color: #1d4ed8; + border: 2px solid #bfdbfe; + + i { + font-size: 16px; + } + + &--entity { + background: #fef3c7; + color: #92400e; + border-color: #fde68a; + } +} + +.preview-soa-table-wrap { + margin-top: 14px; + overflow-x: auto; + border-radius: 8px; + border: 1px solid #e2e8f0; +} + +.preview-soa-tbl { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.preview-soa-th { + background: #f8fafc; + padding: 8px 12px; + text-align: left; + font-weight: 600; + color: #475569; + border-bottom: 1px solid #e2e8f0; + white-space: nowrap; +} + +.preview-soa-td { + padding: 10px 12px; + border-bottom: 1px solid #f1f5f9; + color: #0f172a; + vertical-align: middle; +} + +.preview-soa-tr--primary { + background: #f0f9ff; + + .preview-soa-td { + font-weight: 600; + } +} + +.preview-soa-primary-tag { + display: inline-block; + margin-left: 6px; + padding: 1px 8px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + background: #dbeafe; + color: #1d4ed8; + border: 1px solid #bfdbfe; + vertical-align: middle; +} + +.preview-muted { + color: #94a3b8; + font-size: 13px; + font-style: italic; + margin: 0; +} diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts index 5f8e0063b..1bf189d93 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts @@ -105,6 +105,8 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { clientCif: string; sharePercentage: number; isMandatory: boolean; + signingAuthority?: string; + role?: string; }> = []; modeOfOperation = ''; @@ -147,7 +149,12 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { guardianRelationship = ''; minorDob: Date | null = null; - readonly guardianRelationshipOptions = ['Father', 'Mother', 'Legal Guardian', 'Court Appointed Guardian']; + readonly guardianRelationshipOptions = [ + 'Father', + 'Mother', + 'Legal Guardian', + 'Court Appointed Guardian' + ]; // ── Deposit Account Link ─────────────────────────────────────────────── dalSelectedType: 'SAVINGS' | 'CURRENT' | null = null; @@ -212,7 +219,9 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { this.dalLoading = false; this.cdr.detectChanges(); }, - error: () => { this.dalLoading = false; } + error: () => { + this.dalLoading = false; + } }); } @@ -319,8 +328,14 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { */ createRecurringDepositsAccountDetailsForm() { this.recurringDepositAccountDetailsForm = this.formBuilder.group({ - productId: ['', Validators.required], - submittedOnDate: ['', Validators.required], + productId: [ + '', + Validators.required + ], + submittedOnDate: [ + '', + Validators.required + ], fieldOfficerId: [''], externalId: [''], balanceSwapOption: [false], @@ -424,7 +439,9 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { alert(`Account holder with CIF ${result.clientCif} is already added.`); return; } - this.jointHolders = [...this.jointHolders, { ...result, isMandatory: false }]; + this.jointHolders = [ + ...this.jointHolders, + { ...result, isMandatory: false }]; this.autoSetModeOfOperation(); } }); @@ -490,7 +507,9 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { clientCif: this.primaryClientCif, sharePercentage: this.primaryHolderShare, isMandatory: true, - isPrimary: true + isPrimary: true, + signingAuthority: 'Mandatory', + role: 'Primary Account Holder' }, ...this.jointHolders.map((h) => ({ clientId: h.clientId, @@ -498,7 +517,9 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { clientCif: h.clientCif, sharePercentage: h.sharePercentage, isMandatory: h.isMandatory, - isPrimary: false + isPrimary: false, + signingAuthority: h.signingAuthority || (h.isMandatory ? 'Mandatory' : 'Optional'), + role: h.role || 'Co-Holder' })) ]; } diff --git a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts index d9d9558a3..28fb63a38 100644 --- a/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts +++ b/src/app/savings/savings-account-stepper/savings-account-details-step/savings-account-details-step.component.ts @@ -416,7 +416,9 @@ export class SavingsAccountDetailsStepComponent implements OnInit { cifNumber: this.primaryClientCif, sharePercentage: this.primaryHolderShare, isMandatory: true, - isPrimary: true + isPrimary: true, + signingAuthority: 'Mandatory', + role: 'Primary Account Holder' }, ...this.jointHolders.map((h) => ({ clientId: h.clientId, @@ -425,8 +427,8 @@ export class SavingsAccountDetailsStepComponent implements OnInit { sharePercentage: h.sharePercentage, isPrimary: false, isMandatory: h.isMandatory, - signingAuthority: h.signingAuthority || null, - role: h.role || null + signingAuthority: h.signingAuthority || (h.isMandatory ? 'Mandatory' : 'Optional'), + role: h.role || 'Co-Holder' })) ]; } diff --git a/src/environments/.env.ts b/src/environments/.env.ts index 6dc70b65f..e826c099c 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -5,12 +5,13 @@ * 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/. */ + // IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN! /* tslint:disable */ export default { 'mifos_x': { - 'version': '260427', - 'hash': '65345f99' + 'version': '260428', + 'hash': '31b2490f' }, 'allow_switching_backend_instance': true }; From f2a5035c8bafdcd80592300a437abdd1402f27d1 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Wed, 29 Apr 2026 15:10:26 +0530 Subject: [PATCH 12/12] feat: current account details step and client general tab updates --- .../general-tab/general-tab.component.html | 138 ++++++++++++++++-- .../general-tab/general-tab.component.ts | 3 + .../current-account-details-step.component.ts | 22 ++- src/app/pipes/accounts-filter.pipe.ts | 4 + src/environments/.env.ts | 5 +- 5 files changed, 147 insertions(+), 25 deletions(-) diff --git a/src/app/clients/clients-view/general-tab/general-tab.component.html b/src/app/clients/clients-view/general-tab/general-tab.component.html index 461b246c8..0da32a41a 100644 --- a/src/app/clients/clients-view/general-tab/general-tab.component.html +++ b/src/app/clients/clients-view/general-tab/general-tab.component.html @@ -7,7 +7,6 @@ -->
-

{{ 'labels.heading.Performance History' | translate }}

@@ -164,7 +163,9 @@
-

{{ 'labels.messages.No Active Saving Accounts Available' | translate }}

+

+ {{ 'labels.messages.No Active Saving Accounts Available' | translate }} +

-

{{ 'labels.messages.No Closed Saving Accounts Available' | translate }}

+

+ {{ 'labels.messages.No Closed Saving Accounts Available' | translate }} +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.Account No' | translate }} + + + Current Account Product + + {{ 'labels.inputs.Last Active' | translate }}{{ element.lastActiveTransactionDate | dateFormat }}{{ 'labels.inputs.Balance' | translate }} + {{ element.accountBalance | currency: element.currency?.code || 'INR' : 'symbol-narrow' : '1.2-2' }} + {{ 'labels.inputs.Actions' | translate }} + @if (element.status.submittedAndPendingApproval) { + + } + + +
+ +

No Active Current Accounts Available

+
+
+
+ + } +
-
\ No newline at end of file +
diff --git a/src/app/clients/clients-view/general-tab/general-tab.component.ts b/src/app/clients/clients-view/general-tab/general-tab.component.ts index ed9585096..dbbd1538d 100644 --- a/src/app/clients/clients-view/general-tab/general-tab.component.ts +++ b/src/app/clients/clients-view/general-tab/general-tab.component.ts @@ -228,6 +228,8 @@ export class GeneralTabComponent implements OnDestroy { loanAccounts: any[] = []; /** Savings Accounts Data */ savingAccounts: any[] = []; + /** Current Accounts Data */ + currentAccounts: any[] = []; /** Shares Accounts Data */ shareAccounts: any[] = []; /** Upcoming Charges Data */ @@ -273,6 +275,7 @@ export class GeneralTabComponent implements OnDestroy { (data: { clientAccountsData: any; clientChargesData: any; clientSummary: any; clientCollateralData: any }) => { this.clientAccountData = data.clientAccountsData; this.savingAccounts = data.clientAccountsData?.savingsAccounts ?? []; + this.currentAccounts = data.clientAccountsData?.currentAccounts ?? []; this.loanAccounts = data.clientAccountsData?.loanAccounts ?? []; this.shareAccounts = data.clientAccountsData?.shareAccounts ?? []; diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts index cd2f8d1e4..cc44792be 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.ts @@ -472,10 +472,18 @@ export class CurrentAccountDetailsStepComponent implements OnInit { if (this.introducerId) { value.introducerId = this.introducerId; value.introducerName = this.introducerName; - value.introducerCif = this.introducerCif; + value.introducerCifNumber = this.introducerCif; } value.isNonIndividualClient = this.isNonIndividualClient; + // Product classification fields + if (this.selectedAccountType) { + value.accountType = this.selectedAccountType; + } + if (this.selectedCustomerSegment) { + value.customerSegment = this.selectedCustomerSegment; + } + // accountStatus applies to all client types value.accountStatus = this.selectedAccountStatus || null; if (this.selectedAccountStatus === 'INDIVIDUAL_MALE' && this.selectedSpecialCategory) @@ -487,8 +495,8 @@ export class CurrentAccountDetailsStepComponent implements OnInit { value.jointHolders = [ { clientId: this.primaryClientId, - clientName: this.primaryClientName, - clientCif: this.primaryClientCif, + displayName: this.primaryClientName, + cifNumber: this.primaryClientCif, sharePercentage: this.primaryHolderShare, isMandatory: true, isPrimary: true, @@ -497,8 +505,8 @@ export class CurrentAccountDetailsStepComponent implements OnInit { }, ...this.jointHolders.map((h) => ({ clientId: h.clientId, - clientName: h.clientName, - clientCif: h.clientCif, + displayName: h.clientName, + cifNumber: h.clientCif, sharePercentage: h.sharePercentage, isMandatory: h.isMandatory, isPrimary: false, @@ -510,9 +518,9 @@ export class CurrentAccountDetailsStepComponent implements OnInit { if (this.selectedAccountStatus === 'MINOR') { value.guardianClientId = this.guardianClientId; value.guardianName = this.guardianName; - value.guardianCif = this.guardianCif; + value.guardianCifNumber = this.guardianCif; value.guardianRelationship = this.guardianRelationship; - value.minorDob = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'yyyy-MM-dd') : null; + value.minorDateOfBirth = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'yyyy-MM-dd') : null; } // non-individual: partners are a separate dataset diff --git a/src/app/pipes/accounts-filter.pipe.ts b/src/app/pipes/accounts-filter.pipe.ts index a9f3a02b1..3b7363383 100644 --- a/src/app/pipes/accounts-filter.pipe.ts +++ b/src/app/pipes/accounts-filter.pipe.ts @@ -48,6 +48,10 @@ export class AccountsFilterPipe implements PipeTransform { accounts = accounts.filter((account: any) => { return account.depositType.value === 'Savings'; }); + } else if (checkSavings === 'isCurrent') { + accounts = accounts.filter((account: any) => { + return account.depositType.id === 400 || account.depositType.value === 'Current Deposit'; + }); } if (status === 'closed') { accounts = accounts.filter((account: any) => { diff --git a/src/environments/.env.ts b/src/environments/.env.ts index e826c099c..2a3c8fdfb 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -5,13 +5,12 @@ * 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/. */ - // IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN! /* tslint:disable */ export default { 'mifos_x': { - 'version': '260428', - 'hash': '31b2490f' + 'version': '260429', + 'hash': '0d3d8379' }, 'allow_switching_backend_instance': true };