1268 lines
57 KiB
HTML
1268 lines
57 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="kyc-tab-container">
|
|
<!-- Loading State -->
|
|
@if (loading) {
|
|
<div class="kyc-loading">
|
|
<mat-progress-spinner mode="indeterminate" diameter="48"></mat-progress-spinner>
|
|
<p>{{ 'labels.inputs.KYC Verification' | translate }}...</p>
|
|
</div>
|
|
}
|
|
|
|
@if (!loading) {
|
|
<!-- Header -->
|
|
<div class="kyc-tab-header">
|
|
<div class="kyc-tab-title">
|
|
<h3>{{ 'labels.inputs.KYC Verification' | translate }}</h3>
|
|
<span class="kyc-badge">KYC</span>
|
|
@if (kycData?.kycStatusCode) {
|
|
<span class="kyc-overall-status" [class]="'status-' + getStatusClass(kycData.kycStatusCode)">
|
|
{{ getStatusLabel(kycData.kycStatusCode) }}
|
|
</span>
|
|
}
|
|
</div>
|
|
<div class="kyc-header-actions">
|
|
@if (!isEditMode && kycData) {
|
|
<button mat-stroked-button class="kyc-print-btn" type="button" (click)="printSelfDeclaration()">
|
|
<fa-icon icon="file-pdf" class="m-r-6"></fa-icon> Self Declaration
|
|
</button>
|
|
}
|
|
<button mat-raised-button color="primary" (click)="toggleEditMode()" type="button">
|
|
<fa-icon [icon]="isEditMode ? 'times' : 'edit'" class="m-r-10"></fa-icon>
|
|
{{ isEditMode ? ('labels.buttons.Cancel' | translate) : ('labels.buttons.Edit' | translate) }}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- KYC Summary (read-only when not in edit mode) -->
|
|
@if (!isEditMode && kycData) {
|
|
@if (kycData.isException && !kycData.exceptionResolved) {
|
|
<div
|
|
class="kyc-exception-banner"
|
|
style="
|
|
background: #fef3c7;
|
|
border: 1px solid #f59e0b;
|
|
border-radius: 8px;
|
|
padding: 16px;
|
|
margin-bottom: 20px;
|
|
display: flex;
|
|
align-items: flex-start;
|
|
gap: 12px;
|
|
"
|
|
>
|
|
<mat-icon style="color: #d97706; margin-top: 2px">warning</mat-icon>
|
|
<div>
|
|
<strong style="color: #92400e; font-size: 14px">KYC Exception Active</strong>
|
|
<p style="color: #b45309; font-size: 13px; margin: 4px 0 0 0">
|
|
Account is open under a temporary KYC Exception. Reason:
|
|
<strong>{{ kycData.exceptionReason || 'Other' }}</strong
|
|
>. Please contact the customer and complete verification as soon as possible.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<!--
|
|
The OVD captured at onboarding has gone stale, or is about to. Sits beside the exception
|
|
banner because it is the same kind of thing: something staff must act on before the account
|
|
drifts out of compliance.
|
|
-->
|
|
@if (showKycDocumentAlert) {
|
|
<div class="kyc-doc-alert" [class.kyc-doc-alert--critical]="kycDocumentAlertSeverity === 'critical'">
|
|
<mat-icon class="kyc-doc-alert__icon">{{
|
|
kycDocumentAlertSeverity === 'critical' ? 'error' : 'schedule'
|
|
}}</mat-icon>
|
|
<div class="kyc-doc-alert__body">
|
|
<strong class="kyc-doc-alert__title">{{ kycDocumentAlertTitle }}</strong>
|
|
<p class="kyc-doc-alert__text">{{ kycDocumentAlertMessage }}</p>
|
|
</div>
|
|
<button
|
|
mat-flat-button
|
|
color="primary"
|
|
type="button"
|
|
class="kyc-doc-alert__action"
|
|
(click)="toggleEditMode()"
|
|
>
|
|
Update Document
|
|
</button>
|
|
</div>
|
|
}
|
|
|
|
<!-- Verification Table -->
|
|
<div class="kv-table">
|
|
<div class="kv-table__header">
|
|
<span class="kv-table__col kv-table__col--doc">Document</span>
|
|
<span class="kv-table__col kv-table__col--num">Number</span>
|
|
<span class="kv-table__col kv-table__col--status">Status</span>
|
|
<span class="kv-table__col kv-table__col--date">Verified On</span>
|
|
<span class="kv-table__col kv-table__col--action">Action</span>
|
|
</div>
|
|
|
|
<!--
|
|
PAN and Aadhaar below are the individual's own OVDs. An entity holds neither: its PAN is
|
|
the Enterprise PAN reported further down (the same stored panNumber, under the name the
|
|
entity actually knows it by), and Aadhaar is issued to people, not companies — the masked
|
|
number on a non-individual record is a placeholder the KYC step writes to satisfy the
|
|
column, so showing it as "Pending" invited officers to chase a document that cannot exist.
|
|
-->
|
|
@if (!isNonIndividual) {
|
|
<!-- PAN Row -->
|
|
<div class="kv-table__row">
|
|
<span class="kv-table__col kv-table__col--doc">
|
|
<mat-icon class="kv-doc-icon">credit_card</mat-icon> PAN
|
|
</span>
|
|
<span class="kv-table__col kv-table__col--num kv-mono">{{ kycData.panNumber || '—' }}</span>
|
|
<span class="kv-table__col kv-table__col--status">
|
|
@if (kycData.panVerified) {
|
|
<span class="kv-status kv-status--ok"><mat-icon>check_circle</mat-icon> Verified</span>
|
|
} @else if (panDeclarationTaken) {
|
|
<!-- Deliberately not "Verified": the check never passed, paperwork covers it. -->
|
|
<span class="kv-status kv-status--declared"><mat-icon>assignment_turned_in</mat-icon> Declaration</span>
|
|
} @else {
|
|
<span class="kv-status kv-status--pending"><mat-icon>radio_button_unchecked</mat-icon> Pending</span>
|
|
}
|
|
</span>
|
|
<span class="kv-table__col kv-table__col--date">{{
|
|
kycData.panVerified
|
|
? kycData.panVerifiedOn
|
|
? formatDate(kycData.panVerifiedOn)
|
|
: '—'
|
|
: panDeclarationTaken && kycData.panDeclarationTakenOn
|
|
? formatDate(kycData.panDeclarationTakenOn)
|
|
: '—'
|
|
}}</span>
|
|
<span class="kv-table__col kv-table__col--action">
|
|
<button
|
|
mat-stroked-button
|
|
class="kv-action-btn"
|
|
type="button"
|
|
[disabled]="verifying.pan || !kycData.panNumber"
|
|
(click)="verifyPAN()"
|
|
>
|
|
@if (verifying.pan) {
|
|
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
|
} @else {
|
|
Re-Verify
|
|
}
|
|
</button>
|
|
<!--
|
|
A PAN that has never passed is not always fixable by another attempt. Offer the
|
|
declaration here too, so an existing client sitting in the exception list can be
|
|
cleared without the officer having to run a verification they know will fail.
|
|
-->
|
|
@if (!kycData.panVerified && !panDeclarationTaken && kycData.panNumber) {
|
|
<button
|
|
mat-stroked-button
|
|
class="kv-action-btn"
|
|
type="button"
|
|
[disabled]="!kycId || saving"
|
|
(click)="takePanDeclaration()"
|
|
>
|
|
Declaration
|
|
</button>
|
|
}
|
|
</span>
|
|
</div>
|
|
|
|
<!--
|
|
PAN is unverified but a declaration is on file. Shown in full because it is the only
|
|
reason PAN carries no KYC exception — a reviewer has to see what was declared.
|
|
-->
|
|
@if (!kycData.panVerified && panDeclarationTaken) {
|
|
<div class="kv-decl">
|
|
<div class="kv-decl__head">
|
|
<mat-icon>assignment_turned_in</mat-icon>
|
|
<span class="kv-decl__title">PAN Declaration</span>
|
|
@if (!saving) {
|
|
<span class="kv-decl__actions">
|
|
<button mat-stroked-button type="button" class="kv-action-btn" (click)="takePanDeclaration()">
|
|
Edit
|
|
</button>
|
|
<button
|
|
mat-stroked-button
|
|
type="button"
|
|
class="kv-action-btn danger"
|
|
(click)="removePanDeclaration()"
|
|
>
|
|
Remove
|
|
</button>
|
|
</span>
|
|
}
|
|
</div>
|
|
<div class="kv-decl__rows">
|
|
<div class="kv-decl__row">
|
|
<span class="k">Does not match</span>
|
|
<span class="v strong">{{ panDeclarationMismatchLabel }}</span>
|
|
</div>
|
|
@if (kycData.panDeclarationNameOnPan) {
|
|
<div class="kv-decl__row">
|
|
<span class="k">Name on PAN</span>
|
|
<span class="v">{{ kycData.panDeclarationNameOnPan }}</span>
|
|
</div>
|
|
}
|
|
@if (kycData.panDeclarationDobOnPan) {
|
|
<div class="kv-decl__row">
|
|
<span class="k">DOB on PAN</span>
|
|
<span class="v">{{ formatDate(kycData.panDeclarationDobOnPan) }}</span>
|
|
</div>
|
|
}
|
|
@if (kycData.panDeclarationRemarks) {
|
|
<div class="kv-decl__row">
|
|
<span class="k">Declaration file</span>
|
|
<span class="v">{{ kycData.panDeclarationRemarks }}</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
<div class="kv-decl__note">
|
|
PAN remains unverified. No KYC exception is raised for PAN while this declaration is on file.
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
<!-- Aadhaar Row -->
|
|
<div class="kv-table__row">
|
|
<span class="kv-table__col kv-table__col--doc">
|
|
<mat-icon class="kv-doc-icon">fingerprint</mat-icon> Aadhaar
|
|
</span>
|
|
<span class="kv-table__col kv-table__col--num kv-mono">{{ kycData.aadhaarNumberMasked || '—' }}</span>
|
|
<span class="kv-table__col kv-table__col--status">
|
|
@if (kycData.aadhaarVerified) {
|
|
<span class="kv-status kv-status--ok"><mat-icon>check_circle</mat-icon> Verified</span>
|
|
} @else if (aadhaarReceiptTaken) {
|
|
<!-- Deliberately not "Verified": no Aadhaar exists yet, the slip covers it. -->
|
|
<span class="kv-status kv-status--receipt"><mat-icon>receipt_long</mat-icon> Receipt</span>
|
|
} @else {
|
|
<span class="kv-status kv-status--pending"><mat-icon>radio_button_unchecked</mat-icon> Pending</span>
|
|
}
|
|
</span>
|
|
<span class="kv-table__col kv-table__col--date">{{
|
|
kycData.aadhaarVerified
|
|
? kycData.aadhaarVerifiedOn
|
|
? formatDate(kycData.aadhaarVerifiedOn)
|
|
: '—'
|
|
: aadhaarReceiptTaken && kycData.aadhaarReceiptTakenOn
|
|
? formatDate(kycData.aadhaarReceiptTakenOn)
|
|
: '—'
|
|
}}</span>
|
|
<span class="kv-table__col kv-table__col--action">
|
|
@if (!showAadhaarReVerify && !aadhaarOtpSent) {
|
|
<button
|
|
mat-stroked-button
|
|
class="kv-action-btn"
|
|
type="button"
|
|
[disabled]="verifying.aadhaar || !kycData.aadhaarNumberMasked"
|
|
(click)="startAadhaarReVerify()"
|
|
>
|
|
Re-Verify
|
|
</button>
|
|
}
|
|
<!--
|
|
No Aadhaar number means the OTP check has nothing to run against. Offer the enrolment
|
|
slip here so an existing client sitting in the exception list can be cleared.
|
|
-->
|
|
@if (!kycData.aadhaarVerified && !aadhaarReceiptTaken) {
|
|
<button
|
|
mat-stroked-button
|
|
class="kv-action-btn"
|
|
type="button"
|
|
[disabled]="!kycId || saving"
|
|
(click)="takeAadhaarReceipt()"
|
|
>
|
|
Receipt
|
|
</button>
|
|
}
|
|
</span>
|
|
</div>
|
|
|
|
<!--
|
|
The OTP exchange, directly under the row that started it.
|
|
|
|
It used to sit at the very bottom of the tab, past the PAN-Aadhaar seeding panel: clicking
|
|
Re-Verify appeared to do nothing, because the input it opened was off-screen. Keeping it
|
|
against the Aadhaar row means the field is where the officer is already looking.
|
|
-->
|
|
@if (showAadhaarReVerify && !aadhaarOtpSent) {
|
|
<div class="kv-otp-bar kv-otp-bar--inline">
|
|
<mat-form-field appearance="outline" class="kv-otp-input">
|
|
<mat-label>Enter 12-digit Aadhaar</mat-label>
|
|
<input
|
|
matInput
|
|
[(ngModel)]="aadhaarFullNumber"
|
|
[ngModelOptions]="{ standalone: true }"
|
|
maxlength="12"
|
|
type="password"
|
|
/>
|
|
</mat-form-field>
|
|
<button
|
|
mat-raised-button
|
|
color="primary"
|
|
type="button"
|
|
class="kv-otp-btn"
|
|
[disabled]="!aadhaarFullNumber || aadhaarFullNumber.length !== 12 || verifying.aadhaar"
|
|
(click)="sendAadhaarOtp()"
|
|
>
|
|
@if (verifying.aadhaar) {
|
|
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
|
} @else {
|
|
Send OTP
|
|
}
|
|
</button>
|
|
<button mat-button type="button" class="kv-otp-btn" (click)="cancelAadhaarReVerify()">Cancel</button>
|
|
</div>
|
|
}
|
|
@if (aadhaarOtpSent) {
|
|
<div class="kv-otp-bar kv-otp-bar--inline">
|
|
<span class="kv-otp-label"><mat-icon>sms</mat-icon> OTP sent</span>
|
|
<mat-form-field appearance="outline" class="kv-otp-input">
|
|
<mat-label>Enter OTP</mat-label>
|
|
<input
|
|
matInput
|
|
[(ngModel)]="aadhaarOtp"
|
|
[ngModelOptions]="{ standalone: true }"
|
|
maxlength="6"
|
|
autocomplete="one-time-code"
|
|
/>
|
|
</mat-form-field>
|
|
<button
|
|
mat-raised-button
|
|
color="primary"
|
|
type="button"
|
|
class="kv-otp-btn"
|
|
[disabled]="!aadhaarOtp || aadhaarOtp.length < 4 || aadhaarOtpVerifying"
|
|
(click)="verifyAadhaarOtp()"
|
|
>
|
|
@if (aadhaarOtpVerifying) {
|
|
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
|
} @else {
|
|
Verify
|
|
}
|
|
</button>
|
|
<button
|
|
mat-stroked-button
|
|
type="button"
|
|
class="kv-otp-btn"
|
|
[disabled]="verifying.aadhaar"
|
|
(click)="sendAadhaarOtp()"
|
|
>
|
|
Resend
|
|
</button>
|
|
<button mat-button type="button" class="kv-otp-btn" (click)="cancelAadhaarReVerify()">Cancel</button>
|
|
</div>
|
|
}
|
|
|
|
<!--
|
|
Aadhaar is unverified but an enrolment receipt is on file. Shown in full because it is the
|
|
only reason Aadhaar carries no KYC exception.
|
|
-->
|
|
@if (!kycData.aadhaarVerified && aadhaarReceiptTaken) {
|
|
<div class="kv-rcpt">
|
|
<div class="kv-rcpt__head">
|
|
<mat-icon>receipt_long</mat-icon>
|
|
<span class="kv-rcpt__title">Aadhaar Enrolment Receipt</span>
|
|
@if (!saving) {
|
|
<span class="kv-rcpt__actions">
|
|
<button mat-stroked-button type="button" class="kv-action-btn" (click)="takeAadhaarReceipt()">
|
|
Edit
|
|
</button>
|
|
<button
|
|
mat-stroked-button
|
|
type="button"
|
|
class="kv-action-btn danger"
|
|
(click)="removeAadhaarReceipt()"
|
|
>
|
|
Remove
|
|
</button>
|
|
</span>
|
|
}
|
|
</div>
|
|
<div class="kv-rcpt__rows">
|
|
<div class="kv-rcpt__row">
|
|
<span class="k">Enrolment ID</span>
|
|
<span class="v mono">{{ kycData.aadhaarEnrolmentId || '—' }}</span>
|
|
</div>
|
|
@if (kycData.aadhaarEnrolmentDate) {
|
|
<div class="kv-rcpt__row">
|
|
<span class="k">Enrolment date</span>
|
|
<span class="v">{{ formatDate(kycData.aadhaarEnrolmentDate) }}</span>
|
|
</div>
|
|
}
|
|
@if (kycData.aadhaarReceiptRemarks) {
|
|
<div class="kv-rcpt__row">
|
|
<span class="k">Remarks</span>
|
|
<span class="v">{{ kycData.aadhaarReceiptRemarks }}</span>
|
|
</div>
|
|
}
|
|
</div>
|
|
<div class="kv-rcpt__note">
|
|
Aadhaar remains unverified. No KYC exception is raised for Aadhaar while this receipt is on file.
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
<!--
|
|
The OVD collected at onboarding. It has never appeared here, so the dates driving the
|
|
renewal alert were invisible — an officer told to update the document could not see which
|
|
one, nor when it was issued.
|
|
-->
|
|
@if (kycData.kycDocumentName) {
|
|
<div class="kv-table__row">
|
|
<span class="kv-table__col kv-table__col--doc">
|
|
<mat-icon class="kv-doc-icon">description</mat-icon> {{ kycData.kycDocumentName }}
|
|
</span>
|
|
<span class="kv-table__col kv-table__col--num kv-mono">{{ kycData.kycDocumentNumber || '—' }}</span>
|
|
<span class="kv-table__col kv-table__col--status">
|
|
@switch (kycDocumentRenewal.state) {
|
|
@case ('EXPIRED') {
|
|
<span class="kv-status kv-status--fail"><mat-icon>error</mat-icon> Expired</span>
|
|
}
|
|
@case ('EXPIRING_SOON') {
|
|
<span class="kv-status kv-status--declared"><mat-icon>schedule</mat-icon> Expires Soon</span>
|
|
}
|
|
@case ('DUE') {
|
|
<span class="kv-status kv-status--declared"><mat-icon>update</mat-icon> Update Due</span>
|
|
}
|
|
@case ('OK') {
|
|
<span class="kv-status kv-status--ok"><mat-icon>check_circle</mat-icon> Current</span>
|
|
}
|
|
@default {
|
|
<span class="kv-status kv-status--pending"><mat-icon>help_outline</mat-icon> No dates</span>
|
|
}
|
|
}
|
|
</span>
|
|
<!--
|
|
Issued-on, not verified-on: this document is sighted, never API-verified.
|
|
|
|
Every document shows an expiry, printed or not: one with no expiry on the card is
|
|
still only good for five years from issue, so the derived date is shown rather than a
|
|
blank. It is tagged so a calculated date is never mistaken for one on the document.
|
|
-->
|
|
<span class="kv-table__col kv-table__col--date">
|
|
@if (kycData.kycDocIssueDate || kycData.kycDocExpiryDate) {
|
|
<span class="kv-doc-dates">
|
|
@if (kycData.kycDocIssueDate) {
|
|
<span>Issued {{ formatDate(kycData.kycDocIssueDate) }}</span>
|
|
}
|
|
@if (kycDocumentExpiresOn) {
|
|
<span
|
|
class="kv-doc-dates__expiry"
|
|
[class.kv-doc-dates__expiry--derived]="kycDocumentExpiryIsDerived"
|
|
>
|
|
Expires {{ kycDocumentExpiresOn }}
|
|
@if (kycDocumentExpiryIsDerived) {
|
|
<span
|
|
class="kv-doc-dates__tag"
|
|
matTooltip="No expiry printed on this document — calculated as 5 years from the issue date"
|
|
>5-yr</span
|
|
>
|
|
}
|
|
</span>
|
|
}
|
|
</span>
|
|
} @else {
|
|
—
|
|
}
|
|
</span>
|
|
<span class="kv-table__col kv-table__col--action">
|
|
<button mat-stroked-button class="kv-action-btn" type="button" (click)="toggleEditMode()">Update</button>
|
|
</span>
|
|
</div>
|
|
}
|
|
|
|
<!-- GSTIN Row (Non-Individual only) -->
|
|
@if (isNonIndividual) {
|
|
<div class="kv-table__row">
|
|
<span class="kv-table__col kv-table__col--doc">
|
|
<mat-icon class="kv-doc-icon">business</mat-icon> GSTIN
|
|
</span>
|
|
<span class="kv-table__col kv-table__col--num kv-mono">{{ kycData.gstinNumber || '—' }}</span>
|
|
<span class="kv-table__col kv-table__col--status">
|
|
@if (kycData.gstinVerified) {
|
|
<span class="kv-status kv-status--ok"><mat-icon>check_circle</mat-icon> Verified</span>
|
|
} @else {
|
|
<span class="kv-status kv-status--pending"><mat-icon>radio_button_unchecked</mat-icon> Pending</span>
|
|
}
|
|
</span>
|
|
<span class="kv-table__col kv-table__col--date">{{
|
|
kycData.gstinVerifiedOn ? formatDate(kycData.gstinVerifiedOn) : '—'
|
|
}}</span>
|
|
<span class="kv-table__col kv-table__col--action">
|
|
<button
|
|
mat-stroked-button
|
|
class="kv-action-btn"
|
|
type="button"
|
|
[disabled]="verifying.gstin || !kycData.gstinNumber"
|
|
(click)="verifyGSTIN()"
|
|
>
|
|
@if (verifying.gstin) {
|
|
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
|
} @else {
|
|
Re-Verify
|
|
}
|
|
</button>
|
|
</span>
|
|
</div>
|
|
}
|
|
|
|
<!--
|
|
Enterprise PAN Row (Non-Individual) — the entity's only PAN, so it stands in for the PAN
|
|
row hidden above and carries that row's verification state and Re-Verify action.
|
|
|
|
Status comes from panVerified, not from an enterprisePanVerified flag: the record has no
|
|
such field (ClientKycData holds enterprisePan as a bare string), and the KYC step stores
|
|
the entity PAN in panNumber and verifies it through the same verify-pan command. Reading
|
|
the flag that does not exist left a verified entity PAN permanently showing "Pending".
|
|
-->
|
|
@if (isNonIndividual && enterprisePanNumber) {
|
|
<div class="kv-table__row">
|
|
<span class="kv-table__col kv-table__col--doc">
|
|
<mat-icon class="kv-doc-icon">credit_card</mat-icon> Enterprise PAN
|
|
</span>
|
|
<span class="kv-table__col kv-table__col--num kv-mono">{{ enterprisePanNumber }}</span>
|
|
<span class="kv-table__col kv-table__col--status">
|
|
@if (kycData.panVerified) {
|
|
<span class="kv-status kv-status--ok"><mat-icon>check_circle</mat-icon> Verified</span>
|
|
} @else {
|
|
<span class="kv-status kv-status--pending"><mat-icon>radio_button_unchecked</mat-icon> Pending</span>
|
|
}
|
|
</span>
|
|
<span class="kv-table__col kv-table__col--date">{{
|
|
kycData.panVerified && kycData.panVerifiedOn ? formatDate(kycData.panVerifiedOn) : '—'
|
|
}}</span>
|
|
<span class="kv-table__col kv-table__col--action">
|
|
<button
|
|
mat-stroked-button
|
|
class="kv-action-btn"
|
|
type="button"
|
|
[disabled]="verifying.pan"
|
|
(click)="verifyPAN()"
|
|
>
|
|
@if (verifying.pan) {
|
|
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
|
} @else {
|
|
Re-Verify
|
|
}
|
|
</button>
|
|
</span>
|
|
</div>
|
|
}
|
|
|
|
<!-- Date of Incorporation Row (Non-Individual) — captured alongside the entity PAN
|
|
it is verified against, so it is reported here rather than under personal data. -->
|
|
@if (isNonIndividual && kycData?.incorporationDate) {
|
|
<div class="kv-table__row">
|
|
<span class="kv-table__col kv-table__col--doc">
|
|
<mat-icon class="kv-doc-icon">event_available</mat-icon> Date of Incorporation
|
|
</span>
|
|
<span class="kv-table__col kv-table__col--num">{{ formatDate(kycData.incorporationDate) }}</span>
|
|
<span class="kv-table__col kv-table__col--status">—</span>
|
|
<span class="kv-table__col kv-table__col--date">—</span>
|
|
<span class="kv-table__col kv-table__col--action"></span>
|
|
</div>
|
|
}
|
|
|
|
<!-- CIN Row (Non-Individual / Company) -->
|
|
@if (isNonIndividual && kycData?.cinNumber) {
|
|
<div class="kv-table__row">
|
|
<span class="kv-table__col kv-table__col--doc">
|
|
<mat-icon class="kv-doc-icon">verified_user</mat-icon> CIN
|
|
</span>
|
|
<span class="kv-table__col kv-table__col--num kv-mono">{{ kycData.cinNumber }}</span>
|
|
<span class="kv-table__col kv-table__col--status">
|
|
@if (kycData.cinVerified) {
|
|
<span class="kv-status kv-status--ok"><mat-icon>check_circle</mat-icon> Verified</span>
|
|
} @else {
|
|
<span class="kv-status kv-status--pending"><mat-icon>radio_button_unchecked</mat-icon> Pending</span>
|
|
}
|
|
</span>
|
|
<span class="kv-table__col kv-table__col--date">—</span>
|
|
<span class="kv-table__col kv-table__col--action"></span>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- ─── PAN ↔ Aadhaar Link — premium status card (replaces the legacy
|
|
table row). Dark gradient base with state-driven accent so the
|
|
linking state is the first thing a reviewer sees.
|
|
|
|
Section 139AA seeding applies to a person's PAN and Aadhaar. An entity has no Aadhaar
|
|
to seed, so for a non-individual the card could only ever read "Pending" against a
|
|
requirement that does not apply to it. -->
|
|
@if (!isNonIndividual) {
|
|
<div
|
|
class="pal-card"
|
|
[class.pal-card--ok]="kycData?.panAadhaarSeedingStatus === 'Y'"
|
|
[class.pal-card--fail]="kycData?.panAadhaarSeedingStatus === 'N'"
|
|
[class.pal-card--info]="kycData?.panAadhaarSeedingStatus === 'E'"
|
|
[class.pal-card--pending]="
|
|
kycData?.panAadhaarSeedingStatus !== 'Y' &&
|
|
kycData?.panAadhaarSeedingStatus !== 'N' &&
|
|
kycData?.panAadhaarSeedingStatus !== 'E'
|
|
"
|
|
>
|
|
<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 ↔ Aadhaar</div>
|
|
<div class="pal-card__status">
|
|
@if (kycData?.panAadhaarSeedingStatus === 'Y') {
|
|
<mat-icon>check_circle</mat-icon> Linked (Seeded)
|
|
} @else if (kycData?.panAadhaarSeedingStatus === 'N') {
|
|
<mat-icon>cancel</mat-icon> Not Linked
|
|
} @else if (kycData?.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 (kycData?.panAadhaarSeedingStatus === 'Y') {
|
|
<mat-icon>check</mat-icon>
|
|
} @else if (kycData?.panAadhaarSeedingStatus === 'N') {
|
|
<mat-icon>close</mat-icon>
|
|
} @else if (kycData?.panAadhaarSeedingStatus === 'E') {
|
|
<mat-icon>info</mat-icon>
|
|
} @else {
|
|
<mat-icon>schedule</mat-icon>
|
|
}
|
|
</div>
|
|
<div class="pal-card__hint">As per Income Tax Portal — Section 139AA</div>
|
|
</div>
|
|
}
|
|
|
|
@if (verificationError.aadhaar) {
|
|
<div class="kyc-error-msg">
|
|
<mat-icon class="kyc-error-icon">error_outline</mat-icon> {{ verificationError.aadhaar }}
|
|
</div>
|
|
}
|
|
@if (verificationError.pan) {
|
|
<div class="kyc-error-msg">
|
|
<mat-icon class="kyc-error-icon">error_outline</mat-icon> {{ verificationError.pan }}
|
|
</div>
|
|
}
|
|
|
|
<!-- KYC Meta Grid -->
|
|
<div class="kv-meta-grid">
|
|
<div class="kv-meta-item">
|
|
<mat-icon class="kv-meta-icon icon-blue">assignment_ind</mat-icon>
|
|
<div class="kv-meta-info">
|
|
<span class="kv-meta-label">KYC Type</span>
|
|
<span class="kv-meta-value">
|
|
{{ kycData.kycType === 1 ? 'Individual' : kycData.kycType === 2 ? 'Non-Individual' : '—' }}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="kv-meta-item">
|
|
<mat-icon
|
|
class="kv-meta-icon"
|
|
[ngClass]="{
|
|
'icon-green': kycData.riskCategory === 1,
|
|
'icon-orange': kycData.riskCategory === 2,
|
|
'icon-red': kycData.riskCategory === 3
|
|
}"
|
|
>shield</mat-icon
|
|
>
|
|
<div class="kv-meta-info">
|
|
<span class="kv-meta-label">Risk Category</span>
|
|
<span class="kv-meta-value">
|
|
{{
|
|
kycData.riskCategory === 1
|
|
? 'Low'
|
|
: kycData.riskCategory === 2
|
|
? 'Medium'
|
|
: kycData.riskCategory === 3
|
|
? 'High'
|
|
: '—'
|
|
}}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="kv-meta-item">
|
|
<mat-icon class="kv-meta-icon icon-grey">calendar_today</mat-icon>
|
|
<div class="kv-meta-info">
|
|
<span class="kv-meta-label">Initiated On</span>
|
|
<span class="kv-meta-value">{{ formatDate(kycData.initiatedOn) }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="kv-meta-item">
|
|
<mat-icon class="kv-meta-icon icon-green">check_circle</mat-icon>
|
|
<div class="kv-meta-info">
|
|
<span class="kv-meta-label">Last Verified</span>
|
|
<span class="kv-meta-value">{{ formatDate(kycData.verifiedOn) }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="kv-meta-item">
|
|
<mat-icon class="kv-meta-icon icon-red">history_toggle_off</mat-icon>
|
|
<div class="kv-meta-info">
|
|
<span class="kv-meta-label">Expiry Date</span>
|
|
<span class="kv-meta-value">{{ formatDate(kycData.expiryDate) }}</span>
|
|
</div>
|
|
</div>
|
|
|
|
@if (kycData.remarks) {
|
|
<div class="kv-meta-item kv-meta-item--full">
|
|
<mat-icon class="kv-meta-icon icon-grey">notes</mat-icon>
|
|
<div class="kv-meta-info">
|
|
<span class="kv-meta-label">Remarks</span>
|
|
<span class="kv-meta-value">{{ kycData.remarks }}</span>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Re-KYC: Upload Signed Declaration & Complete -->
|
|
<div class="rekyc-section">
|
|
<div class="rekyc-header">
|
|
<div class="rekyc-icon-wrap">
|
|
<fa-icon icon="sync-alt" class="rekyc-icon"></fa-icon>
|
|
</div>
|
|
<div class="rekyc-header-text">
|
|
<h4 class="rekyc-title">Re-KYC Completion</h4>
|
|
<p class="rekyc-desc">Print self-declaration, get it signed, upload & complete re-KYC.</p>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 1 -->
|
|
<div class="rekyc-timeline">
|
|
<div class="rekyc-tl-item">
|
|
<div class="rekyc-tl-dot">
|
|
<span class="rekyc-tl-num">1</span>
|
|
<div class="rekyc-tl-line"></div>
|
|
</div>
|
|
<div class="rekyc-tl-content">
|
|
<div class="rekyc-tl-label">Download Self Declaration Form</div>
|
|
<div class="rekyc-tl-sub">System-generated PDF with PAN, Aadhaar & KYC details</div>
|
|
<button mat-flat-button class="rekyc-btn-print" type="button" (click)="printSelfDeclaration()">
|
|
<fa-icon icon="file-pdf"></fa-icon> Download PDF
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 2 -->
|
|
<div class="rekyc-tl-item">
|
|
<div class="rekyc-tl-dot">
|
|
<span class="rekyc-tl-num">2</span>
|
|
<div class="rekyc-tl-line"></div>
|
|
</div>
|
|
<div class="rekyc-tl-content">
|
|
<div class="rekyc-tl-label">Upload Signed Declaration</div>
|
|
<div class="rekyc-tl-sub">Get the form signed by the client and upload the scanned copy</div>
|
|
<label class="rekyc-upload-zone" [class.has-file]="selectedDeclarationFile">
|
|
<input type="file" accept=".pdf,.jpg,.jpeg,.png" (change)="onDeclarationFileSelected($event)" hidden />
|
|
@if (!selectedDeclarationFile) {
|
|
<mat-icon class="upload-zone-icon">cloud_upload</mat-icon>
|
|
<span class="upload-zone-text">Click to upload signed form</span>
|
|
<span class="upload-zone-hint">PDF, JPG or PNG</span>
|
|
} @else {
|
|
<fa-icon icon="check-circle" class="upload-zone-check"></fa-icon>
|
|
<span class="upload-zone-filename">{{ selectedDeclarationFile.name }}</span>
|
|
<span class="upload-zone-hint">Click to change file</span>
|
|
}
|
|
</label>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Step 3 -->
|
|
<div class="rekyc-tl-item rekyc-tl-item--last">
|
|
<div class="rekyc-tl-dot">
|
|
<span class="rekyc-tl-num">3</span>
|
|
</div>
|
|
<div class="rekyc-tl-content">
|
|
<div class="rekyc-tl-label">Complete Re-KYC</div>
|
|
<div class="rekyc-tl-sub">Updates Last Verified date and sets new Expiry Date as per RBI norms</div>
|
|
<button
|
|
mat-flat-button
|
|
color="primary"
|
|
class="rekyc-btn-complete"
|
|
type="button"
|
|
[disabled]="!selectedDeclarationFile || reKycCompleting || !kycId"
|
|
(click)="completeReKyc()"
|
|
>
|
|
@if (reKycCompleting) {
|
|
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
|
Completing...
|
|
} @else {
|
|
<fa-icon icon="check-circle"></fa-icon> Complete Re-KYC
|
|
}
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
@if (reKycSuccess) {
|
|
<div class="rekyc-result rekyc-result--success">
|
|
<fa-icon icon="check-circle" class="rekyc-result-icon"></fa-icon>
|
|
<div>
|
|
<strong>Re-KYC completed successfully</strong>
|
|
<p>Last Verified and Expiry Date have been updated.</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
@if (reKycError) {
|
|
<div class="rekyc-result rekyc-result--error">
|
|
<mat-icon class="rekyc-result-icon">error_outline</mat-icon>
|
|
<div>
|
|
<strong>Re-KYC failed</strong>
|
|
<p>{{ reKycError }}</p>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
<!-- No KYC Data State -->
|
|
@if (!isEditMode && !kycData) {
|
|
<div class="kyc-empty">
|
|
<mat-icon class="kyc-empty-icon">verified_user</mat-icon>
|
|
<p>{{ 'labels.inputs.KYC Verification' | translate }} not initiated yet.</p>
|
|
<button mat-raised-button color="primary" (click)="toggleEditMode()" type="button">
|
|
{{ 'labels.inputs.KYC verification initiated' | translate }}
|
|
</button>
|
|
</div>
|
|
}
|
|
|
|
<!-- Edit Form -->
|
|
@if (isEditMode) {
|
|
<mat-card class="kyc-edit-card">
|
|
<mat-card-content>
|
|
<form [formGroup]="kycForm" (ngSubmit)="saveKyc()">
|
|
<div class="kyc-edit-grid">
|
|
<!-- PAN — the same stored panNumber either way, named for whose PAN it is. -->
|
|
<div class="kyc-edit-field">
|
|
<div class="kyc-field-label-row">
|
|
<span class="kyc-field-title">{{
|
|
isNonIndividual ? 'Enterprise PAN' : ('labels.inputs.PAN Number' | translate)
|
|
}}</span>
|
|
<span class="kyc-tag">KYC</span>
|
|
</div>
|
|
<div class="kyc-input-row">
|
|
<mat-form-field appearance="outline" class="kyc-input">
|
|
<mat-label>{{
|
|
isNonIndividual ? 'Enterprise PAN' : ('labels.inputs.PAN Number' | translate)
|
|
}}</mat-label>
|
|
<input
|
|
matInput
|
|
formControlName="panNumber"
|
|
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>{{ 'labels.inputs.Enter 10-character PAN' | translate }}</mat-error>
|
|
}
|
|
@if (verificationStatus.pan === 'VERIFIED') {
|
|
<mat-icon matSuffix class="status-icon verified">check_circle</mat-icon>
|
|
}
|
|
@if (verificationStatus.pan === 'FAILED') {
|
|
<mat-icon matSuffix class="status-icon failed">cancel</mat-icon>
|
|
}
|
|
</mat-form-field>
|
|
<button
|
|
mat-raised-button
|
|
color="accent"
|
|
type="button"
|
|
class="verify-btn"
|
|
[disabled]="
|
|
!kycId || !kycForm.get('panNumber')?.value || kycForm.get('panNumber')?.invalid || verifying.pan
|
|
"
|
|
(click)="verifyPAN()"
|
|
[matTooltip]="!kycId ? 'Save KYC details first' : ''"
|
|
>
|
|
@if (verifying.pan) {
|
|
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
|
} @else {
|
|
{{ 'labels.inputs.Verify' | translate }}
|
|
}
|
|
</button>
|
|
</div>
|
|
@if (verificationError.pan) {
|
|
<div class="kyc-error-msg">
|
|
<mat-icon class="kyc-error-icon">error_outline</mat-icon>
|
|
<span>{{ verificationError.pan }}</span>
|
|
</div>
|
|
<!--
|
|
The check can fail for reasons no retry will fix — a PAN card that spells the
|
|
name differently, or a vendor that is simply down. The declaration is the way
|
|
out, so it is offered here rather than leaving the officer at a dead end.
|
|
|
|
It records a mismatch against the customer's own name and date of birth, so it
|
|
is offered to individuals only — an entity PAN is checked against neither.
|
|
-->
|
|
@if (!isNonIndividual) {
|
|
<div class="kyc-decl-offer">
|
|
<span>Customer's PAN card genuinely carries different details, or PAN cannot be checked?</span>
|
|
<button
|
|
mat-stroked-button
|
|
type="button"
|
|
[disabled]="!kycId || saving"
|
|
(click)="takePanDeclaration()"
|
|
>
|
|
{{ panDeclarationTaken ? 'Edit Declaration' : 'Take Declaration' }}
|
|
</button>
|
|
</div>
|
|
}
|
|
}
|
|
@if (verificationStatus.pan === 'VERIFIED' && verificationDetails.pan?.name) {
|
|
<span class="verification-detail-text">{{ verificationDetails.pan.name }}</span>
|
|
}
|
|
</div>
|
|
|
|
<!-- Aadhaar — individuals only; an entity has no Aadhaar to capture or verify. -->
|
|
@if (!isNonIndividual) {
|
|
<div class="kyc-edit-field">
|
|
<div class="kyc-field-label-row">
|
|
<span class="kyc-field-title">{{ 'labels.inputs.Aadhaar Number' | translate }}</span>
|
|
<span class="kyc-tag">KYC</span>
|
|
</div>
|
|
<div class="kyc-input-row">
|
|
<mat-form-field appearance="outline" class="kyc-input">
|
|
<mat-label>{{ 'labels.inputs.Aadhaar Number' | translate }}</mat-label>
|
|
<input
|
|
matInput
|
|
formControlName="aadhaarNumber"
|
|
maxlength="12"
|
|
inputmode="numeric"
|
|
autocomplete="off"
|
|
placeholder="Enter 12-digit Aadhaar"
|
|
[readonly]="aadhaarOtpSent"
|
|
/>
|
|
@if (kycData?.aadhaarNumberMasked) {
|
|
<mat-hint>On file: {{ kycData.aadhaarNumberMasked }}</mat-hint>
|
|
}
|
|
@if (kycForm.get('aadhaarNumber')?.hasError('pattern') && kycForm.get('aadhaarNumber')?.touched) {
|
|
<mat-error>{{ 'labels.inputs.Enter 12-digit Aadhaar' | translate }}</mat-error>
|
|
}
|
|
@if (verificationStatus.aadhaar === 'VERIFIED') {
|
|
<mat-icon matSuffix class="status-icon verified">check_circle</mat-icon>
|
|
}
|
|
@if (verificationStatus.aadhaar === 'FAILED') {
|
|
<mat-icon matSuffix class="status-icon failed">cancel</mat-icon>
|
|
}
|
|
</mat-form-field>
|
|
@if (!aadhaarOtpSent) {
|
|
<button
|
|
mat-raised-button
|
|
color="accent"
|
|
type="button"
|
|
class="verify-btn"
|
|
[disabled]="
|
|
!kycId ||
|
|
!kycForm.get('aadhaarNumber')?.value ||
|
|
kycForm.get('aadhaarNumber')?.invalid ||
|
|
verifying.aadhaar
|
|
"
|
|
(click)="sendAadhaarOtp()"
|
|
[matTooltip]="!kycId ? 'Save KYC details first' : ''"
|
|
>
|
|
@if (verifying.aadhaar) {
|
|
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
|
} @else {
|
|
Send OTP
|
|
}
|
|
</button>
|
|
}
|
|
</div>
|
|
|
|
<!-- OTP Section in Edit Mode -->
|
|
@if (aadhaarOtpSent) {
|
|
<div class="otp-section">
|
|
<p class="otp-info">
|
|
<mat-icon class="otp-info-icon">sms</mat-icon>
|
|
OTP sent to Aadhaar-linked mobile
|
|
</p>
|
|
<div class="otp-inline">
|
|
<mat-form-field appearance="outline" class="otp-field">
|
|
<mat-label>Enter OTP</mat-label>
|
|
<input
|
|
matInput
|
|
[(ngModel)]="aadhaarOtp"
|
|
[ngModelOptions]="{ standalone: true }"
|
|
maxlength="6"
|
|
autocomplete="one-time-code"
|
|
/>
|
|
</mat-form-field>
|
|
<button
|
|
mat-raised-button
|
|
color="accent"
|
|
class="otp-verify-btn"
|
|
type="button"
|
|
[disabled]="!aadhaarOtp || aadhaarOtp.length < 4 || aadhaarOtpVerifying"
|
|
(click)="verifyAadhaarOtp()"
|
|
>
|
|
@if (aadhaarOtpVerifying) {
|
|
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
|
} @else {
|
|
Verify OTP
|
|
}
|
|
</button>
|
|
<button
|
|
mat-stroked-button
|
|
type="button"
|
|
class="otp-resend-btn"
|
|
[disabled]="verifying.aadhaar"
|
|
(click)="sendAadhaarOtp()"
|
|
>
|
|
Resend
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
|
|
@if (verificationError.aadhaar) {
|
|
<div class="kyc-error-msg">
|
|
<mat-icon class="kyc-error-icon">error_outline</mat-icon>
|
|
<span>{{ verificationError.aadhaar }}</span>
|
|
</div>
|
|
<!--
|
|
A failed OTP is not always something a retry can fix — a customer who has only
|
|
applied for Aadhaar has no number to verify at all. The enrolment slip is the way
|
|
out, so it is offered here rather than leaving the officer at a dead end.
|
|
-->
|
|
<div class="kyc-decl-offer">
|
|
<span>Customer has applied for Aadhaar but not received it yet?</span>
|
|
<button
|
|
mat-stroked-button
|
|
type="button"
|
|
[disabled]="!kycId || saving"
|
|
(click)="takeAadhaarReceipt()"
|
|
>
|
|
{{ aadhaarReceiptTaken ? 'Edit Receipt' : 'Record Receipt' }}
|
|
</button>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
<!-- GSTIN (Non-Individual only) -->
|
|
@if (isNonIndividual) {
|
|
<div class="kyc-edit-field">
|
|
<div class="kyc-field-label-row">
|
|
<span class="kyc-field-title">{{ 'labels.inputs.GSTIN Number' | translate }}</span>
|
|
<span class="kyc-tag">KYC</span>
|
|
</div>
|
|
<div class="kyc-input-row">
|
|
<mat-form-field appearance="outline" class="kyc-input">
|
|
<mat-label>{{ 'labels.inputs.GSTIN Number' | translate }}</mat-label>
|
|
<input
|
|
matInput
|
|
formControlName="gstinNumber"
|
|
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>{{ 'labels.inputs.Enter 15-character GSTIN' | translate }}</mat-error>
|
|
}
|
|
@if (verificationStatus.gstin === 'VERIFIED') {
|
|
<mat-icon matSuffix class="status-icon verified">check_circle</mat-icon>
|
|
}
|
|
@if (verificationStatus.gstin === 'FAILED') {
|
|
<mat-icon matSuffix class="status-icon failed">cancel</mat-icon>
|
|
}
|
|
</mat-form-field>
|
|
<button
|
|
mat-raised-button
|
|
color="accent"
|
|
type="button"
|
|
class="verify-btn"
|
|
[disabled]="
|
|
!kycId ||
|
|
!kycForm.get('gstinNumber')?.value ||
|
|
kycForm.get('gstinNumber')?.invalid ||
|
|
verifying.gstin
|
|
"
|
|
(click)="verifyGSTIN()"
|
|
[matTooltip]="!kycId ? 'Save KYC details first' : ''"
|
|
>
|
|
@if (verifying.gstin) {
|
|
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
|
} @else {
|
|
{{ 'labels.inputs.Verify' | translate }}
|
|
}
|
|
</button>
|
|
</div>
|
|
@if (verificationError.gstin) {
|
|
<div class="kyc-error-msg">
|
|
<mat-icon class="kyc-error-icon">error_outline</mat-icon>
|
|
<span>{{ verificationError.gstin }}</span>
|
|
</div>
|
|
}
|
|
@if (verificationStatus.gstin === 'VERIFIED' && verificationDetails.gstin?.businessName) {
|
|
<span class="verification-detail-text">{{ verificationDetails.gstin.businessName }}</span>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
<!-- KYC Type -->
|
|
@if (!kycId) {
|
|
<mat-form-field appearance="outline">
|
|
<mat-label>{{ 'labels.inputs.KYC Type' | translate }}</mat-label>
|
|
<mat-select formControlName="kycType">
|
|
<mat-option [value]="1">Individual</mat-option>
|
|
<mat-option [value]="2">Non-Individual</mat-option>
|
|
</mat-select>
|
|
</mat-form-field>
|
|
}
|
|
|
|
<!-- Risk Category -->
|
|
<mat-form-field appearance="outline">
|
|
<mat-label>{{ 'labels.inputs.Risk Category' | translate }}</mat-label>
|
|
<mat-select formControlName="riskCategory">
|
|
<mat-option [value]="1">Low</mat-option>
|
|
<mat-option [value]="2">Medium</mat-option>
|
|
<mat-option [value]="3">High</mat-option>
|
|
</mat-select>
|
|
</mat-form-field>
|
|
|
|
<!-- Officially Valid Document. Shown when one is on file or the alert is asking for
|
|
it, so re-collecting an expired document is done on this form rather than
|
|
sending the officer somewhere else. -->
|
|
@if (kycData?.kycDocumentName || kycDocumentNeedsUpdate) {
|
|
<div class="full-span kyc-doc-fields">
|
|
<div class="kyc-doc-fields__head">
|
|
<h4 class="kyc-doc-fields__title">Officially Valid Document</h4>
|
|
@if (kycDocumentNeedsUpdate) {
|
|
<span class="kyc-doc-fields__flag">Re-collect from the customer</span>
|
|
}
|
|
</div>
|
|
|
|
<div class="kyc-doc-fields__grid">
|
|
<mat-form-field appearance="outline">
|
|
<mat-label>Document</mat-label>
|
|
<mat-select formControlName="kycDocumentName">
|
|
<mat-option value="Aadhaar Card">Aadhaar Card</mat-option>
|
|
<mat-option value="Passport">Passport</mat-option>
|
|
<mat-option value="Driving Licence">Driving Licence</mat-option>
|
|
<mat-option value="Voter ID">Voter ID</mat-option>
|
|
<mat-option value="NREGA Job Card">NREGA Job Card</mat-option>
|
|
<mat-option value="Letter from National Population Register">Letter from NPR</mat-option>
|
|
</mat-select>
|
|
</mat-form-field>
|
|
|
|
<mat-form-field appearance="outline">
|
|
<mat-label>Document Number</mat-label>
|
|
<input matInput formControlName="kycDocumentNumber" />
|
|
</mat-form-field>
|
|
|
|
<mat-form-field appearance="outline">
|
|
<mat-label>Issued On</mat-label>
|
|
<input
|
|
matInput
|
|
[matDatepicker]="docIssuePicker"
|
|
[max]="maxDate"
|
|
formControlName="kycDocIssueDate"
|
|
/>
|
|
<mat-datepicker-toggle matSuffix [for]="docIssuePicker"></mat-datepicker-toggle>
|
|
<mat-datepicker #docIssuePicker></mat-datepicker>
|
|
</mat-form-field>
|
|
|
|
<mat-form-field appearance="outline">
|
|
<mat-label>Valid Till</mat-label>
|
|
<input matInput [matDatepicker]="docExpiryPicker" formControlName="kycDocExpiryDate" />
|
|
<mat-datepicker-toggle matSuffix [for]="docExpiryPicker"></mat-datepicker-toggle>
|
|
<mat-datepicker #docExpiryPicker></mat-datepicker>
|
|
<mat-hint>Blank if it does not expire</mat-hint>
|
|
</mat-form-field>
|
|
</div>
|
|
|
|
<!-- The scans live with the document itself, on the identifier the customer's
|
|
original was attached to. Re-scanning is done there rather than duplicated
|
|
here, so both sides, the cropper and the replace-in-place all behave the
|
|
same way they did when the document was first collected. -->
|
|
<a class="kyc-doc-scans-link" [routerLink]="['/clients', clientId, 'edit']">
|
|
<mat-icon>photo_library</mat-icon>
|
|
<span>Replace the scanned front / back of this document</span>
|
|
<mat-icon class="kyc-doc-scans-link__go">chevron_right</mat-icon>
|
|
</a>
|
|
</div>
|
|
}
|
|
|
|
<!-- KYC Exception Flag -->
|
|
<div
|
|
class="full-span exception-controls-row"
|
|
style="display: flex; align-items: center; gap: 20px; margin-bottom: 12px; flex-wrap: wrap"
|
|
>
|
|
<mat-checkbox formControlName="isException" style="margin-top: 8px">Mark as KYC Exception</mat-checkbox>
|
|
|
|
@if (kycForm.get('isException')?.value) {
|
|
<mat-form-field appearance="outline" style="flex: 1; min-width: 250px">
|
|
<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>
|
|
|
|
<!-- Remarks -->
|
|
<mat-form-field appearance="outline" class="full-span">
|
|
<mat-label>{{ 'labels.inputs.Remarks' | translate }}</mat-label>
|
|
<textarea matInput formControlName="remarks" rows="3"></textarea>
|
|
</mat-form-field>
|
|
</div>
|
|
|
|
@if (errorMessage) {
|
|
<div class="kyc-error-msg">
|
|
<mat-icon class="kyc-error-icon">error_outline</mat-icon>
|
|
<span>{{ errorMessage }}</span>
|
|
</div>
|
|
}
|
|
|
|
@if (!kycId) {
|
|
<div class="kyc-info-hint">
|
|
<mat-icon class="kyc-info-hint-icon">info</mat-icon>
|
|
<span>Save KYC details first to enable verification buttons.</span>
|
|
</div>
|
|
}
|
|
|
|
<div class="kyc-form-actions">
|
|
<button mat-raised-button type="submit" color="primary" [disabled]="kycForm.invalid || saving">
|
|
@if (saving) {
|
|
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
|
|
} @else {
|
|
{{ 'labels.buttons.Submit' | translate }}
|
|
}
|
|
</button>
|
|
<button mat-stroked-button type="button" (click)="toggleEditMode()">
|
|
{{ 'labels.buttons.Cancel' | translate }}
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</mat-card-content>
|
|
</mat-card>
|
|
}
|
|
}
|
|
</div>
|