feat(clients, alm): add CIN/MCA verification & authorized signatories tab; refactor ALM report print and export
This commit is contained in:
+95
@@ -0,0 +1,95 @@
|
||||
<!--
|
||||
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="as-view">
|
||||
@if (loading) {
|
||||
<div class="as-loading">
|
||||
<mat-progress-spinner mode="indeterminate" diameter="40"></mat-progress-spinner>
|
||||
<p>Loading authorized signatories...</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!loading) {
|
||||
<div class="as-header">
|
||||
<h3><fa-icon icon="user-shield"></fa-icon> Authorized Signatories</h3>
|
||||
@if (signatories.length) {
|
||||
<span class="as-count">{{ signatories.length }} on record</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- The tab link is entity-only, but the route can be reached directly by URL. -->
|
||||
@if (!isNonIndividual) {
|
||||
<div class="as-empty">
|
||||
<mat-icon>person</mat-icon>
|
||||
<span>Authorized signatories apply to non-individual clients only.</span>
|
||||
</div>
|
||||
} @else if (!signatories.length) {
|
||||
<div class="as-empty">
|
||||
<mat-icon>group_off</mat-icon>
|
||||
<span>No authorized signatories recorded for this entity.</span>
|
||||
</div>
|
||||
} @else {
|
||||
<p class="as-note">
|
||||
The persons authorised to transact on this entity's behalf, as named in its Board Resolution / Authorization
|
||||
Letter.
|
||||
</p>
|
||||
|
||||
@for (signatory of signatories; track $index) {
|
||||
<div class="as-card">
|
||||
<div class="as-card__main">
|
||||
<div class="as-card__avatar"><mat-icon>person</mat-icon></div>
|
||||
<div class="as-card__identity">
|
||||
<span class="as-card__name">{{ signatory.name || 'Unnamed signatory' }}</span>
|
||||
<span class="as-card__meta">
|
||||
@if (signatory.loading) {
|
||||
Loading details…
|
||||
} @else {
|
||||
@if (signatory.cifNumber) {
|
||||
<span class="as-mono">CIF {{ signatory.cifNumber }}</span>
|
||||
}
|
||||
@if (signatory.officeName) {
|
||||
<span class="as-card__dot">•</span>{{ signatory.officeName }}
|
||||
}
|
||||
@if (signatory.status) {
|
||||
<span class="as-card__dot">•</span>{{ signatory.status }}
|
||||
}
|
||||
<!-- A signatory recorded as a name only — nothing to link to, and saying so is
|
||||
better than an empty row the officer cannot explain. -->
|
||||
@if (!signatory.clientId) {
|
||||
<span class="as-card__unlinked">Name on record only — no linked client</span>
|
||||
}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="as-card__side">
|
||||
<div class="as-din">
|
||||
<span class="as-din__label">DIN</span>
|
||||
@if (signatory.dinNumber) {
|
||||
<span class="as-din__value as-mono">{{ signatory.dinNumber }}</span>
|
||||
@if (signatory.dinVerified) {
|
||||
<span class="as-chip as-chip--ok"><mat-icon>check_circle</mat-icon> Verified</span>
|
||||
} @else {
|
||||
<span class="as-chip as-chip--pending"><mat-icon>radio_button_unchecked</mat-icon> Unverified</span>
|
||||
}
|
||||
} @else {
|
||||
<span class="as-din__value as-din__value--none">—</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (signatory.clientId) {
|
||||
<a mat-stroked-button class="as-link-btn" [routerLink]="['/clients', signatory.clientId, 'general']">
|
||||
<mat-icon>open_in_new</mat-icon> View client
|
||||
</a>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
+206
@@ -0,0 +1,206 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
.as-view {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.as-loading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 48px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.as-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 12px;
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
fa-icon {
|
||||
color: #1565c0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.as-count {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #059669;
|
||||
background: #ecfdf5;
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.as-note {
|
||||
margin: 0 0 16px;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.as-empty {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 40px;
|
||||
color: #94a3b8;
|
||||
font-size: 14px;
|
||||
background: #f8fafc;
|
||||
border: 2px dashed #e2e8f0;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.as-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
padding: 14px 18px;
|
||||
margin-bottom: 12px;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 12px;
|
||||
|
||||
&__main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__avatar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
flex-shrink: 0;
|
||||
border-radius: 50%;
|
||||
background: #eaf2fb;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
&__identity {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
&__dot {
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
&__unlinked {
|
||||
font-style: italic;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
&__side {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.as-din {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
&__label {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
text-transform: uppercase;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 13px;
|
||||
color: #1e293b;
|
||||
|
||||
&--none {
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.as-mono {
|
||||
font-family: 'Roboto Mono', 'Courier New', monospace;
|
||||
}
|
||||
|
||||
.as-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 9px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
|
||||
mat-icon {
|
||||
font-size: 13px;
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
&--ok {
|
||||
background: #ecfdf5;
|
||||
color: #047857;
|
||||
border: 1px solid #a7f3d0;
|
||||
}
|
||||
|
||||
&--pending {
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
border: 1px solid #e2e8f0;
|
||||
}
|
||||
}
|
||||
|
||||
.as-link-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 15px;
|
||||
width: 15px;
|
||||
height: 15px;
|
||||
}
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||
// MatAnchor, not MatButton: the link to the signatory's client record is an <a>, and the shared
|
||||
// imports only carry the <button> directive.
|
||||
import { MatAnchor } from '@angular/material/button';
|
||||
import { ClientsService } from '../../clients.service';
|
||||
import { LegalFormId } from '../../models/legal-form.enum';
|
||||
|
||||
/** One row of the tab — a name from the entity's record, enriched from the linked client. */
|
||||
interface AuthorizedSignatory {
|
||||
/** Client id of the individual, when the signatory was linked to a real client record. */
|
||||
clientId: number | null;
|
||||
name: string;
|
||||
cifNumber: string;
|
||||
officeName: string;
|
||||
status: string;
|
||||
dinNumber: string;
|
||||
dinVerified: boolean;
|
||||
/** Details are still being fetched for this row. */
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-authorized-signatories-tab',
|
||||
templateUrl: './authorized-signatories-tab.component.html',
|
||||
styleUrls: ['./authorized-signatories-tab.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
FaIconComponent,
|
||||
MatIcon,
|
||||
MatProgressSpinner,
|
||||
MatAnchor
|
||||
]
|
||||
})
|
||||
export class AuthorizedSignatoriesTabComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
private clientsService = inject(ClientsService);
|
||||
|
||||
clientId = '';
|
||||
loading = true;
|
||||
isNonIndividual = false;
|
||||
signatories: AuthorizedSignatory[] = [];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.clientId = this.route.parent?.snapshot.paramMap.get('clientId') || '';
|
||||
// Read from the parent route's resolved data — the entity's own record already carries the
|
||||
// signatory names and ids, so the tab needs no request of its own to render its rows. Same
|
||||
// subscription style as the other client tabs, which keeps this correct when the view is
|
||||
// re-entered for a different client on a reused component instance.
|
||||
this.route.parent?.data.subscribe((data: any) => {
|
||||
const clientViewData = data?.clientViewData;
|
||||
this.isNonIndividual = clientViewData?.legalForm?.id === LegalFormId.ENTITY;
|
||||
|
||||
const nonPerson = clientViewData?.clientNonPersonDetails;
|
||||
this.signatories = this.buildRows(nonPerson?.authorizedSignatory, nonPerson?.authorizedSignatoryClientIds);
|
||||
this.loading = false;
|
||||
|
||||
// DIN lives on the signatory's own KYC record, not on the entity's — the same place the
|
||||
// General step reads it back from — so each linked row is filled in from there.
|
||||
this.signatories.filter((s) => s.clientId != null).forEach((s) => this.loadSignatoryDetails(s));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Names and client ids are stored as two comma-separated columns written in the same order
|
||||
* (see ClientGeneralStepComponent.syncSignatoryNames), so they are zipped back by position.
|
||||
* A name with no id behind it is still shown: signatories recorded before linking existed, or
|
||||
* typed in without a client record, are part of the entity's mandate and must not disappear.
|
||||
*/
|
||||
private buildRows(names: string | null | undefined, clientIds: string | null | undefined): AuthorizedSignatory[] {
|
||||
const splitCsv = (value: string | null | undefined): string[] =>
|
||||
(value || '')
|
||||
.split(',')
|
||||
.map((part) => part.trim())
|
||||
.filter(Boolean);
|
||||
|
||||
const nameList = splitCsv(names);
|
||||
const idList = splitCsv(clientIds);
|
||||
// An id column longer than the name column would otherwise drop the extra links silently.
|
||||
const rowCount = Math.max(nameList.length, idList.length);
|
||||
|
||||
return Array.from({ length: rowCount }, (_unused, index) => {
|
||||
const rawId = idList[index];
|
||||
const parsedId = rawId ? Number(rawId) : NaN;
|
||||
const clientId = Number.isFinite(parsedId) ? parsedId : null;
|
||||
return {
|
||||
clientId,
|
||||
name: nameList[index] || '',
|
||||
cifNumber: '',
|
||||
officeName: '',
|
||||
status: '',
|
||||
dinNumber: '',
|
||||
dinVerified: false,
|
||||
loading: clientId != null
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private loadSignatoryDetails(signatory: AuthorizedSignatory): void {
|
||||
const id = signatory.clientId!.toString();
|
||||
|
||||
this.clientsService.getClientData(id).subscribe({
|
||||
next: (data: any) => {
|
||||
signatory.name = signatory.name || data?.displayName || '';
|
||||
signatory.cifNumber = data?.cifNumber || data?.accountNo || '';
|
||||
signatory.officeName = data?.officeName || '';
|
||||
signatory.status = data?.status?.value || '';
|
||||
signatory.loading = false;
|
||||
},
|
||||
// A signatory whose client record cannot be read still shows as a row with its stored name,
|
||||
// rather than spinning forever.
|
||||
error: () => {
|
||||
signatory.loading = false;
|
||||
}
|
||||
});
|
||||
|
||||
this.clientsService.getClientKyc(id).subscribe({
|
||||
next: (kyc: any) => {
|
||||
signatory.dinNumber = kyc?.dinNumber || '';
|
||||
signatory.dinVerified = kyc?.dinVerified === true;
|
||||
},
|
||||
error: () => {}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -554,6 +554,22 @@
|
||||
>
|
||||
<i class="fa fa-percent cv-tab-icon"></i> Form 121 / TDS
|
||||
</a>
|
||||
<!--
|
||||
Authorized signatories are an entity concept — the persons named in a Board Resolution as
|
||||
authorised to transact for the company. They are captured in the General step at onboarding
|
||||
but have never been readable anywhere in the client view until now.
|
||||
-->
|
||||
@if (clientViewData.legalForm?.id === 2) {
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./authorized-signatories']"
|
||||
routerLinkActive
|
||||
#signatoriesTab="routerLinkActive"
|
||||
[active]="signatoriesTab.isActive"
|
||||
>
|
||||
<i class="fa fa-user-shield cv-tab-icon"></i> Authorized Signatories
|
||||
</a>
|
||||
}
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./documents']"
|
||||
|
||||
+13
-1
@@ -11,9 +11,21 @@
|
||||
<div>
|
||||
<mifosx-file-upload flex="60%" (change)="onFileSelect($event)" acceptFilter=".png,.jpeg,.jpg"></mifosx-file-upload>
|
||||
|
||||
@if (imagePreview) {
|
||||
<div class="image-preview">
|
||||
<span class="image-preview__label">Preview</span>
|
||||
<img class="image-preview__img" [src]="imagePreview" alt="Selected profile image preview" />
|
||||
<span class="image-preview__name">{{ image.name }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (previewError) {
|
||||
<div class="image-preview__error">{{ previewError }}</div>
|
||||
}
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-raised-button mat-dialog-close>{{ 'labels.buttons.Cancel' | translate }}</button>
|
||||
<button mat-raised-button color="primary" [disabled]="!image" [mat-dialog-close]="image">
|
||||
<button mat-raised-button color="primary" [disabled]="!image || !imagePreview" [mat-dialog-close]="image">
|
||||
{{ 'labels.buttons.Confirm' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
|
||||
+42
@@ -5,3 +5,45 @@
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
.image-preview {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin: 12px 0 4px;
|
||||
}
|
||||
|
||||
.image-preview__label {
|
||||
align-self: flex-start;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.4px;
|
||||
text-transform: uppercase;
|
||||
color: rgb(0 0 0 / 60%);
|
||||
}
|
||||
|
||||
.image-preview__img {
|
||||
/* Capped in vh as well as px so a tall portrait photo cannot push the Cancel/Confirm footer
|
||||
below the fold on a short viewport — `contain` keeps the whole frame visible rather than
|
||||
cropping it, since the point of the preview is to check the picture before it goes live. */
|
||||
max-width: 100%;
|
||||
max-height: min(260px, 40vh);
|
||||
object-fit: contain;
|
||||
border-radius: 10px;
|
||||
background: rgb(0 0 0 / 4%);
|
||||
border: 1px solid rgb(0 0 0 / 12%);
|
||||
}
|
||||
|
||||
.image-preview__name {
|
||||
font-size: 12px;
|
||||
color: rgb(0 0 0 / 60%);
|
||||
overflow-wrap: anywhere;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.image-preview__error {
|
||||
margin: 12px 0 4px;
|
||||
font-size: 13px;
|
||||
color: #b3261e;
|
||||
}
|
||||
|
||||
+33
@@ -33,6 +33,16 @@ export class UploadImageDialogComponent {
|
||||
/** Client Image */
|
||||
image: File;
|
||||
|
||||
/**
|
||||
* Data URL of the selected image, rendered as a preview so the officer sees what they picked
|
||||
* before confirming — the upload replaces the client's profile picture immediately on confirm,
|
||||
* and picking the wrong file is otherwise only discovered once it is already live.
|
||||
*/
|
||||
imagePreview: string | null = null;
|
||||
|
||||
/** Set when the picked file is not a readable image, in place of a preview. */
|
||||
previewError = '';
|
||||
|
||||
/**
|
||||
* Sets file form control value.
|
||||
* @param {any} $event file change event.
|
||||
@@ -40,6 +50,29 @@ export class UploadImageDialogComponent {
|
||||
onFileSelect($event: any) {
|
||||
if ($event.target.files.length > 0) {
|
||||
this.image = $event.target.files[0];
|
||||
this.buildPreview(this.image);
|
||||
}
|
||||
}
|
||||
|
||||
/** Reads the picked file into a data URL for the preview. */
|
||||
private buildPreview(file: File) {
|
||||
this.imagePreview = null;
|
||||
this.previewError = '';
|
||||
|
||||
// The picker's accept filter is a hint the OS dialog can be talked out of, so re-check here
|
||||
// rather than handing a non-image to FileReader and rendering a broken <img>.
|
||||
if (!file.type?.startsWith('image/')) {
|
||||
this.previewError = 'Selected file is not an image. Choose a PNG or JPEG file.';
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
this.imagePreview = reader.result as string;
|
||||
};
|
||||
reader.onerror = () => {
|
||||
this.previewError = 'Could not read the selected file.';
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,221 +104,230 @@
|
||||
<span class="kv-table__col kv-table__col--action">Action</span>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<!--
|
||||
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 {
|
||||
Re-Verify
|
||||
<span class="kv-status kv-status--pending"><mat-icon>radio_button_unchecked</mat-icon> Pending</span>
|
||||
}
|
||||
</button>
|
||||
<!--
|
||||
</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>
|
||||
@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>
|
||||
@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>
|
||||
@if (kycData.panDeclarationNameOnPan) {
|
||||
<div class="kv-decl__rows">
|
||||
<div class="kv-decl__row">
|
||||
<span class="k">Name on PAN</span>
|
||||
<span class="v">{{ kycData.panDeclarationNameOnPan }}</span>
|
||||
<span class="k">Does not match</span>
|
||||
<span class="v strong">{{ panDeclarationMismatchLabel }}</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>
|
||||
}
|
||||
@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>
|
||||
<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>
|
||||
}
|
||||
<!--
|
||||
<!-- 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>
|
||||
@if (!kycData.aadhaarVerified && !aadhaarReceiptTaken) {
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="kv-action-btn"
|
||||
type="button"
|
||||
[disabled]="!kycId || saving"
|
||||
(click)="takeAadhaarReceipt()"
|
||||
>
|
||||
Receipt
|
||||
</button>
|
||||
}
|
||||
</span>
|
||||
</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>
|
||||
@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>
|
||||
@if (kycData.aadhaarEnrolmentDate) {
|
||||
<div class="kv-rcpt__rows">
|
||||
<div class="kv-rcpt__row">
|
||||
<span class="k">Enrolment date</span>
|
||||
<span class="v">{{ formatDate(kycData.aadhaarEnrolmentDate) }}</span>
|
||||
<span class="k">Enrolment ID</span>
|
||||
<span class="v mono">{{ kycData.aadhaarEnrolmentId || '—' }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (kycData.aadhaarReceiptRemarks) {
|
||||
<div class="kv-rcpt__row">
|
||||
<span class="k">Remarks</span>
|
||||
<span class="v">{{ kycData.aadhaarReceiptRemarks }}</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>
|
||||
<div class="kv-rcpt__note">
|
||||
Aadhaar remains unverified. No KYC exception is raised for Aadhaar while this receipt is on file.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<!--
|
||||
@@ -425,23 +434,59 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Enterprise PAN Row (Non-Individual) -->
|
||||
@if (isNonIndividual && kycData?.enterprisePan) {
|
||||
<!--
|
||||
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">{{ kycData.enterprisePan }}</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.enterprisePanVerified) {
|
||||
@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.enterprisePanVerifiedOn ? formatDate(kycData.enterprisePanVerifiedOn) : '—'
|
||||
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>
|
||||
}
|
||||
@@ -464,68 +509,61 @@
|
||||
<span class="kv-table__col kv-table__col--action"></span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Incorporation Date Row (Non-Individual) -->
|
||||
@if (isNonIndividual && kycData?.incorporationDate) {
|
||||
<div class="kv-table__row kv-table__row--link">
|
||||
<span class="kv-table__col kv-table__col--doc">
|
||||
<mat-icon class="kv-doc-icon">event</mat-icon> Incorporation Date
|
||||
</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>
|
||||
}
|
||||
</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. -->
|
||||
<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
|
||||
}
|
||||
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>
|
||||
<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>
|
||||
}
|
||||
|
||||
<!-- Aadhaar OTP inline (shown below table when re-verifying) -->
|
||||
@if (showAadhaarReVerify && !aadhaarOtpSent) {
|
||||
@@ -795,15 +833,19 @@
|
||||
<mat-card-content>
|
||||
<form [formGroup]="kycForm" (ngSubmit)="saveKyc()">
|
||||
<div class="kyc-edit-grid">
|
||||
<!-- PAN -->
|
||||
<!-- 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">{{ 'labels.inputs.PAN Number' | translate }}</span>
|
||||
<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>{{ 'labels.inputs.PAN Number' | translate }}</mat-label>
|
||||
<mat-label>{{
|
||||
isNonIndividual ? 'Enterprise PAN' : ('labels.inputs.PAN Number' | translate)
|
||||
}}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="panNumber"
|
||||
@@ -852,147 +894,154 @@
|
||||
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.
|
||||
-->
|
||||
<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 (!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 -->
|
||||
<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
|
||||
<!-- 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>
|
||||
}
|
||||
</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>
|
||||
@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"
|
||||
class="otp-verify-btn"
|
||||
type="button"
|
||||
[disabled]="!aadhaarOtp || aadhaarOtp.length < 4 || aadhaarOtpVerifying"
|
||||
(click)="verifyAadhaarOtp()"
|
||||
class="verify-btn"
|
||||
[disabled]="
|
||||
!kycId ||
|
||||
!kycForm.get('aadhaarNumber')?.value ||
|
||||
kycForm.get('aadhaarNumber')?.invalid ||
|
||||
verifying.aadhaar
|
||||
"
|
||||
(click)="sendAadhaarOtp()"
|
||||
[matTooltip]="!kycId ? 'Save KYC details first' : ''"
|
||||
>
|
||||
@if (aadhaarOtpVerifying) {
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
@if (verifying.aadhaar) {
|
||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
Verify OTP
|
||||
Send 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>
|
||||
<!--
|
||||
<!-- 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>
|
||||
<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) {
|
||||
|
||||
@@ -221,6 +221,17 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
return this.kycData?.panDeclarationTaken === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* The entity's PAN, for the Enterprise PAN row a non-individual gets in place of the PAN row.
|
||||
*
|
||||
* Falls back to panNumber because that is where the KYC step actually stores it — the entity PAN
|
||||
* is written to both fields on create, but a record saved before `enterprisePan` existed, or one
|
||||
* saved from a screen that only writes panNumber, would otherwise show no PAN at all.
|
||||
*/
|
||||
get enterprisePanNumber(): string {
|
||||
return this.kycData?.enterprisePan || this.kycData?.panNumber || '';
|
||||
}
|
||||
|
||||
/** "Name", "Date of Birth", "Name and Date of Birth", or the not-checked-at-all case. */
|
||||
get panDeclarationMismatchLabel(): string {
|
||||
const fields: string = this.kycData?.panDeclarationMismatchFields;
|
||||
|
||||
@@ -222,6 +222,14 @@
|
||||
<div class="pd-field__value monospace">{{ clientViewData.clientNonPersonDetails.incorpNumber }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.clientNonPersonDetails.dateOfRegistration) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Incorporation Date' | translate }}</div>
|
||||
<div class="pd-field__value">
|
||||
{{ clientViewData.clientNonPersonDetails.dateOfRegistration | dateFormat: 'DD-MM-YYYY' }}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.clientNonPersonDetails.remarks) {
|
||||
<div class="pd-field full-width">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Remarks' | translate }}</div>
|
||||
|
||||
Reference in New Issue
Block a user