From 4679c6f729cb023dee4aa832d37f3bd7578f0269 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Mon, 13 Apr 2026 14:02:54 +0530 Subject: [PATCH] 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 };