Files
cbsnexora/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.html
T

1901 lines
85 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!--
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 &amp; Documents</h3>
<p class="step-header__sub">RBI-compliant KYC verification — PAN, Aadhaar &amp; 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>
}
}
<!-- What PAN / Aadhaar are matched against. Shown before the officer clicks Verify, because
the provider matches on exactly these two values and returns nothing identifiable when
they disagree — a blind check that fails is much harder to act on than a visible one. -->
@if (!isNonIndividual) {
<div
class="identity-basis full-width"
[class.identity-basis--incomplete]="!verificationName || !verificationDateOfBirth"
>
<mat-icon class="identity-basis__icon">badge</mat-icon>
<div class="identity-basis__body">
<span class="identity-basis__legend">Documents are verified against the General tab</span>
<div class="identity-basis__pairs">
<span class="identity-basis__pair">
<span class="identity-basis__k">Name</span>
<span class="identity-basis__v">{{ verificationName || 'Not entered' }}</span>
</span>
<span class="identity-basis__pair">
<span class="identity-basis__k">Date of Birth</span>
<span class="identity-basis__v">
{{ verificationDateOfBirth ? (verificationDateOfBirth | date: 'dd MMM yyyy') : 'Not entered' }}
</span>
</span>
</div>
@if (!verificationName || !verificationDateOfBirth) {
<span class="identity-basis__warn">
Both are required for PAN and Aadhaar verification — complete them on the General tab first.
</span>
} @else {
<span class="identity-basis__hint">
Must match the card exactly. Salutation is excluded; a mismatch in either fails verification.
</span>
}
</div>
</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>
}
@if (panVerifyStatus !== 'VERIFIED' && panDeclarationTaken) {
<span class="verify-chip declared"><fa-icon icon="file-signature"></fa-icon> Declaration</span>
}
</div>
</div>
@if (panVerifyStatus === 'VERIFIED') {
<div
class="pal-card full-width"
[class.pal-card--ok]="panAadhaarSeedingStatus === 'Y'"
[class.pal-card--fail]="panAadhaarSeedingStatus === 'N'"
[class.pal-card--info]="panAadhaarSeedingStatus === 'E'"
[class.pal-card--pending]="
panAadhaarSeedingStatus !== 'Y' && panAadhaarSeedingStatus !== 'N' && panAadhaarSeedingStatus !== 'E'
"
style="margin-bottom: 16px"
>
<div class="pal-card__left">
<div class="pal-card__icon">
<mat-icon>credit_card</mat-icon>
</div>
<div class="pal-card__txt">
<div class="pal-card__small">Verification Status</div>
<div class="pal-card__title">PAN &harr; Aadhaar</div>
<div class="pal-card__status">
@if (panAadhaarSeedingStatus === 'Y') {
<mat-icon>check_circle</mat-icon> Linked (Seeded)
} @else if (panAadhaarSeedingStatus === 'N') {
<mat-icon>cancel</mat-icon> Not Linked
} @else if (panAadhaarSeedingStatus === 'E') {
<mat-icon>info</mat-icon> Exempt
} @else {
<mat-icon>radio_button_unchecked</mat-icon> Pending
}
</div>
</div>
</div>
<div class="pal-card__badge">
@if (panAadhaarSeedingStatus === 'Y') {
<mat-icon>check</mat-icon>
} @else if (panAadhaarSeedingStatus === 'N') {
<mat-icon>close</mat-icon>
} @else if (panAadhaarSeedingStatus === 'E') {
<mat-icon>info</mat-icon>
} @else {
<mat-icon>schedule</mat-icon>
}
</div>
<div class="pal-card__hint">As per Income Tax Portal &mdash; Section 139AA</div>
</div>
}
<!--
PAN could not be verified but a declaration is on file. Shown in full because it is the
only thing standing between this client and a PAN KYC exception — an officer reviewing
the record has to be able to see what was declared, not just that something was.
-->
@if (panVerifyStatus !== 'VERIFIED' && panDeclarationTaken) {
<div class="pan-decl-card">
<div class="pan-decl-card__head">
<mat-icon class="pan-decl-card__icon">assignment_turned_in</mat-icon>
<span class="pan-decl-card__title">PAN Declaration Taken</span>
@if (panDeclarationTakenOn) {
<span class="pan-decl-card__date">{{ panDeclarationTakenOn }}</span>
}
</div>
<div class="pan-decl-card__rows">
<div class="pan-decl-card__row">
<span class="k">Does not match</span>
<span class="v strong">{{ panDeclarationMismatchLabel }}</span>
</div>
@if (panDeclaration?.nameOnPan) {
<div class="pan-decl-card__row">
<span class="k">Name on PAN</span>
<span class="v">{{ panDeclaration?.nameOnPan }}</span>
</div>
}
@if (panDeclaration?.dobOnPan) {
<div class="pan-decl-card__row">
<span class="k">DOB on PAN</span>
<span class="v">{{ panDeclaration?.dobOnPan | date: 'dd MMMM yyyy' }}</span>
</div>
}
@if (panDeclaration?.fileName) {
<div class="pan-decl-card__row">
<span class="k">Declaration file</span>
<span class="v">{{ panDeclaration?.fileName }}</span>
</div>
}
</div>
<div class="pan-decl-card__note">
PAN remains unverified. No KYC exception is raised for PAN while this declaration is on file.
</div>
@if (!readOnly) {
<div class="pan-decl-card__actions">
<button mat-stroked-button type="button" (click)="openPanDeclaration()">Edit Declaration</button>
<button mat-stroked-button type="button" class="danger" (click)="removePanDeclaration()">
Remove
</button>
</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>
}
@if (aadhaarVerifyStatus !== 'VERIFIED' && aadhaarReceiptTaken) {
<span class="verify-chip declared"><fa-icon icon="receipt"></fa-icon> Receipt</span>
}
</div>
</div>
<!--
A customer who has applied for Aadhaar but not received it has no number to verify, so no
amount of retrying the OTP will clear the check. Offer the enrolment slip instead rather
than leaving the officer unable to finish the client.
-->
@if (aadhaarVerifyStatus !== 'VERIFIED' && !aadhaarReceiptTaken && !aadhaarOtpSent && !readOnly) {
<div class="aad-rcpt-offer full-width">
<mat-icon>receipt_long</mat-icon>
<span>Customer has applied for Aadhaar but not received it yet?</span>
<button mat-stroked-button type="button" (click)="openAadhaarReceipt()">Record Enrolment Receipt</button>
</div>
}
<!--
Aadhaar is unverified but an enrolment receipt is on file. Shown in full because it is the
only thing standing between this client and an Aadhaar KYC exception.
-->
@if (aadhaarVerifyStatus !== 'VERIFIED' && aadhaarReceiptTaken) {
<div class="aad-rcpt-card full-width">
<div class="aad-rcpt-card__head">
<mat-icon class="aad-rcpt-card__icon">receipt_long</mat-icon>
<span class="aad-rcpt-card__title">Aadhaar Enrolment Receipt Recorded</span>
@if (aadhaarReceiptTakenOn) {
<span class="aad-rcpt-card__date">{{ aadhaarReceiptTakenOn }}</span>
}
</div>
<div class="aad-rcpt-card__rows">
<div class="aad-rcpt-card__row">
<span class="k">Enrolment ID</span>
<span class="v mono">{{ aadhaarReceipt?.enrolmentId }}</span>
</div>
@if (aadhaarReceipt?.enrolmentDate) {
<div class="aad-rcpt-card__row">
<span class="k">Enrolment date</span>
<span class="v">{{ aadhaarReceipt?.enrolmentDate | date: 'dd MMMM yyyy' }}</span>
</div>
}
@if (aadhaarReceipt?.remarks) {
<div class="aad-rcpt-card__row">
<span class="k">Remarks</span>
<span class="v">{{ aadhaarReceipt?.remarks }}</span>
</div>
}
</div>
<div class="aad-rcpt-card__note">
Aadhaar remains unverified. No KYC exception is raised for Aadhaar while this receipt is on file.
</div>
@if (!readOnly) {
<div class="aad-rcpt-card__actions">
<button mat-stroked-button type="button" (click)="openAadhaarReceipt()">Edit Receipt</button>
<button mat-stroked-button type="button" class="danger" (click)="removeAadhaarReceipt()">Remove</button>
</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 &amp; Aadhaar confirmed against NSDL &amp; 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 &mdash; 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 &mdash; Section 139AA</span>
</div>
}
</div>
}
}
<!-- ══ NON-INDIVIDUAL KYC ══ -->
@if (isNonIndividual) {
<!-- Date of Incorporation — captured before PAN because Enterprise PAN verification
matches the entity on name + date of incorporation as held in the PAN records. -->
<mat-form-field appearance="outline" class="form-field" (click)="incorporationDatePicker.open()">
<mat-label>Date of Incorporation</mat-label>
<span matPrefix class="field-prefix-icon"><mat-icon>event_available</mat-icon></span>
<input
matInput
[max]="today"
[matDatepicker]="incorporationDatePicker"
formControlName="incorporationDate"
readonly
/>
<mat-datepicker-toggle matSuffix [for]="incorporationDatePicker"></mat-datepicker-toggle>
<mat-datepicker #incorporationDatePicker></mat-datepicker>
<mat-hint>As per Certificate of Incorporation / Registration</mat-hint>
</mat-form-field>
<!-- 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>
}
}
<!-- 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>
}
<!-- CIN -->
@if (isNonIndividual) {
<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 !== 'VERIFIED') {
<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>
{{ cinVerifyStatus === 'UNCONFIRMED' ? 'Retry' : 'Verify CIN' }}
}
</button>
}
@if (cinVerifyStatus === 'VERIFIED') {
<span class="verify-chip verified"><fa-icon icon="check-circle"></fa-icon> Verified</span>
}
@if (cinVerifyStatus === 'UNCONFIRMED') {
<span class="verify-chip unconfirmed"
><fa-icon icon="info-circle"></fa-icon> Saved — not confirmed</span
>
}
</div>
</div>
<!-- Accepted, but MCA returned no match. Stated plainly rather than as a failure: the
number is stored and the officer has nothing to correct. -->
@if (cinUnconfirmedMessage) {
<div class="cin-unconfirmed-note full-width">
<fa-icon icon="info-circle" class="cin-unconfirmed-note__icon"></fa-icon>
<span>{{ cinUnconfirmedMessage }}</span>
</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 -->
@if (!kycAllDone) {
<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 (existingKycDocSides.front && !kycDocFiles.front && readOnly) {
<div class="kyc-doc-dropzone kyc-doc-dropzone--has kyc-doc-dropzone--locked">
<div class="kyc-doc-dropzone__existing">
@if (existingKycDocSides.front?.previewUrl) {
<img
[src]="existingKycDocSides.front?.previewUrl"
alt="Saved front side"
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">
{{ existingKycDocSides.front?.fileName }}
</span>
<button type="button" class="kyc-doc-dropzone__view" (click)="viewExistingKycDoc('front')">
<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 || existingKycDocSides.front"
[class.kyc-doc-dropzone--req]="!kycDocFiles.front && !existingKycDocSides.front"
>
<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 (existingKycDocSides.front) {
<!-- Edit mode WITH existing doc — let the user view it OR
click anywhere to replace. -->
<div class="kyc-doc-dropzone__existing">
@if (existingKycDocSides.front?.previewUrl) {
<img
[src]="existingKycDocSides.front?.previewUrl"
alt="Saved front side"
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">
{{ existingKycDocSides.front?.fileName }}
</span>
<div class="kyc-doc-dropzone__existing-actions">
<button
type="button"
class="kyc-doc-dropzone__view"
(click)="$event.preventDefault(); $event.stopPropagation(); viewExistingKycDoc('front')"
>
<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 a back scan (or a stacked scan holding it) is on file -->
@if (!((existingKycDocSides.back || kycDocIsLegacyCombined) && !kycDocFiles.back)) {
<span class="kyc-doc-upload-req">Required</span>
}
</div>
<!--
A stacked scan that could not be cut in two has no separate back image. Say so rather
than repeating the front slot's picture here.
-->
@if (kycDocIsLegacyCombined && !kycDocFiles.back) {
<div class="kyc-doc-dropzone kyc-doc-dropzone--has kyc-doc-dropzone--locked">
<div class="kyc-doc-dropzone__existing">
<mat-icon class="kyc-doc-dropzone__doc-ic">filter_none</mat-icon>
<div class="kyc-doc-dropzone__existing-meta">
<span class="kyc-doc-dropzone__existing-name">Included in the combined scan</span>
<button type="button" class="kyc-doc-dropzone__view" (click)="viewExistingKycDoc('front')">
<mat-icon>visibility</mat-icon>
View both sides
</button>
</div>
</div>
</div>
} @else if (existingKycDocSides.back && !kycDocFiles.back && readOnly) {
<div class="kyc-doc-dropzone kyc-doc-dropzone--has kyc-doc-dropzone--locked">
<div class="kyc-doc-dropzone__existing">
@if (existingKycDocSides.back?.previewUrl) {
<img
[src]="existingKycDocSides.back?.previewUrl"
alt="Saved back side"
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">
{{ existingKycDocSides.back?.fileName }}
</span>
<button type="button" class="kyc-doc-dropzone__view" (click)="viewExistingKycDoc('back')">
<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 || existingKycDocSides.back"
[class.kyc-doc-dropzone--req]="!kycDocFiles.back && !existingKycDocSides.back"
>
<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 (existingKycDocSides.back) {
<!-- Edit mode WITH an existing back scan — view + replace. -->
<div class="kyc-doc-dropzone__existing">
@if (existingKycDocSides.back?.previewUrl) {
<img
[src]="existingKycDocSides.back?.previewUrl"
alt="Saved back side"
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">
{{ existingKycDocSides.back?.fileName }}
</span>
<div class="kyc-doc-dropzone__existing-actions">
<button
type="button"
class="kyc-doc-dropzone__view"
(click)="$event.preventDefault(); $event.stopPropagation(); viewExistingKycDoc('back')"
>
<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">
Preview only — each side is saved as its own file. Re-crop a side to update it.
</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>
} @else if (
kycForm.get('kycDocumentNumber')?.hasError('aadhaarLast4Mismatch') &&
kycForm.get('kycDocumentNumber')?.touched
) {
<mat-error>Last 4 digits do not match the Aadhaar number entered above</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 &amp; 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>
}
<!--
Overriding the risk category is the one thing that makes remarks mandatory here. A verified
record still gets the box — the officer needs somewhere to record a re-edit reason, and the
server asks for one if the save actually replaces data already on the record — but it is
offered, not demanded, so a record that auto-verified mid-wizard does not open in error.
-->
@if (kycForm.get('allowRiskOverride')?.value || isKycVerified) {
<mat-form-field appearance="outline" class="form-field full-width">
<mat-label>
{{
kycForm.get('allowRiskOverride')?.value
? 'Remarks (Required for Risk Override)'
: 'Reason for Re-edit (optional)'
}}
</mat-label>
<textarea
matInput
formControlName="remarks"
rows="2"
[placeholder]="
kycForm.get('allowRiskOverride')?.value
? 'Mandatory — explain why you are overriding the auto-calculated risk category'
: 'Optional — needed only if you change details already recorded on this verified KYC'
"
></textarea>
@if (kycForm.get('allowRiskOverride')?.value && !kycForm.get('remarks')?.value?.trim()) {
<mat-error> Remarks are mandatory when overriding the risk category </mat-error>
}
</mat-form-field>
}
</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 &amp; 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 &amp; 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>
<mat-form-field appearance="outline" class="form-field" (click)="freezeDatePicker.open()">
<mat-label>Freeze Date</mat-label>
<span matPrefix class="field-prefix-icon"><mat-icon>calendar_today</mat-icon></span>
<input matInput required [max]="today" [matDatepicker]="freezeDatePicker" formControlName="freezeDate" />
<mat-datepicker-toggle matSuffix [for]="freezeDatePicker"></mat-datepicker-toggle>
<mat-datepicker #freezeDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field appearance="outline" class="form-field">
<mat-label>Freeze Amount</mat-label>
<span matPrefix class="field-prefix-icon"><mat-icon>currency_rupee</mat-icon></span>
<input matInput type="number" min="0" formControlName="freezeAmount" />
<mat-hint>Leave blank for a full freeze</mat-hint>
</mat-form-field>
}
@if (kycForm.get('accountStatus')?.value === 'UNFREEZE') {
<mat-form-field appearance="outline" class="form-field" (click)="unfreezeDatePicker.open()">
<mat-label>Unfreeze Date</mat-label>
<span matPrefix class="field-prefix-icon"><mat-icon>lock_open</mat-icon></span>
<input matInput required [max]="today" [matDatepicker]="unfreezeDatePicker" formControlName="unfreezeDate" />
<mat-datepicker-toggle matSuffix [for]="unfreezeDatePicker"></mat-datepicker-toggle>
<mat-datepicker #unfreezeDatePicker></mat-datepicker>
</mat-form-field>
}
@if (kycForm.get('accountStatus')?.value === 'NO_DEBIT') {
<mat-form-field appearance="outline" class="form-field" (click)="noDebitDatePicker.open()">
<mat-label>No Debit From Date</mat-label>
<span matPrefix class="field-prefix-icon"><mat-icon>money_off</mat-icon></span>
<input matInput required [matDatepicker]="noDebitDatePicker" formControlName="noDebitDate" />
<mat-datepicker-toggle matSuffix [for]="noDebitDatePicker"></mat-datepicker-toggle>
<mat-datepicker #noDebitDatePicker></mat-datepicker>
</mat-form-field>
}
</div>
</div>
<!-- ═══ SECTION: Form 97/98 ═══ -->
@if (!isNonIndividual) {
<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 ═══ -->
@if (!isNonIndividual) {
<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 ═══
An entity gets the photograph box too — a shopfront, signboard or logo is what staff
recognise the client by in the ledger, and the client view has always had a photo slot in
its header with no way to fill it from the stepper. The specimen signature stays individual-
only: an entity signs through its authorised signatories, whose signatures are captured with
their own KYC rather than as one image for the company. Optional here, so a company can
still be onboarded before anyone has a usable photograph of the premises. -->
<div class="section-block">
<h4 class="section-title">
<fa-icon icon="camera" class="section-icon"></fa-icon>
{{ isNonIndividual ? 'Entity Photograph' : 'Photograph & Signature' }}
@if (isNonIndividual) {
<span class="section-optional-tag">Optional</span>
} @else {
<span class="section-required-tag">Required</span>
}
</h4>
<div class="photo-sig-grid" [class.photo-sig-grid--single]="isNonIndividual">
<div
class="upload-box"
[class.upload-box--has-preview]="photographPreview"
[class.upload-box--missing]="!photographPreview && photoSigTouched && !isNonIndividual"
>
@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]="isNonIndividual ? 'building' : 'user'"></fa-icon></div>
<span class="upload-box__label">
{{ isNonIndividual ? 'Entity Photograph / Logo' : 'Client Photograph' }}
@if (!isNonIndividual) {
<span class="upload-box__req">*</span>
}
</span>
<span class="upload-box__hint">
{{ isNonIndividual ? 'Office premises, signboard or logo — JPG / PNG, max 2MB' : '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 &amp; 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 && !isNonIndividual) {
<span class="upload-box__error">Photograph is required</span>
}
}
</div>
@if (!isNonIndividual) {
<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 &amp; 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>
<!-- Form Validation Summary -->
@if (kycForm && kycForm.invalid) {
<div
class="kyc-validation-summary"
style="
margin-top: 24px;
padding: 16px;
background: #fff5f5;
border: 1px solid #fee2e2;
border-radius: 12px;
display: flex;
align-items: flex-start;
gap: 12px;
clear: both;
"
>
<mat-icon style="color: #ef4444; flex-shrink: 0">error_outline</mat-icon>
<div style="display: flex; flex-direction: column; gap: 4px">
<span style="font-size: 14px; font-weight: 600; color: #991b1b"
>Please complete/correct the following fields to proceed:</span
>
<ul style="margin: 4px 0 0 16px; padding: 0; font-size: 13px; color: #b91c1c; line-height: 1.5">
@for (label of invalidFieldLabels; track label) {
<li>{{ label }}</li>
}
</ul>
</div>
</div>
}
</div>