From 58cef1c20b0d842b1b5551704e12522ccc97e1f7 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Thu, 30 Apr 2026 16:20:35 +0530 Subject: [PATCH 1/2] rd account save api update --- ...te-recurring-deposits-account.component.ts | 37 ++- ...deposits-account-details-step.component.ts | 19 +- ...posits-account-preview-step.component.html | 231 ++++++++++++++++++ ...posits-account-preview-step.component.scss | 124 ++++++++-- .../images/current_account_placeholder.png | Bin 0 -> 27274 bytes src/environments/.env.ts | 4 +- src/theme/_content.scss | 6 +- src/theme/_dark_content.scss | 6 +- 8 files changed, 384 insertions(+), 43 deletions(-) create mode 100644 src/assets/images/current_account_placeholder.png diff --git a/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts b/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts index 46d217f0f..9a73f3c25 100644 --- a/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts +++ b/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts @@ -145,22 +145,41 @@ export class CreateRecurringDepositsAccountComponent { const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; const monthDayFormat = 'dd MMMM'; - const recurringDepositAccount = { - ...this.recurringDepositAccount, - ...this.recurringDepositsAccountCurrencyStep.currencyStepData, + + const details = this.recurringDepositAccount; + const currencyStep = this.recurringDepositsAccountCurrencyStep; + + const recurringDepositAccount: any = { + ...details, clientId: this.recurringDepositsAccountTemplate.clientId, charges: [], isCalendarInherited: false, - submittedOnDate: this.dateUtils.formatDate(this.recurringDepositAccount.submittedOnDate, dateFormat), - expectedFirstDepositOnDate: this.dateUtils.formatDate( - this.recurringDepositAccount.expectedFirstDepositOnDate, - dateFormat - ), + mandatoryRecommendedDepositAmount: 0, + recurringFrequency: 1, + recurringFrequencyType: 2, + submittedOnDate: this.dateUtils.formatDate(details.submittedOnDate, dateFormat), + expectedFirstDepositOnDate: this.dateUtils.formatDate(details.expectedFirstDepositOnDate, dateFormat), dateFormat, monthDayFormat, - locale + locale, + // Currency step overrides + balanceSwapOption: currencyStep?.balanceSwapOption ?? details.balanceSwapOption ?? false, + msme: currencyStep?.msme ?? details.msme ?? false, + autoDebitFacility: currencyStep?.autoDebitFacility ?? details.autoDebitFacility ?? false }; + // Tenure from interest rate chart step (depositPeriod in days) + const tenureDays = this.rdTenureDays; + if (tenureDays && tenureDays > 0) { + recurringDepositAccount.depositPeriod = tenureDays; + recurringDepositAccount.depositPeriodFrequencyId = 0; // 0 = Days + } + + // Nominee from currency step + if (currencyStep?.nomineeData) { + recurringDepositAccount.nominee = currencyStep.nomineeData; + } + this.recurringDepositsService.createRecurringDepositAccount(recurringDepositAccount).subscribe((response: any) => { this.router.navigate( [ diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts index 1bf189d93..62ea2c590 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts @@ -494,6 +494,13 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { get recurringDepositAccountDetails() { const value = this.recurringDepositAccountDetailsForm.value; + if (this.selectedAccountType) { + value.accountType = this.selectedAccountType; + } + if (this.selectedCustomerSegment) { + value.customerSegment = this.selectedCustomerSegment; + } + if (this.selectedAccountStatus) { value.accountStatus = this.selectedAccountStatus; } @@ -503,8 +510,8 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { value.jointHolders = [ { clientId: this.primaryClientId, - clientName: this.primaryClientName, - clientCif: this.primaryClientCif, + displayName: this.primaryClientName, + cifNumber: this.primaryClientCif, sharePercentage: this.primaryHolderShare, isMandatory: true, isPrimary: true, @@ -513,8 +520,8 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { }, ...this.jointHolders.map((h) => ({ clientId: h.clientId, - clientName: h.clientName, - clientCif: h.clientCif, + displayName: h.clientName, + cifNumber: h.clientCif, sharePercentage: h.sharePercentage, isMandatory: h.isMandatory, isPrimary: false, @@ -535,9 +542,9 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { if (this.selectedAccountStatus === 'MINOR') { value.guardianClientId = this.guardianClientId; value.guardianName = this.guardianName; - value.guardianCif = this.guardianCif; + value.guardianCifNumber = this.guardianCif; value.guardianRelationship = this.guardianRelationship; - value.minorDob = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'yyyy-MM-dd') : null; + value.minorDateOfBirth = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'yyyy-MM-dd') : null; } if (this.selectedLinkedAccount) { diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html index d3699f0a1..38cf810f9 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html @@ -43,6 +43,237 @@ + + @if (recurringDepositAccountData?.accountStatus) { +
+
+ + Status of Account + RBI Guidelines +
+
+ +
+ + @switch (recurringDepositAccountData.accountStatus) { + @case ('INDIVIDUAL_MALE') { + Individual (Male) + } + @case ('INDIVIDUAL_FEMALE') { + Individual (Female) + } + @case ('SINGLE') { + Single Account + } + @case ('JOINT') { + Joint Account + } + @case ('MINOR') { + Minor Account + } + @case ('SENIOR_CITIZEN') { + Senior Citizen + } + @default { + {{ recurringDepositAccountData.accountStatus }} + } + } + +
+ + + @if (recurringDepositAccountData.accountStatus === 'JOINT') { +
+
+ Mode of Operation + + @switch (recurringDepositAccountData.modeOfOperation) { + @case ('EITHER_OR_SURVIVOR') { + Either or Survivor + } + @case ('FORMER_OR_SURVIVOR') { + Former or Survivor + } + @case ('JOINTLY') { + Jointly + } + @case ('ANYONE_OR_SURVIVOR') { + Anyone or Survivor + } + @default { + {{ recurringDepositAccountData.modeOfOperation || '—' }} + } + } + +
+
+ @if (recurringDepositAccountData.jointHolders?.length) { +
+ + + + + + + + + + + + + @for (holder of recurringDepositAccountData.jointHolders; track holder.clientId; let i = $index) { + + + + + + + + + } + +
Sr.Holder NameCIF No.Ownership %SigningRole
{{ i + 1 }} + {{ holder.displayName || holder.clientName || '—' }} + @if (holder.isPrimary) { + Primary + } + {{ holder.cifNumber || holder.clientCif || '—' }}{{ holder.sharePercentage ?? '—' }}%{{ holder.isMandatory ? 'Mandatory' : 'Optional' }}{{ holder.isPrimary ? 'Primary Account Holder' : 'Co-Holder' }}
+
+ } + } + + + @if ( + recurringDepositAccountData.accountStatus === 'INDIVIDUAL_MALE' && recurringDepositAccountData.specialCategory + ) { +
+
+ Special Category + + @switch (recurringDepositAccountData.specialCategory) { + @case ('DEFENCE') { + Defence Personnel + } + @case ('EX_SERVICEMAN') { + Ex-Serviceman + } + @case ('GOVT_EMPLOYEE') { + Government Employee + } + @case ('PHYSICALLY_CHALLENGED') { + Physically Challenged + } + @case ('NRI') { + NRI (Non-Resident Indian) + } + @case ('NONE') { + None / General + } + @default { + {{ recurringDepositAccountData.specialCategory }} + } + } + +
+
+ } + + + @if ( + recurringDepositAccountData.accountStatus === 'INDIVIDUAL_FEMALE' && recurringDepositAccountData.weakerSection + ) { +
+
+ Weaker Section + + @switch (recurringDepositAccountData.weakerSection) { + @case ('BPL') { + BPL (Below Poverty Line) + } + @case ('SC_ST') { + SC / ST Category + } + @case ('MINORITY') { + Minority Community + } + @case ('SHG') { + Self Help Group Member + } + @case ('PMJDY') { + PMJDY Beneficiary + } + @case ('NONE') { + None / General + } + @default { + {{ recurringDepositAccountData.weakerSection }} + } + } + +
+
+ } + + + @if (recurringDepositAccountData.accountStatus === 'MINOR') { +
+ @if (recurringDepositAccountData.guardianName) { +
+ Guardian Name + {{ recurringDepositAccountData.guardianName }} +
+ } + @if (recurringDepositAccountData.guardianCifNumber) { +
+ Guardian CIF + {{ recurringDepositAccountData.guardianCifNumber }} +
+ } + @if (recurringDepositAccountData.guardianRelationship) { +
+ Relationship + {{ recurringDepositAccountData.guardianRelationship }} +
+ } + @if (recurringDepositAccountData.minorDateOfBirth) { +
+ Minor DOB + {{ recurringDepositAccountData.minorDateOfBirth }} +
+ } +
+ } +
+
+ } + + + @if (recurringDepositAccountData?.linkedAccountId) { +
+
+ + Linked Deposit Account + {{ recurringDepositAccountData.linkedAccountType }} +
+
+
+
+ Account No + {{ recurringDepositAccountData.linkedAccountNo }} +
+
+ Account Type + {{ recurringDepositAccountData.linkedAccountType }} +
+
+ Account ID + {{ recurringDepositAccountData.linkedAccountId }} +
+
+
+
+ } + @if ( recurringDepositsAccountProductTemplate?.accountType || diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.scss b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.scss index 99d36156b..d64f7dfdd 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.scss +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.scss @@ -16,18 +16,18 @@ // ── Section card ───────────────────────────────────────────────────────────── .preview-section { - border: 1px solid #e0e0e0; - border-radius: 8px; + border: 1px solid #e2e8f0; + border-radius: 10px; overflow: hidden; background: #fff; - box-shadow: 0 1px 3px rgb(0 0 0 / 6%); + box-shadow: 0 1px 4px rgb(0 0 0 / 5%); &__header { display: flex; align-items: center; gap: 8px; - background: #f5f5f5; - border-bottom: 1px solid #e0e0e0; + background: #f8fafc; + border-bottom: 1px solid #e2e8f0; padding: 0.6rem 1rem; } @@ -40,11 +40,11 @@ &__title { flex: 1; - font-size: 0.95rem; - font-weight: 600; - color: #333; + font-size: 0.82rem; + font-weight: 700; + color: #1e293b; text-transform: uppercase; - letter-spacing: 0.04em; + letter-spacing: 0.06em; } &__badge { @@ -93,7 +93,7 @@ // ── Grid ───────────────────────────────────────────────────────────────────── .preview-grid { display: grid; - gap: 0.75rem 1.5rem; + gap: 0.85rem 1.5rem; &--three { grid-template-columns: repeat(3, 1fr); @@ -116,33 +116,38 @@ } .preview-label { - font-size: 0.72rem; - color: #888; - font-weight: 500; + font-size: 0.68rem; + color: #94a3b8; + font-weight: 600; text-transform: uppercase; - letter-spacing: 0.04em; + letter-spacing: 0.05em; } .preview-value { - font-size: 0.92rem; - color: #222; + font-size: 0.9rem; + color: #1e293b; font-weight: 500; } // ── Yes/No badge ───────────────────────────────────────────────────────────── .preview-badge { display: inline-block; - padding: 0.1rem 0.55rem; + padding: 0.15rem 0.6rem; border-radius: 12px; - font-size: 0.78rem; + font-size: 0.75rem; font-weight: 600; - background: #ffebee; - color: #c62828; + background: #fef2f2; + color: #dc2626; width: fit-content; &--yes { - background: #e8f5e9; - color: #2e7d32; + background: #f0fdf4; + color: #15803d; + } + + &--warn { + background: #fef3c7; + color: #92400e; } } @@ -339,6 +344,81 @@ } } +// ── Status of Account (SOA) ─────────────────────────────────────────────────── +.preview-soa-status-row { + display: flex; + align-items: center; + gap: 10px; +} + +.preview-soa-tile { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 20px; + border-radius: 999px; + font-size: 14px; + font-weight: 700; + background: #eff6ff; + color: #1d4ed8; + border: 2px solid #bfdbfe; + + i { + font-size: 16px; + } +} + +.preview-soa-table-wrap { + margin-top: 14px; + overflow-x: auto; + border-radius: 8px; + border: 1px solid #e2e8f0; +} + +.preview-soa-tbl { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.preview-soa-th { + background: #f8fafc; + padding: 8px 12px; + text-align: left; + font-weight: 600; + color: #475569; + border-bottom: 1px solid #e2e8f0; + white-space: nowrap; +} + +.preview-soa-td { + padding: 10px 12px; + border-bottom: 1px solid #f1f5f9; + color: #0f172a; + vertical-align: middle; +} + +.preview-soa-tr--primary { + background: #f0f9ff; + + .preview-soa-td { + font-weight: 600; + } +} + +.preview-soa-primary-tag { + display: inline-block; + margin-left: 6px; + padding: 1px 8px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + background: #dbeafe; + color: #1d4ed8; + border: 1px solid #bfdbfe; + vertical-align: middle; +} + // ── Product feature flag rows (same style as details step) ──────────────────── .preview-feature-list { margin-top: 1rem; diff --git a/src/assets/images/current_account_placeholder.png b/src/assets/images/current_account_placeholder.png new file mode 100644 index 0000000000000000000000000000000000000000..b45fe9be0c90b2a88b8508085c4563199c153d4c GIT binary patch literal 27274 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 Date: Fri, 1 May 2026 10:32:43 +0530 Subject: [PATCH 2/2] feat: update FD product and account creation UI to match RD layout - Matched all FD product stepper steps (Details, Currency, Terms, Settings, Charges, Accounting) to RD product UI - Updated FD account creation stepper structure with conditional steps and Previous/Next navigation --- ...reate-fixed-deposit-account.component.html | 120 ++- .../create-fixed-deposit-account.component.ts | 21 +- ...reate-fixed-deposit-product.component.html | 58 +- ...reate-fixed-deposit-product.component.scss | 48 ++ .../create-fixed-deposit-product.component.ts | 72 +- ...sit-product-accounting-step.component.html | 385 +++------- ...sit-product-accounting-step.component.scss | 307 +++++++- ...posit-product-accounting-step.component.ts | 703 ++++++++---------- ...eposit-product-charges-step.component.html | 121 ++- ...eposit-product-charges-step.component.scss | 34 +- ...posit-product-currency-step.component.html | 138 ++-- ...posit-product-currency-step.component.scss | 63 +- ...deposit-product-currency-step.component.ts | 77 +- ...eposit-product-details-step.component.html | 97 ++- ...eposit-product-details-step.component.scss | 34 +- ...-deposit-product-details-step.component.ts | 98 ++- ...posit-product-settings-step.component.html | 290 +++----- ...posit-product-settings-step.component.scss | 128 +++- ...deposit-product-settings-step.component.ts | 130 +--- ...-deposit-product-terms-step.component.html | 259 +++---- ...-deposit-product-terms-step.component.scss | 105 ++- ...ed-deposit-product-terms-step.component.ts | 88 ++- 22 files changed, 1952 insertions(+), 1424 deletions(-) diff --git a/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.html b/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.html index 25e5d2c38..66a5f262e 100644 --- a/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.html +++ b/src/app/deposits/fixed-deposits/create-fixed-deposit-account/create-fixed-deposit-account.component.html @@ -34,7 +34,8 @@ - + + {{ 'labels.inputs.DETAILS' | translate }} - - {{ 'labels.inputs.TERMS' | translate }} + @if (fixedDepositsAccountProductTemplate) { + + + {{ 'labels.inputs.TERMS' | translate }} - - - + + - - {{ 'labels.inputs.SETTINGS' | translate }} +
+ + +
+
- - -
+ + + {{ 'labels.inputs.SETTINGS' | translate }} - - {{ 'labels.inputs.INTEREST RATE CHART' | translate }} + + - - - +
+ + +
+
- - {{ 'labels.inputs.CHARGES' | translate }} + + + {{ 'labels.inputs.INTEREST RATE CHART' | translate }} - - - + + - @if (fixedDepositAccountFormValid) { +
+ + +
+
+ + + + {{ 'labels.inputs.CHARGES' | translate }} + + + + +
+ + +
+
+ + {{ 'labels.inputs.PREVIEW' | translate }} - {{ 'labels.inputs.INTEREST RATE CHART' | translate }} + CBS Base Chart
- + @if (activeBaseVersion) { +
+
+
+

{{ activeBaseVersion.versionLabel }}

+

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

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

{{ activeBaseVersion.remarks }}

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

Loading CBS base chart...

+ }
@@ -112,7 +160,7 @@
{ this.fixedDepositProductsTemplate = data.fixedDepositProductsTemplate; }); this.accountingRuleData = this.accounting.getAccountingRulesForSavings(); + this.productsService.getRdBaseInterestRates().subscribe({ + next: (rates: any[]) => { + this.fdInterestRates = rates || []; + }, + error: () => { + this.fdInterestRates = []; + } + }); } get fixedDepositProductDetailsForm() { @@ -102,10 +130,6 @@ export class CreateFixedDepositProductComponent { return this.fixedDepositProductSettingsStep.fixedDepositProductSettingsForm; } - get fixedDepositProductInterestRateChartForm() { - return this.fixedDepositProductInterestRateChartStep.fixedDepositProductInterestRateChartForm; - } - get fixedDepositProductAccountingForm() { return this.fixedDepositProductAccountingStep.fixedDepositProductAccountingForm; } @@ -120,29 +144,35 @@ export class CreateFixedDepositProductComponent { this.fixedDepositProductCurrencyForm.valid && this.fixedDepositProductTermsForm.valid && this.fixedDepositProductSettingsForm.valid && - this.fixedDepositProductInterestRateChartForm.valid && this.fixedDepositProductAccountingForm.valid ); } + get activeBaseVersion() { + return this.fdInterestRates.find((v: any) => v.isActive) ?? this.fdInterestRates[0] ?? null; + } + + get activeBaseSlabs() { + return this.activeBaseVersion?.slabs || []; + } + get fixedDepositProduct() { return { ...this.fixedDepositProductDetailsStep.fixedDepositProductDetails, ...this.fixedDepositProductCurrencyStep.fixedDepositProductCurrency, ...this.fixedDepositProductTermsStep.fixedDepositProductTerms, ...this.fixedDepositProductSettingsStep.fixedDepositProductSettings, - ...this.fixedDepositProductInterestRateChartStep.fixedDepositProductInterestRateChart, ...this.fixedDepositProductChargesStep.fixedDepositProductCharges, - ...this.fixedDepositProductAccountingStep.fixedDepositProductAccounting + ...this.fixedDepositProductAccountingStep.fixedDepositProductAccounting, + fdSlabs: this.activeBaseSlabs }; } submit() { - // TODO: Update once language and date settings are setup const fixedDepositProduct = { ...this.fixedDepositProduct, charges: this.fixedDepositProduct.charges.map((charge: any) => ({ id: charge.id })), - locale: this.settingsService.language.code // locale required for depositAmount + locale: this.settingsService.language.code }; delete fixedDepositProduct.advancedAccountingRules; this.productsService.createFixedDepositProduct(fixedDepositProduct).subscribe((response: any) => { diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.html b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.html index 86939918d..2df0b809d 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.html +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.html @@ -9,310 +9,111 @@
- -

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

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

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

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

Accounting

-
-
- -

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

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

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

-
-
- - -
-
- -
-
- -

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

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

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

-
-
-
- +
+
+
+

Accounting Rule

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

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

- -
- @if (paymentChannelToFundSourceMappings.value.length !== 0) { -
- - - - - - - - - - - - - - - -
{{ 'labels.heading.Payment Type' | translate }} - {{ paymentFundSource.paymentTypeId | find: paymentTypeData : 'id' : 'name' }} - {{ 'labels.inputs.Fund Source' | translate }} - {{ paymentFundSource.fundSourceAccountId | find: assetAccountData : 'id' : 'name' }} - {{ 'labels.buttons.Actions' | translate }} - - -
-
- } -
- - @if (existCharges()) { - -
-
-

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

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

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

- + {{ section.pillLabel }} + + {{ row.label }} +
+ +
+ } + }
- @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.buttons.Actions' | translate }} - - -
-
- } + {{ section.pillLabel }} + + +
+ +
+ + GL Code + + @for (glAccount of section.otherAccountOptions; track glAccount.id) { + + {{ '(' + glAccount.glCode + ') ' + glAccount.name }} + + } + + +
+
+ +
+ } +
} } - } + diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.scss b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.scss index 74f18ffcf..bf75de1b0 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.scss +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.scss @@ -6,8 +6,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-700: #334155; +$gray-900: #202124; + .fdpa-section { - border: 1px solid #e2e8f0; + border: 1px solid $border; border-radius: 10px; overflow: hidden; background: #fff; @@ -15,45 +21,244 @@ & + & { margin-top: 16px; } +} + +.fdpa-section__header { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 24px; + background: $gray-50; + border-bottom: 1px solid $gray-200; + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } +} + +.fdpa-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; +} + +.fdpa-section__body { + padding: 16px 24px 20px; +} + +.fdpa-group { + & + & { + margin-top: 20px; + } &__header { display: flex; align-items: center; - gap: 10px; - padding: 12px 24px; - background: #f8f9fa; - border-bottom: 1px solid #e8eaed; + justify-content: space-between; + gap: 16px; + margin: 0 0 12px; + } - i { - font-size: 14px; - color: #0ea5e9; - } - - h3 { - margin: 0; - font-size: 14px; - font-weight: 600; - color: #202124; - } + h4 { + margin: 0 0 12px; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #94a3b8; } } -.fdpa-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 4px 16px; - padding: 16px 24px; +.fdpa-add-btn { + border: 0; + background: transparent; + color: #2563eb; + font-size: 12px; + font-weight: 700; + padding: 0; + cursor: pointer; +} - @media (width <= 600px) { +.fdpa-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.fdpa-list + .fdpa-list { + margin-top: 12px; +} + +.fdpa-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 10px 12px; + border: 1px solid $border; + border-radius: 8px; + background: #fff; + + @media (width <= 900px) { + flex-direction: column; + align-items: stretch; + } +} + +.fdpa-row:has(mifosx-gl-account-selector) { + align-items: stretch; +} + +.fdpa-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; } } -.fdpa-field { +.fdpa-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; + } +} + +.fdpa-row__meta { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + flex: 1 1 auto; +} + +.fdpa-row__meta--other { + min-width: 0; + flex: 1 1 auto; + align-self: stretch; +} + +.fdpa-row__label { + font-size: 14px; + font-weight: 600; + color: $gray-700; +} + +.fdpa-row__input { + flex: 0 0 22rem; + max-width: 22rem; + min-width: 18rem; + + @media (width <= 900px) { + width: 100%; + max-width: 100%; + min-width: 0; + } +} + +.fdpa-row__input :is(mifosx-gl-account-selector) { + width: 100%; + display: block; +} + +.fdpa-row__input :is(mifosx-gl-account-selector) :is(.layout-column) { + width: 100%; + margin-right: 0 !important; +} + +.fdpa-row__input :is(mifosx-gl-account-selector) :is(mat-form-field) { + width: 100%; +} + +.fdpa-row__actions { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.fdpa-row__actions--other { + justify-content: flex-end; + align-self: stretch; width: 100%; - &--full { - grid-column: 1 / -1; + @media (width <= 1100px) { + justify-content: flex-start; + } +} + +.fdpa-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%; + } +} + +.fdpa-row__name--other-account { + max-width: 100%; + flex: 1 1 auto; + height: 48px; +} + +.fdpa-row__other-field { + width: 100%; + min-width: 0; +} + +.fdpa-row__other-field :is(mat-form-field) { + width: 100%; +} + +.fdpa-row__other-field :is(.mat-mdc-form-field-required-marker) { + display: none; +} + +.fdpa-remove-btn { + border: 0; + background: transparent; + color: #ef4444; + font-size: 18px; + line-height: 1; + cursor: pointer; + padding: 0 4px; +} + +.fdpa-remove-btn--side { + place-self: center end; + margin-right: 4px; + + @media (width <= 1100px) { + justify-self: start; + margin-right: 0; } } @@ -61,23 +266,34 @@ display: flex; gap: 24px; flex-wrap: wrap; - - @media (width <= 600px) { - flex-direction: column; - } } -.fdpa-checkbox-row { - display: flex; +.fdpa-pill { + display: inline-flex; align-items: center; - padding: 4px 0; -} + 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; -.fdpa-add-btn { - height: 36px; - border-radius: 8px; - font-size: 13px; - font-weight: 600; + &--green { + background: #dcfce7; + color: #16a34a; + } + + &--amber { + background: #fef3c7; + color: #d97706; + } + + &--blue { + background: #dbeafe; + color: #2563eb; + } } .fdpa-subsection { @@ -95,13 +311,13 @@ margin: 0; font-size: 14px; font-weight: 600; - color: #202124; + color: $gray-900; } } } .fdpa-table-wrapper { - border: 1px solid #e2e8f0; + border: 1px solid $border; border-radius: 8px; overflow: hidden; @@ -109,3 +325,14 @@ width: 100%; } } + +@media (width <= 900px) { + .fdpa-row__meta { + margin-bottom: 4px; + } + + .fdpa-row__input { + flex: 1 1 auto; + max-width: 100%; + } +} diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.ts b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.ts index 6fa7c4100..34d17ab66 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.ts +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.ts @@ -8,434 +8,391 @@ import { Component, Input, OnInit, inject } from '@angular/core'; import { + FormsModule, + ReactiveFormsModule, UntypedFormArray, UntypedFormBuilder, UntypedFormControl, UntypedFormGroup, - Validators, - ReactiveFormsModule + Validators } from '@angular/forms'; -import { MatDialog } from '@angular/material/dialog'; - -import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; -import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component'; - -import { TranslateService } from '@ngx-translate/core'; +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 { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base'; -import { SelectBase } from 'app/shared/form-dialog/formfield/model/select-base'; -import { MatRadioGroup, MatRadioButton } from '@angular/material/radio'; -import { GlAccountSelectorComponent } from '../../../../shared/accounting/gl-account-selector/gl-account-selector.component'; -import { MatCheckbox } from '@angular/material/checkbox'; -import { MatButton, MatIconButton } from '@angular/material/button'; -import { FaIconComponent } from '@fortawesome/angular-fontawesome'; -import { - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow -} from '@angular/material/table'; -import { FindPipe } from '../../../../pipes/find.pipe'; +import { GLAccount } from 'app/shared/models/general.model'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +type AccountingSectionKey = 'assets' | 'liabilities' | 'expenses' | 'income'; + +type AccountingRowConfig = { + key: string; + label: string; + inputLabel: string; + controlName: string; + options: GLAccount[]; + required?: boolean; + accrualOnly?: boolean; +}; + +type AccountingSectionConfig = { + key: AccountingSectionKey; + title: string; + pillLabel: string; + pillClass: 'green' | 'amber' | 'blue'; + rows: AccountingRowConfig[]; + otherMappingsControlName: string; + otherAccountOptions: GLAccount[]; +}; + +type OtherGlMappingValue = { + accountName: string; + glAccountId: number | null; +}; + @Component({ selector: 'mifosx-fixed-deposit-product-accounting-step', templateUrl: './fixed-deposit-product-accounting-step.component.html', styleUrls: ['./fixed-deposit-product-accounting-step.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, + FormsModule, + ReactiveFormsModule, MatRadioGroup, MatRadioButton, - GlAccountSelectorComponent, - MatCheckbox, - FaIconComponent, - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatIconButton, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow, - FindPipe + GlAccountSelectorComponent ] }) export class FixedDepositProductAccountingStepComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); - dialog = inject(MatDialog); private accounting = inject(Accounting); - private translateService = inject(TranslateService); @Input() fixedDepositProductsTemplate: any; @Input() accountingRuleData: any; @Input() fixedDepositProductFormValid: boolean; fixedDepositProductAccountingForm: UntypedFormGroup; - - chargeData: any; - penaltyData: any; - paymentTypeData: any; - assetAccountData: any; - incomeAccountData: any; - expenseAccountData: any; - liabilityAccountData: any; - - paymentFundSourceDisplayedColumns: string[] = [ - 'paymentTypeId', - 'fundSourceAccountId', - 'actions' - ]; - feesPenaltyIncomeDisplayedColumns: string[] = [ - 'chargeId', - 'incomeAccountId', - 'actions' - ]; + assetAccountOptions: GLAccount[] = []; + liabilityAccountOptions: GLAccount[] = []; + expenseAccountOptions: GLAccount[] = []; + incomeAccountOptions: GLAccount[] = []; + accountingSectionConfigs: AccountingSectionConfig[] = []; constructor() { - this.createfixedDepositProductAccountingForm(); + this.createFixedDepositProductAccountingForm(); this.setConditionalControls(); } ngOnInit() { - this.chargeData = this.fixedDepositProductsTemplate.chargeOptions || []; - this.penaltyData = this.fixedDepositProductsTemplate.penaltyOptions || []; - this.paymentTypeData = this.fixedDepositProductsTemplate.paymentTypeOptions || []; - this.assetAccountData = this.fixedDepositProductsTemplate.accountingMappingOptions.assetAccountOptions || []; - this.incomeAccountData = this.fixedDepositProductsTemplate.accountingMappingOptions.incomeAccountOptions || []; - this.expenseAccountData = this.fixedDepositProductsTemplate.accountingMappingOptions.expenseAccountOptions || []; - this.liabilityAccountData = - this.fixedDepositProductsTemplate.accountingMappingOptions.liabilityAccountOptions || []; - if (!(this.fixedDepositProductsTemplate === undefined) && this.fixedDepositProductsTemplate.id) { - this.assignAccountingStepData(); - } + const accountingMappings = this.fixedDepositProductsTemplate?.accountingMappings || {}; + const mappings = { + ...accountingMappings, + otherGlMappingsMap: this.fixedDepositProductsTemplate?.otherGlMappingsMap + }; + const accountingMappingOptions = this.fixedDepositProductsTemplate?.accountingMappingOptions || {}; + const accountingRuleId = 2; // Always Cash + + this.assetAccountOptions = accountingMappingOptions.assetAccountOptions || []; + this.liabilityAccountOptions = accountingMappingOptions.liabilityAccountOptions || []; + this.expenseAccountOptions = accountingMappingOptions.expenseAccountOptions || []; + this.incomeAccountOptions = accountingMappingOptions.incomeAccountOptions || []; + + this.buildAccountingSectionConfigs(); + + this.fixedDepositProductAccountingForm.patchValue({ accountingRule: accountingRuleId }); + this.syncAccountingControls(accountingRuleId); + this.patchAccountingValues(mappings); } - assignAccountingStepData() { - this.fixedDepositProductAccountingForm.patchValue({ - accountingRule: this.fixedDepositProductsTemplate.accountingRule.id - }); - if (this.isCashOrAccrualAccounting()) { - this.fixedDepositProductAccountingForm.patchValue({ - savingsReferenceAccountId: this.fixedDepositProductsTemplate.accountingMappings.savingsReferenceAccount.id, - savingsControlAccountId: this.fixedDepositProductsTemplate.accountingMappings.savingsControlAccount.id, - transfersInSuspenseAccountId: - this.fixedDepositProductsTemplate.accountingMappings.transfersInSuspenseAccount.id, - incomeFromFeeAccountId: this.fixedDepositProductsTemplate.accountingMappings.incomeFromFeeAccount.id, - incomeFromPenaltyAccountId: this.fixedDepositProductsTemplate.accountingMappings.incomeFromPenaltyAccount.id, - interestOnSavingsAccountId: this.fixedDepositProductsTemplate.accountingMappings.interestOnSavingsAccount.id - }); - - if (this.isAccrualAccounting()) { - this.fixedDepositProductAccountingForm.patchValue({ - feesReceivableAccountId: this.fixedDepositProductsTemplate.accountingMappings.feeReceivableAccount.id, - penaltiesReceivableAccountId: - this.fixedDepositProductsTemplate.accountingMappings.penaltyReceivableAccount.id, - interestPayableAccountId: this.fixedDepositProductsTemplate.accountingMappings.interestPayableAccount.id - }); - } - - if ( - this.fixedDepositProductsTemplate.paymentChannelToFundSourceMappings || - this.fixedDepositProductsTemplate.feeToIncomeAccountMappings || - this.fixedDepositProductsTemplate.penaltyToIncomeAccountMappings - ) { - this.fixedDepositProductAccountingForm.patchValue({ - advancedAccountingRules: true - }); - } - if (this.fixedDepositProductsTemplate.paymentChannelToFundSourceMappings) { - this.fixedDepositProductsTemplate.paymentChannelToFundSourceMappings.forEach( - (paymentChannelToFundSourceMapping: any) => { - const paymentChannelToFundSourceMappingData = this.formBuilder.group({ - paymentTypeId: [ - paymentChannelToFundSourceMapping.paymentType.id, - Validators.required - ], - fundSourceAccountId: [ - paymentChannelToFundSourceMapping.fundSourceAccount.id, - Validators.required - ] - }); - const formArray = this.fixedDepositProductAccountingForm.controls[ - 'paymentChannelToFundSourceMappings' - ] as UntypedFormArray; - formArray.push(paymentChannelToFundSourceMappingData); - } - ); - } - if (this.fixedDepositProductsTemplate.feeToIncomeAccountMappings) { - this.fixedDepositProductsTemplate.feeToIncomeAccountMappings.forEach((feeToIncomeAccountMapping: any) => { - const feeToIncomeAccountMappingData = this.formBuilder.group({ - chargeId: [ - feeToIncomeAccountMapping.charge.id, - Validators.required - ], - incomeAccountId: [ - feeToIncomeAccountMapping.incomeAccount.id, - Validators.required - ] - }); - const formArray = this.fixedDepositProductAccountingForm.controls[ - 'feeToIncomeAccountMappings' - ] as UntypedFormArray; - formArray.push(feeToIncomeAccountMappingData); - }); - } - if (this.fixedDepositProductsTemplate.penaltyToIncomeAccountMappings) { - this.fixedDepositProductsTemplate.penaltyToIncomeAccountMappings.forEach( - (penaltyToIncomeAccountMapping: any) => { - const penaltyToIncomeAccountMappingData = this.formBuilder.group({ - chargeId: [ - penaltyToIncomeAccountMapping.charge.id, - Validators.required - ], - incomeAccountId: [ - penaltyToIncomeAccountMapping.incomeAccount.id, - Validators.required - ] - }); - const formArray = this.fixedDepositProductAccountingForm.controls[ - 'penaltyToIncomeAccountMappings' - ] as UntypedFormArray; - formArray.push(penaltyToIncomeAccountMappingData); - } - ); - } - } - } - - createfixedDepositProductAccountingForm() { + createFixedDepositProductAccountingForm() { this.fixedDepositProductAccountingForm = this.formBuilder.group({ - accountingRule: [1] + accountingRule: [2], + assetsOtherGlMappings: this.formBuilder.array([]), + liabilitiesOtherGlMappings: this.formBuilder.array([]), + expensesOtherGlMappings: this.formBuilder.array([]), + incomeOtherGlMappings: this.formBuilder.array([]) }); } - existCharges(): boolean { - return this.chargeData.length > 0; - } - - setConditionalControls() { - this.fixedDepositProductAccountingForm.get('accountingRule').valueChanges.subscribe((accountingRule: any) => { - if (accountingRule === 2 || accountingRule === 3) { - this.fixedDepositProductAccountingForm.addControl( - 'savingsReferenceAccountId', - new UntypedFormControl('', Validators.required) - ); - this.fixedDepositProductAccountingForm.addControl( - 'savingsControlAccountId', - new UntypedFormControl('', Validators.required) - ); - this.fixedDepositProductAccountingForm.addControl( - 'transfersInSuspenseAccountId', - new UntypedFormControl('', Validators.required) - ); - this.fixedDepositProductAccountingForm.addControl( - 'interestOnSavingsAccountId', - new UntypedFormControl('', Validators.required) - ); - this.fixedDepositProductAccountingForm.addControl( - 'incomeFromFeeAccountId', - new UntypedFormControl('', Validators.required) - ); - this.fixedDepositProductAccountingForm.addControl( - 'incomeFromPenaltyAccountId', - new UntypedFormControl('', Validators.required) - ); - this.fixedDepositProductAccountingForm.addControl('advancedAccountingRules', new UntypedFormControl(false)); - - if (accountingRule === 3) { - this.fixedDepositProductAccountingForm.addControl( - 'feesReceivableAccountId', - new UntypedFormControl('', Validators.required) - ); - this.fixedDepositProductAccountingForm.addControl( - 'penaltiesReceivableAccountId', - new UntypedFormControl('', Validators.required) - ); - this.fixedDepositProductAccountingForm.addControl( - 'interestPayableAccountId', - new UntypedFormControl('', Validators.required) - ); - } - - this.fixedDepositProductAccountingForm - .get('advancedAccountingRules') - .valueChanges.subscribe((advancedAccountingRules: boolean) => { - if (advancedAccountingRules) { - this.fixedDepositProductAccountingForm.addControl( - 'paymentChannelToFundSourceMappings', - this.formBuilder.array([]) - ); - this.fixedDepositProductAccountingForm.addControl( - 'feeToIncomeAccountMappings', - this.formBuilder.array([]) - ); - this.fixedDepositProductAccountingForm.addControl( - 'penaltyToIncomeAccountMappings', - this.formBuilder.array([]) - ); - } else { - this.fixedDepositProductAccountingForm.removeControl('paymentChannelToFundSourceMappings'); - this.fixedDepositProductAccountingForm.removeControl('feeToIncomeAccountMappings'); - this.fixedDepositProductAccountingForm.removeControl('penaltyToIncomeAccountMappings'); - } - }); - } else { - this.fixedDepositProductAccountingForm.removeControl('savingsReferenceAccountId'); - this.fixedDepositProductAccountingForm.removeControl('overdraftPortfolioControlId'); - this.fixedDepositProductAccountingForm.removeControl('savingsControlAccountId'); - this.fixedDepositProductAccountingForm.removeControl('transfersInSuspenseAccountId'); - this.fixedDepositProductAccountingForm.removeControl('interestOnSavingsAccountId'); - this.fixedDepositProductAccountingForm.removeControl('writeOffAccountId'); - this.fixedDepositProductAccountingForm.removeControl('incomeFromFeeAccountId'); - this.fixedDepositProductAccountingForm.removeControl('incomeFromPenaltyAccountId'); - this.fixedDepositProductAccountingForm.removeControl('incomeFromInterestId'); - this.fixedDepositProductAccountingForm.removeControl('advancedAccountingRules'); - this.fixedDepositProductAccountingForm.removeControl('escheatLiabilityId'); - this.fixedDepositProductAccountingForm.removeControl('feesReceivableAccountId'); - this.fixedDepositProductAccountingForm.removeControl('penaltiesReceivableAccountId'); - this.fixedDepositProductAccountingForm.removeControl('interestPayableAccountId'); - } + private setConditionalControls() { + this.fixedDepositProductAccountingForm.get('accountingRule')?.valueChanges.subscribe((rule: number) => { + this.syncAccountingControls(rule); }); } - get paymentChannelToFundSourceMappings(): UntypedFormArray { - return this.fixedDepositProductAccountingForm.get('paymentChannelToFundSourceMappings') as UntypedFormArray; - } - - get feeToIncomeAccountMappings(): UntypedFormArray { - return this.fixedDepositProductAccountingForm.get('feeToIncomeAccountMappings') as UntypedFormArray; - } - - get penaltyToIncomeAccountMappings(): UntypedFormArray { - return this.fixedDepositProductAccountingForm.get('penaltyToIncomeAccountMappings') as UntypedFormArray; - } - - add(formType: string, formArray: UntypedFormArray) { - const data = { ...this.getData(formType), pristine: false }; - const dialogRef = this.dialog.open(FormDialogComponent, { data }); - dialogRef.afterClosed().subscribe((response: any) => { - if (response.data) { - formArray.push(response.data); - } + private patchAccountingValues(mappings: any) { + this.fixedDepositProductAccountingForm.patchValue({ + savingsReferenceAccountId: mappings.savingsReferenceAccount?.id || '', + savingsControlAccountId: mappings.savingsControlAccount?.id || '', + transfersInSuspenseAccountId: mappings.transfersInSuspenseAccount?.id || '', + interestOnSavingsAccountId: mappings.interestOnSavingsAccount?.id || '', + incomeFromFeeAccountId: mappings.incomeFromFeeAccount?.id || '', + incomeFromPenaltyAccountId: mappings.incomeFromPenaltyAccount?.id || '', + feesReceivableAccountId: mappings.feeReceivableAccount?.id || '', + penaltiesReceivableAccountId: mappings.penaltyReceivableAccount?.id || '', + interestPayableAccountId: mappings.interestPayableAccount?.id || '' }); - } - edit(formType: string, formArray: UntypedFormArray, index: number) { - const data = { ...this.getData(formType, formArray.at(index).value), layout: { addButtonText: 'Edit' } }; - const dialogRef = this.dialog.open(FormDialogComponent, { data }); - dialogRef.afterClosed().subscribe((response: any) => { - if (response.data) { - formArray.at(index).patchValue(response.data.value); - } - }); - } - - delete(formArray: UntypedFormArray, index: number) { - const dialogRef = this.dialog.open(DeleteDialogComponent, { - data: { deleteContext: this.translateService.instant('labels.text.this') } - }); - dialogRef.afterClosed().subscribe((response: any) => { - if (response.delete) { - formArray.removeAt(index); - } - }); - } - - getData(formType: string, values?: any) { - switch (formType) { - case 'PaymentFundSource': - return { - title: 'Configure Fund Sources for Payment Channels', - formfields: this.getPaymentFundSourceFormfields(values) - }; - case 'FeesIncome': - return { title: 'Map Fees to Income Accounts', formfields: this.getFeesIncomeFormfields(values) }; - case 'PenaltyIncome': - return { - title: 'Map Penalties to Specific Income Accounts', - formfields: this.getPenaltyIncomeFormfields(values) - }; + if (this.isCashOrAccrualAccounting()) { + const glMappings = mappings.otherGlMappingsMap || mappings.otherGlMappings; + this.setOtherGlMappings('assetsOtherGlMappings', glMappings?.assets || [], this.assetAccountOptions); + this.setOtherGlMappings( + 'liabilitiesOtherGlMappings', + glMappings?.liabilities || [], + this.liabilityAccountOptions + ); + this.setOtherGlMappings('expensesOtherGlMappings', glMappings?.expenses || [], this.expenseAccountOptions); + this.setOtherGlMappings('incomeOtherGlMappings', glMappings?.income || [], this.incomeAccountOptions); } } - getPaymentFundSourceFormfields(values?: any) { - const formfields: FormfieldBase[] = [ - new SelectBase({ - controlName: 'paymentTypeId', - label: '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: 'Fund Source', - value: values ? values.fundSourceAccountId : this.assetAccountData[0].id, - options: { label: 'name', value: 'id', data: this.assetAccountData }, - required: true, - order: 2 - }) - ]; - return formfields; + private createOtherGlMappingGroup(otherGlMapping: Partial = {}) { + return this.formBuilder.group({ + accountName: [ + otherGlMapping.accountName || '', + Validators.required + ], + glAccountId: [ + otherGlMapping.glAccountId || '', + Validators.required + ] + }); } - getFeesIncomeFormfields(values?: any) { - const formfields: FormfieldBase[] = [ - new SelectBase({ - controlName: 'chargeId', - label: '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: 'Income Account', - value: values ? values.incomeAccountId : this.incomeAccountData[0].id, - options: { label: 'name', value: 'id', data: this.incomeAccountData }, - required: true, - order: 2 - }) - ]; - return formfields; + getOtherGlMappingsArray(controlName: string): UntypedFormArray { + return this.fixedDepositProductAccountingForm.get(controlName) as UntypedFormArray; } - getPenaltyIncomeFormfields(values?: any) { - const formfields: FormfieldBase[] = [ - new SelectBase({ - controlName: 'chargeId', - label: 'Penalty', - value: values ? values.chargeId : this.penaltyData[0].id, - options: { label: 'name', value: 'id', data: this.penaltyData }, - required: true, - order: 1 - }), - new SelectBase({ - controlName: 'incomeAccountId', - label: 'Income Account', - value: values ? values.incomeAccountId : this.incomeAccountData[0].id, - options: { label: 'name', value: 'id', data: this.incomeAccountData }, - required: true, - order: 2 - }) + private setOtherGlMappings(controlName: string, otherGlMappings: any[], options: GLAccount[]) { + const arr = this.getOtherGlMappingsArray(controlName); + arr.clear(); + (otherGlMappings || []).forEach((m) => { + arr.push( + this.createOtherGlMappingGroup({ + accountName: m.accountName, + glAccountId: m.glAccountId || options.find((o) => o.glCode === m.glCode)?.id || null + }) + ); + }); + } + + addOtherGlMapping(controlName: string) { + this.getOtherGlMappingsArray(controlName).push(this.createOtherGlMappingGroup()); + } + + removeOtherGlMapping(controlName: string, index: number) { + this.getOtherGlMappingsArray(controlName).removeAt(index); + } + + getOtherGlMappingControl(controlName: string, index: number, fieldName: string): UntypedFormControl { + return this.getOtherGlMappingsArray(controlName).at(index).get(fieldName) as UntypedFormControl; + } + + private clearOtherGlMappings() { + [ + 'assetsOtherGlMappings', + 'liabilitiesOtherGlMappings', + 'expensesOtherGlMappings', + 'incomeOtherGlMappings' + ].forEach((c) => this.getOtherGlMappingsArray(c).clear()); + } + + private getOtherGlMappings() { + const map = (controlName: string, options: GLAccount[]) => + this.getOtherGlMappingsArray(controlName) + .value.filter((item: OtherGlMappingValue) => !!item.accountName || !!item.glAccountId) + .map((item: OtherGlMappingValue) => ({ + accountName: item.accountName, + glCode: options.find((o) => o.id === item.glAccountId)?.glCode || '' + })) + .filter((item: { accountName: string; glCode: string }) => !!item.accountName || !!item.glCode); + + const otherGlMappings = { + assets: map('assetsOtherGlMappings', this.assetAccountOptions), + liabilities: map('liabilitiesOtherGlMappings', this.liabilityAccountOptions), + expenses: map('expensesOtherGlMappings', this.expenseAccountOptions), + income: map('incomeOtherGlMappings', this.incomeAccountOptions) + }; + return Object.values(otherGlMappings).some((v) => v.length) ? otherGlMappings : null; + } + + private syncAccountingControls(accountingRule: number) { + const addControl = (name: string, required = false) => { + if (!this.fixedDepositProductAccountingForm.contains(name)) { + this.fixedDepositProductAccountingForm.addControl( + name, + new UntypedFormControl('', required ? Validators.required : undefined) + ); + } else if (required) { + this.fixedDepositProductAccountingForm.get(name)?.setValidators(Validators.required); + this.fixedDepositProductAccountingForm.get(name)?.updateValueAndValidity({ emitEvent: false }); + } + }; + const removeControl = (name: string) => { + if (this.fixedDepositProductAccountingForm.contains(name)) { + this.fixedDepositProductAccountingForm.removeControl(name); + } + }; + + if (this.accounting.isCashOrAccrualAccountingRuleId(accountingRule)) { + addControl('savingsReferenceAccountId', true); + addControl('savingsControlAccountId', true); + addControl('transfersInSuspenseAccountId', true); + addControl('interestOnSavingsAccountId', true); + addControl('incomeFromFeeAccountId', true); + addControl('incomeFromPenaltyAccountId', true); + + if (this.accounting.isAccrualAccountingRuleId(accountingRule)) { + addControl('feesReceivableAccountId', true); + addControl('penaltiesReceivableAccountId', true); + addControl('interestPayableAccountId', true); + } else { + removeControl('feesReceivableAccountId'); + removeControl('penaltiesReceivableAccountId'); + removeControl('interestPayableAccountId'); + } + } else { + [ + 'savingsReferenceAccountId', + 'savingsControlAccountId', + 'transfersInSuspenseAccountId', + 'interestOnSavingsAccountId', + 'incomeFromFeeAccountId', + 'incomeFromPenaltyAccountId', + 'feesReceivableAccountId', + 'penaltiesReceivableAccountId', + 'interestPayableAccountId' + ].forEach(removeControl); + this.clearOtherGlMappings(); + } + } + + buildAccountingSectionConfigs() { + this.accountingSectionConfigs = [ + { + key: 'assets', + title: 'Assets', + pillLabel: 'Asset', + pillClass: 'green', + rows: [ + { + key: 'savingsReferenceAccountId', + label: 'Savings Reference Account', + inputLabel: 'Savings reference account', + controlName: 'savingsReferenceAccountId', + options: this.assetAccountOptions, + required: true + }, + { + key: 'feesReceivableAccountId', + label: 'Fees Receivable Account', + inputLabel: 'Fees receivable account', + controlName: 'feesReceivableAccountId', + options: this.assetAccountOptions, + accrualOnly: true + }, + { + key: 'penaltiesReceivableAccountId', + label: 'Penalties Receivable Account', + inputLabel: 'Penalties receivable account', + controlName: 'penaltiesReceivableAccountId', + options: this.assetAccountOptions, + accrualOnly: true + } + ], + otherMappingsControlName: 'assetsOtherGlMappings', + otherAccountOptions: this.assetAccountOptions + }, + { + key: 'liabilities', + title: 'Liabilities', + pillLabel: 'Liability', + pillClass: 'green', + rows: [ + { + key: 'savingsControlAccountId', + label: 'Savings Control Account', + inputLabel: 'Savings control account', + controlName: 'savingsControlAccountId', + options: this.liabilityAccountOptions, + required: true + }, + { + key: 'transfersInSuspenseAccountId', + label: 'Transfers In Suspense Account', + inputLabel: 'Transfers in suspense account', + controlName: 'transfersInSuspenseAccountId', + options: this.liabilityAccountOptions, + required: true + }, + { + key: 'interestPayableAccountId', + label: 'Interest Payable Account', + inputLabel: 'Interest payable account', + controlName: 'interestPayableAccountId', + options: this.liabilityAccountOptions, + accrualOnly: true + } + ], + otherMappingsControlName: 'liabilitiesOtherGlMappings', + otherAccountOptions: this.liabilityAccountOptions + }, + { + key: 'expenses', + title: 'Expenses', + pillLabel: 'Expense', + pillClass: 'amber', + rows: [ + { + key: 'interestOnSavingsAccountId', + label: 'Interest On Savings Account', + inputLabel: 'Interest on savings account', + controlName: 'interestOnSavingsAccountId', + options: this.expenseAccountOptions, + required: true + } + ], + otherMappingsControlName: 'expensesOtherGlMappings', + otherAccountOptions: this.expenseAccountOptions + }, + { + key: 'income', + title: 'Income', + pillLabel: 'Income', + pillClass: 'blue', + rows: [ + { + key: 'incomeFromFeeAccountId', + label: 'Income From Fees', + inputLabel: 'Income from fees', + controlName: 'incomeFromFeeAccountId', + options: this.incomeAccountOptions, + required: true + }, + { + key: 'incomeFromPenaltyAccountId', + label: 'Income From Penalties', + inputLabel: 'Income from penalties', + controlName: 'incomeFromPenaltyAccountId', + options: this.incomeAccountOptions, + required: true + } + ], + otherMappingsControlName: 'incomeOtherGlMappings', + otherAccountOptions: this.incomeAccountOptions + } ]; - return formfields; } get fixedDepositProductAccounting() { - return this.fixedDepositProductAccountingForm.value; + const { + assetsOtherGlMappings, + liabilitiesOtherGlMappings, + expensesOtherGlMappings, + incomeOtherGlMappings, + ...accounting + } = this.fixedDepositProductAccountingForm.value; + const otherGlMappings = this.getOtherGlMappings(); + return { ...accounting, ...(otherGlMappings ? { otherGlMappings } : {}) }; } isCashOrAccrualAccounting(): boolean { diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-charges-step/fixed-deposit-product-charges-step.component.html b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-charges-step/fixed-deposit-product-charges-step.component.html index a8de6dfc4..2a806a267 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-charges-step/fixed-deposit-product-charges-step.component.html +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-charges-step/fixed-deposit-product-charges-step.component.html @@ -8,22 +8,21 @@
- -

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

+ +

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

-
- - {{ 'labels.inputs.Charge' | translate }} - - @for (charge of chargeData | chargesFilter: chargesDataSource : currencyCode.value; track charge) { - - {{ charge.name }} - - } - - - -
+
+
+ + {{ 'labels.inputs.Charge' | translate }} + + @for (charge of chargeData | chargesFilter: chargesDataSource : currencyCode.value; track charge) { + + {{ charge.name }} + + } + +
+ + @if (chargesDataSource.length > 0) { +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.Name' | translate }} + {{ charge.name + ', ' + charge.currency.displaySymbol }} + {{ 'labels.inputs.Type' | translate }} + {{ charge.chargeCalculationType.value }} + {{ 'labels.inputs.Amount' | translate }} + {{ charge.amount | formatNumber }} + {{ 'labels.inputs.Collected On' | translate }} + {{ charge.chargeTimeType.value }} + {{ 'labels.inputs.Actions' | translate }} + +
+
+ }
- - @if (chargesDataSource.length > 0) { -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Name' | translate }} - {{ charge.name + ', ' + charge.currency.displaySymbol }} - {{ 'labels.inputs.Type' | translate }} - {{ charge.chargeCalculationType.value }} - {{ 'labels.inputs.Amount' | translate }} - {{ charge.amount | formatNumber }} - {{ 'labels.inputs.Collected On' | translate }} - {{ charge.chargeTimeType.value }} - {{ 'labels.inputs.Actions' | translate }} - -
-
- }
diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-charges-step/fixed-deposit-product-charges-step.component.scss b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-charges-step/fixed-deposit-product-charges-step.component.scss index ccfe94921..21e98cf16 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-charges-step/fixed-deposit-product-charges-step.component.scss +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-charges-step/fixed-deposit-product-charges-step.component.scss @@ -12,10 +12,6 @@ overflow: hidden; background: #fff; - & + & { - margin-top: 16px; - } - &__header { display: flex; align-items: center; @@ -36,30 +32,22 @@ color: #202124; } } -} -.fdpch-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 4px 16px; - padding: 16px 24px; - - @media (width <= 600px) { - grid-template-columns: 1fr; + &__body { + padding: 16px 24px; } } -.fdpch-field { - width: 100%; - - &--full { - grid-column: 1 / -1; - } -} - -.fdpch-btn-wrap { +.fdpch-add-row { display: flex; align-items: center; + gap: 16px; + flex-wrap: wrap; + + &__select { + flex: 1; + min-width: 200px; + } } .fdpch-add-btn { @@ -73,7 +61,7 @@ border: 1px solid #e2e8f0; border-radius: 8px; overflow: hidden; - margin: 0 24px 16px; + margin-top: 12px; table { width: 100%; diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-currency-step/fixed-deposit-product-currency-step.component.html b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-currency-step/fixed-deposit-product-currency-step.component.html index 68ed1a8f3..df8a5251d 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-currency-step/fixed-deposit-product-currency-step.component.html +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-currency-step/fixed-deposit-product-currency-step.component.html @@ -10,60 +10,102 @@
-

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

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

Dormancy Alert

+

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

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

Balance Swap Option

+

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

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

MSME

+

Micro, Small and Medium Enterprises

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

Nomination Facility

+

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

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

Auto Debit Facility

+

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

+
+ + yes + no + +
diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-currency-step/fixed-deposit-product-currency-step.component.scss b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-currency-step/fixed-deposit-product-currency-step.component.scss index fe1c47dc5..f32a534a6 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-currency-step/fixed-deposit-product-currency-step.component.scss +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-currency-step/fixed-deposit-product-currency-step.component.scss @@ -6,8 +6,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +$accent: #0ea5e9; +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-900: #202124; + .fdpc-section { - border: 1px solid #e2e8f0; + border: 1px solid $border; border-radius: 10px; overflow: hidden; background: #fff; @@ -21,23 +27,27 @@ align-items: center; gap: 10px; padding: 12px 24px; - background: #f8f9fa; - border-bottom: 1px solid #e8eaed; + background: $gray-50; + border-bottom: 1px solid $gray-200; i { font-size: 14px; - color: #0ea5e9; + color: $accent; } - h3 { + span { margin: 0; font-size: 14px; font-weight: 600; - color: #202124; + color: $gray-900; } } } +.fdpc-body { + background: #fff; +} + .fdpc-grid { display: grid; grid-template-columns: repeat(2, 1fr); @@ -51,14 +61,43 @@ .fdpc-field { width: 100%; - - &--full { - grid-column: 1 / -1; - } } -.fdpc-checkbox-row { +.fdpc-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/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-currency-step/fixed-deposit-product-currency-step.component.ts b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-currency-step/fixed-deposit-product-currency-step.component.ts index 3fb6c6f2d..6e0466dfa 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-currency-step/fixed-deposit-product-currency-step.component.ts +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-currency-step/fixed-deposit-product-currency-step.component.ts @@ -6,12 +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 { startWith } from 'rxjs/operators'; -import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; -import { MatCheckbox } from '@angular/material/checkbox'; +import { Component, OnInit, Input, inject } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; import { MatTooltip } from '@angular/material/tooltip'; +import { MatRadioGroup, MatRadioButton } from '@angular/material/radio'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ @@ -21,18 +19,23 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; imports: [ ...STANDALONE_SHARED_IMPORTS, MatTooltip, - MatCheckbox + MatRadioGroup, + MatRadioButton ] }) export class FixedDepositProductCurrencyStepComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); - private destroyRef = inject(DestroyRef); @Input() fixedDepositProductsTemplate: any; fixedDepositProductCurrencyForm: UntypedFormGroup; currencyData: any; + decimalPlacesData = [ + 2, + 0, + 3 + ]; constructor() { this.createFixedDepositProductCurrencyForm(); @@ -42,13 +45,17 @@ export class FixedDepositProductCurrencyStepComponent implements OnInit { this.currencyData = this.fixedDepositProductsTemplate.currencyOptions; this.fixedDepositProductCurrencyForm.patchValue({ - currencyCode: this.fixedDepositProductsTemplate.currency?.code || this.currencyData[0].code, - digitsAfterDecimal: this.fixedDepositProductsTemplate.digitsAfterDecimal ?? '', - setMultiples: !!this.fixedDepositProductsTemplate.inMultiplesOf, - inMultiplesOf: this.fixedDepositProductsTemplate.inMultiplesOf ?? '' + currencyCode: this.fixedDepositProductsTemplate.currency?.code || this.currencyData?.[0]?.code || '', + digitsAfterDecimal: + this.fixedDepositProductsTemplate.digitsAfterDecimal ?? + this.fixedDepositProductsTemplate.currency?.decimalPlaces ?? + 2, + dormancyAlert: this.fixedDepositProductsTemplate.dormancyAlert ?? false, + balanceSwapOption: this.fixedDepositProductsTemplate.balanceSwapOption ?? false, + msme: this.fixedDepositProductsTemplate.msme ?? false, + nominationFacility: this.fixedDepositProductsTemplate.nominationFacility ?? false, + autoDebitFacility: this.fixedDepositProductsTemplate.autoDebitFacility ?? false }); - - this.setupConditionalValidation(); } createFixedDepositProductCurrencyForm() { @@ -58,48 +65,30 @@ export class FixedDepositProductCurrencyStepComponent implements OnInit { Validators.required ], digitsAfterDecimal: [ - '', + 2, [ Validators.required, Validators.min(0) ] ], - setMultiples: [false], - inMultiplesOf: [''] + dormancyAlert: [false], + balanceSwapOption: [false], + msme: [false], + nominationFacility: [false], + autoDebitFacility: [false] }); } - setupConditionalValidation() { - const inMultiplesOfControl = this.fixedDepositProductCurrencyForm.get('inMultiplesOf'); - const setMultiplesControl = this.fixedDepositProductCurrencyForm.get('setMultiples'); - - setMultiplesControl?.valueChanges - .pipe(startWith(setMultiplesControl.value), takeUntilDestroyed(this.destroyRef)) - .subscribe((checked) => { - if (checked) { - inMultiplesOfControl?.setValidators([ - Validators.required, - Validators.min(1) - ]); - } else { - inMultiplesOfControl?.clearValidators(); - inMultiplesOfControl?.setValue(''); - } - inMultiplesOfControl?.updateValueAndValidity(); - }); - } - get fixedDepositProductCurrency() { const formValue = this.fixedDepositProductCurrencyForm.value; - const result: any = { + return { currencyCode: formValue.currencyCode, - digitsAfterDecimal: formValue.digitsAfterDecimal + digitsAfterDecimal: formValue.digitsAfterDecimal, + dormancyAlert: formValue.dormancyAlert, + balanceSwapOption: formValue.balanceSwapOption, + msme: formValue.msme, + nominationFacility: formValue.nominationFacility, + autoDebitFacility: formValue.autoDebitFacility }; - - if (formValue.inMultiplesOf !== '' && formValue.inMultiplesOf !== null && formValue.inMultiplesOf !== undefined) { - result.inMultiplesOf = formValue.inMultiplesOf; - } - - return result; } } diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.html b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.html index 92f540da7..98add2dc8 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.html +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.html @@ -9,13 +9,18 @@
- +

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

{{ 'labels.inputs.Product Name' | translate }} - + @if (fixedDepositProductDetailsForm.controls.name.hasError('required')) { {{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }} @@ -26,7 +31,13 @@ {{ 'labels.inputs.Short Name' | translate }} - + @if (fixedDepositProductDetailsForm.controls.shortName.hasError('required')) { {{ 'labels.inputs.Short Name' | translate }} {{ 'labels.commons.is' | translate }} @@ -37,14 +48,80 @@ {{ 'labels.inputs.Description' | translate }} - - @if (fixedDepositProductDetailsForm.controls.description.hasError('required')) { - - {{ 'labels.inputs.Description' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } +
+ +
+
+ +

Product Classification

+
+ +
+ + Account Type + + @for (accountType of accountTypeData; track accountType) { + {{ accountType }} + } + + Account Type is required + + + + Customer Segment + + @for (customerSegment of customerSegmentData; track customerSegment) { + {{ customerSegment }} + } + + + + + GL Account (Liability) + + @for (account of liabilityAccountOptions; track account.id) { + {{ account.name }} ({{ account.glCode }}) + } + + + + + Effective To + + + + + + + End Date + + + + + + + Introducer Required + + @for (option of yesNoOptions; track option.value) { + {{ option.label }} + } + + + +
+ Deposit Account Link +
+
+
diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.scss b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.scss index 7747019fc..59c20ba02 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.scss +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.scss @@ -6,8 +6,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +$accent: #0ea5e9; +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-900: #202124; + .fdpd-section { - border: 1px solid #e2e8f0; + border: 1px solid $border; border-radius: 10px; overflow: hidden; background: #fff; @@ -21,19 +27,19 @@ align-items: center; gap: 10px; padding: 12px 24px; - background: #f8f9fa; - border-bottom: 1px solid #e8eaed; + background: $gray-50; + border-bottom: 1px solid $gray-200; i { font-size: 14px; - color: #0ea5e9; + color: $accent; } h3 { margin: 0; font-size: 14px; font-weight: 600; - color: #202124; + color: $gray-900; } } } @@ -49,6 +55,18 @@ } } +.fdpd-grid--classification { + grid-template-columns: repeat(3, 1fr); + + @media (width <= 900px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + .fdpd-field { width: 100%; @@ -56,3 +74,9 @@ grid-column: 1 / -1; } } + +.fdpd-checkbox-row { + display: flex; + align-items: center; + min-height: 56px; +} diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.ts b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.ts index bce5e5a57..30b91dffb 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.ts +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-details-step/fixed-deposit-product-details-step.component.ts @@ -6,8 +6,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, OnInit, Input, inject } from '@angular/core'; -import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +import { Component, OnInit, OnChanges, Input, SimpleChanges, inject, ChangeDetectorRef } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; +import { CdkTextareaAutosize } from '@angular/cdk/text-field'; +import { MatTooltip } from '@angular/material/tooltip'; +import { Dates } from 'app/core/utils/dates'; +import { SettingsService } from 'app/settings/settings.service'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ @@ -15,27 +19,41 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; templateUrl: './fixed-deposit-product-details-step.component.html', styleUrls: ['./fixed-deposit-product-details-step.component.scss'], imports: [ - ...STANDALONE_SHARED_IMPORTS + ...STANDALONE_SHARED_IMPORTS, + CdkTextareaAutosize, + MatTooltip ] }) -export class FixedDepositProductDetailsStepComponent implements OnInit { +export class FixedDepositProductDetailsStepComponent implements OnInit, OnChanges { private formBuilder = inject(UntypedFormBuilder); + private dateUtils = inject(Dates); + private settingsService = inject(SettingsService); + private cdr = inject(ChangeDetectorRef); @Input() fixedDepositProductsTemplate: any; fixedDepositProductDetailsForm: UntypedFormGroup; + accountTypeData: string[] = []; + customerSegmentData: string[] = []; + liabilityAccountOptions: any[] = []; + yesNoOptions: Array<{ value: boolean; label: string }> = [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' } + ]; constructor() { this.createFixedDepositProductDetailsForm(); } ngOnInit() { - if (!(this.fixedDepositProductsTemplate === undefined) && this.fixedDepositProductsTemplate.id) { - this.fixedDepositProductDetailsForm.patchValue({ - name: this.fixedDepositProductsTemplate.name, - shortName: this.fixedDepositProductsTemplate.shortName, - description: this.fixedDepositProductsTemplate.description - }); + if (this.fixedDepositProductsTemplate) { + this.applyTemplate(this.fixedDepositProductsTemplate); + } + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.fixedDepositProductsTemplate?.currentValue) { + this.applyTemplate(changes.fixedDepositProductsTemplate.currentValue); } } @@ -49,14 +67,66 @@ export class FixedDepositProductDetailsStepComponent implements OnInit { '', Validators.required ], - description: [ - '', + description: [''], + accountType: [ + null, Validators.required - ] + ], + customerSegment: [null], + glAccountId: [null], + effectiveTo: [''], + endDate: [''], + introducerRequired: [false], + shareLinkingEnabled: [false] + }); + } + + private getOptionNames(options: any[]): string[] { + if (!Array.isArray(options)) return []; + return options.map((o) => o?.name ?? o?.value ?? o).filter((v) => typeof v === 'string' && v.trim().length > 0); + } + + private parseDate(dateValue: any): Date | null { + if (!dateValue) return null; + if (Array.isArray(dateValue)) return new Date(dateValue[0], dateValue[1] - 1, dateValue[2]); + if (dateValue instanceof Date) return dateValue; + return new Date(dateValue); + } + + compareById(o1: any, o2: any): boolean { + return o1 != null && o2 != null && Number(o1) === Number(o2); + } + + private applyTemplate(template: any) { + this.accountTypeData = this.getOptionNames(template.accountTypeOptions); + this.customerSegmentData = this.getOptionNames(template.customerSegmentOptions); + this.liabilityAccountOptions = template.accountingMappingOptions?.liabilityAccountOptions ?? []; + this.cdr.detectChanges(); + + const isEditing = !!template.id; + this.fixedDepositProductDetailsForm.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, + glAccountId: template.glAccountId ?? null, + effectiveTo: this.parseDate(template.effectiveTo), + endDate: this.parseDate(template.endDate), + introducerRequired: template.introducerRequired === true || template.introducerRequired === 'Yes', + shareLinkingEnabled: !!template.shareLinkingEnabled }); } get fixedDepositProductDetails() { - return this.fixedDepositProductDetailsForm.value; + const formValue = { ...this.fixedDepositProductDetailsForm.value }; + const dateFormat = this.settingsService.dateFormat; + if (formValue.effectiveTo instanceof Date) { + formValue.effectiveTo = this.dateUtils.formatDate(formValue.effectiveTo, dateFormat) || ''; + } + if (formValue.endDate instanceof Date) { + formValue.endDate = this.dateUtils.formatDate(formValue.endDate, dateFormat) || ''; + } + return formValue; } } diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.html b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.html index 5c8f66550..7843f2654 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.html +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.html @@ -7,204 +7,124 @@ -->
+
- -

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

+ + Premature option
-
- - {{ 'labels.inputs.Frequency' | translate }} - - - - - {{ 'labels.inputs.Type' | translate }} - - @for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) { - - {{ lockinPeriodFrequencyType.value }} - - } - - +
+
+ Loan Facility + Premature Withdrawal option + Tenure Flexibility + Lien Marked + Apply Withdrawal Fee for Transfers +
+
- -

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

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

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

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

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

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

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

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

{{ 'labels.inputs.Is Withhold Tax Applicable' | translate }}

-
-
-
- - {{ 'labels.inputs.Is Withhold Tax Applicable' | translate }} - -
- - @if (fixedDepositProductSettingsForm.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.Type' | translate }} + + @for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) { + + {{ lockinPeriodFrequencyType.value | translateKey: 'catalogs' }} + + } + + +
}
+ + +
+
+ + Maturity Instructions +
+
+
+ Maturity Instructions + +
+
+ Option to credit maturity proceeds directly to the linked account or reinvest in another product (like + FD). + +
+
+ Penalty / Grace Period for Missed Installments + +
+
+ Panel Request Applicable + +
+
+ No penalty for nominee / legal heir closure + +
+
+ Deceased Settlement + +
+
+ Allow Debit Txn after due date + +
+
+
+ + +
+
+ + Additional Rules +
+
+
+
+ + Account Closed with Zero balance in Day Closing + +
+ + + Balance Required for Interest Calculation + + +
+
+
diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.scss b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.scss index 62b1187f8..6017718c9 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.scss +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.scss @@ -6,8 +6,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +$accent: #0ea5e9; +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-900: #202124; + .fdps-section { - border: 1px solid #e2e8f0; + border: 1px solid $border; border-radius: 10px; overflow: hidden; background: #fff; @@ -21,32 +27,41 @@ align-items: center; gap: 10px; padding: 12px 24px; - background: #f8f9fa; - border-bottom: 1px solid #e8eaed; + background: $gray-50; + border-bottom: 1px solid $gray-200; i { font-size: 14px; - color: #0ea5e9; + color: $accent; } - h3 { + span:not(.fdps-section__badge) { margin: 0; font-size: 14px; font-weight: 600; - color: #202124; + color: $gray-900; } } } -.fdps-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 4px 16px; - padding: 16px 24px; +.fdps-section__badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: #eff6ff; + font-size: 14px; +} - @media (width <= 600px) { - grid-template-columns: 1fr; - } +.fdps-body { + background: #fff; + padding: 18px 24px 24px; +} + +.fdps-body--list { + padding: 0 24px; } .fdps-field { @@ -55,10 +70,91 @@ &--full { grid-column: 1 / -1; } + + &--compact { + width: 200px; + min-width: 150px; + } } -.fdps-checkbox-row { +.fdps-premature-row { display: flex; align-items: center; - padding: 4px 0; + gap: 24px; + flex-wrap: wrap; + + mat-checkbox { + font-size: 14px; + color: $gray-900; + } +} + +.fdps-list-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 14px 0; + border-bottom: 1px solid $gray-200; + font-size: 14px; + color: $gray-900; + + &:last-child { + border-bottom: none; + } + + span { + max-width: 85%; + line-height: 1.4; + } +} + +.fdps-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; +} + +.fdps-lockin-fields { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + + @media (width <= 640px) { + grid-template-columns: 1fr; + } +} + +.fdps-layout { + display: grid; + gap: 16px; +} + +.fdps-layout--additional { + grid-template-columns: repeat(2, minmax(0, 1fr)); + + @media (width <= 1100px) { + grid-template-columns: 1fr; + } +} + +.fdps-toggle { + display: flex; + align-items: center; + min-height: 40px; + + mat-checkbox { + font-size: 14px; + color: $gray-900; + } + + &--inline { + min-height: auto; + } } diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.ts b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.ts index 4c1bad874..33933731c 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.ts +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.ts @@ -7,13 +7,7 @@ */ import { Component, OnInit, Input, inject } from '@angular/core'; -import { - UntypedFormGroup, - UntypedFormBuilder, - UntypedFormControl, - Validators, - ReactiveFormsModule -} from '@angular/forms'; +import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; import { MatTooltip } from '@angular/material/tooltip'; import { MatCheckbox } from '@angular/material/checkbox'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @@ -36,113 +30,71 @@ export class FixedDepositProductSettingsStepComponent implements OnInit { fixedDepositProductSettingsForm: UntypedFormGroup; lockinPeriodFrequencyTypeData: any; - periodFrequencyTypeData: any; - preClosurePenalInterestOnTypeData: any; - taxGroupData: any; constructor() { this.createFixedDepositProductSettingsForm(); - this.setConditionalControls(); } ngOnInit() { this.lockinPeriodFrequencyTypeData = this.fixedDepositProductsTemplate.lockinPeriodFrequencyTypeOptions; - this.periodFrequencyTypeData = this.fixedDepositProductsTemplate.periodFrequencyTypeOptions.slice(0, -1); - this.preClosurePenalInterestOnTypeData = this.fixedDepositProductsTemplate.preClosurePenalInterestOnTypeOptions; - this.taxGroupData = this.fixedDepositProductsTemplate.taxGroupOptions; - if (!(this.fixedDepositProductsTemplate === undefined) && this.fixedDepositProductsTemplate.id) { + if (this.fixedDepositProductsTemplate?.id) { this.fixedDepositProductSettingsForm.patchValue({ - isMandatoryDeposit: this.fixedDepositProductsTemplate.isMandatoryDeposit, - adjustAdvanceTowardsFuturePayments: this.fixedDepositProductsTemplate.adjustAdvanceTowardsFuturePayments, - allowWithdrawal: this.fixedDepositProductsTemplate.allowWithdrawal, - lockinPeriodFrequency: this.fixedDepositProductsTemplate.lockinPeriodFrequency, - lockinPeriodFrequencyType: this.fixedDepositProductsTemplate.lockinPeriodFrequencyType - ? this.fixedDepositProductsTemplate.lockinPeriodFrequencyType.id - : '', - minDepositTerm: this.fixedDepositProductsTemplate.minDepositTerm, - minDepositTermTypeId: this.fixedDepositProductsTemplate.minDepositTermType - ? this.fixedDepositProductsTemplate.minDepositTermType.id - : '', - inMultiplesOfDepositTerm: this.fixedDepositProductsTemplate.inMultiplesOfDepositTerm, - inMultiplesOfDepositTermTypeId: this.fixedDepositProductsTemplate.inMultiplesOfDepositTermType - ? this.fixedDepositProductsTemplate.inMultiplesOfDepositTerm.id - : '', - maxDepositTerm: this.fixedDepositProductsTemplate.maxDepositTerm, - maxDepositTermTypeId: this.fixedDepositProductsTemplate.maxDepositTermType - ? this.fixedDepositProductsTemplate.minDepositTermType.id - : '', - preClosurePenalApplicable: this.fixedDepositProductsTemplate.preClosurePenalApplicable, - preClosurePenalInterest: this.fixedDepositProductsTemplate.preClosurePenalInterest, - preClosurePenalInterestOnTypeId: this.fixedDepositProductsTemplate.preClosurePenalInterestOnType - ? this.fixedDepositProductsTemplate.preClosurePenalInterestOnType.id - : '', - withHoldTax: this.fixedDepositProductsTemplate.withHoldTax - }); - } - - if (this.fixedDepositProductsTemplate.withHoldTax) { - this.fixedDepositProductSettingsForm.patchValue({ - taxGroupId: this.fixedDepositProductsTemplate.taxGroup ? this.fixedDepositProductsTemplate.taxGroup.id : '' + loanFacility: this.fixedDepositProductsTemplate.loanFacility ?? false, + prematureWithdrawalOption: this.fixedDepositProductsTemplate.prematureWithdrawalOption ?? false, + tenureFlexibility: this.fixedDepositProductsTemplate.tenureFlexibility ?? false, + lienMarked: this.fixedDepositProductsTemplate.lienMarked ?? false, + withdrawalFeeForTransfers: this.fixedDepositProductsTemplate.withdrawalFeeForTransfers ?? false, + enableLockinPeriod: this.fixedDepositProductsTemplate.enableLockinPeriod ?? false, + lockinPeriodFrequency: this.fixedDepositProductsTemplate.lockinPeriodFrequency ?? '', + lockinPeriodFrequencyType: this.fixedDepositProductsTemplate.lockinPeriodFrequencyType?.id ?? '', + maturityInstructions: this.fixedDepositProductsTemplate.maturityInstructions ?? false, + creditMaturityProceeds: this.fixedDepositProductsTemplate.creditMaturityProceeds ?? false, + penaltyGracePeriodMissedInstallments: + this.fixedDepositProductsTemplate.penaltyGracePeriodMissedInstallments ?? false, + panelRequestApplicable: this.fixedDepositProductsTemplate.panelRequestApplicable ?? false, + noPenaltyNomineeHeirClosure: this.fixedDepositProductsTemplate.noPenaltyNomineeHeirClosure ?? false, + deceasedSettlement: this.fixedDepositProductsTemplate.deceasedSettlement ?? false, + allowDebitTxnAfterDueDate: this.fixedDepositProductsTemplate.allowDebitTxnAfterDueDate ?? false, + accountClosedZeroBalanceInDayClosing: + this.fixedDepositProductsTemplate.accountClosedZeroBalanceInDayClosing ?? false, + minBalanceForInterestCalculation: this.fixedDepositProductsTemplate.minBalanceForInterestCalculation ?? '' }); } } createFixedDepositProductSettingsForm() { this.fixedDepositProductSettingsForm = this.formBuilder.group({ + loanFacility: [false], + prematureWithdrawalOption: [false], + tenureFlexibility: [false], + lienMarked: [false], + withdrawalFeeForTransfers: [false], + enableLockinPeriod: [false], lockinPeriodFrequency: [ '', Validators.min(0) ], lockinPeriodFrequencyType: [''], - minDepositTerm: [ - '', - [ - Validators.required, - Validators.min(0) - ] - ], - minDepositTermTypeId: [ - '', - Validators.required - ], - inMultiplesOfDepositTerm: [ - '', - Validators.min(0) - ], - inMultiplesOfDepositTermTypeId: [''], - maxDepositTerm: [ - '', - Validators.min(0) - ], - maxDepositTermTypeId: [''], - preClosurePenalApplicable: [false], - preClosurePenalInterest: [ - '', - Validators.min(0) - ], - preClosurePenalInterestOnTypeId: [''], - withHoldTax: [false] - }); - } - - setConditionalControls() { - this.fixedDepositProductSettingsForm.get('withHoldTax').valueChanges.subscribe((withHoldTax: any) => { - if (withHoldTax) { - this.fixedDepositProductSettingsForm.addControl('taxGroupId', new UntypedFormControl('', Validators.required)); - } else { - this.fixedDepositProductSettingsForm.removeControl('taxGroupId'); - } + maturityInstructions: [false], + creditMaturityProceeds: [false], + penaltyGracePeriodMissedInstallments: [false], + panelRequestApplicable: [false], + noPenaltyNomineeHeirClosure: [false], + deceasedSettlement: [false], + allowDebitTxnAfterDueDate: [false], + accountClosedZeroBalanceInDayClosing: [false], + minBalanceForInterestCalculation: [''] }); } get fixedDepositProductSettings() { - const fixedDepositProductSettings = this.fixedDepositProductSettingsForm.value; - for (const key in fixedDepositProductSettings) { - if (fixedDepositProductSettings[key] === '') { - delete fixedDepositProductSettings[key]; + const settings = { ...this.fixedDepositProductSettingsForm.value }; + for (const key in settings) { + if (settings[key] === '') { + delete settings[key]; } } - return fixedDepositProductSettings; + return settings; } } diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.html b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.html index c2228368b..ab0346c21 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.html +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.html @@ -7,139 +7,148 @@ -->
+
- -

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

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

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

-
-
- - {{ 'labels.inputs.Interest Compounding Period' | translate }} - - @for ( - interestCompoundingPeriodType of interestCompoundingPeriodTypeData; - track interestCompoundingPeriodType - ) { - - {{ interestCompoundingPeriodType.value }} - +
+ + {{ 'labels.inputs.Nominal Annual Interest' | translate }} + + @if (fixedDepositProductTermsForm.get('nominalAnnualInterestRate').hasError('required')) { + + {{ 'labels.inputs.Nominal Annual Interest' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + } - - - {{ 'labels.inputs.Interest Compounding Period' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.Interest Posting Period' | translate }} - - @for (interestPostingPeriodType of interestPostingPeriodTypeData; track interestPostingPeriodType) { - - {{ interestPostingPeriodType.value }} - + @if (fixedDepositProductTermsForm.get('nominalAnnualInterestRate').hasError('min')) { + {{ 'labels.inputs.Nominal Annual Interest' | translate }} must be zero or greater } - - - {{ 'labels.inputs.Interest Posting Period' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - + - - {{ 'labels.inputs.Interest Calculated using' | translate }} - - @for (interestCalculationType of interestCalculationTypeData; track interestCalculationType) { - - {{ interestCalculationType.value }} - - } - - - {{ 'labels.inputs.Interest Calculated using' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - + + Effective Date + + + + - - {{ 'labels.inputs.Days in Year' | translate }} - - @for ( - interestCalculationDaysInYearType of interestCalculationDaysInYearTypeData; - track interestCalculationDaysInYearType - ) { - - {{ interestCalculationDaysInYearType.value }} - - } - - - {{ 'labels.inputs.Days in Year' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - + + {{ 'labels.inputs.Interest Calculation Type' | translate }} + + @for (opt of interestCalculationTypeOptionData; track opt.id) { + {{ opt.value }} + } + + +
+ +
+ + Interest Calculate + + @for (mode of interestCalculationTypeData; track mode.id) { + {{ mode.value | translateKey: 'catalogs' }} + } + + + {{ 'labels.inputs.Interest Calculation Type' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + @if (fixedDepositProductTermsForm.value.interestCalculationType === 2) { + + Average Period + + @for (period of periodFrequencyData; track period.id) { + {{ period.value }} + } + + + } + + + {{ 'labels.inputs.Interest Posting Period' | translate }} + + @for (interestPostingPeriodType of interestPostingPeriodTypeData; track interestPostingPeriodType) { + + {{ interestPostingPeriodType.value | translateKey: 'catalogs' }} + + } + + + {{ 'labels.inputs.Interest Posting Period' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + +
+ +
diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.scss b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.scss index ac8a2ddd5..8b4b08ace 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.scss +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.scss @@ -6,8 +6,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +$accent: #0ea5e9; +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-900: #202124; + .fdpt-section { - border: 1px solid #e2e8f0; + border: 1px solid $border; border-radius: 10px; overflow: hidden; background: #fff; @@ -21,31 +27,54 @@ align-items: center; gap: 10px; padding: 12px 24px; - background: #f8f9fa; - border-bottom: 1px solid #e8eaed; + background: $gray-50; + border-bottom: 1px solid $gray-200; i { font-size: 14px; - color: #0ea5e9; + color: $accent; } - h3 { + span:not(.fdpt-section__badge) { margin: 0; font-size: 14px; font-weight: 600; - color: #202124; + color: $gray-900; } } } +.fdpt-section__badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: #eff6ff; + font-size: 14px; +} + +.fdpt-body { + background: #fff; +} + .fdpt-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 4px 16px; padding: 16px 24px; + &--three { + grid-template-columns: repeat(3, 1fr); + } + @media (width <= 600px) { grid-template-columns: 1fr; + + &--three { + grid-template-columns: 1fr; + } } } @@ -56,3 +85,67 @@ grid-column: 1 / -1; } } + +.fdpt-footer { + padding: 0 24px 24px; + border-top: 1px solid #f1f5f9; +} + +.fdpt-footer__grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + padding-top: 18px; + + @media (width <= 900px) { + grid-template-columns: 1fr; + } +} + +.fdpt-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; + } +} + +.fdpt-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; + } +} + +.fdpt-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/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.ts b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.ts index c58163ebf..29dc4defa 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.ts +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.ts @@ -7,8 +7,10 @@ */ import { Component, OnInit, Input, inject } from '@angular/core'; -import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; import { MatTooltip } from '@angular/material/tooltip'; +import { Dates } from 'app/core/utils/dates'; +import { SettingsService } from 'app/settings/settings.service'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ @@ -22,6 +24,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; }) export class FixedDepositProductTermsStepComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); + private dateUtils = inject(Dates); + private settingsService = inject(SettingsService); @Input() fixedDepositProductsTemplate: any; @@ -30,7 +34,13 @@ export class FixedDepositProductTermsStepComponent 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.createFixedDepositProductTermsForm(); @@ -40,10 +50,22 @@ export class FixedDepositProductTermsStepComponent implements OnInit { this.interestCompoundingPeriodTypeData = this.fixedDepositProductsTemplate.interestCompoundingPeriodTypeOptions; this.interestPostingPeriodTypeData = this.fixedDepositProductsTemplate.interestPostingPeriodTypeOptions; this.interestCalculationTypeData = this.fixedDepositProductsTemplate.interestCalculationTypeOptions; + this.interestCalculationTypeOptionData = this.fixedDepositProductsTemplate.interestCalculationTypeOption || []; this.interestCalculationDaysInYearTypeData = this.fixedDepositProductsTemplate.interestCalculationDaysInYearTypeOptions; + this.periodFrequencyData = this.fixedDepositProductsTemplate.periodFrequencyOptions || []; - if (!(this.fixedDepositProductsTemplate === undefined) && this.fixedDepositProductsTemplate.id) { + const isEditing = !!this.fixedDepositProductsTemplate.id; + const defaultPeriod = this.periodFrequencyData[0]?.value || 'Monthly'; + const savedEnumTypeId = + this.fixedDepositProductsTemplate.interestCalculationType?.id || this.interestCalculationTypeData?.[0]?.id || 1; + const savedSelectorId = isEditing + ? (this.fixedDepositProductsTemplate.interestCalculateSelector ?? + this.interestCalculationTypeOptionData?.[0]?.id ?? + null) + : (this.interestCalculationTypeOptionData?.[0]?.id ?? null); + + if (isEditing) { this.fixedDepositProductTermsForm.patchValue({ minDepositAmount: this.fixedDepositProductsTemplate.minDepositAmount, depositAmount: this.fixedDepositProductsTemplate.depositAmount, @@ -52,13 +74,31 @@ export class FixedDepositProductTermsStepComponent implements OnInit { } this.fixedDepositProductTermsForm.patchValue({ - interestCompoundingPeriodType: this.fixedDepositProductsTemplate.interestCompoundingPeriodType.id, - interestPostingPeriodType: this.fixedDepositProductsTemplate.interestPostingPeriodType.id, - interestCalculationType: this.fixedDepositProductsTemplate.interestCalculationType.id, - interestCalculationDaysInYearType: this.fixedDepositProductsTemplate.interestCalculationDaysInYearType.id + nominalAnnualInterestRate: this.fixedDepositProductsTemplate.nominalAnnualInterestRate ?? 0, + interestCompoundingPeriodType: this.fixedDepositProductsTemplate.interestCompoundingPeriodType?.id, + interestPostingPeriodType: this.fixedDepositProductsTemplate.interestPostingPeriodType?.id, + interestCalculationType: savedEnumTypeId, + interestCalculationDaysInYearType: this.fixedDepositProductsTemplate.interestCalculationDaysInYearType?.id, + effectiveDate: this.fixedDepositProductsTemplate.effectiveDate + ? this.parseDate(this.fixedDepositProductsTemplate.effectiveDate) + : '', + interestCalculateSelector: savedSelectorId, + averagePeriod: isEditing ? this.fixedDepositProductsTemplate.averagePeriod || defaultPeriod : defaultPeriod, + autoPost: isEditing ? (this.fixedDepositProductsTemplate.autoPost ?? true) : true, + frequencyFixDay: isEditing ? (this.fixedDepositProductsTemplate.frequencyFixDay ?? 30) : 30, + lastInterestDate: this.fixedDepositProductsTemplate.lastInterestDate + ? this.parseDate(this.fixedDepositProductsTemplate.lastInterestDate) + : '' }); } + private parseDate(dateValue: any): Date | null { + if (!dateValue) return null; + if (Array.isArray(dateValue)) return new Date(dateValue[0], dateValue[1] - 1, dateValue[2]); + if (dateValue instanceof Date) return dateValue; + return new Date(dateValue); + } + createFixedDepositProductTermsForm() { this.fixedDepositProductTermsForm = this.formBuilder.group({ minDepositAmount: [ @@ -76,6 +116,13 @@ export class FixedDepositProductTermsStepComponent implements OnInit { '', Validators.min(0) ], + nominalAnnualInterestRate: [ + '', + [ + Validators.required, + Validators.min(0) + ] + ], interestCompoundingPeriodType: [ '', Validators.required @@ -91,17 +138,34 @@ export class FixedDepositProductTermsStepComponent implements OnInit { interestCalculationDaysInYearType: [ '', Validators.required - ] + ], + effectiveDate: [''], + interestCalculateSelector: [1], + averagePeriod: ['Monthly'], + autoPost: [true], + frequencyFixDay: [ + 30, + [Validators.min(0)] + ], + lastInterestDate: [''] }); } get fixedDepositProductTerms() { - const fixedDepositProductTerms = this.fixedDepositProductTermsForm.value; - for (const key in fixedDepositProductTerms) { - if (fixedDepositProductTerms[key] === '') { - delete fixedDepositProductTerms[key]; + const formValue = { ...this.fixedDepositProductTermsForm.value }; + const dateFormat = this.settingsService.dateFormat; + + if (formValue.effectiveDate instanceof Date) { + formValue.effectiveDate = this.dateUtils.formatDate(formValue.effectiveDate, dateFormat) || ''; + } + if (formValue.lastInterestDate instanceof Date) { + formValue.lastInterestDate = this.dateUtils.formatDate(formValue.lastInterestDate, dateFormat) || ''; + } + for (const key in formValue) { + if (formValue[key] === '') { + delete formValue[key]; } } - return fixedDepositProductTerms; + return formValue; } }