Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
408 lines
14 KiB
TypeScript
408 lines
14 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 { 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<string, 'VERIFIED' | 'NOT_VERIFIED'> = {};
|
|
|
|
/**
|
|
* @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<string, 'VERIFIED' | 'NOT_VERIFIED'> = {};
|
|
// 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;
|
|
}
|
|
}
|