Files
cbsnexora/src/app/reports/dip-lookups/dip-decision-dialog.component.ts
T
keshavandClaude Opus 5 7ef75c902d
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s
feat: add database export, welcome kit dialog, and DIP/sanctions updates
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-17 17:52:28 +05:30

364 lines
9.8 KiB
TypeScript

/**
* 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, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { MatDialogRef, MatDialogModule, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { DipLookupsService } from './dip-lookups.service';
/**
* Records — or displays — the officer's decision on a flagged DIP/MNRL lookup.
*
* The same decision can be taken inline during onboarding; this is the back-office path for
* numbers that were left undecided, and the read-only view once a decision exists.
*/
@Component({
selector: 'mifosx-dip-decision-dialog',
template: `
<h2 mat-dialog-title>
<fa-icon icon="satellite-dish" class="title-icon"></fa-icon>
Mobile Lookup #{{ row.id }}
</h2>
<mat-dialog-content>
<div class="detail-grid">
<div class="detail-row">
<span class="detail-label">Mobile Number</span>
<span class="detail-value strong">{{ row.mobileNumber }}</span>
</div>
<div class="detail-row">
<span class="detail-label">Status</span>
<span class="detail-value" [class.danger]="row.lookupStatus !== 'CLEAN'">{{ row.lookupStatus }}</span>
</div>
<div class="detail-row">
<span class="detail-label">Result</span>
<span class="detail-value">{{ row.responseMessage || '—' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">Source</span>
<span class="detail-value">{{ row.lookupSource || '—' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">Client</span>
<span class="detail-value">
@if (row.clientId) {
{{ row.clientName || 'Client #' + row.clientId }}
<span class="detail-sub">{{ row.clientAccountNo || row.clientId }}</span>
} @else {
Not onboarded
}
</span>
</div>
<div class="detail-row">
<span class="detail-label">Looked Up</span>
<span class="detail-value">{{ row.lookedUpOn | date: 'medium' }}</span>
</div>
</div>
@if (row.overrideDecision) {
<div class="section-title">Recorded Decision</div>
<div class="decision-card" [class.decision-rejected]="row.overrideDecision === 'REJECTED'">
<div class="decision-badge">
{{ row.overrideDecision === 'PROCEEDED' ? 'PROCEEDED' : 'REJECTED' }}
</div>
<p class="decision-reason">{{ row.overrideReason || 'No reason recorded.' }}</p>
<div class="decision-meta">
Decided by user {{ row.decidedBy || '—' }} on {{ row.decidedOn | date: 'medium' }}
</div>
</div>
} @else {
<div class="section-title">Record a Decision</div>
<div class="pending-note">
<fa-icon icon="exclamation-circle"></fa-icon>
This number is flagged and has not been decided. It cannot be saved on a client until an officer proceeds with
reasons.
</div>
@if (showReasonForm) {
<div class="reason-form">
<label class="field-label">Why is this number acceptable despite the DIP/MNRL result?</label>
<textarea
class="field-input"
rows="3"
[(ngModel)]="reason"
placeholder="e.g. Number re-issued to the applicant on 02/2026; ownership confirmed against the operator's CAF and a fresh OTP."
></textarea>
<div class="field-hint" [class.field-error]="touched && !reasonValid">
Minimum 10 characters — this reason is kept on the lookup record.
</div>
</div>
}
}
@if (errorMessage) {
<div class="error-note">{{ errorMessage }}</div>
}
</mat-dialog-content>
<mat-dialog-actions align="end">
<button mat-stroked-button (click)="close()" [disabled]="saving">Close</button>
@if (!row.overrideDecision && !showReasonForm) {
<button mat-stroked-button color="warn" (click)="reject()" [disabled]="saving">Reject Number</button>
<button mat-raised-button color="primary" (click)="showReasonForm = true" [disabled]="saving">
Proceed with Reason
</button>
}
@if (!row.overrideDecision && showReasonForm) {
<button mat-raised-button color="primary" (click)="proceed()" [disabled]="saving">
{{ saving ? 'Saving…' : 'Save Decision' }}
</button>
}
</mat-dialog-actions>
`,
styles: [
`
:host {
display: block;
min-width: 500px;
}
h2[mat-dialog-title] {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
padding: 16px 24px;
font-size: 18px;
font-weight: 600;
color: #1074b9;
}
mat-dialog-content {
padding: 16px 24px;
}
.detail-grid {
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 8px 14px;
}
.detail-row {
display: flex;
justify-content: space-between;
gap: 16px;
padding: 6px 0;
border-bottom: 1px solid #f3f4f6;
}
.detail-row:last-child {
border-bottom: none;
}
.detail-label {
font-size: 12px;
color: #9ca3af;
font-weight: 500;
white-space: nowrap;
}
.detail-value {
font-size: 12px;
color: #374151;
font-weight: 600;
text-align: right;
}
.detail-value.strong {
color: #111827;
font-size: 13px;
}
.detail-value.danger {
color: #dc2626;
}
.detail-sub {
display: block;
font-size: 11px;
font-weight: 400;
color: #9ca3af;
}
.section-title {
font-size: 11px;
font-weight: 700;
letter-spacing: 0.6px;
text-transform: uppercase;
color: #6b7280;
margin: 16px 0 6px;
}
.pending-note {
display: flex;
align-items: flex-start;
gap: 8px;
font-size: 13px;
color: #92400e;
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 6px;
padding: 10px 12px;
line-height: 1.5;
}
.decision-card {
background: #ecfdf5;
border: 1px solid #a7f3d0;
border-left: 4px solid #059669;
border-radius: 6px;
padding: 10px 14px;
}
.decision-card.decision-rejected {
background: #fef2f2;
border-color: #fecaca;
border-left-color: #dc2626;
}
.decision-badge {
font-size: 11px;
font-weight: 800;
letter-spacing: 1px;
color: #059669;
}
.decision-rejected .decision-badge {
color: #dc2626;
}
.decision-reason {
font-size: 13px;
color: #374151;
line-height: 1.6;
margin: 6px 0;
white-space: pre-wrap;
}
.decision-meta {
font-size: 11px;
color: #9ca3af;
}
.reason-form {
display: flex;
flex-direction: column;
gap: 4px;
margin-top: 10px;
}
.field-label {
font-size: 12px;
font-weight: 600;
color: #374151;
}
.field-input {
width: 100%;
box-sizing: border-box;
font-family: inherit;
font-size: 13px;
padding: 8px 10px;
border: 1px solid #d1d5db;
border-radius: 4px;
resize: vertical;
}
.field-input:focus {
outline: none;
border-color: #1074b9;
}
.field-hint {
font-size: 11px;
color: #9ca3af;
}
.field-error {
color: #dc2626;
font-weight: 600;
}
.error-note {
font-size: 12px;
font-weight: 600;
color: #dc2626;
background: #fef2f2;
border: 1px solid #fecaca;
border-radius: 6px;
padding: 8px 12px;
margin-top: 12px;
}
mat-dialog-actions {
padding: 8px 24px 16px;
gap: 8px;
}
`
],
imports: [
CommonModule,
FormsModule,
MatDialogModule,
MatButtonModule,
FaIconComponent
]
})
export class DipDecisionDialogComponent {
private dialogRef = inject(MatDialogRef<DipDecisionDialogComponent>);
private service = inject(DipLookupsService);
row: any = inject(MAT_DIALOG_DATA);
showReasonForm = false;
reason = '';
touched = false;
saving = false;
errorMessage = '';
private changed = false;
get reasonValid(): boolean {
return this.reason.trim().length >= 10;
}
proceed() {
this.touched = true;
if (!this.reasonValid) {
return;
}
this.save('PROCEEDED', this.reason.trim());
}
reject() {
this.save('REJECTED', 'Number rejected on review of the DIP/MNRL result.');
}
private save(decision: string, reason: string) {
this.saving = true;
this.errorMessage = '';
this.service.recordDecision(this.row.id, decision, reason).subscribe({
next: (updated: any) => {
this.saving = false;
this.changed = true;
this.row = updated || this.row;
this.showReasonForm = false;
},
error: (err: any) => {
this.saving = false;
this.errorMessage =
err?.error?.defaultUserMessage || err?.error?.message || err?.message || 'Could not save the decision.';
}
});
}
close() {
this.dialogRef.close(this.changed);
}
}