/** * 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 { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute } from '@angular/router'; import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base'; import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base'; import { SelectBase } from 'app/shared/form-dialog/formfield/model/select-base'; /** Custom Components */ import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component'; import { EditAddressDialogComponent } from './edit-address-dialog/edit-address-dialog.component'; import { SuccessDialogComponent } from 'app/shared/success-dialog/success-dialog.component'; import { ErrorDialogComponent } from 'app/shared/error-dialog/error-dialog.component'; /** Custom Services */ import { TranslateService } from '@ngx-translate/core'; import { ClientsService } from '../../clients.service'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatAccordion, MatExpansionPanel, MatExpansionPanelHeader, MatExpansionPanelTitle, MatExpansionPanelDescription } from '@angular/material/expansion'; import { MatDivider } from '@angular/material/divider'; import { MatSlideToggle } from '@angular/material/slide-toggle'; import { MatTooltip } from '@angular/material/tooltip'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; /** * Clients Address Tab Component */ @Component({ selector: 'mifosx-address-tab', templateUrl: './address-tab.component.html', styleUrls: ['./address-tab.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, FaIconComponent, MatAccordion, MatExpansionPanel, MatExpansionPanelHeader, MatExpansionPanelTitle, MatExpansionPanelDescription, MatDivider, MatSlideToggle, MatTooltip ] }) export class AddressTabComponent { private route = inject(ActivatedRoute); private clientService = inject(ClientsService); private dialog = inject(MatDialog); private translateService = inject(TranslateService); /** Client Address Data */ clientAddressData: any; /** Client Address Field Config */ clientAddressFieldConfig: any; /** Client Address Template */ clientAddressTemplate: any; /** Client Id */ clientId: string; /** * Welcome kit outcome per address id. Only addresses a kit was actually posted to appear here: delivery * proves the address is real, a returned kit proves it is not, and an address with no kit stays unmarked. */ addressVerification: Record = {}; /** * @param {ActivatedRoute} route Activated Route * @param {ClientsService} clientService Clients Service * @param {MatDialog} dialog Mat Dialog * @param {TranslateService} translateService Translate Service. */ constructor() { this.route.data.subscribe( (data: { clientAddressData: any; clientAddressFieldConfig: any; clientAddressTemplateData: any }) => { this.clientAddressData = data.clientAddressData; this.clientAddressFieldConfig = data.clientAddressFieldConfig; this.clientAddressTemplate = data.clientAddressTemplateData; this.clientId = this.route.parent.snapshot.paramMap.get('clientId'); this.loadWelcomeKitVerification(); } ); } /** * Marks addresses a welcome kit was posted to with the outcome of that delivery. The kit is the physical * check on the address, so a delivered kit reads as verified and a returned one as not verified. */ private loadWelcomeKitVerification(): void { if (!this.clientId) { return; } this.clientService.getWelcomeKits(this.clientId).subscribe({ next: (response: any) => { const map: Record = {}; // Oldest first, so that when an address was posted to more than once the newest outcome wins. const dispatches = [...(response?.dispatches || [])].reverse(); for (const dispatch of dispatches) { if (dispatch?.addressId == null) { continue; } if (dispatch.status === 'DELIVERED') { map[dispatch.addressId] = 'VERIFIED'; } else if (dispatch.status === 'RETURNED') { map[dispatch.addressId] = 'NOT_VERIFIED'; } } this.addressVerification = map; }, // A client with no welcome kit permission or no kit yet simply shows no badge. error: () => { this.addressVerification = {}; } }); } /** Verification badge for an address, or null when no kit has been posted to it. */ verificationOf(address: any): 'VERIFIED' | 'NOT_VERIFIED' | null { return this.addressVerification[address?.addressId] || null; } /** * Adds a client address. */ addAddress() { // Use the Nexora dialog with PIN lookup + "Copy from existing address" picker. const ref = this.dialog.open(EditAddressDialogComponent, { data: { mode: 'add', title: this.translateService.instant('labels.buttons.Add') + ' ' + this.translateService.instant('labels.catalogs.Client') + ' ' + this.translateService.instant('labels.heading.Address'), address: null, template: this.clientAddressTemplate, fieldConfig: this.clientAddressFieldConfig, existingAddresses: this.clientAddressData || [] }, panelClass: 'ead-panel', autoFocus: false }); ref.afterClosed().subscribe((response: any) => { if (!response?.value) return; const payload = { ...response.value }; const typeName = this.getSelectedValue('addressTypeIdOptions', payload.addressType)?.name; this.clientService.createClientAddress(this.clientId, payload.addressType, payload).subscribe({ next: (res: any) => { payload.addressId = res.resourceId; payload.addressType = typeName; payload.isActive = false; this.clientAddressData.push(payload); this.showSuccess('Address Added', `${typeName || 'Address'} has been added to this client's profile.`); }, error: (err: any) => { const apiMsg = err?.error?.errors?.[0]?.defaultUserMessage || err?.error?.defaultUserMessage || 'Please try again.'; this.showError(`Failed to add address. ${apiMsg}`); } }); }); } /** * Edits an existing address. * @param {any} address Client address * @param {number} index address index */ editAddress(address: any, index: number) { // Open the Nexora edit dialog with PIN lookup (mirrors the stepper). const ref = this.dialog.open(EditAddressDialogComponent, { data: { title: this.translateService.instant('labels.buttons.Edit') + ' ' + this.translateService.instant('labels.catalogs.Client') + ' ' + this.translateService.instant('labels.heading.Address'), address, template: this.clientAddressTemplate, fieldConfig: this.clientAddressFieldConfig }, panelClass: 'ead-panel', autoFocus: false }); ref.afterClosed().subscribe((response: any) => { if (!response?.value) return; const addressData = { ...response.value }; addressData.addressId = address.addressId; addressData.isActive = address.isActive; this.clientService.editClientAddress(this.clientId, address.addressTypeId, addressData).subscribe({ next: () => { addressData.addressTypeId = address.addressTypeId; addressData.addressType = address.addressType; this.clientAddressData[index] = addressData; this.showSuccess('Address Updated', `${address.addressType || 'Address'} has been updated successfully.`); }, error: (err: any) => { const apiMsg = err?.error?.errors?.[0]?.defaultUserMessage || err?.error?.defaultUserMessage || 'Please try again.'; this.showError(`Failed to update address. ${apiMsg}`); } }); }); } /** Polished success modal — matches the rest of the Nexora UI. */ private showSuccess(title: string, message: string): void { this.dialog.open(SuccessDialogComponent, { data: { title, message, buttonLabel: 'OK' }, panelClass: 'sd-panel' }); } /** Polished error modal — single string body. */ private showError(message: string): void { this.dialog.open(ErrorDialogComponent, { data: message }); } /** * Toggles address activity. * @param {any} address Client Address */ toggleAddress(address: any) { const addressData = { addressId: address.addressId, isActive: address.isActive ? false : true }; this.clientService.editClientAddress(this.clientId, address.addressTypeId, addressData).subscribe(() => { address.isActive = address.isActive ? false : true; }); } /** * Checks if field is enabled in address config. * @param {any} fieldName Field Name */ isFieldEnabled(fieldName: any) { return this.clientAddressFieldConfig.find((fieldObj: any) => fieldObj.field === fieldName)?.isEnabled; } /** * Find Pipe doesn't work with accordian * @param {any} fieldName Field Name * @param {any} fieldId Field Id */ getSelectedValue(fieldName: any, fieldId: any) { return this.clientAddressTemplate[fieldName].find((fieldObj: any) => fieldObj.id === fieldId); } /** * Returns address form fields for form dialog. * @param {string} formType Form Type * @param {any} address Address */ getAddressFormFields(formType?: string, address?: any) { let formfields: FormfieldBase[] = []; for (let index = 0; index < this.clientAddressTemplate.addressTypeIdOptions.length; index++) { this.clientAddressTemplate.addressTypeIdOptions[index].name = this.translateService.instant( `labels.catalogs.${this.clientAddressTemplate.addressTypeIdOptions[index].name}` ); } if (formType === 'add') { formfields.push( this.isFieldEnabled('addressType') ? new SelectBase({ controlName: 'addressType', label: this.translateService.instant('labels.inputs.Address Type'), value: address ? address.addressType : '', options: { label: 'name', value: 'id', data: this.clientAddressTemplate.addressTypeIdOptions }, order: 1 }) : null ); } formfields.push( this.isFieldEnabled('postalCode') ? new InputBase({ controlName: 'postalCode', label: this.translateService.instant('labels.inputs.Postal Code'), value: address ? address.postalCode : '', type: 'text', order: 2 }) : null ); formfields.push( this.isFieldEnabled('street') ? new InputBase({ controlName: 'street', label: this.translateService.instant('labels.inputs.Street'), value: address ? address.street : '', type: 'text', required: false, order: 3 }) : null ); formfields.push( this.isFieldEnabled('addressLine1') ? new InputBase({ controlName: 'addressLine1', label: this.translateService.instant('labels.inputs.Address Line') + ' 1', value: address ? address.addressLine1 : '', type: 'text', order: 3 }) : null ); formfields.push( this.isFieldEnabled('addressLine2') ? new InputBase({ controlName: 'addressLine2', label: this.translateService.instant('labels.inputs.Address Line') + ' 2', value: address ? address.addressLine2 : '', type: 'text', order: 4 }) : null ); formfields.push( this.isFieldEnabled('addressLine3') ? new InputBase({ controlName: 'addressLine3', label: this.translateService.instant('labels.inputs.Address Line') + ' 3', value: address ? address.addressLine3 : '', type: 'text', order: 5 }) : null ); formfields.push( this.isFieldEnabled('townVillage') ? new InputBase({ controlName: 'townVillage', label: this.translateService.instant('labels.inputs.Town / Village'), value: address ? address.townVillage : '', type: 'text', order: 6 }) : null ); formfields.push( this.isFieldEnabled('city') ? new InputBase({ controlName: 'city', label: this.translateService.instant('labels.inputs.City'), value: address ? address.city : '', type: 'text', order: 7 }) : null ); formfields.push( this.isFieldEnabled('stateProvinceId') ? new SelectBase({ controlName: 'stateProvinceId', label: this.translateService.instant('labels.inputs.State / Province'), value: address ? address.stateProvinceId : '', options: { label: 'name', value: 'id', data: this.clientAddressTemplate.stateProvinceIdOptions }, order: 8 }) : null ); formfields.push( this.isFieldEnabled('countyDistrict') ? new InputBase({ controlName: 'countyDistrict', // Was mistakenly labeled "State / Province" — that label belongs to // the stateProvinceId select above. This control holds the District. label: this.translateService.instant('labels.inputs.County / District'), value: address ? address.countyDistrict : '', type: 'text', order: 11 }) : null ); formfields.push( this.isFieldEnabled('countryId') ? new SelectBase({ controlName: 'countryId', label: this.translateService.instant('labels.inputs.Country'), value: address ? address.countryId : '', options: { label: 'name', value: 'id', data: this.clientAddressTemplate.countryIdOptions }, order: 10 }) : null ); formfields = formfields.filter((field) => field !== null); return formfields; } }