The demo-backup merge's git 3-way auto-resolve dropped several hunks without flagging conflicts, wherever demo had independently touched adjacent lines: the KYC Exception feature (client-kyc-step, kyc-tab, create-client, clients.service — checkbox, derivation logic, banners, contact-log endpoints), TDS report routes/menu links (reports-routing, shell), the Form 121 client tab (clients-routing, clients-view), the kyc-management exceptions dashboard, and the Form 121/15G-H register's real-data view integration. Restored by diffing demo-backup against dev per file and re-applying each genuine gap while preserving demo-backup's own independent work (Approval Center tab style, Loan Origination/Investment menus, form-97-98/risk-review report routes).
1549 lines
68 KiB
HTML
1549 lines
68 KiB
HTML
<!--
|
||
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="step-header">
|
||
<div class="step-header__left">
|
||
<fa-icon icon="id-card" class="step-header__icon"></fa-icon>
|
||
<div>
|
||
<h3 class="step-header__title">KYC & Documents</h3>
|
||
<p class="step-header__sub">RBI-compliant KYC verification — PAN, Aadhaar & document upload.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ SECTION: KYC Verification ═══ -->
|
||
<div class="section-block">
|
||
<h4 class="section-title">
|
||
<fa-icon icon="fingerprint" class="section-icon"></fa-icon>
|
||
KYC Verification
|
||
<span class="kyc-badge">KYC</span>
|
||
</h4>
|
||
|
||
<div class="form-grid">
|
||
<!-- ══ INDIVIDUAL KYC ══ -->
|
||
@if (!isNonIndividual) {
|
||
<!-- Minors don't hold a PAN card — skip the PAN checkbox/section entirely -->
|
||
@if (!isMinor) {
|
||
@if (readOnly) {
|
||
<!-- In view mode the mat-checkbox renders as a faded grey square
|
||
that reads as "blurry". Show a clean Yes/No pill instead. -->
|
||
<div class="form-field checkbox-field full-width">
|
||
<div class="ro-flag">
|
||
<span class="ro-flag__label">Client has PAN Card</span>
|
||
@if (kycForm.get('hasPan')?.value) {
|
||
<span class="ro-flag__chip ro-flag__chip--yes">
|
||
<mat-icon>check_circle</mat-icon>
|
||
Yes
|
||
</span>
|
||
} @else {
|
||
<span class="ro-flag__chip ro-flag__chip--no">
|
||
<mat-icon>cancel</mat-icon>
|
||
No
|
||
</span>
|
||
}
|
||
</div>
|
||
</div>
|
||
} @else {
|
||
<div class="form-field checkbox-field full-width">
|
||
<mat-checkbox formControlName="hasPan">Client has PAN Card</mat-checkbox>
|
||
</div>
|
||
}
|
||
}
|
||
|
||
<!-- Individual PAN with inline verify -->
|
||
@if (!isMinor && kycForm.get('hasPan')?.value) {
|
||
<div class="form-field verify-field">
|
||
<mat-form-field appearance="outline" class="verify-input">
|
||
<mat-label>PAN Number</mat-label>
|
||
<span matPrefix class="field-prefix-icon"><mat-icon>credit_card</mat-icon></span>
|
||
<input
|
||
matInput
|
||
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) {
|
||
<mat-error>Invalid PAN format</mat-error>
|
||
}
|
||
@if (panVerifyStatus === 'VERIFIED') {
|
||
<mat-icon matSuffix class="verify-icon-ok">check_circle</mat-icon>
|
||
}
|
||
@if (panVerifyStatus === 'FAILED') {
|
||
<mat-icon matSuffix class="verify-icon-fail">cancel</mat-icon>
|
||
}
|
||
<mat-hint>Format: ABCDE1234F</mat-hint>
|
||
</mat-form-field>
|
||
<div class="verify-action">
|
||
@if (panVerifyStatus === 'NONE' || panVerifyStatus === 'FAILED') {
|
||
<button
|
||
mat-raised-button
|
||
color="primary"
|
||
type="button"
|
||
class="verify-btn-inline"
|
||
[disabled]="
|
||
readOnly || !kycForm.get('panNumber')?.value || kycForm.get('panNumber')?.invalid || panVerifying
|
||
"
|
||
(click)="verifyPan()"
|
||
>
|
||
@if (panVerifying) {
|
||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||
} @else {
|
||
<fa-icon icon="shield-alt"></fa-icon> Verify PAN
|
||
}
|
||
</button>
|
||
}
|
||
@if (panVerifyStatus === 'VERIFIED') {
|
||
<span class="verify-chip verified"><fa-icon icon="check-circle"></fa-icon> Verified</span>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- Aadhaar with independent verify (Individual only) -->
|
||
<div class="form-field verify-field">
|
||
<mat-form-field appearance="outline" class="verify-input">
|
||
<mat-label>Aadhaar Number</mat-label>
|
||
<span matPrefix class="field-prefix-icon"><mat-icon>fingerprint</mat-icon></span>
|
||
<input
|
||
matInput
|
||
formControlName="aadhaarNumber"
|
||
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>
|
||
</button>
|
||
@if (kycForm.get('aadhaarNumber')?.hasError('pattern') && kycForm.get('aadhaarNumber')?.touched) {
|
||
<mat-error>Must be 12 digits</mat-error>
|
||
}
|
||
@if (aadhaarVerifyStatus === 'VERIFIED') {
|
||
<mat-icon matSuffix class="verify-icon-ok">check_circle</mat-icon>
|
||
}
|
||
</mat-form-field>
|
||
<div class="verify-action">
|
||
@if ((aadhaarVerifyStatus === 'NONE' || aadhaarVerifyStatus === 'FAILED') && !aadhaarOtpSent) {
|
||
<button
|
||
mat-raised-button
|
||
color="primary"
|
||
type="button"
|
||
class="verify-btn-inline"
|
||
[disabled]="
|
||
readOnly ||
|
||
!kycForm.get('aadhaarNumber')?.value ||
|
||
kycForm.get('aadhaarNumber')?.invalid ||
|
||
aadhaarVerifying
|
||
"
|
||
(click)="sendAadhaarOtp()"
|
||
>
|
||
@if (aadhaarVerifying) {
|
||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||
} @else {
|
||
<fa-icon icon="shield-alt"></fa-icon> Verify Aadhaar
|
||
}
|
||
</button>
|
||
}
|
||
@if (aadhaarOtpSent && aadhaarVerifyStatus !== 'VERIFIED') {
|
||
<button
|
||
mat-stroked-button
|
||
type="button"
|
||
class="change-number-btn"
|
||
[disabled]="readOnly"
|
||
(click)="changeAadhaarNumber()"
|
||
>
|
||
<mat-icon>edit</mat-icon> Change Number
|
||
</button>
|
||
}
|
||
@if (aadhaarVerifyStatus === 'VERIFIED') {
|
||
<span class="verify-chip verified"><fa-icon icon="check-circle"></fa-icon> Verified</span>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Aadhaar OTP Section -->
|
||
@if (aadhaarOtpSent && aadhaarVerifyStatus !== 'VERIFIED') {
|
||
<div class="otp-card full-width">
|
||
<div class="otp-card__header">
|
||
<div class="otp-card__header-icon">
|
||
<mat-icon>verified_user</mat-icon>
|
||
</div>
|
||
<div class="otp-card__header-text">
|
||
<span class="otp-card__title">Aadhaar OTP Verification</span>
|
||
<span class="otp-card__subtitle">A 6-digit OTP has been sent to the Aadhaar-linked mobile number</span>
|
||
</div>
|
||
</div>
|
||
<div class="otp-card__body">
|
||
<label class="otp-card__label">Enter OTP</label>
|
||
<div class="otp-digits" (paste)="onOtpPaste($event)">
|
||
@for (digit of otpDigits; track $index) {
|
||
<input
|
||
type="text"
|
||
inputmode="numeric"
|
||
maxlength="1"
|
||
class="otp-digit-box"
|
||
[class.filled]="digit"
|
||
[value]="digit"
|
||
(input)="onOtpDigitInput($index, $event)"
|
||
(keydown)="onOtpKeydown($index, $event)"
|
||
autocomplete="one-time-code"
|
||
/>
|
||
}
|
||
</div>
|
||
<div class="otp-card__actions">
|
||
<button
|
||
mat-flat-button
|
||
color="primary"
|
||
type="button"
|
||
class="otp-card__verify-btn"
|
||
[disabled]="readOnly || aadhaarOtp.length < 6 || aadhaarOtpVerifying"
|
||
(click)="verifyAadhaarOtp()"
|
||
>
|
||
@if (aadhaarOtpVerifying) {
|
||
<mat-progress-spinner diameter="16" mode="indeterminate" class="otp-spinner"></mat-progress-spinner>
|
||
Verifying...
|
||
} @else {
|
||
<fa-icon icon="check-circle" class="otp-btn-icon"></fa-icon>
|
||
Verify OTP
|
||
}
|
||
</button>
|
||
<div class="otp-card__resend">
|
||
@if (resendCountdown > 0) {
|
||
<span class="otp-card__countdown">
|
||
Resend in <strong>00:{{ resendCountdown.toString().padStart(2, '0') }}</strong>
|
||
</span>
|
||
} @else {
|
||
<span class="otp-card__resend-text">Didn't receive the code?</span>
|
||
<button
|
||
mat-button
|
||
type="button"
|
||
class="otp-card__resend-btn"
|
||
[disabled]="readOnly || aadhaarVerifying"
|
||
(click)="sendAadhaarOtp()"
|
||
>
|
||
Resend OTP
|
||
</button>
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
@if (aadhaarVerifyError) {
|
||
<div class="verify-error full-width">
|
||
<fa-icon icon="exclamation-circle"></fa-icon> {{ aadhaarVerifyError }}
|
||
</div>
|
||
}
|
||
<!-- ─── Unified Identity Verification Summary ─── -->
|
||
<!-- One polished card that consolidates PAN result, Aadhaar result, and -->
|
||
<!-- the PAN–Aadhaar link status into a single, scannable panel. -->
|
||
@if (panVerifyStatus === 'VERIFIED' || aadhaarVerifyStatus === 'VERIFIED') {
|
||
<div
|
||
class="idv-summary full-width"
|
||
[class.both]="panVerifyStatus === 'VERIFIED' && aadhaarVerifyStatus === 'VERIFIED'"
|
||
[class.partial]="
|
||
!(panVerifyStatus === 'VERIFIED' && aadhaarVerifyStatus === 'VERIFIED') &&
|
||
(panVerifyStatus === 'VERIFIED' || aadhaarVerifyStatus === 'VERIFIED')
|
||
"
|
||
>
|
||
<!-- Hero strip -->
|
||
<div class="idv-hero">
|
||
<div class="idv-hero__ic"><fa-icon icon="shield-alt"></fa-icon></div>
|
||
<div class="idv-hero__txt">
|
||
<div class="idv-hero__title">
|
||
@if (panVerifyStatus === 'VERIFIED' && aadhaarVerifyStatus === 'VERIFIED') {
|
||
Identity Verified
|
||
} @else if (panVerifyStatus === 'VERIFIED') {
|
||
PAN Verified
|
||
} @else {
|
||
Aadhaar Verified
|
||
}
|
||
</div>
|
||
<div class="idv-hero__sub">
|
||
@if (panVerifyStatus === 'VERIFIED' && aadhaarVerifyStatus === 'VERIFIED') {
|
||
Both PAN & Aadhaar confirmed against NSDL & UIDAI records.
|
||
} @else if (panVerifyStatus === 'VERIFIED') {
|
||
PAN confirmed against NSDL records. Aadhaar verification pending.
|
||
} @else {
|
||
Aadhaar confirmed via UIDAI OTP. PAN verification pending.
|
||
}
|
||
</div>
|
||
</div>
|
||
<div class="idv-hero__count">
|
||
<span class="idv-hero__count-num">
|
||
{{ (panVerifyStatus === 'VERIFIED' ? 1 : 0) + (aadhaarVerifyStatus === 'VERIFIED' ? 1 : 0) }}
|
||
</span>
|
||
<span class="idv-hero__count-of">/ 2</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Two-column body: PAN | Aadhaar -->
|
||
<div class="idv-body">
|
||
<!-- PAN column -->
|
||
<div class="idv-col" [class.muted]="panVerifyStatus !== 'VERIFIED'">
|
||
<div class="idv-col__head">
|
||
<fa-icon icon="id-card" class="idv-col__ic"></fa-icon>
|
||
<span class="idv-col__label">PAN</span>
|
||
@if (panVerifyStatus === 'VERIFIED') {
|
||
<span class="idv-chip ok"> <fa-icon icon="check-circle"></fa-icon> Verified </span>
|
||
} @else {
|
||
<span class="idv-chip pending">Pending</span>
|
||
}
|
||
</div>
|
||
@if (panVerifyStatus === 'VERIFIED') {
|
||
<div class="idv-col__rows">
|
||
<div class="idv-row">
|
||
<span class="idv-row__k">Name on PAN</span>
|
||
<span class="idv-row__v name">{{ panVerifyName }}</span>
|
||
</div>
|
||
<div class="idv-row">
|
||
<span class="idv-row__k">PAN Number</span>
|
||
<span class="idv-row__v mono">{{ kycForm.get('panNumber')?.value }}</span>
|
||
</div>
|
||
<div class="idv-row">
|
||
<span class="idv-row__k">Verified On</span>
|
||
<span class="idv-row__v">{{ panVerifiedOn }}</span>
|
||
</div>
|
||
</div>
|
||
} @else {
|
||
<div class="idv-col__empty">Enter PAN above and click <strong>Verify PAN</strong>.</div>
|
||
}
|
||
</div>
|
||
|
||
<!-- Aadhaar column -->
|
||
<div class="idv-col" [class.muted]="aadhaarVerifyStatus !== 'VERIFIED'">
|
||
<div class="idv-col__head">
|
||
<fa-icon icon="fingerprint" class="idv-col__ic"></fa-icon>
|
||
<span class="idv-col__label">Aadhaar</span>
|
||
@if (aadhaarVerifyStatus === 'VERIFIED') {
|
||
<span class="idv-chip ok"> <fa-icon icon="check-circle"></fa-icon> Verified </span>
|
||
} @else {
|
||
<span class="idv-chip pending">Pending</span>
|
||
}
|
||
</div>
|
||
@if (aadhaarVerifyStatus === 'VERIFIED') {
|
||
<div class="idv-col__rows">
|
||
<div class="idv-row">
|
||
<span class="idv-row__k">Name on Aadhaar</span>
|
||
<span class="idv-row__v name">{{ aadhaarVerifyName }}</span>
|
||
</div>
|
||
<div class="idv-row">
|
||
<span class="idv-row__k">Aadhaar Number</span>
|
||
<span class="idv-row__v mono"
|
||
>XXXX XXXX {{ (kycForm.get('aadhaarNumber')?.value || '').slice(-4) }}</span
|
||
>
|
||
</div>
|
||
<div class="idv-row">
|
||
<span class="idv-row__k">Channel</span>
|
||
<span class="idv-row__v">UIDAI OTP</span>
|
||
</div>
|
||
</div>
|
||
} @else {
|
||
<div class="idv-col__empty">Enter Aadhaar above and click <strong>Verify Aadhaar</strong>.</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- PAN ↔ Aadhaar link footer -->
|
||
@if (panAadhaarSeedingStatus) {
|
||
<div
|
||
class="idv-link"
|
||
[class.linked]="panAadhaarSeedingStatus === 'Y'"
|
||
[class.not-linked]="panAadhaarSeedingStatus === 'N'"
|
||
[class.exempt]="panAadhaarSeedingStatus === 'E'"
|
||
>
|
||
<fa-icon icon="link" class="idv-link__ic"></fa-icon>
|
||
<div class="idv-link__txt">
|
||
<span class="idv-link__label">PAN — Aadhaar Link</span>
|
||
<span class="idv-link__value">
|
||
@if (panAadhaarSeedingStatus === 'Y') {
|
||
Linked (Seeded)
|
||
} @else if (panAadhaarSeedingStatus === 'N') {
|
||
Not Linked
|
||
} @else if (panAadhaarSeedingStatus === 'E') {
|
||
Exempt
|
||
}
|
||
</span>
|
||
</div>
|
||
<span class="idv-link__hint">Income Tax Portal — Section 139AA</span>
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
}
|
||
|
||
<!-- ══ NON-INDIVIDUAL KYC ══ -->
|
||
@if (isNonIndividual) {
|
||
<!-- Incorporation Date -->
|
||
<mat-form-field appearance="outline" class="form-field" (click)="incorpDatePicker.open()">
|
||
<mat-label>Incorporation Date</mat-label>
|
||
<span matPrefix class="field-prefix-icon"><mat-icon>calendar_today</mat-icon></span>
|
||
<input matInput [max]="today" [matDatepicker]="incorpDatePicker" formControlName="incorporationDate" />
|
||
<mat-datepicker-toggle matSuffix [for]="incorpDatePicker"></mat-datepicker-toggle>
|
||
<mat-datepicker #incorpDatePicker></mat-datepicker>
|
||
</mat-form-field>
|
||
|
||
<!-- GSTIN (all non-individual) with Verify -->
|
||
<div class="form-field verify-field">
|
||
<mat-form-field appearance="outline" class="verify-input">
|
||
<mat-label>GSTIN Number</mat-label>
|
||
<span matPrefix class="field-prefix-icon"><mat-icon>receipt_long</mat-icon></span>
|
||
<input
|
||
matInput
|
||
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) {
|
||
<mat-error>Invalid GSTIN format (e.g. 22AAAAA0000A1Z5)</mat-error>
|
||
}
|
||
@if (gstinVerifyStatus === 'VERIFIED') {
|
||
<mat-icon matSuffix class="verify-icon-ok">check_circle</mat-icon>
|
||
}
|
||
@if (gstinVerifyStatus === 'FAILED') {
|
||
<mat-icon matSuffix class="verify-icon-fail">cancel</mat-icon>
|
||
}
|
||
</mat-form-field>
|
||
<div class="verify-action">
|
||
@if (gstinVerifyStatus === 'NONE' || gstinVerifyStatus === 'FAILED') {
|
||
<button
|
||
mat-raised-button
|
||
color="primary"
|
||
type="button"
|
||
class="verify-btn-inline"
|
||
[disabled]="
|
||
readOnly ||
|
||
!kycForm.get('gstinNumber')?.value ||
|
||
kycForm.get('gstinNumber')?.value.length < 15 ||
|
||
gstinVerifying ||
|
||
!clientId
|
||
"
|
||
(click)="verifyGstin()"
|
||
>
|
||
@if (gstinVerifying) {
|
||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||
} @else {
|
||
<fa-icon icon="shield-alt"></fa-icon> Verify GSTIN
|
||
}
|
||
</button>
|
||
}
|
||
@if (gstinVerifyStatus === 'VERIFIED') {
|
||
<span class="verify-chip verified"><fa-icon icon="check-circle"></fa-icon> Verified</span>
|
||
}
|
||
</div>
|
||
</div>
|
||
@if (gstinVerifyStatus === 'VERIFIED') {
|
||
<div class="pan-result-card full-width">
|
||
<div class="pan-result-header">
|
||
<fa-icon icon="check-circle" class="pan-result-ok-icon"></fa-icon>
|
||
<span>GSTIN Verified Successfully</span>
|
||
</div>
|
||
<div class="pan-result-body">
|
||
@if (gstinVerifyName) {
|
||
<div class="pan-result-row">
|
||
<span class="pan-result-label">Trade Name / Legal Name</span>
|
||
<span class="pan-result-value name">{{ gstinVerifyName }}</span>
|
||
</div>
|
||
}
|
||
@if (gstinVerifyState) {
|
||
<div class="pan-result-row">
|
||
<span class="pan-result-label">State</span>
|
||
<span class="pan-result-value">{{ gstinVerifyState }}</span>
|
||
</div>
|
||
}
|
||
<div class="pan-result-row">
|
||
<span class="pan-result-label">Verified On</span>
|
||
<span class="pan-result-value date">{{ gstinVerifiedOn }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- PAN — Proprietorship uses Individual PAN, others use Enterprise PAN -->
|
||
@if (constitutionMeta?.value === 'PROPRIETORSHIP') {
|
||
<!-- Proprietorship: Individual PAN (proprietor's personal PAN) -->
|
||
<div class="form-field verify-field">
|
||
<mat-form-field appearance="outline" class="verify-input">
|
||
<mat-label>PAN of Proprietor</mat-label>
|
||
<span matPrefix class="field-prefix-icon"><mat-icon>credit_card</mat-icon></span>
|
||
<input
|
||
matInput
|
||
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) {
|
||
<mat-error>Invalid PAN format</mat-error>
|
||
}
|
||
@if (panVerifyStatus === 'VERIFIED') {
|
||
<mat-icon matSuffix class="verify-icon-ok">check_circle</mat-icon>
|
||
}
|
||
@if (panVerifyStatus === 'FAILED') {
|
||
<mat-icon matSuffix class="verify-icon-fail">cancel</mat-icon>
|
||
}
|
||
<mat-hint>PAN of Proprietor (no separate entity PAN needed)</mat-hint>
|
||
</mat-form-field>
|
||
<div class="verify-action">
|
||
@if (panVerifyStatus === 'NONE' || panVerifyStatus === 'FAILED') {
|
||
<button
|
||
mat-raised-button
|
||
color="primary"
|
||
type="button"
|
||
class="verify-btn-inline"
|
||
[disabled]="
|
||
readOnly ||
|
||
!kycForm.get('panNumber')?.value ||
|
||
kycForm.get('panNumber')?.invalid ||
|
||
panVerifying ||
|
||
!clientId
|
||
"
|
||
(click)="verifyPan()"
|
||
>
|
||
@if (panVerifying) {
|
||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||
} @else {
|
||
<fa-icon icon="shield-alt"></fa-icon> Verify PAN
|
||
}
|
||
</button>
|
||
}
|
||
@if (panVerifyStatus === 'VERIFIED') {
|
||
<span class="verify-chip verified"><fa-icon icon="check-circle"></fa-icon> Verified</span>
|
||
}
|
||
</div>
|
||
</div>
|
||
@if (panVerifyStatus === 'VERIFIED') {
|
||
<div class="pan-result-card full-width">
|
||
<div class="pan-result-header">
|
||
<fa-icon icon="check-circle" class="pan-result-ok-icon"></fa-icon>
|
||
<span>PAN Verified Successfully</span>
|
||
</div>
|
||
<div class="pan-result-body">
|
||
<div class="pan-result-row">
|
||
<span class="pan-result-label">Proprietor Name (as per PAN)</span>
|
||
<span class="pan-result-value name">{{ panVerifyName }}</span>
|
||
</div>
|
||
<div class="pan-result-row">
|
||
<span class="pan-result-label">Verified On</span>
|
||
<span class="pan-result-value date">{{ panVerifiedOn }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
} @else {
|
||
<!-- Non-Proprietorship: Enterprise/Company PAN -->
|
||
<div class="form-field verify-field">
|
||
<mat-form-field appearance="outline" class="verify-input">
|
||
<mat-label>{{ constitutionMeta?.panLabel || 'PAN of Entity' }}</mat-label>
|
||
<span matPrefix class="field-prefix-icon"><mat-icon>credit_card</mat-icon></span>
|
||
<input
|
||
matInput
|
||
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) {
|
||
<mat-error>Invalid PAN format</mat-error>
|
||
}
|
||
@if (entPanVerifyStatus === 'VERIFIED') {
|
||
<mat-icon matSuffix class="verify-icon-ok">check_circle</mat-icon>
|
||
}
|
||
@if (entPanVerifyStatus === 'FAILED') {
|
||
<mat-icon matSuffix class="verify-icon-fail">cancel</mat-icon>
|
||
}
|
||
<mat-hint>{{ constitutionMeta?.panHint || 'PAN mandatory' }}</mat-hint>
|
||
</mat-form-field>
|
||
<div class="verify-action">
|
||
@if (entPanVerifyStatus === 'NONE' || entPanVerifyStatus === 'FAILED') {
|
||
<button
|
||
mat-raised-button
|
||
color="primary"
|
||
type="button"
|
||
class="verify-btn-inline"
|
||
[disabled]="
|
||
readOnly ||
|
||
!kycForm.get('enterprisePan')?.value ||
|
||
kycForm.get('enterprisePan')?.invalid ||
|
||
entPanVerifying ||
|
||
!clientId
|
||
"
|
||
(click)="verifyEnterprisePan()"
|
||
>
|
||
@if (entPanVerifying) {
|
||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||
} @else {
|
||
<fa-icon icon="shield-alt"></fa-icon> Verify PAN
|
||
}
|
||
</button>
|
||
}
|
||
@if (entPanVerifyStatus === 'VERIFIED') {
|
||
<span class="verify-chip verified"><fa-icon icon="check-circle"></fa-icon> Verified</span>
|
||
}
|
||
</div>
|
||
</div>
|
||
@if (entPanVerifyStatus === 'VERIFIED') {
|
||
<div class="pan-result-card full-width">
|
||
<div class="pan-result-header">
|
||
<fa-icon icon="check-circle" class="pan-result-ok-icon"></fa-icon>
|
||
<span>Enterprise PAN Verified</span>
|
||
</div>
|
||
<div class="pan-result-body">
|
||
<div class="pan-result-row">
|
||
<span class="pan-result-label">Entity Name (as per PAN)</span>
|
||
<span class="pan-result-value name">{{ entPanVerifyName }}</span>
|
||
</div>
|
||
<div class="pan-result-row">
|
||
<span class="pan-result-label">Verified On</span>
|
||
<span class="pan-result-value date">{{ entPanVerifiedOn }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
}
|
||
|
||
<!-- CIN (Companies & LLP only) -->
|
||
@if (constitutionMeta?.showCin) {
|
||
<div class="form-field verify-field">
|
||
<mat-form-field appearance="outline" class="verify-input">
|
||
<mat-label>CIN (Corporate Identity Number)</mat-label>
|
||
<span matPrefix class="field-prefix-icon"><mat-icon>business</mat-icon></span>
|
||
<input
|
||
matInput
|
||
formControlName="cinNumber"
|
||
maxlength="21"
|
||
placeholder="From MCA"
|
||
style="text-transform: uppercase"
|
||
(input)="
|
||
kycForm.get('cinNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
|
||
"
|
||
/>
|
||
@if (kycForm.get('cinNumber')?.hasError('pattern') && kycForm.get('cinNumber')?.touched) {
|
||
<mat-error>Invalid CIN format (21 characters)</mat-error>
|
||
}
|
||
@if (cinVerifyStatus === 'VERIFIED') {
|
||
<mat-icon matSuffix class="verify-icon-ok">check_circle</mat-icon>
|
||
}
|
||
</mat-form-field>
|
||
<div class="verify-action">
|
||
@if (cinVerifyStatus === 'NONE' || cinVerifyStatus === 'FAILED') {
|
||
<button
|
||
mat-raised-button
|
||
color="primary"
|
||
type="button"
|
||
class="verify-btn-inline"
|
||
[disabled]="readOnly || !kycForm.get('cinNumber')?.value || cinVerifying"
|
||
(click)="verifyCin()"
|
||
>
|
||
@if (cinVerifying) {
|
||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||
} @else {
|
||
<fa-icon icon="shield-alt"></fa-icon> Verify CIN
|
||
}
|
||
</button>
|
||
}
|
||
@if (cinVerifyStatus === 'VERIFIED') {
|
||
<span class="verify-chip verified"><fa-icon icon="check-circle"></fa-icon> Verified</span>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- CIN verification progress -->
|
||
@if (cinVerifying) {
|
||
<div class="cin-progress full-width">
|
||
<mat-progress-bar mode="determinate" [value]="cinProgressValue" color="primary"></mat-progress-bar>
|
||
<div class="cin-progress-percent">{{ cinProgressValue }}%</div>
|
||
<div class="cin-progress-steps">
|
||
@for (step of cinProgressSteps; track step.label; let i = $index) {
|
||
<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>
|
||
<span>{{ step.label }}</span>
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
@if (cinVerifyStatus === 'VERIFIED') {
|
||
<div class="pan-result-card full-width">
|
||
<div class="pan-result-header">
|
||
<fa-icon icon="check-circle" class="pan-result-ok-icon"></fa-icon>
|
||
<span>CIN Verified — {{ cinCompanyName }}</span>
|
||
</div>
|
||
<div class="pan-result-body">
|
||
<div class="pan-result-row">
|
||
<span class="pan-result-label">CIN</span>
|
||
<span class="pan-result-value name">{{ kycForm.get('cinNumber')?.value }}</span>
|
||
</div>
|
||
<div class="pan-result-row">
|
||
<span class="pan-result-label">Verified On</span>
|
||
<span class="pan-result-value date">{{ cinVerifiedOn }}</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
}
|
||
}
|
||
</div>
|
||
|
||
@if (!clientId) {
|
||
<div class="verify-info-banner">
|
||
<fa-icon icon="info-circle" class="verify-info-icon"></fa-icon>
|
||
<span>PAN verification requires client to be created first. Complete General tab and click Next to save.</span>
|
||
</div>
|
||
}
|
||
|
||
<!-- KYC Exception Flag -->
|
||
<div
|
||
class="exception-flag-box"
|
||
style="
|
||
margin-top: 20px;
|
||
padding: 16px;
|
||
border: 1px solid #f59e0b;
|
||
background: #fffbeb;
|
||
border-radius: 8px;
|
||
clear: both;
|
||
"
|
||
>
|
||
<h5
|
||
style="
|
||
margin: 0 0 12px 0;
|
||
font-size: 14px;
|
||
font-weight: 600;
|
||
color: #b45309;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
"
|
||
>
|
||
<fa-icon icon="exclamation-triangle"></fa-icon>
|
||
KYC Exception Flag
|
||
</h5>
|
||
<div style="display: flex; align-items: center; gap: 20px; flex-wrap: wrap">
|
||
<mat-checkbox formControlName="isException">Mark as KYC Exception</mat-checkbox>
|
||
|
||
@if (kycForm.get('isException')?.value) {
|
||
<mat-form-field appearance="outline" style="flex: 1; min-width: 280px; margin-bottom: -1.25em">
|
||
<mat-label>Exception Reasons</mat-label>
|
||
<mat-select formControlName="exceptionReason" multiple>
|
||
@if (!isNonIndividual) {
|
||
<mat-option value="PAN Card Verification Failed">PAN Card Verification Failed</mat-option>
|
||
<mat-option value="Aadhaar Verification Failed">Aadhaar Verification Failed</mat-option>
|
||
} @else {
|
||
<mat-option value="Enterprise PAN Verification Failed">Enterprise PAN Verification Failed</mat-option>
|
||
<mat-option value="GSTIN Verification Failed">GSTIN Verification Failed</mat-option>
|
||
<mat-option value="CIN Verification Failed">CIN Verification Failed</mat-option>
|
||
}
|
||
</mat-select>
|
||
</mat-form-field>
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ SECTION: KYC Document Management ═══ -->
|
||
<div class="section-block">
|
||
<h4 class="section-title">
|
||
<fa-icon icon="file-alt" class="section-icon"></fa-icon>
|
||
KYC Document
|
||
<span class="kyc-badge doc-badge">OVD</span>
|
||
@if (kycTemplate?.clientTypeName) {
|
||
<span class="kyc-client-type-chip">{{ kycTemplate.clientTypeName }}</span>
|
||
}
|
||
</h4>
|
||
|
||
<!-- Chip-grouped document type picker -->
|
||
<div class="kyc-doc-pickbox">
|
||
<div class="kyc-doc-pickbox__head">
|
||
<mat-icon class="kyc-doc-pickbox__icon">badge</mat-icon>
|
||
<div class="kyc-doc-pickbox__titles">
|
||
<p class="kyc-doc-pickbox__title">Identity proof</p>
|
||
<p class="kyc-doc-pickbox__sub">Select document type — upload requirements will update automatically</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="kyc-doc-field">
|
||
<label class="kyc-doc-label"><mat-icon>description</mat-icon> Document type</label>
|
||
<div class="kyc-doc-chip-wrap" [class.kyc-doc-chip-wrap--locked]="readOnly">
|
||
@for (group of kycDocGroups; track group.label; let last = $last) {
|
||
<div class="kyc-doc-chip-group-label"><mat-icon>folder_open</mat-icon> {{ group.label }}</div>
|
||
<div class="kyc-doc-chip-grid">
|
||
@for (p of group.items; track p.id) {
|
||
<span
|
||
class="kyc-doc-chip"
|
||
[class.kyc-doc-chip--sel]="isKycDocSelected(p)"
|
||
[class.kyc-doc-chip--locked]="readOnly && !isKycDocSelected(p)"
|
||
[attr.aria-disabled]="readOnly ? 'true' : null"
|
||
(click)="readOnly ? null : selectKycDoc(p)"
|
||
(keydown.enter)="readOnly ? null : selectKycDoc(p)"
|
||
[attr.tabindex]="readOnly ? -1 : 0"
|
||
role="button"
|
||
>
|
||
<mat-icon class="kyc-doc-chip__icon">
|
||
{{ isKycDocSelected(p) ? 'check_circle' : 'radio_button_unchecked' }}
|
||
</mat-icon>
|
||
{{ p.name }}
|
||
</span>
|
||
}
|
||
</div>
|
||
@if (!last) {
|
||
<div class="kyc-doc-chip-divider"></div>
|
||
}
|
||
}
|
||
</div>
|
||
@if (kycForm.get('kycDocumentName')?.hasError('required') && kycForm.get('kycDocumentName')?.touched) {
|
||
<div class="kyc-doc-error">KYC Document Type is <strong>required</strong></div>
|
||
}
|
||
</div>
|
||
|
||
@if (selectedKycDoc?.info) {
|
||
<div class="kyc-doc-banner">
|
||
<mat-icon>check_circle</mat-icon>
|
||
<span>{{ selectedKycDoc.info }}</span>
|
||
</div>
|
||
}
|
||
|
||
@if (selectedKycDoc) {
|
||
<div class="kyc-doc-upload-grid" [class.kyc-doc-upload-grid--two]="selectedKycDoc.sides === 2">
|
||
<div class="kyc-doc-upload-slot">
|
||
<div class="kyc-doc-upload-head">
|
||
<span class="kyc-doc-upload-title">
|
||
<mat-icon>image</mat-icon>
|
||
{{ selectedKycDoc.sides === 2 ? 'Front side' : selectedKycDoc.name + ' (single copy)' }}
|
||
</span>
|
||
<span class="kyc-doc-upload-req">Required</span>
|
||
</div>
|
||
<!-- Existing saved ID-proof doc — locked by default. Replace flow
|
||
unlocks automatically when the section-level Edit button toggles
|
||
`readOnly` off. -->
|
||
@if (existingKycDocuments.length > 0 && !kycDocFiles.front && readOnly) {
|
||
<div class="kyc-doc-dropzone kyc-doc-dropzone--has kyc-doc-dropzone--locked">
|
||
<div class="kyc-doc-dropzone__existing">
|
||
@if (kycDocMergedPreview) {
|
||
<img [src]="kycDocMergedPreview" alt="Saved KYC document" class="kyc-doc-dropzone__thumb" />
|
||
} @else {
|
||
<mat-icon class="kyc-doc-dropzone__doc-ic">description</mat-icon>
|
||
}
|
||
<div class="kyc-doc-dropzone__existing-meta">
|
||
<span class="kyc-doc-dropzone__existing-name">
|
||
{{ existingKycDocuments[0].fileName || existingKycDocuments[0].name }}
|
||
</span>
|
||
<button type="button" class="kyc-doc-dropzone__view" (click)="viewExistingKycDoc()">
|
||
<mat-icon>visibility</mat-icon>
|
||
Click here to view
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
} @else {
|
||
<label
|
||
class="kyc-doc-dropzone"
|
||
[class.kyc-doc-dropzone--has]="kycDocFiles.front || existingKycDocuments.length > 0"
|
||
[class.kyc-doc-dropzone--req]="!kycDocFiles.front && existingKycDocuments.length === 0"
|
||
>
|
||
<input type="file" accept=".jpg,.jpeg,.png,.pdf" hidden (change)="onKycDocFile($event, 'front')" />
|
||
@if (kycDocFiles.front) {
|
||
<div class="kyc-doc-dropzone__file">
|
||
<mat-icon>insert_drive_file</mat-icon>
|
||
<span class="kyc-doc-dropzone__name">{{ kycDocFiles.front.name }}</span>
|
||
<button
|
||
type="button"
|
||
mat-icon-button
|
||
class="kyc-doc-dropzone__remove"
|
||
(click)="$event.preventDefault(); removeKycDocFile('front')"
|
||
>
|
||
<mat-icon>close</mat-icon>
|
||
</button>
|
||
</div>
|
||
} @else if (existingKycDocuments.length > 0) {
|
||
<!-- Edit mode WITH existing doc — let the user view it OR
|
||
click anywhere to replace. -->
|
||
<div class="kyc-doc-dropzone__existing">
|
||
@if (kycDocMergedPreview) {
|
||
<img [src]="kycDocMergedPreview" alt="Saved KYC document" class="kyc-doc-dropzone__thumb" />
|
||
} @else {
|
||
<mat-icon class="kyc-doc-dropzone__doc-ic">description</mat-icon>
|
||
}
|
||
<div class="kyc-doc-dropzone__existing-meta">
|
||
<span class="kyc-doc-dropzone__existing-name">
|
||
{{ existingKycDocuments[0].fileName || existingKycDocuments[0].name }}
|
||
</span>
|
||
<div class="kyc-doc-dropzone__existing-actions">
|
||
<button
|
||
type="button"
|
||
class="kyc-doc-dropzone__view"
|
||
(click)="$event.preventDefault(); $event.stopPropagation(); viewExistingKycDoc()"
|
||
>
|
||
<mat-icon>visibility</mat-icon>
|
||
View
|
||
</button>
|
||
<span class="kyc-doc-dropzone__sep">·</span>
|
||
<span class="kyc-doc-dropzone__existing-hint">
|
||
<mat-icon>cloud_upload</mat-icon>
|
||
Click here to replace
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
} @else {
|
||
<div class="kyc-doc-dropzone__hint">
|
||
<mat-icon>cloud_upload</mat-icon>
|
||
<span>Click to upload (JPG / PNG / PDF · max 5 MB)</span>
|
||
</div>
|
||
}
|
||
</label>
|
||
}
|
||
</div>
|
||
|
||
@if (selectedKycDoc.sides === 2) {
|
||
<div class="kyc-doc-upload-slot">
|
||
<div class="kyc-doc-upload-head">
|
||
<span class="kyc-doc-upload-title"> <mat-icon>image</mat-icon> Back side </span>
|
||
<!-- Hide Required pill when saved combined doc covers both sides -->
|
||
@if (!(existingKycDocuments.length > 0 && !kycDocFiles.back)) {
|
||
<span class="kyc-doc-upload-req">Required</span>
|
||
}
|
||
</div>
|
||
<!-- Locked view: saved combined doc covers the back side too. -->
|
||
@if (existingKycDocuments.length > 0 && !kycDocFiles.back && readOnly) {
|
||
<div class="kyc-doc-dropzone kyc-doc-dropzone--has kyc-doc-dropzone--locked">
|
||
<div class="kyc-doc-dropzone__existing">
|
||
@if (kycDocMergedPreview) {
|
||
<img [src]="kycDocMergedPreview" alt="Saved KYC document" class="kyc-doc-dropzone__thumb" />
|
||
} @else {
|
||
<mat-icon class="kyc-doc-dropzone__doc-ic">description</mat-icon>
|
||
}
|
||
<div class="kyc-doc-dropzone__existing-meta">
|
||
<span class="kyc-doc-dropzone__existing-name">
|
||
{{ existingKycDocuments[0].fileName || existingKycDocuments[0].name }}
|
||
</span>
|
||
<button type="button" class="kyc-doc-dropzone__view" (click)="viewExistingKycDoc()">
|
||
<mat-icon>visibility</mat-icon>
|
||
Click here to view
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
} @else {
|
||
<label
|
||
class="kyc-doc-dropzone"
|
||
[class.kyc-doc-dropzone--has]="kycDocFiles.back || existingKycDocuments.length > 0"
|
||
[class.kyc-doc-dropzone--req]="!kycDocFiles.back && existingKycDocuments.length === 0"
|
||
>
|
||
<input type="file" accept=".jpg,.jpeg,.png,.pdf" hidden (change)="onKycDocFile($event, 'back')" />
|
||
@if (kycDocFiles.back) {
|
||
<div class="kyc-doc-dropzone__file">
|
||
<mat-icon>insert_drive_file</mat-icon>
|
||
<span class="kyc-doc-dropzone__name">{{ kycDocFiles.back.name }}</span>
|
||
<button
|
||
type="button"
|
||
mat-icon-button
|
||
class="kyc-doc-dropzone__remove"
|
||
(click)="$event.preventDefault(); removeKycDocFile('back')"
|
||
>
|
||
<mat-icon>close</mat-icon>
|
||
</button>
|
||
</div>
|
||
} @else if (existingKycDocuments.length > 0) {
|
||
<!-- Edit mode WITH existing combined doc — view + replace. -->
|
||
<div class="kyc-doc-dropzone__existing">
|
||
@if (kycDocMergedPreview) {
|
||
<img [src]="kycDocMergedPreview" alt="Saved KYC document" class="kyc-doc-dropzone__thumb" />
|
||
} @else {
|
||
<mat-icon class="kyc-doc-dropzone__doc-ic">description</mat-icon>
|
||
}
|
||
<div class="kyc-doc-dropzone__existing-meta">
|
||
<span class="kyc-doc-dropzone__existing-name">
|
||
{{ existingKycDocuments[0].fileName || existingKycDocuments[0].name }}
|
||
</span>
|
||
<div class="kyc-doc-dropzone__existing-actions">
|
||
<button
|
||
type="button"
|
||
class="kyc-doc-dropzone__view"
|
||
(click)="$event.preventDefault(); $event.stopPropagation(); viewExistingKycDoc()"
|
||
>
|
||
<mat-icon>visibility</mat-icon>
|
||
View
|
||
</button>
|
||
<span class="kyc-doc-dropzone__sep">·</span>
|
||
<span class="kyc-doc-dropzone__existing-hint">
|
||
<mat-icon>cloud_upload</mat-icon>
|
||
Click here to replace
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
} @else {
|
||
<div class="kyc-doc-dropzone__hint">
|
||
<mat-icon>cloud_upload</mat-icon>
|
||
<span>Click to upload (JPG / PNG / PDF · max 5 MB)</span>
|
||
</div>
|
||
}
|
||
</label>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<!-- Combined preview + re-crop is an edit-mode tool — hide in read-only,
|
||
and don't duplicate the thumbnail when nothing's been picked yet. -->
|
||
@if (
|
||
selectedKycDoc.sides === 2 && kycDocMergedPreview && !readOnly && (kycDocFiles.front || kycDocFiles.back)
|
||
) {
|
||
<div class="kyc-merged">
|
||
<div class="kyc-merged__head">
|
||
<span class="kyc-merged__title">
|
||
<mat-icon>filter_none</mat-icon>
|
||
Combined preview (front + back)
|
||
</span>
|
||
<span class="kyc-merged__hint">Saved as a single PNG. Re-crop a side to update the merged file.</span>
|
||
</div>
|
||
<div class="kyc-merged__preview">
|
||
<img [src]="kycDocMergedPreview" alt="Merged KYC document" />
|
||
</div>
|
||
<div class="kyc-merged__actions">
|
||
<button type="button" mat-stroked-button class="kyc-merged__btn" (click)="reCropKycDocFile('front')">
|
||
<mat-icon>crop</mat-icon> Re-crop front
|
||
</button>
|
||
<button type="button" mat-stroked-button class="kyc-merged__btn" (click)="reCropKycDocFile('back')">
|
||
<mat-icon>crop</mat-icon> Re-crop back
|
||
</button>
|
||
</div>
|
||
</div>
|
||
}
|
||
}
|
||
</div>
|
||
|
||
<div class="form-grid">
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>{{ selectedKycDoc?.documentKeyLabel || 'Document Number' }}</mat-label>
|
||
<span matPrefix class="field-prefix-icon"><mat-icon>tag</mat-icon></span>
|
||
<input
|
||
matInput
|
||
required
|
||
formControlName="kycDocumentNumber"
|
||
[placeholder]="selectedKycDoc?.documentKeyPlaceholder || 'As printed on document'"
|
||
maxlength="30"
|
||
style="text-transform: uppercase"
|
||
(input)="
|
||
kycForm.get('kycDocumentNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
|
||
"
|
||
/>
|
||
@if (kycForm.get('kycDocumentNumber')?.hasError('required') && kycForm.get('kycDocumentNumber')?.touched) {
|
||
<mat-error>Document number is <strong>required</strong></mat-error>
|
||
} @else if (
|
||
kycForm.get('kycDocumentNumber')?.hasError('pattern') && kycForm.get('kycDocumentNumber')?.touched
|
||
) {
|
||
<mat-error>Only letters, numbers, hyphens and slashes allowed</mat-error>
|
||
}
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="form-field" (click)="docIssueDatePicker.open()">
|
||
<mat-label>Issue Date</mat-label>
|
||
<span matPrefix class="field-prefix-icon"><mat-icon>calendar_today</mat-icon></span>
|
||
<input matInput [max]="today" [matDatepicker]="docIssueDatePicker" formControlName="kycDocIssueDate" />
|
||
<mat-datepicker-toggle matSuffix [for]="docIssueDatePicker"></mat-datepicker-toggle>
|
||
<mat-datepicker #docIssueDatePicker></mat-datepicker>
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="form-field" (click)="docExpiryDatePicker.open()">
|
||
<mat-label>Expiry Date</mat-label>
|
||
<span matPrefix class="field-prefix-icon"><mat-icon>event_busy</mat-icon></span>
|
||
<input matInput [matDatepicker]="docExpiryDatePicker" formControlName="kycDocExpiryDate" />
|
||
<mat-datepicker-toggle matSuffix [for]="docExpiryDatePicker"></mat-datepicker-toggle>
|
||
<mat-datepicker #docExpiryDatePicker></mat-datepicker>
|
||
<mat-hint>If applicable (Passport, DL, Visa)</mat-hint>
|
||
</mat-form-field>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ SECTION: Risk & KYC Review ═══ -->
|
||
<div class="section-block">
|
||
<h4 class="section-title">
|
||
<fa-icon icon="shield-alt" class="section-icon"></fa-icon>
|
||
Risk Category & KYC Review
|
||
</h4>
|
||
|
||
<div class="form-grid">
|
||
<div class="checkbox-field override-checkbox-wrapper full-width">
|
||
<mat-checkbox formControlName="allowRiskOverride" [disabled]="readOnly">
|
||
Override Auto-calculated Risk Category
|
||
</mat-checkbox>
|
||
</div>
|
||
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Risk Category</mat-label>
|
||
<mat-select formControlName="riskCategory">
|
||
<mat-select-trigger>
|
||
@if (kycForm.get('riskCategory')?.value === 1) {
|
||
<span class="risk-option"><span class="risk-meter risk-low"></span> Low Risk</span>
|
||
} @else if (kycForm.get('riskCategory')?.value === 2) {
|
||
<span class="risk-option"><span class="risk-meter risk-medium"></span> Medium Risk</span>
|
||
} @else if (kycForm.get('riskCategory')?.value === 3) {
|
||
<span class="risk-option"><span class="risk-meter risk-high"></span> High Risk</span>
|
||
}
|
||
</mat-select-trigger>
|
||
<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
|
||
>
|
||
</mat-option>
|
||
<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
|
||
>
|
||
</mat-option>
|
||
<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
|
||
>
|
||
</mat-option>
|
||
</mat-select>
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>KYC Review Due Date</mat-label>
|
||
<input matInput [value]="getKycExpiryDate(getRenewalYears())" disabled />
|
||
<mat-hint>Auto: Low 10yr, Medium 8yr, High 2yr</mat-hint>
|
||
</mat-form-field>
|
||
|
||
@if (riskCalculationReason && !kycForm.get('allowRiskOverride')?.value && !readOnly) {
|
||
<div
|
||
class="risk-auto-calc-info full-width"
|
||
[class.risk-auto-calc-info--low]="kycForm.get('riskCategory')?.value === 1"
|
||
[class.risk-auto-calc-info--medium]="kycForm.get('riskCategory')?.value === 2"
|
||
[class.risk-auto-calc-info--high]="kycForm.get('riskCategory')?.value === 3"
|
||
>
|
||
<mat-icon class="info-icon">info</mat-icon>
|
||
<span class="info-text"
|
||
>Auto-determined: <strong>{{ riskCalculationReason }}</strong></span
|
||
>
|
||
</div>
|
||
}
|
||
|
||
<div class="risk-rules-toggle-wrapper full-width">
|
||
<button mat-button type="button" class="rules-toggle-btn" (click)="showRiskRulesTable = !showRiskRulesTable">
|
||
<mat-icon class="rules-toggle-icon">{{ showRiskRulesTable ? 'expand_less' : 'expand_more' }}</mat-icon>
|
||
{{
|
||
showRiskRulesTable ? 'Hide Risk Classification Criteria Table' : 'View Risk Classification Criteria Table'
|
||
}}
|
||
</button>
|
||
</div>
|
||
|
||
@if (showRiskRulesTable) {
|
||
<div class="risk-rules-card mat-elevation-z1 full-width">
|
||
<table class="risk-rules-table">
|
||
<thead>
|
||
<tr>
|
||
<th>Risk Category</th>
|
||
<th>Primary Criteria / Basis</th>
|
||
<th>Examples / Thresholds</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr class="rule-row rule-row--high">
|
||
<td><span class="risk-badge risk-badge--high">High Risk</span></td>
|
||
<td>PEPs, High-Risk Industries, High Income / Turnovers</td>
|
||
<td>
|
||
Politicians; Real Estate, Import/Export; Commission Agents; Income > ₹20L/yr; Turnover > ₹1 Crore
|
||
</td>
|
||
</tr>
|
||
<tr class="rule-row rule-row--medium">
|
||
<td><span class="risk-badge risk-badge--medium">Medium Risk</span></td>
|
||
<td>Self-Employed, Professionals, Cross-Border, Moderate Income</td>
|
||
<td>
|
||
Business Owners, Traders; Doctors, Lawyers, CAs; NRI, PIO, OCI; Income > ₹6L/yr; Turnover > ₹20L
|
||
</td>
|
||
</tr>
|
||
<tr class="rule-row rule-row--low">
|
||
<td><span class="risk-badge risk-badge--low">Low Risk</span></td>
|
||
<td>Non-Earning, Government Employment, Agriculture / Manual</td>
|
||
<td>Students, Minors, Homemakers; Govt/PSU Employees; Farmers, Daily Wage; Income <= ₹6L/yr</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
}
|
||
|
||
<mat-form-field appearance="outline" class="form-field full-width">
|
||
<mat-label>{{ isKycVerified ? 'Reason for Re-edit *' : 'Remarks' }}</mat-label>
|
||
<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>
|
||
|
||
@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>
|
||
|
||
<!-- KYC Expiry Alert Cards -->
|
||
<div class="kyc-expiry-section">
|
||
<div class="kyc-expiry-header">
|
||
<fa-icon icon="bell" class="kyc-expiry-header-icon"></fa-icon>
|
||
<span>KYC Expiry Alert</span>
|
||
</div>
|
||
<div class="kyc-expiry-body">
|
||
<div class="kyc-expiry-cards">
|
||
@for (rc of riskCards; track rc.value) {
|
||
<div class="kyc-expiry-card" [class.active]="kycForm.get('riskCategory')?.value === rc.value">
|
||
<span class="risk-gauge" [class]="'risk-gauge--' + rc.color">
|
||
<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
|
||
d="M4 22 A16 16 0 0 1 36 22"
|
||
fill="none"
|
||
[attr.stroke]="rc.color === 'green' ? '#22c55e' : rc.color === 'amber' ? '#f59e0b' : '#ef4444'"
|
||
stroke-width="4"
|
||
stroke-linecap="round"
|
||
[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.y2]="rc.color === 'green' ? '10' : rc.color === 'amber' ? '6' : '10'"
|
||
stroke="#334155"
|
||
stroke-width="2"
|
||
stroke-linecap="round"
|
||
/>
|
||
<circle cx="20" cy="22" r="2" fill="#334155" />
|
||
</svg>
|
||
</span>
|
||
<div class="kyc-expiry-card__info">
|
||
<span class="kyc-expiry-card__label">{{ rc.label }}</span>
|
||
<span class="kyc-expiry-card__cycle"
|
||
>Renewal every <strong>{{ rc.years }} years</strong></span
|
||
>
|
||
</div>
|
||
@if (kycForm.get('riskCategory')?.value === rc.value) {
|
||
<span class="kyc-expiry-card__next">Next: {{ getKycExpiryDate(rc.years) }}</span>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
@if (kycForm.get('riskCategory')?.value) {
|
||
<div
|
||
class="kyc-expiry-alert"
|
||
[class.kyc-expiry-alert--low]="kycForm.get('riskCategory')?.value === 1"
|
||
[class.kyc-expiry-alert--medium]="kycForm.get('riskCategory')?.value === 2"
|
||
[class.kyc-expiry-alert--high]="kycForm.get('riskCategory')?.value === 3"
|
||
>
|
||
<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
|
||
>
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ SECTION: AML & Compliance Checks ═══ -->
|
||
<div class="section-block">
|
||
<h4 class="section-title aml-title">
|
||
<fa-icon icon="exclamation-triangle" class="section-icon"></fa-icon>
|
||
AML & Compliance Checks
|
||
</h4>
|
||
|
||
<div class="form-grid">
|
||
<div class="checkbox-field override-checkbox-wrapper full-width">
|
||
<mat-checkbox formControlName="allowAmlOverride" [disabled]="readOnly">
|
||
Override Auto-calculated AML & Compliance Checks
|
||
</mat-checkbox>
|
||
</div>
|
||
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>AML Check Status</mat-label>
|
||
<mat-select required formControlName="amlCheckStatus">
|
||
<mat-option value="CLEAR">Clear</mat-option>
|
||
<mat-option value="FLAGGED">Flagged</mat-option>
|
||
<mat-option value="BLOCKED">Blocked</mat-option>
|
||
</mat-select>
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Banned Entity List Match</mat-label>
|
||
<mat-select required formControlName="bannedEntityMatch">
|
||
<mat-option value="NO">No</mat-option>
|
||
<mat-option value="YES">Yes</mat-option>
|
||
<mat-option value="PARTIAL_MATCH">Partial Match</mat-option>
|
||
</mat-select>
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Willful Defaulter Flag</mat-label>
|
||
<mat-select formControlName="willfulDefaulter">
|
||
<mat-option value="NO">No</mat-option>
|
||
<mat-option value="YES">Yes</mat-option>
|
||
</mat-select>
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>ECGC SAL Flag</mat-label>
|
||
<mat-select formControlName="ecgcSalFlag">
|
||
<mat-option value="NO">No</mat-option>
|
||
<mat-option value="YES">Yes</mat-option>
|
||
</mat-select>
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Dormant / Inoperative Flag</mat-label>
|
||
<mat-select formControlName="dormantFlag">
|
||
<mat-option value="ACTIVE">Active</mat-option>
|
||
<mat-option value="DORMANT">Dormant</mat-option>
|
||
<mat-option value="INOPERATIVE">Inoperative</mat-option>
|
||
</mat-select>
|
||
</mat-form-field>
|
||
</div>
|
||
|
||
@if (kycForm.get('amlCheckStatus')?.value === 'BLOCKED' || kycForm.get('bannedEntityMatch')?.value === 'YES') {
|
||
<div class="aml-block-alert">
|
||
<fa-icon icon="ban" class="aml-block-icon"></fa-icon>
|
||
<span><strong>BLOCKED</strong> — Account opening cannot proceed.</span>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<!-- ═══ SECTION: Freeze / Debit Controls ═══ -->
|
||
<div class="section-block">
|
||
<h4 class="section-title freeze-title">
|
||
<fa-icon icon="lock" class="section-icon"></fa-icon>
|
||
Freeze / Debit Controls
|
||
</h4>
|
||
|
||
<div class="form-grid">
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Account Status</mat-label>
|
||
<mat-select formControlName="accountStatus">
|
||
<mat-option value="ACTIVE">Active</mat-option>
|
||
<mat-option value="FROZEN">Frozen</mat-option>
|
||
<mat-option value="UNFREEZE">Unfreeze</mat-option>
|
||
<mat-option value="NO_DEBIT">No Debit</mat-option>
|
||
</mat-select>
|
||
</mat-form-field>
|
||
|
||
@if (kycForm.get('accountStatus')?.value === 'FROZEN') {
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Reason for Freeze</mat-label>
|
||
<mat-select required formControlName="freezeReason">
|
||
<mat-option value="COURT_ORDER">Court Order</mat-option>
|
||
<mat-option value="AML">AML</mat-option>
|
||
<mat-option value="REGULATORY">Regulatory</mat-option>
|
||
<mat-option value="INTERNAL">Internal</mat-option>
|
||
<mat-option value="OTHER">Other</mat-option>
|
||
</mat-select>
|
||
</mat-form-field>
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Freeze Authority</mat-label>
|
||
<mat-select required formControlName="freezeAuthority">
|
||
<mat-option value="BRANCH_MANAGER">Branch Manager</mat-option>
|
||
<mat-option value="HO">Head Office</mat-option>
|
||
<mat-option value="REGULATORY">Regulatory Authority</mat-option>
|
||
</mat-select>
|
||
</mat-form-field>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ SECTION: Form 97/98 ═══ -->
|
||
<div class="section-block">
|
||
<h4 class="section-title">
|
||
<fa-icon icon="file-invoice" class="section-icon"></fa-icon>
|
||
Form 97 / 98
|
||
@if (!kycForm.get('hasPan')?.value) {
|
||
<span class="kyc-badge required-badge">Required — No PAN</span>
|
||
}
|
||
</h4>
|
||
@if (!kycForm.get('hasPan')?.value) {
|
||
<div class="kyc-info-hint warning-hint">
|
||
<mat-icon class="kyc-info-hint-icon">warning</mat-icon>
|
||
<span>Client does not have PAN. Form 97/98 is mandatory.</span>
|
||
</div>
|
||
<div class="form-grid">
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Form Type</mat-label>
|
||
<mat-select formControlName="form60Type">
|
||
<mat-option value="FORM_60">Form 97</mat-option>
|
||
<mat-option value="FORM_61">Form 98</mat-option>
|
||
</mat-select>
|
||
</mat-form-field>
|
||
<mat-form-field appearance="outline" class="form-field" (click)="form60DatePicker.open()">
|
||
<mat-label>Declaration Date</mat-label>
|
||
<input matInput [matDatepicker]="form60DatePicker" formControlName="form60Date" />
|
||
<mat-datepicker-toggle matSuffix [for]="form60DatePicker"></mat-datepicker-toggle>
|
||
<mat-datepicker #form60DatePicker></mat-datepicker>
|
||
</mat-form-field>
|
||
</div>
|
||
} @else {
|
||
<p class="form60-not-needed">Client has PAN — Form 97/98 not required.</p>
|
||
}
|
||
</div>
|
||
|
||
<!-- ═══ SECTION: Form 121 ═══ -->
|
||
<div class="section-block">
|
||
<h4 class="section-title">
|
||
<fa-icon icon="receipt" class="section-icon"></fa-icon>
|
||
Form 121
|
||
</h4>
|
||
<div class="form-grid">
|
||
<div class="form-field checkbox-field">
|
||
<mat-checkbox formControlName="form15Applicable">Form 121 Applicable</mat-checkbox>
|
||
</div>
|
||
@if (kycForm.get('form15Applicable')?.value) {
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Form Type</mat-label>
|
||
<mat-select formControlName="form15Type">
|
||
<mat-option value="15G">Form 121 (Below 60 years)</mat-option>
|
||
<mat-option value="15H">Form 121 (Senior Citizen 60+)</mat-option>
|
||
</mat-select>
|
||
</mat-form-field>
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Financial Year</mat-label>
|
||
<input matInput formControlName="form15FY" placeholder="e.g. 2025-26" />
|
||
</mat-form-field>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══ SECTION: Photograph & Signature ═══ -->
|
||
<div class="section-block">
|
||
<h4 class="section-title">
|
||
<fa-icon icon="camera" class="section-icon"></fa-icon>
|
||
Photograph & Signature
|
||
<span class="section-required-tag">Required</span>
|
||
</h4>
|
||
<div class="photo-sig-grid">
|
||
<div
|
||
class="upload-box"
|
||
[class.upload-box--has-preview]="photographPreview"
|
||
[class.upload-box--missing]="!photographPreview && photoSigTouched"
|
||
>
|
||
@if (photographPreview) {
|
||
<div class="upload-preview">
|
||
<img [src]="photographPreview" alt="Photo" class="upload-preview__img upload-preview__img--photo" />
|
||
<div class="upload-preview__actions">
|
||
<label class="upload-preview__btn recrop">
|
||
<input type="file" accept="image/*" hidden (change)="onPhotoSelected($event)" />
|
||
<fa-icon icon="crop"></fa-icon> Re-crop
|
||
</label>
|
||
<button type="button" class="upload-preview__btn remove" (click)="removePhoto()">
|
||
<fa-icon icon="trash"></fa-icon> Remove
|
||
</button>
|
||
</div>
|
||
</div>
|
||
} @else {
|
||
<div class="upload-box__icon"><fa-icon icon="user"></fa-icon></div>
|
||
<span class="upload-box__label">
|
||
Client Photograph
|
||
<span class="upload-box__req">*</span>
|
||
</span>
|
||
<span class="upload-box__hint">JPG / PNG, max 2MB</span>
|
||
<div class="upload-box__btn-row">
|
||
<label class="upload-box__btn">
|
||
<input type="file" accept="image/*" hidden (change)="onPhotoSelected($event)" />
|
||
<fa-icon icon="camera"></fa-icon> Choose & Crop
|
||
</label>
|
||
<button type="button" class="upload-box__btn upload-box__btn--secondary" (click)="captureWebcamPhoto()">
|
||
<fa-icon icon="video"></fa-icon> Use Webcam
|
||
</button>
|
||
</div>
|
||
@if (photoSigTouched) {
|
||
<span class="upload-box__error">Photograph is required</span>
|
||
}
|
||
}
|
||
</div>
|
||
<div
|
||
class="upload-box"
|
||
[class.upload-box--has-preview]="signaturePreview"
|
||
[class.upload-box--missing]="!signaturePreview && photoSigTouched"
|
||
>
|
||
@if (signaturePreview) {
|
||
<div class="upload-preview">
|
||
<img [src]="signaturePreview" alt="Signature" class="upload-preview__img upload-preview__img--sig" />
|
||
<div class="upload-preview__actions">
|
||
<label class="upload-preview__btn recrop">
|
||
<input type="file" accept="image/*" hidden (change)="onSignatureSelected($event)" />
|
||
<fa-icon icon="crop"></fa-icon> Re-crop
|
||
</label>
|
||
<button type="button" class="upload-preview__btn remove" (click)="removeSignature()">
|
||
<fa-icon icon="trash"></fa-icon> Remove
|
||
</button>
|
||
</div>
|
||
</div>
|
||
} @else {
|
||
<div class="upload-box__icon"><fa-icon icon="pen"></fa-icon></div>
|
||
<span class="upload-box__label">
|
||
Specimen Signature
|
||
<span class="upload-box__req">*</span>
|
||
</span>
|
||
<span class="upload-box__hint">Scanned image, max 1MB</span>
|
||
<div class="upload-box__btn-row">
|
||
<label class="upload-box__btn">
|
||
<input type="file" accept="image/*" hidden (change)="onSignatureSelected($event)" />
|
||
<fa-icon icon="crop"></fa-icon> Choose & Crop
|
||
</label>
|
||
<button type="button" class="upload-box__btn upload-box__btn--secondary" (click)="drawSignature()">
|
||
<fa-icon icon="pen"></fa-icon> Draw Signature
|
||
</button>
|
||
</div>
|
||
@if (photoSigTouched) {
|
||
<span class="upload-box__error">Signature is required</span>
|
||
}
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|