Merge remote-tracking branch 'origin/demo' into demo
This commit is contained in:
@@ -29,6 +29,11 @@
|
||||
Account Status
|
||||
<span class="km-cnt km-cnt--red">34</span>
|
||||
</button>
|
||||
<button class="km-itab" [class.active]="activeSection === 'exceptions'" (click)="showSection('exceptions')">
|
||||
<fa-icon icon="exclamation-triangle"></fa-icon>
|
||||
KYC Exceptions
|
||||
<span class="km-cnt km-cnt--amb">{{ exceptionRows.length }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="km-main">
|
||||
@@ -722,6 +727,284 @@
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- KYC EXCEPTIONS -->
|
||||
@if (activeSection === 'exceptions') {
|
||||
<div class="km-page">
|
||||
<div class="km-page-head">
|
||||
<span class="km-page-title">KYC Exceptions Dashboard</span>
|
||||
<div class="km-actions">
|
||||
<button class="km-btn km-btn-g" (click)="toast('Exporting exception report...')">
|
||||
<fa-icon icon="download"></fa-icon> Export Exceptions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="km-stat-row">
|
||||
<div class="km-stat">
|
||||
<div class="km-stat-n" style="color: var(--km-amber)">{{ exceptionRows.length }}</div>
|
||||
<div class="km-stat-l">Pending Exceptions</div>
|
||||
<div class="km-stat-t" style="color: var(--km-amber)">Immediate attention</div>
|
||||
</div>
|
||||
<div class="km-stat">
|
||||
<div class="km-stat-n" style="color: var(--km-red)">
|
||||
{{ notYetContactedCount }}
|
||||
</div>
|
||||
<div class="km-stat-l">Not Yet Contacted</div>
|
||||
<div class="km-stat-t" style="color: var(--km-red)">Action required</div>
|
||||
</div>
|
||||
<div class="km-stat">
|
||||
<div class="km-stat-n" style="color: var(--km-orange)">
|
||||
{{ panMismatchesCount }}
|
||||
</div>
|
||||
<div class="km-stat-l">PAN Mismatches</div>
|
||||
<div class="km-stat-t" style="color: var(--km-orange)">Requires physical review</div>
|
||||
</div>
|
||||
<div class="km-stat">
|
||||
<div class="km-stat-n" style="color: var(--km-blue)">
|
||||
{{ aadhaarFailuresCount }}
|
||||
</div>
|
||||
<div class="km-stat-l">Aadhaar Failures</div>
|
||||
<div class="km-stat-t" style="color: var(--km-blue)">Re-KYC OTP needed</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="km-card km-mt">
|
||||
<!-- Tab Toggle Header -->
|
||||
<div
|
||||
class="km-ch"
|
||||
style="
|
||||
border-bottom: 1px solid var(--km-g200);
|
||||
padding-bottom: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
"
|
||||
>
|
||||
<div style="display: flex; gap: 8px; align-items: center">
|
||||
<div class="km-ci" style="background: #fef3c7; color: #d97706">
|
||||
<fa-icon icon="exclamation-triangle"></fa-icon>
|
||||
</div>
|
||||
<div class="km-ct">Exceptions Register</div>
|
||||
</div>
|
||||
|
||||
<div class="km-inner-nav" style="width: 100%; border: none; margin: 10px 0 0 0; padding: 0; gap: 8px">
|
||||
<button
|
||||
class="km-itab"
|
||||
[class.active]="activeExceptionSubTab === 'pending'"
|
||||
(click)="activeExceptionSubTab = 'pending'"
|
||||
style="padding: 8px 16px; border-radius: 4px; font-weight: 600; font-size: 13px"
|
||||
>
|
||||
<fa-icon icon="clock"></fa-icon>
|
||||
Pending Exceptions ({{ pendingExceptions.length }})
|
||||
</button>
|
||||
<button
|
||||
class="km-itab"
|
||||
[class.active]="activeExceptionSubTab === 'resolved'"
|
||||
(click)="activeExceptionSubTab = 'resolved'"
|
||||
style="padding: 8px 16px; border-radius: 4px; font-weight: 600; font-size: 13px"
|
||||
>
|
||||
<fa-icon icon="check-circle"></fa-icon>
|
||||
Resolved Exceptions ({{ resolvedExceptions.length }})
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PENDING EXCEPTIONS TAB -->
|
||||
@if (activeExceptionSubTab === 'pending') {
|
||||
@if (pendingExceptions.length === 0) {
|
||||
<div class="km-empty" style="padding: 40px; text-align: center">
|
||||
<div class="km-empty-i">🎉</div>
|
||||
<div style="font-weight: 500; font-size: 16px; margin-top: 12px">All exceptions resolved!</div>
|
||||
<div class="km-empty-s" style="color: var(--km-g500); font-size: 13px; margin-top: 4px">
|
||||
There are no active client KYC exceptions on record.
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<table class="km-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Client / Account</th>
|
||||
<th>Branch</th>
|
||||
<th>Exception Reason & Date</th>
|
||||
<th>Risk</th>
|
||||
<th>Contact Status</th>
|
||||
<th>Contact Logs</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (c of pendingExceptions; track c.id) {
|
||||
<tr>
|
||||
<td>
|
||||
<div
|
||||
class="km-an"
|
||||
[routerLink]="['/clients', c.id, 'kyc']"
|
||||
style="cursor: pointer; color: var(--km-blue); text-decoration: underline"
|
||||
>
|
||||
{{ c.name }}
|
||||
</div>
|
||||
<div class="km-ai">{{ c.account }}</div>
|
||||
</td>
|
||||
<td>{{ c.contact.branch }}</td>
|
||||
<td>
|
||||
<div style="font-weight: 500; color: #b45309">{{ c.exceptionReason }}</div>
|
||||
<div class="km-small">Opened: {{ c.exceptionDate }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="km-b" [ngClass]="riskClass(c.risk)">{{ c.riskLabel }}</span>
|
||||
</td>
|
||||
<td>
|
||||
@if (c.contactHistory && c.contactHistory.length > 0) {
|
||||
<span class="km-del d-ok">Contacted</span>
|
||||
<div class="km-small" style="margin-top: 2px">
|
||||
Last: {{ c.contactHistory[c.contactHistory.length - 1].date }}
|
||||
</div>
|
||||
} @else {
|
||||
<span class="km-del d-fa">Not Contacted</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
@if (c.contactHistory && c.contactHistory.length > 0) {
|
||||
<div class="contact-logs-list" style="max-height: 80px; overflow-y: auto; font-size: 11px">
|
||||
@for (log of c.contactHistory; track $index) {
|
||||
<div style="border-bottom: 1px dashed var(--km-g300); padding: 2px 0">
|
||||
<strong>{{ log.date }} ({{ log.action }}):</strong> {{ log.notes }}
|
||||
<em>— {{ log.officer }}</em>
|
||||
@if (log.platformSent) {
|
||||
@if (log.delivered) {
|
||||
<span class="km-sent-tag km-sent-ok" [title]="log.deliveryMessage"
|
||||
>Sent {{ log.destinationMasked }}</span
|
||||
>
|
||||
} @else {
|
||||
<span class="km-sent-tag km-sent-fail" [title]="log.deliveryMessage">Not sent</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<span style="color: var(--km-g500); font-size: 11px">No logs recorded</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<div style="display: flex; gap: 8px">
|
||||
<button
|
||||
class="km-btn km-btn-ot"
|
||||
style="border-color: var(--km-blue); color: var(--km-blue)"
|
||||
[routerLink]="['/clients', c.id, 'kyc']"
|
||||
>
|
||||
<fa-icon icon="user-check"></fa-icon> Do KYC
|
||||
</button>
|
||||
<button class="km-btn km-btn-ot" (click)="openContactDialog(c)">
|
||||
<fa-icon icon="phone"></fa-icon> Log Call
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- RESOLVED EXCEPTIONS TAB -->
|
||||
@if (activeExceptionSubTab === 'resolved') {
|
||||
@if (resolvedExceptions.length === 0) {
|
||||
<div class="km-empty" style="padding: 40px; text-align: center">
|
||||
<div class="km-empty-i" style="font-size: 32px; filter: grayscale(1)">📋</div>
|
||||
<div style="font-weight: 500; font-size: 16px; margin-top: 12px; color: var(--km-g500)">
|
||||
No resolved exceptions
|
||||
</div>
|
||||
<div class="km-empty-s" style="color: var(--km-g500); font-size: 13px; margin-top: 4px">
|
||||
Resolved KYC exceptions will appear here once verification is completed.
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
<table class="km-tbl">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Client / Account</th>
|
||||
<th>Branch</th>
|
||||
<th>Exception Reason & Resolution</th>
|
||||
<th>Risk</th>
|
||||
<th>Contact Logs</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (c of resolvedExceptions; track c.id) {
|
||||
<tr>
|
||||
<td>
|
||||
<div
|
||||
class="km-an"
|
||||
[routerLink]="['/clients', c.id, 'kyc']"
|
||||
style="cursor: pointer; color: var(--km-blue); text-decoration: underline"
|
||||
>
|
||||
{{ c.name }}
|
||||
</div>
|
||||
<div class="km-ai">{{ c.account }}</div>
|
||||
</td>
|
||||
<td>{{ c.contact.branch }}</td>
|
||||
<td>
|
||||
<div style="font-weight: 500; color: var(--km-g500); text-decoration: line-through">
|
||||
{{ c.exceptionReason }}
|
||||
</div>
|
||||
<div class="km-small">Resolved On: {{ c.exceptionResolvedOn }}</div>
|
||||
</td>
|
||||
<td>
|
||||
<span class="km-b" [ngClass]="riskClass(c.risk)">{{ c.riskLabel }}</span>
|
||||
</td>
|
||||
<td>
|
||||
@if (c.contactHistory && c.contactHistory.length > 0) {
|
||||
<div class="contact-logs-list" style="max-height: 80px; overflow-y: auto; font-size: 11px">
|
||||
@for (log of c.contactHistory; track $index) {
|
||||
<div style="border-bottom: 1px dashed var(--km-g300); padding: 2px 0">
|
||||
<strong>{{ log.date }} ({{ log.action }}):</strong> {{ log.notes }}
|
||||
<em>— {{ log.officer }}</em>
|
||||
@if (log.platformSent) {
|
||||
@if (log.delivered) {
|
||||
<span class="km-sent-tag km-sent-ok" [title]="log.deliveryMessage"
|
||||
>Sent {{ log.destinationMasked }}</span
|
||||
>
|
||||
} @else {
|
||||
<span class="km-sent-tag km-sent-fail" [title]="log.deliveryMessage">Not sent</span>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<span style="color: var(--km-g500); font-size: 11px">No logs recorded</span>
|
||||
}
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
style="
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
background: #e6f4ea;
|
||||
color: #137333;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-weight: 500;
|
||||
font-size: 12px;
|
||||
"
|
||||
>
|
||||
<fa-icon icon="check-circle"></fa-icon> Exception Resolved
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- CLIENT DETAIL MODAL -->
|
||||
@@ -1165,6 +1448,251 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- LOG CONTACT DIALOG -->
|
||||
@if (contactDialogOpen && current) {
|
||||
<div class="km-overlay" (click)="closeContactDialog()">
|
||||
<div class="km-modal hv-modal" (click)="$event.stopPropagation()">
|
||||
<div class="km-mh">
|
||||
<div class="km-mh-av">{{ current.av }}</div>
|
||||
<div>
|
||||
<div class="km-mh-name">Log Customer Contact</div>
|
||||
<div class="km-mh-meta">{{ current.name }} · {{ current.account }}</div>
|
||||
</div>
|
||||
<div class="km-mclose" (click)="closeContactDialog()">✕</div>
|
||||
</div>
|
||||
|
||||
<div class="km-mbody hv-body">
|
||||
<div class="hv-addr">
|
||||
<div class="hv-addr-label">Customer Contact Info</div>
|
||||
<div class="hv-addr-name">{{ current.name }}</div>
|
||||
<div class="hv-addr-text">
|
||||
<fa-icon icon="mobile-alt" class="vkyc-ic"></fa-icon> {{ current.contact.mobile }}
|
||||
</div>
|
||||
<div class="hv-addr-text">
|
||||
<fa-icon icon="envelope" class="vkyc-ic"></fa-icon> {{ current.contact.email }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hv-row">
|
||||
<label class="hv-label">Contact Channel<span class="hv-req">*</span></label>
|
||||
<select class="hv-input" [(ngModel)]="contactChannel">
|
||||
@for (ch of contactChannels; track ch.value) {
|
||||
<option [value]="ch.value">
|
||||
{{ ch.label }}{{ ch.sends ? ' — sends a notice now' : ' — record only' }}
|
||||
</option>
|
||||
}
|
||||
</select>
|
||||
@if (selectedChannelSends) {
|
||||
<div class="hv-hint hv-hint-send">
|
||||
<fa-icon icon="paper-plane" class="vkyc-ic"></fa-icon>
|
||||
A KYC notice will be sent to the customer when you save.
|
||||
</div>
|
||||
} @else {
|
||||
<div class="hv-hint">Records an action you already took. Nothing is sent to the customer.</div>
|
||||
}
|
||||
@if (selectedChannelUnavailable) {
|
||||
<div class="hv-hint hv-hint-error">
|
||||
This customer has no {{ selectedChannel?.requires === 'email' ? 'email address' : 'mobile number' }} on
|
||||
file — choose a different channel.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="hv-row">
|
||||
<label class="hv-label">KYC Officer Logging<span class="hv-req">*</span></label>
|
||||
<select class="hv-input" [(ngModel)]="contactOfficer">
|
||||
@for (officer of bankOfficers; track officer) {
|
||||
<option [value]="officer">{{ officer }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="hv-row">
|
||||
<label class="hv-label">Summary / Notes<span class="hv-req">*</span></label>
|
||||
<textarea
|
||||
class="hv-input hv-textarea"
|
||||
rows="3"
|
||||
placeholder="Enter details of conversation e.g. Customer promised to visit branch next Tuesday..."
|
||||
[(ngModel)]="contactNotes"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
@if (contactError) {
|
||||
<div class="hv-hint hv-hint-error" role="alert">
|
||||
<fa-icon icon="exclamation-circle" class="vkyc-ic"></fa-icon> {{ contactError }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="hv-footer">
|
||||
<button class="km-btn" [disabled]="contactSaving" (click)="closeContactDialog()">Cancel</button>
|
||||
<button class="km-btn km-btn-t" [disabled]="contactSaveDisabled" (click)="saveContactLog()">
|
||||
{{ contactSaveLabel }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- RESOLVE KYC DIALOG -->
|
||||
@if (resolveDialogOpen && current) {
|
||||
<div class="km-overlay" (click)="closeResolveDialog()">
|
||||
<div class="km-modal hv-modal" (click)="$event.stopPropagation()">
|
||||
<div class="km-mh" style="border-bottom: 2px solid var(--km-green)">
|
||||
<div class="km-mh-av" style="background: var(--km-green)">{{ current.av }}</div>
|
||||
<div>
|
||||
<div class="km-mh-name">Resolve KYC Exception</div>
|
||||
<div class="km-mh-meta">{{ current.name }} · {{ current.account }}</div>
|
||||
</div>
|
||||
<div class="km-mclose" (click)="closeResolveDialog()">✕</div>
|
||||
</div>
|
||||
|
||||
<div class="km-mbody hv-body">
|
||||
<div
|
||||
style="
|
||||
background: #f0fdf4;
|
||||
border: 1px solid #bbf7d0;
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 13px;
|
||||
color: #166534;
|
||||
"
|
||||
>
|
||||
<strong>Pending Reason:</strong> {{ current.exceptionReason }}
|
||||
@if (current.exceptionRemarks) {
|
||||
<div style="margin-top: 4px; font-style: italic">"{{ current.exceptionRemarks }}"</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (showPanResolution) {
|
||||
<div
|
||||
style="
|
||||
border: 1px solid var(--km-g300);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin-bottom: 16px;
|
||||
background: #fafafa;
|
||||
"
|
||||
>
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px">
|
||||
<input type="checkbox" id="resolvePanChk" [(ngModel)]="resolvePanActive" />
|
||||
<label for="resolvePanChk" style="font-weight: 600; cursor: pointer; color: var(--km-g800)"
|
||||
>Resolve PAN Card Exception</label
|
||||
>
|
||||
</div>
|
||||
@if (resolvePanActive) {
|
||||
<div class="hv-row" style="margin-left: 20px">
|
||||
<label class="hv-label">Verified PAN Number<span class="hv-req">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
class="hv-input"
|
||||
[(ngModel)]="resolvePanNumber"
|
||||
placeholder="e.g. ABCDE1234F"
|
||||
maxlength="10"
|
||||
style="text-transform: uppercase"
|
||||
/>
|
||||
@if (resolvePanNumber && resolvePanNumber.length !== 10) {
|
||||
<div style="color: red; font-size: 11px; margin-top: 4px">PAN must be exactly 10 characters.</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (showAadhaarResolution) {
|
||||
<div
|
||||
style="
|
||||
border: 1px solid var(--km-g300);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin-bottom: 16px;
|
||||
background: #fafafa;
|
||||
"
|
||||
>
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px">
|
||||
<input type="checkbox" id="resolveAadhChk" [(ngModel)]="resolveAadhaarActive" />
|
||||
<label for="resolveAadhChk" style="font-weight: 600; cursor: pointer; color: var(--km-g800)"
|
||||
>Resolve Aadhaar Card Exception</label
|
||||
>
|
||||
</div>
|
||||
@if (resolveAadhaarActive) {
|
||||
<div class="hv-row" style="margin-left: 20px">
|
||||
<label class="hv-label">Verified Aadhaar Number<span class="hv-req">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
class="hv-input"
|
||||
[(ngModel)]="resolveAadhaarNumber"
|
||||
placeholder="e.g. 597855025299"
|
||||
maxlength="12"
|
||||
/>
|
||||
@if (resolveAadhaarNumber && resolveAadhaarNumber.length !== 12) {
|
||||
<div style="color: red; font-size: 11px; margin-top: 4px">Aadhaar must be exactly 12 digits.</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (showGeneralResolution) {
|
||||
<div
|
||||
style="
|
||||
border: 1px solid var(--km-g300);
|
||||
border-radius: 6px;
|
||||
padding: 12px;
|
||||
margin-bottom: 16px;
|
||||
background: #fafafa;
|
||||
"
|
||||
>
|
||||
<div style="display: flex; align-items: center; gap: 8px; margin-bottom: 8px">
|
||||
<input type="checkbox" id="resolveGenChk" [(ngModel)]="resolveGeneralActive" />
|
||||
<label for="resolveGenChk" style="font-weight: 600; cursor: pointer; color: var(--km-g800)"
|
||||
>Resolve General Exception</label
|
||||
>
|
||||
</div>
|
||||
@if (resolveGeneralActive) {
|
||||
<div style="margin-left: 20px">
|
||||
<div class="hv-row">
|
||||
<label class="hv-label">Verification Document Type<span class="hv-req">*</span></label>
|
||||
<select class="hv-input" [(ngModel)]="resolveGeneralDocumentType">
|
||||
<option value="OVD">Officially Valid Document (Passport/Voter ID)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="hv-row">
|
||||
<label class="hv-label">Document Identification Number<span class="hv-req">*</span></label>
|
||||
<input
|
||||
type="text"
|
||||
class="hv-input"
|
||||
[(ngModel)]="resolveGeneralDocumentNumber"
|
||||
placeholder="Enter verified document number"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="hv-row">
|
||||
<label class="hv-label">Resolution Comments<span class="hv-req">*</span></label>
|
||||
<textarea
|
||||
class="hv-input hv-textarea"
|
||||
rows="3"
|
||||
placeholder="Describe how verification was resolved..."
|
||||
[(ngModel)]="resolveRemarks"
|
||||
></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="hv-footer">
|
||||
<button class="km-btn" (click)="closeResolveDialog()">Cancel</button>
|
||||
<button class="km-btn km-btn-g" [disabled]="!isResolveValid" (click)="saveResolveKyc()">
|
||||
Complete KYC & Resolve Exception
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- TOAST -->
|
||||
<div class="km-toast" [class.show]="toastVisible">{{ toastMsg }}</div>
|
||||
</div>
|
||||
|
||||
@@ -1588,6 +1588,58 @@
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
/* Inline note under a field. The default (neutral) form states that nothing is transmitted;
|
||||
the -send and -error variants call out the two cases the officer must not misread. */
|
||||
.hv-hint {
|
||||
font-size: 11px;
|
||||
line-height: 1.45;
|
||||
color: var(--km-g500);
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.hv-hint-send {
|
||||
color: #0f766e;
|
||||
background: #f0fdfa;
|
||||
border: 1px solid #99f6e4;
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.hv-hint-error {
|
||||
color: #b91c1c;
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
border-radius: 4px;
|
||||
padding: 6px 8px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Delivery outcome chip on a contact-log row. A logged attempt and a delivered notice are
|
||||
different facts, so they never share a colour. */
|
||||
.km-sent-tag {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
padding: 0 5px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
vertical-align: baseline;
|
||||
}
|
||||
|
||||
.km-sent-ok {
|
||||
background: #e6f4ea;
|
||||
color: #137333;
|
||||
}
|
||||
|
||||
.km-sent-fail {
|
||||
background: #fef2f2;
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
.hv-footer {
|
||||
padding: 14px 22px;
|
||||
border-top: 1px solid var(--km-g200);
|
||||
@@ -1648,3 +1700,28 @@
|
||||
color: #0f172a;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
/* KYC Exceptions Styles */
|
||||
.km-ci-e {
|
||||
background: var(--km-amber-l) !important;
|
||||
color: var(--km-amber) !important;
|
||||
}
|
||||
|
||||
.contact-logs-list {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
font-family: inherit;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
&::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: #cbd5e1;
|
||||
border-radius: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,10 +6,11 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { ClientsService } from '../clients/clients.service';
|
||||
|
||||
interface KmNotice {
|
||||
no: string;
|
||||
@@ -31,6 +32,24 @@ interface KmEvidence {
|
||||
docket?: string;
|
||||
}
|
||||
|
||||
/** One outreach attempt, as returned by GET /kyc/exceptions/{clientId}/contact-log. */
|
||||
interface KmContactLog {
|
||||
date: string;
|
||||
/** Display label for the channel used, e.g. 'SMS' / 'In-Branch Visit'. */
|
||||
action: string;
|
||||
notes: string;
|
||||
officer: string;
|
||||
/** True when this channel transmits (SMS/Email/WhatsApp) rather than being recorded offline. */
|
||||
platformSent?: boolean;
|
||||
/** True only when the notice actually reached the gateway. */
|
||||
delivered?: boolean;
|
||||
/** SENT | FAILED | SKIPPED_NO_DESTINATION | PROVIDER_NOT_CONFIGURED | NOT_APPLICABLE */
|
||||
deliveryStatus?: string;
|
||||
/** Officer-facing outcome sentence — shown verbatim so a failure is never mistaken for a send. */
|
||||
deliveryMessage?: string;
|
||||
destinationMasked?: string;
|
||||
}
|
||||
|
||||
interface KmClient {
|
||||
id: string;
|
||||
av: string;
|
||||
@@ -66,6 +85,17 @@ interface KmClient {
|
||||
pendingActions: string[];
|
||||
actionTaken: string[];
|
||||
evidence: KmEvidence | null;
|
||||
isException?: boolean;
|
||||
exceptionReason?: string;
|
||||
exceptionRemarks?: string;
|
||||
exceptionDate?: string;
|
||||
exceptionResolved?: boolean;
|
||||
exceptionResolvedOn?: string;
|
||||
contactHistory?: KmContactLog[];
|
||||
kycId?: string;
|
||||
/** Whether the customer has a mobile / email on file — drives channel availability in the dialog. */
|
||||
hasMobile?: boolean;
|
||||
hasEmail?: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -79,7 +109,8 @@ interface KmClient {
|
||||
]
|
||||
})
|
||||
export class KycManagementComponent implements OnInit {
|
||||
activeSection: 'alerts' | 'notif' | 'audit' | 'status' = 'alerts';
|
||||
private clientsService = inject(ClientsService);
|
||||
activeSection: 'alerts' | 'notif' | 'audit' | 'status' | 'exceptions' = 'alerts';
|
||||
|
||||
modalOpen = false;
|
||||
modalTab: 'overview' | 'timeline' | 'notices' | 'evidence' | 'action' = 'overview';
|
||||
@@ -529,15 +560,399 @@ export class KycManagementComponent implements OnInit {
|
||||
};
|
||||
|
||||
alertRows: KmClient[] = [];
|
||||
exceptionRows: KmClient[] = [];
|
||||
pendingExceptions: KmClient[] = [];
|
||||
resolvedExceptions: KmClient[] = [];
|
||||
activeExceptionSubTab: 'pending' | 'resolved' = 'pending';
|
||||
selectedExceptionReason = '';
|
||||
selectedExceptionContactStatus = '';
|
||||
|
||||
// Dialog properties for exceptions
|
||||
contactDialogOpen = false;
|
||||
contactChannel = 'PHONE_CALL';
|
||||
contactNotes = '';
|
||||
contactOfficer = 'Neha Verma — KYC Officer (Bhopal Main)';
|
||||
/** In-flight guard — a double-click must not send the customer two notices. */
|
||||
contactSaving = false;
|
||||
/** Populated when the save itself failed (not when the save worked but delivery didn't). */
|
||||
contactError = '';
|
||||
|
||||
/**
|
||||
* Channels offered in the Log Customer Contact dialog. `sends` marks the ones where saving
|
||||
* transmits a real notice to the customer, as opposed to recording an action the officer already
|
||||
* took offline — the dialog labels and the confirm button both key off it.
|
||||
*/
|
||||
readonly contactChannels: { value: string; label: string; sends: boolean; requires?: 'mobile' | 'email' }[] = [
|
||||
{ value: 'PHONE_CALL', label: 'Phone Call', sends: false },
|
||||
{ value: 'SMS', label: 'SMS', sends: true, requires: 'mobile' },
|
||||
{ value: 'EMAIL', label: 'Email', sends: true, requires: 'email' },
|
||||
{ value: 'WHATSAPP', label: 'WhatsApp Message', sends: true, requires: 'mobile' },
|
||||
{ value: 'BRANCH_VISIT', label: 'In-Branch Visit', sends: false },
|
||||
{ value: 'FIELD_VISIT', label: 'Agent Field Visit', sends: false }
|
||||
];
|
||||
|
||||
resolveDialogOpen = false;
|
||||
resolveDocumentType = 'PAN';
|
||||
resolveDocumentNumber = '';
|
||||
resolveRemarks = '';
|
||||
|
||||
resolvePanActive = false;
|
||||
resolvePanNumber = '';
|
||||
resolveAadhaarActive = false;
|
||||
resolveAadhaarNumber = '';
|
||||
resolveGeneralActive = false;
|
||||
resolveGeneralDocumentType = 'OVD';
|
||||
resolveGeneralDocumentNumber = '';
|
||||
|
||||
showPanResolution = false;
|
||||
showAadhaarResolution = false;
|
||||
showGeneralResolution = false;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.alertRows = Object.values(this.clients);
|
||||
this.loadExceptions();
|
||||
}
|
||||
|
||||
showSection(section: 'alerts' | 'notif' | 'audit' | 'status'): void {
|
||||
loadExceptions(): void {
|
||||
this.clientsService.getKycExceptions(false).subscribe({
|
||||
next: (data) => {
|
||||
this.pendingExceptions = data.map((item: any) => this.mapException(item, false));
|
||||
this.exceptionRows = this.pendingExceptions;
|
||||
this.loadContactHistories(this.pendingExceptions);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Failed to load active exceptions', err);
|
||||
this.pendingExceptions = [];
|
||||
this.exceptionRows = [];
|
||||
}
|
||||
});
|
||||
|
||||
this.clientsService.getKycExceptions(true).subscribe({
|
||||
next: (data) => {
|
||||
this.resolvedExceptions = data.map((item: any) => this.mapException(item, true));
|
||||
this.loadContactHistories(this.resolvedExceptions);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Failed to load resolved exceptions', err);
|
||||
this.resolvedExceptions = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private mapException(item: any, resolved: boolean): KmClient {
|
||||
return {
|
||||
id: item.clientId?.toString(),
|
||||
av: (item.clientDisplayName || 'Client').split(' ').map((n: string) => n[0]).join('').substring(0, 2).toUpperCase(),
|
||||
name: item.clientDisplayName,
|
||||
meta: `${item.clientAccountNo || `CLI-${item.clientId}`} · Savings A/c · ${item.clientOfficeName || 'Bhopal Main'}`,
|
||||
account: item.clientAccountNo || `CLI-${item.clientId}`,
|
||||
state: 'alert',
|
||||
risk: item.riskCategoryCode?.toLowerCase() || 'low',
|
||||
riskLabel: item.riskCategory || 'Low Risk',
|
||||
expiry: 'Pending',
|
||||
daysLeft: 30,
|
||||
kyc: {
|
||||
type: item.kycType || 'Individual',
|
||||
initiated: item.initiatedOn || '—',
|
||||
lastVerified: item.verifiedOn || '—',
|
||||
validity: '—',
|
||||
pan: item.panNumber || '—',
|
||||
aadhaar: item.aadhaarNumberMasked || '—',
|
||||
panStatus: item.panVerified ? 'Verified' : 'Verification Failed',
|
||||
aadhaarStatus: item.aadhaarVerified ? 'Verified' : 'Verification Failed',
|
||||
panLink: 'Not Linked'
|
||||
},
|
||||
contact: {
|
||||
// Masked server-side; the full values never reach the browser. 'Not on file' is meaningful
|
||||
// here — it's why an SMS or Email channel will be unavailable for this customer.
|
||||
mobile: item.clientMobileMasked || 'Not on file',
|
||||
email: item.clientEmailMasked || 'Not on file',
|
||||
address: 'Client Address, Bhopal MP 462002',
|
||||
branch: item.clientOfficeName || 'Bhopal Main'
|
||||
},
|
||||
notices: [],
|
||||
noticesSent: 0,
|
||||
noticePending: resolved ? 'Resolved Exception' : 'Pending verification',
|
||||
nextStage: resolved ? 'Exception Resolved' : 'KYC Exceptions review',
|
||||
pendingActions: resolved ? [] : ['Resolve Exception reasons'],
|
||||
actionTaken: resolved ? ['KYC Exception resolved'] : [],
|
||||
evidence: null,
|
||||
isException: true,
|
||||
// No invented default — a record whose reason was never stored (or was cleared by the old
|
||||
// resolve path) must not be relabelled as a PAN failure it may never have had.
|
||||
exceptionReason: item.exceptionReason || 'Reason not recorded',
|
||||
exceptionRemarks: item.remarks || '',
|
||||
exceptionDate: item.initiatedOn || '—',
|
||||
exceptionResolved: resolved,
|
||||
exceptionResolvedOn: item.exceptionResolvedOn || '—',
|
||||
contactHistory: [],
|
||||
kycId: item.id?.toString(),
|
||||
hasMobile: item.hasMobile === true,
|
||||
hasEmail: item.hasEmail === true
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull each row's persisted outreach history. Fired after the exception list loads; a failure on
|
||||
* one row leaves that row's history empty rather than blanking the whole grid.
|
||||
*/
|
||||
private loadContactHistories(rows: KmClient[]): void {
|
||||
for (const row of rows) {
|
||||
if (!row.id) continue;
|
||||
this.clientsService.getKycContactLog(row.id).subscribe({
|
||||
next: (logs) => {
|
||||
row.contactHistory = (logs || []).map((log: any) => this.mapContactLog(log));
|
||||
},
|
||||
error: (err) => {
|
||||
console.error(`Failed to load contact log for client ${row.id}`, err);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
get notYetContactedCount(): number {
|
||||
return this.exceptionRows.filter(c => !c.contactHistory || c.contactHistory.length === 0).length;
|
||||
}
|
||||
|
||||
get panMismatchesCount(): number {
|
||||
return this.exceptionRows.filter(c => c.exceptionReason?.includes('PAN')).length;
|
||||
}
|
||||
|
||||
get aadhaarFailuresCount(): number {
|
||||
return this.exceptionRows.filter(c => c.exceptionReason?.includes('Aadhaar')).length;
|
||||
}
|
||||
|
||||
showSection(section: 'alerts' | 'notif' | 'audit' | 'status' | 'exceptions'): void {
|
||||
this.activeSection = section;
|
||||
}
|
||||
|
||||
openContactDialog(client: KmClient): void {
|
||||
this.current = client;
|
||||
this.contactChannel = 'PHONE_CALL';
|
||||
this.contactNotes = '';
|
||||
this.contactError = '';
|
||||
this.contactSaving = false;
|
||||
this.contactOfficer = this.bankOfficers[1]; // Neha Verma
|
||||
this.contactDialogOpen = true;
|
||||
}
|
||||
|
||||
closeContactDialog(): void {
|
||||
this.contactDialogOpen = false;
|
||||
}
|
||||
|
||||
/** The channel currently picked in the dialog. */
|
||||
get selectedChannel(): { value: string; label: string; sends: boolean; requires?: 'mobile' | 'email' } | undefined {
|
||||
return this.contactChannels.find(c => c.value === this.contactChannel);
|
||||
}
|
||||
|
||||
/** True when saving will transmit a notice to the customer, not merely record one. */
|
||||
get selectedChannelSends(): boolean {
|
||||
return this.selectedChannel?.sends === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the picked channel needs a destination the customer doesn't have. Blocks the save so
|
||||
* the officer isn't left reading a failure they could have seen up front.
|
||||
*/
|
||||
get selectedChannelUnavailable(): boolean {
|
||||
const requires = this.selectedChannel?.requires;
|
||||
if (!requires || !this.current) return false;
|
||||
return requires === 'mobile' ? this.current.hasMobile === false : this.current.hasEmail === false;
|
||||
}
|
||||
|
||||
get contactSaveDisabled(): boolean {
|
||||
return !this.contactNotes?.trim() || !this.contactChannel || this.contactSaving || this.selectedChannelUnavailable;
|
||||
}
|
||||
|
||||
/** 'Send & Save' when a notice goes out, plain 'Save Log' when it's an offline record. */
|
||||
get contactSaveLabel(): string {
|
||||
if (this.contactSaving) return this.selectedChannelSends ? 'Sending…' : 'Saving…';
|
||||
return this.selectedChannelSends ? 'Send & Save' : 'Save Log';
|
||||
}
|
||||
|
||||
saveContactLog(): void {
|
||||
if (!this.current || this.contactSaveDisabled) return;
|
||||
|
||||
const client = this.current;
|
||||
this.contactSaving = true;
|
||||
this.contactError = '';
|
||||
|
||||
this.clientsService
|
||||
.saveKycContactLog(client.id, {
|
||||
channel: this.contactChannel,
|
||||
notes: this.contactNotes.trim(),
|
||||
officerName: this.contactOfficer
|
||||
})
|
||||
.subscribe({
|
||||
next: (log: any) => {
|
||||
this.contactSaving = false;
|
||||
client.contactHistory = [...(client.contactHistory || []), this.mapContactLog(log)];
|
||||
|
||||
// The request succeeding does not mean the customer was reached — a gateway failure comes
|
||||
// back as HTTP 200 with delivered=false, so report what actually happened.
|
||||
if (log?.platformSent && !log?.delivered) {
|
||||
this.toast(log?.deliveryMessage || `Logged, but the ${log?.channelLabel || 'message'} could not be sent.`);
|
||||
} else {
|
||||
this.toast(log?.deliveryMessage || `Contact attempt logged successfully for ${client.name}`);
|
||||
}
|
||||
this.contactDialogOpen = false;
|
||||
},
|
||||
error: (err) => {
|
||||
this.contactSaving = false;
|
||||
// Keep the dialog open — the officer's notes are still in the textarea and would
|
||||
// otherwise be lost.
|
||||
this.contactError =
|
||||
err?.error?.errors?.[0]?.defaultUserMessage ||
|
||||
err?.error?.defaultUserMessage ||
|
||||
'Could not save the contact log. Please try again.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private mapContactLog(log: any): KmContactLog {
|
||||
return {
|
||||
date: log?.contactedAt ? this.formatLogDate(log.contactedAt) : '—',
|
||||
action: log?.channelLabel || log?.channel || 'Contact',
|
||||
notes: log?.notes || '',
|
||||
officer: (log?.officerName || '').split(' — ')[0],
|
||||
platformSent: log?.platformSent,
|
||||
delivered: log?.delivered,
|
||||
deliveryStatus: log?.deliveryStatus,
|
||||
deliveryMessage: log?.deliveryMessage,
|
||||
destinationMasked: log?.destinationMasked
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* `contactedAt` arrives as a LocalDateTime — Jackson serialises it as an ISO string or as a
|
||||
* [y, m, d, h, min, s] array depending on the tenant's date-format setting, so handle both.
|
||||
*/
|
||||
private formatLogDate(value: any): string {
|
||||
const date = Array.isArray(value)
|
||||
? new Date(value[0], (value[1] || 1) - 1, value[2] || 1, value[3] || 0, value[4] || 0)
|
||||
: new Date(value);
|
||||
if (isNaN(date.getTime())) return '—';
|
||||
return date.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
|
||||
}
|
||||
|
||||
get isResolveValid(): boolean {
|
||||
if (!this.resolveRemarks?.trim()) return false;
|
||||
let anyActive = false;
|
||||
if (this.showPanResolution && this.resolvePanActive) {
|
||||
anyActive = true;
|
||||
if (!this.resolvePanNumber || this.resolvePanNumber.trim().length !== 10) return false;
|
||||
}
|
||||
if (this.showAadhaarResolution && this.resolveAadhaarActive) {
|
||||
anyActive = true;
|
||||
if (!this.resolveAadhaarNumber || this.resolveAadhaarNumber.trim().length !== 12) return false;
|
||||
}
|
||||
if (this.showGeneralResolution && this.resolveGeneralActive) {
|
||||
anyActive = true;
|
||||
if (!this.resolveGeneralDocumentNumber?.trim()) return false;
|
||||
}
|
||||
return anyActive;
|
||||
}
|
||||
|
||||
openResolveDialog(client: KmClient): void {
|
||||
this.current = client;
|
||||
const reason = client.exceptionReason || '';
|
||||
this.showPanResolution = reason.includes('PAN');
|
||||
this.showAadhaarResolution = reason.includes('Aadhaar');
|
||||
this.showGeneralResolution = !this.showPanResolution && !this.showAadhaarResolution;
|
||||
|
||||
this.resolvePanActive = this.showPanResolution;
|
||||
this.resolveAadhaarActive = this.showAadhaarResolution;
|
||||
this.resolveGeneralActive = this.showGeneralResolution;
|
||||
|
||||
this.resolvePanNumber = '';
|
||||
this.resolveAadhaarNumber = '';
|
||||
this.resolveGeneralDocumentType = 'OVD';
|
||||
this.resolveGeneralDocumentNumber = '';
|
||||
this.resolveRemarks = '';
|
||||
this.resolveDialogOpen = true;
|
||||
}
|
||||
|
||||
closeResolveDialog(): void {
|
||||
this.resolveDialogOpen = false;
|
||||
}
|
||||
|
||||
saveResolveKyc(): void {
|
||||
if (!this.current || !this.current.kycId) return;
|
||||
const clientId = this.current.id;
|
||||
const kycId = this.current.kycId;
|
||||
|
||||
const today = new Date();
|
||||
const day = today.getDate().toString().padStart(2, '0');
|
||||
const months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
||||
const monthStr = months[today.getMonth()];
|
||||
const year = today.getFullYear();
|
||||
const dateStr = `${day} ${monthStr} ${year}`;
|
||||
|
||||
const resolvingPan = this.showPanResolution && this.resolvePanActive;
|
||||
const resolvingAadhaar = this.showAadhaarResolution && this.resolveAadhaarActive;
|
||||
const resolvingGeneral = this.showGeneralResolution && this.resolveGeneralActive;
|
||||
|
||||
const remainsPan = this.showPanResolution && !this.resolvePanActive;
|
||||
const remainsAadhaar = this.showAadhaarResolution && !this.resolveAadhaarActive;
|
||||
|
||||
const isFullyResolved = !remainsPan && !remainsAadhaar;
|
||||
|
||||
const resolvedKycData: any = {
|
||||
locale: 'en',
|
||||
dateFormat: 'dd MMMM yyyy',
|
||||
remarks: this.resolveRemarks || 'Resolved via exceptions dashboard'
|
||||
};
|
||||
|
||||
if (isFullyResolved) {
|
||||
resolvedKycData.isException = false;
|
||||
resolvedKycData.exceptionResolved = true;
|
||||
resolvedKycData.exceptionResolvedOn = dateStr;
|
||||
// Keep exceptionReason — the Resolved tab shows what was resolved, and clearing it destroys
|
||||
// the audit record of why the exception was ever raised.
|
||||
} else {
|
||||
resolvedKycData.isException = true;
|
||||
resolvedKycData.exceptionResolved = false;
|
||||
const remainingReasons: string[] = [];
|
||||
if (remainsPan) remainingReasons.push('PAN Card Verification Failed');
|
||||
if (remainsAadhaar) remainingReasons.push('Aadhaar Verification Failed');
|
||||
resolvedKycData.exceptionReason = remainingReasons.join(', ');
|
||||
}
|
||||
|
||||
if (resolvingPan) {
|
||||
resolvedKycData.panNumber = this.resolvePanNumber.toUpperCase().trim();
|
||||
resolvedKycData.panVerified = true;
|
||||
}
|
||||
if (resolvingAadhaar) {
|
||||
const aadhaarRaw = this.resolveAadhaarNumber.trim();
|
||||
resolvedKycData.aadhaarNumberMasked = 'XXXXXXXX' + aadhaarRaw.slice(-4);
|
||||
resolvedKycData.aadhaarVerified = true;
|
||||
}
|
||||
if (resolvingGeneral) {
|
||||
resolvedKycData.kycDocumentName = this.resolveGeneralDocumentType;
|
||||
resolvedKycData.kycDocumentNumber = this.resolveGeneralDocumentNumber;
|
||||
resolvedKycData.isException = false;
|
||||
resolvedKycData.exceptionResolved = true;
|
||||
resolvedKycData.exceptionResolvedOn = dateStr;
|
||||
// exceptionReason preserved — see the fully-resolved branch above.
|
||||
}
|
||||
|
||||
this.clientsService.updateClientKyc(clientId, kycId, resolvedKycData).subscribe({
|
||||
next: () => {
|
||||
const msg = isFullyResolved
|
||||
? `KYC Exception fully resolved and completed for ${this.current?.name}`
|
||||
: `KYC Exception partially resolved for ${this.current?.name}`;
|
||||
this.toast(msg);
|
||||
this.loadExceptions();
|
||||
this.resolveDialogOpen = false;
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Failed to resolve exception', err);
|
||||
this.toast('Failed to resolve exception. Please try again.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
openModal(id: string): void {
|
||||
const c = this.clients[id];
|
||||
if (!c) return;
|
||||
|
||||
Reference in New Issue
Block a user