/** * 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, DestroyRef, EventEmitter, Input, OnChanges, OnInit, Output, SimpleChanges, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { MatAutocompleteModule, MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { of } from 'rxjs'; import { catchError, debounceTime, distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; /** Custom Services */ import { LockerService } from '../../locker.service'; /** What a pick hands back — enough to show the client and link them. */ export interface PickedClient { clientId: number; displayName: string; mobileNo?: string | null; officeName?: string | null; } /** * Client name box with type-ahead (case-insensitive, from 2 letters) over existing clients. Emits the picked client, * and emits null as soon as the text is changed away from it, so a stale client id is never kept for another name. */ @Component({ selector: 'mifosx-locker-client-picker', template: ` {{ label | translate }} @for (c of suggestions; track c.clientId) { {{ c.displayName }} {{ 'ID' | translate }} {{ c.clientId }} @if (c.mobileNo) { · {{ c.mobileNo }} } @if (c.officeName) { · {{ c.officeName }} } } @if (pickedId) { {{ 'Client ID' | translate }} {{ pickedId }} } @else if (!disabled) { {{ 'Type 2+ letters and pick a client' | translate }} } `, styles: [ ` .picker-field { width: 100%; } .opt-name { display: block; font-weight: 500; line-height: 1.2; } .opt-meta { display: block; font-size: 0.75rem; color: rgb(0 0 0 / 55%); line-height: 1.2; } .picked { color: #1976d2; font-weight: 600; } ` ], imports: [ ...STANDALONE_SHARED_IMPORTS, ReactiveFormsModule, MatAutocompleteModule ] }) export class LockerClientPickerComponent implements OnInit, OnChanges { private lockerService = inject(LockerService); private destroyRef = inject(DestroyRef); @Input() label = 'Client'; @Input() required = false; @Input() disabled = false; /** Pre-selected client, e.g. when an existing application is opened. */ @Input() clientId: number | null = null; @Input() clientName: string | null = null; /** Client ids that may not be picked here (e.g. the main applicant in the joint-holder list). */ @Input() excludeIds: number[] = []; @Output() picked = new EventEmitter(); nameControl = new FormControl(''); suggestions: PickedClient[] = []; pickedId: number | null = null; private pickedName = ''; ngOnInit(): void { this.nameControl.valueChanges .pipe( // A picked option puts the object in the box; only typed text counts as a change. filter((value: any): value is string => typeof value === 'string'), debounceTime(300), map((value: string) => value.trim()), distinctUntilChanged(), switchMap((name: string) => { if (this.pickedId && name !== this.pickedName) { this.pickedId = null; this.pickedName = ''; this.picked.emit(null); } if (name.length < 2 || this.pickedId) { return of([]); } return this.lockerService.searchClientsByName(name).pipe(catchError(() => of([]))); }), takeUntilDestroyed(this.destroyRef) ) .subscribe((rows: any[]) => { // The search returns one row per account; keep one per client. const seen = new Set(); this.suggestions = (rows ?? []) .filter( (r: any) => r?.clientId && !this.excludeIds.includes(r.clientId) && !seen.has(r.clientId) && seen.add(r.clientId) ) .slice(0, 10); }); } ngOnChanges(changes: SimpleChanges): void { if (changes['clientId'] || changes['clientName']) { this.pickedId = this.clientId ?? null; this.pickedName = this.clientName ?? ''; this.nameControl.setValue(this.pickedName, { emitEvent: false }); } } onSelected(event: MatAutocompleteSelectedEvent): void { const client: PickedClient = event.option.value; this.pickedId = client.clientId; this.pickedName = client.displayName; this.suggestions = []; this.nameControl.setValue(client.displayName, { emitEvent: false }); this.picked.emit(client); } displayName = (value: any): string => (typeof value === 'string' ? value : (value?.displayName ?? '')); }