Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
203 lines
6.6 KiB
TypeScript
203 lines
6.6 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/.
|
||
*/
|
||
|
||
/** Angular Imports */
|
||
import { Component, inject } from '@angular/core';
|
||
import { ActivatedRoute } from '@angular/router';
|
||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||
|
||
/** Custom Imports */
|
||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
|
||
import { DecimalPipe } from '@angular/common';
|
||
import { LegalFormId } from 'app/clients/models/legal-form.enum';
|
||
|
||
/** Interfaces */
|
||
interface ClientViewData {
|
||
id: number;
|
||
accountNo?: string;
|
||
externalId?: string;
|
||
status?: { id: number; code: string; value: string };
|
||
active?: boolean;
|
||
activationDate?: number[];
|
||
firstname?: string;
|
||
middlename?: string;
|
||
lastname?: string;
|
||
fullname?: string;
|
||
displayName?: string;
|
||
mobileNo?: string;
|
||
emailAddress?: string;
|
||
/**
|
||
* Current DIP/MNRL status resolved from the latest lookup, as opposed to the `dipVerified`
|
||
* flag captured when the client was onboarded.
|
||
*/
|
||
dipLiveStatus?: string;
|
||
alternateDipLiveStatus?: string;
|
||
/** Disposition of the latest flagged sanctions alert, or absent when the client carries none. */
|
||
sanctionsAlertStatus?: string;
|
||
dateOfBirth?: number[];
|
||
gender?: { id: number; name: string; active?: boolean };
|
||
clientType?: { id: number; name: string; active?: boolean };
|
||
clientClassification?: { id: number; name: string; active?: boolean };
|
||
legalForm?: { id: number; name: string };
|
||
officeId?: number;
|
||
officeName?: string;
|
||
staffId?: number;
|
||
staffName?: string;
|
||
savingsProductId?: number;
|
||
savingsProductName?: string;
|
||
[key: string]: any; // Allow additional properties from API
|
||
}
|
||
|
||
/**
|
||
* Personal Data Tab Component.
|
||
* Displays all personal/client details in a read-only format.
|
||
*/
|
||
@Component({
|
||
selector: 'mifosx-personal-data-tab',
|
||
templateUrl: './personal-data-tab.component.html',
|
||
styleUrls: ['./personal-data-tab.component.scss'],
|
||
standalone: true,
|
||
imports: [
|
||
...STANDALONE_SHARED_IMPORTS,
|
||
DateFormatPipe,
|
||
DecimalPipe
|
||
]
|
||
})
|
||
export class PersonalDataTabComponent {
|
||
private route = inject(ActivatedRoute);
|
||
|
||
/** Client View Data */
|
||
clientViewData!: ClientViewData;
|
||
|
||
/**
|
||
* Whether the number's CURRENT DIP/MNRL status is a hit.
|
||
*
|
||
* The onboarding-time `dipVerified` flag never changes, so a customer cleared when they joined
|
||
* kept reading "Verified" even after their number was reported for fraud or reassigned. These
|
||
* helpers read `dipLiveStatus`, which the API resolves from the most recent lookup.
|
||
*/
|
||
isDipFlagged(liveStatus: string | null | undefined): boolean {
|
||
return !!liveStatus && liveStatus !== 'CLEAN' && liveStatus !== 'ERROR';
|
||
}
|
||
|
||
/** True only when the number is affirmatively clean — not merely "not flagged". */
|
||
isDipClear(client: any): boolean {
|
||
if (client?.dipLiveStatus) {
|
||
return client.dipLiveStatus === 'CLEAN';
|
||
}
|
||
return (
|
||
client?.mobileVerified === true ||
|
||
client?.mobileVerified === 'true' ||
|
||
client?.mobileVerified === 1 ||
|
||
client?.dipVerified === true ||
|
||
client?.dipVerified === 'true' ||
|
||
client?.dipVerified === 1
|
||
);
|
||
}
|
||
|
||
dipStatusLabel(client: any): string {
|
||
return this.labelFor(client?.dipLiveStatus, this.isDipClear(client));
|
||
}
|
||
|
||
/** Headline for the sanctions banner, driven by how far the alert has progressed. */
|
||
sanctionsAlertHeadline(client: any): string {
|
||
switch (client?.sanctionsAlertStatus) {
|
||
case 'CONFIRMED_MATCH':
|
||
return 'Confirmed UN Security Council sanctions match.';
|
||
case 'FALSE_POSITIVE':
|
||
return 'Sanctions hit reviewed as a false positive.';
|
||
default:
|
||
return 'Sanctions match awaiting Principal Officer review.';
|
||
}
|
||
}
|
||
|
||
sanctionsAlertDetail(client: any): string {
|
||
switch (client?.sanctionsAlertStatus) {
|
||
case 'CONFIRMED_MATCH':
|
||
return (
|
||
'Debits are frozen on this client’s deposit accounts and the case has been escalated. ' +
|
||
'Report to FIU-IND and the nodal officer within 24 hours if not already done.'
|
||
);
|
||
case 'FALSE_POSITIVE':
|
||
return 'Recorded as a different person from the listed individual. See the Sanctions Logs for the reasoning.';
|
||
default:
|
||
return (
|
||
'This client matched the UN Security Council Consolidated List on a later screening. ' +
|
||
'Review the alert under Reports → Sanctions Screening & Alerts before transacting.'
|
||
);
|
||
}
|
||
}
|
||
|
||
alternateDipStatusLabel(client: any): string {
|
||
const clear =
|
||
client?.alternateDipLiveStatus === 'CLEAN' ||
|
||
client?.alternateDipVerified === true ||
|
||
client?.alternateDipVerified === 'true' ||
|
||
client?.alternateDipVerified === 1;
|
||
return this.labelFor(client?.alternateDipLiveStatus, clear);
|
||
}
|
||
|
||
private labelFor(liveStatus: string | null | undefined, clear: boolean): string {
|
||
switch (liveStatus) {
|
||
case 'SUSPECTED':
|
||
return 'Flagged — Suspected';
|
||
case 'DISCONNECTED':
|
||
return 'Flagged — Disconnected';
|
||
case 'SUSPECTED_AND_DISCONNECTED':
|
||
return 'Flagged — Suspected & Disconnected';
|
||
case 'SERVICE_UNAVAILABLE':
|
||
return 'Not Verified';
|
||
case 'CLEAN':
|
||
return 'Verified';
|
||
default:
|
||
return clear ? 'Verified' : 'Pending';
|
||
}
|
||
}
|
||
|
||
constructor() {
|
||
this.route.parent.data.pipe(takeUntilDestroyed()).subscribe((data: { clientViewData: ClientViewData }) => {
|
||
this.clientViewData = data.clientViewData;
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Check if client is a person (individual)
|
||
*/
|
||
isPerson(): boolean {
|
||
return this.clientViewData?.legalForm?.id === LegalFormId.PERSON;
|
||
}
|
||
|
||
/**
|
||
* Check if client is a legal entity (organization)
|
||
*/
|
||
isLegalEntity(): boolean {
|
||
return this.clientViewData?.legalForm?.id === LegalFormId.ENTITY;
|
||
}
|
||
|
||
/**
|
||
* Format ISO timestamp to DD-MM-YYYY HH:MM:SS
|
||
*/
|
||
formatDateTime(value: string): string {
|
||
if (!value) return '-';
|
||
try {
|
||
const d = new Date(value);
|
||
if (isNaN(d.getTime())) return value;
|
||
const dd = String(d.getDate()).padStart(2, '0');
|
||
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
||
const yyyy = d.getFullYear();
|
||
const hh = String(d.getHours()).padStart(2, '0');
|
||
const min = String(d.getMinutes()).padStart(2, '0');
|
||
const ss = String(d.getSeconds()).padStart(2, '0');
|
||
return `${dd}-${mm}-${yyyy} ${hh}:${min}:${ss}`;
|
||
} catch {
|
||
return value;
|
||
}
|
||
}
|
||
}
|