feat: edit client — save per tab, DOB re-KYC alert, audit logs, KYC improvements
- Edit button per tab with Save & Next confirmation dialog - DOB change triggers re-KYC alert, resets PAN verification, auto-fills remarks - Mobile/email changes logged as audit notes via client notes API - Redesigned confirmation dialog with icon + title, type-based theming - KYC re-edit flow: remarks mandatory for VERIFIED records, re-edit API command - Load existing photo & signature in edit KYC, immediate upload on change - Populate all KYC fields (document, AML, form60/15) from API on edit - Non-individual PAN verification uses same flow as individual - Fix table overflow when sidebar panel is open - Add fr unit to stylelint allowed-list Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
b890d24bb7
commit
79e455c4ed
+2
-1
@@ -67,7 +67,8 @@
|
|||||||
"vh",
|
"vh",
|
||||||
"deg",
|
"deg",
|
||||||
"s",
|
"s",
|
||||||
"ms"
|
"ms",
|
||||||
|
"fr"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
<!--
|
<!--
|
||||||
Copyright since 2025 Mifos Initiative
|
Copyright since 2025 Mifos Initiative
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<div class="mat-typography" [formGroup]="kycForm">
|
<div class="mat-typography" [formGroup]="kycForm">
|
||||||
@@ -22,7 +26,6 @@
|
|||||||
</h4>
|
</h4>
|
||||||
|
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
|
|
||||||
<!-- ══ INDIVIDUAL KYC ══ -->
|
<!-- ══ INDIVIDUAL KYC ══ -->
|
||||||
@if (!isNonIndividual) {
|
@if (!isNonIndividual) {
|
||||||
<div class="form-field checkbox-field full-width">
|
<div class="form-field checkbox-field full-width">
|
||||||
@@ -35,9 +38,16 @@
|
|||||||
<mat-form-field appearance="outline" class="verify-input">
|
<mat-form-field appearance="outline" class="verify-input">
|
||||||
<mat-label>PAN Number</mat-label>
|
<mat-label>PAN Number</mat-label>
|
||||||
<span matPrefix class="field-prefix-icon"><mat-icon>credit_card</mat-icon></span>
|
<span matPrefix class="field-prefix-icon"><mat-icon>credit_card</mat-icon></span>
|
||||||
<input matInput formControlName="panNumber" placeholder="e.g. ABCDE1234F"
|
<input
|
||||||
maxlength="10" style="text-transform: uppercase"
|
matInput
|
||||||
(input)="kycForm.get('panNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })" />
|
formControlName="panNumber"
|
||||||
|
placeholder="e.g. ABCDE1234F"
|
||||||
|
maxlength="10"
|
||||||
|
style="text-transform: uppercase"
|
||||||
|
(input)="
|
||||||
|
kycForm.get('panNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
|
||||||
|
"
|
||||||
|
/>
|
||||||
@if (kycForm.get('panNumber')?.hasError('pattern') && kycForm.get('panNumber')?.touched) {
|
@if (kycForm.get('panNumber')?.hasError('pattern') && kycForm.get('panNumber')?.touched) {
|
||||||
<mat-error>Invalid PAN format</mat-error>
|
<mat-error>Invalid PAN format</mat-error>
|
||||||
}
|
}
|
||||||
@@ -51,9 +61,16 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<div class="verify-action">
|
<div class="verify-action">
|
||||||
@if (panVerifyStatus === 'NONE' || panVerifyStatus === 'FAILED') {
|
@if (panVerifyStatus === 'NONE' || panVerifyStatus === 'FAILED') {
|
||||||
<button mat-raised-button color="primary" type="button" class="verify-btn-inline"
|
<button
|
||||||
[disabled]="readOnly || !kycForm.get('panNumber')?.value || kycForm.get('panNumber')?.invalid || panVerifying"
|
mat-raised-button
|
||||||
(click)="verifyPan()">
|
color="primary"
|
||||||
|
type="button"
|
||||||
|
class="verify-btn-inline"
|
||||||
|
[disabled]="
|
||||||
|
readOnly || !kycForm.get('panNumber')?.value || kycForm.get('panNumber')?.invalid || panVerifying
|
||||||
|
"
|
||||||
|
(click)="verifyPan()"
|
||||||
|
>
|
||||||
@if (panVerifying) {
|
@if (panVerifying) {
|
||||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||||||
} @else {
|
} @else {
|
||||||
@@ -79,10 +96,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="pan-result-row">
|
<div class="pan-result-row">
|
||||||
<span class="pan-result-label">PAN–Aadhaar Link Status</span>
|
<span class="pan-result-label">PAN–Aadhaar Link Status</span>
|
||||||
<span class="pan-result-value" [class.seeded]="panAadhaarSeedingStatus?.toUpperCase() === 'Y'" [class.not-seeded]="panAadhaarSeedingStatus?.toUpperCase() === 'N'">
|
<span
|
||||||
@if (panAadhaarSeedingStatus?.toUpperCase() === 'Y') { Linked }
|
class="pan-result-value"
|
||||||
@else if (panAadhaarSeedingStatus?.toUpperCase() === 'N') { Not Linked }
|
[class.seeded]="panAadhaarSeedingStatus?.toUpperCase() === 'Y'"
|
||||||
@else { — }
|
[class.not-seeded]="panAadhaarSeedingStatus?.toUpperCase() === 'N'"
|
||||||
|
>
|
||||||
|
@if (panAadhaarSeedingStatus?.toUpperCase() === 'Y') {
|
||||||
|
Linked
|
||||||
|
} @else if (panAadhaarSeedingStatus?.toUpperCase() === 'N') {
|
||||||
|
Not Linked
|
||||||
|
} @else {
|
||||||
|
—
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="pan-result-row">
|
<div class="pan-result-row">
|
||||||
@@ -99,10 +124,22 @@
|
|||||||
<mat-form-field appearance="outline" class="verify-input">
|
<mat-form-field appearance="outline" class="verify-input">
|
||||||
<mat-label>Aadhaar Number</mat-label>
|
<mat-label>Aadhaar Number</mat-label>
|
||||||
<span matPrefix class="field-prefix-icon"><mat-icon>fingerprint</mat-icon></span>
|
<span matPrefix class="field-prefix-icon"><mat-icon>fingerprint</mat-icon></span>
|
||||||
<input matInput formControlName="aadhaarNumber" placeholder="12-digit Aadhaar"
|
<input
|
||||||
maxlength="12" [type]="aadhaarVisible ? 'text' : 'password'" [readonly]="aadhaarOtpSent"
|
matInput
|
||||||
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()" />
|
formControlName="aadhaarNumber"
|
||||||
<button mat-icon-button matSuffix type="button" (click)="aadhaarVisible = !aadhaarVisible" class="toggle-vis-btn">
|
placeholder="12-digit Aadhaar"
|
||||||
|
maxlength="12"
|
||||||
|
[type]="aadhaarVisible ? 'text' : 'password'"
|
||||||
|
[readonly]="aadhaarOtpSent"
|
||||||
|
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
mat-icon-button
|
||||||
|
matSuffix
|
||||||
|
type="button"
|
||||||
|
(click)="aadhaarVisible = !aadhaarVisible"
|
||||||
|
class="toggle-vis-btn"
|
||||||
|
>
|
||||||
<fa-icon [icon]="aadhaarVisible ? 'eye-slash' : 'eye'"></fa-icon>
|
<fa-icon [icon]="aadhaarVisible ? 'eye-slash' : 'eye'"></fa-icon>
|
||||||
</button>
|
</button>
|
||||||
@if (kycForm.get('aadhaarNumber')?.hasError('pattern') && kycForm.get('aadhaarNumber')?.touched) {
|
@if (kycForm.get('aadhaarNumber')?.hasError('pattern') && kycForm.get('aadhaarNumber')?.touched) {
|
||||||
@@ -114,9 +151,19 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<div class="verify-action">
|
<div class="verify-action">
|
||||||
@if ((aadhaarVerifyStatus === 'NONE' || aadhaarVerifyStatus === 'FAILED') && !aadhaarOtpSent) {
|
@if ((aadhaarVerifyStatus === 'NONE' || aadhaarVerifyStatus === 'FAILED') && !aadhaarOtpSent) {
|
||||||
<button mat-raised-button color="primary" type="button" class="verify-btn-inline"
|
<button
|
||||||
[disabled]="readOnly || !kycForm.get('aadhaarNumber')?.value || kycForm.get('aadhaarNumber')?.invalid || aadhaarVerifying"
|
mat-raised-button
|
||||||
(click)="sendAadhaarOtp()">
|
color="primary"
|
||||||
|
type="button"
|
||||||
|
class="verify-btn-inline"
|
||||||
|
[disabled]="
|
||||||
|
readOnly ||
|
||||||
|
!kycForm.get('aadhaarNumber')?.value ||
|
||||||
|
kycForm.get('aadhaarNumber')?.invalid ||
|
||||||
|
aadhaarVerifying
|
||||||
|
"
|
||||||
|
(click)="sendAadhaarOtp()"
|
||||||
|
>
|
||||||
@if (aadhaarVerifying) {
|
@if (aadhaarVerifying) {
|
||||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||||||
} @else {
|
} @else {
|
||||||
@@ -146,18 +193,28 @@
|
|||||||
<label class="otp-card__label">Enter OTP</label>
|
<label class="otp-card__label">Enter OTP</label>
|
||||||
<div class="otp-digits" (paste)="onOtpPaste($event)">
|
<div class="otp-digits" (paste)="onOtpPaste($event)">
|
||||||
@for (digit of otpDigits; track $index) {
|
@for (digit of otpDigits; track $index) {
|
||||||
<input type="text" inputmode="numeric" maxlength="1"
|
<input
|
||||||
class="otp-digit-box" [class.filled]="digit"
|
type="text"
|
||||||
|
inputmode="numeric"
|
||||||
|
maxlength="1"
|
||||||
|
class="otp-digit-box"
|
||||||
|
[class.filled]="digit"
|
||||||
[value]="digit"
|
[value]="digit"
|
||||||
(input)="onOtpDigitInput($index, $event)"
|
(input)="onOtpDigitInput($index, $event)"
|
||||||
(keydown)="onOtpKeydown($index, $event)"
|
(keydown)="onOtpKeydown($index, $event)"
|
||||||
autocomplete="one-time-code" />
|
autocomplete="one-time-code"
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<div class="otp-card__actions">
|
<div class="otp-card__actions">
|
||||||
<button mat-flat-button color="primary" type="button" class="otp-card__verify-btn"
|
<button
|
||||||
|
mat-flat-button
|
||||||
|
color="primary"
|
||||||
|
type="button"
|
||||||
|
class="otp-card__verify-btn"
|
||||||
[disabled]="readOnly || aadhaarOtp.length < 6 || aadhaarOtpVerifying"
|
[disabled]="readOnly || aadhaarOtp.length < 6 || aadhaarOtpVerifying"
|
||||||
(click)="verifyAadhaarOtp()">
|
(click)="verifyAadhaarOtp()"
|
||||||
|
>
|
||||||
@if (aadhaarOtpVerifying) {
|
@if (aadhaarOtpVerifying) {
|
||||||
<mat-progress-spinner diameter="16" mode="indeterminate" class="otp-spinner"></mat-progress-spinner>
|
<mat-progress-spinner diameter="16" mode="indeterminate" class="otp-spinner"></mat-progress-spinner>
|
||||||
Verifying...
|
Verifying...
|
||||||
@@ -173,8 +230,13 @@
|
|||||||
</span>
|
</span>
|
||||||
} @else {
|
} @else {
|
||||||
<span class="otp-card__resend-text">Didn't receive the code?</span>
|
<span class="otp-card__resend-text">Didn't receive the code?</span>
|
||||||
<button mat-button type="button" class="otp-card__resend-btn"
|
<button
|
||||||
[disabled]="readOnly || aadhaarVerifying" (click)="sendAadhaarOtp()">
|
mat-button
|
||||||
|
type="button"
|
||||||
|
class="otp-card__resend-btn"
|
||||||
|
[disabled]="readOnly || aadhaarVerifying"
|
||||||
|
(click)="sendAadhaarOtp()"
|
||||||
|
>
|
||||||
Resend OTP
|
Resend OTP
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@@ -185,7 +247,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@if (aadhaarVerifyError) {
|
@if (aadhaarVerifyError) {
|
||||||
<div class="verify-error full-width"><fa-icon icon="exclamation-circle"></fa-icon> {{ aadhaarVerifyError }}</div>
|
<div class="verify-error full-width">
|
||||||
|
<fa-icon icon="exclamation-circle"></fa-icon> {{ aadhaarVerifyError }}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
@if (aadhaarVerifyStatus === 'VERIFIED' && aadhaarVerifyName) {
|
@if (aadhaarVerifyStatus === 'VERIFIED' && aadhaarVerifyName) {
|
||||||
<div class="aadhaar-result-card full-width">
|
<div class="aadhaar-result-card full-width">
|
||||||
@@ -203,14 +267,25 @@
|
|||||||
<!-- PAN-Aadhaar Seeding Status -->
|
<!-- PAN-Aadhaar Seeding Status -->
|
||||||
@if (kycForm.get('hasPan')?.value && panAadhaarSeedingStatus) {
|
@if (kycForm.get('hasPan')?.value && panAadhaarSeedingStatus) {
|
||||||
<div class="form-field pan-link-status full-width">
|
<div class="form-field pan-link-status full-width">
|
||||||
<div class="pan-link-box" [class.linked]="panAadhaarSeedingStatus === 'Y'" [class.not-linked]="panAadhaarSeedingStatus === 'N'" [class.exempt]="panAadhaarSeedingStatus === 'E'">
|
<div
|
||||||
|
class="pan-link-box"
|
||||||
|
[class.linked]="panAadhaarSeedingStatus === 'Y'"
|
||||||
|
[class.not-linked]="panAadhaarSeedingStatus === 'N'"
|
||||||
|
[class.exempt]="panAadhaarSeedingStatus === 'E'"
|
||||||
|
>
|
||||||
<fa-icon icon="link" class="pan-link-icon"></fa-icon>
|
<fa-icon icon="link" class="pan-link-icon"></fa-icon>
|
||||||
<div class="pan-link-info">
|
<div class="pan-link-info">
|
||||||
<span class="pan-link-label">PAN–Aadhaar Link Status</span>
|
<span class="pan-link-label">PAN–Aadhaar Link Status</span>
|
||||||
<span class="pan-link-value">
|
<span class="pan-link-value">
|
||||||
@if (panAadhaarSeedingStatus === 'Y') { Linked (Seeded) }
|
@if (panAadhaarSeedingStatus === 'Y') {
|
||||||
@if (panAadhaarSeedingStatus === 'N') { Not Linked }
|
Linked (Seeded)
|
||||||
@if (panAadhaarSeedingStatus === 'E') { Exempt }
|
}
|
||||||
|
@if (panAadhaarSeedingStatus === 'N') {
|
||||||
|
Not Linked
|
||||||
|
}
|
||||||
|
@if (panAadhaarSeedingStatus === 'E') {
|
||||||
|
Exempt
|
||||||
|
}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span class="pan-link-hint">As per Income Tax Portal — Section 139AA</span>
|
<span class="pan-link-hint">As per Income Tax Portal — Section 139AA</span>
|
||||||
@@ -233,9 +308,16 @@
|
|||||||
<!-- GSTIN (all non-individual) -->
|
<!-- GSTIN (all non-individual) -->
|
||||||
<mat-form-field appearance="outline" class="form-field">
|
<mat-form-field appearance="outline" class="form-field">
|
||||||
<mat-label>GSTIN Number</mat-label>
|
<mat-label>GSTIN Number</mat-label>
|
||||||
<input matInput formControlName="gstinNumber" placeholder="15-character GSTIN"
|
<input
|
||||||
maxlength="15" style="text-transform: uppercase"
|
matInput
|
||||||
(input)="kycForm.get('gstinNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })" />
|
formControlName="gstinNumber"
|
||||||
|
placeholder="15-character GSTIN"
|
||||||
|
maxlength="15"
|
||||||
|
style="text-transform: uppercase"
|
||||||
|
(input)="
|
||||||
|
kycForm.get('gstinNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
|
||||||
|
"
|
||||||
|
/>
|
||||||
@if (kycForm.get('gstinNumber')?.hasError('pattern') && kycForm.get('gstinNumber')?.touched) {
|
@if (kycForm.get('gstinNumber')?.hasError('pattern') && kycForm.get('gstinNumber')?.touched) {
|
||||||
<mat-error>Invalid GSTIN format (e.g. 22AAAAA0000A1Z5)</mat-error>
|
<mat-error>Invalid GSTIN format (e.g. 22AAAAA0000A1Z5)</mat-error>
|
||||||
}
|
}
|
||||||
@@ -248,9 +330,16 @@
|
|||||||
<mat-form-field appearance="outline" class="verify-input">
|
<mat-form-field appearance="outline" class="verify-input">
|
||||||
<mat-label>PAN of Proprietor</mat-label>
|
<mat-label>PAN of Proprietor</mat-label>
|
||||||
<span matPrefix class="field-prefix-icon"><mat-icon>credit_card</mat-icon></span>
|
<span matPrefix class="field-prefix-icon"><mat-icon>credit_card</mat-icon></span>
|
||||||
<input matInput formControlName="panNumber" placeholder="e.g. ABCDE1234F"
|
<input
|
||||||
maxlength="10" style="text-transform: uppercase"
|
matInput
|
||||||
(input)="kycForm.get('panNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })" />
|
formControlName="panNumber"
|
||||||
|
placeholder="e.g. ABCDE1234F"
|
||||||
|
maxlength="10"
|
||||||
|
style="text-transform: uppercase"
|
||||||
|
(input)="
|
||||||
|
kycForm.get('panNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
|
||||||
|
"
|
||||||
|
/>
|
||||||
@if (kycForm.get('panNumber')?.hasError('pattern') && kycForm.get('panNumber')?.touched) {
|
@if (kycForm.get('panNumber')?.hasError('pattern') && kycForm.get('panNumber')?.touched) {
|
||||||
<mat-error>Invalid PAN format</mat-error>
|
<mat-error>Invalid PAN format</mat-error>
|
||||||
}
|
}
|
||||||
@@ -264,9 +353,20 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<div class="verify-action">
|
<div class="verify-action">
|
||||||
@if (panVerifyStatus === 'NONE' || panVerifyStatus === 'FAILED') {
|
@if (panVerifyStatus === 'NONE' || panVerifyStatus === 'FAILED') {
|
||||||
<button mat-raised-button color="primary" type="button" class="verify-btn-inline"
|
<button
|
||||||
[disabled]="readOnly || !kycForm.get('panNumber')?.value || kycForm.get('panNumber')?.invalid || panVerifying || !clientId"
|
mat-raised-button
|
||||||
(click)="verifyPan()">
|
color="primary"
|
||||||
|
type="button"
|
||||||
|
class="verify-btn-inline"
|
||||||
|
[disabled]="
|
||||||
|
readOnly ||
|
||||||
|
!kycForm.get('panNumber')?.value ||
|
||||||
|
kycForm.get('panNumber')?.invalid ||
|
||||||
|
panVerifying ||
|
||||||
|
!clientId
|
||||||
|
"
|
||||||
|
(click)="verifyPan()"
|
||||||
|
>
|
||||||
@if (panVerifying) {
|
@if (panVerifying) {
|
||||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||||||
} @else {
|
} @else {
|
||||||
@@ -303,9 +403,16 @@
|
|||||||
<mat-form-field appearance="outline" class="verify-input">
|
<mat-form-field appearance="outline" class="verify-input">
|
||||||
<mat-label>{{ constitutionMeta?.panLabel || 'PAN of Entity' }}</mat-label>
|
<mat-label>{{ constitutionMeta?.panLabel || 'PAN of Entity' }}</mat-label>
|
||||||
<span matPrefix class="field-prefix-icon"><mat-icon>credit_card</mat-icon></span>
|
<span matPrefix class="field-prefix-icon"><mat-icon>credit_card</mat-icon></span>
|
||||||
<input matInput formControlName="enterprisePan" placeholder="e.g. ABCDE1234F"
|
<input
|
||||||
maxlength="10" style="text-transform: uppercase"
|
matInput
|
||||||
(input)="kycForm.get('enterprisePan')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })" />
|
formControlName="enterprisePan"
|
||||||
|
placeholder="e.g. ABCDE1234F"
|
||||||
|
maxlength="10"
|
||||||
|
style="text-transform: uppercase"
|
||||||
|
(input)="
|
||||||
|
kycForm.get('enterprisePan')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
|
||||||
|
"
|
||||||
|
/>
|
||||||
@if (kycForm.get('enterprisePan')?.hasError('pattern') && kycForm.get('enterprisePan')?.touched) {
|
@if (kycForm.get('enterprisePan')?.hasError('pattern') && kycForm.get('enterprisePan')?.touched) {
|
||||||
<mat-error>Invalid PAN format</mat-error>
|
<mat-error>Invalid PAN format</mat-error>
|
||||||
}
|
}
|
||||||
@@ -319,9 +426,20 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<div class="verify-action">
|
<div class="verify-action">
|
||||||
@if (entPanVerifyStatus === 'NONE' || entPanVerifyStatus === 'FAILED') {
|
@if (entPanVerifyStatus === 'NONE' || entPanVerifyStatus === 'FAILED') {
|
||||||
<button mat-raised-button color="primary" type="button" class="verify-btn-inline"
|
<button
|
||||||
[disabled]="readOnly || !kycForm.get('enterprisePan')?.value || kycForm.get('enterprisePan')?.invalid || entPanVerifying || !clientId"
|
mat-raised-button
|
||||||
(click)="verifyEnterprisePan()">
|
color="primary"
|
||||||
|
type="button"
|
||||||
|
class="verify-btn-inline"
|
||||||
|
[disabled]="
|
||||||
|
readOnly ||
|
||||||
|
!kycForm.get('enterprisePan')?.value ||
|
||||||
|
kycForm.get('enterprisePan')?.invalid ||
|
||||||
|
entPanVerifying ||
|
||||||
|
!clientId
|
||||||
|
"
|
||||||
|
(click)="verifyEnterprisePan()"
|
||||||
|
>
|
||||||
@if (entPanVerifying) {
|
@if (entPanVerifying) {
|
||||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||||||
} @else {
|
} @else {
|
||||||
@@ -360,9 +478,16 @@
|
|||||||
<mat-form-field appearance="outline" class="verify-input">
|
<mat-form-field appearance="outline" class="verify-input">
|
||||||
<mat-label>CIN (Corporate Identity Number)</mat-label>
|
<mat-label>CIN (Corporate Identity Number)</mat-label>
|
||||||
<span matPrefix class="field-prefix-icon"><mat-icon>business</mat-icon></span>
|
<span matPrefix class="field-prefix-icon"><mat-icon>business</mat-icon></span>
|
||||||
<input matInput formControlName="cinNumber" maxlength="21" placeholder="From MCA"
|
<input
|
||||||
|
matInput
|
||||||
|
formControlName="cinNumber"
|
||||||
|
maxlength="21"
|
||||||
|
placeholder="From MCA"
|
||||||
style="text-transform: uppercase"
|
style="text-transform: uppercase"
|
||||||
(input)="kycForm.get('cinNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })" />
|
(input)="
|
||||||
|
kycForm.get('cinNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
|
||||||
|
"
|
||||||
|
/>
|
||||||
@if (kycForm.get('cinNumber')?.hasError('pattern') && kycForm.get('cinNumber')?.touched) {
|
@if (kycForm.get('cinNumber')?.hasError('pattern') && kycForm.get('cinNumber')?.touched) {
|
||||||
<mat-error>Invalid CIN format (21 characters)</mat-error>
|
<mat-error>Invalid CIN format (21 characters)</mat-error>
|
||||||
}
|
}
|
||||||
@@ -372,9 +497,14 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
<div class="verify-action">
|
<div class="verify-action">
|
||||||
@if (cinVerifyStatus === 'NONE' || cinVerifyStatus === 'FAILED') {
|
@if (cinVerifyStatus === 'NONE' || cinVerifyStatus === 'FAILED') {
|
||||||
<button mat-raised-button color="primary" type="button" class="verify-btn-inline"
|
<button
|
||||||
|
mat-raised-button
|
||||||
|
color="primary"
|
||||||
|
type="button"
|
||||||
|
class="verify-btn-inline"
|
||||||
[disabled]="readOnly || !kycForm.get('cinNumber')?.value || cinVerifying"
|
[disabled]="readOnly || !kycForm.get('cinNumber')?.value || cinVerifying"
|
||||||
(click)="verifyCin()">
|
(click)="verifyCin()"
|
||||||
|
>
|
||||||
@if (cinVerifying) {
|
@if (cinVerifying) {
|
||||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||||||
} @else {
|
} @else {
|
||||||
@@ -396,7 +526,10 @@
|
|||||||
<div class="cin-progress-steps">
|
<div class="cin-progress-steps">
|
||||||
@for (step of cinProgressSteps; track step.label; let i = $index) {
|
@for (step of cinProgressSteps; track step.label; let i = $index) {
|
||||||
<div class="cin-step" [class.active]="i === cinCurrentStep" [class.done]="i < cinCurrentStep">
|
<div class="cin-step" [class.active]="i === cinCurrentStep" [class.done]="i < cinCurrentStep">
|
||||||
<fa-icon [icon]="i < cinCurrentStep ? 'check-circle' : step.icon" [class.done-icon]="i < cinCurrentStep"></fa-icon>
|
<fa-icon
|
||||||
|
[icon]="i < cinCurrentStep ? 'check-circle' : step.icon"
|
||||||
|
[class.done-icon]="i < cinCurrentStep"
|
||||||
|
></fa-icon>
|
||||||
<span>{{ step.label }}</span>
|
<span>{{ step.label }}</span>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -467,12 +600,22 @@
|
|||||||
<mat-form-field appearance="outline" class="form-field">
|
<mat-form-field appearance="outline" class="form-field">
|
||||||
<mat-label>Document Number</mat-label>
|
<mat-label>Document Number</mat-label>
|
||||||
<span matPrefix class="field-prefix-icon"><mat-icon>tag</mat-icon></span>
|
<span matPrefix class="field-prefix-icon"><mat-icon>tag</mat-icon></span>
|
||||||
<input matInput required formControlName="kycDocumentNumber" placeholder="As printed on document" maxlength="30"
|
<input
|
||||||
|
matInput
|
||||||
|
required
|
||||||
|
formControlName="kycDocumentNumber"
|
||||||
|
placeholder="As printed on document"
|
||||||
|
maxlength="30"
|
||||||
style="text-transform: uppercase"
|
style="text-transform: uppercase"
|
||||||
(input)="kycForm.get('kycDocumentNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })" />
|
(input)="
|
||||||
|
kycForm.get('kycDocumentNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
|
||||||
|
"
|
||||||
|
/>
|
||||||
@if (kycForm.get('kycDocumentNumber')?.hasError('required') && kycForm.get('kycDocumentNumber')?.touched) {
|
@if (kycForm.get('kycDocumentNumber')?.hasError('required') && kycForm.get('kycDocumentNumber')?.touched) {
|
||||||
<mat-error>Document number is <strong>required</strong></mat-error>
|
<mat-error>Document number is <strong>required</strong></mat-error>
|
||||||
} @else if (kycForm.get('kycDocumentNumber')?.hasError('pattern') && kycForm.get('kycDocumentNumber')?.touched) {
|
} @else if (
|
||||||
|
kycForm.get('kycDocumentNumber')?.hasError('pattern') && kycForm.get('kycDocumentNumber')?.touched
|
||||||
|
) {
|
||||||
<mat-error>Only letters, numbers, hyphens and slashes allowed</mat-error>
|
<mat-error>Only letters, numbers, hyphens and slashes allowed</mat-error>
|
||||||
}
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
@@ -517,13 +660,20 @@
|
|||||||
}
|
}
|
||||||
</mat-select-trigger>
|
</mat-select-trigger>
|
||||||
<mat-option [value]="1">
|
<mat-option [value]="1">
|
||||||
<span class="risk-option"><span class="risk-meter risk-low"></span> Low Risk <span class="risk-hint">10 yr renewal</span></span>
|
<span class="risk-option"
|
||||||
|
><span class="risk-meter risk-low"></span> Low Risk <span class="risk-hint">10 yr renewal</span></span
|
||||||
|
>
|
||||||
</mat-option>
|
</mat-option>
|
||||||
<mat-option [value]="2">
|
<mat-option [value]="2">
|
||||||
<span class="risk-option"><span class="risk-meter risk-medium"></span> Medium Risk <span class="risk-hint">8 yr renewal</span></span>
|
<span class="risk-option"
|
||||||
|
><span class="risk-meter risk-medium"></span> Medium Risk
|
||||||
|
<span class="risk-hint">8 yr renewal</span></span
|
||||||
|
>
|
||||||
</mat-option>
|
</mat-option>
|
||||||
<mat-option [value]="3">
|
<mat-option [value]="3">
|
||||||
<span class="risk-option"><span class="risk-meter risk-high"></span> High Risk <span class="risk-hint">2 yr renewal</span></span>
|
<span class="risk-option"
|
||||||
|
><span class="risk-meter risk-high"></span> High Risk <span class="risk-hint">2 yr renewal</span></span
|
||||||
|
>
|
||||||
</mat-option>
|
</mat-option>
|
||||||
</mat-select>
|
</mat-select>
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
@@ -535,9 +685,27 @@
|
|||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
<mat-form-field appearance="outline" class="form-field full-width">
|
<mat-form-field appearance="outline" class="form-field full-width">
|
||||||
<mat-label>Remarks</mat-label>
|
<mat-label>{{ isKycVerified ? 'Reason for Re-edit *' : 'Remarks' }}</mat-label>
|
||||||
<textarea matInput formControlName="remarks" rows="2"></textarea>
|
<textarea
|
||||||
|
matInput
|
||||||
|
formControlName="remarks"
|
||||||
|
rows="2"
|
||||||
|
[placeholder]="isKycVerified ? 'Mandatory — explain why KYC is being re-edited' : ''"
|
||||||
|
></textarea>
|
||||||
|
@if (isKycVerified && !kycForm.get('remarks')?.value?.trim()) {
|
||||||
|
<mat-error>Remarks are mandatory when updating a verified KYC record</mat-error>
|
||||||
|
}
|
||||||
</mat-form-field>
|
</mat-form-field>
|
||||||
|
|
||||||
|
@if (isKycVerified) {
|
||||||
|
<div class="kyc-reedit-alert">
|
||||||
|
<mat-icon class="kyc-reedit-alert__icon">info</mat-icon>
|
||||||
|
<span
|
||||||
|
>KYC is <strong>VERIFIED</strong>. Any update will reset the status to <strong>IN PROGRESS</strong> and
|
||||||
|
require PAN re-verification.</span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- KYC Expiry Alert Cards -->
|
<!-- KYC Expiry Alert Cards -->
|
||||||
@@ -552,21 +720,38 @@
|
|||||||
<div class="kyc-expiry-card" [class.active]="kycForm.get('riskCategory')?.value === rc.value">
|
<div class="kyc-expiry-card" [class.active]="kycForm.get('riskCategory')?.value === rc.value">
|
||||||
<span class="risk-gauge" [class]="'risk-gauge--' + rc.color">
|
<span class="risk-gauge" [class]="'risk-gauge--' + rc.color">
|
||||||
<svg viewBox="0 0 40 24" class="risk-gauge-svg">
|
<svg viewBox="0 0 40 24" class="risk-gauge-svg">
|
||||||
<path d="M4 22 A16 16 0 0 1 36 22" fill="none" stroke="#e2e8f0" stroke-width="4" stroke-linecap="round"/>
|
<path
|
||||||
<path d="M4 22 A16 16 0 0 1 36 22" fill="none"
|
d="M4 22 A16 16 0 0 1 36 22"
|
||||||
|
fill="none"
|
||||||
|
stroke="#e2e8f0"
|
||||||
|
stroke-width="4"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M4 22 A16 16 0 0 1 36 22"
|
||||||
|
fill="none"
|
||||||
[attr.stroke]="rc.color === 'green' ? '#22c55e' : rc.color === 'amber' ? '#f59e0b' : '#ef4444'"
|
[attr.stroke]="rc.color === 'green' ? '#22c55e' : rc.color === 'amber' ? '#f59e0b' : '#ef4444'"
|
||||||
stroke-width="4" stroke-linecap="round"
|
stroke-width="4"
|
||||||
[attr.stroke-dasharray]="rc.color === 'green' ? '16 50' : rc.color === 'amber' ? '33 50' : '50 50'"/>
|
stroke-linecap="round"
|
||||||
<line x1="20" y1="22"
|
[attr.stroke-dasharray]="rc.color === 'green' ? '16 50' : rc.color === 'amber' ? '33 50' : '50 50'"
|
||||||
|
/>
|
||||||
|
<line
|
||||||
|
x1="20"
|
||||||
|
y1="22"
|
||||||
[attr.x2]="rc.color === 'green' ? '10' : rc.color === 'amber' ? '20' : '30'"
|
[attr.x2]="rc.color === 'green' ? '10' : rc.color === 'amber' ? '20' : '30'"
|
||||||
[attr.y2]="rc.color === 'green' ? '10' : rc.color === 'amber' ? '6' : '10'"
|
[attr.y2]="rc.color === 'green' ? '10' : rc.color === 'amber' ? '6' : '10'"
|
||||||
stroke="#334155" stroke-width="2" stroke-linecap="round"/>
|
stroke="#334155"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
/>
|
||||||
<circle cx="20" cy="22" r="2" fill="#334155" />
|
<circle cx="20" cy="22" r="2" fill="#334155" />
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<div class="kyc-expiry-card__info">
|
<div class="kyc-expiry-card__info">
|
||||||
<span class="kyc-expiry-card__label">{{ rc.label }}</span>
|
<span class="kyc-expiry-card__label">{{ rc.label }}</span>
|
||||||
<span class="kyc-expiry-card__cycle">Renewal every <strong>{{ rc.years }} years</strong></span>
|
<span class="kyc-expiry-card__cycle"
|
||||||
|
>Renewal every <strong>{{ rc.years }} years</strong></span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
@if (kycForm.get('riskCategory')?.value === rc.value) {
|
@if (kycForm.get('riskCategory')?.value === rc.value) {
|
||||||
<span class="kyc-expiry-card__next">Next: {{ getKycExpiryDate(rc.years) }}</span>
|
<span class="kyc-expiry-card__next">Next: {{ getKycExpiryDate(rc.years) }}</span>
|
||||||
@@ -575,12 +760,17 @@
|
|||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
@if (kycForm.get('riskCategory')?.value) {
|
@if (kycForm.get('riskCategory')?.value) {
|
||||||
<div class="kyc-expiry-alert"
|
<div
|
||||||
|
class="kyc-expiry-alert"
|
||||||
[class.kyc-expiry-alert--low]="kycForm.get('riskCategory')?.value === 1"
|
[class.kyc-expiry-alert--low]="kycForm.get('riskCategory')?.value === 1"
|
||||||
[class.kyc-expiry-alert--medium]="kycForm.get('riskCategory')?.value === 2"
|
[class.kyc-expiry-alert--medium]="kycForm.get('riskCategory')?.value === 2"
|
||||||
[class.kyc-expiry-alert--high]="kycForm.get('riskCategory')?.value === 3">
|
[class.kyc-expiry-alert--high]="kycForm.get('riskCategory')?.value === 3"
|
||||||
|
>
|
||||||
<fa-icon icon="exclamation-circle" class="kyc-expiry-alert__icon"></fa-icon>
|
<fa-icon icon="exclamation-circle" class="kyc-expiry-alert__icon"></fa-icon>
|
||||||
<span>System will auto-alert before KYC expiry on <strong>{{ getKycExpiryDate(getRenewalYears()) }}</strong>.</span>
|
<span
|
||||||
|
>System will auto-alert before KYC expiry on <strong>{{ getKycExpiryDate(getRenewalYears()) }}</strong
|
||||||
|
>.</span
|
||||||
|
>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #333;
|
color: #333;
|
||||||
margin: 0 0 16px 0;
|
margin: 0 0 16px;
|
||||||
|
|
||||||
.section-icon {
|
.section-icon {
|
||||||
color: var(--md-sys-color-primary, #1074b9);
|
color: var(--md-sys-color-primary, #1074b9);
|
||||||
@@ -119,7 +119,7 @@
|
|||||||
&.active {
|
&.active {
|
||||||
border-color: #1353a0;
|
border-color: #1353a0;
|
||||||
background: #eff6ff;
|
background: #eff6ff;
|
||||||
box-shadow: 0 0 0 2px rgba(19, 83, 160, 0.12);
|
box-shadow: 0 0 0 2px rgb(19 83 160 / 12%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&__dot {
|
&__dot {
|
||||||
@@ -128,9 +128,17 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
&.green { background: #10b981; }
|
&.green {
|
||||||
&.amber { background: #f59e0b; }
|
background: #10b981;
|
||||||
&.red { background: #ef4444; }
|
}
|
||||||
|
|
||||||
|
&.amber {
|
||||||
|
background: #f59e0b;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.red {
|
||||||
|
background: #ef4444;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&__info {
|
&__info {
|
||||||
@@ -186,7 +194,9 @@
|
|||||||
color: #065f46;
|
color: #065f46;
|
||||||
border: 1px solid #a7f3d0;
|
border: 1px solid #a7f3d0;
|
||||||
|
|
||||||
.kyc-expiry-alert__icon { color: #10b981; }
|
.kyc-expiry-alert__icon {
|
||||||
|
color: #10b981;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--medium {
|
&--medium {
|
||||||
@@ -194,7 +204,9 @@
|
|||||||
color: #92400e;
|
color: #92400e;
|
||||||
border: 1px solid #fde68a;
|
border: 1px solid #fde68a;
|
||||||
|
|
||||||
.kyc-expiry-alert__icon { color: #f59e0b; }
|
.kyc-expiry-alert__icon {
|
||||||
|
color: #f59e0b;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&--high {
|
&--high {
|
||||||
@@ -202,11 +214,13 @@
|
|||||||
color: #991b1b;
|
color: #991b1b;
|
||||||
border: 1px solid #fecaca;
|
border: 1px solid #fecaca;
|
||||||
|
|
||||||
.kyc-expiry-alert__icon { color: #ef4444; }
|
.kyc-expiry-alert__icon {
|
||||||
|
color: #ef4444;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (width <= 768px) {
|
||||||
.kyc-expiry-cards {
|
.kyc-expiry-cards {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@@ -451,7 +465,7 @@
|
|||||||
border: 1px solid #e0e7ff;
|
border: 1px solid #e0e7ff;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.08);
|
box-shadow: 0 2px 8px rgb(99 102 241 / 8%);
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -475,7 +489,11 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
mat-icon { font-size: 16px; width: 16px; height: 16px; }
|
mat-icon {
|
||||||
|
font-size: 16px;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.otp-card__header-text {
|
.otp-card__header-text {
|
||||||
@@ -536,7 +554,7 @@
|
|||||||
&:focus {
|
&:focus {
|
||||||
border-color: #1074b9;
|
border-color: #1074b9;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 0 0 2px rgba(16, 116, 185, 0.12);
|
box-shadow: 0 0 0 2px rgb(16 116 185 / 12%);
|
||||||
}
|
}
|
||||||
|
|
||||||
&.filled {
|
&.filled {
|
||||||
@@ -652,8 +670,10 @@
|
|||||||
color: #065f46;
|
color: #065f46;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 480px) {
|
@media (width <= 480px) {
|
||||||
.otp-digits { gap: 6px; }
|
.otp-digits {
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
.otp-digit-box {
|
.otp-digit-box {
|
||||||
width: 40px;
|
width: 40px;
|
||||||
@@ -662,8 +682,13 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.otp-card__header { padding: 12px 14px; }
|
.otp-card__header {
|
||||||
.otp-card__body { padding: 20px 14px; }
|
padding: 12px 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.otp-card__body {
|
||||||
|
padding: 20px 14px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* PAN-Aadhaar Link Status (read-only) */
|
/* PAN-Aadhaar Link Status (read-only) */
|
||||||
@@ -683,22 +708,40 @@
|
|||||||
&.linked {
|
&.linked {
|
||||||
border-color: #a7f3d0;
|
border-color: #a7f3d0;
|
||||||
background: #ecfdf5;
|
background: #ecfdf5;
|
||||||
.pan-link-value { color: #059669; }
|
|
||||||
.pan-link-icon { color: #059669; }
|
.pan-link-value {
|
||||||
|
color: #059669;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pan-link-icon {
|
||||||
|
color: #059669;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.not-linked {
|
&.not-linked {
|
||||||
border-color: #fecaca;
|
border-color: #fecaca;
|
||||||
background: #fef2f2;
|
background: #fef2f2;
|
||||||
.pan-link-value { color: #dc2626; }
|
|
||||||
.pan-link-icon { color: #dc2626; }
|
.pan-link-value {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pan-link-icon {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.exempt {
|
&.exempt {
|
||||||
border-color: #bfdbfe;
|
border-color: #bfdbfe;
|
||||||
background: #eff6ff;
|
background: #eff6ff;
|
||||||
.pan-link-value { color: #2563eb; }
|
|
||||||
.pan-link-icon { color: #2563eb; }
|
.pan-link-value {
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pan-link-icon {
|
||||||
|
color: #2563eb;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1037,7 +1080,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (width <= 768px) {
|
||||||
.form-grid {
|
.form-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
@@ -1073,7 +1116,7 @@
|
|||||||
|
|
||||||
&__sub {
|
&__sub {
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: rgba(255, 255, 255, 0.5);
|
color: rgb(255 255 255 / 50%);
|
||||||
margin: 2px 0 0;
|
margin: 2px 0 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1092,9 +1135,20 @@
|
|||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
&.risk-low { background: #22c55e; box-shadow: 0 0 0 3px rgba(34, 197, 94, 0.2); }
|
&.risk-low {
|
||||||
&.risk-medium { background: #f59e0b; box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.2); }
|
background: #22c55e;
|
||||||
&.risk-high { background: #ef4444; box-shadow: 0 0 0 3px rgba(239, 68, 68, 0.2); }
|
box-shadow: 0 0 0 3px rgb(34 197 94 / 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.risk-medium {
|
||||||
|
background: #f59e0b;
|
||||||
|
box-shadow: 0 0 0 3px rgb(245 158 11 / 20%);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.risk-high {
|
||||||
|
background: #ef4444;
|
||||||
|
box-shadow: 0 0 0 3px rgb(239 68 68 / 20%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.risk-hint {
|
.risk-hint {
|
||||||
@@ -1133,3 +1187,30 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Re-edit Info Alert ─── */
|
||||||
|
.kyc-reedit-alert {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 12px 16px;
|
||||||
|
margin-top: 8px;
|
||||||
|
background: #eff6ff;
|
||||||
|
border: 1px solid #93c5fd;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #1e40af;
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
color: #3b82f6;
|
||||||
|
font-size: 20px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-top: 1px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
/**
|
/**
|
||||||
* Copyright since 2025 Mifos Initiative
|
* Copyright since 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, inject } from '@angular/core';
|
import { Component, Input, OnInit, OnChanges, OnDestroy, SimpleChanges, inject } from '@angular/core';
|
||||||
@@ -54,77 +58,88 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
kycId: string = '';
|
kycId: string = '';
|
||||||
/** Whether existing KYC data has been loaded (edit mode) */
|
/** Whether existing KYC data has been loaded (edit mode) */
|
||||||
kycLoaded = false;
|
kycLoaded = false;
|
||||||
|
/** KYC status from API: 100=PENDING, 200=IN_PROGRESS, 300=VERIFIED */
|
||||||
|
kycStatus: number = 0;
|
||||||
|
/** Whether the KYC record is in VERIFIED state (needs re-edit command) */
|
||||||
|
get isKycVerified(): boolean {
|
||||||
|
return this.kycStatus === 300;
|
||||||
|
}
|
||||||
|
|
||||||
/** KYC Document Types — populated from API template, falls back to defaults */
|
/** KYC Document Types — populated from API template, falls back to defaults */
|
||||||
kycDocumentTypes: { name: string; code: string; mandatory: boolean }[] = [];
|
kycDocumentTypes: { name: string; code: string; mandatory: boolean }[] = [];
|
||||||
/** Fallback document types when template API is unavailable */
|
/** Fallback document types when template API is unavailable */
|
||||||
private defaultDocumentTypes = [
|
private defaultDocumentTypes = [
|
||||||
'Aadhaar Card', 'Passport', 'PAN Card', 'Driving Licence',
|
'Aadhaar Card',
|
||||||
'Voter ID', 'NREGA Job Card', 'National Population Register Letter'
|
'Passport',
|
||||||
|
'PAN Card',
|
||||||
|
'Driving Licence',
|
||||||
|
'Voter ID',
|
||||||
|
'NREGA Job Card',
|
||||||
|
'National Population Register Letter'
|
||||||
];
|
];
|
||||||
/** Constitution-based KYC document types for non-individual clients */
|
/** Constitution-based KYC document types for non-individual clients */
|
||||||
private constitutionDocTypes: Record<string, { name: string; mandatory: boolean }[]> = {
|
private constitutionDocTypes: Record<string, { name: string; mandatory: boolean }[]> = {
|
||||||
'HUF': [
|
HUF: [
|
||||||
{ name: 'PAN Card of HUF', mandatory: true },
|
{ name: 'PAN Card of HUF', mandatory: true },
|
||||||
{ name: 'Declaration by Karta (with name, address & HUF member details)', mandatory: true },
|
{ name: 'Declaration by Karta (with name, address & HUF member details)', mandatory: true },
|
||||||
{ name: 'KYC of Karta (OVDs + PAN + Photo)', mandatory: true },
|
{ name: 'KYC of Karta (OVDs + PAN + Photo)', mandatory: true },
|
||||||
{ name: 'Proof of Address of HUF', mandatory: false }
|
{ name: 'Proof of Address of HUF', mandatory: false }
|
||||||
],
|
],
|
||||||
'PRIVATE_LIMITED': [
|
PRIVATE_LIMITED: [
|
||||||
{ name: 'Certificate of Incorporation', mandatory: true },
|
{ name: 'Certificate of Incorporation', mandatory: true },
|
||||||
{ name: 'Memorandum & Articles of Association', mandatory: true },
|
{ name: 'Memorandum & Articles of Association', mandatory: true },
|
||||||
{ name: 'PAN of Company', mandatory: true },
|
{ name: 'PAN of Company', mandatory: true },
|
||||||
{ name: 'Board Resolution (authorizing signatories)', mandatory: true },
|
{ name: 'Board Resolution (authorizing signatories)', mandatory: true },
|
||||||
{ name: 'KYC of Authorized Signatories', mandatory: true }
|
{ name: 'KYC of Authorized Signatories', mandatory: true }
|
||||||
],
|
],
|
||||||
'PUBLIC_LIMITED': [
|
PUBLIC_LIMITED: [
|
||||||
{ name: 'Certificate of Incorporation', mandatory: true },
|
{ name: 'Certificate of Incorporation', mandatory: true },
|
||||||
{ name: 'Memorandum & Articles of Association', mandatory: true },
|
{ name: 'Memorandum & Articles of Association', mandatory: true },
|
||||||
{ name: 'PAN of Company', mandatory: true },
|
{ name: 'PAN of Company', mandatory: true },
|
||||||
{ name: 'Board Resolution (authorizing signatories)', mandatory: true },
|
{ name: 'Board Resolution (authorizing signatories)', mandatory: true },
|
||||||
{ name: 'KYC of Authorized Signatories', mandatory: true }
|
{ name: 'KYC of Authorized Signatories', mandatory: true }
|
||||||
],
|
],
|
||||||
'LLP': [
|
LLP: [
|
||||||
{ name: 'Certificate of Incorporation', mandatory: true },
|
{ name: 'Certificate of Incorporation', mandatory: true },
|
||||||
{ name: 'LLP Agreement', mandatory: true },
|
{ name: 'LLP Agreement', mandatory: true },
|
||||||
{ name: 'PAN of LLP', mandatory: true },
|
{ name: 'PAN of LLP', mandatory: true },
|
||||||
{ name: 'Designated Partners Resolution', mandatory: true },
|
{ name: 'Designated Partners Resolution', mandatory: true },
|
||||||
{ name: 'KYC of Designated Partners / Authorized Signatories', mandatory: true }
|
{ name: 'KYC of Designated Partners / Authorized Signatories', mandatory: true }
|
||||||
],
|
],
|
||||||
'PARTNERSHIP': [
|
PARTNERSHIP: [
|
||||||
{ name: 'Registration Certificate', mandatory: false },
|
{ name: 'Registration Certificate', mandatory: false },
|
||||||
{ name: 'Partnership Deed', mandatory: true },
|
{ name: 'Partnership Deed', mandatory: true },
|
||||||
{ name: 'PAN of Firm', mandatory: true },
|
{ name: 'PAN of Firm', mandatory: true },
|
||||||
{ name: 'KYC of Authorized Signatories', mandatory: true }
|
{ name: 'KYC of Authorized Signatories', mandatory: true }
|
||||||
],
|
],
|
||||||
'TRUST': [
|
TRUST: [
|
||||||
{ name: 'Registration Certificate', mandatory: true },
|
{ name: 'Registration Certificate', mandatory: true },
|
||||||
{ name: 'Trust Deed', mandatory: true },
|
{ name: 'Trust Deed', mandatory: true },
|
||||||
{ name: 'PAN of Trust', mandatory: true },
|
{ name: 'PAN of Trust', mandatory: true },
|
||||||
{ name: 'KYC of Trustees / Authorized Signatories', mandatory: true }
|
{ name: 'KYC of Trustees / Authorized Signatories', mandatory: true }
|
||||||
],
|
],
|
||||||
'SOCIETY': [
|
SOCIETY: [
|
||||||
{ name: 'Registration Certificate', mandatory: true },
|
{ name: 'Registration Certificate', mandatory: true },
|
||||||
{ name: 'PAN of Society', mandatory: true },
|
{ name: 'PAN of Society', mandatory: true },
|
||||||
{ name: 'KYC of Authorized Signatories', mandatory: true }
|
{ name: 'KYC of Authorized Signatories', mandatory: true }
|
||||||
],
|
],
|
||||||
'PROPRIETORSHIP': [
|
PROPRIETORSHIP: [
|
||||||
{ name: 'Proof of Business Existence (GST Certificate / Shop License)', mandatory: true },
|
{ name: 'Proof of Business Existence (GST Certificate / Shop License)', mandatory: true },
|
||||||
{ name: 'PAN of Proprietor', mandatory: true },
|
{ name: 'PAN of Proprietor', mandatory: true },
|
||||||
{ name: "Proprietor's KYC (OVDs + PAN + Photo)", mandatory: true }
|
{ name: "Proprietor's KYC (OVDs + PAN + Photo)", mandatory: true }
|
||||||
],
|
],
|
||||||
'ASSOCIATION': [
|
ASSOCIATION: [
|
||||||
{ name: 'Registration Certificate', mandatory: true },
|
{ name: 'Registration Certificate', mandatory: true },
|
||||||
{ name: 'PAN', mandatory: true },
|
{ name: 'PAN', mandatory: true },
|
||||||
{ name: 'Resolution authorizing signatories', mandatory: true },
|
{ name: 'Resolution authorizing signatories', mandatory: true },
|
||||||
{ name: 'KYC of Authorized Signatories', mandatory: true }
|
{ name: 'KYC of Authorized Signatories', mandatory: true }
|
||||||
],
|
],
|
||||||
'GOVT_PSU': [
|
GOVT_PSU: [
|
||||||
{ name: 'Government Notification / Order', mandatory: true },
|
{ name: 'Government Notification / Order', mandatory: true },
|
||||||
{ name: 'PAN (if applicable)', mandatory: false },
|
{ name: 'PAN (if applicable)', mandatory: false },
|
||||||
{ name: 'Authorized Signatory KYC', mandatory: true }
|
{ name: 'Authorized Signatory KYC', mandatory: true }
|
||||||
],
|
],
|
||||||
'COOPERATIVE': [
|
COOPERATIVE: [
|
||||||
{ name: 'Registration Certificate', mandatory: true },
|
{ name: 'Registration Certificate', mandatory: true },
|
||||||
{ name: 'PAN', mandatory: true },
|
{ name: 'PAN', mandatory: true },
|
||||||
{ name: 'Resolution authorizing signatories', mandatory: true },
|
{ name: 'Resolution authorizing signatories', mandatory: true },
|
||||||
@@ -161,7 +176,14 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
aadhaarOtpReferenceId = '';
|
aadhaarOtpReferenceId = '';
|
||||||
|
|
||||||
/** OTP digit boxes */
|
/** OTP digit boxes */
|
||||||
otpDigits: string[] = ['', '', '', '', '', ''];
|
otpDigits: string[] = [
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
''
|
||||||
|
];
|
||||||
/** Resend countdown */
|
/** Resend countdown */
|
||||||
resendCountdown = 0;
|
resendCountdown = 0;
|
||||||
private resendTimer: any = null;
|
private resendTimer: any = null;
|
||||||
@@ -221,7 +243,14 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resetOtpDigits(): void {
|
resetOtpDigits(): void {
|
||||||
this.otpDigits = ['', '', '', '', '', ''];
|
this.otpDigits = [
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
''
|
||||||
|
];
|
||||||
this.aadhaarOtp = '';
|
this.aadhaarOtp = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -254,25 +283,73 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
photographPreview: string | null = null;
|
photographPreview: string | null = null;
|
||||||
signatureFile: File | null = null;
|
signatureFile: File | null = null;
|
||||||
signaturePreview: string | null = null;
|
signaturePreview: string | null = null;
|
||||||
|
/** Existing signature document ID (for re-upload/delete) */
|
||||||
|
existingSignatureDocId: string = '';
|
||||||
|
|
||||||
/** KYC Form */
|
/** KYC Form */
|
||||||
kycForm: UntypedFormGroup = this.formBuilder.group({
|
kycForm: UntypedFormGroup = this.formBuilder.group({
|
||||||
hasPan: [true],
|
hasPan: [true],
|
||||||
panNumber: ['', [Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/), Validators.maxLength(10)]],
|
panNumber: [
|
||||||
aadhaarNumber: ['', [Validators.pattern(/^\d{12}$/), Validators.maxLength(12)]],
|
'',
|
||||||
|
[
|
||||||
|
Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/),
|
||||||
|
Validators.maxLength(10)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
aadhaarNumber: [
|
||||||
|
'',
|
||||||
|
[
|
||||||
|
Validators.pattern(/^\d{12}$/),
|
||||||
|
Validators.maxLength(12)
|
||||||
|
]
|
||||||
|
],
|
||||||
panAadhaarSeedingStatus: [''],
|
panAadhaarSeedingStatus: [''],
|
||||||
gstinNumber: ['', [Validators.pattern(/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/), Validators.maxLength(15)]],
|
gstinNumber: [
|
||||||
enterprisePan: ['', [Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/), Validators.maxLength(10)]],
|
'',
|
||||||
cinNumber: ['', [Validators.pattern(/^[A-Z]{1}[0-9]{5}[A-Z]{2}[0-9]{4}[A-Z]{3}[0-9]{6}$/), Validators.maxLength(21)]],
|
[
|
||||||
|
Validators.pattern(/^[0-9]{2}[A-Z]{5}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}Z[0-9A-Z]{1}$/),
|
||||||
|
Validators.maxLength(15)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
enterprisePan: [
|
||||||
|
'',
|
||||||
|
[
|
||||||
|
Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/),
|
||||||
|
Validators.maxLength(10)
|
||||||
|
]
|
||||||
|
],
|
||||||
|
cinNumber: [
|
||||||
|
'',
|
||||||
|
[
|
||||||
|
Validators.pattern(/^[A-Z]{1}[0-9]{5}[A-Z]{2}[0-9]{4}[A-Z]{3}[0-9]{6}$/),
|
||||||
|
Validators.maxLength(21)
|
||||||
|
]
|
||||||
|
],
|
||||||
incorporationDate: [''],
|
incorporationDate: [''],
|
||||||
kycDocumentName: ['', Validators.required],
|
kycDocumentName: [
|
||||||
kycDocumentNumber: ['', [Validators.required, Validators.pattern(/^[A-Za-z0-9\-\/\s]+$/), Validators.maxLength(30)]],
|
'',
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
kycDocumentNumber: [
|
||||||
|
'',
|
||||||
|
[
|
||||||
|
Validators.required,
|
||||||
|
Validators.pattern(/^[A-Za-z0-9\-\/\s]+$/),
|
||||||
|
Validators.maxLength(30)
|
||||||
|
]
|
||||||
|
],
|
||||||
kycDocIssueDate: [''],
|
kycDocIssueDate: [''],
|
||||||
kycDocExpiryDate: [''],
|
kycDocExpiryDate: [''],
|
||||||
riskCategory: [1],
|
riskCategory: [1],
|
||||||
remarks: [''],
|
remarks: [''],
|
||||||
amlCheckStatus: ['CLEAR', Validators.required],
|
amlCheckStatus: [
|
||||||
bannedEntityMatch: ['NO', Validators.required],
|
'CLEAR',
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
|
bannedEntityMatch: [
|
||||||
|
'NO',
|
||||||
|
Validators.required
|
||||||
|
],
|
||||||
willfulDefaulter: ['NO'],
|
willfulDefaulter: ['NO'],
|
||||||
ecgcSalFlag: ['NO'],
|
ecgcSalFlag: ['NO'],
|
||||||
dormantFlag: ['ACTIVE'],
|
dormantFlag: ['ACTIVE'],
|
||||||
@@ -320,7 +397,7 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
private updateDocTypesForConstitution(): void {
|
private updateDocTypesForConstitution(): void {
|
||||||
const constitutionValue = this.constitutionMeta?.value;
|
const constitutionValue = this.constitutionMeta?.value;
|
||||||
if (constitutionValue && this.constitutionDocTypes[constitutionValue]) {
|
if (constitutionValue && this.constitutionDocTypes[constitutionValue]) {
|
||||||
this.kycDocumentTypes = this.constitutionDocTypes[constitutionValue].map(doc => ({
|
this.kycDocumentTypes = this.constitutionDocTypes[constitutionValue].map((doc) => ({
|
||||||
name: doc.name,
|
name: doc.name,
|
||||||
code: doc.name,
|
code: doc.name,
|
||||||
mandatory: doc.mandatory
|
mandatory: doc.mandatory
|
||||||
@@ -339,13 +416,51 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.kycLoaded = true;
|
this.kycLoaded = true;
|
||||||
if (data) {
|
if (data) {
|
||||||
this.kycId = data.id?.toString() || '';
|
this.kycId = data.id?.toString() || '';
|
||||||
|
this.kycStatus = data.status?.id || data.kycStatus || 0;
|
||||||
|
// Helper to parse API date arrays [y,m,d] or strings into Date objects
|
||||||
|
const parseDate = (val: any): Date | string => {
|
||||||
|
if (Array.isArray(val) && val.length >= 3) {
|
||||||
|
return new Date(val[0], val[1] - 1, val[2]);
|
||||||
|
}
|
||||||
|
if (val && typeof val === 'string') {
|
||||||
|
const d = new Date(val);
|
||||||
|
return isNaN(d.getTime()) ? '' : d;
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
this.kycForm.patchValue({
|
this.kycForm.patchValue({
|
||||||
panNumber: data.panNumber || '',
|
panNumber: data.panNumber || '',
|
||||||
aadhaarNumber: data.aadhaarNumberMasked || '',
|
aadhaarNumber: data.aadhaarNumberMasked || '',
|
||||||
gstinNumber: data.gstinNumber || '',
|
gstinNumber: data.gstinNumber || '',
|
||||||
|
enterprisePan: data.enterprisePan || '',
|
||||||
|
cinNumber: data.cinNumber || '',
|
||||||
|
incorporationDate: parseDate(data.incorporationDate),
|
||||||
|
// KYC Document
|
||||||
|
kycDocumentName: data.kycDocumentName || '',
|
||||||
|
kycDocumentNumber: data.kycDocumentNumber || '',
|
||||||
|
kycDocIssueDate: parseDate(data.kycDocIssueDate),
|
||||||
|
kycDocExpiryDate: parseDate(data.kycDocExpiryDate),
|
||||||
|
// Risk & Remarks
|
||||||
riskCategory: data.riskCategory || 1,
|
riskCategory: data.riskCategory || 1,
|
||||||
remarks: data.remarks || '',
|
remarks: data.remarks || '',
|
||||||
hasPan: !!data.panNumber
|
hasPan: !!data.panNumber,
|
||||||
|
// AML & Compliance
|
||||||
|
amlCheckStatus: data.amlCheckStatus || 'CLEAR',
|
||||||
|
bannedEntityMatch: data.bannedEntityMatch || 'NO',
|
||||||
|
willfulDefaulter: data.willfulDefaulter || 'NO',
|
||||||
|
ecgcSalFlag: data.ecgcSalFlag || 'NO',
|
||||||
|
dormantFlag: data.dormantFlag || 'ACTIVE',
|
||||||
|
// Account Status & Freeze
|
||||||
|
accountStatus: data.accountStatus || 'ACTIVE',
|
||||||
|
freezeReason: data.freezeReason || '',
|
||||||
|
freezeAuthority: data.freezeAuthority || '',
|
||||||
|
// Form 60 / Form 15
|
||||||
|
form60Type: data.form60Type || 'FORM_60',
|
||||||
|
form60Date: parseDate(data.form60Date),
|
||||||
|
form15Applicable: data.form15Applicable || false,
|
||||||
|
form15Type: data.form15Type || '15G',
|
||||||
|
form15FY: data.form15FY || ''
|
||||||
});
|
});
|
||||||
// Set verification statuses from existing data
|
// Set verification statuses from existing data
|
||||||
if (data.panVerified) {
|
if (data.panVerified) {
|
||||||
@@ -366,6 +481,9 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.panAadhaarSeedingStatus = data.panAadhaarSeedingStatus;
|
this.panAadhaarSeedingStatus = data.panAadhaarSeedingStatus;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Load existing photo & signature
|
||||||
|
this.loadExistingPhoto();
|
||||||
|
this.loadExistingSignature();
|
||||||
},
|
},
|
||||||
error: () => {
|
error: () => {
|
||||||
// No existing KYC — that's fine for new clients
|
// No existing KYC — that's fine for new clients
|
||||||
@@ -374,6 +492,45 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Fetch existing client profile photo */
|
||||||
|
private loadExistingPhoto(): void {
|
||||||
|
if (!this.clientId) return;
|
||||||
|
this.clientsService.getClientProfileImage(this.clientId).subscribe({
|
||||||
|
next: (imageData: any) => {
|
||||||
|
if (imageData) {
|
||||||
|
this.photographPreview = imageData;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: () => {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Fetch existing client signature from documents */
|
||||||
|
private loadExistingSignature(): void {
|
||||||
|
if (!this.clientId) return;
|
||||||
|
this.clientsService.getClientDocuments(this.clientId).subscribe({
|
||||||
|
next: (documents: any[]) => {
|
||||||
|
const sigDoc = documents?.find((d: any) => d.name === 'clientSignature');
|
||||||
|
if (sigDoc?.id) {
|
||||||
|
this.existingSignatureDocId = sigDoc.id.toString();
|
||||||
|
this.clientsService.getClientSignatureImage(this.clientId, this.existingSignatureDocId).subscribe({
|
||||||
|
next: async (blob: any) => {
|
||||||
|
const buffer = await blob.arrayBuffer();
|
||||||
|
const bytes = new Uint8Array(buffer);
|
||||||
|
let binary = '';
|
||||||
|
for (let i = 0; i < bytes.length; i++) {
|
||||||
|
binary += String.fromCharCode(bytes[i]);
|
||||||
|
}
|
||||||
|
this.signaturePreview = 'data:' + blob.type + ';base64,' + btoa(binary);
|
||||||
|
},
|
||||||
|
error: () => {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: () => {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** Load KYC template to get required documents based on client type */
|
/** Load KYC template to get required documents based on client type */
|
||||||
private loadKycTemplate(): void {
|
private loadKycTemplate(): void {
|
||||||
if (!this.clientId || this.kycTemplateLoaded) return;
|
if (!this.clientId || this.kycTemplateLoaded) return;
|
||||||
@@ -388,7 +545,7 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
mandatory: doc.mandatory ?? false
|
mandatory: doc.mandatory ?? false
|
||||||
}));
|
}));
|
||||||
// Update kycDocumentName validator based on mandatory docs
|
// Update kycDocumentName validator based on mandatory docs
|
||||||
const hasMandatory = this.kycDocumentTypes.some(d => d.mandatory);
|
const hasMandatory = this.kycDocumentTypes.some((d) => d.mandatory);
|
||||||
if (hasMandatory) {
|
if (hasMandatory) {
|
||||||
this.kycForm.get('kycDocumentName')?.setValidators(Validators.required);
|
this.kycForm.get('kycDocumentName')?.setValidators(Validators.required);
|
||||||
}
|
}
|
||||||
@@ -398,7 +555,7 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
error: () => {
|
error: () => {
|
||||||
// Fallback to defaults if template API unavailable
|
// Fallback to defaults if template API unavailable
|
||||||
this.kycTemplateLoaded = true;
|
this.kycTemplateLoaded = true;
|
||||||
this.kycDocumentTypes = this.defaultDocumentTypes.map(name => ({
|
this.kycDocumentTypes = this.defaultDocumentTypes.map((name) => ({
|
||||||
name,
|
name,
|
||||||
code: name,
|
code: name,
|
||||||
mandatory: false
|
mandatory: false
|
||||||
@@ -410,19 +567,48 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
/** Save or update KYC, then execute callback */
|
/** Save or update KYC, then execute callback */
|
||||||
private saveKycAndDo(payload: any, callback: () => void, onError: (msg: string) => void) {
|
private saveKycAndDo(payload: any, callback: () => void, onError: (msg: string) => void) {
|
||||||
if (this.kycId) {
|
if (this.kycId) {
|
||||||
// KYC already exists — use PUT to update (strip create-only fields)
|
|
||||||
const { kycType, ...updatePayload } = payload;
|
const { kycType, ...updatePayload } = payload;
|
||||||
|
|
||||||
|
// If KYC is VERIFIED, remarks are mandatory — use re-edit command
|
||||||
|
if (this.isKycVerified) {
|
||||||
|
const remarks = this.kycForm.get('remarks')?.value?.trim();
|
||||||
|
if (!remarks) {
|
||||||
|
onError('Remarks are mandatory when updating a verified KYC record.');
|
||||||
|
this.kycForm.get('remarks')?.markAsTouched();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
updatePayload.remarks = remarks;
|
||||||
|
this.clientsService.reEditClientKyc(this.clientId, this.kycId, updatePayload).subscribe({
|
||||||
|
next: () => {
|
||||||
|
// Status transitions to IN_PROGRESS, PAN verification is invalidated
|
||||||
|
this.kycStatus = 200;
|
||||||
|
this.panVerifyStatus = 'NONE';
|
||||||
|
this.panVerifyName = '';
|
||||||
|
this.panVerifiedOn = '';
|
||||||
|
callback();
|
||||||
|
},
|
||||||
|
error: (err: any) =>
|
||||||
|
onError(
|
||||||
|
err?.error?.errors?.[0]?.defaultUserMessage || err?.error?.defaultUserMessage || 'Failed to re-edit KYC.'
|
||||||
|
)
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Normal update for non-verified records
|
||||||
this.clientsService.updateClientKyc(this.clientId, this.kycId, updatePayload).subscribe({
|
this.clientsService.updateClientKyc(this.clientId, this.kycId, updatePayload).subscribe({
|
||||||
next: () => callback(),
|
next: () => callback(),
|
||||||
error: (err: any) => onError(err?.error?.defaultUserMessage || 'Failed to update KYC.')
|
error: (err: any) => onError(err?.error?.defaultUserMessage || 'Failed to update KYC.')
|
||||||
});
|
});
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// First time — use POST to create
|
// First time — use POST to create
|
||||||
this.clientsService.saveClientKyc(this.clientId, payload).subscribe({
|
this.clientsService.saveClientKyc(this.clientId, payload).subscribe({
|
||||||
next: (res: any) => {
|
next: (res: any) => {
|
||||||
this.kycId = res?.resourceId?.toString() || '';
|
this.kycId = res?.resourceId?.toString() || '';
|
||||||
if (this.kycId) { callback(); }
|
if (this.kycId) {
|
||||||
else { onError('Failed to save KYC.'); }
|
callback();
|
||||||
|
} else {
|
||||||
|
onError('Failed to save KYC.');
|
||||||
|
}
|
||||||
},
|
},
|
||||||
error: (err: any) => onError(err?.error?.defaultUserMessage || 'Failed to save KYC.')
|
error: (err: any) => onError(err?.error?.defaultUserMessage || 'Failed to save KYC.')
|
||||||
});
|
});
|
||||||
@@ -436,7 +622,10 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const pan = this.kycForm.value.panNumber;
|
const pan = this.kycForm.value.panNumber;
|
||||||
if (!pan) { this.panVerifyError = 'Enter PAN number first.'; return; }
|
if (!pan) {
|
||||||
|
this.panVerifyError = 'Enter PAN number first.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.panVerifying = true;
|
this.panVerifying = true;
|
||||||
this.panVerifyError = '';
|
this.panVerifyError = '';
|
||||||
@@ -451,21 +640,31 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
aadhaarNumberMasked: aadhaarRaw.length === 12 ? 'XXXXXXXX' + aadhaarRaw.slice(-4) : 'XXXXXXXX0000'
|
aadhaarNumberMasked: aadhaarRaw.length === 12 ? 'XXXXXXXX' + aadhaarRaw.slice(-4) : 'XXXXXXXX0000'
|
||||||
};
|
};
|
||||||
|
|
||||||
this.saveKycAndDo(panPayload, () => {
|
this.saveKycAndDo(
|
||||||
|
panPayload,
|
||||||
|
() => {
|
||||||
this.clientsService.verifyPAN(this.clientId, this.kycId).subscribe({
|
this.clientsService.verifyPAN(this.clientId, this.kycId).subscribe({
|
||||||
next: (res: any) => {
|
next: (res: any) => {
|
||||||
this.panVerifying = false;
|
this.panVerifying = false;
|
||||||
if (res?.changes?.panVerified === true) {
|
if (res?.changes?.panVerified === true) {
|
||||||
// Full name from API
|
// Full name from API
|
||||||
const resolvedName = res.changes?.panFullName
|
const resolvedName =
|
||||||
|| res.changes?.panHolderName
|
res.changes?.panFullName ||
|
||||||
|| [res.changes?.panFirstName, res.changes?.panLastName].filter(Boolean).join(' ')
|
res.changes?.panHolderName ||
|
||||||
|| '';
|
[
|
||||||
|
res.changes?.panFirstName,
|
||||||
|
res.changes?.panLastName
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' ') ||
|
||||||
|
'';
|
||||||
|
|
||||||
// If name is empty, the client name/DOB didn't match PAN holder
|
// If name is empty, the client name/DOB didn't match PAN holder
|
||||||
if (!resolvedName || !resolvedName.trim()) {
|
if (!resolvedName || !resolvedName.trim()) {
|
||||||
this.panVerifyStatus = 'FAILED';
|
this.panVerifyStatus = 'FAILED';
|
||||||
this.showPanErrorAlert('PAN verification failed: Client name or date of birth does not match the PAN card holder. Please ensure the client\'s name and date of birth exactly match the PAN card details.');
|
this.showPanErrorAlert(
|
||||||
|
"PAN verification failed: Client name or date of birth does not match the PAN card holder. Please ensure the client's name and date of birth exactly match the PAN card details."
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -493,40 +692,47 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
error: (err: any) => {
|
error: (err: any) => {
|
||||||
this.panVerifying = false;
|
this.panVerifying = false;
|
||||||
this.panVerifyStatus = 'FAILED';
|
this.panVerifyStatus = 'FAILED';
|
||||||
const msg = err?.error?.errors?.[0]?.defaultUserMessage
|
const msg =
|
||||||
|| err?.error?.defaultUserMessage
|
err?.error?.errors?.[0]?.defaultUserMessage ||
|
||||||
|| 'PAN verification failed.';
|
err?.error?.defaultUserMessage ||
|
||||||
|
'PAN verification failed.';
|
||||||
this.showPanErrorAlert(msg);
|
this.showPanErrorAlert(msg);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, (msg) => { this.panVerifying = false; this.showPanErrorAlert(msg); });
|
},
|
||||||
|
(msg) => {
|
||||||
|
this.panVerifying = false;
|
||||||
|
this.showPanErrorAlert(msg);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Verify Enterprise PAN (non-individual) — uses same verify-pan API */
|
/**
|
||||||
|
* Verify Enterprise PAN (non-individual) — same flow as individual PAN verification.
|
||||||
|
* Maps: Company PAN → panNumber, Company Name → client name, Date of Incorporation → DOB
|
||||||
|
*/
|
||||||
verifyEnterprisePan() {
|
verifyEnterprisePan() {
|
||||||
if (!this.clientId) return;
|
if (!this.clientId) return;
|
||||||
const pan = this.kycForm.value.enterprisePan;
|
const pan = this.kycForm.value.enterprisePan;
|
||||||
if (!pan || this.kycForm.get('enterprisePan')?.invalid) return;
|
if (!pan || this.kycForm.get('enterprisePan')?.invalid) return;
|
||||||
|
|
||||||
this.entPanVerifying = true;
|
this.entPanVerifying = true;
|
||||||
|
const dateFormat = this.settingsService.dateFormat;
|
||||||
const incorpDate = this.kycForm.value.incorporationDate;
|
const incorpDate = this.kycForm.value.incorporationDate;
|
||||||
|
|
||||||
|
// Same payload structure as individual — backend uses panNumber + client name/DOB for verification
|
||||||
const payload: any = {
|
const payload: any = {
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
kycType: 1,
|
kycType: 1,
|
||||||
riskCategory: this.kycForm.value.riskCategory || 1,
|
riskCategory: this.kycForm.value.riskCategory || 1,
|
||||||
panNumber: pan,
|
panNumber: pan, // Company PAN → panNumber
|
||||||
dateFormat: this.settingsService.dateFormat
|
dateFormat,
|
||||||
|
aadhaarNumberMasked: 'XXXXXXXX0000' // Placeholder — not applicable for entities
|
||||||
};
|
};
|
||||||
if (incorpDate) {
|
|
||||||
payload.dateOfIncorporation = incorpDate instanceof Date
|
|
||||||
? this.dateUtils.formatDate(incorpDate, this.settingsService.dateFormat)
|
|
||||||
: incorpDate;
|
|
||||||
}
|
|
||||||
if (this.entityName) {
|
|
||||||
payload.companyFullName = this.entityName;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.saveKycAndDo(payload, () => {
|
this.saveKycAndDo(
|
||||||
|
payload,
|
||||||
|
() => {
|
||||||
this.clientsService.verifyPAN(this.clientId, this.kycId).subscribe({
|
this.clientsService.verifyPAN(this.clientId, this.kycId).subscribe({
|
||||||
next: (res: any) => {
|
next: (res: any) => {
|
||||||
this.entPanVerifying = false;
|
this.entPanVerifying = false;
|
||||||
@@ -538,20 +744,32 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.entPanVerifiedOn = `${rawDate[2]?.toString().padStart(2, '0')}/${rawDate[1]?.toString().padStart(2, '0')}/${rawDate[0]}`;
|
this.entPanVerifiedOn = `${rawDate[2]?.toString().padStart(2, '0')}/${rawDate[1]?.toString().padStart(2, '0')}/${rawDate[0]}`;
|
||||||
} else {
|
} else {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
this.entPanVerifiedOn = rawDate || `${now.getDate().toString().padStart(2, '0')}/${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getFullYear()}`;
|
this.entPanVerifiedOn =
|
||||||
|
rawDate ||
|
||||||
|
`${now.getDate().toString().padStart(2, '0')}/${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getFullYear()}`;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.entPanVerifyStatus = 'FAILED';
|
this.entPanVerifyStatus = 'FAILED';
|
||||||
this.showPanErrorAlert(res?.changes?.panVerificationMessage || 'Enterprise PAN verification failed.');
|
this.showPanErrorAlert(
|
||||||
|
res?.changes?.panVerificationMessage ||
|
||||||
|
'Enterprise PAN verification failed. Ensure company name and date of incorporation match the PAN records.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: (err: any) => {
|
error: (err: any) => {
|
||||||
this.entPanVerifying = false;
|
this.entPanVerifying = false;
|
||||||
this.entPanVerifyStatus = 'FAILED';
|
this.entPanVerifyStatus = 'FAILED';
|
||||||
this.showPanErrorAlert(err?.error?.errors?.[0]?.defaultUserMessage || 'Enterprise PAN verification failed.');
|
this.showPanErrorAlert(
|
||||||
|
err?.error?.errors?.[0]?.defaultUserMessage || 'Enterprise PAN verification failed.'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, (msg) => { this.entPanVerifying = false; this.showPanErrorAlert(msg); });
|
},
|
||||||
|
(msg) => {
|
||||||
|
this.entPanVerifying = false;
|
||||||
|
this.showPanErrorAlert(msg);
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Verify CIN (dummy — no real API) */
|
/** Verify CIN (dummy — no real API) */
|
||||||
@@ -563,7 +781,12 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.cinProgressValue = 0;
|
this.cinProgressValue = 0;
|
||||||
this.cinCurrentStep = 0;
|
this.cinCurrentStep = 0;
|
||||||
|
|
||||||
const stepDurations = [800, 1000, 1200, 800];
|
const stepDurations = [
|
||||||
|
800,
|
||||||
|
1000,
|
||||||
|
1200,
|
||||||
|
800
|
||||||
|
];
|
||||||
const totalDuration = stepDurations.reduce((a, b) => a + b, 0);
|
const totalDuration = stepDurations.reduce((a, b) => a + b, 0);
|
||||||
let elapsed = 0;
|
let elapsed = 0;
|
||||||
const interval = 50;
|
const interval = 50;
|
||||||
@@ -573,7 +796,10 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
let accum = 0;
|
let accum = 0;
|
||||||
for (let i = 0; i < stepDurations.length; i++) {
|
for (let i = 0; i < stepDurations.length; i++) {
|
||||||
accum += stepDurations[i];
|
accum += stepDurations[i];
|
||||||
if (elapsed <= accum) { this.cinCurrentStep = i; break; }
|
if (elapsed <= accum) {
|
||||||
|
this.cinCurrentStep = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
this.cinProgressValue = Math.min(Math.round((elapsed / totalDuration) * 100), 100);
|
this.cinProgressValue = Math.min(Math.round((elapsed / totalDuration) * 100), 100);
|
||||||
|
|
||||||
@@ -598,7 +824,10 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const aadhaar = this.kycForm.value.aadhaarNumber;
|
const aadhaar = this.kycForm.value.aadhaarNumber;
|
||||||
if (!aadhaar || aadhaar.length !== 12) { this.aadhaarVerifyError = 'Enter valid 12-digit Aadhaar first.'; return; }
|
if (!aadhaar || aadhaar.length !== 12) {
|
||||||
|
this.aadhaarVerifyError = 'Enter valid 12-digit Aadhaar first.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.aadhaarVerifying = true;
|
this.aadhaarVerifying = true;
|
||||||
this.aadhaarVerifyError = '';
|
this.aadhaarVerifyError = '';
|
||||||
@@ -611,9 +840,13 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
aadhaarNumberMasked: aadhaarMasked
|
aadhaarNumberMasked: aadhaarMasked
|
||||||
};
|
};
|
||||||
const panVal = this.kycForm.value.panNumber;
|
const panVal = this.kycForm.value.panNumber;
|
||||||
if (panVal) { aadhaarPayload.panNumber = panVal; }
|
if (panVal) {
|
||||||
|
aadhaarPayload.panNumber = panVal;
|
||||||
|
}
|
||||||
|
|
||||||
this.saveKycAndDo(aadhaarPayload, () => {
|
this.saveKycAndDo(
|
||||||
|
aadhaarPayload,
|
||||||
|
() => {
|
||||||
this.clientsService.sendAadhaarOtp(this.clientId, this.kycId, aadhaar).subscribe({
|
this.clientsService.sendAadhaarOtp(this.clientId, this.kycId, aadhaar).subscribe({
|
||||||
next: (res: any) => {
|
next: (res: any) => {
|
||||||
this.aadhaarVerifying = false;
|
this.aadhaarVerifying = false;
|
||||||
@@ -627,7 +860,12 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.aadhaarVerifyError = err?.error?.defaultUserMessage || 'Failed to send OTP.';
|
this.aadhaarVerifyError = err?.error?.defaultUserMessage || 'Failed to send OTP.';
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}, (msg) => { this.aadhaarVerifying = false; this.aadhaarVerifyError = msg; });
|
},
|
||||||
|
(msg) => {
|
||||||
|
this.aadhaarVerifying = false;
|
||||||
|
this.aadhaarVerifyError = msg;
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Verify Aadhaar OTP */
|
/** Verify Aadhaar OTP */
|
||||||
@@ -636,7 +874,9 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
this.aadhaarOtpVerifying = true;
|
this.aadhaarOtpVerifying = true;
|
||||||
this.aadhaarVerifyError = '';
|
this.aadhaarVerifyError = '';
|
||||||
|
|
||||||
this.clientsService.verifyAadhaarOtp(this.clientId, this.kycId, this.aadhaarOtp, this.aadhaarOtpReferenceId).subscribe({
|
this.clientsService
|
||||||
|
.verifyAadhaarOtp(this.clientId, this.kycId, this.aadhaarOtp, this.aadhaarOtpReferenceId)
|
||||||
|
.subscribe({
|
||||||
next: (res: any) => {
|
next: (res: any) => {
|
||||||
this.aadhaarOtpVerifying = false;
|
this.aadhaarOtpVerifying = false;
|
||||||
if (res?.changes?.aadhaarVerified === true) {
|
if (res?.changes?.aadhaarVerified === true) {
|
||||||
@@ -667,17 +907,34 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
getRenewalYears(): number {
|
getRenewalYears(): number {
|
||||||
switch (this.kycForm.value.riskCategory) {
|
switch (this.kycForm.value.riskCategory) {
|
||||||
case 1: return 10;
|
case 1:
|
||||||
case 2: return 8;
|
return 10;
|
||||||
case 3: return 2;
|
case 2:
|
||||||
default: return 10;
|
return 8;
|
||||||
|
case 3:
|
||||||
|
return 2;
|
||||||
|
default:
|
||||||
|
return 10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getKycExpiryDate(years: number): string {
|
getKycExpiryDate(years: number): string {
|
||||||
const d = new Date();
|
const d = new Date();
|
||||||
d.setFullYear(d.getFullYear() + years);
|
d.setFullYear(d.getFullYear() + years);
|
||||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
const months = [
|
||||||
|
'Jan',
|
||||||
|
'Feb',
|
||||||
|
'Mar',
|
||||||
|
'Apr',
|
||||||
|
'May',
|
||||||
|
'Jun',
|
||||||
|
'Jul',
|
||||||
|
'Aug',
|
||||||
|
'Sep',
|
||||||
|
'Oct',
|
||||||
|
'Nov',
|
||||||
|
'Dec'
|
||||||
|
];
|
||||||
return `${String(d.getDate()).padStart(2, '0')} ${months[d.getMonth()]} ${d.getFullYear()}`;
|
return `${String(d.getDate()).padStart(2, '0')} ${months[d.getMonth()]} ${d.getFullYear()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -688,11 +945,21 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
input.value = '';
|
input.value = '';
|
||||||
const { ImageCropDialogComponent } = await import('./image-crop-dialog/image-crop-dialog.component');
|
const { ImageCropDialogComponent } = await import('./image-crop-dialog/image-crop-dialog.component');
|
||||||
const dialogRef = this.dialog.open(ImageCropDialogComponent, {
|
const dialogRef = this.dialog.open(ImageCropDialogComponent, {
|
||||||
width: '680px', maxWidth: '95vw',
|
width: '680px',
|
||||||
|
maxWidth: '95vw',
|
||||||
data: { file, title: 'Crop Client Photograph', aspectRatio: 1, maxWidth: 300, maxHeight: 300 }
|
data: { file, title: 'Crop Client Photograph', aspectRatio: 1, maxWidth: 300, maxHeight: 300 }
|
||||||
});
|
});
|
||||||
dialogRef.afterClosed().subscribe((result: any) => {
|
dialogRef.afterClosed().subscribe((result: any) => {
|
||||||
if (result) { this.photographFile = result.croppedFile; this.photographPreview = result.previewUrl; }
|
if (result) {
|
||||||
|
this.photographFile = result.croppedFile;
|
||||||
|
this.photographPreview = result.previewUrl;
|
||||||
|
// In edit mode, upload immediately
|
||||||
|
if (this.clientId) {
|
||||||
|
this.clientsService
|
||||||
|
.uploadClientProfileImage(this.clientId, result.croppedFile)
|
||||||
|
.subscribe({ error: () => {} });
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -703,16 +970,70 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
input.value = '';
|
input.value = '';
|
||||||
const { ImageCropDialogComponent } = await import('./image-crop-dialog/image-crop-dialog.component');
|
const { ImageCropDialogComponent } = await import('./image-crop-dialog/image-crop-dialog.component');
|
||||||
const dialogRef = this.dialog.open(ImageCropDialogComponent, {
|
const dialogRef = this.dialog.open(ImageCropDialogComponent, {
|
||||||
width: '680px', maxWidth: '95vw',
|
width: '680px',
|
||||||
|
maxWidth: '95vw',
|
||||||
data: { file, title: 'Crop Specimen Signature', aspectRatio: 2.5, maxWidth: 400, maxHeight: 160 }
|
data: { file, title: 'Crop Specimen Signature', aspectRatio: 2.5, maxWidth: 400, maxHeight: 160 }
|
||||||
});
|
});
|
||||||
dialogRef.afterClosed().subscribe((result: any) => {
|
dialogRef.afterClosed().subscribe((result: any) => {
|
||||||
if (result) { this.signatureFile = result.croppedFile; this.signaturePreview = result.previewUrl; }
|
if (result) {
|
||||||
|
this.signatureFile = result.croppedFile;
|
||||||
|
this.signaturePreview = result.previewUrl;
|
||||||
|
// In edit mode, delete old signature doc then upload new one
|
||||||
|
if (this.clientId) {
|
||||||
|
if (this.existingSignatureDocId) {
|
||||||
|
this.clientsService.deleteClientDocument(this.clientId, this.existingSignatureDocId).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.clientsService.uploadClientSignatureImage(this.clientId, result.croppedFile).subscribe({
|
||||||
|
next: (res: any) => {
|
||||||
|
if (res?.resourceId) this.existingSignatureDocId = res.resourceId.toString();
|
||||||
|
},
|
||||||
|
error: () => {}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.clientsService.uploadClientSignatureImage(this.clientId, result.croppedFile).subscribe({
|
||||||
|
next: (res: any) => {
|
||||||
|
if (res?.resourceId) this.existingSignatureDocId = res.resourceId.toString();
|
||||||
|
},
|
||||||
|
error: () => {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.clientsService.uploadClientSignatureImage(this.clientId, result.croppedFile).subscribe({
|
||||||
|
next: (res: any) => {
|
||||||
|
if (res?.resourceId) this.existingSignatureDocId = res.resourceId.toString();
|
||||||
|
},
|
||||||
|
error: () => {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
removePhoto() { this.photographFile = null; this.photographPreview = null; }
|
removePhoto() {
|
||||||
removeSignature() { this.signatureFile = null; this.signaturePreview = null; }
|
this.photographFile = null;
|
||||||
|
this.photographPreview = null;
|
||||||
|
// In edit mode, delete from server
|
||||||
|
if (this.clientId) {
|
||||||
|
this.clientsService.deleteClientProfileImage(this.clientId).subscribe({ error: () => {} });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removeSignature() {
|
||||||
|
this.signatureFile = null;
|
||||||
|
this.signaturePreview = null;
|
||||||
|
// In edit mode, delete from server
|
||||||
|
if (this.clientId && this.existingSignatureDocId) {
|
||||||
|
this.clientsService.deleteClientDocument(this.clientId, this.existingSignatureDocId).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.existingSignatureDocId = '';
|
||||||
|
},
|
||||||
|
error: () => {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
get kycData(): any {
|
get kycData(): any {
|
||||||
const formVal = this.readOnly ? this.kycForm.getRawValue() : this.kycForm.value;
|
const formVal = this.readOnly ? this.kycForm.getRawValue() : this.kycForm.value;
|
||||||
@@ -720,15 +1041,16 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
const aadhaarMasked = aadhaarRaw.length === 12 ? 'XXXXXXXX' + aadhaarRaw.slice(-4) : null;
|
const aadhaarMasked = aadhaarRaw.length === 12 ? 'XXXXXXXX' + aadhaarRaw.slice(-4) : null;
|
||||||
const dateFormat = this.settingsService.dateFormat;
|
const dateFormat = this.settingsService.dateFormat;
|
||||||
const v = formVal;
|
const v = formVal;
|
||||||
const formatDate = (d: any) => d instanceof Date ? this.dateUtils.formatDate(d, dateFormat) : (d || null);
|
const formatDate = (d: any) => (d instanceof Date ? this.dateUtils.formatDate(d, dateFormat) : d || null);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
locale: 'en',
|
locale: 'en',
|
||||||
dateFormat,
|
dateFormat,
|
||||||
kycType: 1,
|
kycType: 1,
|
||||||
riskCategory: v.riskCategory || 1,
|
riskCategory: v.riskCategory || 1,
|
||||||
panNumber: v.hasPan ? (v.panNumber || null) : null,
|
// For non-individual: Company PAN is used as panNumber for verification
|
||||||
aadhaarNumberMasked: aadhaarMasked,
|
panNumber: this.isNonIndividual ? v.enterprisePan || null : v.hasPan ? v.panNumber || null : null,
|
||||||
|
aadhaarNumberMasked: this.isNonIndividual ? 'XXXXXXXX0000' : aadhaarMasked,
|
||||||
panAadhaarSeedingStatus: this.panAadhaarSeedingStatus || null,
|
panAadhaarSeedingStatus: this.panAadhaarSeedingStatus || null,
|
||||||
gstinNumber: v.gstinNumber || null,
|
gstinNumber: v.gstinNumber || null,
|
||||||
enterprisePan: v.enterprisePan || null,
|
enterprisePan: v.enterprisePan || null,
|
||||||
@@ -761,7 +1083,15 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
|
|
||||||
get hasKycData(): boolean {
|
get hasKycData(): boolean {
|
||||||
const v = this.readOnly ? this.kycForm.getRawValue() : this.kycForm.value;
|
const v = this.readOnly ? this.kycForm.getRawValue() : this.kycForm.value;
|
||||||
return !!(v.panNumber || v.aadhaarNumber || v.gstinNumber || v.enterprisePan || v.cinNumber || v.kycDocumentName || v.form15Applicable);
|
return !!(
|
||||||
|
v.panNumber ||
|
||||||
|
v.aadhaarNumber ||
|
||||||
|
v.gstinNumber ||
|
||||||
|
v.enterprisePan ||
|
||||||
|
v.cinNumber ||
|
||||||
|
v.kycDocumentName ||
|
||||||
|
v.form15Applicable
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -784,22 +1114,44 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
|||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
</div>
|
</div>
|
||||||
`,
|
`,
|
||||||
styles: [`
|
styles: [
|
||||||
.pan-err-dialog { max-width: 460px; }
|
`
|
||||||
.pan-err-header {
|
.pan-err-dialog {
|
||||||
display: flex; align-items: center; gap: 10px;
|
max-width: 460px;
|
||||||
padding: 16px 24px 12px; border-bottom: 1px solid #fecaca;
|
|
||||||
background: #fef2f2; font-size: 16px; font-weight: 600; color: #991b1b;
|
|
||||||
}
|
}
|
||||||
.pan-err-icon { color: #dc2626; }
|
.pan-err-header {
|
||||||
.pan-err-body { padding: 18px 24px !important; font-size: 14px; line-height: 1.6; color: #475569; }
|
display: flex;
|
||||||
.pan-err-actions { padding: 10px 24px 16px !important; margin: 0 !important; }
|
align-items: center;
|
||||||
`],
|
gap: 10px;
|
||||||
imports: [MatIcon, MatDialogContent, MatDialogActions, STANDALONE_SHARED_IMPORTS],
|
padding: 16px 24px 12px;
|
||||||
|
border-bottom: 1px solid #fecaca;
|
||||||
|
background: #fef2f2;
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #991b1b;
|
||||||
|
}
|
||||||
|
.pan-err-icon {
|
||||||
|
color: #dc2626;
|
||||||
|
}
|
||||||
|
.pan-err-body {
|
||||||
|
padding: 18px 24px !important;
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #475569;
|
||||||
|
}
|
||||||
|
.pan-err-actions {
|
||||||
|
padding: 10px 24px 16px !important;
|
||||||
|
margin: 0 !important;
|
||||||
|
}
|
||||||
|
`
|
||||||
|
],
|
||||||
|
imports: [
|
||||||
|
MatIcon,
|
||||||
|
MatDialogContent,
|
||||||
|
MatDialogActions,
|
||||||
|
STANDALONE_SHARED_IMPORTS
|
||||||
|
]
|
||||||
})
|
})
|
||||||
export class PanErrorDialogComponent {
|
export class PanErrorDialogComponent {
|
||||||
constructor(
|
data = inject<{ message: string }>(MAT_DIALOG_DATA);
|
||||||
@Inject(MAT_DIALOG_DATA) public data: { message: string }
|
|
||||||
) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -243,18 +243,18 @@ export class ClientsService {
|
|||||||
};
|
};
|
||||||
for (const client of results) {
|
for (const client of results) {
|
||||||
// Fetch DOB from client data
|
// Fetch DOB from client data
|
||||||
this.getClientData(client.id.toString()).pipe(
|
this.getClientData(client.id.toString())
|
||||||
catchError(() => of(null))
|
.pipe(catchError(() => of(null)))
|
||||||
).subscribe((data: any) => {
|
.subscribe((data: any) => {
|
||||||
if (data?.dateOfBirth) {
|
if (data?.dateOfBirth) {
|
||||||
client.dateOfBirth = data.dateOfBirth;
|
client.dateOfBirth = data.dateOfBirth;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fetch father/spouse from family members
|
// Fetch father/spouse from family members
|
||||||
this.getClientFamilyMembers(client.id.toString()).pipe(
|
this.getClientFamilyMembers(client.id.toString())
|
||||||
catchError(() => of([]))
|
.pipe(catchError(() => of([])))
|
||||||
).subscribe((members: any) => {
|
.subscribe((members: any) => {
|
||||||
const list = Array.isArray(members) ? members : [];
|
const list = Array.isArray(members) ? members : [];
|
||||||
const spouse = list.find((m: any) => getRelName(m) === 'SPOUSE');
|
const spouse = list.find((m: any) => getRelName(m) === 'SPOUSE');
|
||||||
const father = list.find((m: any) => getRelName(m) === 'FATHER');
|
const father = list.find((m: any) => getRelName(m) === 'FATHER');
|
||||||
@@ -291,8 +291,12 @@ export class ClientsService {
|
|||||||
|
|
||||||
getClientNominees(clientId: string, accountNo?: string, accountType?: string): Observable<any> {
|
getClientNominees(clientId: string, accountNo?: string, accountType?: string): Observable<any> {
|
||||||
let params = new HttpParams();
|
let params = new HttpParams();
|
||||||
if (accountNo) { params = params.set('accountNo', accountNo); }
|
if (accountNo) {
|
||||||
if (accountType) { params = params.set('accountType', accountType); }
|
params = params.set('accountNo', accountNo);
|
||||||
|
}
|
||||||
|
if (accountType) {
|
||||||
|
params = params.set('accountType', accountType);
|
||||||
|
}
|
||||||
return this.http.get(`/clients/${clientId}/nominees`, { params });
|
return this.http.get(`/clients/${clientId}/nominees`, { params });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,6 +603,14 @@ export class ClientsService {
|
|||||||
return this.http.put(`/clients/${clientId}/kyc/${kycId}`, kycData);
|
return this.http.put(`/clients/${clientId}/kyc/${kycId}`, kycData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-edit a VERIFIED KYC record. Transitions status VERIFIED → IN_PROGRESS.
|
||||||
|
* Remarks are mandatory in the payload.
|
||||||
|
*/
|
||||||
|
reEditClientKyc(clientId: string, kycId: string, kycData: any): Observable<any> {
|
||||||
|
return this.http.put(`/clients/${clientId}/kyc/${kycId}?command=re-edit`, kycData);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifies PAN number via LincPay (reads PAN from the stored KYC record).
|
* Verifies PAN number via LincPay (reads PAN from the stored KYC record).
|
||||||
* @param clientId Client ID
|
* @param clientId Client ID
|
||||||
|
|||||||
@@ -12,7 +12,12 @@
|
|||||||
<h2>{{ 'labels.heading.Edit Client' | translate }}</h2>
|
<h2>{{ 'labels.heading.Edit Client' | translate }}</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<mat-tab-group class="client-tabs" animationDuration="200ms" [preserveContent]="true" [(selectedIndex)]="selectedTabIndex">
|
<mat-tab-group
|
||||||
|
class="client-tabs"
|
||||||
|
animationDuration="200ms"
|
||||||
|
[preserveContent]="true"
|
||||||
|
[(selectedIndex)]="selectedTabIndex"
|
||||||
|
>
|
||||||
<!-- Tab 1: General Details -->
|
<!-- Tab 1: General Details -->
|
||||||
<mat-tab>
|
<mat-tab>
|
||||||
<ng-template mat-tab-label>
|
<ng-template mat-tab-label>
|
||||||
@@ -58,8 +63,18 @@
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
@if (dobChanged) {
|
||||||
|
<div class="rekyc-alert">
|
||||||
|
<mat-icon class="rekyc-alert__icon">warning</mat-icon>
|
||||||
|
<div class="rekyc-alert__text">
|
||||||
|
<strong>Re-KYC Required:</strong> Date of birth was changed. Please re-verify PAN and update the remarks
|
||||||
|
below before proceeding.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
<div class="tab-section">
|
<div class="tab-section">
|
||||||
<mifosx-client-kyc-step #clientKyc
|
<mifosx-client-kyc-step
|
||||||
|
#clientKyc
|
||||||
[isNonIndividual]="legalFormType === 2"
|
[isNonIndividual]="legalFormType === 2"
|
||||||
[clientId]="clientId"
|
[clientId]="clientId"
|
||||||
[readOnly]="isTabReadOnly(1)"
|
[readOnly]="isTabReadOnly(1)"
|
||||||
@@ -115,7 +130,8 @@
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<mifosx-client-india-address-step #clientIndiaAddress
|
<mifosx-client-india-address-step
|
||||||
|
#clientIndiaAddress
|
||||||
[clientId]="clientId"
|
[clientId]="clientId"
|
||||||
[readOnly]="isTabReadOnly(addressTabIdx)"
|
[readOnly]="isTabReadOnly(addressTabIdx)"
|
||||||
></mifosx-client-india-address-step>
|
></mifosx-client-india-address-step>
|
||||||
@@ -141,7 +157,8 @@
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
<mifosx-client-family-members-step #clientFamilyMembers
|
<mifosx-client-family-members-step
|
||||||
|
#clientFamilyMembers
|
||||||
[clientTemplate]="clientDataAndTemplate"
|
[clientTemplate]="clientDataAndTemplate"
|
||||||
[clientId]="clientId"
|
[clientId]="clientId"
|
||||||
[readOnly]="isTabReadOnly(familyTabIdx)"
|
[readOnly]="isTabReadOnly(familyTabIdx)"
|
||||||
@@ -182,9 +199,18 @@
|
|||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@if (selectedTabIndex < totalTabs - 1) {
|
@if (selectedTabIndex < totalTabs - 1) {
|
||||||
<button mat-raised-button color="primary" (click)="onNext()">
|
<button mat-raised-button color="primary" (click)="onNext()" [disabled]="savingClient">
|
||||||
|
@if (savingClient) {
|
||||||
|
<mat-progress-spinner diameter="18" mode="indeterminate" class="m-r-10"></mat-progress-spinner>
|
||||||
|
Saving...
|
||||||
|
} @else if (isCurrentTabEdited()) {
|
||||||
|
<fa-icon icon="save" class="m-r-10"></fa-icon>
|
||||||
|
Save & Next
|
||||||
|
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||||
|
} @else {
|
||||||
{{ 'labels.buttons.Next' | translate }}
|
{{ 'labels.buttons.Next' | translate }}
|
||||||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||||||
|
}
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
@if (selectedTabIndex === totalTabs - 1 && areFormvalids()) {
|
@if (selectedTabIndex === totalTabs - 1 && areFormvalids()) {
|
||||||
|
|||||||
@@ -48,11 +48,11 @@
|
|||||||
.client-tabs {
|
.client-tabs {
|
||||||
background: var(--md-sys-color-surface, #fff);
|
background: var(--md-sys-color-surface, #fff);
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
|
||||||
|
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
.mat-mdc-tab-header {
|
.mat-mdc-tab-header {
|
||||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
border-bottom: 1px solid rgb(0 0 0 / 8%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.mat-mdc-tab {
|
.mat-mdc-tab {
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
margin-top: 20px;
|
margin-top: 20px;
|
||||||
padding: 16px 0;
|
padding: 16px 0;
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
border-top: 1px solid rgb(0 0 0 / 8%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-bar-right {
|
.action-bar-right {
|
||||||
@@ -106,7 +106,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cancel-btn {
|
.cancel-btn {
|
||||||
color: rgba(0, 0, 0, 0.6);
|
color: rgb(0 0 0 / 60%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ─── Read-only tab state ─── */
|
/* ─── Read-only tab state ─── */
|
||||||
@@ -149,3 +149,34 @@
|
|||||||
color: #ef4444 !important;
|
color: #ef4444 !important;
|
||||||
border: 1px solid #fca5a5 !important;
|
border: 1px solid #fca5a5 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Re-KYC Alert Banner ─── */
|
||||||
|
.rekyc-alert {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 14px 18px;
|
||||||
|
margin: 0 0 16px;
|
||||||
|
background: #fef3c7;
|
||||||
|
border: 1px solid #f59e0b;
|
||||||
|
border-radius: 8px;
|
||||||
|
color: #92400e;
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
color: #f59e0b;
|
||||||
|
font-size: 22px;
|
||||||
|
width: 22px;
|
||||||
|
height: 22px;
|
||||||
|
margin-top: 1px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
font-size: 13.5px;
|
||||||
|
line-height: 1.5;
|
||||||
|
|
||||||
|
strong {
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,10 +9,12 @@
|
|||||||
/** Angular Imports */
|
/** Angular Imports */
|
||||||
import { Component, QueryList, ViewChild, ViewChildren, inject } from '@angular/core';
|
import { Component, QueryList, ViewChild, ViewChildren, inject } from '@angular/core';
|
||||||
import { Router, ActivatedRoute } from '@angular/router';
|
import { Router, ActivatedRoute } from '@angular/router';
|
||||||
|
import { MatDialog } from '@angular/material/dialog';
|
||||||
|
|
||||||
/** Custom Services */
|
/** Custom Services */
|
||||||
import { ClientsService } from '../clients.service';
|
import { ClientsService } from '../clients.service';
|
||||||
import { SettingsService } from 'app/settings/settings.service';
|
import { SettingsService } from 'app/settings/settings.service';
|
||||||
|
import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component';
|
||||||
|
|
||||||
/** Custom Components */
|
/** Custom Components */
|
||||||
import { ClientGeneralStepComponent } from '../client-stepper/client-general-step/client-general-step.component';
|
import { ClientGeneralStepComponent } from '../client-stepper/client-general-step/client-general-step.component';
|
||||||
@@ -60,6 +62,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|||||||
export class EditClientComponent {
|
export class EditClientComponent {
|
||||||
private route = inject(ActivatedRoute);
|
private route = inject(ActivatedRoute);
|
||||||
private router = inject(Router);
|
private router = inject(Router);
|
||||||
|
private dialog = inject(MatDialog);
|
||||||
private clientsService = inject(ClientsService);
|
private clientsService = inject(ClientsService);
|
||||||
private settingsService = inject(SettingsService);
|
private settingsService = inject(SettingsService);
|
||||||
|
|
||||||
@@ -79,6 +82,9 @@ export class EditClientComponent {
|
|||||||
/** Track which tabs are in edit mode (false = read-only, true = editable) */
|
/** Track which tabs are in edit mode (false = read-only, true = editable) */
|
||||||
tabEditMode: Record<number, boolean> = {};
|
tabEditMode: Record<number, boolean> = {};
|
||||||
|
|
||||||
|
/** Flag: DOB was changed — requires re-KYC (PAN re-verification) */
|
||||||
|
dobChanged = false;
|
||||||
|
|
||||||
/** Client data and template (merged object from resolver) */
|
/** Client data and template (merged object from resolver) */
|
||||||
clientDataAndTemplate: any;
|
clientDataAndTemplate: any;
|
||||||
/** Client ID */
|
/** Client ID */
|
||||||
@@ -139,8 +145,13 @@ export class EditClientComponent {
|
|||||||
return areValids;
|
return areValids;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Whether the current tab has unsaved edits */
|
||||||
|
isCurrentTabEdited(): boolean {
|
||||||
|
return this.tabEditMode[this.selectedTabIndex] === true;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Next button handler — just navigates between tabs (client already exists in edit mode).
|
* Next button handler — saves changes via API if the current tab was edited, then moves to next tab.
|
||||||
*/
|
*/
|
||||||
onNext() {
|
onNext() {
|
||||||
if (this.selectedTabIndex === 0) {
|
if (this.selectedTabIndex === 0) {
|
||||||
@@ -155,8 +166,270 @@ export class EditClientComponent {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If tab was edited, confirm and save before moving on
|
||||||
|
if (this.isCurrentTabEdited()) {
|
||||||
|
this.confirmAndSaveTab();
|
||||||
|
} else {
|
||||||
this.selectedTabIndex = this.selectedTabIndex + 1;
|
this.selectedTabIndex = this.selectedTabIndex + 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show confirmation dialog and save the current tab's data via API.
|
||||||
|
* Shows a special warning if DOB was changed (re-KYC required).
|
||||||
|
*/
|
||||||
|
private confirmAndSaveTab() {
|
||||||
|
let heading = 'Confirm Edit';
|
||||||
|
let dialogContext = 'Are you sure you want to save the changes';
|
||||||
|
let dialogType = 'basic';
|
||||||
|
|
||||||
|
// Check if DOB changed on the General tab — warn about re-KYC
|
||||||
|
if (this.selectedTabIndex === 0) {
|
||||||
|
const changes = this.detectSensitiveFieldChanges(this.clientDataAndTemplate);
|
||||||
|
if (changes.dobChanged) {
|
||||||
|
heading = 'Re-KYC Required';
|
||||||
|
dialogContext =
|
||||||
|
'Date of birth has been changed. This will require PAN re-verification in the KYC section. A remark will be added automatically. Do you want to proceed';
|
||||||
|
dialogType = 'warn';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const dialogRef = this.dialog.open(ConfirmationDialogComponent, {
|
||||||
|
data: { heading, dialogContext, type: dialogType }
|
||||||
|
});
|
||||||
|
|
||||||
|
dialogRef.afterClosed().subscribe((response: any) => {
|
||||||
|
if (response?.confirm) {
|
||||||
|
this.saveCurrentTab();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the current tab's data via API, then advance to the next tab.
|
||||||
|
*/
|
||||||
|
private saveCurrentTab() {
|
||||||
|
const tabIndex = this.selectedTabIndex;
|
||||||
|
|
||||||
|
// General Details tab (always index 0)
|
||||||
|
if (tabIndex === 0) {
|
||||||
|
const locale = this.settingsService.language.code;
|
||||||
|
const dateFormat = this.settingsService.dateFormat;
|
||||||
|
const clientData = { ...this.client, dateFormat, locale };
|
||||||
|
delete clientData.officeId;
|
||||||
|
|
||||||
|
// Detect changes to sensitive fields before saving
|
||||||
|
const originalData = this.clientDataAndTemplate;
|
||||||
|
const changedFields = this.detectSensitiveFieldChanges(originalData);
|
||||||
|
|
||||||
|
this.savingClient = true;
|
||||||
|
this.clientsService.updateClient(this.clientId, clientData).subscribe({
|
||||||
|
next: () => {
|
||||||
|
this.savingClient = false;
|
||||||
|
this.tabEditMode[tabIndex] = false;
|
||||||
|
|
||||||
|
// Log mobile/email changes as client notes for risk audit
|
||||||
|
this.logFieldChangeNotes(changedFields);
|
||||||
|
|
||||||
|
// If DOB changed, flag re-KYC requirement and reset PAN verification
|
||||||
|
if (changedFields.dobChanged) {
|
||||||
|
this.dobChanged = true;
|
||||||
|
this.tabEditMode[1] = true; // auto-enable KYC tab for re-verification
|
||||||
|
this.flagReKycRequired(changedFields.oldDob, changedFields.newDob);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update original data snapshot so subsequent edits compare against latest
|
||||||
|
if (changedFields.mobileChanged) {
|
||||||
|
originalData.mobileNo = changedFields.newMobile;
|
||||||
|
}
|
||||||
|
if (changedFields.emailChanged) {
|
||||||
|
originalData.emailAddress = changedFields.newEmail;
|
||||||
|
}
|
||||||
|
if (changedFields.dobChanged) {
|
||||||
|
originalData.dateOfBirth = changedFields.newDob;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.selectedTabIndex = this.selectedTabIndex + 1;
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.savingClient = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// KYC tab (always index 1)
|
||||||
|
if (tabIndex === 1) {
|
||||||
|
this.savingClient = true;
|
||||||
|
|
||||||
|
// Upload photo if selected
|
||||||
|
if (this.clientKycStep?.photographFile) {
|
||||||
|
this.clientsService
|
||||||
|
.uploadClientProfileImage(this.clientId, this.clientKycStep.photographFile)
|
||||||
|
.subscribe({ error: () => {} });
|
||||||
|
}
|
||||||
|
// Upload signature if selected
|
||||||
|
if (this.clientKycStep?.signatureFile) {
|
||||||
|
this.clientsService
|
||||||
|
.uploadClientSignatureImage(this.clientId, this.clientKycStep.signatureFile)
|
||||||
|
.subscribe({ error: () => {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save KYC data — use correct API based on KYC state
|
||||||
|
if (this.clientKycStep?.hasKycData) {
|
||||||
|
const kycData = this.clientKycStep.kycData;
|
||||||
|
const kycId = this.clientKycStep.kycId;
|
||||||
|
const isVerified = this.clientKycStep.isKycVerified;
|
||||||
|
|
||||||
|
// Validate remarks are present for verified KYC
|
||||||
|
if (isVerified && !kycData.remarks?.trim()) {
|
||||||
|
this.savingClient = false;
|
||||||
|
this.clientKycStep.kycForm.get('remarks')?.markAsTouched();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let saveObs;
|
||||||
|
if (kycId && isVerified) {
|
||||||
|
// VERIFIED → re-edit command
|
||||||
|
const { kycType, ...updatePayload } = kycData;
|
||||||
|
saveObs = this.clientsService.reEditClientKyc(this.clientId, kycId, updatePayload);
|
||||||
|
} else if (kycId) {
|
||||||
|
// Existing but not verified → normal PUT
|
||||||
|
const { kycType, ...updatePayload } = kycData;
|
||||||
|
saveObs = this.clientsService.updateClientKyc(this.clientId, kycId, updatePayload);
|
||||||
|
} else {
|
||||||
|
// New → POST
|
||||||
|
saveObs = this.clientsService.saveClientKyc(this.clientId, kycData);
|
||||||
|
}
|
||||||
|
|
||||||
|
saveObs.subscribe({
|
||||||
|
next: (res: any) => {
|
||||||
|
this.savingClient = false;
|
||||||
|
this.tabEditMode[tabIndex] = false;
|
||||||
|
// If re-edit, update KYC component state
|
||||||
|
if (isVerified && this.clientKycStep) {
|
||||||
|
this.clientKycStep.kycStatus = 200;
|
||||||
|
this.clientKycStep.panVerifyStatus = 'NONE';
|
||||||
|
this.clientKycStep.panVerifyName = '';
|
||||||
|
this.clientKycStep.panVerifiedOn = '';
|
||||||
|
}
|
||||||
|
if (!kycId && res?.resourceId) {
|
||||||
|
this.clientKycStep.kycId = res.resourceId.toString();
|
||||||
|
}
|
||||||
|
this.selectedTabIndex = this.selectedTabIndex + 1;
|
||||||
|
},
|
||||||
|
error: () => {
|
||||||
|
this.savingClient = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.savingClient = false;
|
||||||
|
this.tabEditMode[tabIndex] = false;
|
||||||
|
this.selectedTabIndex = this.selectedTabIndex + 1;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Address tab
|
||||||
|
const addressTabIdx = this.legalFormType === 2 ? 3 : 2;
|
||||||
|
if (tabIndex === addressTabIdx) {
|
||||||
|
this.savingClient = true;
|
||||||
|
if (this.clientIndiaAddressStep?.hasAddressData) {
|
||||||
|
this.saveClientAddresses(this.clientId);
|
||||||
|
}
|
||||||
|
this.savingClient = false;
|
||||||
|
this.tabEditMode[tabIndex] = false;
|
||||||
|
this.selectedTabIndex = this.selectedTabIndex + 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Family tab
|
||||||
|
const familyTabIdx = this.legalFormType === 2 ? 4 : 3;
|
||||||
|
if (tabIndex === familyTabIdx) {
|
||||||
|
this.savingClient = true;
|
||||||
|
if (this.clientFamilyMembersStep?.hasFamilyMembers) {
|
||||||
|
this.saveClientFamilyMembers(this.clientId);
|
||||||
|
}
|
||||||
|
this.savingClient = false;
|
||||||
|
this.tabEditMode[tabIndex] = false;
|
||||||
|
this.selectedTabIndex = this.selectedTabIndex + 1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default: just advance
|
||||||
|
this.tabEditMode[tabIndex] = false;
|
||||||
|
this.selectedTabIndex = this.selectedTabIndex + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compare current form values with original data to detect changes in DOB, mobile, email.
|
||||||
|
*/
|
||||||
|
private detectSensitiveFieldChanges(originalData: any) {
|
||||||
|
const form = this.clientGeneralForm;
|
||||||
|
const newMobile = form?.get('mobileNo')?.value || '';
|
||||||
|
const newEmail = form?.get('emailAddress')?.value || '';
|
||||||
|
const newDobRaw = form?.get('dateOfBirth')?.value;
|
||||||
|
const newDob = newDobRaw instanceof Date ? newDobRaw.toISOString().split('T')[0] : newDobRaw || '';
|
||||||
|
|
||||||
|
const oldMobile = originalData.mobileNo || '';
|
||||||
|
const oldEmail = originalData.emailAddress || '';
|
||||||
|
const oldDobRaw = originalData.dateOfBirth;
|
||||||
|
const oldDob = Array.isArray(oldDobRaw)
|
||||||
|
? `${oldDobRaw[0]}-${String(oldDobRaw[1]).padStart(2, '0')}-${String(oldDobRaw[2]).padStart(2, '0')}`
|
||||||
|
: oldDobRaw instanceof Date
|
||||||
|
? oldDobRaw.toISOString().split('T')[0]
|
||||||
|
: oldDobRaw || '';
|
||||||
|
|
||||||
|
return {
|
||||||
|
mobileChanged: newMobile !== oldMobile,
|
||||||
|
emailChanged: newEmail !== oldEmail,
|
||||||
|
dobChanged: newDob !== oldDob,
|
||||||
|
oldMobile,
|
||||||
|
newMobile,
|
||||||
|
oldEmail,
|
||||||
|
newEmail,
|
||||||
|
oldDob,
|
||||||
|
newDob
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save audit notes for mobile/email changes via the client notes API.
|
||||||
|
*/
|
||||||
|
private logFieldChangeNotes(changes: any) {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
|
||||||
|
if (changes.mobileChanged) {
|
||||||
|
const note = `[AUDIT] Mobile number changed from "${changes.oldMobile}" to "${changes.newMobile}" on ${timestamp}`;
|
||||||
|
this.clientsService.createClientNote(this.clientId, { note }).subscribe({ error: () => {} });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (changes.emailChanged) {
|
||||||
|
const note = `[AUDIT] Email address changed from "${changes.oldEmail}" to "${changes.newEmail}" on ${timestamp}`;
|
||||||
|
this.clientsService.createClientNote(this.clientId, { note }).subscribe({ error: () => {} });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When DOB changes, reset PAN verification in KYC step and pre-fill remark for re-KYC.
|
||||||
|
*/
|
||||||
|
private flagReKycRequired(oldDob: string, newDob: string) {
|
||||||
|
// Reset PAN verification status so user must re-verify
|
||||||
|
if (this.clientKycStep) {
|
||||||
|
this.clientKycStep.panVerifyStatus = 'NONE';
|
||||||
|
this.clientKycStep.panVerifyName = '';
|
||||||
|
this.clientKycStep.panVerifiedOn = '';
|
||||||
|
// Pre-fill remarks with re-KYC note
|
||||||
|
const remarkText = `[Re-KYC Required] Date of birth changed from ${oldDob} to ${newDob}. PAN re-verification is mandatory.`;
|
||||||
|
this.clientKycStep.kycForm.get('remarks')?.setValue(remarkText);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Also log DOB change as an audit note
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
const note = `[AUDIT] Date of birth changed from "${oldDob}" to "${newDob}" on ${timestamp}. Re-KYC (PAN re-verification) required.`;
|
||||||
|
this.clientsService.createClientNote(this.clientId, { note }).subscribe({ error: () => {} });
|
||||||
|
}
|
||||||
|
|
||||||
legalFormChange(eventData: { legalForm: number }) {
|
legalFormChange(eventData: { legalForm: number }) {
|
||||||
this.legalFormType = eventData.legalForm;
|
this.legalFormType = eventData.legalForm;
|
||||||
@@ -185,12 +458,16 @@ export class EditClientComponent {
|
|||||||
|
|
||||||
// Upload photo if selected
|
// Upload photo if selected
|
||||||
if (this.clientKycStep?.photographFile) {
|
if (this.clientKycStep?.photographFile) {
|
||||||
this.clientsService.uploadClientProfileImage(this.clientId, this.clientKycStep.photographFile).subscribe({ error: () => {} });
|
this.clientsService
|
||||||
|
.uploadClientProfileImage(this.clientId, this.clientKycStep.photographFile)
|
||||||
|
.subscribe({ error: () => {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload signature if selected
|
// Upload signature if selected
|
||||||
if (this.clientKycStep?.signatureFile) {
|
if (this.clientKycStep?.signatureFile) {
|
||||||
this.clientsService.uploadClientSignatureImage(this.clientId, this.clientKycStep.signatureFile).subscribe({ error: () => {} });
|
this.clientsService
|
||||||
|
.uploadClientSignatureImage(this.clientId, this.clientKycStep.signatureFile)
|
||||||
|
.subscribe({ error: () => {} });
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save addresses from India address step
|
// Save addresses from India address step
|
||||||
@@ -205,7 +482,20 @@ export class EditClientComponent {
|
|||||||
|
|
||||||
// Save/update KYC data if available
|
// Save/update KYC data if available
|
||||||
if (this.clientKycStep?.hasKycData) {
|
if (this.clientKycStep?.hasKycData) {
|
||||||
this.clientsService.saveClientKyc(this.clientId, this.clientKycStep.kycData).subscribe({
|
const kycData = this.clientKycStep.kycData;
|
||||||
|
const kycId = this.clientKycStep.kycId;
|
||||||
|
const isVerified = this.clientKycStep.isKycVerified;
|
||||||
|
let kycObs;
|
||||||
|
if (kycId && isVerified) {
|
||||||
|
const { kycType, ...updatePayload } = kycData;
|
||||||
|
kycObs = this.clientsService.reEditClientKyc(this.clientId, kycId, updatePayload);
|
||||||
|
} else if (kycId) {
|
||||||
|
const { kycType, ...updatePayload } = kycData;
|
||||||
|
kycObs = this.clientsService.updateClientKyc(this.clientId, kycId, updatePayload);
|
||||||
|
} else {
|
||||||
|
kycObs = this.clientsService.saveClientKyc(this.clientId, kycData);
|
||||||
|
}
|
||||||
|
kycObs.subscribe({
|
||||||
next: () => navigateBack(),
|
next: () => navigateBack(),
|
||||||
error: () => navigateBack()
|
error: () => navigateBack()
|
||||||
});
|
});
|
||||||
@@ -225,16 +515,36 @@ export class EditClientComponent {
|
|||||||
private saveClientAddresses(clientId: string) {
|
private saveClientAddresses(clientId: string) {
|
||||||
const addressData = this.clientIndiaAddressStep.addressData;
|
const addressData = this.clientIndiaAddressStep.addressData;
|
||||||
const typeNameMap: Record<string, string[]> = {
|
const typeNameMap: Record<string, string[]> = {
|
||||||
permanent: ['Permanent', 'Permanent Address'],
|
permanent: [
|
||||||
current: ['Residential', 'Current', 'Current / Residential', 'Communication'],
|
'Permanent',
|
||||||
office: ['Office', 'Office / Work', 'Business', 'Work'],
|
'Permanent Address'
|
||||||
correspondence: ['Correspondence', 'Mailing', 'Postal']
|
],
|
||||||
|
current: [
|
||||||
|
'Residential',
|
||||||
|
'Current',
|
||||||
|
'Current / Residential',
|
||||||
|
'Communication'
|
||||||
|
],
|
||||||
|
office: [
|
||||||
|
'Office',
|
||||||
|
'Office / Work',
|
||||||
|
'Business',
|
||||||
|
'Work'
|
||||||
|
],
|
||||||
|
correspondence: [
|
||||||
|
'Correspondence',
|
||||||
|
'Mailing',
|
||||||
|
'Postal'
|
||||||
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
this.clientsService.getClientAddressTemplate().subscribe({
|
this.clientsService.getClientAddressTemplate().subscribe({
|
||||||
next: (template: any) => {
|
next: (template: any) => {
|
||||||
const typeOptions = template?.addressTypeIdOptions || [];
|
const typeOptions = template?.addressTypeIdOptions || [];
|
||||||
for (const [tab, addr] of Object.entries(addressData)) {
|
for (const [
|
||||||
|
tab,
|
||||||
|
addr
|
||||||
|
] of Object.entries(addressData)) {
|
||||||
if (!addr || !(addr as any).addressLine1) continue;
|
if (!addr || !(addr as any).addressLine1) continue;
|
||||||
|
|
||||||
const possibleNames = typeNameMap[tab] || [tab];
|
const possibleNames = typeNameMap[tab] || [tab];
|
||||||
@@ -259,8 +569,8 @@ export class EditClientComponent {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if ((addr as any).state && template?.stateProvinceIdOptions?.length) {
|
if ((addr as any).state && template?.stateProvinceIdOptions?.length) {
|
||||||
const stateMatch = template.stateProvinceIdOptions.find((s: any) =>
|
const stateMatch = template.stateProvinceIdOptions.find(
|
||||||
s.name?.toLowerCase() === (addr as any).state?.toLowerCase()
|
(s: any) => s.name?.toLowerCase() === (addr as any).state?.toLowerCase()
|
||||||
);
|
);
|
||||||
if (stateMatch) payload.stateProvinceId = stateMatch.id;
|
if (stateMatch) payload.stateProvinceId = stateMatch.id;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,6 +90,15 @@ $color: #e2e4ec;
|
|||||||
.sidenav {
|
.sidenav {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
|
// Constrain routed page content so tables/cards shrink when sidebar opens
|
||||||
|
mifosx-content {
|
||||||
|
display: block;
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== 2-Row Main Header =====
|
// ===== 2-Row Main Header =====
|
||||||
@@ -142,15 +151,28 @@ $gray-700: #334155;
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
letter-spacing: 0.01em;
|
letter-spacing: 0.01em;
|
||||||
transition: background 0.12s, color 0.12s;
|
transition:
|
||||||
|
background 0.12s,
|
||||||
|
color 0.12s;
|
||||||
user-select: none;
|
user-select: none;
|
||||||
border-right: 1px solid $gray-200;
|
border-right: 1px solid $gray-200;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-family: 'DM Sans', 'Roboto', sans-serif;
|
font-family: 'DM Sans', Roboto, sans-serif;
|
||||||
|
|
||||||
&:last-child { border-right: none; }
|
&:last-child {
|
||||||
&:hover { background: $brand-hover; color: $brand; }
|
border-right: none;
|
||||||
&.active { background: $brand-light; color: $brand; font-weight: 600; }
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $brand-hover;
|
||||||
|
color: $brand;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.active {
|
||||||
|
background: $brand-light;
|
||||||
|
color: $brand;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
&.active::before {
|
&.active::before {
|
||||||
content: '';
|
content: '';
|
||||||
@@ -178,7 +200,9 @@ $gray-700: #334155;
|
|||||||
.ni-caret {
|
.ni-caret {
|
||||||
font-size: 8px;
|
font-size: 8px;
|
||||||
opacity: 0.3;
|
opacity: 0.3;
|
||||||
transition: transform 0.18s, opacity 0.18s;
|
transition:
|
||||||
|
transform 0.18s,
|
||||||
|
opacity 0.18s;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|
||||||
.ni:hover & {
|
.ni:hover & {
|
||||||
@@ -211,7 +235,7 @@ $gray-700: #334155;
|
|||||||
min-height: 34px !important;
|
min-height: 34px !important;
|
||||||
padding: 0 14px !important;
|
padding: 0 14px !important;
|
||||||
color: $gray-700 !important;
|
color: $gray-700 !important;
|
||||||
font-family: 'DM Sans', 'Roboto', sans-serif !important;
|
font-family: 'DM Sans', Roboto, sans-serif !important;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background: $brand-hover !important;
|
background: $brand-hover !important;
|
||||||
@@ -266,7 +290,7 @@ $gray-700: #334155;
|
|||||||
width: 860px;
|
width: 860px;
|
||||||
max-width: 95vw;
|
max-width: 95vw;
|
||||||
padding: 20px 24px;
|
padding: 20px 24px;
|
||||||
box-shadow: 0 6px 24px rgba(0, 0, 0, 0.22);
|
box-shadow: 0 6px 24px rgb(0 0 0 / 22%);
|
||||||
border-radius: 0 0 10px 10px;
|
border-radius: 0 0 10px 10px;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
border-top: 3px solid #0d47a1;
|
border-top: 3px solid #0d47a1;
|
||||||
@@ -307,7 +331,7 @@ $gray-700: #334155;
|
|||||||
outline: none;
|
outline: none;
|
||||||
border-color: #0d47a1;
|
border-color: #0d47a1;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
box-shadow: 0 0 0 3px rgba(13, 71, 161, 0.1);
|
box-shadow: 0 0 0 3px rgb(13 71 161 / 10%);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,13 +6,42 @@
|
|||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<h1 mat-dialog-title>{{ data.heading }}</h1>
|
<div class="confirm-dialog" [class]="'confirm-dialog confirm-dialog--' + (data.type || 'basic')">
|
||||||
<div mat-dialog-content>
|
<!-- Icon + Title -->
|
||||||
<p>{{ data.dialogContext }} ?</p>
|
<div class="confirm-header">
|
||||||
|
<div class="confirm-header__icon-wrap">
|
||||||
|
@switch (data.type) {
|
||||||
|
@case ('warn') {
|
||||||
|
<i class="fa fa-exclamation-triangle"></i>
|
||||||
|
}
|
||||||
|
@case ('Strong') {
|
||||||
|
<i class="fa fa-trash-alt"></i>
|
||||||
|
}
|
||||||
|
@default {
|
||||||
|
<i class="fa fa-pen-to-square"></i>
|
||||||
|
}
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
<mat-dialog-actions align="end">
|
<h2 class="confirm-header__title">{{ data.heading }}</h2>
|
||||||
<button mat-raised-button mat-dialog-close>{{ 'labels.buttons.Cancel' | translate }}</button>
|
</div>
|
||||||
<button mat-raised-button color="{{ color }}" [mat-dialog-close]="{ confirm: true }">
|
|
||||||
|
<!-- Body -->
|
||||||
|
<mat-dialog-content class="confirm-body">
|
||||||
|
<p class="confirm-body__text">{{ data.dialogContext }}</p>
|
||||||
|
</mat-dialog-content>
|
||||||
|
|
||||||
|
<!-- Actions -->
|
||||||
|
<mat-dialog-actions class="confirm-actions">
|
||||||
|
<button mat-stroked-button class="confirm-actions__cancel" mat-dialog-close>
|
||||||
|
{{ 'labels.buttons.Cancel' | translate }}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
mat-flat-button
|
||||||
|
class="confirm-actions__confirm confirm-actions__confirm--{{ data.type || 'basic' }}"
|
||||||
|
[mat-dialog-close]="{ confirm: true }"
|
||||||
|
>
|
||||||
|
<i class="fa fa-check confirm-actions__btn-icon"></i>
|
||||||
{{ 'labels.buttons.Confirm' | translate }}
|
{{ 'labels.buttons.Confirm' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</mat-dialog-actions>
|
</mat-dialog-actions>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -5,3 +5,145 @@
|
|||||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
$primary: #1a73e8;
|
||||||
|
$primary-light: #e8f0fe;
|
||||||
|
$amber: #f59e0b;
|
||||||
|
$amber-light: #fef7e0;
|
||||||
|
$red: #ef4444;
|
||||||
|
$red-light: #fef2f2;
|
||||||
|
$gray-100: #f1f3f4;
|
||||||
|
$gray-200: #e8eaed;
|
||||||
|
$gray-500: #9aa0a6;
|
||||||
|
$gray-700: #5f6368;
|
||||||
|
$gray-900: #202124;
|
||||||
|
$white: #fff;
|
||||||
|
|
||||||
|
.confirm-dialog {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
max-width: 460px;
|
||||||
|
min-width: 380px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Header ───
|
||||||
|
.confirm-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 14px;
|
||||||
|
padding: 22px 24px 16px;
|
||||||
|
border-bottom: 1px solid $gray-200;
|
||||||
|
|
||||||
|
&__icon-wrap {
|
||||||
|
width: 42px;
|
||||||
|
height: 42px;
|
||||||
|
border-radius: 10px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.confirm-dialog--basic & {
|
||||||
|
background: $primary-light;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: $primary;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-dialog--warn & {
|
||||||
|
background: $amber-light;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: $amber;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-dialog--Strong & {
|
||||||
|
background: $red-light;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: $red;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
font-size: 17px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: $gray-900;
|
||||||
|
letter-spacing: -0.2px;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Body ───
|
||||||
|
.confirm-body {
|
||||||
|
padding: 18px 24px !important;
|
||||||
|
overflow: visible !important;
|
||||||
|
max-height: none !important;
|
||||||
|
|
||||||
|
&__text {
|
||||||
|
font-size: 13.5px;
|
||||||
|
line-height: 1.65;
|
||||||
|
color: $gray-700;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Actions ───
|
||||||
|
.confirm-actions {
|
||||||
|
padding: 14px 24px 20px !important;
|
||||||
|
border-top: 1px solid $gray-200;
|
||||||
|
margin: 0 !important;
|
||||||
|
display: flex !important;
|
||||||
|
justify-content: flex-end !important;
|
||||||
|
align-items: center !important;
|
||||||
|
gap: 10px !important;
|
||||||
|
|
||||||
|
&__cancel {
|
||||||
|
border-radius: 8px !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
padding: 0 20px !important;
|
||||||
|
height: 38px !important;
|
||||||
|
color: $gray-700 !important;
|
||||||
|
border-color: $gray-200 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__confirm {
|
||||||
|
border-radius: 8px !important;
|
||||||
|
font-size: 13px !important;
|
||||||
|
font-weight: 600 !important;
|
||||||
|
padding: 0 24px !important;
|
||||||
|
height: 38px !important;
|
||||||
|
letter-spacing: 0.2px;
|
||||||
|
color: $white !important;
|
||||||
|
|
||||||
|
&--basic {
|
||||||
|
background: $primary !important;
|
||||||
|
box-shadow: 0 1px 3px rgba($primary, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--warn {
|
||||||
|
background: $amber !important;
|
||||||
|
box-shadow: 0 1px 3px rgba($amber, 0.3) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--Strong {
|
||||||
|
background: $red !important;
|
||||||
|
box-shadow: 0 1px 3px rgba($red, 0.3) !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&__btn-icon {
|
||||||
|
margin-right: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -7,22 +7,21 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
/** Angular Imports */
|
/** Angular Imports */
|
||||||
import { Component, OnInit, inject } from '@angular/core';
|
import { Component, inject } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
MatDialogRef,
|
MatDialogRef,
|
||||||
MAT_DIALOG_DATA,
|
MAT_DIALOG_DATA,
|
||||||
MatDialogTitle,
|
|
||||||
MatDialogContent,
|
MatDialogContent,
|
||||||
MatDialogActions,
|
MatDialogActions,
|
||||||
MatDialogClose
|
MatDialogClose
|
||||||
} from '@angular/material/dialog';
|
} from '@angular/material/dialog';
|
||||||
import { DialogData } from 'app/core/dialogs/dialog-data.model';
|
import { DialogData } from 'app/core/dialogs/dialog-data.model';
|
||||||
import { Dialogs } from 'app/core/dialogs/dialogs';
|
|
||||||
import { CdkScrollable } from '@angular/cdk/scrolling';
|
import { CdkScrollable } from '@angular/cdk/scrolling';
|
||||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete dialog component.
|
* Confirmation dialog component.
|
||||||
|
* Supports types: 'basic' (blue), 'warn' (amber), 'Strong' (red/destructive).
|
||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'mifosx-confirmation-dialog',
|
selector: 'mifosx-confirmation-dialog',
|
||||||
@@ -30,25 +29,13 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|||||||
styleUrls: ['./confirmation-dialog.component.scss'],
|
styleUrls: ['./confirmation-dialog.component.scss'],
|
||||||
imports: [
|
imports: [
|
||||||
...STANDALONE_SHARED_IMPORTS,
|
...STANDALONE_SHARED_IMPORTS,
|
||||||
MatDialogTitle,
|
|
||||||
CdkScrollable,
|
CdkScrollable,
|
||||||
MatDialogContent,
|
MatDialogContent,
|
||||||
MatDialogActions,
|
MatDialogActions,
|
||||||
MatDialogClose
|
MatDialogClose
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class ConfirmationDialogComponent implements OnInit {
|
export class ConfirmationDialogComponent {
|
||||||
dialogRef = inject<MatDialogRef<ConfirmationDialogComponent>>(MatDialogRef);
|
dialogRef = inject<MatDialogRef<ConfirmationDialogComponent>>(MatDialogRef);
|
||||||
private dialogs = inject(Dialogs);
|
|
||||||
data = inject<DialogData>(MAT_DIALOG_DATA);
|
data = inject<DialogData>(MAT_DIALOG_DATA);
|
||||||
|
|
||||||
color: string;
|
|
||||||
|
|
||||||
ngOnInit() {
|
|
||||||
this.setColor();
|
|
||||||
}
|
|
||||||
|
|
||||||
setColor() {
|
|
||||||
this.color = this.dialogs.setColor(this.data.type);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,9 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
border: 1px solid $border;
|
border: 1px solid $border;
|
||||||
box-shadow: 0 1px 3px rgb(0 0 0 / 5%);
|
box-shadow: 0 1px 3px rgb(0 0 0 / 5%);
|
||||||
|
min-width: 0;
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
|
||||||
// Legacy layout support
|
// Legacy layout support
|
||||||
.layout-column,
|
.layout-column,
|
||||||
@@ -45,14 +48,21 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
|
|
||||||
&.m-r-30 { margin-right: 12px; }
|
&.m-r-30 {
|
||||||
|
margin-right: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
mat-form-field {
|
mat-form-field {
|
||||||
min-width: 200px;
|
min-width: 200px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
|
|
||||||
&.search-box { width: auto; }
|
&.search-box {
|
||||||
+ mat-form-field { margin-left: 12px; }
|
width: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ mat-form-field {
|
||||||
|
margin-left: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-checkbox {
|
mat-checkbox {
|
||||||
@@ -60,7 +70,9 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
|
|
||||||
&.m-r-10 { margin-right: 8px; }
|
&.m-r-10 {
|
||||||
|
margin-right: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,14 +82,18 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
|
||||||
&.gap-25px { gap: 10px; }
|
&.gap-25px {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
||||||
fa-icon {
|
fa-icon {
|
||||||
&.m-r-10,
|
&.m-r-10,
|
||||||
&.m-r-20 { margin-right: 6px; }
|
&.m-r-20 {
|
||||||
|
margin-right: 6px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,11 +108,16 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
|
|
||||||
.select-row {
|
.select-row {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
&:hover { background-color: $hover-bg; }
|
|
||||||
|
&:hover {
|
||||||
|
background-color: $hover-bg;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
th,
|
th,
|
||||||
td { padding: 10px 16px; }
|
td {
|
||||||
|
padding: 10px 16px;
|
||||||
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
@@ -110,7 +131,10 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
|
|
||||||
tr {
|
tr {
|
||||||
border-bottom: 1px solid #f1f5f9;
|
border-bottom: 1px solid #f1f5f9;
|
||||||
&:last-child { border-bottom: none; }
|
|
||||||
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
td {
|
td {
|
||||||
@@ -125,7 +149,9 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
background: transparent;
|
background: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-progress-bar { margin: 0; }
|
mat-progress-bar {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ===== Modern Table Header =====
|
// ===== Modern Table Header =====
|
||||||
@@ -167,10 +193,21 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
span {
|
span {
|
||||||
border-radius: 2px;
|
border-radius: 2px;
|
||||||
|
|
||||||
&:nth-child(1) { background: $brand; }
|
&:nth-child(1) {
|
||||||
&:nth-child(2) { background: #0ea5e9; }
|
background: $brand;
|
||||||
&:nth-child(3) { background: #10b981; }
|
}
|
||||||
&:nth-child(4) { background: #cbd5e1; }
|
|
||||||
|
&:nth-child(2) {
|
||||||
|
background: #0ea5e9;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(3) {
|
||||||
|
background: #10b981;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(4) {
|
||||||
|
background: #cbd5e1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -198,7 +235,9 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
max-width: 360px;
|
max-width: 360px;
|
||||||
|
|
||||||
::ng-deep {
|
::ng-deep {
|
||||||
.mat-mdc-form-field-flex { height: 38px; }
|
.mat-mdc-form-field-flex {
|
||||||
|
height: 38px;
|
||||||
|
}
|
||||||
|
|
||||||
.mat-mdc-form-field-infix {
|
.mat-mdc-form-field-infix {
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
@@ -206,8 +245,13 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mdc-notched-outline {
|
.mdc-notched-outline {
|
||||||
.mdc-notched-outline__leading { border-radius: 8px 0 0 8px; }
|
.mdc-notched-outline__leading {
|
||||||
.mdc-notched-outline__trailing { border-radius: 0 8px 8px 0; }
|
border-radius: 8px 0 0 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mdc-notched-outline__trailing {
|
||||||
|
border-radius: 0 8px 8px 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.mat-mdc-form-field-icon-prefix {
|
.mat-mdc-form-field-icon-prefix {
|
||||||
@@ -215,7 +259,9 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
color: $text-muted;
|
color: $text-muted;
|
||||||
}
|
}
|
||||||
|
|
||||||
input { font-size: 13px; }
|
input {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -237,7 +283,10 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
height: 36px;
|
height: 36px;
|
||||||
padding: 0 16px;
|
padding: 0 16px;
|
||||||
|
|
||||||
fa-icon { margin-right: 6px; font-size: 12px; }
|
fa-icon {
|
||||||
|
margin-right: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-import {
|
.btn-import {
|
||||||
@@ -259,7 +308,7 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
.table-container {
|
.table-container {
|
||||||
border: 1px solid $border;
|
border: 1px solid $border;
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-loader {
|
.table-loader {
|
||||||
@@ -294,7 +343,9 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
border-bottom: 1px solid #f1f5f9;
|
border-bottom: 1px solid #f1f5f9;
|
||||||
transition: background 0.1s;
|
transition: background 0.1s;
|
||||||
|
|
||||||
&:last-child { border-bottom: none; }
|
&:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
}
|
||||||
|
|
||||||
&:nth-child(even) {
|
&:nth-child(even) {
|
||||||
background: #fafbfc;
|
background: #fafbfc;
|
||||||
@@ -314,7 +365,11 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
font-weight: 400;
|
font-weight: 400;
|
||||||
color: $text;
|
color: $text;
|
||||||
padding: 0 12px;
|
padding: 0 12px;
|
||||||
font-family: 'Inter', 'Roboto', -apple-system, sans-serif;
|
font-family:
|
||||||
|
Inter,
|
||||||
|
Roboto,
|
||||||
|
-apple-system,
|
||||||
|
sans-serif;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -324,9 +379,15 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
font-family: 'Inter', 'Roboto', -apple-system, sans-serif;
|
font-family:
|
||||||
|
Inter,
|
||||||
|
Roboto,
|
||||||
|
-apple-system,
|
||||||
|
sans-serif;
|
||||||
|
|
||||||
&:hover { text-decoration: underline; }
|
&:hover {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cell-mono {
|
.cell-mono {
|
||||||
@@ -350,7 +411,11 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
letter-spacing: 0.02em;
|
letter-spacing: 0.02em;
|
||||||
text-transform: capitalize;
|
text-transform: capitalize;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
font-family: 'Inter', 'Roboto', -apple-system, sans-serif;
|
font-family:
|
||||||
|
Inter,
|
||||||
|
Roboto,
|
||||||
|
-apple-system,
|
||||||
|
sans-serif;
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
@@ -367,7 +432,9 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
background: #ecfdf5 !important;
|
background: #ecfdf5 !important;
|
||||||
color: #059669 !important;
|
color: #059669 !important;
|
||||||
|
|
||||||
&::before { background: #059669; }
|
&::before {
|
||||||
|
background: #059669;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-active-overdue,
|
.status-active-overdue,
|
||||||
@@ -375,7 +442,9 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
background: #fef2f2 !important;
|
background: #fef2f2 !important;
|
||||||
color: #dc2626 !important;
|
color: #dc2626 !important;
|
||||||
|
|
||||||
&::before { background: #dc2626; }
|
&::before {
|
||||||
|
background: #dc2626;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-submitted-and-pending-approval,
|
.status-submitted-and-pending-approval,
|
||||||
@@ -383,14 +452,18 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
background: #fffbeb !important;
|
background: #fffbeb !important;
|
||||||
color: #d97706 !important;
|
color: #d97706 !important;
|
||||||
|
|
||||||
&::before { background: #d97706; }
|
&::before {
|
||||||
|
background: #d97706;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.status-overpaid {
|
.status-overpaid {
|
||||||
background: #eff6ff !important;
|
background: #eff6ff !important;
|
||||||
color: #2563eb !important;
|
color: #2563eb !important;
|
||||||
|
|
||||||
&::before { background: #2563eb; }
|
&::before {
|
||||||
|
background: #2563eb;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,8 +475,13 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
margin-right: 6px;
|
margin-right: 6px;
|
||||||
|
|
||||||
&.person { color: #2563eb; }
|
&.person {
|
||||||
&.entity { color: #7c3aed; }
|
color: #2563eb;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.entity {
|
||||||
|
color: #7c3aed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.legal-form-badge {
|
.legal-form-badge {
|
||||||
@@ -550,10 +628,15 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
min-width: 150px;
|
min-width: 150px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
|
|
||||||
+ mat-form-field { margin-left: 0; }
|
+ mat-form-field {
|
||||||
|
margin-left: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
mat-checkbox { margin-left: 0; margin-top: 8px; }
|
mat-checkbox {
|
||||||
|
margin-left: 0;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-button {
|
.action-button {
|
||||||
@@ -561,25 +644,50 @@ $mono: 'Roboto Mono', 'DM Mono', monospace;
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
|
|
||||||
&.gap-25px { gap: 10px; }
|
&.gap-25px {
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.table-actions { flex-direction: column; align-items: stretch; }
|
.table-actions {
|
||||||
.table-search { max-width: 100%; }
|
flex-direction: column;
|
||||||
.table-buttons { margin-left: 0; justify-content: stretch; }
|
align-items: stretch;
|
||||||
.table-buttons button { flex: 1; }
|
}
|
||||||
|
|
||||||
|
.table-search {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-buttons {
|
||||||
|
margin-left: 0;
|
||||||
|
justify-content: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table-buttons button {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy table styles
|
// Legacy table styles
|
||||||
table {
|
table {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
|
||||||
tr.select-row:hover { cursor: pointer; }
|
tr.select-row:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility classes
|
// Utility classes
|
||||||
.m-r-10 { margin-right: 10px !important; }
|
.m-r-10 {
|
||||||
.m-r-20 { margin-right: 20px !important; }
|
margin-right: 10px !important;
|
||||||
.m-r-30 { margin-right: 30px !important; }
|
}
|
||||||
|
|
||||||
|
.m-r-20 {
|
||||||
|
margin-right: 20px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.m-r-30 {
|
||||||
|
margin-right: 30px !important;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Copyright since 2025 Mifos Initiative
|
||||||
|
*
|
||||||
|
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
*/
|
||||||
|
|
||||||
// IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN!
|
// IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN!
|
||||||
/* tslint:disable */
|
/* tslint:disable */
|
||||||
export default {
|
export default {
|
||||||
'mifos_x': {
|
'mifos_x': {
|
||||||
'version': '260404',
|
'version': '260407',
|
||||||
'hash': 'gab0111142'
|
'hash': 'gb890d24bb'
|
||||||
},
|
},
|
||||||
'allow_switching_backend_instance': true
|
'allow_switching_backend_instance': true
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user