/** * 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: `

Mobile Lookup #{{ row.id }}

Mobile Number {{ row.mobileNumber }}
Status {{ row.lookupStatus }}
Result {{ row.responseMessage || '—' }}
Source {{ row.lookupSource || '—' }}
Client @if (row.clientId) { {{ row.clientName || 'Client #' + row.clientId }} {{ row.clientAccountNo || row.clientId }} } @else { Not onboarded }
Looked Up {{ row.lookedUpOn | date: 'medium' }}
@if (row.overrideDecision) {
Recorded Decision
{{ row.overrideDecision === 'PROCEEDED' ? 'PROCEEDED' : 'REJECTED' }}

{{ row.overrideReason || 'No reason recorded.' }}

Decided by user {{ row.decidedBy || '—' }} on {{ row.decidedOn | date: 'medium' }}
} @else {
Record a Decision
This number is flagged and has not been decided. It cannot be saved on a client until an officer proceeds with reasons.
@if (showReasonForm) {
Minimum 10 characters — this reason is kept on the lookup record.
} } @if (errorMessage) {
{{ errorMessage }}
}
@if (!row.overrideDecision && !showReasonForm) { } @if (!row.overrideDecision && showReasonForm) { } `, 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); 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); } }