feat: update kyc tab, branch day register, and dormancy reports
This commit is contained in:
@@ -280,6 +280,82 @@
|
||||
</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.
|
||||
@@ -565,73 +641,6 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Aadhaar OTP inline (shown below table when re-verifying) -->
|
||||
@if (showAadhaarReVerify && !aadhaarOtpSent) {
|
||||
<div class="kv-otp-bar">
|
||||
<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>
|
||||
</div>
|
||||
}
|
||||
@if (aadhaarOtpSent) {
|
||||
<div class="kv-otp-bar">
|
||||
<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>
|
||||
</div>
|
||||
}
|
||||
@if (verificationError.aadhaar) {
|
||||
<div class="kyc-error-msg">
|
||||
<mat-icon class="kyc-error-icon">error_outline</mat-icon> {{ verificationError.aadhaar }}
|
||||
@@ -1128,6 +1137,69 @@
|
||||
</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"
|
||||
|
||||
@@ -856,6 +856,18 @@
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
// Sitting inside the table, directly under the Aadhaar row it belongs to. Indented and tinted so
|
||||
// it reads as that row opening up rather than as a new section, and the flat top edge joins it to
|
||||
// the row above.
|
||||
.kv-otp-bar--inline {
|
||||
margin: 0 0 8px 28px;
|
||||
background: #eff6ff;
|
||||
border-color: #bfdbfe;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.kv-otp-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -1576,3 +1588,96 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Officially valid document, on the edit form ────────────────────────────
|
||||
// Set apart from the identity fields above it because it is one document with its own
|
||||
// provenance — a type, a number, and the two dates that decide whether it is still valid —
|
||||
// rather than four unrelated inputs.
|
||||
|
||||
.kyc-doc-fields {
|
||||
margin: 4px 0 20px;
|
||||
padding: 16px;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.kyc-doc-fields__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 14px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.kyc-doc-fields__title {
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.02em;
|
||||
text-transform: uppercase;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.kyc-doc-fields__flag {
|
||||
padding: 3px 10px;
|
||||
border-radius: 999px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #92400e;
|
||||
background: #fef3c7;
|
||||
}
|
||||
|
||||
// Four across on a wide screen, folding to two and then one. The dates sit next to each other
|
||||
// at every width, since it is the pair that has to be read together.
|
||||
.kyc-doc-fields__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
|
||||
@media (width <= 1280px) {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
@media (width <= 640px) {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-doc-scans-link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
padding: 10px 12px;
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 6px;
|
||||
font-size: 13px;
|
||||
color: #334155;
|
||||
text-decoration: none;
|
||||
background: #fff;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
background 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #94a3b8;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-doc-scans-link__go {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
@@ -126,6 +126,12 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
1,
|
||||
[Validators.required]
|
||||
],
|
||||
// The officially valid document, so a lapsed one can be re-collected on this form. The
|
||||
// backend has always accepted these four; only the fields were missing.
|
||||
kycDocumentName: [''],
|
||||
kycDocumentNumber: [''],
|
||||
kycDocIssueDate: [null],
|
||||
kycDocExpiryDate: [null],
|
||||
panNumber: [
|
||||
'',
|
||||
[Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)]
|
||||
@@ -176,6 +182,10 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
// the full 12-digit number to re-verify, so don't pre-fill.
|
||||
aadhaarNumber: '',
|
||||
gstinNumber: data.gstinNumber || '',
|
||||
kycDocumentName: data.kycDocumentName || '',
|
||||
kycDocumentNumber: data.kycDocumentNumber || '',
|
||||
kycDocIssueDate: this.parseApiDate(data.kycDocIssueDate),
|
||||
kycDocExpiryDate: this.parseApiDate(data.kycDocExpiryDate),
|
||||
remarks: data.remarks || '',
|
||||
isException: data.isException || false,
|
||||
exceptionReason: data.exceptionReason ? data.exceptionReason.split(', ') : []
|
||||
@@ -355,6 +365,41 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
* stored record; either way the customer has cleared that check. Verifying one of two
|
||||
* outstanding items narrows the exception to the remaining one rather than closing it.
|
||||
*/
|
||||
/**
|
||||
* The officially valid document, for the update payload.
|
||||
*
|
||||
* Only sent when a document is named, so an officer editing the risk category on a client who
|
||||
* never had an OVD recorded does not silently blank the other three columns.
|
||||
*/
|
||||
private buildDocumentFields() {
|
||||
const name = (this.kycForm.value.kycDocumentName || '').trim();
|
||||
if (!name) {
|
||||
return {};
|
||||
}
|
||||
const dateFormat = 'dd MMMM yyyy';
|
||||
return {
|
||||
kycDocumentName: name,
|
||||
kycDocumentNumber: (this.kycForm.value.kycDocumentNumber || '').trim() || null,
|
||||
kycDocIssueDate: this.kycForm.value.kycDocIssueDate
|
||||
? this.dateUtils.formatDate(this.kycForm.value.kycDocIssueDate, dateFormat)
|
||||
: null,
|
||||
// Blank is meaningful: a passport expires, a PAN card does not.
|
||||
kycDocExpiryDate: this.kycForm.value.kycDocExpiryDate
|
||||
? this.dateUtils.formatDate(this.kycForm.value.kycDocExpiryDate, dateFormat)
|
||||
: null
|
||||
};
|
||||
}
|
||||
|
||||
/** True while the banner is asking for a document, so the form shows the fields to supply it. */
|
||||
get kycDocumentNeedsUpdate(): boolean {
|
||||
return this.showKycDocumentAlert;
|
||||
}
|
||||
|
||||
/** Today, as the latest an existing document can have been issued. */
|
||||
get maxDate(): Date {
|
||||
return new Date();
|
||||
}
|
||||
|
||||
private buildExceptionFields() {
|
||||
const panVerified = this.verificationStatus.pan === 'VERIFIED' || this.kycData?.panVerified === true;
|
||||
const aadhaarVerified = this.verificationStatus.aadhaar === 'VERIFIED' || this.kycData?.aadhaarVerified === true;
|
||||
@@ -392,6 +437,7 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
remarks: this.kycForm.value.remarks || null,
|
||||
// dateFormat is required so the backend can parse exceptionResolvedOn when an exception closes.
|
||||
dateFormat: 'dd MMMM yyyy',
|
||||
...this.buildDocumentFields(),
|
||||
...this.buildExceptionFields()
|
||||
};
|
||||
|
||||
@@ -478,11 +524,12 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
if (response?.changes?.panVerified) {
|
||||
this.verificationStatus.pan = 'VERIFIED';
|
||||
this.verificationDetails.pan = response.changes;
|
||||
// A PAN that now verifies makes any earlier declaration moot.
|
||||
// A PAN that now verifies makes any earlier declaration moot. That write already
|
||||
// re-derives the exception columns, so it settles the exception too.
|
||||
if (this.panDeclarationTaken) {
|
||||
this.writePanDeclaration(null);
|
||||
} else {
|
||||
this.loadKycData();
|
||||
this.syncExceptionAfterVerify();
|
||||
}
|
||||
} else {
|
||||
this.verificationStatus.pan = 'FAILED';
|
||||
@@ -660,6 +707,60 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-derives the exception columns once a verification has actually passed.
|
||||
*
|
||||
* `saveKycBeforeVerify()` writes the exception state *before* the vendor is called, when the
|
||||
* check is by definition still outstanding — so a check that then succeeds leaves the record
|
||||
* carrying an exception nobody closes. The KYC tab reads `panVerified` and shows "Verified"
|
||||
* while the exceptions register keeps reading `isException`, which is why a client could sit in
|
||||
* the Pending tab with every document verified.
|
||||
*
|
||||
* Called on each verification success. `buildExceptionFields()` already prefers the in-session
|
||||
* `verificationStatus` over the stored record, so the freshly passed check counts even though
|
||||
* `kycData` has not been reloaded yet; `hadException` still reads the stored value, which is
|
||||
* what tells it to close the entry rather than leave every flag off.
|
||||
*
|
||||
* Verifying one of two outstanding items narrows the exception instead of closing it — that
|
||||
* falls out of the derivation, so this needs no special case for it.
|
||||
*/
|
||||
private syncExceptionAfterVerify(): void {
|
||||
if (!this.kycId) {
|
||||
this.loadKycData();
|
||||
return;
|
||||
}
|
||||
|
||||
const exceptionFields = this.buildExceptionFields();
|
||||
|
||||
// Nothing recorded and nothing outstanding — no write would change anything.
|
||||
if (this.kycData?.isException !== true && !exceptionFields.isException) {
|
||||
this.loadKycData();
|
||||
return;
|
||||
}
|
||||
|
||||
this.clientsService
|
||||
.updateClientKyc(this.clientId, this.kycId, {
|
||||
locale: 'en',
|
||||
dateFormat: 'dd MMMM yyyy',
|
||||
riskCategory: this.kycForm.value.riskCategory || 1,
|
||||
...exceptionFields
|
||||
})
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
// The verification itself succeeded either way, so a failure here must not read as a
|
||||
// failed check — surface it as the save problem it is and still refresh.
|
||||
next: () => this.loadKycData(),
|
||||
error: (err: any) => {
|
||||
this.errorMessage =
|
||||
err?.error?.developerMessage ||
|
||||
err?.error?.defaultUserMessage ||
|
||||
err?.message ||
|
||||
'Verification succeeded but the KYC exception could not be updated.';
|
||||
this.loadKycData();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Records the UIDAI enrolment receipt accepted in place of a verified Aadhaar.
|
||||
*
|
||||
@@ -786,6 +887,12 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
this.verificationError.aadhaar = null;
|
||||
}
|
||||
|
||||
/** Abandons the OTP exchange, so a mis-click can be backed out of without reloading the tab. */
|
||||
cancelAadhaarReVerify(): void {
|
||||
this.resetOtpState();
|
||||
this.verificationError.aadhaar = null;
|
||||
}
|
||||
|
||||
/** Step 1: Send OTP to Aadhaar-linked mobile */
|
||||
sendAadhaarOtp(): void {
|
||||
const aadhaar = this.isEditMode ? this.kycForm?.get('aadhaarNumber')?.value : this.aadhaarFullNumber;
|
||||
@@ -855,7 +962,7 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
if (this.aadhaarReceiptTaken) {
|
||||
this.writeAadhaarReceipt(null);
|
||||
} else {
|
||||
this.loadKycData();
|
||||
this.syncExceptionAfterVerify();
|
||||
}
|
||||
} else {
|
||||
this.verificationStatus.aadhaar = 'FAILED';
|
||||
|
||||
Reference in New Issue
Block a user