feat: merge feature/merge-updates into feature/ui-reports-product

- FD product stepper steps updated to match RD layout (Details, Currency, Terms, Settings, Charges, Accounting)
- FD account creation stepper restructured with conditional steps and Previous/Next navigation
- RD account details step and preview step updates
- Theme and environment updates
This commit is contained in:
Nitesh Yadav
2026-05-01 10:40:22 +05:30
30 changed files with 2336 additions and 1467 deletions
@@ -34,7 +34,8 @@
<fa-icon icon="eye" size="sm"></fa-icon> <fa-icon icon="eye" size="sm"></fa-icon>
</ng-template> </ng-template>
<mat-step [stepControl]="fixedDepositAccountDetailsForm"> <!-- Step 1: Details -->
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.DETAILS' | translate }}</ng-template> <ng-template matStepLabel>{{ 'labels.inputs.DETAILS' | translate }}</ng-template>
<mifosx-fixed-deposit-account-details-step <mifosx-fixed-deposit-account-details-step
@@ -44,48 +45,97 @@
</mifosx-fixed-deposit-account-details-step> </mifosx-fixed-deposit-account-details-step>
</mat-step> </mat-step>
<mat-step [stepControl]="fixedDepositAccountTermsForm"> @if (fixedDepositsAccountProductTemplate) {
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template> <!-- Step 2: Terms -->
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template>
<mifosx-fixed-deposit-account-terms-step <mifosx-fixed-deposit-account-terms-step
[fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate" [fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate"
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate" [fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
> >
</mifosx-fixed-deposit-account-terms-step> </mifosx-fixed-deposit-account-terms-step>
</mat-step>
<mat-step [stepControl]="fixedDepositAccountSettingsForm"> <div class="layout-row align-center gap-2percent margin-t responsive-column">
<ng-template matStepLabel>{{ 'labels.inputs.SETTINGS' | translate }}</ng-template> <button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<mifosx-fixed-deposit-account-settings-step <!-- Step 3: Settings -->
[fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate" <mat-step>
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate" <ng-template matStepLabel>{{ 'labels.inputs.SETTINGS' | translate }}</ng-template>
>
</mifosx-fixed-deposit-account-settings-step>
</mat-step>
<mat-step> <mifosx-fixed-deposit-account-settings-step
<ng-template matStepLabel>{{ 'labels.inputs.INTEREST RATE CHART' | translate }}</ng-template> [fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate"
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
>
</mifosx-fixed-deposit-account-settings-step>
<mifosx-fixed-deposit-account-interest-rate-chart-step <div class="layout-row align-center gap-2percent margin-t responsive-column">
[fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate" <button mat-raised-button matStepperPrevious>
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate" <fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
> {{ 'labels.buttons.Previous' | translate }}
</mifosx-fixed-deposit-account-interest-rate-chart-step> </button>
</mat-step> <button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<mat-step> <!-- Step 4: Interest Rate Chart -->
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template> <mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.INTEREST RATE CHART' | translate }}</ng-template>
<mifosx-fixed-deposit-account-charges-step <mifosx-fixed-deposit-account-interest-rate-chart-step
[fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate" [fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate"
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate" [fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
[fixedDepositAccountFormValid]="fixedDepositAccountFormValid" >
> </mifosx-fixed-deposit-account-interest-rate-chart-step>
</mifosx-fixed-deposit-account-charges-step>
</mat-step>
@if (fixedDepositAccountFormValid) { <div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<!-- Step 5: Charges -->
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template>
<mifosx-fixed-deposit-account-charges-step
[fixedDepositsAccountTemplate]="fixedDepositsAccountTemplate"
[fixedDepositsAccountProductTemplate]="fixedDepositsAccountProductTemplate"
[fixedDepositAccountFormValid]="fixedDepositAccountFormValid"
>
</mifosx-fixed-deposit-account-charges-step>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</mat-step>
<!-- Step 6: Preview -->
<mat-step state="preview" completed> <mat-step state="preview" completed>
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template> <ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template>
<mifosx-fixed-deposit-account-preview-step <mifosx-fixed-deposit-account-preview-step
@@ -20,7 +20,14 @@ import { FixedDepositAccountTermsStepComponent } from '../fixed-deposit-account-
import { FixedDepositAccountSettingsStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-settings-step/fixed-deposit-account-settings-step.component'; import { FixedDepositAccountSettingsStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-settings-step/fixed-deposit-account-settings-step.component';
import { FixedDepositAccountChargesStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-charges-step/fixed-deposit-account-charges-step.component'; import { FixedDepositAccountChargesStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-charges-step/fixed-deposit-account-charges-step.component';
import { Dates } from 'app/core/utils/dates'; import { Dates } from 'app/core/utils/dates';
import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper'; import {
MatStepper,
MatStepperIcon,
MatStep,
MatStepLabel,
MatStepperNext,
MatStepperPrevious
} from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { FixedDepositAccountInterestRateChartStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component'; import { FixedDepositAccountInterestRateChartStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-interest-rate-chart-step/fixed-deposit-account-interest-rate-chart-step.component';
import { FixedDepositAccountPreviewStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component'; import { FixedDepositAccountPreviewStepComponent } from '../fixed-deposit-account-stepper/fixed-deposit-account-preview-step/fixed-deposit-account-preview-step.component';
@@ -37,6 +44,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
...STANDALONE_SHARED_IMPORTS, ...STANDALONE_SHARED_IMPORTS,
MatStepper, MatStepper,
MatStepperIcon, MatStepperIcon,
MatStepperNext,
MatStepperPrevious,
FaIconComponent, FaIconComponent,
MatStep, MatStep,
MatStepLabel, MatStepLabel,
@@ -55,17 +64,13 @@ export class CreateFixedDepositAccountComponent {
private fixedDepositsService = inject(FixedDepositsService); private fixedDepositsService = inject(FixedDepositsService);
private settingsService = inject(SettingsService); private settingsService = inject(SettingsService);
/** Fixed Deposits Account Details Step */
@ViewChild(FixedDepositAccountDetailsStepComponent, { static: true }) @ViewChild(FixedDepositAccountDetailsStepComponent, { static: true })
fixedDepositsAccountDetailsStep: FixedDepositAccountDetailsStepComponent; fixedDepositsAccountDetailsStep: FixedDepositAccountDetailsStepComponent;
/** Fixed Deposits Account Terms Step */ @ViewChild(FixedDepositAccountTermsStepComponent)
@ViewChild(FixedDepositAccountTermsStepComponent, { static: true })
fixedDepositAccountTermsStep: FixedDepositAccountTermsStepComponent; fixedDepositAccountTermsStep: FixedDepositAccountTermsStepComponent;
/** Fixed Deposits Account Settings Step */ @ViewChild(FixedDepositAccountSettingsStepComponent)
@ViewChild(FixedDepositAccountSettingsStepComponent, { static: true })
fixedDepositAccountSettingsStep: FixedDepositAccountSettingsStepComponent; fixedDepositAccountSettingsStep: FixedDepositAccountSettingsStepComponent;
/** Fixed Deposits Account Charges Step */ @ViewChild(FixedDepositAccountChargesStepComponent)
@ViewChild(FixedDepositAccountChargesStepComponent, { static: true })
fixedDepositAccountChargesStep: FixedDepositAccountChargesStepComponent; fixedDepositAccountChargesStep: FixedDepositAccountChargesStepComponent;
/** Fixed Deposits Account Template */ /** Fixed Deposits Account Template */
@@ -145,22 +145,41 @@ export class CreateRecurringDepositsAccountComponent {
const locale = this.settingsService.language.code; const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat; const dateFormat = this.settingsService.dateFormat;
const monthDayFormat = 'dd MMMM'; const monthDayFormat = 'dd MMMM';
const recurringDepositAccount = {
...this.recurringDepositAccount, const details = this.recurringDepositAccount;
...this.recurringDepositsAccountCurrencyStep.currencyStepData, const currencyStep = this.recurringDepositsAccountCurrencyStep;
const recurringDepositAccount: any = {
...details,
clientId: this.recurringDepositsAccountTemplate.clientId, clientId: this.recurringDepositsAccountTemplate.clientId,
charges: [], charges: [],
isCalendarInherited: false, isCalendarInherited: false,
submittedOnDate: this.dateUtils.formatDate(this.recurringDepositAccount.submittedOnDate, dateFormat), mandatoryRecommendedDepositAmount: 0,
expectedFirstDepositOnDate: this.dateUtils.formatDate( recurringFrequency: 1,
this.recurringDepositAccount.expectedFirstDepositOnDate, recurringFrequencyType: 2,
dateFormat submittedOnDate: this.dateUtils.formatDate(details.submittedOnDate, dateFormat),
), expectedFirstDepositOnDate: this.dateUtils.formatDate(details.expectedFirstDepositOnDate, dateFormat),
dateFormat, dateFormat,
monthDayFormat, 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.recurringDepositsService.createRecurringDepositAccount(recurringDepositAccount).subscribe((response: any) => {
this.router.navigate( this.router.navigate(
[ [
@@ -494,6 +494,13 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
get recurringDepositAccountDetails() { get recurringDepositAccountDetails() {
const value = this.recurringDepositAccountDetailsForm.value; const value = this.recurringDepositAccountDetailsForm.value;
if (this.selectedAccountType) {
value.accountType = this.selectedAccountType;
}
if (this.selectedCustomerSegment) {
value.customerSegment = this.selectedCustomerSegment;
}
if (this.selectedAccountStatus) { if (this.selectedAccountStatus) {
value.accountStatus = this.selectedAccountStatus; value.accountStatus = this.selectedAccountStatus;
} }
@@ -503,8 +510,8 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
value.jointHolders = [ value.jointHolders = [
{ {
clientId: this.primaryClientId, clientId: this.primaryClientId,
clientName: this.primaryClientName, displayName: this.primaryClientName,
clientCif: this.primaryClientCif, cifNumber: this.primaryClientCif,
sharePercentage: this.primaryHolderShare, sharePercentage: this.primaryHolderShare,
isMandatory: true, isMandatory: true,
isPrimary: true, isPrimary: true,
@@ -513,8 +520,8 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
}, },
...this.jointHolders.map((h) => ({ ...this.jointHolders.map((h) => ({
clientId: h.clientId, clientId: h.clientId,
clientName: h.clientName, displayName: h.clientName,
clientCif: h.clientCif, cifNumber: h.clientCif,
sharePercentage: h.sharePercentage, sharePercentage: h.sharePercentage,
isMandatory: h.isMandatory, isMandatory: h.isMandatory,
isPrimary: false, isPrimary: false,
@@ -535,9 +542,9 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
if (this.selectedAccountStatus === 'MINOR') { if (this.selectedAccountStatus === 'MINOR') {
value.guardianClientId = this.guardianClientId; value.guardianClientId = this.guardianClientId;
value.guardianName = this.guardianName; value.guardianName = this.guardianName;
value.guardianCif = this.guardianCif; value.guardianCifNumber = this.guardianCif;
value.guardianRelationship = this.guardianRelationship; 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) { if (this.selectedLinkedAccount) {
@@ -43,6 +43,237 @@
</div> </div>
</div> </div>
<!-- ══ 1b. Status of Account ════════════════════════════════════ -->
@if (recurringDepositAccountData?.accountStatus) {
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-id-card preview-section__icon"></i>
<span class="preview-section__title">Status of Account</span>
<span class="preview-section__badge">RBI Guidelines</span>
</div>
<div class="preview-body">
<!-- Status Tile -->
<div class="preview-soa-status-row">
<span class="preview-soa-tile">
@switch (recurringDepositAccountData.accountStatus) {
@case ('INDIVIDUAL_MALE') {
<i class="fas fa-male"></i> Individual (Male)
}
@case ('INDIVIDUAL_FEMALE') {
<i class="fas fa-female"></i> Individual (Female)
}
@case ('SINGLE') {
<i class="fas fa-user-circle"></i> Single Account
}
@case ('JOINT') {
<i class="fas fa-users"></i> Joint Account
}
@case ('MINOR') {
<i class="fas fa-child"></i> Minor Account
}
@case ('SENIOR_CITIZEN') {
<i class="fas fa-user-clock"></i> Senior Citizen
}
@default {
{{ recurringDepositAccountData.accountStatus }}
}
}
</span>
</div>
<!-- JOINT: Mode of Operation + Holders Table -->
@if (recurringDepositAccountData.accountStatus === 'JOINT') {
<div class="preview-grid preview-grid--three" style="margin-top: 12px">
<div class="preview-field">
<span class="preview-label">Mode of Operation</span>
<span class="preview-value">
@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 || '—' }}
}
}
</span>
</div>
</div>
@if (recurringDepositAccountData.jointHolders?.length) {
<div class="preview-soa-table-wrap">
<table class="preview-soa-tbl">
<thead>
<tr>
<th class="preview-soa-th">Sr.</th>
<th class="preview-soa-th">Holder Name</th>
<th class="preview-soa-th">CIF No.</th>
<th class="preview-soa-th">Ownership %</th>
<th class="preview-soa-th">Signing</th>
<th class="preview-soa-th">Role</th>
</tr>
</thead>
<tbody>
@for (holder of recurringDepositAccountData.jointHolders; track holder.clientId; let i = $index) {
<tr [class.preview-soa-tr--primary]="holder.isPrimary">
<td class="preview-soa-td">{{ i + 1 }}</td>
<td class="preview-soa-td">
{{ holder.displayName || holder.clientName || '—' }}
@if (holder.isPrimary) {
<span class="preview-soa-primary-tag">Primary</span>
}
</td>
<td class="preview-soa-td preview-mono">{{ holder.cifNumber || holder.clientCif || '—' }}</td>
<td class="preview-soa-td">{{ holder.sharePercentage ?? '—' }}%</td>
<td class="preview-soa-td">{{ holder.isMandatory ? 'Mandatory' : 'Optional' }}</td>
<td class="preview-soa-td">{{ holder.isPrimary ? 'Primary Account Holder' : 'Co-Holder' }}</td>
</tr>
}
</tbody>
</table>
</div>
}
}
<!-- INDIVIDUAL_MALE: Special Category -->
@if (
recurringDepositAccountData.accountStatus === 'INDIVIDUAL_MALE' && recurringDepositAccountData.specialCategory
) {
<div class="preview-grid preview-grid--three" style="margin-top: 12px">
<div class="preview-field">
<span class="preview-label">Special Category</span>
<span class="preview-value">
@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 }}
}
}
</span>
</div>
</div>
}
<!-- INDIVIDUAL_FEMALE: Weaker Section -->
@if (
recurringDepositAccountData.accountStatus === 'INDIVIDUAL_FEMALE' && recurringDepositAccountData.weakerSection
) {
<div class="preview-grid preview-grid--three" style="margin-top: 12px">
<div class="preview-field">
<span class="preview-label">Weaker Section</span>
<span class="preview-value">
@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 }}
}
}
</span>
</div>
</div>
}
<!-- MINOR: Guardian Details -->
@if (recurringDepositAccountData.accountStatus === 'MINOR') {
<div class="preview-grid preview-grid--three" style="margin-top: 12px">
@if (recurringDepositAccountData.guardianName) {
<div class="preview-field">
<span class="preview-label">Guardian Name</span>
<span class="preview-value">{{ recurringDepositAccountData.guardianName }}</span>
</div>
}
@if (recurringDepositAccountData.guardianCifNumber) {
<div class="preview-field">
<span class="preview-label">Guardian CIF</span>
<span class="preview-value preview-mono">{{ recurringDepositAccountData.guardianCifNumber }}</span>
</div>
}
@if (recurringDepositAccountData.guardianRelationship) {
<div class="preview-field">
<span class="preview-label">Relationship</span>
<span class="preview-value">{{ recurringDepositAccountData.guardianRelationship }}</span>
</div>
}
@if (recurringDepositAccountData.minorDateOfBirth) {
<div class="preview-field">
<span class="preview-label">Minor DOB</span>
<span class="preview-value">{{ recurringDepositAccountData.minorDateOfBirth }}</span>
</div>
}
</div>
}
</div>
</div>
}
<!-- ══ 1c. Linked Account ════════════════════════════════════════ -->
@if (recurringDepositAccountData?.linkedAccountId) {
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-link preview-section__icon"></i>
<span class="preview-section__title">Linked Deposit Account</span>
<span class="preview-section__badge">{{ recurringDepositAccountData.linkedAccountType }}</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
<div class="preview-field">
<span class="preview-label">Account No</span>
<span class="preview-value preview-mono">{{ recurringDepositAccountData.linkedAccountNo }}</span>
</div>
<div class="preview-field">
<span class="preview-label">Account Type</span>
<span class="preview-value">{{ recurringDepositAccountData.linkedAccountType }}</span>
</div>
<div class="preview-field">
<span class="preview-label">Account ID</span>
<span class="preview-value preview-mono">{{ recurringDepositAccountData.linkedAccountId }}</span>
</div>
</div>
</div>
</div>
}
<!-- ══ 2. Product Classification ════════════════════════════════ --> <!-- ══ 2. Product Classification ════════════════════════════════ -->
@if ( @if (
recurringDepositsAccountProductTemplate?.accountType || recurringDepositsAccountProductTemplate?.accountType ||
@@ -16,18 +16,18 @@
// ── Section card ───────────────────────────────────────────────────────────── // ── Section card ─────────────────────────────────────────────────────────────
.preview-section { .preview-section {
border: 1px solid #e0e0e0; border: 1px solid #e2e8f0;
border-radius: 8px; border-radius: 10px;
overflow: hidden; overflow: hidden;
background: #fff; background: #fff;
box-shadow: 0 1px 3px rgb(0 0 0 / 6%); box-shadow: 0 1px 4px rgb(0 0 0 / 5%);
&__header { &__header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
background: #f5f5f5; background: #f8fafc;
border-bottom: 1px solid #e0e0e0; border-bottom: 1px solid #e2e8f0;
padding: 0.6rem 1rem; padding: 0.6rem 1rem;
} }
@@ -40,11 +40,11 @@
&__title { &__title {
flex: 1; flex: 1;
font-size: 0.95rem; font-size: 0.82rem;
font-weight: 600; font-weight: 700;
color: #333; color: #1e293b;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.04em; letter-spacing: 0.06em;
} }
&__badge { &__badge {
@@ -93,7 +93,7 @@
// ── Grid ───────────────────────────────────────────────────────────────────── // ── Grid ─────────────────────────────────────────────────────────────────────
.preview-grid { .preview-grid {
display: grid; display: grid;
gap: 0.75rem 1.5rem; gap: 0.85rem 1.5rem;
&--three { &--three {
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
@@ -116,33 +116,38 @@
} }
.preview-label { .preview-label {
font-size: 0.72rem; font-size: 0.68rem;
color: #888; color: #94a3b8;
font-weight: 500; font-weight: 600;
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.04em; letter-spacing: 0.05em;
} }
.preview-value { .preview-value {
font-size: 0.92rem; font-size: 0.9rem;
color: #222; color: #1e293b;
font-weight: 500; font-weight: 500;
} }
// ── Yes/No badge ───────────────────────────────────────────────────────────── // ── Yes/No badge ─────────────────────────────────────────────────────────────
.preview-badge { .preview-badge {
display: inline-block; display: inline-block;
padding: 0.1rem 0.55rem; padding: 0.15rem 0.6rem;
border-radius: 12px; border-radius: 12px;
font-size: 0.78rem; font-size: 0.75rem;
font-weight: 600; font-weight: 600;
background: #ffebee; background: #fef2f2;
color: #c62828; color: #dc2626;
width: fit-content; width: fit-content;
&--yes { &--yes {
background: #e8f5e9; background: #f0fdf4;
color: #2e7d32; 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) ──────────────────── // ── Product feature flag rows (same style as details step) ────────────────────
.preview-feature-list { .preview-feature-list {
margin-top: 1rem; margin-top: 1rem;
@@ -67,12 +67,60 @@
<mat-tab> <mat-tab>
<ng-template mat-tab-label> <ng-template mat-tab-label>
<i class="fas fa-chart-area fdp-tab-icon"></i> <i class="fas fa-chart-area fdp-tab-icon"></i>
{{ 'labels.inputs.INTEREST RATE CHART' | translate }} CBS Base Chart
</ng-template> </ng-template>
<div class="fdp-tab-content"> <div class="fdp-tab-content">
<mifosx-fixed-deposit-product-interest-rate-chart-step @if (activeBaseVersion) {
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate" <div class="base-rate-card">
></mifosx-fixed-deposit-product-interest-rate-chart-step> <div class="base-rate-header">
<div>
<h3 class="base-rate-title">{{ activeBaseVersion.versionLabel }}</h3>
<p class="base-rate-meta">
Active CBS base chart
<span>Effective from {{ activeBaseVersion.effectiveFromDate }}</span>
</p>
</div>
<span class="base-rate-badge">Active</span>
</div>
@if (activeBaseVersion.remarks) {
<p class="base-rate-remarks">{{ activeBaseVersion.remarks }}</p>
}
<table class="base-rate-table" mat-table [dataSource]="activeBaseVersion.slabs">
<ng-container matColumnDef="srNo">
<th mat-header-cell *matHeaderCellDef>Sr.</th>
<td mat-cell *matCellDef="let slab">{{ slab.srNo }}</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>Particular</th>
<td mat-cell *matCellDef="let slab">{{ slab.description }}</td>
</ng-container>
<ng-container matColumnDef="fromPeriod">
<th mat-header-cell *matHeaderCellDef>From</th>
<td mat-cell *matCellDef="let slab">{{ slab.fromPeriod }}</td>
</ng-container>
<ng-container matColumnDef="toPeriod">
<th mat-header-cell *matHeaderCellDef>To</th>
<td mat-cell *matCellDef="let slab">{{ slab.toPeriod ?? 'N/A' }}</td>
</ng-container>
<ng-container matColumnDef="periodTypeLabel">
<th mat-header-cell *matHeaderCellDef>Period Type</th>
<td mat-cell *matCellDef="let slab">{{ slab.periodTypeLabel }}</td>
</ng-container>
<ng-container matColumnDef="annualInterestRate">
<th mat-header-cell *matHeaderCellDef>Annual Rate (%)</th>
<td mat-cell *matCellDef="let slab">
<strong>{{ slab.annualInterestRate | number: '1.2-2' }}</strong>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="fdSlabColumns"></tr>
<tr mat-row *matRowDef="let row; columns: fdSlabColumns"></tr>
</table>
</div>
} @else {
<p>Loading CBS base chart...</p>
}
</div> </div>
</mat-tab> </mat-tab>
@@ -112,7 +160,7 @@
<div class="fdp-tab-content"> <div class="fdp-tab-content">
<mifosx-fixed-deposit-product-preview-step <mifosx-fixed-deposit-product-preview-step
[fixedDepositProductsTemplate]="fixedDepositProductsTemplate" [fixedDepositProductsTemplate]="fixedDepositProductsTemplate"
[chartSlabsDisplayedColumns]="fixedDepositProductInterestRateChartStep.chartSlabsDisplayedColumns" [chartSlabsDisplayedColumns]="fdSlabColumns"
[accountingRuleData]="accountingRuleData" [accountingRuleData]="accountingRuleData"
[fixedDepositProduct]="fixedDepositProduct" [fixedDepositProduct]="fixedDepositProduct"
(submitEvent)="submit()" (submitEvent)="submit()"
@@ -71,6 +71,54 @@ $primary: #1353a0;
padding: 24px 16px; padding: 24px 16px;
} }
.base-rate-card {
padding: 16px;
border: 1px solid #dbe4f0;
border-radius: 14px;
background: linear-gradient(180deg, #f8fbff 0%, #fff 100%);
}
.base-rate-header {
display: flex;
align-items: start;
justify-content: space-between;
gap: 12px;
margin-bottom: 12px;
}
.base-rate-title {
margin: 0;
font-size: 18px;
font-weight: 600;
}
.base-rate-meta {
margin: 6px 0 0;
color: #5b6b7f;
display: flex;
gap: 12px;
flex-wrap: wrap;
}
.base-rate-badge {
padding: 4px 10px;
border-radius: 999px;
background: #dcfce7;
color: #166534;
font-size: 12px;
font-weight: 700;
}
.base-rate-remarks {
margin: 0 0 12px;
color: #334155;
}
.base-rate-table {
width: 100%;
border-collapse: collapse;
}
.fdp-action-bar { .fdp-action-bar {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
@@ -15,9 +15,20 @@ import { FixedDepositProductDetailsStepComponent } from '../fixed-deposit-produc
import { FixedDepositProductCurrencyStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-currency-step/fixed-deposit-product-currency-step.component'; import { FixedDepositProductCurrencyStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-currency-step/fixed-deposit-product-currency-step.component';
import { FixedDepositProductTermsStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component'; import { FixedDepositProductTermsStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component';
import { FixedDepositProductSettingsStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component'; import { FixedDepositProductSettingsStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component';
import { FixedDepositProductInterestRateChartStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-interest-rate-chart-step/fixed-deposit-product-interest-rate-chart-step.component';
import { FixedDepositProductChargesStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-charges-step/fixed-deposit-product-charges-step.component'; import { FixedDepositProductChargesStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-charges-step/fixed-deposit-product-charges-step.component';
import { FixedDepositProductAccountingStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component'; import { FixedDepositProductAccountingStepComponent } from '../fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component';
import {
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
/** Custom Services */ /** Custom Services */
import { ProductsService } from 'app/products/products.service'; import { ProductsService } from 'app/products/products.service';
@@ -40,10 +51,19 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
FixedDepositProductCurrencyStepComponent, FixedDepositProductCurrencyStepComponent,
FixedDepositProductTermsStepComponent, FixedDepositProductTermsStepComponent,
FixedDepositProductSettingsStepComponent, FixedDepositProductSettingsStepComponent,
FixedDepositProductInterestRateChartStepComponent,
FixedDepositProductChargesStepComponent, FixedDepositProductChargesStepComponent,
FixedDepositProductAccountingStepComponent, FixedDepositProductAccountingStepComponent,
FixedDepositProductPreviewStepComponent FixedDepositProductPreviewStepComponent,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
] ]
}) })
export class CreateFixedDepositProductComponent { export class CreateFixedDepositProductComponent {
@@ -61,8 +81,6 @@ export class CreateFixedDepositProductComponent {
fixedDepositProductTermsStep: FixedDepositProductTermsStepComponent; fixedDepositProductTermsStep: FixedDepositProductTermsStepComponent;
@ViewChild(FixedDepositProductSettingsStepComponent, { static: true }) @ViewChild(FixedDepositProductSettingsStepComponent, { static: true })
fixedDepositProductSettingsStep: FixedDepositProductSettingsStepComponent; fixedDepositProductSettingsStep: FixedDepositProductSettingsStepComponent;
@ViewChild(FixedDepositProductInterestRateChartStepComponent, { static: true })
fixedDepositProductInterestRateChartStep: FixedDepositProductInterestRateChartStepComponent;
@ViewChild(FixedDepositProductChargesStepComponent, { static: true }) @ViewChild(FixedDepositProductChargesStepComponent, { static: true })
fixedDepositProductChargesStep: FixedDepositProductChargesStepComponent; fixedDepositProductChargesStep: FixedDepositProductChargesStepComponent;
@ViewChild(FixedDepositProductAccountingStepComponent, { static: true }) @ViewChild(FixedDepositProductAccountingStepComponent, { static: true })
@@ -71,19 +89,29 @@ export class CreateFixedDepositProductComponent {
fixedDepositProductsTemplate: any; fixedDepositProductsTemplate: any;
accountingRuleData: string[] = []; accountingRuleData: string[] = [];
selectedTabIndex = 0; selectedTabIndex = 0;
fdInterestRates: any[] = [];
/** readonly fdSlabColumns = [
* @param {ActivatedRoute} route Activated Route. 'srNo',
* @param {ProductsService} productsService Products Service. 'description',
* @param {Router} router Router for navigation. 'fromPeriod',
* @param {SettingsService} settingsService Settings Service. 'toPeriod',
*/ 'periodTypeLabel',
'annualInterestRate'
];
constructor() { constructor() {
this.route.data.subscribe((data: { fixedDepositProductsTemplate: any }) => { this.route.data.subscribe((data: { fixedDepositProductsTemplate: any }) => {
this.fixedDepositProductsTemplate = data.fixedDepositProductsTemplate; this.fixedDepositProductsTemplate = data.fixedDepositProductsTemplate;
}); });
this.accountingRuleData = this.accounting.getAccountingRulesForSavings(); this.accountingRuleData = this.accounting.getAccountingRulesForSavings();
this.productsService.getRdBaseInterestRates().subscribe({
next: (rates: any[]) => {
this.fdInterestRates = rates || [];
},
error: () => {
this.fdInterestRates = [];
}
});
} }
get fixedDepositProductDetailsForm() { get fixedDepositProductDetailsForm() {
@@ -102,10 +130,6 @@ export class CreateFixedDepositProductComponent {
return this.fixedDepositProductSettingsStep.fixedDepositProductSettingsForm; return this.fixedDepositProductSettingsStep.fixedDepositProductSettingsForm;
} }
get fixedDepositProductInterestRateChartForm() {
return this.fixedDepositProductInterestRateChartStep.fixedDepositProductInterestRateChartForm;
}
get fixedDepositProductAccountingForm() { get fixedDepositProductAccountingForm() {
return this.fixedDepositProductAccountingStep.fixedDepositProductAccountingForm; return this.fixedDepositProductAccountingStep.fixedDepositProductAccountingForm;
} }
@@ -120,29 +144,35 @@ export class CreateFixedDepositProductComponent {
this.fixedDepositProductCurrencyForm.valid && this.fixedDepositProductCurrencyForm.valid &&
this.fixedDepositProductTermsForm.valid && this.fixedDepositProductTermsForm.valid &&
this.fixedDepositProductSettingsForm.valid && this.fixedDepositProductSettingsForm.valid &&
this.fixedDepositProductInterestRateChartForm.valid &&
this.fixedDepositProductAccountingForm.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() { get fixedDepositProduct() {
return { return {
...this.fixedDepositProductDetailsStep.fixedDepositProductDetails, ...this.fixedDepositProductDetailsStep.fixedDepositProductDetails,
...this.fixedDepositProductCurrencyStep.fixedDepositProductCurrency, ...this.fixedDepositProductCurrencyStep.fixedDepositProductCurrency,
...this.fixedDepositProductTermsStep.fixedDepositProductTerms, ...this.fixedDepositProductTermsStep.fixedDepositProductTerms,
...this.fixedDepositProductSettingsStep.fixedDepositProductSettings, ...this.fixedDepositProductSettingsStep.fixedDepositProductSettings,
...this.fixedDepositProductInterestRateChartStep.fixedDepositProductInterestRateChart,
...this.fixedDepositProductChargesStep.fixedDepositProductCharges, ...this.fixedDepositProductChargesStep.fixedDepositProductCharges,
...this.fixedDepositProductAccountingStep.fixedDepositProductAccounting ...this.fixedDepositProductAccountingStep.fixedDepositProductAccounting,
fdSlabs: this.activeBaseSlabs
}; };
} }
submit() { submit() {
// TODO: Update once language and date settings are setup
const fixedDepositProduct = { const fixedDepositProduct = {
...this.fixedDepositProduct, ...this.fixedDepositProduct,
charges: this.fixedDepositProduct.charges.map((charge: any) => ({ id: charge.id })), 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; delete fixedDepositProduct.advancedAccountingRules;
this.productsService.createFixedDepositProduct(fixedDepositProduct).subscribe((response: any) => { this.productsService.createFixedDepositProduct(fixedDepositProduct).subscribe((response: any) => {
@@ -9,310 +9,111 @@
<form [formGroup]="fixedDepositProductAccountingForm"> <form [formGroup]="fixedDepositProductAccountingForm">
<div class="fdpa-section"> <div class="fdpa-section">
<div class="fdpa-section__header"> <div class="fdpa-section__header">
<i class="fas fa-book"></i> <span class="fdpa-section__icon">
<h3>{{ 'labels.heading.Accounting' | translate }}</h3> <i class="fas fa-book"></i>
</div> </span>
<div class="fdpa-grid"> <h3>Accounting</h3>
<div class="fdpa-field fdpa-field--full">
<mat-radio-group class="fdpa-radio-group" formControlName="accountingRule">
@for (accountingRule of accountingRuleData; track accountingRule; let i = $index) {
<mat-radio-button [value]="i + 1">
{{ 'labels.accounting.' + accountingRule | translate }}
</mat-radio-button>
}
</mat-radio-group>
</div>
</div>
</div>
@if (isCashOrAccrualAccounting()) {
<div class="fdpa-section">
<div class="fdpa-section__header">
<i class="fas fa-landmark"></i>
<h3>{{ 'labels.heading.Assets' | translate }}</h3>
</div>
<div class="fdpa-grid">
<mifosx-gl-account-selector
class="fdpa-field"
[inputFormControl]="fixedDepositProductAccountingForm.controls.savingsReferenceAccountId"
[glAccountList]="assetAccountData"
[required]="true"
[inputLabel]="'Saving reference'"
>
</mifosx-gl-account-selector>
@if (isAccrualAccounting()) {
<mifosx-gl-account-selector
class="fdpa-field"
[inputFormControl]="fixedDepositProductAccountingForm.controls.feesReceivableAccountId"
[glAccountList]="assetAccountData"
[required]="true"
[inputLabel]="'Fees Receivable'"
>
</mifosx-gl-account-selector>
}
@if (isAccrualAccounting()) {
<mifosx-gl-account-selector
class="fdpa-field"
[inputFormControl]="fixedDepositProductAccountingForm.controls.penaltiesReceivableAccountId"
[glAccountList]="assetAccountData"
[required]="true"
[inputLabel]="'Penalties Receivable'"
>
</mifosx-gl-account-selector>
}
</div>
</div> </div>
<div class="fdpa-section"> <div class="fdpa-section__body">
<div class="fdpa-section__header"> <div class="fdpa-group">
<i class="fas fa-balance-scale"></i> <div class="fdpa-group__header">
<h3>{{ 'labels.heading.Liabilities' | translate }}</h3> <h4>Accounting Rule</h4>
</div> </div>
<div class="fdpa-grid"> <div class="fdpa-radio-group">
<mifosx-gl-account-selector <mat-radio-button [value]="2" [checked]="true" [disabled]="true">Cash</mat-radio-button>
class="fdpa-field"
[inputFormControl]="fixedDepositProductAccountingForm.controls.savingsControlAccountId"
[glAccountList]="liabilityAccountData"
[required]="true"
[inputLabel]="'Saving control'"
>
</mifosx-gl-account-selector>
<mifosx-gl-account-selector
class="fdpa-field"
[inputFormControl]="fixedDepositProductAccountingForm.controls.transfersInSuspenseAccountId"
[glAccountList]="liabilityAccountData"
[required]="true"
[inputLabel]="'Savings transfers in suspense'"
>
</mifosx-gl-account-selector>
@if (isAccrualAccounting()) {
<mifosx-gl-account-selector
class="fdpa-field"
[inputFormControl]="fixedDepositProductAccountingForm.controls.interestPayableAccountId"
[glAccountList]="liabilityAccountData"
[required]="true"
[inputLabel]="'Interest Payable'"
>
</mifosx-gl-account-selector>
}
</div>
</div>
<div class="fdpa-section">
<div class="fdpa-section__header">
<i class="fas fa-arrow-circle-down"></i>
<h3>{{ 'labels.heading.Expenses' | translate }}</h3>
</div>
<div class="fdpa-grid">
<mifosx-gl-account-selector
class="fdpa-field"
[inputFormControl]="fixedDepositProductAccountingForm.controls.interestOnSavingsAccountId"
[glAccountList]="expenseAccountData"
[required]="true"
[inputLabel]="'Interest on savings'"
>
</mifosx-gl-account-selector>
</div>
</div>
<div class="fdpa-section">
<div class="fdpa-section__header">
<i class="fas fa-arrow-circle-up"></i>
<h3>{{ 'labels.heading.Income' | translate }}</h3>
</div>
<div class="fdpa-grid">
<mifosx-gl-account-selector
class="fdpa-field"
[inputFormControl]="fixedDepositProductAccountingForm.controls.incomeFromFeeAccountId"
[glAccountList]="incomeAccountData"
[required]="true"
[inputLabel]="'Income from fees'"
>
</mifosx-gl-account-selector>
<mifosx-gl-account-selector
class="fdpa-field"
[inputFormControl]="fixedDepositProductAccountingForm.controls.incomeFromPenaltyAccountId"
[glAccountList]="incomeAccountData"
[required]="true"
[inputLabel]="'Income from penalties'"
>
</mifosx-gl-account-selector>
</div>
</div>
<div class="fdpa-section">
<div class="fdpa-section__header">
<i class="fas fa-cogs"></i>
<h3>{{ 'labels.heading.Advanced Accounting Rules' | translate }}</h3>
</div>
<div class="fdpa-grid">
<div class="fdpa-field fdpa-field--full fdpa-checkbox-row">
<mat-checkbox formControlName="advancedAccountingRules"></mat-checkbox>
</div> </div>
</div> </div>
@if (fixedDepositProductAccountingForm.value.advancedAccountingRules) { @if (isCashOrAccrualAccounting()) {
<!-- Payment Fund Source --> @for (section of accountingSectionConfigs; track section.key) {
<div class="fdpa-subsection"> <div class="fdpa-group">
<div class="fdpa-subsection__header"> <div class="fdpa-group__header">
<h4>{{ 'labels.heading.Configure Fund Sources for Payment Channels' | translate }}</h4> <h4>{{ section.title }}</h4>
<button <button type="button" class="fdpa-add-btn" (click)="addOtherGlMapping(section.otherMappingsControlName)">
type="button" + Add Other
mat-raised-button
color="primary"
class="fdpa-add-btn"
(click)="add('PaymentFundSource', paymentChannelToFundSourceMappings)"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@if (paymentChannelToFundSourceMappings.value.length !== 0) {
<div class="fdpa-table-wrapper">
<table mat-table [dataSource]="paymentChannelToFundSourceMappings.value">
<ng-container matColumnDef="paymentTypeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.heading.Payment Type' | translate }}</th>
<td mat-cell *matCellDef="let paymentFundSource">
{{ paymentFundSource.paymentTypeId | find: paymentTypeData : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="fundSourceAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Fund Source' | translate }}</th>
<td mat-cell *matCellDef="let paymentFundSource">
{{ paymentFundSource.fundSourceAccountId | find: assetAccountData : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.buttons.Actions' | translate }}</th>
<td mat-cell *matCellDef="let paymentFundSource; let i = index">
<button
mat-icon-button
color="primary"
(click)="edit('PaymentFundSource', paymentChannelToFundSourceMappings, i)"
>
<fa-icon icon="edit"></fa-icon>
</button>
<button mat-icon-button color="warn" (click)="delete(paymentChannelToFundSourceMappings, i)">
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="paymentFundSourceDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: paymentFundSourceDisplayedColumns"></tr>
</table>
</div>
}
</div>
@if (existCharges()) {
<!-- Fees Income -->
<div class="fdpa-subsection">
<div class="fdpa-subsection__header">
<h4>{{ 'labels.heading.Map Fees to Specific Income Accounts' | translate }}</h4>
<button
type="button"
mat-raised-button
color="primary"
class="fdpa-add-btn"
(click)="add('FeesIncome', feeToIncomeAccountMappings)"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button> </button>
</div> </div>
@if (feeToIncomeAccountMappings.value.length !== 0) { <div class="fdpa-list">
<div class="fdpa-table-wrapper"> @for (row of section.rows; track row.key) {
<table mat-table [dataSource]="feeToIncomeAccountMappings.value"> @if (!row.accrualOnly || isAccrualAccounting()) {
<ng-container matColumnDef="chargeId"> <div class="fdpa-row">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.catalogs.Fees' | translate }}</th> <div class="fdpa-row__meta">
<td mat-cell *matCellDef="let feesIncome"> <span
{{ feesIncome.chargeId | find: chargeData : 'id' : 'name' }} class="fdpa-pill"
</td> [class.fdpa-pill--green]="section.pillClass === 'green'"
</ng-container> [class.fdpa-pill--amber]="section.pillClass === 'amber'"
<ng-container matColumnDef="incomeAccountId"> [class.fdpa-pill--blue]="section.pillClass === 'blue'"
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Income Account' | translate }}</th>
<td mat-cell *matCellDef="let feesIncome">
{{ feesIncome.incomeAccountId | find: incomeAccountData : 'id' : 'name' }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.buttons.Actions' | translate }}</th>
<td mat-cell *matCellDef="let feesIncome; let i = index">
<button
mat-icon-button
color="primary"
(click)="edit('FeesIncome', feeToIncomeAccountMappings, i)"
> >
<fa-icon icon="edit"></fa-icon> {{ section.pillLabel }}
</button> </span>
<button mat-icon-button color="warn" (click)="delete(feeToIncomeAccountMappings, i)"> <span class="fdpa-row__label">{{ row.label }}</span>
<fa-icon icon="trash"></fa-icon> </div>
</button> <mifosx-gl-account-selector
</td> class="fdpa-row__input"
</ng-container> [inputFormControl]="fixedDepositProductAccountingForm.get(row.controlName)"
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr> [glAccountList]="row.options"
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr> [required]="row.required"
</table> [inputLabel]="row.inputLabel"
</div> ></mifosx-gl-account-selector>
} </div>
</div> }
}
<!-- Penalty Income -->
<div class="fdpa-subsection">
<div class="fdpa-subsection__header">
<h4>
{{ 'labels.catalogs.Penalties' | translate }}
{{ 'labels.heading.Map Penalties to Specific Income Accounts' | translate }}
</h4>
<button
type="button"
mat-raised-button
color="primary"
class="fdpa-add-btn"
(click)="add('PenaltyIncome', penaltyToIncomeAccountMappings)"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add' | translate }}
</button>
</div> </div>
@if (penaltyToIncomeAccountMappings.value.length !== 0) {
<div class="fdpa-table-wrapper"> <div class="fdpa-list" [formArrayName]="section.otherMappingsControlName">
<table mat-table [dataSource]="penaltyToIncomeAccountMappings.value"> @for (
<ng-container matColumnDef="chargeId"> otherRow of getOtherGlMappingsArray(section.otherMappingsControlName).controls;
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Penalty' | translate }}</th> track $index;
<td mat-cell *matCellDef="let penaltyIncome"> let i = $index
{{ penaltyIncome.chargeId | find: penaltyData : 'id' : 'name' }} ) {
</td> <div class="fdpa-other-row-wrap">
</ng-container> <div class="fdpa-row fdpa-row--other" [formGroupName]="i">
<ng-container matColumnDef="incomeAccountId"> <div class="fdpa-row__meta fdpa-row__meta--other">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Income Account' | translate }}</th> <span
<td mat-cell *matCellDef="let penaltyIncome"> class="fdpa-pill"
{{ penaltyIncome.incomeAccountId | find: incomeAccountData : 'id' : 'name' }} [class.fdpa-pill--green]="section.pillClass === 'green'"
</td> [class.fdpa-pill--amber]="section.pillClass === 'amber'"
</ng-container> [class.fdpa-pill--blue]="section.pillClass === 'blue'"
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.buttons.Actions' | translate }}</th>
<td mat-cell *matCellDef="let penaltyIncome; let i = index">
<button
mat-icon-button
color="primary"
(click)="edit('PenaltyIncome', penaltyToIncomeAccountMappings, i)"
> >
<fa-icon icon="edit"></fa-icon> {{ section.pillLabel }}
</button> </span>
<button mat-icon-button color="warn" (click)="delete(penaltyToIncomeAccountMappings, i)"> <input
<fa-icon icon="trash"></fa-icon> class="fdpa-row__name fdpa-row__name--other-account"
</button> formControlName="accountName"
</td> placeholder="Enter account name..."
</ng-container> type="text"
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr> />
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr> </div>
</table>
</div> <div class="fdpa-row__actions fdpa-row__actions--other">
} <mat-form-field class="fdpa-row__other-field" appearance="outline" subscriptSizing="dynamic">
<mat-label>GL Code</mat-label>
<mat-select
[formControl]="getOtherGlMappingControl(section.otherMappingsControlName, i, 'glAccountId')"
>
@for (glAccount of section.otherAccountOptions; track glAccount.id) {
<mat-option [value]="glAccount.id">
{{ '(' + glAccount.glCode + ') ' + glAccount.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</div>
<button
type="button"
class="fdpa-remove-btn fdpa-remove-btn--side"
(click)="removeOtherGlMapping(section.otherMappingsControlName, i)"
>
<i class="fas fa-times"></i>
</button>
</div>
}
</div>
</div> </div>
} }
} }
</div> </div>
} </div>
</form> </form>
@@ -6,8 +6,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * 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 { .fdpa-section {
border: 1px solid #e2e8f0; border: 1px solid $border;
border-radius: 10px; border-radius: 10px;
overflow: hidden; overflow: hidden;
background: #fff; background: #fff;
@@ -15,45 +21,244 @@
& + & { & + & {
margin-top: 16px; 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 { &__header {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 10px; justify-content: space-between;
padding: 12px 24px; gap: 16px;
background: #f8f9fa; margin: 0 0 12px;
border-bottom: 1px solid #e8eaed; }
i { h4 {
font-size: 14px; margin: 0 0 12px;
color: #0ea5e9; font-size: 12px;
} font-weight: 700;
letter-spacing: 0.08em;
h3 { text-transform: uppercase;
margin: 0; color: #94a3b8;
font-size: 14px;
font-weight: 600;
color: #202124;
}
} }
} }
.fdpa-grid { .fdpa-add-btn {
display: grid; border: 0;
grid-template-columns: repeat(2, 1fr); background: transparent;
gap: 4px 16px; color: #2563eb;
padding: 16px 24px; 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; 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%; width: 100%;
&--full { @media (width <= 1100px) {
grid-column: 1 / -1; 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; display: flex;
gap: 24px; gap: 24px;
flex-wrap: wrap; flex-wrap: wrap;
@media (width <= 600px) {
flex-direction: column;
}
} }
.fdpa-checkbox-row { .fdpa-pill {
display: flex; display: inline-flex;
align-items: center; 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 { &--green {
height: 36px; background: #dcfce7;
border-radius: 8px; color: #16a34a;
font-size: 13px; }
font-weight: 600;
&--amber {
background: #fef3c7;
color: #d97706;
}
&--blue {
background: #dbeafe;
color: #2563eb;
}
} }
.fdpa-subsection { .fdpa-subsection {
@@ -95,13 +311,13 @@
margin: 0; margin: 0;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: #202124; color: $gray-900;
} }
} }
} }
.fdpa-table-wrapper { .fdpa-table-wrapper {
border: 1px solid #e2e8f0; border: 1px solid $border;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
@@ -109,3 +325,14 @@
width: 100%; width: 100%;
} }
} }
@media (width <= 900px) {
.fdpa-row__meta {
margin-bottom: 4px;
}
.fdpa-row__input {
flex: 1 1 auto;
max-width: 100%;
}
}
@@ -8,434 +8,391 @@
import { Component, Input, OnInit, inject } from '@angular/core'; import { Component, Input, OnInit, inject } from '@angular/core';
import { import {
FormsModule,
ReactiveFormsModule,
UntypedFormArray, UntypedFormArray,
UntypedFormBuilder, UntypedFormBuilder,
UntypedFormControl, UntypedFormControl,
UntypedFormGroup, UntypedFormGroup,
Validators, Validators
ReactiveFormsModule
} from '@angular/forms'; } from '@angular/forms';
import { MatDialog } from '@angular/material/dialog'; import { MatRadioButton, MatRadioGroup } from '@angular/material/radio';
import { GlAccountSelectorComponent } from 'app/shared/accounting/gl-account-selector/gl-account-selector.component';
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 { Accounting } from 'app/core/utils/accounting'; import { Accounting } from 'app/core/utils/accounting';
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base'; import { GLAccount } from 'app/shared/models/general.model';
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 { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; 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({ @Component({
selector: 'mifosx-fixed-deposit-product-accounting-step', selector: 'mifosx-fixed-deposit-product-accounting-step',
templateUrl: './fixed-deposit-product-accounting-step.component.html', templateUrl: './fixed-deposit-product-accounting-step.component.html',
styleUrls: ['./fixed-deposit-product-accounting-step.component.scss'], styleUrls: ['./fixed-deposit-product-accounting-step.component.scss'],
imports: [ imports: [
...STANDALONE_SHARED_IMPORTS, ...STANDALONE_SHARED_IMPORTS,
FormsModule,
ReactiveFormsModule,
MatRadioGroup, MatRadioGroup,
MatRadioButton, MatRadioButton,
GlAccountSelectorComponent, GlAccountSelectorComponent
MatCheckbox,
FaIconComponent,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatIconButton,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
FindPipe
] ]
}) })
export class FixedDepositProductAccountingStepComponent implements OnInit { export class FixedDepositProductAccountingStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder); private formBuilder = inject(UntypedFormBuilder);
dialog = inject(MatDialog);
private accounting = inject(Accounting); private accounting = inject(Accounting);
private translateService = inject(TranslateService);
@Input() fixedDepositProductsTemplate: any; @Input() fixedDepositProductsTemplate: any;
@Input() accountingRuleData: any; @Input() accountingRuleData: any;
@Input() fixedDepositProductFormValid: boolean; @Input() fixedDepositProductFormValid: boolean;
fixedDepositProductAccountingForm: UntypedFormGroup; fixedDepositProductAccountingForm: UntypedFormGroup;
assetAccountOptions: GLAccount[] = [];
chargeData: any; liabilityAccountOptions: GLAccount[] = [];
penaltyData: any; expenseAccountOptions: GLAccount[] = [];
paymentTypeData: any; incomeAccountOptions: GLAccount[] = [];
assetAccountData: any; accountingSectionConfigs: AccountingSectionConfig[] = [];
incomeAccountData: any;
expenseAccountData: any;
liabilityAccountData: any;
paymentFundSourceDisplayedColumns: string[] = [
'paymentTypeId',
'fundSourceAccountId',
'actions'
];
feesPenaltyIncomeDisplayedColumns: string[] = [
'chargeId',
'incomeAccountId',
'actions'
];
constructor() { constructor() {
this.createfixedDepositProductAccountingForm(); this.createFixedDepositProductAccountingForm();
this.setConditionalControls(); this.setConditionalControls();
} }
ngOnInit() { ngOnInit() {
this.chargeData = this.fixedDepositProductsTemplate.chargeOptions || []; const accountingMappings = this.fixedDepositProductsTemplate?.accountingMappings || {};
this.penaltyData = this.fixedDepositProductsTemplate.penaltyOptions || []; const mappings = {
this.paymentTypeData = this.fixedDepositProductsTemplate.paymentTypeOptions || []; ...accountingMappings,
this.assetAccountData = this.fixedDepositProductsTemplate.accountingMappingOptions.assetAccountOptions || []; otherGlMappingsMap: this.fixedDepositProductsTemplate?.otherGlMappingsMap
this.incomeAccountData = this.fixedDepositProductsTemplate.accountingMappingOptions.incomeAccountOptions || []; };
this.expenseAccountData = this.fixedDepositProductsTemplate.accountingMappingOptions.expenseAccountOptions || []; const accountingMappingOptions = this.fixedDepositProductsTemplate?.accountingMappingOptions || {};
this.liabilityAccountData = const accountingRuleId = 2; // Always Cash
this.fixedDepositProductsTemplate.accountingMappingOptions.liabilityAccountOptions || [];
if (!(this.fixedDepositProductsTemplate === undefined) && this.fixedDepositProductsTemplate.id) { this.assetAccountOptions = accountingMappingOptions.assetAccountOptions || [];
this.assignAccountingStepData(); 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() { createFixedDepositProductAccountingForm() {
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() {
this.fixedDepositProductAccountingForm = this.formBuilder.group({ 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 { private setConditionalControls() {
return this.chargeData.length > 0; this.fixedDepositProductAccountingForm.get('accountingRule')?.valueChanges.subscribe((rule: number) => {
} this.syncAccountingControls(rule);
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');
}
}); });
} }
get paymentChannelToFundSourceMappings(): UntypedFormArray { private patchAccountingValues(mappings: any) {
return this.fixedDepositProductAccountingForm.get('paymentChannelToFundSourceMappings') as UntypedFormArray; this.fixedDepositProductAccountingForm.patchValue({
} savingsReferenceAccountId: mappings.savingsReferenceAccount?.id || '',
savingsControlAccountId: mappings.savingsControlAccount?.id || '',
get feeToIncomeAccountMappings(): UntypedFormArray { transfersInSuspenseAccountId: mappings.transfersInSuspenseAccount?.id || '',
return this.fixedDepositProductAccountingForm.get('feeToIncomeAccountMappings') as UntypedFormArray; interestOnSavingsAccountId: mappings.interestOnSavingsAccount?.id || '',
} incomeFromFeeAccountId: mappings.incomeFromFeeAccount?.id || '',
incomeFromPenaltyAccountId: mappings.incomeFromPenaltyAccount?.id || '',
get penaltyToIncomeAccountMappings(): UntypedFormArray { feesReceivableAccountId: mappings.feeReceivableAccount?.id || '',
return this.fixedDepositProductAccountingForm.get('penaltyToIncomeAccountMappings') as UntypedFormArray; penaltiesReceivableAccountId: mappings.penaltyReceivableAccount?.id || '',
} interestPayableAccountId: mappings.interestPayableAccount?.id || ''
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);
}
}); });
}
edit(formType: string, formArray: UntypedFormArray, index: number) { if (this.isCashOrAccrualAccounting()) {
const data = { ...this.getData(formType, formArray.at(index).value), layout: { addButtonText: 'Edit' } }; const glMappings = mappings.otherGlMappingsMap || mappings.otherGlMappings;
const dialogRef = this.dialog.open(FormDialogComponent, { data }); this.setOtherGlMappings('assetsOtherGlMappings', glMappings?.assets || [], this.assetAccountOptions);
dialogRef.afterClosed().subscribe((response: any) => { this.setOtherGlMappings(
if (response.data) { 'liabilitiesOtherGlMappings',
formArray.at(index).patchValue(response.data.value); glMappings?.liabilities || [],
} this.liabilityAccountOptions
}); );
} this.setOtherGlMappings('expensesOtherGlMappings', glMappings?.expenses || [], this.expenseAccountOptions);
this.setOtherGlMappings('incomeOtherGlMappings', glMappings?.income || [], this.incomeAccountOptions);
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)
};
} }
} }
getPaymentFundSourceFormfields(values?: any) { private createOtherGlMappingGroup(otherGlMapping: Partial<OtherGlMappingValue> = {}) {
const formfields: FormfieldBase[] = [ return this.formBuilder.group({
new SelectBase({ accountName: [
controlName: 'paymentTypeId', otherGlMapping.accountName || '',
label: 'Payment Type', Validators.required
value: values ? values.paymentTypeId : this.paymentTypeData[0].id, ],
options: { label: 'name', value: 'id', data: this.paymentTypeData }, glAccountId: [
required: true, otherGlMapping.glAccountId || '',
order: 1 Validators.required
}), ]
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;
} }
getFeesIncomeFormfields(values?: any) { getOtherGlMappingsArray(controlName: string): UntypedFormArray {
const formfields: FormfieldBase[] = [ return this.fixedDepositProductAccountingForm.get(controlName) as UntypedFormArray;
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;
} }
getPenaltyIncomeFormfields(values?: any) { private setOtherGlMappings(controlName: string, otherGlMappings: any[], options: GLAccount[]) {
const formfields: FormfieldBase[] = [ const arr = this.getOtherGlMappingsArray(controlName);
new SelectBase({ arr.clear();
controlName: 'chargeId', (otherGlMappings || []).forEach((m) => {
label: 'Penalty', arr.push(
value: values ? values.chargeId : this.penaltyData[0].id, this.createOtherGlMappingGroup({
options: { label: 'name', value: 'id', data: this.penaltyData }, accountName: m.accountName,
required: true, glAccountId: m.glAccountId || options.find((o) => o.glCode === m.glCode)?.id || null
order: 1 })
}), );
new SelectBase({ });
controlName: 'incomeAccountId', }
label: 'Income Account',
value: values ? values.incomeAccountId : this.incomeAccountData[0].id, addOtherGlMapping(controlName: string) {
options: { label: 'name', value: 'id', data: this.incomeAccountData }, this.getOtherGlMappingsArray(controlName).push(this.createOtherGlMappingGroup());
required: true, }
order: 2
}) 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() { 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 { isCashOrAccrualAccounting(): boolean {
@@ -8,22 +8,21 @@
<div class="fdpch-section"> <div class="fdpch-section">
<div class="fdpch-section__header"> <div class="fdpch-section__header">
<i class="fas fa-money-check-alt"></i> <i class="fa fa-file-invoice-dollar"></i>
<h3>{{ 'labels.inputs.Charge' | translate }}</h3> <h3>{{ 'labels.heading.Charges' | translate }}</h3>
</div> </div>
<div class="fdpch-grid"> <div class="fdpch-section__body">
<mat-form-field appearance="outline" class="fdpch-field"> <div class="fdpch-add-row">
<mat-label>{{ 'labels.inputs.Charge' | translate }}</mat-label> <mat-form-field appearance="outline" class="fdpch-add-row__select">
<mat-select #charge> <mat-label>{{ 'labels.inputs.Charge' | translate }}</mat-label>
@for (charge of chargeData | chargesFilter: chargesDataSource : currencyCode.value; track charge) { <mat-select #charge>
<mat-option [value]="charge"> @for (charge of chargeData | chargesFilter: chargesDataSource : currencyCode.value; track charge) {
{{ charge.name }} <mat-option [value]="charge">
</mat-option> {{ charge.name }}
} </mat-option>
</mat-select> }
</mat-form-field> </mat-select>
</mat-form-field>
<div class="fdpch-field fdpch-btn-wrap">
<button <button
type="button" type="button"
mat-raised-button mat-raised-button
@@ -36,51 +35,51 @@
{{ 'labels.buttons.Add' | translate }} {{ 'labels.buttons.Add' | translate }}
</button> </button>
</div> </div>
@if (chargesDataSource.length > 0) {
<div class="fdpch-table-wrapper">
<table mat-table [dataSource]="chargesDataSource">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.name + ', ' + charge.currency.displaySymbol }}
</td>
</ng-container>
<ng-container matColumnDef="chargeCalculationType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeCalculationType.value }}
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.amount | formatNumber }}
</td>
</ng-container>
<ng-container matColumnDef="chargeTimeType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Collected On' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeTimeType.value }}
</td>
</ng-container>
<ng-container matColumnDef="action">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let charge">
<button mat-icon-button color="warn" (click)="deleteCharge(charge)">
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</div>
}
</div> </div>
@if (chargesDataSource.length > 0) {
<div class="fdpch-table-wrapper">
<table mat-table [dataSource]="chargesDataSource">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.name + ', ' + charge.currency.displaySymbol }}
</td>
</ng-container>
<ng-container matColumnDef="chargeCalculationType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeCalculationType.value }}
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.amount | formatNumber }}
</td>
</ng-container>
<ng-container matColumnDef="chargeTimeType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Collected On' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeTimeType.value }}
</td>
</ng-container>
<ng-container matColumnDef="action">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let charge">
<button mat-icon-button color="warn" (click)="deleteCharge(charge)">
<fa-icon icon="trash"></fa-icon>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns"></tr>
</table>
</div>
}
</div> </div>
@@ -12,10 +12,6 @@
overflow: hidden; overflow: hidden;
background: #fff; background: #fff;
& + & {
margin-top: 16px;
}
&__header { &__header {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -36,30 +32,22 @@
color: #202124; color: #202124;
} }
} }
}
.fdpch-grid { &__body {
display: grid; padding: 16px 24px;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
@media (width <= 600px) {
grid-template-columns: 1fr;
} }
} }
.fdpch-field { .fdpch-add-row {
width: 100%;
&--full {
grid-column: 1 / -1;
}
}
.fdpch-btn-wrap {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px;
flex-wrap: wrap;
&__select {
flex: 1;
min-width: 200px;
}
} }
.fdpch-add-btn { .fdpch-add-btn {
@@ -73,7 +61,7 @@
border: 1px solid #e2e8f0; border: 1px solid #e2e8f0;
border-radius: 8px; border-radius: 8px;
overflow: hidden; overflow: hidden;
margin: 0 24px 16px; margin-top: 12px;
table { table {
width: 100%; width: 100%;
@@ -10,60 +10,102 @@
<div class="fdpc-section"> <div class="fdpc-section">
<div class="fdpc-section__header"> <div class="fdpc-section__header">
<i class="fas fa-coins"></i> <i class="fas fa-coins"></i>
<h3>{{ 'labels.inputs.Currency' | translate }}</h3> <span>Currency &amp; Account Status Configuration</span>
</div> </div>
<div class="fdpc-grid"> <div class="fdpc-body">
<mat-form-field appearance="outline" class="fdpc-field"> <div class="fdpc-grid">
<mat-label>{{ 'labels.inputs.Currency' | translate }}</mat-label>
<mat-select formControlName="currencyCode" required>
@for (currency of currencyData; track currency) {
<mat-option [value]="currency.code">
{{ currency.name }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Currency' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpc-field">
<mat-label>{{ 'labels.inputs.Decimal Places' | translate }}</mat-label>
<input
type="number"
min="0"
matInput
matTooltip="{{ 'tooltips.Decimal places to be used to track and report' | translate }}"
formControlName="digitsAfterDecimal"
required
/>
<mat-error *ngIf="fixedDepositProductCurrencyForm.get('digitsAfterDecimal')?.hasError('required')">
{{ 'labels.inputs.Decimal Places' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
<mat-error *ngIf="fixedDepositProductCurrencyForm.get('digitsAfterDecimal')?.hasError('min')">
{{ 'labels.inputs.Decimal Places' | translate }} {{ 'labels.commons.Minimum Value must be' | translate }}
<strong>0</strong>
</mat-error>
</mat-form-field>
<div class="fdpc-field fdpc-field--full fdpc-checkbox-row">
<mat-checkbox labelPosition="before" formControlName="setMultiples">
{{ 'labels.inputs.Set the fixed deposit installment in multiples of' | translate }}
</mat-checkbox>
</div>
@if (fixedDepositProductCurrencyForm.value.setMultiples) {
<mat-form-field appearance="outline" class="fdpc-field"> <mat-form-field appearance="outline" class="fdpc-field">
<mat-label>{{ 'labels.inputs.Currency in multiples of' | translate }}</mat-label> <mat-label>{{ 'labels.inputs.Currency' | translate }}</mat-label>
<input type="number" min="1" matInput formControlName="inMultiplesOf" /> <mat-select
formControlName="currencyCode"
matTooltip="{{ 'tooltips.The currency in which the deposit will be disbursed' | translate }}"
required
>
@for (currency of currencyData; track currency) {
<mat-option [value]="currency.code">
{{ currency.name }}
</mat-option>
}
</mat-select>
<mat-hint>Default: Indian Rupee - auto-set for domestic products</mat-hint>
<mat-error> <mat-error>
{{ 'labels.inputs.Currency in multiples of' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.inputs.Currency' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong> <strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error> </mat-error>
</mat-form-field> </mat-form-field>
}
<mat-form-field appearance="outline" class="fdpc-field">
<mat-label>{{ 'labels.inputs.Decimal Places' | translate }}</mat-label>
<mat-select
formControlName="digitsAfterDecimal"
matTooltip="{{ 'tooltips.Decimal places to be used to track and report' | translate }}"
required
>
@for (places of decimalPlacesData; track places) {
<mat-option [value]="places">{{ places }}</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Decimal Places' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
</div>
<div class="fdpc-alert">
<div class="fdpc-alert__content">
<h4>Dormancy Alert</h4>
<p>Enable automated notifications when an account becomes inactive based on product rules.</p>
</div>
<mat-radio-group class="fdpc-alert__choices" formControlName="dormancyAlert">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
<div class="fdpc-alert">
<div class="fdpc-alert__content">
<h4>Balance Swap Option</h4>
<p>Enable automated transfer of excess funds to linked deposits or vice-versa to maintain liquidity.</p>
</div>
<mat-radio-group class="fdpc-alert__choices" formControlName="balanceSwapOption">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
<div class="fdpc-alert">
<div class="fdpc-alert__content">
<h4>MSME</h4>
<p>Micro, Small and Medium Enterprises</p>
</div>
<mat-radio-group class="fdpc-alert__choices" formControlName="msme">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
<div class="fdpc-alert">
<div class="fdpc-alert__content">
<h4>Nomination Facility</h4>
<p>Allow account holders to designate individuals to receive funds in the event of account holder's death.</p>
</div>
<mat-radio-group class="fdpc-alert__choices" formControlName="nominationFacility">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
<div class="fdpc-alert">
<div class="fdpc-alert__content">
<h4>Auto Debit Facility</h4>
<p>Enable automatic deduction of funds for recurring payments, utility bills, or EMI obligations.</p>
</div>
<mat-radio-group class="fdpc-alert__choices" formControlName="autoDebitFacility">
<mat-radio-button [value]="true">yes</mat-radio-button>
<mat-radio-button [value]="false">no</mat-radio-button>
</mat-radio-group>
</div>
</div> </div>
</div> </div>
</form> </form>
@@ -6,8 +6,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * 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 { .fdpc-section {
border: 1px solid #e2e8f0; border: 1px solid $border;
border-radius: 10px; border-radius: 10px;
overflow: hidden; overflow: hidden;
background: #fff; background: #fff;
@@ -21,23 +27,27 @@
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 12px 24px; padding: 12px 24px;
background: #f8f9fa; background: $gray-50;
border-bottom: 1px solid #e8eaed; border-bottom: 1px solid $gray-200;
i { i {
font-size: 14px; font-size: 14px;
color: #0ea5e9; color: $accent;
} }
h3 { span {
margin: 0; margin: 0;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: #202124; color: $gray-900;
} }
} }
} }
.fdpc-body {
background: #fff;
}
.fdpc-grid { .fdpc-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
@@ -51,14 +61,43 @@
.fdpc-field { .fdpc-field {
width: 100%; width: 100%;
&--full {
grid-column: 1 / -1;
}
} }
.fdpc-checkbox-row { .fdpc-alert {
display: flex; display: flex;
align-items: center; 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;
}
} }
@@ -6,12 +6,10 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
import { Component, OnInit, Input, inject, DestroyRef } from '@angular/core'; import { Component, OnInit, Input, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
import { startWith } from 'rxjs/operators';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatTooltip } from '@angular/material/tooltip'; import { MatTooltip } from '@angular/material/tooltip';
import { MatRadioGroup, MatRadioButton } from '@angular/material/radio';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({ @Component({
@@ -21,18 +19,23 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
imports: [ imports: [
...STANDALONE_SHARED_IMPORTS, ...STANDALONE_SHARED_IMPORTS,
MatTooltip, MatTooltip,
MatCheckbox MatRadioGroup,
MatRadioButton
] ]
}) })
export class FixedDepositProductCurrencyStepComponent implements OnInit { export class FixedDepositProductCurrencyStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder); private formBuilder = inject(UntypedFormBuilder);
private destroyRef = inject(DestroyRef);
@Input() fixedDepositProductsTemplate: any; @Input() fixedDepositProductsTemplate: any;
fixedDepositProductCurrencyForm: UntypedFormGroup; fixedDepositProductCurrencyForm: UntypedFormGroup;
currencyData: any; currencyData: any;
decimalPlacesData = [
2,
0,
3
];
constructor() { constructor() {
this.createFixedDepositProductCurrencyForm(); this.createFixedDepositProductCurrencyForm();
@@ -42,13 +45,17 @@ export class FixedDepositProductCurrencyStepComponent implements OnInit {
this.currencyData = this.fixedDepositProductsTemplate.currencyOptions; this.currencyData = this.fixedDepositProductsTemplate.currencyOptions;
this.fixedDepositProductCurrencyForm.patchValue({ this.fixedDepositProductCurrencyForm.patchValue({
currencyCode: this.fixedDepositProductsTemplate.currency?.code || this.currencyData[0].code, currencyCode: this.fixedDepositProductsTemplate.currency?.code || this.currencyData?.[0]?.code || '',
digitsAfterDecimal: this.fixedDepositProductsTemplate.digitsAfterDecimal ?? '', digitsAfterDecimal:
setMultiples: !!this.fixedDepositProductsTemplate.inMultiplesOf, this.fixedDepositProductsTemplate.digitsAfterDecimal ??
inMultiplesOf: this.fixedDepositProductsTemplate.inMultiplesOf ?? '' 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() { createFixedDepositProductCurrencyForm() {
@@ -58,48 +65,30 @@ export class FixedDepositProductCurrencyStepComponent implements OnInit {
Validators.required Validators.required
], ],
digitsAfterDecimal: [ digitsAfterDecimal: [
'', 2,
[ [
Validators.required, Validators.required,
Validators.min(0) Validators.min(0)
] ]
], ],
setMultiples: [false], dormancyAlert: [false],
inMultiplesOf: [''] 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() { get fixedDepositProductCurrency() {
const formValue = this.fixedDepositProductCurrencyForm.value; const formValue = this.fixedDepositProductCurrencyForm.value;
const result: any = { return {
currencyCode: formValue.currencyCode, 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;
} }
} }
@@ -9,13 +9,18 @@
<form [formGroup]="fixedDepositProductDetailsForm"> <form [formGroup]="fixedDepositProductDetailsForm">
<div class="fdpd-section"> <div class="fdpd-section">
<div class="fdpd-section__header"> <div class="fdpd-section__header">
<i class="fas fa-info-circle"></i> <i class="fa fa-info-circle"></i>
<h3>{{ 'labels.inputs.Product Name' | translate }} &amp; {{ 'labels.inputs.Description' | translate }}</h3> <h3>{{ 'labels.inputs.Product Name' | translate }} &amp; {{ 'labels.inputs.Description' | translate }}</h3>
</div> </div>
<div class="fdpd-grid"> <div class="fdpd-grid">
<mat-form-field appearance="outline" class="fdpd-field"> <mat-form-field appearance="outline" class="fdpd-field">
<mat-label>{{ 'labels.inputs.Product Name' | translate }}</mat-label> <mat-label>{{ 'labels.inputs.Product Name' | translate }}</mat-label>
<input matInput formControlName="name" required /> <input
matInput
formControlName="name"
matTooltip="{{ 'tooltips.Product name is a unique identifier' | translate }}"
required
/>
@if (fixedDepositProductDetailsForm.controls.name.hasError('required')) { @if (fixedDepositProductDetailsForm.controls.name.hasError('required')) {
<mat-error> <mat-error>
{{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }}
@@ -26,7 +31,13 @@
<mat-form-field appearance="outline" class="fdpd-field"> <mat-form-field appearance="outline" class="fdpd-field">
<mat-label>{{ 'labels.inputs.Short Name' | translate }}</mat-label> <mat-label>{{ 'labels.inputs.Short Name' | translate }}</mat-label>
<input matInput formControlName="shortName" maxlength="4" required /> <input
matInput
formControlName="shortName"
maxlength="4"
matTooltip="{{ 'tooltips.Short name is a unique identifier' | translate }}"
required
/>
@if (fixedDepositProductDetailsForm.controls.shortName.hasError('required')) { @if (fixedDepositProductDetailsForm.controls.shortName.hasError('required')) {
<mat-error> <mat-error>
{{ 'labels.inputs.Short Name' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.inputs.Short Name' | translate }} {{ 'labels.commons.is' | translate }}
@@ -37,14 +48,80 @@
<mat-form-field appearance="outline" class="fdpd-field fdpd-field--full"> <mat-form-field appearance="outline" class="fdpd-field fdpd-field--full">
<mat-label>{{ 'labels.inputs.Description' | translate }}</mat-label> <mat-label>{{ 'labels.inputs.Description' | translate }}</mat-label>
<textarea matInput formControlName="description" required></textarea> <textarea
@if (fixedDepositProductDetailsForm.controls.description.hasError('required')) { matInput
<mat-error> formControlName="description"
{{ 'labels.inputs.Description' | translate }} {{ 'labels.commons.is' | translate }} matTooltip="{{
<strong>{{ 'labels.commons.required' | translate }}</strong> 'tooltips.Description is used to provide additional information regarding the purpose' | translate
</mat-error> }}"
} cdkTextareaAutosize
cdkAutosizeMinRows="2"
></textarea>
</mat-form-field> </mat-form-field>
</div> </div>
</div> </div>
<div class="fdpd-section">
<div class="fdpd-section__header">
<i class="fa fa-tags"></i>
<h3>Product Classification</h3>
</div>
<div class="fdpd-grid fdpd-grid--classification">
<mat-form-field appearance="outline" class="fdpd-field">
<mat-label>Account Type</mat-label>
<mat-select formControlName="accountType" required>
@for (accountType of accountTypeData; track accountType) {
<mat-option [value]="accountType">{{ accountType }}</mat-option>
}
</mat-select>
<mat-error>Account Type <strong>is required</strong></mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpd-field">
<mat-label>Customer Segment</mat-label>
<mat-select formControlName="customerSegment">
@for (customerSegment of customerSegmentData; track customerSegment) {
<mat-option [value]="customerSegment">{{ customerSegment }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpd-field">
<mat-label>GL Account (Liability)</mat-label>
<mat-select formControlName="glAccountId" [compareWith]="compareById">
@for (account of liabilityAccountOptions; track account.id) {
<mat-option [value]="account.id">{{ account.name }} ({{ account.glCode }})</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpd-field" (click)="effectiveToPicker.open()">
<mat-label>Effective To</mat-label>
<input matInput [matDatepicker]="effectiveToPicker" formControlName="effectiveTo" />
<mat-datepicker-toggle matSuffix [for]="effectiveToPicker"></mat-datepicker-toggle>
<mat-datepicker #effectiveToPicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpd-field" (click)="endDatePicker.open()">
<mat-label>End Date</mat-label>
<input matInput [matDatepicker]="endDatePicker" formControlName="endDate" />
<mat-datepicker-toggle matSuffix [for]="endDatePicker"></mat-datepicker-toggle>
<mat-datepicker #endDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpd-field">
<mat-label>Introducer Required</mat-label>
<mat-select formControlName="introducerRequired">
@for (option of yesNoOptions; track option.value) {
<mat-option [value]="option.value">{{ option.label }}</mat-option>
}
</mat-select>
</mat-form-field>
<div class="fdpd-field fdpd-field--full fdpd-checkbox-row">
<mat-checkbox formControlName="shareLinkingEnabled">Deposit Account Link</mat-checkbox>
</div>
</div>
</div>
</form> </form>
@@ -6,8 +6,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * 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 { .fdpd-section {
border: 1px solid #e2e8f0; border: 1px solid $border;
border-radius: 10px; border-radius: 10px;
overflow: hidden; overflow: hidden;
background: #fff; background: #fff;
@@ -21,19 +27,19 @@
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 12px 24px; padding: 12px 24px;
background: #f8f9fa; background: $gray-50;
border-bottom: 1px solid #e8eaed; border-bottom: 1px solid $gray-200;
i { i {
font-size: 14px; font-size: 14px;
color: #0ea5e9; color: $accent;
} }
h3 { h3 {
margin: 0; margin: 0;
font-size: 14px; font-size: 14px;
font-weight: 600; 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 { .fdpd-field {
width: 100%; width: 100%;
@@ -56,3 +74,9 @@
grid-column: 1 / -1; grid-column: 1 / -1;
} }
} }
.fdpd-checkbox-row {
display: flex;
align-items: center;
min-height: 56px;
}
@@ -6,8 +6,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/ */
import { Component, OnInit, Input, inject } from '@angular/core'; import { Component, OnInit, OnChanges, Input, SimpleChanges, inject, ChangeDetectorRef } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; 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'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({ @Component({
@@ -15,27 +19,41 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
templateUrl: './fixed-deposit-product-details-step.component.html', templateUrl: './fixed-deposit-product-details-step.component.html',
styleUrls: ['./fixed-deposit-product-details-step.component.scss'], styleUrls: ['./fixed-deposit-product-details-step.component.scss'],
imports: [ 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 formBuilder = inject(UntypedFormBuilder);
private dateUtils = inject(Dates);
private settingsService = inject(SettingsService);
private cdr = inject(ChangeDetectorRef);
@Input() fixedDepositProductsTemplate: any; @Input() fixedDepositProductsTemplate: any;
fixedDepositProductDetailsForm: UntypedFormGroup; fixedDepositProductDetailsForm: UntypedFormGroup;
accountTypeData: string[] = [];
customerSegmentData: string[] = [];
liabilityAccountOptions: any[] = [];
yesNoOptions: Array<{ value: boolean; label: string }> = [
{ value: true, label: 'Yes' },
{ value: false, label: 'No' }
];
constructor() { constructor() {
this.createFixedDepositProductDetailsForm(); this.createFixedDepositProductDetailsForm();
} }
ngOnInit() { ngOnInit() {
if (!(this.fixedDepositProductsTemplate === undefined) && this.fixedDepositProductsTemplate.id) { if (this.fixedDepositProductsTemplate) {
this.fixedDepositProductDetailsForm.patchValue({ this.applyTemplate(this.fixedDepositProductsTemplate);
name: this.fixedDepositProductsTemplate.name, }
shortName: this.fixedDepositProductsTemplate.shortName, }
description: this.fixedDepositProductsTemplate.description
}); ngOnChanges(changes: SimpleChanges) {
if (changes.fixedDepositProductsTemplate?.currentValue) {
this.applyTemplate(changes.fixedDepositProductsTemplate.currentValue);
} }
} }
@@ -49,14 +67,66 @@ export class FixedDepositProductDetailsStepComponent implements OnInit {
'', '',
Validators.required Validators.required
], ],
description: [ description: [''],
'', accountType: [
null,
Validators.required 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() { 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;
} }
} }
@@ -7,204 +7,124 @@
--> -->
<form [formGroup]="fixedDepositProductSettingsForm"> <form [formGroup]="fixedDepositProductSettingsForm">
<!-- Premature option -->
<div class="fdps-section"> <div class="fdps-section">
<div class="fdps-section__header"> <div class="fdps-section__header">
<i class="fas fa-lock"></i> <span class="fdps-section__badge"><i class="fas fa-balance-scale"></i></span>
<h3>{{ 'labels.inputs.Lock-in Period' | translate }}</h3> <span>Premature option</span>
</div> </div>
<div class="fdps-grid"> <div class="fdps-body">
<mat-form-field appearance="outline" class="fdps-field"> <div class="fdps-premature-row">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label> <mat-checkbox formControlName="loanFacility">Loan Facility</mat-checkbox>
<input <mat-checkbox formControlName="prematureWithdrawalOption">Premature Withdrawal option</mat-checkbox>
type="number" <mat-checkbox formControlName="tenureFlexibility">Tenure Flexibility</mat-checkbox>
min="0" <mat-checkbox formControlName="lienMarked">Lien Marked</mat-checkbox>
matInput <mat-checkbox formControlName="withdrawalFeeForTransfers">Apply Withdrawal Fee for Transfers</mat-checkbox>
formControlName="lockinPeriodFrequency" </div>
matTooltip="{{ 'tooltips.A period of time during which a fixed deposit account' | translate }}"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Type' | translate }}</mat-label>
<mat-select formControlName="lockinPeriodFrequencyType">
@for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) {
<mat-option [value]="lockinPeriodFrequencyType.id">
{{ lockinPeriodFrequencyType.value }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div> </div>
</div> </div>
<!-- Lock-in Period -->
<div class="fdps-section"> <div class="fdps-section">
<div class="fdps-section__header"> <div class="fdps-section__header">
<i class="fas fa-calendar-check"></i> <span class="fdps-section__badge"><i class="fas fa-lock"></i></span>
<h3>{{ 'labels.inputs.Minimum Deposit Term' | translate }}</h3> <span>{{ 'labels.inputs.Lock-in Period' | translate }}</span>
</div> </div>
<div class="fdps-grid"> <div class="fdps-body">
<mat-form-field appearance="outline" class="fdps-field"> <div class="fdps-lockin-row">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label> <span>Enable Lock-in Period</span>
<input <mat-checkbox formControlName="enableLockinPeriod"></mat-checkbox>
type="number"
min="0"
matInput
formControlName="minDepositTerm"
required
matTooltip="{{ 'tooltips.The minimum length of time' | translate }}"
/>
<mat-error>
{{ 'labels.inputs.Minimum Deposit Term Frequency' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Frequency Type' | translate }}</mat-label>
<mat-select formControlName="minDepositTermTypeId" required>
@for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) {
<mat-option [value]="periodFrequencyType.id">
{{ periodFrequencyType.value }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Minimum Deposit Term Frequency Type' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
</div>
</div>
<div class="fdps-section">
<div class="fdps-section__header">
<i class="fas fa-layer-group"></i>
<h3>{{ 'labels.heading.And thereafter, in Multiples of' | translate }}</h3>
</div>
<div class="fdps-grid">
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<input
type="number"
min="0"
matInput
formControlName="inMultiplesOfDepositTerm"
matTooltip="{{ 'tooltips.After the minimum deposit term has passed' | translate }}"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Type' | translate }}</mat-label>
<mat-select formControlName="inMultiplesOfDepositTermTypeId">
@for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) {
<mat-option [value]="periodFrequencyType.id">
{{ periodFrequencyType.value }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</div>
<div class="fdps-section">
<div class="fdps-section__header">
<i class="fas fa-calendar-alt"></i>
<h3>{{ 'labels.inputs.Maximum Deposit Term' | translate }}</h3>
</div>
<div class="fdps-grid">
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<input
type="number"
min="0"
matInput
formControlName="maxDepositTerm"
matTooltip="{{ 'tooltips.The maximum length of time funds' | translate }}"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Type' | translate }}</mat-label>
<mat-select formControlName="maxDepositTermTypeId">
@for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) {
<mat-option [value]="periodFrequencyType.id">
{{ periodFrequencyType.value }}
</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</div>
<div class="fdps-section">
<div class="fdps-section__header">
<i class="fas fa-exclamation-triangle"></i>
<h3>{{ 'labels.heading.For Pre-mature closure' | translate }}</h3>
</div>
<div class="fdps-grid">
<div class="fdps-field fdps-field--full fdps-checkbox-row">
<mat-checkbox
labelPosition="before"
formControlName="preClosurePenalApplicable"
matTooltip="{{ 'tooltips.This sections defines an interest penalty' | translate }}"
>
{{ 'labels.inputs.Apply Penal Interest (less)' | translate }}
</mat-checkbox>
</div> </div>
<mat-form-field appearance="outline" class="fdps-field"> @if (fixedDepositProductSettingsForm.value.enableLockinPeriod) {
<mat-label>{{ 'labels.inputs.Penal Interest' | translate }} (%)</mat-label> <div class="fdps-lockin-fields">
<input type="number" min="0" matInput formControlName="preClosurePenalInterest" /> <mat-form-field appearance="outline" class="fdps-field">
</mat-form-field> <mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<input
type="number"
matInput
formControlName="lockinPeriodFrequency"
min="1"
step="1"
matTooltip="{{ 'tooltips.A period of time during which a fixed deposit account' | translate }}"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="fdps-field"> <mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Period' | translate }}</mat-label> <mat-label>{{ 'labels.inputs.Type' | translate }}</mat-label>
<mat-select formControlName="preClosurePenalInterestOnTypeId"> <mat-select formControlName="lockinPeriodFrequencyType">
@for ( @for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) {
preClosurePenalInterestOnType of preClosurePenalInterestOnTypeData; <mat-option [value]="lockinPeriodFrequencyType.id">
track preClosurePenalInterestOnType {{ lockinPeriodFrequencyType.value | translateKey: 'catalogs' }}
) { </mat-option>
<mat-option [value]="preClosurePenalInterestOnType.id"> }
{{ preClosurePenalInterestOnType.value }} </mat-select>
</mat-option> </mat-form-field>
} </div>
</mat-select>
</mat-form-field>
</div>
</div>
<div class="fdps-section">
<div class="fdps-section__header">
<i class="fas fa-file-invoice-dollar"></i>
<h3>{{ 'labels.inputs.Is Withhold Tax Applicable' | translate }}</h3>
</div>
<div class="fdps-grid">
<div class="fdps-field fdps-field--full fdps-checkbox-row">
<mat-checkbox
labelPosition="before"
formControlName="withHoldTax"
matTooltip="{{ 'tooltips.An boolean flag to attach' | translate }}"
>
{{ 'labels.inputs.Is Withhold Tax Applicable' | translate }}
</mat-checkbox>
</div>
@if (fixedDepositProductSettingsForm.value.withHoldTax) {
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>{{ 'labels.inputs.Tax Group' | translate }}</mat-label>
<mat-select formControlName="taxGroupId" required>
@for (taxGroup of taxGroupData; track taxGroup) {
<mat-option [value]="taxGroup.id">
{{ taxGroup.name }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Tax Group' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
} }
</div> </div>
</div> </div>
<!-- Maturity Instructions -->
<div class="fdps-section">
<div class="fdps-section__header">
<span class="fdps-section__badge"><i class="fas fa-lock"></i></span>
<span>Maturity Instructions</span>
</div>
<div class="fdps-body fdps-body--list">
<div class="fdps-list-row">
<span>Maturity Instructions</span>
<mat-checkbox formControlName="maturityInstructions"></mat-checkbox>
</div>
<div class="fdps-list-row">
<span
>Option to credit maturity proceeds directly to the linked account or reinvest in another product (like
FD).</span
>
<mat-checkbox formControlName="creditMaturityProceeds"></mat-checkbox>
</div>
<div class="fdps-list-row">
<span>Penalty / Grace Period for Missed Installments</span>
<mat-checkbox formControlName="penaltyGracePeriodMissedInstallments"></mat-checkbox>
</div>
<div class="fdps-list-row">
<span>Panel Request Applicable</span>
<mat-checkbox formControlName="panelRequestApplicable"></mat-checkbox>
</div>
<div class="fdps-list-row">
<span>No penalty for nominee / legal heir closure</span>
<mat-checkbox formControlName="noPenaltyNomineeHeirClosure"></mat-checkbox>
</div>
<div class="fdps-list-row">
<span>Deceased Settlement</span>
<mat-checkbox formControlName="deceasedSettlement"></mat-checkbox>
</div>
<div class="fdps-list-row">
<span>Allow Debit Txn after due date</span>
<mat-checkbox formControlName="allowDebitTxnAfterDueDate"></mat-checkbox>
</div>
</div>
</div>
<!-- Additional Rules -->
<div class="fdps-section">
<div class="fdps-section__header">
<span class="fdps-section__badge"><i class="fas fa-cog"></i></span>
<span>Additional Rules</span>
</div>
<div class="fdps-body">
<div class="fdps-layout fdps-layout--additional">
<div class="fdps-toggle fdps-toggle--inline">
<mat-checkbox formControlName="accountClosedZeroBalanceInDayClosing">
<span>Account Closed with Zero balance in Day Closing</span>
</mat-checkbox>
</div>
<mat-form-field appearance="outline" class="fdps-field">
<mat-label>Balance Required for Interest Calculation</mat-label>
<input type="number" matInput formControlName="minBalanceForInterestCalculation" min="0" step="0.01" />
</mat-form-field>
</div>
</div>
</div>
</form> </form>
@@ -6,8 +6,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * 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 { .fdps-section {
border: 1px solid #e2e8f0; border: 1px solid $border;
border-radius: 10px; border-radius: 10px;
overflow: hidden; overflow: hidden;
background: #fff; background: #fff;
@@ -21,32 +27,41 @@
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 12px 24px; padding: 12px 24px;
background: #f8f9fa; background: $gray-50;
border-bottom: 1px solid #e8eaed; border-bottom: 1px solid $gray-200;
i { i {
font-size: 14px; font-size: 14px;
color: #0ea5e9; color: $accent;
} }
h3 { span:not(.fdps-section__badge) {
margin: 0; margin: 0;
font-size: 14px; font-size: 14px;
font-weight: 600; font-weight: 600;
color: #202124; color: $gray-900;
} }
} }
} }
.fdps-grid { .fdps-section__badge {
display: grid; display: inline-flex;
grid-template-columns: repeat(2, 1fr); align-items: center;
gap: 4px 16px; justify-content: center;
padding: 16px 24px; width: 24px;
height: 24px;
border-radius: 6px;
background: #eff6ff;
font-size: 14px;
}
@media (width <= 600px) { .fdps-body {
grid-template-columns: 1fr; background: #fff;
} padding: 18px 24px 24px;
}
.fdps-body--list {
padding: 0 24px;
} }
.fdps-field { .fdps-field {
@@ -55,10 +70,91 @@
&--full { &--full {
grid-column: 1 / -1; grid-column: 1 / -1;
} }
&--compact {
width: 200px;
min-width: 150px;
}
} }
.fdps-checkbox-row { .fdps-premature-row {
display: flex; display: flex;
align-items: center; 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;
}
} }
@@ -7,13 +7,7 @@
*/ */
import { Component, OnInit, Input, inject } from '@angular/core'; import { Component, OnInit, Input, inject } from '@angular/core';
import { import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms';
UntypedFormGroup,
UntypedFormBuilder,
UntypedFormControl,
Validators,
ReactiveFormsModule
} from '@angular/forms';
import { MatTooltip } from '@angular/material/tooltip'; import { MatTooltip } from '@angular/material/tooltip';
import { MatCheckbox } from '@angular/material/checkbox'; import { MatCheckbox } from '@angular/material/checkbox';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -36,113 +30,71 @@ export class FixedDepositProductSettingsStepComponent implements OnInit {
fixedDepositProductSettingsForm: UntypedFormGroup; fixedDepositProductSettingsForm: UntypedFormGroup;
lockinPeriodFrequencyTypeData: any; lockinPeriodFrequencyTypeData: any;
periodFrequencyTypeData: any;
preClosurePenalInterestOnTypeData: any;
taxGroupData: any;
constructor() { constructor() {
this.createFixedDepositProductSettingsForm(); this.createFixedDepositProductSettingsForm();
this.setConditionalControls();
} }
ngOnInit() { ngOnInit() {
this.lockinPeriodFrequencyTypeData = this.fixedDepositProductsTemplate.lockinPeriodFrequencyTypeOptions; 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({ this.fixedDepositProductSettingsForm.patchValue({
isMandatoryDeposit: this.fixedDepositProductsTemplate.isMandatoryDeposit, loanFacility: this.fixedDepositProductsTemplate.loanFacility ?? false,
adjustAdvanceTowardsFuturePayments: this.fixedDepositProductsTemplate.adjustAdvanceTowardsFuturePayments, prematureWithdrawalOption: this.fixedDepositProductsTemplate.prematureWithdrawalOption ?? false,
allowWithdrawal: this.fixedDepositProductsTemplate.allowWithdrawal, tenureFlexibility: this.fixedDepositProductsTemplate.tenureFlexibility ?? false,
lockinPeriodFrequency: this.fixedDepositProductsTemplate.lockinPeriodFrequency, lienMarked: this.fixedDepositProductsTemplate.lienMarked ?? false,
lockinPeriodFrequencyType: this.fixedDepositProductsTemplate.lockinPeriodFrequencyType withdrawalFeeForTransfers: this.fixedDepositProductsTemplate.withdrawalFeeForTransfers ?? false,
? this.fixedDepositProductsTemplate.lockinPeriodFrequencyType.id enableLockinPeriod: this.fixedDepositProductsTemplate.enableLockinPeriod ?? false,
: '', lockinPeriodFrequency: this.fixedDepositProductsTemplate.lockinPeriodFrequency ?? '',
minDepositTerm: this.fixedDepositProductsTemplate.minDepositTerm, lockinPeriodFrequencyType: this.fixedDepositProductsTemplate.lockinPeriodFrequencyType?.id ?? '',
minDepositTermTypeId: this.fixedDepositProductsTemplate.minDepositTermType maturityInstructions: this.fixedDepositProductsTemplate.maturityInstructions ?? false,
? this.fixedDepositProductsTemplate.minDepositTermType.id creditMaturityProceeds: this.fixedDepositProductsTemplate.creditMaturityProceeds ?? false,
: '', penaltyGracePeriodMissedInstallments:
inMultiplesOfDepositTerm: this.fixedDepositProductsTemplate.inMultiplesOfDepositTerm, this.fixedDepositProductsTemplate.penaltyGracePeriodMissedInstallments ?? false,
inMultiplesOfDepositTermTypeId: this.fixedDepositProductsTemplate.inMultiplesOfDepositTermType panelRequestApplicable: this.fixedDepositProductsTemplate.panelRequestApplicable ?? false,
? this.fixedDepositProductsTemplate.inMultiplesOfDepositTerm.id noPenaltyNomineeHeirClosure: this.fixedDepositProductsTemplate.noPenaltyNomineeHeirClosure ?? false,
: '', deceasedSettlement: this.fixedDepositProductsTemplate.deceasedSettlement ?? false,
maxDepositTerm: this.fixedDepositProductsTemplate.maxDepositTerm, allowDebitTxnAfterDueDate: this.fixedDepositProductsTemplate.allowDebitTxnAfterDueDate ?? false,
maxDepositTermTypeId: this.fixedDepositProductsTemplate.maxDepositTermType accountClosedZeroBalanceInDayClosing:
? this.fixedDepositProductsTemplate.minDepositTermType.id this.fixedDepositProductsTemplate.accountClosedZeroBalanceInDayClosing ?? false,
: '', minBalanceForInterestCalculation: this.fixedDepositProductsTemplate.minBalanceForInterestCalculation ?? ''
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 : ''
}); });
} }
} }
createFixedDepositProductSettingsForm() { createFixedDepositProductSettingsForm() {
this.fixedDepositProductSettingsForm = this.formBuilder.group({ this.fixedDepositProductSettingsForm = this.formBuilder.group({
loanFacility: [false],
prematureWithdrawalOption: [false],
tenureFlexibility: [false],
lienMarked: [false],
withdrawalFeeForTransfers: [false],
enableLockinPeriod: [false],
lockinPeriodFrequency: [ lockinPeriodFrequency: [
'', '',
Validators.min(0) Validators.min(0)
], ],
lockinPeriodFrequencyType: [''], lockinPeriodFrequencyType: [''],
minDepositTerm: [ maturityInstructions: [false],
'', creditMaturityProceeds: [false],
[ penaltyGracePeriodMissedInstallments: [false],
Validators.required, panelRequestApplicable: [false],
Validators.min(0) noPenaltyNomineeHeirClosure: [false],
] deceasedSettlement: [false],
], allowDebitTxnAfterDueDate: [false],
minDepositTermTypeId: [ accountClosedZeroBalanceInDayClosing: [false],
'', minBalanceForInterestCalculation: ['']
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');
}
}); });
} }
get fixedDepositProductSettings() { get fixedDepositProductSettings() {
const fixedDepositProductSettings = this.fixedDepositProductSettingsForm.value; const settings = { ...this.fixedDepositProductSettingsForm.value };
for (const key in fixedDepositProductSettings) { for (const key in settings) {
if (fixedDepositProductSettings[key] === '') { if (settings[key] === '') {
delete fixedDepositProductSettings[key]; delete settings[key];
} }
} }
return fixedDepositProductSettings; return settings;
} }
} }
@@ -7,139 +7,148 @@
--> -->
<form [formGroup]="fixedDepositProductTermsForm"> <form [formGroup]="fixedDepositProductTermsForm">
<!-- Interest Rate & Posting Terms -->
<div class="fdpt-section"> <div class="fdpt-section">
<div class="fdpt-section__header"> <div class="fdpt-section__header">
<i class="fas fa-wallet"></i> <span class="fdpt-section__badge"><i class="fas fa-chart-line"></i></span>
<h3>{{ 'labels.inputs.Deposit Amount' | translate }}</h3> <span>Interest Rate &amp; Posting Terms</span>
</div> </div>
<div class="fdpt-grid"> <div class="fdpt-body">
<mat-form-field appearance="outline" class="fdpt-field"> <div class="fdpt-banner">
<mat-label>{{ 'labels.inputs.Minimum' | translate }}</mat-label> <span class="fdpt-banner__icon">ℹ️</span>
<input <span>
type="number" Fixed Deposit base rate:
min="0" <b>{{ fixedDepositProductTermsForm.get('nominalAnnualInterestRate').value }}%</b>
matInput </span>
formControlName="minDepositAmount" </div>
matTooltip="{{ 'tooltips.The minimum deposit amount required to open a fixed deposit' | translate }}"
/>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpt-field"> <div class="fdpt-grid fdpt-grid--three">
<mat-label>{{ 'labels.inputs.Default' | translate }}</mat-label> <mat-form-field appearance="outline" class="fdpt-field">
<input <mat-label>{{ 'labels.inputs.Nominal Annual Interest' | translate }}</mat-label>
type="number" <input
min="0" type="number"
matInput matInput
formControlName="depositAmount" matTooltip="{{ 'tooltips.The default interest rate set' | translate }}"
matTooltip="{{ 'tooltips.The default deposit amount expected' | translate }}" formControlName="nominalAnnualInterestRate"
required required
/> min="0"
<mat-error> step="0.01"
{{ 'labels.inputs.Default' | translate }} {{ 'labels.inputs.Deposit Amount' | translate }} />
{{ 'labels.commons.is' | translate }} <strong>{{ 'labels.commons.required' | translate }}</strong> @if (fixedDepositProductTermsForm.get('nominalAnnualInterestRate').hasError('required')) {
</mat-error> <mat-error>
</mat-form-field> {{ 'labels.inputs.Nominal Annual Interest' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
<mat-form-field appearance="outline" class="fdpt-field"> </mat-error>
<mat-label>{{ 'labels.inputs.Maximum' | translate }}</mat-label>
<input
type="number"
min="0"
matInput
formControlName="maxDepositAmount"
matTooltip="{{ 'tooltips.The maximum deposit amount allowed when a fixed deposit' | translate }}"
/>
</mat-form-field>
</div>
</div>
<div class="fdpt-section">
<div class="fdpt-section__header">
<i class="fas fa-percentage"></i>
<h3>{{ 'labels.inputs.Interest Compounding Period' | translate }}</h3>
</div>
<div class="fdpt-grid">
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>{{ 'labels.inputs.Interest Compounding Period' | translate }}</mat-label>
<mat-select
formControlName="interestCompoundingPeriodType"
matTooltip="{{ 'tooltips.The period at which interest rate' | translate }}"
required
>
@for (
interestCompoundingPeriodType of interestCompoundingPeriodTypeData;
track interestCompoundingPeriodType
) {
<mat-option [value]="interestCompoundingPeriodType.id">
{{ interestCompoundingPeriodType.value }}
</mat-option>
} }
</mat-select> @if (fixedDepositProductTermsForm.get('nominalAnnualInterestRate').hasError('min')) {
<mat-error> <mat-error>{{ 'labels.inputs.Nominal Annual Interest' | translate }} must be zero or greater</mat-error>
{{ 'labels.inputs.Interest Compounding Period' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>{{ 'labels.inputs.Interest Posting Period' | translate }}</mat-label>
<mat-select
formControlName="interestPostingPeriodType"
matTooltip="{{ 'tooltips.The period at which interest rate is posted or credited' | translate }}"
required
>
@for (interestPostingPeriodType of interestPostingPeriodTypeData; track interestPostingPeriodType) {
<mat-option [value]="interestPostingPeriodType.id">
{{ interestPostingPeriodType.value }}
</mat-option>
} }
</mat-select> </mat-form-field>
<mat-error>
{{ 'labels.inputs.Interest Posting Period' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpt-field"> <mat-form-field appearance="outline" class="fdpt-field">
<mat-label>{{ 'labels.inputs.Interest Calculated using' | translate }}</mat-label> <mat-label>Effective Date</mat-label>
<mat-select <input matInput [matDatepicker]="effectiveDatePicker" formControlName="effectiveDate" />
formControlName="interestCalculationType" <mat-datepicker-toggle matSuffix [for]="effectiveDatePicker"></mat-datepicker-toggle>
matTooltip="{{ 'tooltips.The method used to calculate interest' | translate }}" <mat-datepicker #effectiveDatePicker></mat-datepicker>
required </mat-form-field>
>
@for (interestCalculationType of interestCalculationTypeData; track interestCalculationType) {
<mat-option [value]="interestCalculationType.id">
{{ interestCalculationType.value }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Interest Calculated using' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpt-field"> <mat-form-field appearance="outline" class="fdpt-field">
<mat-label>{{ 'labels.inputs.Days in Year' | translate }}</mat-label> <mat-label>{{ 'labels.inputs.Interest Calculation Type' | translate }}</mat-label>
<mat-select <mat-select
formControlName="interestCalculationDaysInYearType" formControlName="interestCalculateSelector"
matTooltip="{{ 'tooltips.The setting for number of days' | translate }}" matTooltip="{{ 'tooltips.The method used to calculate interest' | translate }}"
required >
> @for (opt of interestCalculationTypeOptionData; track opt.id) {
@for ( <mat-option [value]="opt.id">{{ opt.value }}</mat-option>
interestCalculationDaysInYearType of interestCalculationDaysInYearTypeData; }
track interestCalculationDaysInYearType </mat-select>
) { </mat-form-field>
<mat-option [value]="interestCalculationDaysInYearType.id"> </div>
{{ interestCalculationDaysInYearType.value }}
</mat-option> <div class="fdpt-grid fdpt-grid--three">
} <mat-form-field appearance="outline" class="fdpt-field">
</mat-select> <mat-label>Interest Calculate</mat-label>
<mat-error> <mat-select formControlName="interestCalculationType" required>
{{ 'labels.inputs.Days in Year' | translate }} {{ 'labels.commons.is' | translate }} @for (mode of interestCalculationTypeData; track mode.id) {
<strong>{{ 'labels.commons.required' | translate }}</strong> <mat-option [value]="mode.id">{{ mode.value | translateKey: 'catalogs' }}</mat-option>
</mat-error> }
</mat-form-field> </mat-select>
<mat-error>
{{ 'labels.inputs.Interest Calculation Type' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
@if (fixedDepositProductTermsForm.value.interestCalculationType === 2) {
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>Average Period</mat-label>
<mat-select formControlName="averagePeriod">
@for (period of periodFrequencyData; track period.id) {
<mat-option [value]="period.value">{{ period.value }}</mat-option>
}
</mat-select>
</mat-form-field>
}
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>{{ 'labels.inputs.Interest Posting Period' | translate }}</mat-label>
<mat-select
formControlName="interestPostingPeriodType"
matTooltip="{{ 'tooltips.Interest rate is posted or credited to a saving account' | translate }}"
required
>
@for (interestPostingPeriodType of interestPostingPeriodTypeData; track interestPostingPeriodType) {
<mat-option [value]="interestPostingPeriodType.id">
{{ interestPostingPeriodType.value | translateKey: 'catalogs' }}
</mat-option>
}
</mat-select>
<mat-error>
{{ 'labels.inputs.Interest Posting Period' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
</div>
<div class="fdpt-footer">
<div class="fdpt-footer__grid">
<div class="fdpt-toggle-card">
<div>
<h4>Auto Interest Post</h4>
</div>
<div class="fdpt-toggle-card__choices">
@for (option of yesNoOptions; track option.value) {
<label class="inline-flex items-center cursor-pointer">
<input type="radio" class="w-4 h-4" formControlName="autoPost" [value]="option.value" />
<span class="ml-2 text-sm">{{ option.label }}</span>
</label>
}
</div>
</div>
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>Frequency (Fix Day)</mat-label>
<input type="number" matInput formControlName="frequencyFixDay" />
<mat-hint>Days</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="fdpt-field">
<mat-label>Last Interest Date</mat-label>
<input matInput [matDatepicker]="lastInterestDatePicker" formControlName="lastInterestDate" />
<mat-datepicker-toggle matSuffix [for]="lastInterestDatePicker"></mat-datepicker-toggle>
<mat-datepicker #lastInterestDatePicker></mat-datepicker>
</mat-form-field>
</div>
@if (fixedDepositProductTermsForm.value.interestCalculationType === 1) {
<div class="fdpt-note">
<span>⚠️</span>
<span
>Daily product-based interest accrual enabled - interest calculated EOD on daily closing balance.</span
>
</div>
}
</div>
</div> </div>
</div> </div>
</form> </form>
@@ -6,8 +6,14 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. * 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 { .fdpt-section {
border: 1px solid #e2e8f0; border: 1px solid $border;
border-radius: 10px; border-radius: 10px;
overflow: hidden; overflow: hidden;
background: #fff; background: #fff;
@@ -21,31 +27,54 @@
align-items: center; align-items: center;
gap: 10px; gap: 10px;
padding: 12px 24px; padding: 12px 24px;
background: #f8f9fa; background: $gray-50;
border-bottom: 1px solid #e8eaed; border-bottom: 1px solid $gray-200;
i { i {
font-size: 14px; font-size: 14px;
color: #0ea5e9; color: $accent;
} }
h3 { span:not(.fdpt-section__badge) {
margin: 0; margin: 0;
font-size: 14px; font-size: 14px;
font-weight: 600; 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 { .fdpt-grid {
display: grid; display: grid;
grid-template-columns: repeat(2, 1fr); grid-template-columns: repeat(2, 1fr);
gap: 4px 16px; gap: 4px 16px;
padding: 16px 24px; padding: 16px 24px;
&--three {
grid-template-columns: repeat(3, 1fr);
}
@media (width <= 600px) { @media (width <= 600px) {
grid-template-columns: 1fr; grid-template-columns: 1fr;
&--three {
grid-template-columns: 1fr;
}
} }
} }
@@ -56,3 +85,67 @@
grid-column: 1 / -1; 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;
}
@@ -7,8 +7,10 @@
*/ */
import { Component, OnInit, Input, inject } from '@angular/core'; 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 { 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'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({ @Component({
@@ -22,6 +24,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
}) })
export class FixedDepositProductTermsStepComponent implements OnInit { export class FixedDepositProductTermsStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder); private formBuilder = inject(UntypedFormBuilder);
private dateUtils = inject(Dates);
private settingsService = inject(SettingsService);
@Input() fixedDepositProductsTemplate: any; @Input() fixedDepositProductsTemplate: any;
@@ -30,7 +34,13 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
interestCompoundingPeriodTypeData: any; interestCompoundingPeriodTypeData: any;
interestPostingPeriodTypeData: any; interestPostingPeriodTypeData: any;
interestCalculationTypeData: any; interestCalculationTypeData: any;
interestCalculationTypeOptionData: any;
interestCalculationDaysInYearTypeData: any; interestCalculationDaysInYearTypeData: any;
periodFrequencyData: any[] = [];
yesNoOptions: Array<{ value: boolean; label: string }> = [
{ value: true, label: 'Yes' },
{ value: false, label: 'No' }
];
constructor() { constructor() {
this.createFixedDepositProductTermsForm(); this.createFixedDepositProductTermsForm();
@@ -40,10 +50,22 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
this.interestCompoundingPeriodTypeData = this.fixedDepositProductsTemplate.interestCompoundingPeriodTypeOptions; this.interestCompoundingPeriodTypeData = this.fixedDepositProductsTemplate.interestCompoundingPeriodTypeOptions;
this.interestPostingPeriodTypeData = this.fixedDepositProductsTemplate.interestPostingPeriodTypeOptions; this.interestPostingPeriodTypeData = this.fixedDepositProductsTemplate.interestPostingPeriodTypeOptions;
this.interestCalculationTypeData = this.fixedDepositProductsTemplate.interestCalculationTypeOptions; this.interestCalculationTypeData = this.fixedDepositProductsTemplate.interestCalculationTypeOptions;
this.interestCalculationTypeOptionData = this.fixedDepositProductsTemplate.interestCalculationTypeOption || [];
this.interestCalculationDaysInYearTypeData = this.interestCalculationDaysInYearTypeData =
this.fixedDepositProductsTemplate.interestCalculationDaysInYearTypeOptions; 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({ this.fixedDepositProductTermsForm.patchValue({
minDepositAmount: this.fixedDepositProductsTemplate.minDepositAmount, minDepositAmount: this.fixedDepositProductsTemplate.minDepositAmount,
depositAmount: this.fixedDepositProductsTemplate.depositAmount, depositAmount: this.fixedDepositProductsTemplate.depositAmount,
@@ -52,13 +74,31 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
} }
this.fixedDepositProductTermsForm.patchValue({ this.fixedDepositProductTermsForm.patchValue({
interestCompoundingPeriodType: this.fixedDepositProductsTemplate.interestCompoundingPeriodType.id, nominalAnnualInterestRate: this.fixedDepositProductsTemplate.nominalAnnualInterestRate ?? 0,
interestPostingPeriodType: this.fixedDepositProductsTemplate.interestPostingPeriodType.id, interestCompoundingPeriodType: this.fixedDepositProductsTemplate.interestCompoundingPeriodType?.id,
interestCalculationType: this.fixedDepositProductsTemplate.interestCalculationType.id, interestPostingPeriodType: this.fixedDepositProductsTemplate.interestPostingPeriodType?.id,
interestCalculationDaysInYearType: this.fixedDepositProductsTemplate.interestCalculationDaysInYearType.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() { createFixedDepositProductTermsForm() {
this.fixedDepositProductTermsForm = this.formBuilder.group({ this.fixedDepositProductTermsForm = this.formBuilder.group({
minDepositAmount: [ minDepositAmount: [
@@ -76,6 +116,13 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
'', '',
Validators.min(0) Validators.min(0)
], ],
nominalAnnualInterestRate: [
'',
[
Validators.required,
Validators.min(0)
]
],
interestCompoundingPeriodType: [ interestCompoundingPeriodType: [
'', '',
Validators.required Validators.required
@@ -91,17 +138,34 @@ export class FixedDepositProductTermsStepComponent implements OnInit {
interestCalculationDaysInYearType: [ interestCalculationDaysInYearType: [
'', '',
Validators.required Validators.required
] ],
effectiveDate: [''],
interestCalculateSelector: [1],
averagePeriod: ['Monthly'],
autoPost: [true],
frequencyFixDay: [
30,
[Validators.min(0)]
],
lastInterestDate: ['']
}); });
} }
get fixedDepositProductTerms() { get fixedDepositProductTerms() {
const fixedDepositProductTerms = this.fixedDepositProductTermsForm.value; const formValue = { ...this.fixedDepositProductTermsForm.value };
for (const key in fixedDepositProductTerms) { const dateFormat = this.settingsService.dateFormat;
if (fixedDepositProductTerms[key] === '') {
delete fixedDepositProductTerms[key]; 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;
} }
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

+2 -2
View File
@@ -9,8 +9,8 @@
/* tslint:disable */ /* tslint:disable */
export default { export default {
'mifos_x': { 'mifos_x': {
'version': '260429', 'version': '260430',
'hash': '0d3d8379' 'hash': '2d4fb423'
}, },
'allow_switching_backend_instance': true 'allow_switching_backend_instance': true
}; };
+4 -2
View File
@@ -31,7 +31,8 @@
mifosx-loans-view, mifosx-loans-view,
mifosx-shares-account-view, mifosx-shares-account-view,
mifosx-recurring-deposits-account-view, mifosx-recurring-deposits-account-view,
mifosx-savings-account-view { mifosx-savings-account-view,
mifosx-current-account-account-view {
.account-card { .account-card {
margin: 0 auto; margin: 0 auto;
max-width: 90rem; max-width: 90rem;
@@ -98,7 +99,8 @@
} }
.content { .content {
.savings-account-tables { .savings-account-tables,
.current-account-tables {
padding: 1%; padding: 1%;
margin: 1%; margin: 1%;
+4 -2
View File
@@ -69,7 +69,8 @@
mifosx-loans-view, mifosx-loans-view,
mifosx-shares-account-view, mifosx-shares-account-view,
mifosx-recurring-deposits-account-view, mifosx-recurring-deposits-account-view,
mifosx-savings-account-view { mifosx-savings-account-view,
mifosx-current-account-account-view {
.account-card { .account-card {
margin: 0 auto; margin: 0 auto;
max-width: 90rem; max-width: 90rem;
@@ -137,7 +138,8 @@
} }
.content { .content {
.savings-account-tables { .savings-account-tables,
.current-account-tables {
padding: 1%; padding: 1%;
margin: 1%; margin: 1%;