- Savings account creation shows a required "Default Share Account" picker once Share Linking is enabled, populated from the client's active share accounts; submits shareAccountId alongside it. - Fixes a real data-loss bug on Share Account creation: the Charges step re-seeded its table from the product's default charges on every ngOnChanges firing (e.g. stepping back to Details and forward again), silently discarding a charge the clerk had already added, edited, or removed - a pristine guard existed for exactly this but wasn't checked in ngOnChanges. Now it is. - Removes the hard "required" on Share Account creation's Default Savings Account field, matching the backend now accepting a share account with no linked savings account. - New /accounting/migrate-cash-denomination screen: counts a branch's opening cash into notes and coins, checks it against (or defines, if unset) the Cash GL account's balance, and opens the branch's first business day with that same count - the vault-side counterpart to Migrate Opening Balances, kept under Accounting rather than Branch Ops/Cashier so it isn't reachable by mistake. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
189 lines
6.3 KiB
TypeScript
189 lines
6.3 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, OnInit, OnChanges, Input, inject } from '@angular/core';
|
|
import { MatDialog } from '@angular/material/dialog';
|
|
import { UntypedFormControl } from '@angular/forms';
|
|
|
|
/** Custom Dialogs */
|
|
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
|
|
import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component';
|
|
|
|
/** Custom Models */
|
|
import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base';
|
|
import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base';
|
|
import { TranslateService } from '@ngx-translate/core';
|
|
import { MatButton, MatIconButton } from '@angular/material/button';
|
|
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
|
import {
|
|
MatTable,
|
|
MatColumnDef,
|
|
MatHeaderCellDef,
|
|
MatHeaderCell,
|
|
MatCellDef,
|
|
MatCell,
|
|
MatHeaderRowDef,
|
|
MatHeaderRow,
|
|
MatRowDef,
|
|
MatRow
|
|
} from '@angular/material/table';
|
|
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
|
|
import { ChargesFilterPipe } from '../../../pipes/charges-filter.pipe';
|
|
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
|
|
|
/**
|
|
* Shares Account Charges Step
|
|
*/
|
|
@Component({
|
|
selector: 'mifosx-shares-account-charges-step',
|
|
templateUrl: './shares-account-charges-step.component.html',
|
|
styleUrls: ['./shares-account-charges-step.component.scss'],
|
|
imports: [
|
|
...STANDALONE_SHARED_IMPORTS,
|
|
FaIconComponent,
|
|
MatTable,
|
|
MatColumnDef,
|
|
MatHeaderCellDef,
|
|
MatHeaderCell,
|
|
MatCellDef,
|
|
MatCell,
|
|
MatIconButton,
|
|
MatHeaderRowDef,
|
|
MatHeaderRow,
|
|
MatRowDef,
|
|
MatRow,
|
|
MatStepperPrevious,
|
|
MatStepperNext,
|
|
ChargesFilterPipe
|
|
]
|
|
})
|
|
export class SharesAccountChargesStepComponent implements OnInit, OnChanges {
|
|
private dialog = inject(MatDialog);
|
|
private translateService = inject(TranslateService);
|
|
|
|
/** Shares Account Product Template */
|
|
@Input() sharesAccountProductTemplate: any;
|
|
/** Shares Account Template */
|
|
@Input() sharesAccountTemplate: any;
|
|
/** Currency Code */
|
|
@Input() currencyCode: UntypedFormControl;
|
|
/** When true (Share Account "Edit Details" maker-checker flow), charges are locked - they aren't
|
|
* part of that flow's editable field set. */
|
|
@Input() approvalEditMode = false;
|
|
|
|
/** Charge Data */
|
|
chargeData: any = [];
|
|
/** Charges Data Source */
|
|
chargesDataSource: {}[] = [];
|
|
/** Component is pristine if there has been no changes by user interaction */
|
|
pristine = true;
|
|
/** For Edit Shares Account Form */
|
|
isChargesPatched = false;
|
|
/** Display columns for charges table */
|
|
displayedColumns: string[] = [
|
|
'name',
|
|
'chargeCalculationType',
|
|
'amount',
|
|
'chargeTimeType',
|
|
'action'
|
|
];
|
|
|
|
ngOnInit() {
|
|
// Only relevant to the "edit an existing application" flow, where sharesAccountTemplate.charges
|
|
// carries the charges the account was actually submitted with, patched back in once the currency
|
|
// control settles. For a brand-new account, sharesAccountTemplate never carries a charges array at
|
|
// all -- this must NOT fall back to wiping chargesDataSource, or it stomps the product's own
|
|
// charges that ngOnChanges just seeded moments earlier from the same product-selection event.
|
|
this.currencyCode.valueChanges.subscribe(() => {
|
|
if (!this.isChargesPatched && this.sharesAccountTemplate.charges) {
|
|
this.chargesDataSource = this.sharesAccountTemplate.charges;
|
|
this.isChargesPatched = true;
|
|
}
|
|
});
|
|
}
|
|
|
|
ngOnChanges() {
|
|
if (this.sharesAccountProductTemplate) {
|
|
this.chargeData = this.sharesAccountTemplate.chargeOptions;
|
|
// Re-seeding from the product's own charges is only correct the first time this template
|
|
// arrives (picking the product). The productId control's valueChanges fires again on any
|
|
// later patchValue with the same id - stepping back to Details and forward again, for
|
|
// instance - and re-emits this same template; without this guard, that silently wipes
|
|
// whatever the user had already added, edited, or removed on this step.
|
|
if (this.pristine) {
|
|
this.chargesDataSource = this.sharesAccountProductTemplate.charges;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds the charge to charges table
|
|
* @param {any} charge Charge
|
|
*/
|
|
addCharge(charge: any) {
|
|
this.chargesDataSource = this.chargesDataSource.concat([charge.value]);
|
|
charge.value = '';
|
|
this.pristine = false;
|
|
}
|
|
|
|
/**
|
|
* Edits Charge Amount
|
|
* @param {any} charge Charge
|
|
*/
|
|
editCharge(charge: any) {
|
|
const formfields: FormfieldBase[] = [
|
|
new InputBase({
|
|
controlName: 'amount',
|
|
label: this.translateService.instant('labels.inputs.Amount'),
|
|
value: charge.amount || charge.amountOrPercentage,
|
|
type: 'number',
|
|
required: false
|
|
})
|
|
];
|
|
const data = {
|
|
title: this.translateService.instant('labels.heading.Edit Charge'),
|
|
layout: { addButtonText: 'Submit' },
|
|
formfields: formfields
|
|
};
|
|
const editNoteDialogRef = this.dialog.open(FormDialogComponent, { data });
|
|
editNoteDialogRef.afterClosed().subscribe((response: any) => {
|
|
if (response.data) {
|
|
const newCharge = { ...charge, amount: response.data.value.amount };
|
|
this.chargesDataSource.splice(this.chargesDataSource.indexOf(charge), 1, newCharge);
|
|
this.chargesDataSource = this.chargesDataSource.concat([]);
|
|
}
|
|
});
|
|
this.pristine = false;
|
|
}
|
|
|
|
/**
|
|
* Removes the charge from charges table
|
|
* @param {any} charge Charge
|
|
*/
|
|
deleteCharge(charge: any) {
|
|
const deleteChargeDialogRef = this.dialog.open(DeleteDialogComponent, {
|
|
data: { deleteContext: `charge ${charge.name}` }
|
|
});
|
|
deleteChargeDialogRef.afterClosed().subscribe((response: any) => {
|
|
if (response.delete) {
|
|
this.chargesDataSource.splice(this.chargesDataSource.indexOf(charge), 1);
|
|
this.chargesDataSource = this.chargesDataSource.concat([]);
|
|
}
|
|
});
|
|
this.pristine = false;
|
|
}
|
|
|
|
/**
|
|
* Returns shares account charges
|
|
*/
|
|
get sharesAccountCharges() {
|
|
return { charges: this.chargesDataSource };
|
|
}
|
|
}
|