feat: add Current Account module, DIP verification, charges integration, and translations

- Current Account module with full CRUD, transactions, charges, hold/release, block/unblock
- Current Products management under Products module
- DIP mobile number verification with real API call to /lookup/check
- Sanctions verification dialog
- Charges support for Current Account (chargeAppliesTo=5) in create/edit
- Fixed permissions from SAVINGSACCOUNT to CURRENTACCOUNT across all current account components
- Added all missing translation keys for Current Account in en-US.json

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-03-17 18:23:37 +05:30
co-authored by Claude Opus 4.6
parent 73aa69ee02
commit 0a32d7135e
210 changed files with 15048 additions and 155 deletions
@@ -170,6 +170,14 @@ export class CreateChargeComponent implements OnInit {
this.chargeCalculationTypeData = this.chargesTemplateData.shareChargeCalculationTypeOptions;
this.chargeTimeTypeData = this.chargesTemplateData.shareChargeTimeTypeOptions;
break;
case 5: // Current Account — uses same options as Savings
this.chargeCalculationTypeData =
this.chargesTemplateData.currentAccountChargeCalculationTypeOptions ||
this.chargesTemplateData.savingsChargeCalculationTypeOptions;
this.chargeTimeTypeData =
this.chargesTemplateData.currentAccountChargeTimeTypeOptions ||
this.chargesTemplateData.savingsChargeTimeTypeOptions;
break;
}
});
}
@@ -188,7 +196,7 @@ export class CreateChargeComponent implements OnInit {
if (this.chargeForm.get('chargeTimeType').value !== 12 && chargeCalculationType.id === 5) {
return false;
}
if (this.chargeForm.get('chargeAppliesTo').value === 2) {
if (this.chargeForm.get('chargeAppliesTo').value === 2 || this.chargeForm.get('chargeAppliesTo').value === 5) {
if (
!(
this.chargeForm.get('chargeTimeType').value === 5 ||
@@ -216,7 +224,7 @@ export class CreateChargeComponent implements OnInit {
chargeCalculationType === 4 ||
chargeCalculationType === 5
);
} else if (chargeAppliesTo === 2) {
} else if (chargeAppliesTo === 2 || chargeAppliesTo === 5) {
return (chargeTimeType === 16 || chargeTimeType === 5) && chargeCalculationType === 2;
} else if (chargeAppliesTo === 4) {
return (chargeTimeType === 14 || chargeTimeType === 15) && chargeCalculationType === 2;
@@ -248,6 +256,10 @@ export class CreateChargeComponent implements OnInit {
this.chargeForm.removeControl('incomeAccountId');
this.chargeForm.get('penalty').setValue(false);
break;
case 5: // Current Account — same as Savings
this.chargeForm.removeControl('chargePaymentMode');
this.chargeForm.removeControl('incomeAccountId');
break;
}
this.chargeForm.get('chargeCalculationType').reset();
this.chargeForm.get('chargeTimeType').reset();
@@ -154,6 +154,15 @@ export class EditChargeComponent implements OnInit {
this.addFeeFrequency = false;
break;
}
case 'Current Account': {
this.chargeTimeTypeOptions =
this.chargeData.currentAccountChargeTimeTypeOptions || this.chargeData.savingsChargeTimeTypeOptions;
this.chargeCalculationTypeOptions =
this.chargeData.currentAccountChargeCalculationTypeOptions ||
this.chargeData.savingsChargeCalculationTypeOptions;
this.addFeeFrequency = false;
break;
}
case 'Shares': {
this.chargeTimeTypeOptions = this.chargeData.shareChargeTimeTypeOptions;
this.chargeCalculationTypeOptions = this.chargeData.shareChargeCalculationTypeOptions;
@@ -36,6 +36,7 @@ export enum ChargeAppliesToCode {
ChargeAppliesToLoan = 'chargeAppliesTo.loan',
ChargeAppliesToSavings = 'chargeAppliesTo.savings',
ChargeAppliesToShares = 'chargeAppliesTo.shares',
ChargeAppliesToCurrentAccount = 'chargeAppliesTo.currentAccount',
ChargeCalculationTypeFlat = 'chargeCalculationType.flat',
ChargeCalculationTypePercentOfAmount = 'chargeCalculationType.percent.of.amount',
ChargeCalculationTypePercentOfAmountAndInterest = 'chargeCalculationType.percent.of.amount.and.interest',
@@ -0,0 +1,67 @@
<div class="container">
<mat-stepper orientation="vertical" [linear]="false" class="mat-elevation-z8" labelPosition="bottom">
<ng-template matStepperIcon="number"><fa-icon icon="pencil-alt" size="sm"></fa-icon></ng-template>
<ng-template matStepperIcon="edit"><fa-icon icon="pencil-alt" size="sm"></fa-icon></ng-template>
<ng-template matStepperIcon="done"><fa-icon icon="check" size="sm"></fa-icon></ng-template>
<ng-template matStepperIcon="error"><fa-icon icon="exclamation-triangle" size="lg"></fa-icon></ng-template>
<ng-template matStepperIcon="preview"><fa-icon icon="eye" size="sm"></fa-icon></ng-template>
<mat-step [stepControl]="detailsForm">
<ng-template matStepLabel>{{ 'labels.inputs.DETAILS' | translate }}</ng-template>
<mifosx-saving-product-details-step></mifosx-saving-product-details-step>
</mat-step>
<mat-step [stepControl]="currencyForm">
<ng-template matStepLabel>{{ 'labels.inputs.CURRENCY' | translate }}</ng-template>
<mifosx-saving-product-currency-step
[savingProductsTemplate]="currentProductsTemplate"
></mifosx-saving-product-currency-step>
</mat-step>
<mat-step [stepControl]="termsForm">
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template>
<mifosx-saving-product-terms-step
[savingProductsTemplate]="currentProductsTemplate"
></mifosx-saving-product-terms-step>
</mat-step>
<mat-step [stepControl]="settingsForm">
<ng-template matStepLabel>{{ 'labels.inputs.SETTINGS' | translate }}</ng-template>
<mifosx-saving-product-settings-step
[savingProductsTemplate]="currentProductsTemplate"
></mifosx-saving-product-settings-step>
</mat-step>
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template>
<mifosx-saving-product-charges-step
[savingProductsTemplate]="currentProductsTemplate"
[currencyCode]="currencyForm.get('currencyCode')"
></mifosx-saving-product-charges-step>
</mat-step>
<mat-step [stepControl]="accountingForm">
<ng-template matStepLabel>{{ 'labels.inputs.ACCOUNTING' | translate }}</ng-template>
<mifosx-saving-product-accounting-step
[savingProductsTemplate]="currentProductsTemplate"
[accountingRuleData]="accountingRuleData"
[isDormancyTrackingActive]="settingsForm.get('isDormancyTrackingActive')"
[savingProductFormValid]="formValid"
[allowOverdraft]="settingsForm.get('allowOverdraft')"
></mifosx-saving-product-accounting-step>
</mat-step>
@if (formValid) {
<mat-step state="preview" completed>
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template>
<mifosx-saving-product-preview-step
[savingProductsTemplate]="currentProductsTemplate"
[accountingRuleData]="accountingRuleData"
[savingProduct]="currentProduct"
[taskPermission]="'CREATE_CURRENTPRODUCT'"
(submitEvent)="submit()"
></mifosx-saving-product-preview-step>
</mat-step>
}
</mat-stepper>
</div>
@@ -0,0 +1,4 @@
fa-icon {
position: relative;
left: 5%;
}
@@ -0,0 +1,116 @@
import { Component, ViewChild, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { SavingProductDetailsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component';
import { SavingProductCurrencyStepComponent } from '../../saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component';
import { SavingProductTermsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component';
import { SavingProductSettingsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component';
import { SavingProductChargesStepComponent } from '../../saving-products/saving-product-stepper/saving-product-charges-step/saving-product-charges-step.component';
import { SavingProductAccountingStepComponent } from '../../saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component';
import { SavingProductPreviewStepComponent } from '../../saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component';
import { ProductsService } from 'app/products/products.service';
import { SettingsService } from 'app/settings/settings.service';
import { Accounting } from 'app/core/utils/accounting';
import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-create-current-product',
templateUrl: './create-current-product.component.html',
styleUrls: ['./create-current-product.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatStepper,
MatStepperIcon,
FaIconComponent,
MatStep,
MatStepLabel,
SavingProductDetailsStepComponent,
SavingProductCurrencyStepComponent,
SavingProductTermsStepComponent,
SavingProductSettingsStepComponent,
SavingProductChargesStepComponent,
SavingProductAccountingStepComponent,
SavingProductPreviewStepComponent
]
})
export class CreateCurrentProductComponent {
private route = inject(ActivatedRoute);
private productsService = inject(ProductsService);
private router = inject(Router);
private settingsService = inject(SettingsService);
private accounting = inject(Accounting);
@ViewChild(SavingProductDetailsStepComponent, { static: true }) detailsStep: SavingProductDetailsStepComponent;
@ViewChild(SavingProductCurrencyStepComponent, { static: true }) currencyStep: SavingProductCurrencyStepComponent;
@ViewChild(SavingProductTermsStepComponent, { static: true }) termsStep: SavingProductTermsStepComponent;
@ViewChild(SavingProductSettingsStepComponent, { static: true }) settingsStep: SavingProductSettingsStepComponent;
@ViewChild(SavingProductChargesStepComponent, { static: true }) chargesStep: SavingProductChargesStepComponent;
@ViewChild(SavingProductAccountingStepComponent, { static: true })
accountingStep: SavingProductAccountingStepComponent;
currentProductsTemplate: any;
accountingRuleData: string[] = [];
constructor() {
this.route.data.subscribe((data: { currentProductsTemplate: any }) => {
this.currentProductsTemplate = data.currentProductsTemplate;
});
this.accountingRuleData = this.accounting.getAccountingRulesForSavings();
}
get detailsForm() {
return this.detailsStep.savingProductDetailsForm;
}
get currencyForm() {
return this.currencyStep.savingProductCurrencyForm;
}
get termsForm() {
return this.termsStep.savingProductTermsForm;
}
get settingsForm() {
return this.settingsStep.savingProductSettingsForm;
}
get accountingForm() {
return this.accountingStep.savingProductAccountingForm;
}
get formValid() {
return (
this.detailsForm.valid &&
this.currencyForm.valid &&
this.termsForm.valid &&
this.settingsForm.valid &&
this.accountingForm.valid
);
}
get currentProduct() {
return {
...this.detailsStep.savingProductDetails,
...this.currencyStep.savingProductCurrency,
...this.termsStep.savingProductTerms,
...this.settingsStep.savingProductSettings,
...this.chargesStep.savingProductCharges,
...this.accountingStep.savingProductAccounting
};
}
submit() {
const product = {
...this.currentProduct,
charges: this.currentProduct.charges.map((charge: any) => ({ id: charge.id })),
locale: this.settingsService.language.code
};
delete product.advancedAccountingRules;
this.productsService.createCurrentProduct(product).subscribe((response: any) => {
this.router.navigate(
[
'../',
response.resourceId
],
{ relativeTo: this.route }
);
});
}
}
@@ -0,0 +1,14 @@
import { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { ProductsService } from '../products.service';
@Injectable()
export class CurrentProductResolver {
private productsService = inject(ProductsService);
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const productId = route.parent.paramMap.get('productId');
return this.productsService.getCurrentProduct(productId);
}
}
@@ -0,0 +1,12 @@
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { ProductsService } from '../products.service';
@Injectable()
export class CurrentProductsTemplateResolver {
private productsService = inject(ProductsService);
resolve(): Observable<any> {
return this.productsService.getCurrentProductsTemplate();
}
}
@@ -0,0 +1,36 @@
<div class="container m-b-20 layout-row align-end gap-20px">
<div class="in-block">
<button mat-raised-button color="primary" [routerLink]="['create']" *mifosxHasPermission="'CREATE_CURRENTPRODUCT'">
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.text.Create Current Product' | translate }}
</button>
</div>
</div>
<div class="container">
<div class="layout-row">
<mat-form-field class="flex-fill">
<mat-label>{{ 'labels.inputs.Filter' | translate }}</mat-label>
<input matInput (keyup)="applyFilter($event.target.value)" />
</mat-form-field>
</div>
<div class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" matSort>
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let product">{{ product.name }}</td>
</ng-container>
<ng-container matColumnDef="shortName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Short Name' | translate }}</th>
<td mat-cell *matCellDef="let product">{{ product.shortName }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" [routerLink]="[row.id]" class="select-row"></tr>
</table>
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons></mat-paginator>
</div>
</div>
@@ -0,0 +1,7 @@
table {
width: 100%;
.select-row:hover {
cursor: pointer;
}
}
@@ -0,0 +1,71 @@
import { Component, OnInit, ViewChild, inject } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort, MatSortHeader } from '@angular/material/sort';
import {
MatTableDataSource,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-current-products',
templateUrl: './current-products.component.html',
styleUrls: ['./current-products.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
MatTable,
MatSort,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatSortHeader,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
MatPaginator
]
})
export class CurrentProductsComponent implements OnInit {
private route = inject(ActivatedRoute);
currentProductsData: any;
displayedColumns: string[] = [
'name',
'shortName'
];
dataSource: MatTableDataSource<any>;
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
@ViewChild(MatSort, { static: true }) sort: MatSort;
constructor() {
this.route.data.subscribe((data: { currentProducts: any }) => {
this.currentProductsData = data.currentProducts;
});
}
ngOnInit() {
this.dataSource = new MatTableDataSource(this.currentProductsData);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
}
applyFilter(filterValue: string) {
this.dataSource.filter = filterValue.trim().toLowerCase();
}
}
@@ -0,0 +1,12 @@
import { Injectable, inject } from '@angular/core';
import { Observable } from 'rxjs';
import { ProductsService } from '../products.service';
@Injectable()
export class CurrentProductsResolver {
private productsService = inject(ProductsService);
resolve(): Observable<any> {
return this.productsService.getCurrentProducts();
}
}
@@ -0,0 +1,14 @@
import { Injectable, inject } from '@angular/core';
import { ActivatedRouteSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { ProductsService } from '../../products.service';
@Injectable()
export class CurrentProductAndTemplateResolver {
private productsService = inject(ProductsService);
resolve(route: ActivatedRouteSnapshot): Observable<any> {
const productId = route.paramMap.get('productId');
return this.productsService.getCurrentProduct(productId, true);
}
}
@@ -0,0 +1,69 @@
<div class="container">
<mat-stepper orientation="vertical" [linear]="false" class="mat-elevation-z8" labelPosition="bottom">
<ng-template matStepperIcon="number"><fa-icon icon="pencil-alt" size="sm"></fa-icon></ng-template>
<ng-template matStepperIcon="edit"><fa-icon icon="pencil-alt" size="sm"></fa-icon></ng-template>
<ng-template matStepperIcon="done"><fa-icon icon="check" size="sm"></fa-icon></ng-template>
<ng-template matStepperIcon="error"><fa-icon icon="exclamation-triangle" size="lg"></fa-icon></ng-template>
<ng-template matStepperIcon="preview"><fa-icon icon="eye" size="sm"></fa-icon></ng-template>
<mat-step [stepControl]="detailsForm" completed>
<ng-template matStepLabel>{{ 'labels.inputs.DETAILS' | translate }}</ng-template>
<mifosx-saving-product-details-step
[savingProductsTemplate]="currentProductAndTemplate"
></mifosx-saving-product-details-step>
</mat-step>
<mat-step [stepControl]="currencyForm" completed>
<ng-template matStepLabel>{{ 'labels.inputs.CURRENCY' | translate }}</ng-template>
<mifosx-saving-product-currency-step
[savingProductsTemplate]="currentProductAndTemplate"
></mifosx-saving-product-currency-step>
</mat-step>
<mat-step [stepControl]="termsForm" completed>
<ng-template matStepLabel>{{ 'labels.inputs.TERMS' | translate }}</ng-template>
<mifosx-saving-product-terms-step
[savingProductsTemplate]="currentProductAndTemplate"
></mifosx-saving-product-terms-step>
</mat-step>
<mat-step [stepControl]="settingsForm" completed>
<ng-template matStepLabel>{{ 'labels.inputs.SETTINGS' | translate }}</ng-template>
<mifosx-saving-product-settings-step
[savingProductsTemplate]="currentProductAndTemplate"
></mifosx-saving-product-settings-step>
</mat-step>
<mat-step completed>
<ng-template matStepLabel>{{ 'labels.inputs.CHARGES' | translate }}</ng-template>
<mifosx-saving-product-charges-step
[savingProductsTemplate]="currentProductAndTemplate"
[currencyCode]="currencyForm.get('currencyCode')"
></mifosx-saving-product-charges-step>
</mat-step>
<mat-step [stepControl]="accountingForm" completed>
<ng-template matStepLabel>{{ 'labels.inputs.ACCOUNTING' | translate }}</ng-template>
<mifosx-saving-product-accounting-step
[savingProductsTemplate]="currentProductAndTemplate"
[accountingRuleData]="accountingRuleData"
[isDormancyTrackingActive]="settingsForm.get('isDormancyTrackingActive')"
[savingProductFormValid]="formValidAndNotPristine"
[allowOverdraft]="settingsForm.get('allowOverdraft')"
></mifosx-saving-product-accounting-step>
</mat-step>
@if (formValidAndNotPristine) {
<mat-step state="preview" completed>
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template>
<mifosx-saving-product-preview-step
[savingProductsTemplate]="currentProductAndTemplate"
[accountingRuleData]="accountingRuleData"
[savingProduct]="currentProduct"
[taskPermission]="'UPDATE_CURRENTPRODUCT'"
(submitEvent)="submit()"
></mifosx-saving-product-preview-step>
</mat-step>
}
</mat-stepper>
</div>
@@ -0,0 +1,4 @@
fa-icon {
position: relative;
left: 5%;
}
@@ -0,0 +1,122 @@
import { Component, ViewChild, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { SavingProductDetailsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component';
import { SavingProductCurrencyStepComponent } from '../../saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component';
import { SavingProductTermsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component';
import { SavingProductSettingsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component';
import { SavingProductChargesStepComponent } from '../../saving-products/saving-product-stepper/saving-product-charges-step/saving-product-charges-step.component';
import { SavingProductAccountingStepComponent } from '../../saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component';
import { SavingProductPreviewStepComponent } from '../../saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component';
import { ProductsService } from 'app/products/products.service';
import { SettingsService } from 'app/settings/settings.service';
import { Accounting } from 'app/core/utils/accounting';
import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-edit-current-product',
templateUrl: './edit-current-product.component.html',
styleUrls: ['./edit-current-product.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatStepper,
MatStepperIcon,
FaIconComponent,
MatStep,
MatStepLabel,
SavingProductDetailsStepComponent,
SavingProductCurrencyStepComponent,
SavingProductTermsStepComponent,
SavingProductSettingsStepComponent,
SavingProductChargesStepComponent,
SavingProductAccountingStepComponent,
SavingProductPreviewStepComponent
]
})
export class EditCurrentProductComponent {
private route = inject(ActivatedRoute);
private productsService = inject(ProductsService);
private router = inject(Router);
private settingsService = inject(SettingsService);
private accounting = inject(Accounting);
@ViewChild(SavingProductDetailsStepComponent, { static: true }) detailsStep: SavingProductDetailsStepComponent;
@ViewChild(SavingProductCurrencyStepComponent, { static: true }) currencyStep: SavingProductCurrencyStepComponent;
@ViewChild(SavingProductTermsStepComponent, { static: true }) termsStep: SavingProductTermsStepComponent;
@ViewChild(SavingProductSettingsStepComponent, { static: true }) settingsStep: SavingProductSettingsStepComponent;
@ViewChild(SavingProductChargesStepComponent, { static: true }) chargesStep: SavingProductChargesStepComponent;
@ViewChild(SavingProductAccountingStepComponent, { static: true })
accountingStep: SavingProductAccountingStepComponent;
currentProductAndTemplate: any;
accountingRuleData: string[] = [];
constructor() {
this.route.data.subscribe((data: { currentProductAndTemplate: any }) => {
this.currentProductAndTemplate = data.currentProductAndTemplate;
});
this.accountingRuleData = this.accounting.getAccountingRulesForSavings();
}
get detailsForm() {
return this.detailsStep.savingProductDetailsForm;
}
get currencyForm() {
return this.currencyStep.savingProductCurrencyForm;
}
get termsForm() {
return this.termsStep.savingProductTermsForm;
}
get settingsForm() {
return this.settingsStep.savingProductSettingsForm;
}
get accountingForm() {
return this.accountingStep.savingProductAccountingForm;
}
get formValidAndNotPristine() {
return (
this.detailsForm.valid &&
this.currencyForm.valid &&
this.termsForm.valid &&
this.settingsForm.valid &&
this.accountingForm.valid &&
(!this.detailsForm.pristine ||
!this.currencyForm.pristine ||
!this.termsForm.pristine ||
!this.settingsForm.pristine ||
!this.chargesStep.pristine ||
!this.accountingForm.pristine)
);
}
get currentProduct() {
return {
...this.detailsStep.savingProductDetails,
...this.currencyStep.savingProductCurrency,
...this.termsStep.savingProductTerms,
...this.settingsStep.savingProductSettings,
...this.chargesStep.savingProductCharges,
...this.accountingStep.savingProductAccounting
};
}
submit() {
const product = {
...this.currentProduct,
charges: this.currentProduct.charges.map((charge: any) => ({ id: charge.id })),
locale: this.settingsService.language.code
};
delete product.advancedAccountingRules;
this.productsService.updateCurrentProduct(this.currentProductAndTemplate.id, product).subscribe((response: any) => {
this.router.navigate(
[
'../../',
response.resourceId
],
{ relativeTo: this.route }
);
});
}
}
@@ -0,0 +1,102 @@
<div class="layout-row align-end gap-2px responsive-column container m-b-20">
<button
mat-raised-button
color="primary"
class="m-t-20"
[routerLink]="['../edit']"
*mifosxHasPermission="'UPDATE_CURRENTPRODUCT'"
>
<fa-icon icon="edit" class="m-r-10"></fa-icon>{{ 'labels.buttons.Edit' | translate }}
</button>
</div>
<div class="tab-container">
<div class="responsive-column">
<h2 class="mat-h2 flex-100">{{ currentProduct.name }}</h2>
<mat-divider [inset]="true"></mat-divider>
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Details' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Short Name' | translate }}:</span>
<span class="flex-60">{{ currentProduct.shortName }}</span>
</div>
@if (currentProduct.description) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Description' | translate }}:</span>
<span class="flex-60">{{ currentProduct.description }}</span>
</div>
}
<h3 class="mat-h3 flex-fill">{{ 'labels.inputs.Currency' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Currency' | translate }}:</span>
<span class="flex-60">{{ currentProduct.currency.name }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Decimal Places' | translate }}:</span>
<span class="flex-60">{{ currentProduct.currency.decimalPlaces }}</span>
</div>
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Terms' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Nominal Annual Interest' | translate }}:</span>
<span class="flex-60">{{ currentProduct.nominalAnnualInterestRate | formatNumber }} %</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Compounding Period' | translate }}:</span>
<span class="flex-60">{{ currentProduct.interestCompoundingPeriodType?.value | translateKey: 'catalogs' }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Posting Period' | translate }}:</span>
<span class="flex-60">{{ currentProduct.interestPostingPeriodType?.value | translateKey: 'catalogs' }}</span>
</div>
@if (currentProduct.charges?.length) {
<div class="layout-row-wrap responsive-column flex-fill">
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Charges' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<table class="flex-fill mat-elevation-z1" mat-table [dataSource]="currentProduct.charges">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.name + ', ' + charge.currency.displaySymbol }}</td>
</ng-container>
<ng-container matColumnDef="chargeCalculationType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeCalculationType.value | translateKey: 'catalogs' }}
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.amount | formatNumber }}</td>
</ng-container>
<ng-container matColumnDef="chargeTimeType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Collected On' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.chargeTimeType.value | translateKey: 'catalogs' }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chargesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chargesDisplayedColumns"></tr>
</table>
</div>
}
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Accounting' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<mifosx-view-savings-accounting-details
class="flex-100"
[accountingRule]="currentProduct.accountingRule"
[accountingMappings]="currentProduct.accountingMappings"
></mifosx-view-savings-accounting-details>
</div>
</div>
@@ -0,0 +1,36 @@
@use 'assets/styles/helper';
@use 'assets/styles/colours' as *;
.tab-container {
padding: 1%;
margin: 1%;
}
.mat-h3 {
margin-top: 10px !important;
font-weight: 300;
color: $status-approved;
}
h2,
h3,
h4 {
margin: 0;
font-weight: 500;
}
table {
width: 100%;
}
.mat-elevation-z1 {
margin: 1em 0 1.5em;
}
span {
margin: 0.5em 0;
}
mat-divider {
margin: 0 0 0.5em;
}
@@ -0,0 +1,68 @@
import { Component, inject } from '@angular/core';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { Accounting } from 'app/core/utils/accounting';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatDivider } from '@angular/material/divider';
import {
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { ViewSavingsAccountingDetailsComponent } from '../../../../shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component';
import { FormatNumberPipe } from '../../../../pipes/format-number.pipe';
import { YesnoPipe } from '../../../../pipes/yesno.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-current-product-general-tab',
templateUrl: './current-product-general-tab.component.html',
styleUrls: ['./current-product-general-tab.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
MatDivider,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
ViewSavingsAccountingDetailsComponent,
FormatNumberPipe,
YesnoPipe
]
})
export class CurrentProductGeneralTabComponent {
private route = inject(ActivatedRoute);
private accounting = inject(Accounting);
currentProduct: any;
chargesDisplayedColumns: string[] = [
'name',
'chargeCalculationType',
'amount',
'chargeTimeType'
];
constructor() {
this.route.data.subscribe((data: { currentProduct: any }) => {
this.currentProduct = data.currentProduct;
});
}
isCashOrAccrualAccounting(): boolean {
return this.accounting.isCashOrAccrualAccounting(this.currentProduct.accountingRule);
}
}
@@ -0,0 +1,18 @@
<mat-card class="product-card">
<mat-card-content>
<nav mat-tab-nav-bar class="navigation-tabs" [tabPanel]="tabPanel">
<a
mat-tab-link
[routerLink]="['./general']"
routerLinkActive
#general="routerLinkActive"
[active]="general.isActive"
>
{{ 'labels.heading.General' | translate }}
</a>
</nav>
<mat-tab-nav-panel #tabPanel>
<router-outlet></router-outlet>
</mat-tab-nav-panel>
</mat-card-content>
</mat-card>
@@ -0,0 +1,14 @@
.product-card {
margin: 0 auto;
max-width: 80rem;
width: 90%;
padding: 0;
.navigation-tabs {
overflow: auto;
}
i:hover {
cursor: pointer;
}
}
@@ -0,0 +1,23 @@
import { Component, inject } from '@angular/core';
import { ActivatedRoute, RouterLinkActive, RouterLink, RouterOutlet } from '@angular/router';
import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-view-current-product',
templateUrl: './view-current-product.component.html',
styleUrls: ['./view-current-product.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatTabNav,
MatTabLink,
RouterLinkActive,
MatTabNavPanel,
RouterOutlet
]
})
export class ViewCurrentProductComponent {
private route = inject(ActivatedRoute);
constructor() {}
}
+70 -1
View File
@@ -135,6 +135,17 @@ import { ShareProductDatatablesResolver } from './share-products/share-product-d
import { ShareProductDatatableResolver } from './share-products/share-product-datatable.resolver';
import { GlobalConfigurationsResolver } from 'app/system/configurations/global-configurations-tab/global-configurations.resolver';
/** Current Products */
import { CurrentProductsComponent } from './current-products/current-products.component';
import { CreateCurrentProductComponent } from './current-products/create-current-product/create-current-product.component';
import { ViewCurrentProductComponent } from './current-products/view-current-product/view-current-product.component';
import { EditCurrentProductComponent } from './current-products/edit-current-product/edit-current-product.component';
import { CurrentProductGeneralTabComponent } from './current-products/view-current-product/current-product-general-tab/current-product-general-tab.component';
import { CurrentProductsResolver } from './current-products/current-products.resolver';
import { CurrentProductsTemplateResolver } from './current-products/current-products-template.resolver';
import { CurrentProductResolver } from './current-products/current-product.resolver';
import { CurrentProductAndTemplateResolver } from './current-products/edit-current-product/current-product-and-template.resolver';
/** Products Routes */
const routes: Routes = [
Route.withShell([
@@ -283,6 +294,60 @@ const routes: Routes = [
}
]
},
{
path: 'current-products',
data: { title: 'Current Products', breadcrumb: 'Current Products' },
children: [
{
path: '',
component: CurrentProductsComponent,
resolve: {
currentProducts: CurrentProductsResolver
}
},
{
path: 'create',
component: CreateCurrentProductComponent,
data: { title: 'Create Current Product', breadcrumb: 'Create' },
resolve: {
currentProductsTemplate: CurrentProductsTemplateResolver
}
},
{
path: ':productId',
data: { title: 'View Current Product', breadcrumb: 'productId', routeParamBreadcrumb: 'productId' },
children: [
{
path: '',
component: ViewCurrentProductComponent,
children: [
{
path: '',
redirectTo: 'general',
pathMatch: 'full'
},
{
path: 'general',
data: { title: 'General', breadcrumb: 'General', routeParamBreadcrumb: false },
component: CurrentProductGeneralTabComponent,
resolve: {
currentProduct: CurrentProductResolver
}
}
]
},
{
path: 'edit',
component: EditCurrentProductComponent,
data: { title: 'Edit Current Product', breadcrumb: 'Edit', routeParamBreadcrumb: false },
resolve: {
currentProductAndTemplate: CurrentProductAndTemplateResolver
}
}
]
}
]
},
{
path: 'share-products',
data: { title: 'Share Products', breadcrumb: 'Share Products' },
@@ -951,7 +1016,11 @@ const routes: Routes = [
CollateralsResolver,
CollateralTemplateResolver,
DelinquencyRangeComponentsResolver,
DelinquencyBucketComponentsResolver
DelinquencyBucketComponentsResolver,
CurrentProductsResolver,
CurrentProductsTemplateResolver,
CurrentProductResolver,
CurrentProductAndTemplateResolver
]
})
export class ProductsRoutingModule {}
+15
View File
@@ -83,6 +83,21 @@
</mat-list-item>
</div>
<div>
<mat-list-item [routerLink]="['current-products']" *mifosxHasPermission="'READ_CURRENTPRODUCT'">
<div class="menu-list-item-content">
<div class="menu-left-section" [routerLink]="['current-products']">
<mat-icon matListIcon>
<fa-icon icon="briefcase" size="sm"></fa-icon>
</mat-icon>
<div matLine>
{{ 'labels.heading.Current Products' | translate }}
</div>
</div>
</div>
</mat-list-item>
</div>
<div #shareProducts>
<mat-list-item [routerLink]="['share-products']" *mifosxHasPermission="'READ_SHAREPRODUCT'">
<div class="menu-list-item-content">
+28
View File
@@ -74,6 +74,34 @@ export class ProductsService {
return this.http.put(`/savingsproducts/${savingProductId}`, savingProduct);
}
/**
* @returns {Observable<any>} Current products data
*/
getCurrentProducts(): Observable<any> {
return this.http.get('/currentproducts');
}
createCurrentProduct(currentProduct: any): Observable<any> {
return this.http.post('/currentproducts', currentProduct);
}
getCurrentProductsTemplate(): Observable<any> {
return this.http.get('/currentproducts/template');
}
getCurrentProduct(currentProductId: string, template: boolean = false): Observable<any> {
const httpParams = new HttpParams().set('template', template.toString());
return this.http.get(`/currentproducts/${currentProductId}`, { params: httpParams });
}
updateCurrentProduct(currentProductId: string, currentProduct: any): Observable<any> {
return this.http.put(`/currentproducts/${currentProductId}`, currentProduct);
}
deleteCurrentProduct(currentProductId: string): Observable<any> {
return this.http.delete(`/currentproducts/${currentProductId}`);
}
/**
* @returns {Observable<any>} Share products data
*/