/** * 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, OnInit, inject } from '@angular/core'; import { ActivatedRoute, Router, RouterLinkActive, RouterLink, RouterOutlet } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; /** Custom Dialogs */ import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; import { ShareCertificateDialogComponent } from './share-certificate-dialog/share-certificate-dialog.component'; import { ApplyCollectionDialogComponent } from './apply-collection-dialog/apply-collection-dialog.component'; import { DebitFromSavingsDialogComponent } from './debit-from-savings-dialog/debit-from-savings-dialog.component'; /** Custom Services */ import { SharesService } from '../shares.service'; /** Custom Buttons Configuration */ import { SharesButtonsConfiguration } from './shares-buttons.config'; import { MatCard, MatCardHeader, MatCardTitleGroup, MatCardMdImage, MatCardTitle, MatCardContent } from '@angular/material/card'; import { MatTooltip } from '@angular/material/tooltip'; import { NgClass } from '@angular/common'; import { LongTextComponent } from '../../shared/long-text/long-text.component'; import { AccountNumberComponent } from '../../shared/account-number/account-number.component'; import { MatIconButton } from '@angular/material/button'; import { MatMenuTrigger, MatMenu, MatMenuItem } from '@angular/material/menu'; import { MatIcon } from '@angular/material/icon'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs'; import { StatusLookupPipe } from '../../pipes/status-lookup.pipe'; import { FormatNumberPipe } from '../../pipes/format-number.pipe'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; /** * Shares Account View */ @Component({ selector: 'mifosx-shares-account-view', templateUrl: './shares-account-view.component.html', styleUrls: ['./shares-account-view.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, MatCardHeader, MatCardTitleGroup, MatCardMdImage, MatTooltip, MatCardTitle, NgClass, LongTextComponent, AccountNumberComponent, MatIconButton, MatMenuTrigger, MatIcon, FaIconComponent, MatMenu, MatMenuItem, MatTabNav, MatTabLink, RouterLinkActive, MatTabNavPanel, RouterOutlet, StatusLookupPipe, FormatNumberPipe ] }) export class SharesAccountViewComponent implements OnInit { private route = inject(ActivatedRoute); private router = inject(Router); private sharesService = inject(SharesService); dialog = inject(MatDialog); /** Shares Account Data */ sharesAccountData: any; /** Button Configurations */ buttonConfig: SharesButtonsConfiguration; entityType: string; /** * Fetches shares account data from `resolve` * @param {ActivatedRoute} route Activated Route * @param {Router} router Router * @param {SharesService} sharesService Shares Service * @param {MatDialog} dialog Mat Dialog */ constructor() { this.route.data.subscribe((data: { sharesAccountData: any }) => { this.sharesAccountData = data.sharesAccountData; }); } /** Whether this account has a details edit awaiting checker approval. */ hasPendingEdit = false; ngOnInit() { this.setConditionalButtons(); if (this.router.url.includes('clients')) { this.entityType = 'Client'; } else if (this.router.url.includes('groups')) { this.entityType = 'Group'; } else if (this.router.url.includes('centers')) { this.entityType = 'Center'; } this.sharesService.getPendingShareAccountEdit(this.sharesAccountData.id).subscribe({ next: (edit: any) => { this.hasPendingEdit = !!edit && edit.status === 'PENDING'; if (this.hasPendingEdit) { this.buttonConfig.removeOption('Edit Details'); } }, error: () => { this.hasPendingEdit = false; } }); } /** * Adds options to button config. conditionaly. */ setConditionalButtons() { const status = this.sharesAccountData.status.value; this.buttonConfig = new SharesButtonsConfiguration(status); if (this.sharesAccountData.charges) { const charges: any[] = this.sharesAccountData.charges; charges.forEach((charge: any) => { if (charge.name === 'Annual fee - INR') { this.buttonConfig.addOption({ name: 'Apply Anuual Fees', taskPermissionName: 'APPLYANNUALFEE_SAVINGSACCOUNT' }); } }); } if (status === 'Active') { const purchasedShares: any[] = this.sharesAccountData.purchasedShares; let sharesPendingForApproval = false; purchasedShares.forEach((share: any) => { if ( share.status.code === 'purchasedSharesStatusType.applied' && share.type.code === 'purchasedSharesType.purchased' ) { sharesPendingForApproval = true; } }); if (!sharesPendingForApproval) { this.buttonConfig.removeButton('Approve Additional Shares'); this.buttonConfig.removeButton('Reject Additional Shares'); } // An Active account can carry "approved" shares nobody has actually paid for yet -- account // creation, approval and activation post no GL of their own; only applying a Share Advance // Collection does (see isSettled). A certificate for shares still unsettled would certify // capital that was never issued. if (!this.isSettled) { this.buttonConfig.removeOption('Share Certificate'); } } } /** * Performs button action * @param {string} name Action name */ doAction(name: string) { switch (name) { case 'Approve': case 'Reject': case 'Close': case 'Activate': case 'Undo Approval': case 'Apply Additional Shares': case 'Redeem Shares': case 'Approve Additional Shares': case 'Reject Additional Shares': this.router.navigate([`actions/${name}`], { relativeTo: this.route }); break; case 'Modify Application': this.router.navigate(['edit'], { relativeTo: this.route }); break; case 'Edit Details': this.router.navigate(['edit-details'], { relativeTo: this.route }); break; case 'Apply Collection': this.applyCollection(); break; case 'Debit from Savings': this.debitFromSavings(); break; case 'Share Certificate': this.viewShareCertificate(); break; case 'Delete': this.deleteSharesAccount(); break; } } /** * Settles the member's Share Advance receipt against this account: the cash moves out of Share * Sundry into share capital, and the purchase entry becomes visible in the transactions and * passbook views. */ private applyCollection() { const dialogRef = this.dialog.open(ApplyCollectionDialogComponent, { width: '560px', data: { shareAccountId: this.sharesAccountData.id, clientId: this.sharesAccountData.clientId, accountNo: this.sharesAccountData.accountNo } }); dialogRef.afterClosed().subscribe((result: any) => { if (result) { // Account data comes from a route resolver, so re-running the route is what refreshes the // newly revealed transaction and the now-paid charges. this.router.navigateByUrl('/', { skipLocationChange: true }).then(() => { this.router.navigate([ '/clients', this.sharesAccountData.clientId, 'shares-accounts', this.sharesAccountData.id ]); }); } }); } /** * Whether the member's money has actually been taken and the shares issued. * * Until then the account is only an application: nothing has been debited and the member holds * nothing. The Transactions, Passbook and Charges tabs all describe money that has moved, so * showing them at that point reads as though the member has paid and holds shares when they have * done neither. * * Neither the account's status nor the purchase transaction's status can stand in for this: an * account goes Approved then Activated before anything is settled, and the purchase reads * Approved from the moment the account is approved. Only `collectionApplied` marks the point at * which the money was actually taken — by cash collection or by debiting savings. */ get isSettled(): boolean { return !!this.sharesAccountData?.collectionApplied; } /** * Settles this account out of the member's linked savings instead of cash at the counter. * * Only records a request — a manager's approval is what debits the member and issues the shares — * so unlike Apply Collection there is nothing to refresh here until that approval happens. */ private debitFromSavings() { this.dialog.open(DebitFromSavingsDialogComponent, { width: '560px', data: { shareAccountId: this.sharesAccountData.id, clientId: this.sharesAccountData.clientId, accountNo: this.sharesAccountData.accountNo } }); } /** * Deletes Shares Account. */ private deleteSharesAccount() { const deleteSharesAccountDialogRef = this.dialog.open(DeleteDialogComponent, { data: { deleteContext: `shares account with id: ${this.sharesAccountData.id}` } }); deleteSharesAccountDialogRef.afterClosed().subscribe((response: any) => { if (response.delete) { this.sharesService.deleteSharesAccount(this.sharesAccountData.id).subscribe(() => { this.router.navigate(['../../'], { relativeTo: this.route }); }); } }); } private viewShareCertificate() { this.dialog.open(ShareCertificateDialogComponent, { data: { sharesAccountData: this.sharesAccountData, clientId: this.sharesAccountData.clientId }, width: '850px', maxWidth: '95vw' }); } }