/** * 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; } } }