update loan product code and rd,fd,saving,current account validation add

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Nitesh Yadav
2026-05-14 07:40:04 +05:30
co-authored by Claude Sonnet 4.6
parent ed45c8637e
commit d0370781e8
30 changed files with 1286 additions and 213 deletions
@@ -9,7 +9,7 @@
$primary: #1353a0;
.lp-container {
max-width: 960px;
max-width: 1200px;
margin: 0 auto;
padding: 24px 16px;
}
@@ -53,11 +53,11 @@ $primary: #1353a0;
}
.mat-mdc-tab-body {
overflow: visible !important;
overflow: hidden visible !important;
}
.mat-mdc-tab-body-content {
overflow: visible !important;
overflow: hidden visible !important;
}
}
}
@@ -69,6 +69,8 @@ $primary: #1353a0;
.lp-tab-content {
padding: 24px 16px;
min-width: 0;
overflow-x: hidden;
}
.lp-action-bar {
@@ -90,4 +90,34 @@
</mat-form-field>
</div>
</div>
<!-- Product Classification + GL Account — combined section -->
<div class="lpd-section">
<div class="lpd-section__header">
<i class="fas fa-sitemap"></i>
<h3>Product Classification</h3>
</div>
<div class="lpd-grid">
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>Product Type Group</mat-label>
<mat-select formControlName="productTypeGroup">
@for (group of productTypeGroups; track group.id) {
<mat-option [value]="group.id">{{ group.label }}</mat-option>
}
</mat-select>
</mat-form-field>
@if (loanProductDetailsForm.get('productTypeGroup')?.value) {
<mat-form-field appearance="outline" class="lpd-field">
<mat-label>GL Account ({{ glClassificationLabel }})</mat-label>
<mat-select formControlName="glAccount">
@for (acc of availableGlAccounts; track acc.id) {
<mat-option [value]="acc.id"> ({{ acc.glCode }}) {{ acc.name }} </mat-option>
}
</mat-select>
<mat-error>GL Account is required</mat-error>
</mat-form-field>
}
</div>
</div>
</form>
@@ -68,3 +68,140 @@ $gray-900: #202124;
align-items: center;
padding: 4px 0;
}
// GL Section
.lpd-gl-section {
border-top: 2px solid #e0f2fe;
}
.lpd-gl-subtype-badge {
margin-left: auto;
padding: 3px 12px;
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 20px;
font-size: 11.5px;
font-weight: 600;
color: #2563eb;
}
.lpd-gl-body {
padding: 16px 24px 20px;
}
.lpd-gl-hint {
font-size: 12.5px;
color: #64748b;
margin: 0 0 16px;
}
.lpd-gl-cards {
display: flex;
flex-direction: column;
gap: 10px;
}
.lpd-gl-card {
display: flex;
align-items: center;
gap: 16px;
padding: 14px 18px;
border: 2px solid #e2e8f0;
border-radius: 10px;
cursor: pointer;
background: #fff;
transition:
border-color 0.15s,
box-shadow 0.15s,
background 0.15s;
user-select: none;
&:hover {
border-color: #93c5fd;
background: #f8faff;
}
&--selected {
border-color: #2563eb !important;
background: #eff6ff !important;
box-shadow: 0 0 0 3px rgb(37 99 235 / 10%);
.lpd-gl-card__check {
opacity: 1;
color: #2563eb;
}
}
}
.lpd-gl-radio {
display: none;
}
.lpd-gl-card__icon {
width: 40px;
height: 40px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
&--asset {
background: #dcfce7;
color: #15803d;
}
&--liability {
background: #fee2e2;
color: #b91c1c;
}
}
.lpd-gl-card__info {
display: flex;
flex-direction: column;
gap: 2px;
flex: 1;
}
.lpd-gl-card__title {
font-size: 14px;
font-weight: 700;
color: #1e293b;
}
.lpd-gl-card__desc {
font-size: 12px;
color: #64748b;
}
.lpd-gl-card__check {
font-size: 18px;
opacity: 0;
transition: opacity 0.15s;
flex-shrink: 0;
}
.lpd-gl-account-field {
width: 100%;
max-width: 480px;
}
.lpd-gl-type-badge {
padding: 3px 12px;
border-radius: 20px;
font-size: 11.5px;
font-weight: 700;
margin-left: 6px;
&--asset {
background: #dcfce7;
color: #15803d;
}
&--liability {
background: #fee2e2;
color: #b91c1c;
}
}
@@ -57,6 +57,68 @@ export class LoanProductDetailsStepComponent implements OnInit {
startDate: this.loanProductsTemplate.startDate && new Date(this.loanProductsTemplate.startDate),
closeDate: this.loanProductsTemplate.closeDate && new Date(this.loanProductsTemplate.closeDate)
});
const mappingOptions = this.loanProductsTemplate?.accountingMappingOptions ?? {};
this.assetAccountOptions = mappingOptions.assetAccountOptions ?? [];
this.liabilityAccountOptions = mappingOptions.liabilityAccountOptions ?? [];
}
/** Product Classification data */
readonly productTypeGroups = [
{ id: 'loans', label: 'Loan' },
{ id: 'advanced', label: 'Advanced' }
];
readonly productTypeSubGroups: Record<string, { id: string; label: string }[]> = {
loans: [
{ id: 'emi', label: 'EMI' },
{ id: 'cc', label: 'CC (Cash Credit)' },
{ id: 'overdraft', label: 'Overdraft' },
{ id: 'warehouse', label: 'Warehouse' },
{ id: 'gold', label: 'Gold' }
],
advanced: [
{ id: 'fd', label: 'FD (Fixed Deposit)' },
{ id: 'rd', label: 'RD (Recurring Deposit)' }
]
};
/** Default GL per product type group */
private readonly glDefaultMap: Record<string, string> = {
loans: 'asset',
advanced: 'asset'
};
get availableSubGroups() {
const group = this.loanProductDetailsForm.get('productTypeGroup')?.value;
return group ? (this.productTypeSubGroups[group] ?? []) : [];
}
get glOptions(): string[] {
return [
'asset',
'liability'
];
}
assetAccountOptions: any[] = [];
liabilityAccountOptions: any[] = [];
get availableGlAccounts(): any[] {
const type = this.loanProductDetailsForm.get('glClassification')?.value;
if (type === 'asset') return this.assetAccountOptions;
if (type === 'liability') return this.liabilityAccountOptions;
return [];
}
get glClassificationLabel(): string {
const type = this.loanProductDetailsForm.get('glClassification')?.value;
return type === 'liability' ? 'Liability' : 'Asset';
}
get selectedGroupLabel(): string {
const group = this.loanProductDetailsForm.get('productTypeGroup')?.value;
return this.productTypeGroups.find((g) => g.id === group)?.label ?? '';
}
createLoanProductDetailsForm() {
@@ -71,7 +133,18 @@ export class LoanProductDetailsStepComponent implements OnInit {
],
description: [''],
startDate: [''],
closeDate: ['']
closeDate: [''],
productTypeGroup: [''],
productTypeSubGroup: [''],
glClassification: ['asset'],
glAccount: ['']
});
// Auto-set GL based on product type group
this.loanProductDetailsForm.get('productTypeGroup')?.valueChanges.subscribe((group: string) => {
const defaultVal = this.glDefaultMap[group] ?? 'asset';
this.loanProductDetailsForm.get('glClassification')?.setValue(defaultVal);
this.loanProductDetailsForm.get('glAccount')?.setValue('');
});
}
@@ -467,7 +467,7 @@
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Against Interest Calculation Enabled</mat-label>
<mat-select formControlName="againstInterestCalculationEnabled">
<mat-option value="YES">YES</mat-option>
@@ -499,7 +499,7 @@
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-3">
<mat-label>Consider Debit Closing Amount For Against Clearing</mat-label>
<mat-select formControlName="considerDebitClosingAmountForAgainstClearing">
<mat-option value="YES">YES</mat-option>
@@ -517,7 +517,7 @@
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Penalty Interest Calculation Enabled</mat-label>
<mat-select formControlName="penaltyInterestCalculationEnabled">
<mat-option value="YES">YES</mat-option>
@@ -525,7 +525,7 @@
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Penalty Interest Calculation Method</mat-label>
<mat-select formControlName="penaltyInterestCalculationMethod">
<mat-option value="Daywise ( 36500 )">Daywise ( 36500 )</mat-option>
@@ -559,7 +559,7 @@
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Interest On Credit Balance</mat-label>
<mat-select formControlName="interestOnCreditBalance">
<mat-option value="YES">YES</mat-option>
@@ -567,7 +567,7 @@
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Penalty Interest Date Different Than Regular Interest</mat-label>
<mat-select formControlName="penaltyInterestDateDifferentThanRegularInterest">
<mat-option value="YES">YES</mat-option>
@@ -575,7 +575,7 @@
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Fixed Deposit Overdraft Certificate Sort Order</mat-label>
<mat-select formControlName="fixedDepositOverdraftCertificateSortOrder">
<mat-option value="ORDER ON INTEREST RATE">Order on Interest Rate</mat-option>
@@ -599,12 +599,12 @@
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Minimum Normal Interest Amount</mat-label>
<input type="number" matInput formControlName="minimumNormalInterestAmount" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Minimum Interest Calculation Method</mat-label>
<mat-select formControlName="minimumInterestCalculationMethod">
<mat-option value="Flat Amount">Flat Amount</mat-option>
@@ -613,12 +613,12 @@
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Minimum Against Clearing Interest Amount</mat-label>
<input type="number" matInput formControlName="minimumAgainstClearingInterestAmount" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Limit Increase Upto Interest Amount In Interest Calculation</mat-label>
<mat-select formControlName="limitIncreaseUptoInterestAmountInInterestCalculation">
<mat-option value="YES">YES</mat-option>
@@ -626,22 +626,22 @@
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Minimum Penalty Interest Amount</mat-label>
<input type="number" matInput formControlName="minimumPenaltyInterestAmount" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Days For Limit Increase Upto Interest Amount</mat-label>
<input type="number" matInput formControlName="daysForLimitIncreaseUptoInterestAmount" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Additional Interest Rate For Fixed Deposit Overdraft</mat-label>
<input type="number" matInput formControlName="additionalInterestRateForFixedDepositOverdraft" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Consider DD/Payorder In Against Clearing Interest Calculation</mat-label>
<mat-select formControlName="considerDdPayorderInAgainstClearingInterestCalculation">
<mat-option value="YES">YES</mat-option>
@@ -649,12 +649,12 @@
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Grace Days After Renewal Date</mat-label>
<input type="number" matInput formControlName="graceDaysAfterRenewDate" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Apply Undebited Interest Concept Enabled</mat-label>
<mat-select formControlName="applyUndebitedInterestConceptEnabled">
<mat-option value="YES">YES</mat-option>
@@ -671,12 +671,12 @@
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Normal Grace Days After Overdue</mat-label>
<input type="number" matInput formControlName="normalGraceDaysAfterOverdue" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Allow Against Clearing Entry</mat-label>
<mat-select formControlName="allowAgainstClearingEntry">
<mat-option value="YES">YES</mat-option>
@@ -684,7 +684,7 @@
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Allow No Balance Entry</mat-label>
<mat-select formControlName="allowNoBalanceEntry">
<mat-option value="YES">YES</mat-option>
@@ -692,7 +692,7 @@
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Fixed Deposit Overdraft Penalty Interest Combined With Normal Interest</mat-label>
<mat-select formControlName="fixedDepositOverdraftPenaltyInterestCombinedWithNormalInterest">
<mat-option value="YES">YES</mat-option>
@@ -700,7 +700,7 @@
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Allow No Balance Entry With Password</mat-label>
<mat-select formControlName="allowNoBalanceEntryWithPassword">
<mat-option value="YES">YES</mat-option>
@@ -708,7 +708,7 @@
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Undebited Simple Interest Account Wise</mat-label>
<mat-select formControlName="undebitedSimpleInterestAccountWise">
<mat-option value="YES">YES</mat-option>
@@ -716,7 +716,7 @@
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Daily Interest Calculation Enabled</mat-label>
<mat-select formControlName="dailyInterestCalculationEnabled">
<mat-option value="YES">YES</mat-option>
@@ -724,12 +724,12 @@
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Daily Interest Calculation Start Date</mat-label>
<input type="text" matInput formControlName="dailyInterestCalculationStartDate" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Non Performing Asset Penal Interest Rate Update</mat-label>
<mat-select formControlName="nonPerformingAssetPenalInterestRateUpdate">
<mat-option value="YES">YES</mat-option>
@@ -737,12 +737,12 @@
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Non Performing Asset Penal Interest Rate</mat-label>
<input type="number" matInput formControlName="nonPerformingAssetPenalInterestRate" />
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Fixed Deposit Overdraft Penal Interest Rate Update In Branch Process</mat-label>
<mat-select formControlName="fixedDepositOverdraftPenalInterestRateUpdateInBranchProcess">
<mat-option value="YES">YES</mat-option>
@@ -750,13 +750,116 @@
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Add Penal Interest Rate</mat-label>
<input type="number" matInput formControlName="addPenalInterestRate" />
</mat-form-field>
</div>
</div>
<!-- RBI Compliance & KFS Settings -->
<div class="lps-section">
<div class="lps-section__header">
<i class="fas fa-balance-scale lps-section__icon"></i>
<h3 class="lps-section__title">RBI Compliance &amp; KFS Settings</h3>
</div>
<div class="lps-section__body lps-grid-4">
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Penal Charge Method</mat-label>
<mat-select formControlName="penalChargeMethod">
<mat-option value="RBI COMPLIANT PENAL CHARGES">RBI Compliant Penal Charges (Non-Compounding)</mat-option>
<mat-option value="PENAL INTEREST">Legacy Penal Interest (Compounding)</mat-option>
</mat-select>
</mat-form-field>
<div class="lps-checkbox-row lps-span-2">
<mat-checkbox labelPosition="before" formControlName="isFloatingRate">
Is Floating Rate Loan? (Individual)
</mat-checkbox>
</div>
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Foreclosure Charges Applicable</mat-label>
<mat-select formControlName="foreclosureChargesApplicable">
<mat-option value="YES">YES</mat-option>
<mat-option value="NO">NO</mat-option>
</mat-select>
<mat-hint>RBI: No charges for Floating Rate Loans to Individuals</mat-hint>
</mat-form-field>
<div class="lps-checkbox-row">
<mat-checkbox labelPosition="before" formControlName="keyFactStatementEnabled">
Enable Key Fact Statement (KFS)
</mat-checkbox>
</div>
<div class="lps-checkbox-row">
<mat-checkbox labelPosition="before" formControlName="annualPercentageRateCalculationEnabled">
Calculate APR
</mat-checkbox>
</div>
</div>
</div>
<!-- Credit Bureau Settings (CIBIL) -->
<div class="lps-section">
<div class="lps-section__header">
<i class="fas fa-file-invoice lps-section__icon"></i>
<h3 class="lps-section__title">Credit Bureau Settings (CIBIL/Highmark)</h3>
</div>
<div class="lps-section__body lps-grid-4">
<mat-form-field appearance="outline" class="lps-full-width">
<mat-label>Loan Product Category (RBI Reporting)</mat-label>
<mat-select formControlName="loanProductCategory">
<mat-option value="PERSONAL_LOAN">Personal/Consumer Loan (Code 02)</mat-option>
<mat-option value="HOUSING_LOAN">Housing/Mortgage Loan (Code 15)</mat-option>
<mat-option value="BUSINESS_LOAN">Business/Commercial Loan (Code 50)</mat-option>
<mat-option value="VEHICLE_LOAN">Vehicle/Auto Loan (Code 04)</mat-option>
</mat-select>
<mat-hint>Selecting a category will auto-fill the credit bureau account types below.</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>CIBIL Account Type</mat-label>
<input type="text" matInput formControlName="cibilAccountType" placeholder="e.g. 02" />
</mat-form-field>
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>CIBIL Commercial Account Type</mat-label>
<input type="text" matInput formControlName="cibilCommercialAccountType" placeholder="e.g. 0420" />
</mat-form-field>
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Highmark Account Type</mat-label>
<input type="text" matInput formControlName="highmarkAccountType" placeholder="e.g. 02" />
</mat-form-field>
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Highmark Commercial Account Type</mat-label>
<input type="text" matInput formControlName="highmarkCommercialAccountType" placeholder="e.g. 0420" />
</mat-form-field>
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Experian Account Type</mat-label>
<input type="text" matInput formControlName="experianAccountType" placeholder="e.g. 02" />
</mat-form-field>
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Experian Commercial Account Type</mat-label>
<input type="text" matInput formControlName="experianCommercialAccountType" placeholder="e.g. 0420" />
</mat-form-field>
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Equifax Account Type</mat-label>
<input type="text" matInput formControlName="equifaxAccountType" placeholder="e.g. 02" />
</mat-form-field>
<mat-form-field appearance="outline" class="lps-span-2">
<mat-label>Equifax Commercial Account Type</mat-label>
<input type="text" matInput formControlName="equifaxCommercialAccountType" placeholder="e.g. 0420" />
</mat-form-field>
</div>
</div>
<!-- Moratorium & Grace -->
<div class="lps-section">
<div class="lps-section__header">
@@ -75,6 +75,14 @@
grid-column: 1 / -1;
}
.lps-span-2 {
grid-column: span 2;
}
.lps-span-3 {
grid-column: span 3;
}
.lps-checkbox-row {
display: flex;
align-items: center;
@@ -408,13 +408,68 @@ export class LoanProductSettingsStepComponent implements OnInit {
nonPerformingAssetPenalInterestRateUpdate: ['YES'],
nonPerformingAssetPenalInterestRate: [''],
fixedDepositOverdraftPenalInterestRateUpdateInBranchProcess: ['YES'],
addPenalInterestRate: ['']
addPenalInterestRate: [''],
// Credit Bureau Settings (CIBIL)
cibilAccountType: [''],
cibilCommercialAccountType: [''],
highmarkAccountType: [''],
highmarkCommercialAccountType: [''],
experianAccountType: [''],
experianCommercialAccountType: [''],
equifaxAccountType: [''],
equifaxCommercialAccountType: [''],
// RBI Compliance & Product Category
loanProductCategory: [''],
penalChargeMethod: ['RBI COMPLIANT PENAL CHARGES'],
isFloatingRate: [false],
foreclosureChargesApplicable: ['NO'],
keyFactStatementEnabled: [true],
annualPercentageRateCalculationEnabled: [true]
});
}
private autoFillCibilFields(category: string) {
const cibilMappings: any = {
PERSONAL_LOAN: { ac: '02', comm: '0420' },
HOUSING_LOAN: { ac: '15', comm: '9004' },
BUSINESS_LOAN: { ac: '50', comm: '0100' },
VEHICLE_LOAN: { ac: '04', comm: '9004' }
};
const mapping = cibilMappings[category];
if (mapping) {
this.loanProductSettingsForm.patchValue({
cibilAccountType: mapping.ac,
cibilCommercialAccountType: mapping.comm,
highmarkAccountType: mapping.ac,
highmarkCommercialAccountType: mapping.comm,
experianAccountType: mapping.ac,
experianCommercialAccountType: mapping.comm,
equifaxAccountType: mapping.ac,
equifaxCommercialAccountType: mapping.comm
});
}
}
setConditionalControls() {
const allowAttributeOverrides = this.loanProductSettingsForm.get('allowAttributeOverrides');
// RBI & CIBIL Category Listener
this.loanProductSettingsForm.get('loanProductCategory').valueChanges.subscribe((category: string) => {
if (category) {
this.autoFillCibilFields(category);
}
});
// Floating Rate Logic (RBI: No foreclosure charges for floating rate loans to individuals)
this.loanProductSettingsForm.get('isFloatingRate').valueChanges.subscribe((isFloating: boolean) => {
if (isFloating) {
this.loanProductSettingsForm.patchValue({ foreclosureChargesApplicable: 'NO' });
}
});
this.loanProductSettingsForm.get('daysInYearType').valueChanges.subscribe((daysInYearType: any) => {
if (this.isAdvancedTransactionProcessingStrategy) {
this.useDaysInYearCustomStrategy = daysInYearType == 1;
@@ -264,6 +264,93 @@
}
</div>
<!-- Interest Rate Slabs Section -->
<div class="lpt-section">
<div class="lpt-section-header">
<i class="fas fa-table lpt-section-icon"></i>
<h3>Interest Rate Slabs</h3>
</div>
<div class="lpt-slab-c">
<!-- Particular Name -->
<div class="lpt-slab-c-particular">
<label class="lpt-slab-c-particular-label">Particular</label>
<input
type="text"
class="lpt-slab-c-name-input"
[formControl]="particularLabel"
placeholder="e.g. Consumer Loan"
/>
</div>
<!-- 2-column slab grid (same as before, General + Special inside each card) -->
<div class="lpt-slab-c-grid">
@for (genGroup of slabGroups; track $index) {
<div class="lpt-slab-c-item">
<span class="lpt-slab-c-range">{{ interestSlabDefinitions[$index].label }}</span>
<div class="lpt-slab-c-rate-pair">
<!-- General -->
<div class="lpt-slab-c-rate-field" [formGroup]="genGroup">
<span class="lpt-slab-c-rate-label">Gen</span>
<div class="lpt-slab-c-input-wrap">
<input
type="number"
class="lpt-slab-c-input"
formControlName="interestRate"
placeholder="0.00"
min="0"
max="100"
step="0.01"
/>
<span class="lpt-slab-c-suffix">%</span>
</div>
</div>
<!-- Special -->
<div class="lpt-slab-c-rate-field" [formGroup]="specialSlabGroups[$index]">
<span class="lpt-slab-c-rate-label lpt-slab-c-rate-label--special">Spl</span>
<div class="lpt-slab-c-input-wrap">
<input
type="number"
class="lpt-slab-c-input lpt-slab-c-input--special"
formControlName="interestRate"
placeholder="0.00"
min="0"
max="100"
step="0.01"
/>
<span class="lpt-slab-c-suffix">%</span>
</div>
</div>
</div>
</div>
}
</div>
<!-- Fixed Interest Rate row -->
<div class="lpt-slab-c-fixed" [formGroup]="fixedRateGroup">
<label class="lpt-slab-c-fixed-toggle">
<input type="checkbox" formControlName="hasFixed" class="lpt-slab-c-checkbox" />
<i class="fas fa-lock"></i>
<span>Fixed Interest Rate (%)</span>
</label>
@if (fixedRateGroup.get('hasFixed').value) {
<div class="lpt-slab-c-input-wrap">
<input
type="number"
class="lpt-slab-c-input lpt-slab-c-input--fixed"
formControlName="fixedAmount"
placeholder="0.00"
min="0"
max="100"
step="0.01"
/>
<span class="lpt-slab-c-suffix">%</span>
</div>
}
</div>
</div>
</div>
<!-- Repayment Schedule Section -->
<div class="lpt-section">
<div class="lpt-section-header">
@@ -6,14 +6,42 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
// ─── Design tokens ────────────────────────────────────────────────────────────
$primary: #2563eb;
$primary-light: #eff6ff;
$primary-border: #bfdbfe;
$surface: #fff;
$surface-alt: #f8fafc;
$border: #e2e8f0;
$text-primary: #1e293b;
$text-secondary: #64748b;
$text-label: #475569;
$accent-green: #059669;
$accent-orange: #d97706;
$radius-card: 12px;
$radius-inner: 8px;
$shadow-card:
0 1px 3px rgb(0 0 0 / 7%),
0 1px 2px rgb(0 0 0 / 4%);
$shadow-hover: 0 4px 12px rgb(37 99 235 / 10%);
// ─── Card / Section ───────────────────────────────────────────────────────────
.lpt-section {
border: 1px solid #e2e8f0;
border-radius: 10px;
overflow: hidden;
background: #fff;
background: $surface;
border: 1px solid $border;
border-radius: $radius-card;
box-shadow: $shadow-card;
// No overflow:hidden here — it clips right-side content inside grids/tables.
// Border-radius is preserved; header gets matching top-radius instead.
overflow: visible;
transition: box-shadow 0.2s ease;
&:hover {
box-shadow: $shadow-hover;
}
& + & {
margin-top: 16px;
margin-top: 20px;
}
}
@@ -21,34 +49,52 @@
display: flex;
align-items: center;
gap: 10px;
padding: 12px 24px;
background: #f8f9fa;
border-bottom: 1px solid #e8eaed;
padding: 14px 24px;
background: linear-gradient(135deg, $surface-alt 0%, #f1f5f9 100%);
border-bottom: 1px solid $border;
border-radius: $radius-card $radius-card 0 0;
h3 {
font-size: 14px;
font-weight: 600;
color: #202124;
font-weight: 700;
color: $text-primary;
margin: 0;
letter-spacing: 0.01em;
text-transform: uppercase;
}
}
.lpt-section-icon {
color: #0ea5e9;
color: $primary;
font-size: 15px;
width: 28px;
height: 28px;
display: inline-flex;
align-items: center;
justify-content: center;
background: $primary-light;
border-radius: 6px;
border: 1px solid $primary-border;
}
.lpt-help-icon {
color: #9aa0a6;
color: $text-secondary;
font-size: 13px;
cursor: help;
margin-left: auto;
transition: color 0.15s;
&:hover {
color: $primary;
}
}
// ─── Grid layout ──────────────────────────────────────────────────────────────
.lpt-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
gap: 8px 20px;
padding: 20px 24px;
@media (width <= 600px) {
grid-template-columns: 1fr;
@@ -58,6 +104,10 @@
.lpt-grid-3 {
grid-template-columns: repeat(3, 1fr);
@media (width <= 768px) {
grid-template-columns: repeat(2, 1fr);
}
@media (width <= 600px) {
grid-template-columns: 1fr;
}
@@ -79,28 +129,42 @@
grid-column: 1 / -1;
}
// ─── Checkbox rows ────────────────────────────────────────────────────────────
.lpt-checkbox-row {
display: flex;
align-items: center;
padding: 4px 0;
}
padding: 6px 0;
.lpt-subsection {
padding: 0 24px 16px;
&:first-of-type {
padding-top: 0;
mat-checkbox {
font-size: 13.5px;
color: $text-label;
}
}
// ─── Subsection ───────────────────────────────────────────────────────────────
.lpt-subsection {
padding: 0 24px 20px;
}
.lpt-subsection-title {
font-size: 13px;
font-weight: 600;
color: #3c4043;
margin: 8px 0 12px;
font-size: 12.5px;
font-weight: 700;
color: $text-secondary;
text-transform: uppercase;
letter-spacing: 0.05em;
margin: 12px 0;
display: flex;
align-items: center;
gap: 8px;
&::before {
content: '';
display: inline-block;
width: 3px;
height: 14px;
background: $primary;
border-radius: 2px;
}
}
.lpt-subsection-header {
@@ -114,9 +178,10 @@
}
}
// ─── Misc ─────────────────────────────────────────────────────────────────────
.lpt-table-wrapper {
border: 1px solid #e2e8f0;
border-radius: 8px;
border: 1px solid $border;
border-radius: $radius-inner;
overflow: hidden;
margin-bottom: 16px;
@@ -128,8 +193,265 @@
.lpt-label-field {
display: flex;
align-items: center;
font-size: 14px;
font-size: 13.5px;
font-weight: 500;
color: #3c4043;
padding: 8px 0;
color: $text-label;
padding: 8px 4px;
}
// ─── Interest Rate Slabs — Option C (2-column grid) ──────────────────────────
.lpt-slab-c {
padding: 16px 24px 20px;
display: flex;
flex-direction: column;
gap: 0;
}
// Particular row
.lpt-slab-c-particular {
display: flex;
align-items: center;
gap: 14px;
padding-bottom: 14px;
border-bottom: 1px solid $border;
margin-bottom: 12px;
flex-wrap: wrap;
}
.lpt-slab-c-particular-label {
font-size: 12px;
font-weight: 700;
color: $text-secondary;
text-transform: uppercase;
letter-spacing: 0.05em;
white-space: nowrap;
}
.lpt-slab-c-name-input {
flex: 1;
max-width: 320px;
padding: 7px 12px;
border: 1px solid $border;
border-radius: 8px;
font-size: 13.5px;
font-weight: 600;
color: $text-primary;
background: $surface;
outline: none;
transition:
border-color 0.15s,
box-shadow 0.15s;
&::placeholder {
color: #b0b8c4;
font-weight: 400;
}
&:focus {
border-color: $primary;
box-shadow: 0 0 0 3px rgb(37 99 235 / 10%);
}
}
// General / Special tab toggle
.lpt-slab-tabs {
display: flex;
gap: 6px;
margin-left: auto;
}
.lpt-slab-tab {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 6px 16px;
border: 1.5px solid $border;
border-radius: 20px;
background: $surface;
font-size: 12.5px;
font-weight: 600;
color: $text-secondary;
cursor: pointer;
transition: all 0.15s;
i {
font-size: 11px;
}
&:hover {
border-color: $primary;
color: $primary;
}
&--active {
background: $primary;
border-color: $primary;
color: $surface;
box-shadow: 0 2px 8px rgb(37 99 235 / 25%);
}
}
// 2-column slab grid
.lpt-slab-c-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px 16px;
@media (width <= 540px) {
grid-template-columns: 1fr;
}
}
.lpt-slab-c-item {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
padding: 8px 12px;
border: 1px solid $border;
border-radius: 8px;
background: $surface;
transition:
border-color 0.15s,
box-shadow 0.15s;
&:focus-within {
border-color: $primary;
box-shadow: 0 0 0 3px rgb(37 99 235 / 8%);
}
}
// General + Special pair inside each card
.lpt-slab-c-rate-pair {
display: flex;
gap: 8px;
align-items: center;
}
.lpt-slab-c-rate-field {
display: flex;
align-items: center;
gap: 4px;
}
.lpt-slab-c-rate-label {
font-size: 10px;
font-weight: 700;
color: $primary;
background: #eff6ff;
padding: 2px 5px;
border-radius: 4px;
white-space: nowrap;
&--special {
color: #92400e;
background: #fef9c3;
}
}
.lpt-slab-c-range {
font-size: 12.5px;
font-weight: 500;
color: $text-label;
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
// Input + suffix wrapper
.lpt-slab-c-input-wrap {
display: flex;
align-items: center;
gap: 4px;
flex-shrink: 0;
}
.lpt-slab-c-input {
width: 70px;
padding: 4px 6px;
border: 1px solid $border;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
color: $text-primary;
text-align: right;
background: $surface-alt;
outline: none;
transition: border-color 0.15s;
appearance: textfield;
&::-webkit-outer-spin-button,
&::-webkit-inner-spin-button {
appearance: none;
}
&:focus {
border-color: $primary;
background: $primary-light;
}
&--fixed {
width: 90px;
}
}
.lpt-slab-c-suffix {
font-size: 12px;
font-weight: 700;
color: $text-secondary;
user-select: none;
&--special {
color: #92400e;
}
}
.lpt-slab-c-input--special {
&:focus {
border-color: #f59e0b;
background: #fef9c3;
}
}
.lpt-slab-c-error {
font-size: 10px;
color: #ef4444;
}
// Fixed Interest Rate row (below grid)
.lpt-slab-c-fixed {
display: flex;
align-items: center;
gap: 20px;
margin: 12px -24px -20px;
padding: 12px 16px;
border-top: 2px dashed $primary-border;
background: $primary-light;
border-radius: 0 0 $radius-card $radius-card;
padding-left: 24px;
padding-right: 24px;
}
.lpt-slab-c-fixed-toggle {
display: flex;
align-items: center;
gap: 8px;
cursor: pointer;
font-size: 13px;
font-weight: 600;
color: $primary;
user-select: none;
i {
font-size: 12px;
opacity: 0.7;
}
}
.lpt-slab-c-checkbox {
accent-color: $primary;
width: 15px;
height: 15px;
cursor: pointer;
}
@@ -12,6 +12,9 @@ import {
UntypedFormBuilder,
Validators,
UntypedFormControl,
FormArray,
FormGroup,
FormControl,
ReactiveFormsModule
} from '@angular/forms';
import { ProcessingStrategyService } from '../../services/processing-strategy.service';
@@ -54,6 +57,25 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
repaymentFrequencyTypeData: any;
repaymentStartDateTypeOptions: any;
readonly interestSlabDefinitions = [
{ label: 'Upto 10,000', maxAmount: 10000 },
{ label: 'Upto 25,000', maxAmount: 25000 },
{ label: 'Upto 1,00,000', maxAmount: 100000 },
{ label: 'Upto 2,00,000', maxAmount: 200000 },
{ label: 'Upto 5,00,000', maxAmount: 500000 },
{ label: 'Upto 10,00,000', maxAmount: 1000000 },
{ label: 'Upto 15,00,000', maxAmount: 1500000 },
{ label: 'Upto 20,00,000', maxAmount: 2000000 },
{ label: 'Upto 25,00,000', maxAmount: 2500000 },
{ label: 'More than 25,00,000', maxAmount: null }
];
interestRateSlabsArray: FormArray;
specialSlabsArray: FormArray;
fixedRateGroup: FormGroup;
particularLabel = new FormControl('');
activeSlabTab: 'general' | 'special' = 'general';
constructor() {
this.createLoanProductTermsForm();
this.setConditionalControls();
@@ -109,6 +131,35 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
}
createLoanProductTermsForm() {
this.interestRateSlabsArray = new FormArray(
this.interestSlabDefinitions.map(
() =>
new FormGroup({
interestRate: new FormControl(null, [
Validators.min(0),
Validators.max(100)
])
})
)
);
this.specialSlabsArray = new FormArray(
this.interestSlabDefinitions.map(
() =>
new FormGroup({
interestRate: new FormControl(null, [
Validators.min(0),
Validators.max(100)
])
})
)
);
this.fixedRateGroup = new FormGroup({
hasFixed: new FormControl(false),
fixedAmount: new FormControl({ value: null, disabled: true }, [Validators.min(0)])
});
this.loanProductTermsForm = this.formBuilder.group({
isLinkedToFloatingInterestRates: [false],
allowApprovedDisbursedAmountsOverApplied: [false],
@@ -230,6 +281,15 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
}
});
this.fixedRateGroup.get('hasFixed').valueChanges.subscribe((hasFixed: boolean) => {
if (hasFixed) {
this.fixedRateGroup.get('fixedAmount').enable();
} else {
this.fixedRateGroup.get('fixedAmount').disable();
this.fixedRateGroup.get('fixedAmount').setValue(null);
}
});
this.zeroInterest.valueChanges.subscribe((zeroInterest) => {
if (zeroInterest) {
this.loanProductTermsForm.get('minInterestRatePerPeriod').patchValue(0);
@@ -256,6 +316,18 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
});
}
get slabGroups(): FormGroup[] {
return this.interestRateSlabsArray.controls as FormGroup[];
}
get activeSlabGroups(): FormGroup[] {
return this.interestRateSlabsArray.controls as FormGroup[];
}
get specialSlabGroups(): FormGroup[] {
return this.specialSlabsArray.controls as FormGroup[];
}
get loanProductTerms() {
const formValue = this.loanProductTermsForm.getRawValue();
// Normalize decimal separators: convert comma to dot for backend compatibility
@@ -270,7 +342,16 @@ export class LoanProductTermsStepComponent implements OnInit, OnChanges {
...formValue,
minInterestRatePerPeriod: normalizeDecimal(formValue.minInterestRatePerPeriod),
interestRatePerPeriod: normalizeDecimal(formValue.interestRatePerPeriod),
maxInterestRatePerPeriod: normalizeDecimal(formValue.maxInterestRatePerPeriod)
maxInterestRatePerPeriod: normalizeDecimal(formValue.maxInterestRatePerPeriod),
interestRateSlabs: this.interestRateSlabsArray.getRawValue().map((slab, i) => ({
maxAmount: this.interestSlabDefinitions[i].maxAmount,
label: this.interestSlabDefinitions[i].label,
interestRate: slab.interestRate
})),
fixedRate: (() => {
const fr = this.fixedRateGroup.getRawValue();
return fr.hasFixed ? fr.fixedAmount : null;
})()
};
}
+18
View File
@@ -564,6 +564,24 @@ export class ProductsService {
return this.http.post('/fixeddepositproducts/baseinterestrates', payload);
}
/** Returns loan interest rate categories. Pass activeOnly=true to get only active ones. */
getLoanInterestRateCategories(activeOnly = false): Observable<any> {
const url = activeOnly
? '/loanproducts/interestratecategories?activeOnly=true'
: '/loanproducts/interestratecategories';
return this.http.get(url);
}
/** Returns the active loan interest rate slab config (grid: loan type x amount range). */
getLoanIrSlabs(): Observable<any> {
return this.http.get('/loanproducts/interestrateslabs');
}
/** Returns all loan interest rate slab config versions. */
getAllLoanIrSlabs(): Observable<any> {
return this.http.get('/loanproducts/interestrateslabs/all');
}
/**
* @returns {Observable<any>} Collateral data.
*/