feat: letter-mgmt module + digital UPI/IMPS/ATM-POS reports + header redesign

letter-mgmt (new module):
- Notice page: overdue loan list with filters, KPI strip, bulk actions,
  call disposition logging (Promise-to-Pay, Not Reachable, etc.), Speed
  Post dispatch dialog with consignment + post-office tracking,
  PDF preview + download, call-contact dialog with primary / alt /
  family / nominee numbers and per-contact disposition history.
- Certificates: Loan Interest, Share, Balance, Form 60 (no-PAN flow),
  Form 15G/H (auto age-based), Hypothecation, NOC - each with shared
  preview dialog (six tone variants) and jsPDF generator using a
  common letterhead / summary-grid / signature-seal helper.

digital:
- IMPS module: report (P2A/P2P/Aadhaar tabs) + entry form with
  MMID/mobile lookup, NPCI switch resolve-to-A/C+IFSC BANL flow,
  sender autocomplete, hide sender field for P2P/Aadhaar, charges +
  18% GST + waive checkbox, per-sender today-only history dialog.
- UPI report with app-mix tab and full transaction filtering.
- ATM/POS/ECOM hub with three ticket cards routing to today/status/cycle.
- NEFT/RTGS entry: sender-account autocomplete, charges + GST + waive,
  field reorder (sender before amount), NEFT cap / RTGS floor
  validation, History button opening per-sender today's transactions.
- NEFT/RTGS report: horizontal-bar trend leaderboard replacing tall bars.

shell + theme:
- Header redesigned with IBM Plex Sans 12/500 typography, 11-column grid
  per row, top-accent hover indicator, bordered cells via grid-gap.
- Live KPI ticker below menu (CASH, TXN TODAY, CLEARING IN/OUT, NPA,
  CASA, CD RATIO, RBI CIRC, REPO) with marquee scroll, pause-on-hover,
  brand-blue gradient, amber bottom strip, pulsing red LIVE chip.
- Sidebar 'KYC Management' -> 'KYC & ReKYC Management'.
- Letter Mgmt / certificate menu items wired with green icon hints for
  items that have a working page.

access-control:
- MFA card layout compacted; method header inlines back button + title.

icons:
- Register faTruck/TruckFast, faEllipsisV/H, faCommentDots.
- Add IBM Plex Sans Google Fonts import to index.html.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-05-19 10:09:06 +05:30
co-authored by Claude Opus 4.7
parent 4322f37c6d
commit 325228c514
138 changed files with 27473 additions and 3997 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));
}
/**
@@ -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,26 +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"
[useDueForRepaymentsConfigurations]="useDueForRepaymentsConfigurations"
[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,71 +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 class="in-block">
<button
mat-stroked-button
color="primary"
[routerLink]="['/products/recurring-deposit-products/base-interest-rates']"
matTooltip="Open the CBS base interest chart"
matTooltipPosition="above"
>
<fa-icon icon="chart-line" class="m-r-10"></fa-icon>
CBS Base Chart
</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 }}
@@ -114,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%;