Merge feature/UI-DSC-MFA: resolve conflicts keeping both UI updates and account-charges changes

This commit is contained in:
Nitesh Yadav
2026-05-19 11:04:33 +05:30
138 changed files with 27495 additions and 3927 deletions
@@ -1,37 +1,163 @@
<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>
<!--
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/.
-->
<div class="pl-root">
<div class="pl-header">
<div class="pl-header-left">
<div class="pl-header-ic"><fa-icon icon="money-bill"></fa-icon></div>
<div class="pl-header-text">
<h2>{{ 'labels.text.Current Products' | translate }}</h2>
<p>Manage current-account products — overdraft limits, transaction rules and accounting.</p>
</div>
</div>
<div class="pl-header-right">
<div class="pl-count-chip" matTooltip="Total configured current-account products">
<fa-icon icon="layer-group"></fa-icon>
<strong>{{ totalCount }}</strong>
<span>{{ totalCount === 1 ? 'product' : 'products' }}</span>
</div>
<button
class="pl-create-btn"
type="button"
[routerLink]="['create']"
*mifosxHasPermission="'CREATE_CURRENTPRODUCT'"
>
<fa-icon icon="plus"></fa-icon>
<span>{{ 'labels.text.Create Current Product' | translate }}</span>
</button>
</div>
</div>
</div>
<div class="container">
<div class="layout-row">
<mat-form-field appearance="outline" class="flex-fill">
<mat-label>{{ 'labels.inputs.Filter' | translate }}</mat-label>
<i matPrefix class="fas fa-search"></i>
<input matInput (keyup)="applyFilter($event.target.value)" />
</mat-form-field>
<div class="pl-kpis">
<div class="pl-kpi pl-kpi--blue">
<div class="pl-kpi-ic"><fa-icon icon="money-bill"></fa-icon></div>
<div>
<div class="pl-kpi-n">{{ totalCount | number }}</div>
<div class="pl-kpi-l">Total Current Products</div>
</div>
</div>
<div class="pl-kpi pl-kpi--teal">
<div class="pl-kpi-ic"><fa-icon icon="coins"></fa-icon></div>
<div>
<div class="pl-kpi-n">{{ currencyCount | number }}</div>
<div class="pl-kpi-l">{{ currencyCount === 1 ? 'Currency' : 'Currencies' }} in use</div>
</div>
</div>
<div class="pl-kpi pl-kpi--purple">
<div class="pl-kpi-ic"><fa-icon icon="filter"></fa-icon></div>
<div>
<div class="pl-kpi-n">{{ filteredCount | number }}</div>
<div class="pl-kpi-l">Showing after filter</div>
</div>
</div>
</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>
<div class="pl-card">
<div class="pl-toolbar">
<div class="pl-search">
<fa-icon icon="search" class="pl-search-ic"></fa-icon>
<input
type="text"
(input)="applyFilter($any($event.target).value)"
placeholder="Search by name or short name…"
/>
</div>
</div>
@if (totalCount === 0) {
<div class="pl-empty">
<div class="pl-empty-ic"><fa-icon icon="money-bill"></fa-icon></div>
<h3>No current-account products yet</h3>
<p>Create your first current-account product to enable transactional account onboarding.</p>
<button
class="pl-create-btn"
type="button"
[routerLink]="['create']"
*mifosxHasPermission="'CREATE_CURRENTPRODUCT'"
>
<fa-icon icon="plus"></fa-icon>
<span>{{ 'labels.text.Create Current Product' | translate }}</span>
</button>
</div>
} @else {
<div class="pl-table-wrap">
<table mat-table [dataSource]="dataSource" matSort class="pl-table">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let p">
<div class="pl-product-cell">
<div class="pl-product-icon"><fa-icon icon="money-bill"></fa-icon></div>
<div class="pl-product-meta">
<div class="pl-product-name">{{ p.name }}</div>
@if (p.currency?.code) {
<div class="pl-product-sub">{{ p.currency.code }}</div>
}
</div>
</div>
</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 p">
<span class="pl-tag">{{ p.shortName }}</span>
</td>
</ng-container>
<ng-container matColumnDef="currency">
<th mat-header-cell *matHeaderCellDef>Currency</th>
<td mat-cell *matCellDef="let p">
@if (p.currency?.code) {
<span class="pl-currency-pill">{{ p.currency.code }}</span>
} @else {
<span class="muted">—</span>
}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="num">Actions</th>
<td mat-cell *matCellDef="let p" class="num">
<button
type="button"
class="pl-action-btn pl-action-btn--view"
[routerLink]="[p.id]"
(click)="$event.stopPropagation()"
matTooltip="View product details"
>
<fa-icon icon="eye"></fa-icon><span>View</span>
</button>
<button
type="button"
class="pl-action-btn pl-action-btn--edit"
[routerLink]="[p.id, 'edit']"
(click)="$event.stopPropagation()"
matTooltip="Edit this product"
*mifosxHasPermission="'UPDATE_CURRENTPRODUCT'"
>
<fa-icon icon="edit"></fa-icon><span>Edit</span>
</button>
</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="pl-row"></tr>
</table>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" [routerLink]="[row.id]" class="select-row"></tr>
</table>
@if (filteredCount === 0 && totalCount > 0) {
<div class="pl-no-match">
<fa-icon icon="search"></fa-icon>
<span>No products match your filter.</span>
</div>
}
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons></mat-paginator>
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" [pageSize]="10" showFirstLastButtons></mat-paginator>
</div>
}
</div>
</div>
@@ -1,7 +1,17 @@
table {
width: 100%;
/**
* 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/.
*/
.select-row:hover {
cursor: pointer;
}
@use 'assets/styles/product-list' as pl;
@include pl.product-list-styles;
/* Current-account-specific green accent */
:host {
--pl-accent: #15803d;
--pl-accent-l: #dcfce7;
}
@@ -1,3 +1,11 @@
/**
* 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/.
*/
import { Component, OnInit, ViewChild, inject } from '@angular/core';
import { MatPaginator } from '@angular/material/paginator';
import { MatSort, MatSortHeader } from '@angular/material/sort';
@@ -16,6 +24,7 @@ import {
} from '@angular/material/table';
import { ActivatedRoute, RouterLink } from '@angular/router';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatTooltip } from '@angular/material/tooltip';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
@@ -37,35 +46,54 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatHeaderRow,
MatRowDef,
MatRow,
MatPaginator
MatPaginator,
MatTooltip
]
})
export class CurrentProductsComponent implements OnInit {
private route = inject(ActivatedRoute);
currentProductsData: any;
currentProductsData: any[] = [];
displayedColumns: string[] = [
'name',
'shortName'
'shortName',
'currency',
'actions'
];
dataSource: MatTableDataSource<any>;
filteredCount = 0;
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
@ViewChild(MatSort, { static: true }) sort: MatSort;
constructor() {
this.route.data.subscribe((data: { currentProducts: any }) => {
this.currentProductsData = data.currentProducts;
this.currentProductsData = data.currentProducts || [];
});
}
get totalCount(): number {
return this.currentProductsData?.length || 0;
}
get currencyCount(): number {
const set = new Set<string>();
for (const p of this.currentProductsData || []) {
if (p?.currency?.code) set.add(p.currency.code);
}
return set.size;
}
ngOnInit() {
this.dataSource = new MatTableDataSource(this.currentProductsData);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
this.filteredCount = this.totalCount;
this.dataSource.connect().subscribe((rows) => (this.filteredCount = rows.length));
}
applyFilter(filterValue: string) {
this.dataSource.filter = filterValue.trim().toLowerCase();
this.dataSource.filter = (filterValue || '').trim().toLowerCase();
}
}
@@ -1,5 +1,22 @@
/**
* 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/.
*/
@use 'assets/styles/helper';
@use 'assets/styles/colours' as *;
@use 'assets/styles/product-view' as pv;
@include pv.product-view-styles;
/* Current-account-specific green accent */
:host {
--pv-accent: #15803d;
--pv-accent-l: #dcfce7;
}
.tab-container {
padding: 1%;
@@ -6,52 +6,167 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="container m-b-20 layout-row align-end gap-20px">
<div #buttonCreateFixedProduct class="in-block">
<button
mat-raised-button
color="primary"
[routerLink]="['create']"
*mifosxHasPermission="'CREATE_FIXEDDEPOSITPRODUCT'"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Create Fixed Deposit Product' | translate }}
</button>
</div>
</div>
<div class="container">
<div #filter class="layout-row gap-20px">
<mat-form-field appearance="outline" class="flex-fill">
<mat-label>{{ 'labels.inputs.Filter' | translate }}</mat-label>
<i matPrefix class="fas fa-search"></i>
<input matInput (keyup)="applyFilter($event.target.value)" />
</mat-form-field>
<div class="pl-root">
<div class="pl-header">
<div class="pl-header-left">
<div class="pl-header-ic"><fa-icon icon="piggy-bank"></fa-icon></div>
<div class="pl-header-text">
<h2>{{ 'labels.heading.Fixed Deposit Products' | translate }}</h2>
<p>Manage fixed-deposit products — tenure, interest charts and maturity rules.</p>
</div>
</div>
<div class="pl-header-right">
<div class="pl-count-chip" matTooltip="Total configured FD products">
<fa-icon icon="layer-group"></fa-icon>
<strong>{{ totalCount }}</strong>
<span>{{ totalCount === 1 ? 'product' : 'products' }}</span>
</div>
<button
#buttonCreateFixedProduct
class="pl-create-btn"
type="button"
[routerLink]="['create']"
*mifosxHasPermission="'CREATE_FIXEDDEPOSITPRODUCT'"
>
<fa-icon icon="plus"></fa-icon>
<span>{{ 'labels.buttons.Create Fixed Deposit Product' | translate }}</span>
</button>
</div>
</div>
<div #fixedProductsTable 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 fixedDepositProduct">{{ fixedDepositProduct.name }}</td>
</ng-container>
<div class="pl-kpis">
<div class="pl-kpi pl-kpi--blue">
<div class="pl-kpi-ic"><fa-icon icon="piggy-bank"></fa-icon></div>
<div>
<div class="pl-kpi-n">{{ totalCount | number }}</div>
<div class="pl-kpi-l">Total FD Products</div>
</div>
</div>
<div class="pl-kpi pl-kpi--teal">
<div class="pl-kpi-ic"><fa-icon icon="coins"></fa-icon></div>
<div>
<div class="pl-kpi-n">{{ currencyCount | number }}</div>
<div class="pl-kpi-l">{{ currencyCount === 1 ? 'Currency' : 'Currencies' }} in use</div>
</div>
</div>
<div class="pl-kpi pl-kpi--purple">
<div class="pl-kpi-ic"><fa-icon icon="filter"></fa-icon></div>
<div>
<div class="pl-kpi-n">{{ filteredCount | number }}</div>
<div class="pl-kpi-l">Showing after filter</div>
</div>
</div>
</div>
<ng-container matColumnDef="shortName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Short Name' | translate }}</th>
<td mat-cell *matCellDef="let fixedDepositProduct">{{ fixedDepositProduct.shortName }}</td>
</ng-container>
<div class="pl-card">
<div class="pl-toolbar">
<div class="pl-search">
<fa-icon icon="search" class="pl-search-ic"></fa-icon>
<input
#filter
type="text"
(input)="applyFilter($any($event.target).value)"
placeholder="Search by name or short name…"
/>
</div>
</div>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" [routerLink]="[row.id]" class="select-row"></tr>
</table>
@if (totalCount === 0) {
<div class="pl-empty">
<div class="pl-empty-ic"><fa-icon icon="piggy-bank"></fa-icon></div>
<h3>No fixed-deposit products yet</h3>
<p>Create your first FD product to enable term-deposit account onboarding.</p>
<button
class="pl-create-btn"
type="button"
[routerLink]="['create']"
*mifosxHasPermission="'CREATE_FIXEDDEPOSITPRODUCT'"
>
<fa-icon icon="plus"></fa-icon>
<span>{{ 'labels.buttons.Create Fixed Deposit Product' | translate }}</span>
</button>
</div>
} @else {
<div #fixedProductsTable class="pl-table-wrap">
<table mat-table [dataSource]="dataSource" matSort class="pl-table">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let p">
<div class="pl-product-cell">
<div class="pl-product-icon"><fa-icon icon="piggy-bank"></fa-icon></div>
<div class="pl-product-meta">
<div class="pl-product-name">{{ p.name }}</div>
@if (p.currency?.code) {
<div class="pl-product-sub">{{ p.currency.code }}</div>
}
</div>
</div>
</td>
</ng-container>
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons></mat-paginator>
<ng-container matColumnDef="shortName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Short Name' | translate }}</th>
<td mat-cell *matCellDef="let p">
<span class="pl-tag">{{ p.shortName }}</span>
</td>
</ng-container>
<ng-container matColumnDef="currency">
<th mat-header-cell *matHeaderCellDef>Currency</th>
<td mat-cell *matCellDef="let p">
@if (p.currency?.code) {
<span class="pl-currency-pill">{{ p.currency.code }}</span>
} @else {
<span class="muted">—</span>
}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="num">Actions</th>
<td mat-cell *matCellDef="let p" class="num">
<button
type="button"
class="pl-action-btn pl-action-btn--view"
[routerLink]="[p.id]"
(click)="$event.stopPropagation()"
matTooltip="View product details"
>
<fa-icon icon="eye"></fa-icon><span>View</span>
</button>
<button
type="button"
class="pl-action-btn pl-action-btn--edit"
[routerLink]="[p.id, 'edit']"
(click)="$event.stopPropagation()"
matTooltip="Edit this product"
*mifosxHasPermission="'UPDATE_FIXEDDEPOSITPRODUCT'"
>
<fa-icon icon="edit"></fa-icon><span>Edit</span>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" [routerLink]="[row.id]" class="pl-row"></tr>
</table>
@if (filteredCount === 0 && totalCount > 0) {
<div class="pl-no-match">
<fa-icon icon="search"></fa-icon>
<span>No products match your filter.</span>
</div>
}
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" [pageSize]="10" showFirstLastButtons></mat-paginator>
</div>
}
</div>
</div>
<ng-template #templateButtonCreateFixedProduct let-popover="popover">
<h2>{{ 'labels.buttons.Create Fixed Deposit Product' | translate }}</h2>
<p class="mw300">{{ 'labels.text.Allows you to create new fixed deposit product' | translate }}</p>
<h2>{{ 'labels.heading.Create Fixed Deposit Product' | translate }}</h2>
<p class="mw300">{{ 'labels.text.This option allows you to create new fixed deposit product.' | translate }}</p>
<div class="layout-row align-end gap-2px responsive-column">
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">
{{ 'labels.buttons.Close' | translate }}
@@ -70,7 +185,7 @@
</ng-template>
<ng-template #templateFilter let-popover="popover">
<h4>{{ 'labels.text.Search bar to filter fixed deposit products.' | translate }}</h4>
<h4>{{ 'labels.heading.Search bar to filter fixed deposit products.' | translate }}</h4>
<div class="layout-row align-end gap-2px responsive-column">
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">
{{ 'labels.buttons.Close' | translate }}
@@ -93,12 +208,7 @@
</ng-template>
<ng-template #templateFixedProductsTable let-popover="popover">
<h4 class="mw300">
{{ 'labels.text.List of fixed deposit products in the organization' | translate }}:
<a [href]="'fixedDepositProducts' | documentationLink" target="_blank" rel="noopener noreferrer">{{
'labels.inputs.Fixed Deposit Products' | translate
}}</a>
</h4>
<h4 class="mw300">{{ 'labels.heading.List of fixed deposit products in the organization' | translate }}</h4>
<div class="layout-row align-end gap-2px responsive-column">
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">
{{ 'labels.buttons.Close' | translate }}
@@ -6,10 +6,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
table {
width: 100%;
@use 'assets/styles/product-list' as pl;
.select-row:hover {
cursor: pointer;
}
@include pl.product-list-styles;
/* FD-specific teal accent */
:host {
--pl-accent: #0e7e6b;
--pl-accent-l: #ccfbf1;
}
@@ -32,6 +32,7 @@ import { of } from 'rxjs';
import { PopoverService } from '../../configuration-wizard/popover/popover.service';
import { ConfigurationWizardService } from '../../configuration-wizard/configuration-wizard.service';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatTooltip } from '@angular/material/tooltip';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
@@ -56,7 +57,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatHeaderRow,
MatRowDef,
MatRow,
MatPaginator
MatPaginator,
MatTooltip
]
})
export class FixedDepositProductsComponent implements OnInit, AfterViewInit {
@@ -66,15 +68,31 @@ export class FixedDepositProductsComponent implements OnInit, AfterViewInit {
private popoverService = inject(PopoverService);
/** Fixed deposit products data. */
fixedDepositProductData: any;
fixedDepositProductData: any[] = [];
/** Columns to be displayed in fixed deposit products table. */
displayedColumns: string[] = [
'name',
'shortName'
'shortName',
'currency',
'actions'
];
/** Data source for fixed deposit products table. */
dataSource: MatTableDataSource<any>;
filteredCount = 0;
get totalCount(): number {
return this.fixedDepositProductData?.length || 0;
}
get currencyCount(): number {
const set = new Set<string>();
for (const p of this.fixedDepositProductData || []) {
if (p?.currency?.code) set.add(p.currency.code);
}
return set.size;
}
/** Paginator for fixed deposit products table. */
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
/** Sorter for fixed deposit products table. */
@@ -124,6 +142,8 @@ export class FixedDepositProductsComponent implements OnInit, AfterViewInit {
this.dataSource = new MatTableDataSource(this.fixedDepositProductData);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
this.filteredCount = this.totalCount;
this.dataSource.connect().subscribe((rows) => (this.filteredCount = rows.length));
}
/**
@@ -6,466 +6,524 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0.
-->
<div class="layout-row align-end gap-2px responsive-column container m-t-20 m-b-20">
<button mat-raised-button color="primary" [routerLink]="['../edit']" *mifosxHasPermission="'UPDATE_SAVINGSPRODUCT'">
<fa-icon icon="edit" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Edit' | translate }}
</button>
</div>
<div class="container">
<mat-card>
<mat-card-content>
<div class="responsive-column">
<h2 class="mat-h2 flex-100">{{ fixedDepositProductData.name }}</h2>
<!-- Details -->
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Details' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Short Name' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.shortName }}</span>
<div class="pv-root">
<!-- HERO -->
<div class="pv-hero">
<div class="pv-hero-left">
<div class="pv-hero-icon"><fa-icon icon="piggy-bank"></fa-icon></div>
<div class="pv-hero-text">
<div class="pv-hero-eyebrow">Fixed Deposit Product</div>
<h2 class="pv-hero-title">{{ fixedDepositProductData.name }}</h2>
<div class="pv-hero-meta">
<span class="pv-pill pv-pill--code">{{ fixedDepositProductData.shortName }}</span>
@if (fixedDepositProductData.currency?.code) {
<span class="pv-pill pv-pill--teal">{{ fixedDepositProductData.currency.code }}</span>
}
</div>
@if (fixedDepositProductData.description) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Description' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.description }}</span>
</div>
}
<!-- Product Classification -->
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">Product Classification</h3>
<mat-divider class="flex-100"></mat-divider>
@if (fixedDepositProductData.accountType) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Account Type' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.accountType }}</span>
</div>
}
@if (fixedDepositProductData.fdType) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.FD Type' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.fdType }}</span>
</div>
}
@if (fixedDepositProductData.glAccountName || fixedDepositProductData.glAccountId) {
<div class="flex-100 layout-row">
<span class="flex-40">GL Account (Liability):</span>
<span class="flex-60">
{{ fixedDepositProductData.glAccountName || fixedDepositProductData.glAccountId }}
</span>
</div>
}
@if (fixedDepositProductData.effectiveTo) {
<div class="flex-100 layout-row">
<span class="flex-40">Effective To:</span>
<span class="flex-60">{{ fixedDepositProductData.effectiveTo | dateFormat }}</span>
</div>
}
@if (fixedDepositProductData.endDate) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.End Date' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.endDate | dateFormat }}</span>
</div>
}
@if (fixedDepositProductData.shareLinkingEnabled !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Share Linking Enabled' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.shareLinkingEnabled | yesNo }}</span>
</div>
}
<!-- Currency -->
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Currency' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.heading.Currency' | translate }}:</span>
<span class="flex-60"
>({{ fixedDepositProductData.currency.code }}) {{ fixedDepositProductData.currency.name }}</span
>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Decimal Places' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.currency.decimalPlaces }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Currency in multiples of' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.currency.inMultiplesOf | formatNumber }}</span>
</div>
@if (fixedDepositProductData.dormancyAlert !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Dormancy Alert:</span>
<span class="flex-60">{{ fixedDepositProductData.dormancyAlert | yesNo }}</span>
</div>
}
@if (fixedDepositProductData.dormancyAlert) {
<div class="flex-100 layout-row">
<span class="flex-40">Days to Inactive:</span>
<span class="flex-60">{{ fixedDepositProductData.daysToInactive }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Days to Dormancy:</span>
<span class="flex-60">{{ fixedDepositProductData.daysToDormancy }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Days to Escheat:</span>
<span class="flex-60">{{ fixedDepositProductData.daysToEscheat }}</span>
</div>
}
@if (fixedDepositProductData.nominationFacility !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Nomination Facility' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.nominationFacility | yesNo }}</span>
</div>
}
@if (fixedDepositProductData.autoDebitFacility !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Auto Debit Facility' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.autoDebitFacility | yesNo }}</span>
</div>
}
<!-- Deposit Amount -->
@if (
fixedDepositProductData.minDepositAmount !== undefined ||
fixedDepositProductData.maxDepositAmount !== undefined
) {
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">Deposit Amount</h3>
<mat-divider class="flex-100"></mat-divider>
@if (fixedDepositProductData.minDepositAmount !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Minimum:</span>
<span class="flex-60">{{ fixedDepositProductData.minDepositAmount }}</span>
</div>
}
@if (fixedDepositProductData.maxDepositAmount !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Maximum:</span>
<span class="flex-60">{{ fixedDepositProductData.maxDepositAmount }}</span>
</div>
}
}
<!-- Terms -->
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Terms' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Nominal Annual Interest Rate' | translate }} (%):</span>
<span class="flex-60">{{ fixedDepositProductData.nominalAnnualInterestRate | formatNumber }} %</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Compounding Period' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.interestCompoundingPeriodType?.value }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Posting Period' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.interestPostingPeriodType?.value }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Calculated Using' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.interestCalculationType?.value }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Days in Year' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.interestCalculationDaysInYearType?.value }}</span>
</div>
@if (fixedDepositProductData.interestCalculateSelector) {
<div class="flex-100 layout-row">
<span class="flex-40">Interest Calculation Selector:</span>
<span class="flex-60">{{ fixedDepositProductData.interestCalculateSelector }}</span>
</div>
}
@if (fixedDepositProductData.averagePeriod) {
<div class="flex-100 layout-row">
<span class="flex-40">Average Period:</span>
<span class="flex-60">{{ fixedDepositProductData.averagePeriod }}</span>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">Auto Interest Post:</span>
<span class="flex-60">{{ fixedDepositProductData.autoPost | yesNo }}</span>
</div>
@if (fixedDepositProductData.frequencyFixDay !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Frequency (Fix Day):</span>
<span class="flex-60">{{ fixedDepositProductData.frequencyFixDay }}</span>
</div>
}
@if (fixedDepositProductData.lastInterestDate) {
<div class="flex-100 layout-row">
<span class="flex-40">Last Interest Date:</span>
<span class="flex-60">{{ fixedDepositProductData.lastInterestDate | dateFormat }}</span>
</div>
}
<!-- Settings -->
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Settings' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Loan Facility' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.loanFacility | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Premature Withdrawal Option' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.prematureWithdrawalOption | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Tenure Flexibility' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.tenureFlexibility | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Lien Marked' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.lienMarked | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.withdrawalFeeForTransfers | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Enable Lock-in Period:</span>
<span class="flex-60">{{ fixedDepositProductData.enableLockinPeriod | yesNo }}</span>
</div>
@if (fixedDepositProductData.lockinPeriodFrequency !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Lock-in Period' | translate }}:</span>
<span class="flex-60">
{{ fixedDepositProductData.lockinPeriodFrequency }}
{{ fixedDepositProductData.lockinPeriodFrequencyType?.value }}
</span>
</div>
}
@if (fixedDepositProductData.maturityInstructions !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Maturity Instructions:</span>
<span class="flex-60">{{ fixedDepositProductData.maturityInstructions | yesNo }}</span>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">Credit Maturity Proceeds:</span>
<span class="flex-60">{{ fixedDepositProductData.creditMaturityProceeds | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Penalty Grace Period (Missed Installments):</span>
<span class="flex-60">{{ fixedDepositProductData.penaltyGracePeriodMissedInstallments | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Panel Request Applicable:</span>
<span class="flex-60">{{ fixedDepositProductData.panelRequestApplicable | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">No Penalty (Nominee / Heir Closure):</span>
<span class="flex-60">{{ fixedDepositProductData.noPenaltyNomineeHeirClosure | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Deceased Settlement:</span>
<span class="flex-60">{{ fixedDepositProductData.deceasedSettlement | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Allow Debit Txn After Due Date:</span>
<span class="flex-60">{{ fixedDepositProductData.allowDebitTxnAfterDueDate | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">Account Closed with Zero Balance in Day Closing:</span>
<span class="flex-60">{{ fixedDepositProductData.accountClosedZeroBalanceInDayClosing | yesNo }}</span>
</div>
@if (fixedDepositProductData.minBalanceForInterestCalculation !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">Min Balance for Interest Calculation:</span>
<span class="flex-60">{{ fixedDepositProductData.minBalanceForInterestCalculation | formatNumber }}</span>
</div>
}
@if (fixedDepositProductData.minDepositTerm !== undefined) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Minimum Deposit Term' | translate }}:</span>
<span class="flex-60"
>{{ fixedDepositProductData.minDepositTerm }}
{{ fixedDepositProductData.minDepositTermType?.value }}</span
>
</div>
}
@if (fixedDepositProductData.inMultiplesOfDepositTerm) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Deposit Term In multiple of' | translate }}:</span>
<span class="flex-60"
>{{ fixedDepositProductData.inMultiplesOfDepositTerm }}
{{ fixedDepositProductData.inMultiplesOfDepositTermType?.value }}</span
>
</div>
}
@if (fixedDepositProductData.maxDepositTerm) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Maximum Deposit Term' | translate }}:</span>
<span class="flex-60"
>{{ fixedDepositProductData.maxDepositTerm }}
{{ fixedDepositProductData.maxDepositTermType?.value }}</span
>
</div>
}
@if (fixedDepositProductData.preClosurePenalApplicable) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Apply Penal Interest (less)' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.preClosurePenalApplicable | yesNo }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Penal Interest' | translate }} (%):</span>
<span class="flex-60"
>{{ fixedDepositProductData.preClosurePenalInterest }} % On
{{ fixedDepositProductData.preClosurePenalInterestOnType?.value }}</span
>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Withhold Tax is Applicable' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.withHoldTax | yesNo }}</span>
</div>
@if (fixedDepositProductData.withHoldTax) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Withhold Tax group' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.taxGroup?.name }}</span>
</div>
}
<!-- CBS Base Interest Rate Charts -->
@if (fdSlabs.length) {
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Interest Rate Charts' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<div class="layout-row-wrap responsive-column flex-100">
<table class="flex-100 mat-elevation-z1" mat-table [dataSource]="fdSlabs">
<ng-container matColumnDef="srNo">
<th mat-header-cell *matHeaderCellDef>Sr.</th>
<td mat-cell *matCellDef="let slab">{{ slab.srNo }}</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>Particular</th>
<td mat-cell *matCellDef="let slab">{{ slab.description }}</td>
</ng-container>
<ng-container matColumnDef="fromPeriod">
<th mat-header-cell *matHeaderCellDef>From</th>
<td mat-cell *matCellDef="let slab">{{ slab.fromPeriod }}</td>
</ng-container>
<ng-container matColumnDef="toPeriod">
<th mat-header-cell *matHeaderCellDef>To</th>
<td mat-cell *matCellDef="let slab">{{ slab.toPeriod ?? '—' }}</td>
</ng-container>
<ng-container matColumnDef="periodTypeLabel">
<th mat-header-cell *matHeaderCellDef>Period</th>
<td mat-cell *matCellDef="let slab">{{ slab.periodTypeLabel }}</td>
</ng-container>
<ng-container matColumnDef="annualInterestRate">
<th mat-header-cell *matHeaderCellDef>Rate (%)</th>
<td mat-cell *matCellDef="let slab">
<strong>{{ slab.annualInterestRate }}</strong>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="fdSlabColumns"></tr>
<tr mat-row *matRowDef="let row; columns: fdSlabColumns"></tr>
</table>
</div>
}
<!-- Charges -->
@if (fixedDepositProductData.charges?.length) {
<div class="layout-row-wrap responsive-column flex-100">
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Charges' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<table class="flex-100 mat-elevation-z1" mat-table [dataSource]="fixedDepositProductData.charges">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.name }}</td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.chargeCalculationType?.value }}</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="collectedon">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Collected On' | translate }}</th>
<td mat-cell *matCellDef="let charge">{{ charge.chargeTimeType?.value }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chargesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chargesDisplayedColumns"></tr>
</table>
</div>
}
<!-- Accounting -->
<mat-divider class="flex-100"></mat-divider>
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Accounting' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
@if (!isNoneAccounting()) {
<mifosx-view-savings-accounting-details
[accountingRule]="fixedDepositProductData.accountingRule"
[accountingMappings]="fixedDepositProductData.accountingMappings"
></mifosx-view-savings-accounting-details>
<p class="pv-hero-desc">{{ fixedDepositProductData.description }}</p>
}
</div>
</mat-card-content>
</mat-card>
</div>
<button class="pv-edit-btn" type="button" [routerLink]="['../edit']" *mifosxHasPermission="'UPDATE_SAVINGSPRODUCT'">
<fa-icon icon="edit"></fa-icon><span>{{ 'labels.buttons.Edit' | translate }}</span>
</button>
</div>
<div class="pv-legacy">
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Details' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Short Name' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.shortName }}</span>
</div>
@if (fixedDepositProductData.description) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Description' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.description }}</span>
</div>
}
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Currency' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.heading.Currency' | translate }}:</span>
<span class="flex-60"
>({{ fixedDepositProductData.currency.code }}) {{ fixedDepositProductData.currency.name }}</span
>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Decimal Places' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.currency.decimalPlaces }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Currency in multiples of' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.currency.inMultiplesOf | formatNumber }}</span>
</div>
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Terms' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Deposit Amount' | translate }}:</span>
<span class="flex-60">{{
fixedDepositProductData.depositAmount +
' (Min: ' +
(fixedDepositProductData.minDepositAmount
? fixedDepositProductData.minDepositAmount
: fixedDepositProductData.depositAmount) +
', Max: ' +
(fixedDepositProductData.maxDepositAmount
? fixedDepositProductData.maxDepositAmount
: fixedDepositProductData.depositAmount) +
')'
}}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Compounding Period' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.interestCompoundingPeriodType.value }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Posting Period' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.interestPostingPeriodType.value }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Interest Calculated Using' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.interestCalculationType.value }}</span>
</div>
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Days in Year' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.interestCalculationDaysInYearType.value }}</span>
</div>
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Settings' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
@if (fixedDepositProductData.lockinPeriodFrequency) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Lock-in Period' | translate }}:</span>
<span class="flex-60">
{{ fixedDepositProductData.lockinPeriodFrequency }}
{{ fixedDepositProductData.lockinPeriodFrequencyType.value }}</span
>
</div>
}
@if (fixedDepositProductData.minDepositTerm) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Minimum Deposit Term' | translate }}:</span>
@if (fixedDepositProductData.minDepositTerm !== undefined) {
<span class="flex-60"
>{{ fixedDepositProductData.minDepositTerm }}
&nbsp;
{{ fixedDepositProductData.minDepositTermType.value }}</span
>
}
</div>
}
@if (fixedDepositProductData.maxDepositTerm) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Maximum Deposit Term' | translate }}:</span>
@if (fixedDepositProductData.maxDepositTerm !== undefined) {
<span class="flex-60"
>{{ fixedDepositProductData.maxDepositTerm }}
&nbsp;
{{ fixedDepositProductData.maxDepositTermType.value }}</span
>
}
</div>
}
@if (fixedDepositProductData.inMultiplesOfDepositTerm) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Deposit Term In multiple of' | translate }}:</span>
<span class="flex-60"
>{{ fixedDepositProductData.inMultiplesOfDepositTerm }}
-
{{ fixedDepositProductData.inMultiplesOfDepositTermType.value }}</span
>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Apply Penal Interest (less)' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.preClosurePenalApplicable | yesNo }}</span>
</div>
@if (fixedDepositProductData.preClosurePenalApplicable) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Penal Interest' | translate }} (%):</span>
<span class="flex-60"
>{{ fixedDepositProductData.preClosurePenalInterest }} &nbsp; &nbsp; % &nbsp; &nbsp; On &nbsp;&nbsp;
{{ fixedDepositProductData.preClosurePenalInterestOnType.value }}</span
>
</div>
}
@if (fixedDepositProductData.minBalanceForInterestCalculation) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Balance Required for Interest Calculation' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.minBalanceForInterestCalculation | formatNumber }}</span>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Withhold Tax is Applicable' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.withHoldTax | yesNo }}</span>
</div>
@if (fixedDepositProductData.withHoldTax) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Withhold Tax group' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.taxGroup.name }}</span>
</div>
}
@if (fixedDepositProductData.activeChart) {
<div class="responsive-column flex-100">
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Interest Rate Chart' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<div class="responsive-column flex-100">
@if (fixedDepositProductData.activeChart.name) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Name' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.activeChart.name }}</span>
</div>
}
@if (fixedDepositProductData.activeChart.description) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Description' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.activeChart.description }}</span>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.From Date' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.activeChart.fromDate | dateFormat }}</span>
</div>
@if (fixedDepositProductData.activeChart.endDate) {
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.End Date' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.activeChart.endDate | dateFormat }}</span>
</div>
}
<div class="flex-100 layout-row">
<span class="flex-40">{{ 'labels.inputs.Primary Grouping by Amount' | translate }}:</span>
<span class="flex-60">{{ fixedDepositProductData.activeChart.isPrimaryGroupingByAmount | yesNo }}</span>
</div>
<table
class="mat-elevation-z1 flex-100"
mat-table
[dataSource]="fixedDepositProductData.activeChart.chartSlabs"
multiTemplateDataRows
>
<ng-container matColumnDef="period">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Period' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">
@if (chartSlab.fromPeriod !== undefined) {
<div>{{ chartSlab.fromPeriod }} - {{ chartSlab.toPeriod }} {{ chartSlab.periodType.value }}</div>
}
@if (chartSlab.fromPeriod === undefined) {
<div>&nbsp;</div>
}
</td>
</ng-container>
<ng-container matColumnDef="amountRange">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount Range' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">
@if (chartSlab.amountRangeFrom !== undefined) {
<div>{{ chartSlab.amountRangeFrom }} - {{ chartSlab.amountRangeTo }}</div>
}
@if (chartSlab.amountRangeFrom === undefined) {
<div>&nbsp;</div>
}
</td>
</ng-container>
<ng-container matColumnDef="annualInterestRate">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Interest' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">{{ chartSlab.annualInterestRate }} %</td>
</ng-container>
<ng-container matColumnDef="description">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Description' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab">
{{ chartSlab.description }}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
<td mat-cell *matCellDef="let chartSlab; let chartSlabIndex = dataIndex">
<button
mat-button
color="primary"
(click)="expandChartSlabIndex = expandChartSlabIndex === chartSlabIndex ? null : chartSlabIndex"
>
@if (expandChartSlabIndex !== chartSlabIndex) {
<span>
<fa-icon icon="eye" class="m-r-10"></fa-icon>
{{ 'labels.buttons.View Incentives' | translate }}
</span>
}
@if (expandChartSlabIndex === chartSlabIndex) {
<span>
<fa-icon icon="eye-slash" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Hide Incentives' | translate }}
</span>
}
</button>
</td>
</ng-container>
<ng-container matColumnDef="incentives">
<td
mat-cell
*matCellDef="let chartSlab; let chartSlabIndex = dataIndex"
[attr.colspan]="chartSlabsDisplayedColumns.length"
>
<div
class="incentives flex-100 layout-row-wrap"
[@expandChartSlab]="chartSlabIndex === expandChartSlabIndex ? 'expanded' : 'collapsed'"
>
<mat-card class="layout-row-wrap flex-100">
<h4 class="mat-h4 flex-23">
{{ 'labels.heading.Incentives' | translate }}
</h4>
<table class="flex-100 mat-elevation-z1" mat-table [dataSource]="chartSlab.incentives">
<ng-container matColumnDef="entityType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Entity Type' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{
incentive.entityType
| find: fixedDepositProductsTemplate.chartTemplate.entityTypeOptions : 'id' : 'value'
}}
</td>
</ng-container>
<ng-container matColumnDef="attributeName">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Name' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{
incentive.attributeName
| find: fixedDepositProductsTemplate.chartTemplate.attributeNameOptions : 'id' : 'value'
}}
</td>
</ng-container>
<ng-container matColumnDef="conditionType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Condition Type' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{
incentive.conditionType
| find: fixedDepositProductsTemplate.chartTemplate.conditionTypeOptions : 'id' : 'value'
}}
</td>
</ng-container>
<ng-container matColumnDef="attributeValue">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Attribute Value' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
@switch (incentive.attributeName) {
@case (2) {
<span>{{
incentive.attributeValue
| find: fixedDepositProductsTemplate.chartTemplate.genderOptions : 'id' : 'name'
}}</span>
}
@case (3) {
<span>{{ incentive.attributeValue }}</span>
}
@case (4) {
<span>{{
incentive.attributeValue
| find: fixedDepositProductsTemplate.chartTemplate.clientTypeOptions : 'id' : 'name'
}}</span>
}
@case (5) {
<span>{{
incentive.attributeValue
| find
: fixedDepositProductsTemplate.chartTemplate.clientClassificationOptions
: 'id'
: 'name'
}}</span>
}
}
</td>
</ng-container>
<ng-container matColumnDef="incentiveType">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Incentive Type' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{
incentive.incentiveType
| find: fixedDepositProductsTemplate.chartTemplate.incentiveTypeOptions : 'id' : 'value'
}}
</td>
</ng-container>
<ng-container matColumnDef="amount">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Amount' | translate }}</th>
<td mat-cell *matCellDef="let incentive">
{{ incentive.amount | formatNumber }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="incentivesDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: incentivesDisplayedColumns"></tr>
</table>
</mat-card>
</div>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="chartSlabsDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chartSlabsDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: chartSlabsIncentivesDisplayedColumns" class="incentives-row"></tr>
</table>
</div>
</div>
}
@if (fixedDepositProductData.charges.length) {
<div class="layout-row-wrap responsive-column flex-100">
<h3 class="mat-h3 flex-100">{{ 'labels.inputs.Charges' | translate }}</h3>
<mat-divider class="flex-100"></mat-divider>
<table class="flex-100 mat-elevation-z1" mat-table [dataSource]="fixedDepositProductData.charges">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.name }}
</td>
</ng-container>
<ng-container matColumnDef="type">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeCalculationType.value }}
</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="collectedon">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Collected On' | translate }}</th>
<td mat-cell *matCellDef="let charge">
{{ charge.chargeTimeType.value }}
</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-100">{{ 'labels.heading.Accounting' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<mifosx-view-savings-accounting-details
class="container"
[accountingRule]="fixedDepositProductData.accountingRule"
[accountingMappings]="fixedDepositProductData.accountingMappings"
>
</mifosx-view-savings-accounting-details>
@if (
fixedDepositProductData.advancedAccountingRules &&
(fixedDepositProductData.paymentChannelToFundSourceMappings?.length ||
fixedDepositProductData.feeToIncomeAccountMappings?.length ||
fixedDepositProductData.penaltyToIncomeAccountMappings?.length)
) {
<div class="flex-100 layout-row-wrap layout-lt-md-column">
<h3 class="mat-h3 flex-100">{{ 'labels.heading.Advanced Accounting Rules' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<hr />
@if (fixedDepositProductData.paymentChannelToFundSourceMappings?.length) {
<div class="flex-100 layout-row-wrap layout-xs-column">
<h4 class="mat-h4 flex-100">
{{ 'labels.inputs.Configure Fund Sources for Payment Channels' | translate }}
</h4>
<table
class="mat-elevation-z1 flex-100"
mat-table
[dataSource]="fixedDepositProductData.paymentChannelToFundSourceMappings"
>
<ng-container matColumnDef="paymentTypeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Payment Type' | translate }}</th>
<td mat-cell *matCellDef="let paymentFundSource">
{{ paymentFundSource.paymentType.name }}
</td>
</ng-container>
<ng-container matColumnDef="fundSourceAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Fund Source' | translate }}</th>
<td mat-cell *matCellDef="let paymentFundSource">
{{ paymentFundSource.fundSourceAccount.name }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="paymentFundSourceDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: paymentFundSourceDisplayedColumns"></tr>
</table>
</div>
}
@if (fixedDepositProductData.feeToIncomeAccountMappings?.length) {
<div class="flex-100 layout-row-wrap layout-xs-column">
<h4 class="mat-h4 flex-100">{{ 'labels.inputs.Fees to Specific Income Accounts' | translate }}</h4>
<table
class="mat-elevation-z1 flex-100"
mat-table
[dataSource]="fixedDepositProductData.feeToIncomeAccountMappings"
>
<ng-container matColumnDef="chargeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Fees' | translate }}</th>
<td mat-cell *matCellDef="let feesIncome">
{{ feesIncome.charge.name }}
</td>
</ng-container>
<ng-container matColumnDef="incomeAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Income Account' | translate }}</th>
<td mat-cell *matCellDef="let feesIncome">
{{ feesIncome.incomeAccount.name }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr>
</table>
</div>
}
@if (fixedDepositProductData.penaltyToIncomeAccountMappings?.length) {
<div class="flex-100 layout-row-wrap layout-xs-column">
<h4 class="mat-h4 flex-100">
{{ 'labels.heading.Penalties to Specific Income Accounts' | translate }}
</h4>
<table
class="mat-elevation-z1 flex-100"
mat-table
[dataSource]="fixedDepositProductData.penaltyToIncomeAccountMappings"
>
<ng-container matColumnDef="chargeId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Penalty' | translate }}</th>
<td mat-cell *matCellDef="let penaltyIncome">
{{ penaltyIncome.charge.name }}
</td>
</ng-container>
<ng-container matColumnDef="incomeAccountId">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Income Account' | translate }}</th>
<td mat-cell *matCellDef="let penaltyIncome">
{{ penaltyIncome.incomeAccount.name }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="feesPenaltyIncomeDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: feesPenaltyIncomeDisplayedColumns"></tr>
</table>
</div>
}
</div>
}
</div>
</div>
@@ -8,6 +8,15 @@
@use 'assets/styles/helper';
@use 'assets/styles/colours' as *;
@use 'assets/styles/product-view' as pv;
@include pv.product-view-styles;
/* FD-specific teal accent */
:host {
--pv-accent: #0e7e6b;
--pv-accent-l: #ccfbf1;
}
.container {
width: 100%;
@@ -6,73 +6,195 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="container m-b-20 layout-row align-end gap-20px">
<div #buttonCreateLoanProduct class="in-block">
<button mat-raised-button color="primary" [routerLink]="['create']" *mifosxHasPermission="'CREATE_LOANPRODUCT'">
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Create Loan Product' | translate }}
</button>
</div>
<div class="in-block">
<button mat-raised-button color="primary" (click)="openImportDialog()" *mifosxHasPermission="'CREATE_LOANPRODUCT'">
<fa-icon icon="upload" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Import' | translate }}
</button>
</div>
</div>
<div class="container">
<div #filter class="layout-row">
<mat-form-field appearance="outline" class="flex-fill">
<mat-label>{{ 'labels.inputs.Filter' | translate }}</mat-label>
<i matPrefix class="fas fa-search"></i>
<input matInput (keyup)="applyFilter($event.target.value)" />
</mat-form-field>
<div class="pl-root">
<!-- HEADER -->
<div class="pl-header">
<div class="pl-header-left">
<div class="pl-header-ic"><fa-icon icon="hand-holding-usd"></fa-icon></div>
<div class="pl-header-text">
<h2>{{ 'labels.heading.Loan Products' | translate }}</h2>
<p>Manage loan products available for disbursement — terms, charges, and accounting rules.</p>
</div>
</div>
<div class="pl-header-right">
<div class="pl-count-chip" matTooltip="Total configured loan products">
<fa-icon icon="layer-group"></fa-icon>
<strong>{{ totalCount }}</strong>
<span>{{ totalCount === 1 ? 'product' : 'products' }}</span>
</div>
<button
class="pl-secondary-btn"
type="button"
(click)="openImportDialog()"
*mifosxHasPermission="'CREATE_LOANPRODUCT'"
>
<fa-icon icon="upload"></fa-icon>
<span>{{ 'labels.buttons.Import' | translate }}</span>
</button>
<button
#buttonCreateLoanProduct
class="pl-create-btn"
type="button"
[routerLink]="['create']"
*mifosxHasPermission="'CREATE_LOANPRODUCT'"
>
<fa-icon icon="plus"></fa-icon>
<span>{{ 'labels.buttons.Create Loan Product' | translate }}</span>
</button>
</div>
</div>
<div #loanProductsTable 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 loanProduct">{{ loanProduct.name }}</td>
</ng-container>
<!-- KPI -->
<div class="pl-kpis">
<div class="pl-kpi pl-kpi--blue">
<div class="pl-kpi-ic"><fa-icon icon="hand-holding-usd"></fa-icon></div>
<div>
<div class="pl-kpi-n">{{ totalCount | number }}</div>
<div class="pl-kpi-l">Total Loan Products</div>
</div>
</div>
<div class="pl-kpi pl-kpi--teal">
<div class="pl-kpi-ic"><fa-icon icon="check-circle"></fa-icon></div>
<div>
<div class="pl-kpi-n">{{ activeCount | number }}</div>
<div class="pl-kpi-l">Active</div>
</div>
</div>
<div class="pl-kpi pl-kpi--purple">
<div class="pl-kpi-ic"><fa-icon icon="filter"></fa-icon></div>
<div>
<div class="pl-kpi-n">{{ filteredCount | number }}</div>
<div class="pl-kpi-l">Showing after filter</div>
</div>
</div>
</div>
<ng-container matColumnDef="shortName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Short Name' | translate }}</th>
<td mat-cell *matCellDef="let loanProduct">{{ loanProduct.shortName }}</td>
</ng-container>
<!-- BODY -->
<div class="pl-card">
<div class="pl-toolbar">
<div class="pl-search">
<fa-icon icon="search" class="pl-search-ic"></fa-icon>
<input
#filter
type="text"
(input)="applyFilter($any($event.target).value)"
placeholder="Search by name or short name…"
/>
</div>
</div>
<ng-container matColumnDef="closeDate">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Expiry Date' | translate }}</th>
<td mat-cell *matCellDef="let loanProduct">{{ loanProduct.closeDate | dateFormat }}</td>
</ng-container>
@if (totalCount === 0) {
<div class="pl-empty">
<div class="pl-empty-ic"><fa-icon icon="hand-holding-usd"></fa-icon></div>
<h3>No loan products yet</h3>
<p>Create your first loan product to enable loan account onboarding for clients.</p>
<button
class="pl-create-btn"
type="button"
[routerLink]="['create']"
*mifosxHasPermission="'CREATE_LOANPRODUCT'"
>
<fa-icon icon="plus"></fa-icon>
<span>{{ 'labels.buttons.Create Loan Product' | translate }}</span>
</button>
</div>
} @else {
<div #loanProductsTable class="pl-table-wrap">
<table mat-table [dataSource]="dataSource" matSort class="pl-table">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let p">
<div class="pl-product-cell">
<div class="pl-product-icon"><fa-icon icon="hand-holding-usd"></fa-icon></div>
<div class="pl-product-meta">
<div class="pl-product-name">{{ p.name }}</div>
@if (p.currency?.code) {
<div class="pl-product-sub">{{ p.currency.code }}</div>
}
</div>
</div>
</td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Status' | translate }}</th>
<td mat-cell *matCellDef="let loanProduct">
<div [className]="loanProduct.status | statusLookup">
<fa-icon
matTooltip="{{
loanProduct.status === 'loanProduct.active'
? ('labels.inputs.Active' | translate)
: ('labels.catalogs.Inactive' | translate)
}}"
matTooltipPosition="right"
icon="circle"
size="lg"
></fa-icon>
<ng-container matColumnDef="shortName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Short Name' | translate }}</th>
<td mat-cell *matCellDef="let p">
<span class="pl-tag">{{ p.shortName }}</span>
</td>
</ng-container>
<ng-container matColumnDef="closeDate">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Expiry Date' | translate }}</th>
<td mat-cell *matCellDef="let p">
@if (p.closeDate) {
{{ p.closeDate | dateFormat }}
} @else {
<span class="muted">—</span>
}
</td>
</ng-container>
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Status' | translate }}</th>
<td mat-cell *matCellDef="let p">
<span
class="pl-status-pill"
[class.pl-status-pill--active]="p.status === 'loanProduct.active'"
[class.pl-status-pill--inactive]="p.status !== 'loanProduct.active'"
>
<fa-icon [icon]="p.status === 'loanProduct.active' ? 'check-circle' : 'times-circle'"></fa-icon>
{{
p.status === 'loanProduct.active'
? ('labels.inputs.Active' | translate)
: ('labels.catalogs.Inactive' | translate)
}}
</span>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="num">Actions</th>
<td mat-cell *matCellDef="let p" class="num">
<button
type="button"
class="pl-action-btn pl-action-btn--view"
[routerLink]="[p.id]"
(click)="$event.stopPropagation()"
matTooltip="View product details"
>
<fa-icon icon="eye"></fa-icon><span>View</span>
</button>
<button
type="button"
class="pl-action-btn pl-action-btn--edit"
[routerLink]="[p.id, 'edit']"
(click)="$event.stopPropagation()"
matTooltip="Edit this product"
*mifosxHasPermission="'UPDATE_LOANPRODUCT'"
>
<fa-icon icon="edit"></fa-icon><span>Edit</span>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" [routerLink]="[row.id]" class="pl-row"></tr>
</table>
@if (filteredCount === 0 && totalCount > 0) {
<div class="pl-no-match">
<fa-icon icon="search"></fa-icon>
<span>No products match your filter.</span>
</div>
</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>
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" [pageSize]="10" showFirstLastButtons></mat-paginator>
</div>
}
</div>
</div>
<!-- Config-Wizard popovers kept intact -->
<ng-template #templateButtonCreateLoanProduct let-popover="popover">
<h2>{{ 'labels.heading.Create loan product' | translate }}</h2>
<p class="mw300">{{ 'labels.text.This option allows you to create new loan product.' | translate }}</p>
@@ -6,10 +6,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
table {
width: 100%;
@use 'assets/styles/product-list' as pl;
.select-row:hover {
cursor: pointer;
}
@include pl.product-list-styles;
/* Loan-specific accent tint (amber/orange) */
:host {
--pl-accent: #b45309;
--pl-accent-l: #fef3c7;
}
@@ -78,15 +78,30 @@ export class LoanProductsComponent implements OnInit, AfterViewInit {
private settingsService = inject(SettingsService);
private errorHandler = inject(ErrorHandlerService);
loanProductsData: any;
loanProductsData: any[] = [];
displayedColumns: string[] = [
'name',
'shortName',
'closeDate',
'status'
'status',
'actions'
];
dataSource: MatTableDataSource<any>;
filteredCount = 0;
get totalCount(): number {
return this.loanProductsData?.length || 0;
}
get activeCount(): number {
return (this.loanProductsData || []).filter((p) => p?.status === 'loanProduct.active').length;
}
get inactiveCount(): number {
return this.totalCount - this.activeCount;
}
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
@ViewChild(MatSort, { static: true }) sort: MatSort;
@@ -115,10 +130,12 @@ export class LoanProductsComponent implements OnInit, AfterViewInit {
this.dataSource = new MatTableDataSource(this.loanProductsData);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
this.filteredCount = this.totalCount;
this.dataSource.connect().subscribe((rows) => (this.filteredCount = rows.length));
}
applyFilter(filterValue: string) {
this.dataSource.filter = filterValue.trim().toLowerCase();
this.dataSource.filter = (filterValue || '').trim().toLowerCase();
}
/**
@@ -6,25 +6,58 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="layout-row align-end gap-2px responsive-column container m-b-20 m-t-10">
<!--
<button mat-raised-button color="primary" *mifosxHasPermission="'UPDATE_LOANPRODUCT'" (click)="copyProduct()">
<fa-icon icon="copy" class="m-r-10"></fa-icon>{{'labels.buttons.Create' | translate}} {{'labels.text.Copy' | translate}}
</button>
-->
<button mat-raised-button color="primary" *mifosxHasPermission="'UPDATE_LOANPRODUCT'" (click)="exportDefinition()">
<fa-icon icon="download" class="m-r-10"></fa-icon>{{ 'labels.buttons.Export' | translate }}
</button>
<button mat-raised-button color="primary" [routerLink]="['../edit']" *mifosxHasPermission="'UPDATE_LOANPRODUCT'">
<fa-icon icon="edit" class="m-r-10"></fa-icon>{{ 'labels.buttons.Edit' | translate }}
</button>
</div>
<div class="pv-root">
<!-- HERO -->
<div class="pv-hero">
<div class="pv-hero-left">
<div class="pv-hero-icon"><fa-icon icon="hand-holding-usd"></fa-icon></div>
<div class="pv-hero-text">
<div class="pv-hero-eyebrow">Loan Product</div>
<h2 class="pv-hero-title">{{ loanProduct.name }}</h2>
<div class="pv-hero-meta">
<span class="pv-pill pv-pill--code">{{ loanProduct.shortName }}</span>
@if (loanProduct.currency?.code) {
<span class="pv-pill pv-pill--teal">{{ loanProduct.currency.code }}</span>
}
<span
class="pv-pill"
[class.pv-pill--green]="loanProduct.status === 'loanProduct.active'"
[class.pv-pill--amber]="loanProduct.status !== 'loanProduct.active'"
>
<fa-icon [icon]="loanProduct.status === 'loanProduct.active' ? 'check-circle' : 'times-circle'"></fa-icon>
{{ loanProduct.status === 'loanProduct.active' ? 'Active' : 'Inactive' }}
</span>
</div>
@if (loanProduct.description) {
<p class="pv-hero-desc">{{ loanProduct.description }}</p>
}
</div>
</div>
<div style="display: flex; gap: 10px; flex-shrink: 0">
<button
class="pv-edit-btn"
type="button"
*mifosxHasPermission="'UPDATE_LOANPRODUCT'"
(click)="exportDefinition()"
>
<fa-icon icon="download"></fa-icon><span>{{ 'labels.buttons.Export' | translate }}</span>
</button>
<button class="pv-edit-btn" type="button" [routerLink]="['../edit']" *mifosxHasPermission="'UPDATE_LOANPRODUCT'">
<fa-icon icon="edit"></fa-icon><span>{{ 'labels.buttons.Edit' | translate }}</span>
</button>
</div>
</div>
<mifosx-loan-product-summary
[action]="'view'"
[loanProduct]="loanProduct"
[paymentAllocations]="loanProduct.paymentAllocation"
[creditAllocations]="loanProduct.creditAllocation"
[supportedInterestRefundTypes]="loanProduct.supportedInterestRefundTypes"
>
</mifosx-loan-product-summary>
<!-- Summary content (rendered by shared component) -->
<div class="pv-legacy">
<mifosx-loan-product-summary
[action]="'view'"
[loanProduct]="loanProduct"
[useDueForRepaymentsConfigurations]="useDueForRepaymentsConfigurations"
[paymentAllocations]="loanProduct.paymentAllocation"
[creditAllocations]="loanProduct.creditAllocation"
[supportedInterestRefundTypes]="loanProduct.supportedInterestRefundTypes"
>
</mifosx-loan-product-summary>
</div>
</div>
@@ -5,3 +5,13 @@
* 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/.
*/
@use 'assets/styles/product-view' as pv;
@include pv.product-view-styles;
/* Loan-specific amber accent */
:host {
--pv-accent: #b45309;
--pv-accent-l: #fef3c7;
}
@@ -6,59 +6,177 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="container m-b-20 layout-row align-end gap-20px">
<div #buttonCreateRecurringProduct class="in-block">
<button
mat-raised-button
color="primary"
[routerLink]="['create']"
*mifosxHasPermission="'CREATE_RECURRINGDEPOSITPRODUCT'"
matTooltip="{{ 'tooltips.Create Recurring Deposit Product' | translate }}"
matTooltipPosition="above"
>
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Create Recurring Deposit Product' | translate }}
</button>
</div>
</div>
<div class="container">
<div #filter class="layout-row gap-20px">
<mat-form-field appearance="outline" class="flex-fill">
<mat-label>{{ 'labels.inputs.Filter' | translate }}</mat-label>
<i matPrefix class="fas fa-search"></i>
<input
matInput
(keyup)="applyFilter($event.target.value)"
matTooltip="{{ 'tooltips.Add Filters' | translate }}"
matTooltipPosition="left"
/>
</mat-form-field>
<div class="pl-root">
<div class="pl-header">
<div class="pl-header-left">
<div class="pl-header-ic"><fa-icon icon="coins"></fa-icon></div>
<div class="pl-header-text">
<h2>{{ 'labels.heading.Recurring Deposit Products' | translate }}</h2>
<p>Manage recurring-deposit products — instalment schedules, interest rates and charges.</p>
</div>
</div>
<div class="pl-header-right">
<div class="pl-count-chip" matTooltip="Total configured RD products">
<fa-icon icon="layer-group"></fa-icon>
<strong>{{ totalCount }}</strong>
<span>{{ totalCount === 1 ? 'product' : 'products' }}</span>
</div>
<button
class="pl-secondary-btn"
type="button"
[routerLink]="['/products/recurring-deposit-products/base-interest-rates']"
matTooltip="Open the CBS base interest chart"
>
<fa-icon icon="chart-line"></fa-icon>
<span>CBS Base Chart</span>
</button>
<button
#buttonCreateRecurringProduct
class="pl-create-btn"
type="button"
[routerLink]="['create']"
*mifosxHasPermission="'CREATE_RECURRINGDEPOSITPRODUCT'"
matTooltip="{{ 'tooltips.Create Recurring Deposit Product' | translate }}"
>
<fa-icon icon="plus"></fa-icon>
<span>{{ 'labels.buttons.Create Recurring Deposit Product' | translate }}</span>
</button>
</div>
</div>
<div #recurringProductsTable 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 recurringDepositProduct">{{ recurringDepositProduct.name }}</td>
</ng-container>
<div class="pl-kpis">
<div class="pl-kpi pl-kpi--blue">
<div class="pl-kpi-ic"><fa-icon icon="coins"></fa-icon></div>
<div>
<div class="pl-kpi-n">{{ totalCount | number }}</div>
<div class="pl-kpi-l">Total RD Products</div>
</div>
</div>
<div class="pl-kpi pl-kpi--teal">
<div class="pl-kpi-ic"><fa-icon icon="coins"></fa-icon></div>
<div>
<div class="pl-kpi-n">{{ currencyCount | number }}</div>
<div class="pl-kpi-l">{{ currencyCount === 1 ? 'Currency' : 'Currencies' }} in use</div>
</div>
</div>
<div class="pl-kpi pl-kpi--purple">
<div class="pl-kpi-ic"><fa-icon icon="filter"></fa-icon></div>
<div>
<div class="pl-kpi-n">{{ filteredCount | number }}</div>
<div class="pl-kpi-l">Showing after filter</div>
</div>
</div>
</div>
<ng-container matColumnDef="shortName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Short Name' | translate }}</th>
<td mat-cell *matCellDef="let recurringDepositProduct">{{ recurringDepositProduct.shortName }}</td>
</ng-container>
<div class="pl-card">
<div class="pl-toolbar">
<div class="pl-search">
<fa-icon icon="search" class="pl-search-ic"></fa-icon>
<input
#filter
type="text"
(input)="applyFilter($any($event.target).value)"
placeholder="Search by name or short name…"
/>
</div>
</div>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" [routerLink]="[row.id]" class="select-row"></tr>
</table>
@if (totalCount === 0) {
<div class="pl-empty">
<div class="pl-empty-ic"><fa-icon icon="coins"></fa-icon></div>
<h3>No recurring-deposit products yet</h3>
<p>Create your first RD product to enable instalment-savings onboarding.</p>
<button
class="pl-create-btn"
type="button"
[routerLink]="['create']"
*mifosxHasPermission="'CREATE_RECURRINGDEPOSITPRODUCT'"
>
<fa-icon icon="plus"></fa-icon>
<span>{{ 'labels.buttons.Create Recurring Deposit Product' | translate }}</span>
</button>
</div>
} @else {
<div #recurringProductsTable class="pl-table-wrap">
<table mat-table [dataSource]="dataSource" matSort class="pl-table">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let p">
<div class="pl-product-cell">
<div class="pl-product-icon"><fa-icon icon="coins"></fa-icon></div>
<div class="pl-product-meta">
<div class="pl-product-name">{{ p.name }}</div>
@if (p.currency?.code) {
<div class="pl-product-sub">{{ p.currency.code }}</div>
}
</div>
</div>
</td>
</ng-container>
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons></mat-paginator>
<ng-container matColumnDef="shortName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Short Name' | translate }}</th>
<td mat-cell *matCellDef="let p">
<span class="pl-tag">{{ p.shortName }}</span>
</td>
</ng-container>
<ng-container matColumnDef="currency">
<th mat-header-cell *matHeaderCellDef>Currency</th>
<td mat-cell *matCellDef="let p">
@if (p.currency?.code) {
<span class="pl-currency-pill">{{ p.currency.code }}</span>
} @else {
<span class="muted">—</span>
}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="num">Actions</th>
<td mat-cell *matCellDef="let p" class="num">
<button
type="button"
class="pl-action-btn pl-action-btn--view"
[routerLink]="[p.id]"
(click)="$event.stopPropagation()"
matTooltip="View product details"
>
<fa-icon icon="eye"></fa-icon><span>View</span>
</button>
<button
type="button"
class="pl-action-btn pl-action-btn--edit"
[routerLink]="[p.id, 'edit']"
(click)="$event.stopPropagation()"
matTooltip="Edit this product"
*mifosxHasPermission="'UPDATE_RECURRINGDEPOSITPRODUCT'"
>
<fa-icon icon="edit"></fa-icon><span>Edit</span>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" [routerLink]="[row.id]" class="pl-row"></tr>
</table>
@if (filteredCount === 0 && totalCount > 0) {
<div class="pl-no-match">
<fa-icon icon="search"></fa-icon>
<span>No products match your filter.</span>
</div>
}
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" [pageSize]="10" showFirstLastButtons></mat-paginator>
</div>
}
</div>
</div>
<ng-template #templateButtonCreateRecurringProduct let-popover="popover">
<h2>{{ 'labels.heading.Create recurring deposit product' | translate }}</h2>
<p class="mw300">{{ 'labels.text.This option allows you to create new recurring product.' | translate }}</p>
<h2>{{ 'labels.heading.Create Recurring Deposit Product' | translate }}</h2>
<p class="mw300">{{ 'labels.text.This option allows you to create new recurring deposit product.' | translate }}</p>
<div class="layout-row align-end gap-2px responsive-column">
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">
{{ 'labels.buttons.Close' | translate }}
@@ -102,12 +220,7 @@
</ng-template>
<ng-template #templateRecurringProductsTable let-popover="popover">
<h4 class="mw300">
{{ 'labels.heading.List of recurring deposit products in the organization' | translate }}:
<a [href]="'recurringDepositProducts' | documentationLink" target="_blank" rel="noopener noreferrer">{{
'labels.heading.Recurring Deposit Products' | translate
}}</a>
</h4>
<h4 class="mw300">{{ 'labels.heading.List of recurring deposit products in the organization' | translate }}</h4>
<div class="layout-row align-end gap-2px responsive-column">
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">
{{ 'labels.buttons.Close' | translate }}
@@ -6,10 +6,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
table {
width: 100%;
@use 'assets/styles/product-list' as pl;
.select-row:hover {
cursor: pointer;
}
@include pl.product-list-styles;
/* RD-specific purple accent */
:host {
--pl-accent: #6d28d9;
--pl-accent-l: #ede9fe;
}
@@ -73,15 +73,31 @@ export class RecurringDepositProductsComponent implements OnInit, AfterViewInit
private popoverService = inject(PopoverService);
/** Data table data. */
recurringDepositProductData: any;
recurringDepositProductData: any[] = [];
/** Columns to be displayed in recurring deposit products table. */
displayedColumns: string[] = [
'name',
'shortName'
'shortName',
'currency',
'actions'
];
/** Data source for recurring deposit products table. */
dataSource: MatTableDataSource<any>;
filteredCount = 0;
get totalCount(): number {
return this.recurringDepositProductData?.length || 0;
}
get currencyCount(): number {
const set = new Set<string>();
for (const p of this.recurringDepositProductData || []) {
if (p?.currency?.code) set.add(p.currency.code);
}
return set.size;
}
/** Paginator for recurring deposit products table. */
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
/** Sorter for recurring deposit products table. */
@@ -131,6 +147,8 @@ export class RecurringDepositProductsComponent implements OnInit, AfterViewInit
this.dataSource = new MatTableDataSource(this.recurringDepositProductData);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
this.filteredCount = this.totalCount;
this.dataSource.connect().subscribe((rows) => (this.filteredCount = rows.length));
}
/**
@@ -8,6 +8,15 @@
@use 'assets/styles/helper';
@use 'assets/styles/colours' as *;
@use 'assets/styles/product-view' as pv;
@include pv.product-view-styles;
/* RD-specific purple accent */
:host {
--pv-accent: #6d28d9;
--pv-accent-l: #ede9fe;
}
.container {
width: 100%;
@@ -6,44 +6,174 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="container m-b-20 layout-row align-end gap-20px">
<div #buttonSavingProduct class="in-block">
<button mat-raised-button color="primary" [routerLink]="['create']" *mifosxHasPermission="'CREATE_SAVINGSPRODUCT'">
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.text.Create Savings Product' | translate }}
</button>
<div class="sp-root">
<!-- HEADER -->
<div class="sp-header">
<div class="sp-header-left">
<div class="sp-header-ic"><fa-icon icon="piggy-bank"></fa-icon></div>
<div class="sp-header-text">
<h2>Savings Products</h2>
<p>Manage the savings products available to clients — interest rates, balances and accounting rules.</p>
</div>
</div>
<div class="sp-header-right">
<div class="sp-count-chip" matTooltip="Total configured products">
<fa-icon icon="layer-group" class="sp-count-chip-ic"></fa-icon>
<strong>{{ totalCount }}</strong>
<span>{{ totalCount === 1 ? 'product' : 'products' }}</span>
</div>
<button
#buttonSavingProduct
type="button"
class="sp-create-btn"
[routerLink]="['create']"
*mifosxHasPermission="'CREATE_SAVINGSPRODUCT'"
>
<fa-icon icon="plus"></fa-icon>
<span>{{ 'labels.text.Create Savings Product' | translate }}</span>
</button>
</div>
</div>
</div>
<div class="container">
<div #filter class="layout-row">
<mat-form-field appearance="outline" class="flex-fill">
<mat-label>{{ 'labels.inputs.Filter' | translate }}</mat-label>
<i matPrefix class="fas fa-search"></i>
<input matInput (keyup)="applyFilter($event.target.value)" />
</mat-form-field>
<!-- KPI STRIP -->
<div class="sp-kpis">
<div class="sp-kpi sp-kpi--blue">
<div class="sp-kpi-ic"><fa-icon icon="piggy-bank"></fa-icon></div>
<div>
<div class="sp-kpi-n">{{ totalCount | number }}</div>
<div class="sp-kpi-l">Total Savings Products</div>
</div>
</div>
<div class="sp-kpi sp-kpi--teal">
<div class="sp-kpi-ic"><fa-icon icon="coins"></fa-icon></div>
<div>
<div class="sp-kpi-n">{{ currencyCount | number }}</div>
<div class="sp-kpi-l">{{ currencyCount === 1 ? 'Currency' : 'Currencies' }} in use</div>
</div>
</div>
<div class="sp-kpi sp-kpi--purple">
<div class="sp-kpi-ic"><fa-icon icon="filter"></fa-icon></div>
<div>
<div class="sp-kpi-n">{{ filteredCount | number }}</div>
<div class="sp-kpi-l">Showing after filter</div>
</div>
</div>
</div>
<div #savingProductTable 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 savingProduct">{{ savingProduct.name }}</td>
</ng-container>
<!-- BODY CARD -->
<div class="sp-card">
<div class="sp-toolbar">
<div class="sp-search">
<fa-icon icon="search" class="sp-search-ic"></fa-icon>
<input
#filter
type="text"
(input)="applyFilter($any($event.target).value)"
placeholder="Search by name or short name…"
/>
</div>
</div>
@if (totalCount === 0) {
<div class="sp-empty">
<div class="sp-empty-ic"><fa-icon icon="piggy-bank"></fa-icon></div>
<h3>No savings products yet</h3>
<p>Create your first savings product to make it available for client accounts.</p>
<button
type="button"
class="sp-create-btn"
[routerLink]="['create']"
*mifosxHasPermission="'CREATE_SAVINGSPRODUCT'"
>
<fa-icon icon="plus"></fa-icon>
<span>{{ 'labels.text.Create Savings Product' | translate }}</span>
</button>
</div>
} @else {
<div #savingProductTable class="sp-table-wrap">
<table mat-table [dataSource]="dataSource" matSort class="sp-table">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let p">
<div class="sp-product-cell">
<div class="sp-product-icon"><fa-icon icon="piggy-bank"></fa-icon></div>
<div class="sp-product-meta">
<div class="sp-product-name">{{ p.name }}</div>
@if (p.currency?.code) {
<div class="sp-product-sub">
{{ p.currency.code }}<span>·</span
>{{ p.currency.displayLabel || p.currency.name || p.currency.code }}
</div>
}
</div>
</div>
</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 p">
<span class="sp-tag">{{ p.shortName }}</span>
</td>
</ng-container>
<ng-container matColumnDef="currency">
<th mat-header-cell *matHeaderCellDef>Currency</th>
<td mat-cell *matCellDef="let p">
@if (p.currency?.code) {
<span class="sp-currency-pill">{{ p.currency.code }}</span>
} @else {
<span class="muted">—</span>
}
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="num">Actions</th>
<td mat-cell *matCellDef="let p" class="num">
<button
type="button"
class="sp-action-btn sp-action-btn--view"
[routerLink]="[p.id]"
(click)="$event.stopPropagation()"
matTooltip="View product details"
*mifosxHasPermission="'READ_SAVINGSPRODUCT'"
>
<fa-icon icon="eye"></fa-icon>
<span>View</span>
</button>
<button
type="button"
class="sp-action-btn sp-action-btn--edit"
[routerLink]="[p.id, 'edit']"
(click)="$event.stopPropagation()"
matTooltip="Edit this product"
*mifosxHasPermission="'UPDATE_SAVINGSPRODUCT'"
>
<fa-icon icon="edit"></fa-icon>
<span>Edit</span>
</button>
</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 savingProduct">{{ savingProduct.shortName }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" [routerLink]="[row.id]" class="sp-row"></tr>
</table>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" [routerLink]="[row.id]" class="select-row"></tr>
</table>
@if (filteredCount === 0 && totalCount > 0) {
<div class="sp-no-match">
<fa-icon icon="search"></fa-icon>
<span>No products match your filter.</span>
</div>
}
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons></mat-paginator>
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" [pageSize]="10" showFirstLastButtons></mat-paginator>
</div>
}
</div>
</div>
<!-- Config-Wizard popovers (kept) -->
<ng-template #templateButtonSavingProduct let-popover="popover">
<h2>{{ 'labels.text.Create Savings Product' | translate }}</h2>
<p class="mw300">{{ 'labels.text.This option allows you to create new savings product.' | translate }}</p>
@@ -6,10 +6,540 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
table {
width: 100%;
:host {
--sp-navy: #0b1e3d;
--sp-blue: #1d4ed8;
--sp-blue-l: #eff6ff;
--sp-teal: #0e7e6b;
--sp-teal-l: #ccfbf1;
--sp-purple: #6d28d9;
--sp-purple-l: #ede9fe;
--sp-amber: #b45309;
--sp-amber-l: #fef3c7;
--sp-green: #15803d;
--sp-green-l: #dcfce7;
--sp-red: #c0392b;
--sp-red-l: #fee2e2;
--sp-g50: #f8fafc;
--sp-g100: #f1f5f9;
--sp-g200: #e2e8f0;
--sp-g500: #64748b;
--sp-g700: #334155;
--sp-g900: #0f172a;
.select-row:hover {
cursor: pointer;
display: block;
font-family:
'DM Sans',
system-ui,
-apple-system,
sans-serif;
background: var(--sp-g50);
color: var(--sp-g900);
font-size: 13.5px;
}
.sp-root {
padding: 18px 22px;
max-width: 1400px;
margin: 0 auto;
}
/* ───────── HEADER ───────── */
.sp-header {
display: flex;
align-items: center;
gap: 16px;
flex-wrap: wrap;
margin-bottom: 14px;
}
.sp-header-left {
display: flex;
align-items: center;
gap: 14px;
flex: 1;
min-width: 320px;
}
.sp-header-ic {
width: 48px;
height: 48px;
border-radius: 14px;
background: linear-gradient(135deg, var(--sp-blue) 0%, var(--sp-navy) 100%);
color: #fff;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
box-shadow:
0 6px 16px rgb(29 78 216 / 25%),
inset 0 1px 0 rgb(255 255 255 / 22%);
}
.sp-header-text {
h2 {
margin: 0;
font-size: 18px;
font-weight: 700;
color: var(--sp-g900);
letter-spacing: 0.01em;
}
p {
margin: 3px 0 0;
font-size: 12px;
color: var(--sp-g500);
line-height: 1.4;
}
}
.sp-header-right {
display: flex;
align-items: center;
gap: 12px;
}
.sp-count-chip {
display: inline-flex;
align-items: center;
gap: 7px;
padding: 6px 14px 6px 12px;
border-radius: 999px;
background: var(--sp-blue-l);
color: var(--sp-blue);
border: 1px solid #dbeafe;
font-size: 12px;
font-weight: 600;
strong {
font-size: 14px;
font-weight: 700;
color: var(--sp-navy);
}
span {
text-transform: uppercase;
letter-spacing: 0.06em;
font-size: 10.5px;
}
}
.sp-count-chip-ic {
font-size: 11px;
}
.sp-create-btn {
display: inline-flex;
align-items: center;
gap: 8px;
height: 38px;
padding: 0 16px;
border-radius: 10px;
background: linear-gradient(135deg, var(--sp-blue) 0%, var(--sp-navy) 100%);
color: #fff;
border: 0;
font-size: 13px;
font-weight: 700;
letter-spacing: 0.02em;
cursor: pointer;
text-decoration: none;
white-space: nowrap;
box-shadow:
0 6px 16px rgb(29 78 216 / 30%),
inset 0 1px 0 rgb(255 255 255 / 18%);
transition: all 0.16s ease;
font-family: inherit;
fa-icon {
font-size: 12px;
}
&:hover {
transform: translateY(-1px);
box-shadow:
0 10px 22px rgb(29 78 216 / 38%),
inset 0 1px 0 rgb(255 255 255 / 22%);
}
&:active {
transform: translateY(0);
}
}
/* ───────── KPI STRIP ───────── */
.sp-kpis {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 14px;
}
@media (width <= 720px) {
.sp-kpis {
grid-template-columns: 1fr;
}
}
.sp-kpi {
background: #fff;
border: 1px solid var(--sp-g200);
border-radius: 12px;
padding: 12px 16px;
display: flex;
align-items: center;
gap: 14px;
box-shadow: 0 1px 2px rgb(15 23 42 / 4%);
border-left-width: 4px;
}
.sp-kpi-ic {
width: 40px;
height: 40px;
border-radius: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 16px;
flex-shrink: 0;
}
.sp-kpi-n {
font-size: 20px;
font-weight: 700;
color: var(--sp-g900);
line-height: 1.1;
}
.sp-kpi-l {
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--sp-g500);
margin-top: 4px;
}
.sp-kpi--blue {
border-left-color: var(--sp-blue);
.sp-kpi-ic {
background: var(--sp-blue-l);
color: var(--sp-blue);
}
}
.sp-kpi--teal {
border-left-color: var(--sp-teal);
.sp-kpi-ic {
background: var(--sp-teal-l);
color: var(--sp-teal);
}
}
.sp-kpi--purple {
border-left-color: var(--sp-purple);
.sp-kpi-ic {
background: var(--sp-purple-l);
color: var(--sp-purple);
}
}
/* ───────── CARD + TOOLBAR ───────── */
.sp-card {
background: #fff;
border: 1px solid var(--sp-g200);
border-radius: 14px;
overflow: hidden;
box-shadow: 0 1px 2px rgb(15 23 42 / 4%);
}
.sp-toolbar {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
background: #fff;
border-bottom: 1px solid var(--sp-g200);
}
.sp-search {
position: relative;
flex: 1;
max-width: 480px;
input {
width: 100%;
box-sizing: border-box;
padding: 9px 14px 9px 36px;
border: 1px solid var(--sp-g200);
border-radius: 10px;
background: var(--sp-g50);
font-size: 13px;
color: var(--sp-g900);
font-family: inherit;
&::placeholder {
color: #94a3b8;
}
&:focus {
outline: none;
border-color: var(--sp-blue);
background: #fff;
box-shadow: 0 0 0 3px rgb(29 78 216 / 14%);
}
}
}
.sp-search-ic {
position: absolute;
top: 50%;
left: 13px;
transform: translateY(-50%);
color: #94a3b8;
font-size: 13px;
pointer-events: none;
}
/* ───────── TABLE ───────── */
.sp-table-wrap {
overflow-x: auto;
}
.sp-table {
width: 100%;
background: #fff;
th.mat-mdc-header-cell {
background: var(--sp-g50);
color: var(--sp-g700);
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 12px 16px;
border-bottom: 1px solid var(--sp-g200);
white-space: nowrap;
&.num {
text-align: right;
}
}
td.mat-mdc-cell {
padding: 12px 16px;
border-bottom: 1px solid var(--sp-g100);
font-size: 13px;
color: var(--sp-g900);
vertical-align: middle;
&.num {
text-align: right;
}
}
}
.sp-row {
cursor: pointer;
transition: background 0.14s ease;
&:hover {
background: var(--sp-blue-l);
}
}
/* Product Name cell */
.sp-product-cell {
display: flex;
align-items: center;
gap: 12px;
}
.sp-product-icon {
width: 36px;
height: 36px;
border-radius: 10px;
background: var(--sp-blue-l);
color: var(--sp-blue);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
transition: all 0.14s ease;
}
.sp-row:hover .sp-product-icon {
background: var(--sp-blue);
color: #fff;
transform: scale(1.04);
}
.sp-product-meta {
display: flex;
flex-direction: column;
line-height: 1.25;
}
.sp-product-name {
font-size: 13.5px;
font-weight: 600;
color: var(--sp-g900);
}
.sp-product-sub {
font-size: 11px;
color: var(--sp-g500);
margin-top: 2px;
span {
margin: 0 4px;
}
}
/* Short Name pill */
.sp-tag {
display: inline-flex;
align-items: center;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
padding: 3px 9px;
border-radius: 999px;
background: var(--sp-g100);
color: var(--sp-g700);
font-family: 'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace;
}
/* Currency pill */
.sp-currency-pill {
display: inline-flex;
align-items: center;
font-size: 11px;
font-weight: 700;
padding: 3px 9px;
border-radius: 999px;
background: var(--sp-teal-l);
color: var(--sp-teal);
border: 1px solid #99f6e4;
font-family: 'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace;
}
.muted {
color: var(--sp-g500);
}
/* Action buttons */
.sp-action-btn {
display: inline-flex;
align-items: center;
gap: 5px;
height: 30px;
padding: 0 12px;
border-radius: 8px;
border: 1px solid;
font-size: 11.5px;
font-weight: 600;
cursor: pointer;
text-decoration: none;
font-family: inherit;
margin-left: 6px;
transition: all 0.14s ease;
fa-icon {
font-size: 11px;
}
&:first-child {
margin-left: 0;
}
&--view {
background: var(--sp-blue-l);
color: var(--sp-blue);
border-color: #dbeafe;
&:hover {
background: var(--sp-blue);
color: #fff;
border-color: var(--sp-blue);
}
}
&--edit {
background: var(--sp-amber-l);
color: var(--sp-amber);
border-color: #fde68a;
&:hover {
background: var(--sp-amber);
color: #fff;
border-color: var(--sp-amber);
}
}
}
/* No match strip */
.sp-no-match {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 28px 16px;
background: var(--sp-g50);
color: var(--sp-g500);
font-size: 13px;
font-weight: 500;
border-top: 1px dashed var(--sp-g200);
fa-icon {
font-size: 13px;
}
}
/* ───────── EMPTY STATE ───────── */
.sp-empty {
text-align: center;
padding: 50px 24px;
}
.sp-empty-ic {
width: 64px;
height: 64px;
margin: 0 auto 16px;
border-radius: 16px;
background: var(--sp-blue-l);
color: var(--sp-blue);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 26px;
}
.sp-empty h3 {
margin: 0 0 8px;
font-size: 16px;
font-weight: 700;
color: var(--sp-g900);
}
.sp-empty p {
margin: 0 0 20px;
font-size: 13px;
color: var(--sp-g500);
line-height: 1.5;
max-width: 420px;
margin-left: auto;
margin-right: auto;
}
/* ───────── Paginator polish ───────── */
::ng-deep .sp-table-wrap .mat-mdc-paginator {
background: var(--sp-g50);
border-top: 1px solid var(--sp-g200);
font-family: inherit;
}
@@ -24,6 +24,7 @@ import {
MatRow
} from '@angular/material/table';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { MatTooltip } from '@angular/material/tooltip';
/* Custom Services */
import { PopoverService } from '../../configuration-wizard/popover/popover.service';
@@ -50,7 +51,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatHeaderRow,
MatRowDef,
MatRow,
MatPaginator
MatPaginator,
MatTooltip
]
})
export class SavingProductsComponent implements OnInit, AfterViewInit {
@@ -59,13 +61,33 @@ export class SavingProductsComponent implements OnInit, AfterViewInit {
private configurationWizardService = inject(ConfigurationWizardService);
private popoverService = inject(PopoverService);
savingProductsData: any;
savingProductsData: any[] = [];
displayedColumns: string[] = [
'name',
'shortName'
'shortName',
'currency',
'actions'
];
dataSource: MatTableDataSource<any>;
/** Cached count of rows after the current filter is applied. */
filteredCount = 0;
/** Total products as returned by the resolver (filter-independent). */
get totalCount(): number {
return this.savingProductsData?.length || 0;
}
/** Number of distinct currencies in the product list. */
get currencyCount(): number {
const set = new Set<string>();
for (const p of this.savingProductsData || []) {
const code = p?.currency?.code;
if (code) set.add(code);
}
return set.size;
}
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
@ViewChild(MatSort, { static: true }) sort: MatSort;
@@ -94,10 +116,13 @@ export class SavingProductsComponent implements OnInit, AfterViewInit {
this.dataSource = new MatTableDataSource(this.savingProductsData);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
this.filteredCount = this.totalCount;
// Keep the filtered-row count chip live as the user types.
this.dataSource.connect().subscribe((rows) => (this.filteredCount = rows.length));
}
applyFilter(filterValue: string) {
this.dataSource.filter = filterValue.trim().toLowerCase();
this.dataSource.filter = (filterValue || '').trim().toLowerCase();
}
/**
@@ -6,160 +6,443 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
@use 'assets/styles/helper';
@use 'assets/styles/colours' as *;
:host {
--vsp-navy: #0b1e3d;
--vsp-blue: #1d4ed8;
--vsp-blue-l: #eff6ff;
--vsp-teal: #0e7e6b;
--vsp-teal-l: #ccfbf1;
--vsp-purple: #6d28d9;
--vsp-purple-l: #ede9fe;
--vsp-indigo: #3730a3;
--vsp-indigo-l: #e0e7ff;
--vsp-amber: #b45309;
--vsp-amber-l: #fef3c7;
--vsp-green: #15803d;
--vsp-green-l: #dcfce7;
--vsp-red: #c0392b;
--vsp-red-l: #fee2e2;
--vsp-pink: #be185d;
--vsp-pink-l: #fce7f3;
--vsp-g50: #f8fafc;
--vsp-g100: #f1f5f9;
--vsp-g200: #e2e8f0;
--vsp-g500: #64748b;
--vsp-g700: #334155;
--vsp-g900: #0f172a;
.tab-container {
padding: 1%;
margin: 1%;
display: block;
font-family:
'DM Sans',
system-ui,
-apple-system,
sans-serif;
color: var(--vsp-g900);
font-size: 13.5px;
}
.mat-h3 {
margin-top: 10px !important;
font-weight: 300;
color: $status-approved;
}
.interestCarts {
float: right;
}
h2,
h3,
h4 {
margin: 0;
font-weight: 500;
}
table {
width: 100%;
tr.incentives-row {
height: 0;
> td {
padding: 0;
}
.incentives {
overflow: hidden;
}
.mat-card {
border-radius: 0;
> h4 {
font-weight: 500;
margin: 0.5em 0 0;
}
}
}
}
.mat-elevation-z1 {
margin: 1em 0 1.5em;
}
span {
margin: 0.5em 0;
}
mat-divider {
margin: 0 0 0.5em;
}
.margin-t {
margin-top: 1em;
}
.spa-gl-summary,
.spa-other-gl-summary {
.vsp-root {
padding: 4px 4px 24px;
display: flex;
flex-direction: column;
gap: 0.9rem;
margin-top: 1.25rem;
gap: 14px;
}
.spa-gl-summary__header {
/* ───────── HERO ───────── */
.vsp-hero {
background: linear-gradient(135deg, var(--vsp-navy) 0%, var(--vsp-blue) 100%);
border-radius: 14px;
padding: 22px 22px 22px 18px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.25rem 0 0.5rem;
}
.spa-gl-section {
display: flex;
flex-direction: column;
gap: 0.75rem;
padding: 1rem 1.15rem;
border: 1px solid #e3e7f2;
border-radius: 12px;
background: #fff;
}
.spa-gl-section h5 {
font-size: 0.9rem;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
color: #91a0bc;
}
.spa-gl-row {
display: grid;
grid-template-columns: minmax(220px, 40%) 1fr;
gap: 1rem;
align-items: baseline;
padding: 0.2rem 0;
}
.spa-gl-row__label {
color: #1f2f4a;
font-weight: 500;
}
.spa-gl-row__value {
color: #101828;
font-weight: 600;
word-break: break-word;
}
.spa-other-gl-table {
margin-top: 0.5rem;
margin-bottom: 0.75rem;
table-layout: fixed;
width: 100%;
}
.spa-other-gl-table:first-of-type {
margin-top: 0.25rem;
}
.spa-other-gl-summary .mat-elevation-z1 {
border-radius: 10px;
align-items: flex-start;
gap: 18px;
box-shadow:
0 10px 24px rgb(11 30 61 / 18%),
0 4px 10px rgb(11 30 61 / 12%);
color: #fff;
position: relative;
overflow: hidden;
}
.spa-other-gl-table th.mat-mdc-header-cell,
.spa-other-gl-table td.mat-mdc-cell {
padding-left: 1.25rem;
padding-right: 1.25rem;
.vsp-hero::before {
content: '';
position: absolute;
top: -60px;
right: -60px;
width: 240px;
height: 240px;
border-radius: 50%;
background: radial-gradient(circle, rgb(255 255 255 / 8%) 0%, transparent 70%);
pointer-events: none;
}
.spa-other-gl-table th.mat-mdc-header-cell:first-child,
.spa-other-gl-table td.mat-mdc-cell:first-child {
width: 38%;
.vsp-hero-left {
display: flex;
align-items: flex-start;
gap: 16px;
flex: 1;
min-width: 0;
}
.spa-other-gl-table th.mat-mdc-header-cell:last-child,
.spa-other-gl-table td.mat-mdc-cell:last-child {
width: 62%;
word-break: break-word;
.vsp-hero-icon {
width: 56px;
height: 56px;
border-radius: 14px;
background: rgb(255 255 255 / 16%);
border: 1px solid rgb(255 255 255 / 22%);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 24px;
flex-shrink: 0;
}
@media (width <= 768px) {
.spa-gl-row {
.vsp-hero-text {
flex: 1;
min-width: 0;
}
.vsp-hero-eyebrow {
font-size: 10.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.12em;
color: rgb(255 255 255 / 70%);
margin-bottom: 4px;
}
.vsp-hero-title {
margin: 0 0 10px;
font-size: 22px;
font-weight: 700;
letter-spacing: 0.01em;
color: #fff;
line-height: 1.2;
}
.vsp-hero-meta {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.vsp-hero-desc {
margin: 12px 0 0;
font-size: 13px;
color: rgb(255 255 255 / 82%);
line-height: 1.5;
max-width: 720px;
}
.vsp-edit-btn {
display: inline-flex;
align-items: center;
gap: 8px;
height: 38px;
padding: 0 16px;
border-radius: 10px;
background: rgb(255 255 255 / 14%);
color: #fff;
border: 1px solid rgb(255 255 255 / 28%);
font-size: 13px;
font-weight: 700;
letter-spacing: 0.02em;
cursor: pointer;
text-decoration: none;
white-space: nowrap;
font-family: inherit;
flex-shrink: 0;
transition: all 0.16s ease;
fa-icon {
font-size: 12px;
}
&:hover {
background: rgb(255 255 255 / 24%);
border-color: rgb(255 255 255 / 50%);
}
}
/* ───────── PILLS ───────── */
.vsp-pill {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.04em;
text-transform: uppercase;
padding: 4px 10px;
border-radius: 999px;
font-family: inherit;
fa-icon {
font-size: 9.5px;
}
&--code {
background: rgb(255 255 255 / 18%);
color: #fff;
border: 1px solid rgb(255 255 255 / 24%);
font-family: 'JetBrains Mono', ui-monospace, Menlo, Consolas, monospace;
}
&--teal {
background: var(--vsp-teal-l);
color: var(--vsp-teal);
border: 1px solid #99f6e4;
}
&--blue {
background: var(--vsp-blue-l);
color: var(--vsp-blue);
border: 1px solid #dbeafe;
}
&--green {
background: var(--vsp-green-l);
color: var(--vsp-green);
border: 1px solid #bbf7d0;
}
&--amber {
background: var(--vsp-amber-l);
color: var(--vsp-amber);
border: 1px solid #fde68a;
}
&--red {
background: var(--vsp-red-l);
color: var(--vsp-red);
border: 1px solid #fecaca;
}
&--gray {
background: var(--vsp-g100);
color: var(--vsp-g700);
border: 1px solid var(--vsp-g200);
}
}
/* ───────── SECTION ───────── */
.vsp-section {
background: #fff;
border: 1px solid var(--vsp-g200);
border-radius: 12px;
padding: 16px 20px;
box-shadow: 0 1px 2px rgb(15 23 42 / 4%);
}
.vsp-section-h {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 14px;
padding-bottom: 12px;
border-bottom: 1px dashed var(--vsp-g200);
h3 {
margin: 0;
font-size: 14px;
font-weight: 700;
color: var(--vsp-g900);
letter-spacing: 0.02em;
}
}
.vsp-section-ic {
width: 36px;
height: 36px;
border-radius: 10px;
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 14px;
flex-shrink: 0;
&--blue {
background: var(--vsp-blue-l);
color: var(--vsp-blue);
}
&--teal {
background: var(--vsp-teal-l);
color: var(--vsp-teal);
}
&--purple {
background: var(--vsp-purple-l);
color: var(--vsp-purple);
}
&--indigo {
background: var(--vsp-indigo-l);
color: var(--vsp-indigo);
}
&--amber {
background: var(--vsp-amber-l);
color: var(--vsp-amber);
}
&--green {
background: var(--vsp-green-l);
color: var(--vsp-green);
}
&--red {
background: var(--vsp-red-l);
color: var(--vsp-red);
}
&--pink {
background: var(--vsp-pink-l);
color: var(--vsp-pink);
}
}
.vsp-sub-h {
margin: 16px 0 10px;
font-size: 12px;
font-weight: 700;
color: var(--vsp-g700);
text-transform: uppercase;
letter-spacing: 0.06em;
}
/* ───────── KEY/VALUE GRID ───────── */
.vsp-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px 24px;
}
.vsp-grid--3 {
grid-template-columns: repeat(3, 1fr);
}
@media (width <= 900px) {
.vsp-grid,
.vsp-grid--3 {
grid-template-columns: 1fr 1fr;
}
}
@media (width <= 600px) {
.vsp-grid,
.vsp-grid--3 {
grid-template-columns: 1fr;
gap: 0.35rem;
}
}
.vsp-kv {
display: flex;
flex-direction: column;
gap: 3px;
padding: 8px 0;
border-bottom: 1px dashed var(--vsp-g100);
&--span {
grid-column: 1 / -1;
}
}
.vsp-k {
font-size: 10.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--vsp-g500);
}
.vsp-v {
font-size: 13.5px;
color: var(--vsp-g900);
font-weight: 500;
word-break: break-word;
strong {
font-weight: 700;
}
}
.vsp-v--accent {
font-size: 18px;
font-weight: 700;
color: var(--vsp-blue);
letter-spacing: 0.01em;
}
.vsp-suffix {
font-size: 12px;
font-weight: 600;
color: var(--vsp-g500);
margin-left: 4px;
}
/* ───────── FEATURE GRID (account facilities) ───────── */
.vsp-feature {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
border-radius: 8px;
background: var(--vsp-g50);
border: 1px solid var(--vsp-g200);
font-size: 12.5px;
font-weight: 600;
color: var(--vsp-g700);
fa-icon {
font-size: 12px;
color: var(--vsp-g500);
}
&.is-on {
background: var(--vsp-green-l);
border-color: #bbf7d0;
color: var(--vsp-green);
fa-icon {
color: var(--vsp-green);
}
}
}
/* ───────── TABLES ───────── */
.vsp-table-wrap {
overflow-x: auto;
border: 1px solid var(--vsp-g200);
border-radius: 10px;
margin-top: 4px;
}
.vsp-table {
width: 100%;
background: #fff;
th.mat-mdc-header-cell {
background: var(--vsp-g50);
color: var(--vsp-g700);
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
padding: 11px 14px;
border-bottom: 1px solid var(--vsp-g200);
white-space: nowrap;
}
td.mat-mdc-cell {
padding: 10px 14px;
border-bottom: 1px solid var(--vsp-g100);
font-size: 12.5px;
color: var(--vsp-g900);
}
tbody tr:last-child td.mat-mdc-cell {
border-bottom: 0;
}
}
@@ -10,7 +10,6 @@ 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,
@@ -36,7 +35,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
MatDivider,
MatTable,
MatColumnDef,
MatHeaderCellDef,
@@ -6,51 +6,164 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="container m-b-20 layout-row align-end gap-20px">
<div #buttonCreateShareProduct class="in-block">
<button mat-raised-button color="primary" [routerLink]="['create']" *mifosxHasPermission="'CREATE_SHAREPRODUCT'">
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Create Share Product' | translate }}
</button>
</div>
</div>
<div class="container">
<div #filter class="layout-row">
<mat-form-field appearance="outline" class="flex-fill">
<mat-label>{{ 'labels.inputs.Filter' | translate }}</mat-label>
<i matPrefix class="fas fa-search"></i>
<input matInput (keyup)="applyFilter($event.target.value)" />
</mat-form-field>
<div class="pl-root">
<div class="pl-header">
<div class="pl-header-left">
<div class="pl-header-ic"><fa-icon icon="coins"></fa-icon></div>
<div class="pl-header-text">
<h2>{{ 'labels.heading.Share Products' | translate }}</h2>
<p>Manage share / equity products — capital pools and dividend rules.</p>
</div>
</div>
<div class="pl-header-right">
<div class="pl-count-chip" matTooltip="Total configured share products">
<fa-icon icon="layer-group"></fa-icon>
<strong>{{ totalCount }}</strong>
<span>{{ totalCount === 1 ? 'product' : 'products' }}</span>
</div>
<button
#buttonCreateShareProduct
class="pl-create-btn"
type="button"
[routerLink]="['create']"
*mifosxHasPermission="'CREATE_SHAREPRODUCT'"
>
<fa-icon icon="plus"></fa-icon>
<span>{{ 'labels.buttons.Create Share Product' | translate }}</span>
</button>
</div>
</div>
<div #shareProductsTable 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 shareProduct">{{ shareProduct.name }}</td>
</ng-container>
<div class="pl-kpis">
<div class="pl-kpi pl-kpi--blue">
<div class="pl-kpi-ic"><fa-icon icon="coins"></fa-icon></div>
<div>
<div class="pl-kpi-n">{{ totalCount | number }}</div>
<div class="pl-kpi-l">Total Share Products</div>
</div>
</div>
<div class="pl-kpi pl-kpi--teal">
<div class="pl-kpi-ic"><fa-icon icon="layer-group"></fa-icon></div>
<div>
<div class="pl-kpi-n">{{ totalSharesAggregate | number }}</div>
<div class="pl-kpi-l">Total Shares (all products)</div>
</div>
</div>
<div class="pl-kpi pl-kpi--purple">
<div class="pl-kpi-ic"><fa-icon icon="filter"></fa-icon></div>
<div>
<div class="pl-kpi-n">{{ filteredCount | number }}</div>
<div class="pl-kpi-l">Showing after filter</div>
</div>
</div>
</div>
<ng-container matColumnDef="shortName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Short Name' | translate }}</th>
<td mat-cell *matCellDef="let shareProduct">{{ shareProduct.shortName }}</td>
</ng-container>
<div class="pl-card">
<div class="pl-toolbar">
<div class="pl-search">
<fa-icon icon="search" class="pl-search-ic"></fa-icon>
<input
#filter
type="text"
(input)="applyFilter($any($event.target).value)"
placeholder="Search by name or short name…"
/>
</div>
</div>
<ng-container matColumnDef="totalShares">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Total Shares' | translate }}</th>
<td mat-cell *matCellDef="let shareProduct">{{ shareProduct.totalShares }}</td>
</ng-container>
@if (totalCount === 0) {
<div class="pl-empty">
<div class="pl-empty-ic"><fa-icon icon="coins"></fa-icon></div>
<h3>No share products yet</h3>
<p>Create your first share product to enable share-capital subscriptions.</p>
<button
class="pl-create-btn"
type="button"
[routerLink]="['create']"
*mifosxHasPermission="'CREATE_SHAREPRODUCT'"
>
<fa-icon icon="plus"></fa-icon>
<span>{{ 'labels.buttons.Create Share Product' | translate }}</span>
</button>
</div>
} @else {
<div #shareProductsTable class="pl-table-wrap">
<table mat-table [dataSource]="dataSource" matSort class="pl-table">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let p">
<div class="pl-product-cell">
<div class="pl-product-icon"><fa-icon icon="coins"></fa-icon></div>
<div class="pl-product-meta">
<div class="pl-product-name">{{ p.name }}</div>
@if (p.currency?.code) {
<div class="pl-product-sub">{{ p.currency.code }}</div>
}
</div>
</div>
</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>
<ng-container matColumnDef="shortName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Short Name' | translate }}</th>
<td mat-cell *matCellDef="let p">
<span class="pl-tag">{{ p.shortName }}</span>
</td>
</ng-container>
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons></mat-paginator>
<ng-container matColumnDef="totalShares">
<th mat-header-cell *matHeaderCellDef mat-sort-header class="num">
{{ 'labels.inputs.Total Shares' | translate }}
</th>
<td mat-cell *matCellDef="let p" class="num">
<strong>{{ p.totalShares | number }}</strong>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef class="num">Actions</th>
<td mat-cell *matCellDef="let p" class="num">
<button
type="button"
class="pl-action-btn pl-action-btn--view"
[routerLink]="[p.id]"
(click)="$event.stopPropagation()"
matTooltip="View product details"
>
<fa-icon icon="eye"></fa-icon><span>View</span>
</button>
<button
type="button"
class="pl-action-btn pl-action-btn--edit"
[routerLink]="[p.id, 'edit']"
(click)="$event.stopPropagation()"
matTooltip="Edit this product"
*mifosxHasPermission="'UPDATE_SHAREPRODUCT'"
>
<fa-icon icon="edit"></fa-icon><span>Edit</span>
</button>
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" [routerLink]="[row.id]" class="pl-row"></tr>
</table>
@if (filteredCount === 0 && totalCount > 0) {
<div class="pl-no-match">
<fa-icon icon="search"></fa-icon>
<span>No products match your filter.</span>
</div>
}
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" [pageSize]="10" showFirstLastButtons></mat-paginator>
</div>
}
</div>
</div>
<ng-template #templateButtonCreateShareProduct let-popover="popover">
<h2>{{ 'labels.buttons.Create Share Product' | translate }}</h2>
<h2>{{ 'labels.heading.Create Share Product' | translate }}</h2>
<p class="mw300">{{ 'labels.text.This option allows you to create new share product.' | translate }}</p>
<div class="layout-row align-end gap-2px responsive-column">
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">
@@ -93,12 +206,7 @@
</ng-template>
<ng-template #templateShareProductsTable let-popover="popover">
<h4 class="mw300">
{{ 'labels.heading.List of share products in the organization' | translate }}:
<a [href]="'shareProducts' | documentationLink" target="_blank" rel="noopener noreferrer">{{
'labels.text.Share Products' | translate
}}</a>
</h4>
<h4 class="mw300">{{ 'labels.heading.List of share products in the organization' | translate }}</h4>
<div class="layout-row align-end gap-2px responsive-column">
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">
{{ 'labels.buttons.Close' | translate }}
@@ -6,10 +6,12 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
table {
width: 100%;
@use 'assets/styles/product-list' as pl;
.select-row:hover {
cursor: pointer;
}
@include pl.product-list-styles;
/* Share-specific pink accent */
:host {
--pl-accent: #be185d;
--pl-accent-l: #fce7f3;
}
@@ -29,6 +29,7 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { PopoverService } from '../../configuration-wizard/popover/popover.service';
import { ConfigurationWizardService } from '../../configuration-wizard/configuration-wizard.service';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatTooltip } from '@angular/material/tooltip';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
@@ -50,7 +51,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatHeaderRow,
MatRowDef,
MatRow,
MatPaginator
MatPaginator,
MatTooltip
]
})
export class ShareProductsComponent implements OnInit, AfterViewInit {
@@ -59,14 +61,25 @@ export class ShareProductsComponent implements OnInit, AfterViewInit {
private configurationWizardService = inject(ConfigurationWizardService);
private popoverService = inject(PopoverService);
shareProductsData: any;
shareProductsData: any[] = [];
displayedColumns: string[] = [
'name',
'shortName',
'totalShares'
'totalShares',
'actions'
];
dataSource: MatTableDataSource<any>;
filteredCount = 0;
get totalCount(): number {
return this.shareProductsData?.length || 0;
}
get totalSharesAggregate(): number {
return (this.shareProductsData || []).reduce((s, p) => s + (Number(p?.totalShares) || 0), 0);
}
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
@ViewChild(MatSort, { static: true }) sort: MatSort;
@@ -95,10 +108,12 @@ export class ShareProductsComponent implements OnInit, AfterViewInit {
this.dataSource = new MatTableDataSource(this.shareProductsData);
this.dataSource.paginator = this.paginator;
this.dataSource.sort = this.sort;
this.filteredCount = this.totalCount;
this.dataSource.connect().subscribe((rows) => (this.filteredCount = rows.length));
}
applyFilter(filterValue: string) {
this.dataSource.filter = filterValue.trim().toLowerCase();
this.dataSource.filter = (filterValue || '').trim().toLowerCase();
}
/**
@@ -6,243 +6,255 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="layout-row align-end gap-2px responsive-column container m-b-20 m-t-10">
<button mat-raised-button color="primary" [routerLink]="['../edit']" *mifosxHasPermission="'UPDATE_SHAREPRODUCT'">
<fa-icon icon="edit" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Edit' | translate }}
</button>
<button
mat-raised-button
color="primary"
[routerLink]="['../dividends']"
*mifosxHasPermission="'UPDATE_SHAREPRODUCT'"
>
<fa-icon icon="hand-holding-usd" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Dividends' | translate }}
</button>
</div>
<div class="tab-container">
<!-- <mat-card>
<mat-card-content> -->
<!-- <div class="responsive-column"> -->
<h2 class="mat-h2 flex-100">{{ shareProduct.name }}</h2>
<mat-divider [inset]="true"></mat-divider>
<h3 class="mat-h3 flex-fill">{{ 'labels.inputs.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">{{ shareProduct.shortName }}</span>
</div>
@if (shareProduct.description) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Description' | translate }}:</span>
<span class="flex-60">{{ shareProduct.description }}</span>
<div class="pv-root">
<!-- HERO -->
<div class="pv-hero">
<div class="pv-hero-left">
<div class="pv-hero-icon"><fa-icon icon="coins"></fa-icon></div>
<div class="pv-hero-text">
<div class="pv-hero-eyebrow">Share Product</div>
<h2 class="pv-hero-title">{{ shareProduct.name }}</h2>
<div class="pv-hero-meta">
<span class="pv-pill pv-pill--code">{{ shareProduct.shortName }}</span>
@if (shareProduct.currency?.code) {
<span class="pv-pill pv-pill--teal">{{ shareProduct.currency.code }}</span>
}
@if (shareProduct.totalShares !== undefined && shareProduct.totalShares !== null) {
<span class="pv-pill pv-pill--blue">{{ shareProduct.totalShares | number }} shares</span>
}
</div>
@if (shareProduct.description) {
<p class="pv-hero-desc">{{ shareProduct.description }}</p>
}
</div>
</div>
}
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.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">{{ shareProduct.currency.name }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Decimal Places' | translate }}:</span>
<span class="flex-60">{{ shareProduct.currency.decimalPlaces }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Currency in multiples of' | translate }}:</span>
<span class="flex-60">{{ shareProduct.currency.inMultiplesOf }}</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.Total Number of Shares' | translate }}:</span>
<span class="flex-60">{{ shareProduct.totalShares | formatNumber }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Shares to be Issued' | translate }}:</span>
<span class="flex-60">{{ shareProduct.totalSharesIssued | formatNumber }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Nominal/Unit Price' | translate }}:</span>
<span class="flex-60">{{ shareProduct.unitPrice | formatNumber }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Capital Value' | translate }}:</span>
<span class="flex-60">{{ shareProduct.shareCapital | formatNumber }}</span>
</div>
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Settings' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Shares per Client' | translate }}:</span>
<span class="flex-60">{{
shareProduct.nominalShares +
' (Min: ' +
(shareProduct.minimumShares ? shareProduct.minimumShares : shareProduct.nominalShares) +
', Max: ' +
(shareProduct.maximumShares ? shareProduct.maximumShares : shareProduct.nominalShares) +
')'
}}</span>
</div>
@if (shareProduct.minimumActivePeriod) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Minimum Active Period' | translate }}:</span>
<span class="flex-60"
>{{ shareProduct.minimumActivePeriod }}
{{ shareProduct.minimumActivePeriodForDividendsTypeEnum.value | translateKey: 'catalogs' }}</span
<div style="display: flex; gap: 10px; flex-shrink: 0">
<button
class="pv-edit-btn"
type="button"
[routerLink]="['../dividends']"
*mifosxHasPermission="'UPDATE_SHAREPRODUCT'"
>
<fa-icon icon="hand-holding-usd"></fa-icon><span>{{ 'labels.buttons.Dividends' | translate }}</span>
</button>
<button class="pv-edit-btn" type="button" [routerLink]="['../edit']" *mifosxHasPermission="'UPDATE_SHAREPRODUCT'">
<fa-icon icon="edit"></fa-icon><span>{{ 'labels.buttons.Edit' | translate }}</span>
</button>
</div>
}
</div>
<div class="pv-legacy">
<h3 class="mat-h3 flex-fill">{{ 'labels.inputs.Details' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
@if (shareProduct.lockinPeriod) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Lock-in Period' | translate }}:</span>
<span class="flex-60"
>{{ shareProduct.lockinPeriod }} {{ shareProduct.lockPeriodTypeEnum.value | translateKey: 'catalogs' }}</span
>
<span class="flex-40">{{ 'labels.inputs.Short Name' | translate }}:</span>
<span class="flex-60">{{ shareProduct.shortName }}</span>
</div>
}
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Allow dividends for inactive clients' | translate }}:</span>
<span class="flex-60">{{ shareProduct.allowDividendCalculationForInactiveClients | yesNo }}</span>
@if (shareProduct.description) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Description' | translate }}:</span>
<span class="flex-60">{{ shareProduct.description }}</span>
</div>
}
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.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">{{ shareProduct.currency.name }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Decimal Places' | translate }}:</span>
<span class="flex-60">{{ shareProduct.currency.decimalPlaces }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Currency in multiples of' | translate }}:</span>
<span class="flex-60">{{ shareProduct.currency.inMultiplesOf }}</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.Total Number of Shares' | translate }}:</span>
<span class="flex-60">{{ shareProduct.totalShares | formatNumber }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Shares to be Issued' | translate }}:</span>
<span class="flex-60">{{ shareProduct.totalSharesIssued | formatNumber }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Nominal/Unit Price' | translate }}:</span>
<span class="flex-60">{{ shareProduct.unitPrice | formatNumber }}</span>
</div>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Capital Value' | translate }}:</span>
<span class="flex-60">{{ shareProduct.shareCapital | formatNumber }}</span>
</div>
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Settings' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Shares per Client' | translate }}:</span>
<span class="flex-60">{{
shareProduct.nominalShares +
' (Min: ' +
(shareProduct.minimumShares ? shareProduct.minimumShares : shareProduct.nominalShares) +
', Max: ' +
(shareProduct.maximumShares ? shareProduct.maximumShares : shareProduct.nominalShares) +
')'
}}</span>
</div>
@if (shareProduct.minimumActivePeriod) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Minimum Active Period' | translate }}:</span>
<span class="flex-60"
>{{ shareProduct.minimumActivePeriod }}
{{ shareProduct.minimumActivePeriodForDividendsTypeEnum.value | translateKey: 'catalogs' }}</span
>
</div>
}
@if (shareProduct.lockinPeriod) {
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Lock-in Period' | translate }}:</span>
<span class="flex-60"
>{{ shareProduct.lockinPeriod }} {{ shareProduct.lockPeriodTypeEnum.value | translateKey: 'catalogs' }}</span
>
</div>
}
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Allow dividends for inactive clients' | translate }}:</span>
<span class="flex-60">{{ shareProduct.allowDividendCalculationForInactiveClients | yesNo }}</span>
</div>
@if (shareProduct.marketPrice?.length) {
<div class="layout-row-wrap responsive-column flex-fill">
<h3 class="mat-h3 flex-fill">{{ 'labels.inputs.Market Price Period' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<table class="flex-fill mat-elevation-z1" mat-table [dataSource]="shareProduct.marketPrice">
<ng-container matColumnDef="fromDate">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.From Date' | translate }}</th>
<td mat-cell *matCellDef="let marketPrice">
{{ marketPrice.fromDate | dateFormat }}
</td>
</ng-container>
<ng-container matColumnDef="shareValue">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Nominal/Unit Price' | translate }}</th>
<td mat-cell *matCellDef="let marketPrice">
{{ marketPrice.shareValue | formatNumber }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="marketPriceDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: marketPriceDisplayedColumns"></tr>
</table>
</div>
}
@if (shareProduct.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]="shareProduct.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 | numberFormat }}
</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>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Type' | translate }}:</span>
<span class="flex-60">{{
'labels.accounting.' + getAccountingRuleName(shareProduct.accountingRule.value) | translate
}}</span>
</div>
@if (shareProduct.accountingRule.id === 2) {
<div class="layout-row-wrap responsive-column flex-fill">
<h4 class="mat-h4 flex-fill">{{ 'labels.heading.Assets' | translate }}</h4>
<div class="flex-fill">
<mifosx-gl-account-display
class="flex-100"
[accountTitle]="'Share reference'"
[glAccount]="shareProduct.accountingMappings.shareReferenceId"
[withTitle]="'40%'"
>
</mifosx-gl-account-display>
</div>
<h4 class="mat-h4 flex-fill">{{ 'labels.heading.Liabilities' | translate }}</h4>
<div class="flex-fill">
<mifosx-gl-account-display
class="flex-100"
[accountTitle]="'Share suspense control'"
[glAccount]="shareProduct.accountingMappings.shareSuspenseId"
[withTitle]="'40%'"
>
</mifosx-gl-account-display>
</div>
<h4 class="mat-h4 flex-fill">{{ 'labels.heading.Share Equity' | translate }}</h4>
<div class="flex-fill">
<mifosx-gl-account-display
class="flex-100"
[accountTitle]="'Equity'"
[glAccount]="shareProduct.accountingMappings.shareEquityId"
[withTitle]="'40%'"
>
</mifosx-gl-account-display>
</div>
<h4 class="mat-h4 flex-fill">{{ 'labels.heading.Income' | translate }}</h4>
<div class="flex-fill">
<mifosx-gl-account-display
class="flex-100"
[accountTitle]="'Income from fees'"
[glAccount]="shareProduct.accountingMappings.incomeFromFeeAccountId"
[withTitle]="'40%'"
>
</mifosx-gl-account-display>
</div>
</div>
}
</div>
@if (shareProduct.marketPrice?.length) {
<div class="layout-row-wrap responsive-column flex-fill">
<h3 class="mat-h3 flex-fill">{{ 'labels.inputs.Market Price Period' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<table class="flex-fill mat-elevation-z1" mat-table [dataSource]="shareProduct.marketPrice">
<ng-container matColumnDef="fromDate">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.From Date' | translate }}</th>
<td mat-cell *matCellDef="let marketPrice">
{{ marketPrice.fromDate | dateFormat }}
</td>
</ng-container>
<ng-container matColumnDef="shareValue">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Nominal/Unit Price' | translate }}</th>
<td mat-cell *matCellDef="let marketPrice">
{{ marketPrice.shareValue | formatNumber }}
</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="marketPriceDisplayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: marketPriceDisplayedColumns"></tr>
</table>
</div>
}
@if (shareProduct.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]="shareProduct.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 | numberFormat }}
</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>
<div class="flex-fill layout-row">
<span class="flex-40">{{ 'labels.inputs.Type' | translate }}:</span>
<span class="flex-60">{{
'labels.accounting.' + getAccountingRuleName(shareProduct.accountingRule.value) | translate
}}</span>
</div>
@if (shareProduct.accountingRule.id === 2) {
<div class="layout-row-wrap responsive-column flex-fill">
<h4 class="mat-h4 flex-fill">{{ 'labels.heading.Assets' | translate }}</h4>
<div class="flex-fill">
<mifosx-gl-account-display
class="flex-100"
[accountTitle]="'Share reference'"
[glAccount]="shareProduct.accountingMappings.shareReferenceId"
[withTitle]="'40%'"
>
</mifosx-gl-account-display>
</div>
<h4 class="mat-h4 flex-fill">{{ 'labels.heading.Liabilities' | translate }}</h4>
<div class="flex-fill">
<mifosx-gl-account-display
class="flex-100"
[accountTitle]="'Share suspense control'"
[glAccount]="shareProduct.accountingMappings.shareSuspenseId"
[withTitle]="'40%'"
>
</mifosx-gl-account-display>
</div>
<h4 class="mat-h4 flex-fill">{{ 'labels.heading.Share Equity' | translate }}</h4>
<div class="flex-fill">
<mifosx-gl-account-display
class="flex-100"
[accountTitle]="'Equity'"
[glAccount]="shareProduct.accountingMappings.shareEquityId"
[withTitle]="'40%'"
>
</mifosx-gl-account-display>
</div>
<h4 class="mat-h4 flex-fill">{{ 'labels.heading.Income' | translate }}</h4>
<div class="flex-fill">
<mifosx-gl-account-display
class="flex-100"
[accountTitle]="'Income from fees'"
[glAccount]="shareProduct.accountingMappings.incomeFromFeeAccountId"
[withTitle]="'40%'"
>
</mifosx-gl-account-display>
</div>
</div>
}
<!-- </div> -->
<!-- </mat-card-content>
</mat-card> -->
</div>
@@ -8,6 +8,15 @@
@use 'assets/styles/helper';
@use 'assets/styles/colours' as *;
@use 'assets/styles/product-view' as pv;
@include pv.product-view-styles;
/* Share-specific pink accent */
:host {
--pv-accent: #be185d;
--pv-accent-l: #fce7f3;
}
.tab-container {
padding: 1%;