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:
co-authored by
Claude Opus 4.7
parent
4322f37c6d
commit
325228c514
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+576
-562
File diff suppressed because it is too large
Load Diff
+17
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+494
-501
File diff suppressed because it is too large
Load Diff
+9
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+54
-22
@@ -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>
|
||||
|
||||
+10
@@ -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;
|
||||
}
|
||||
|
||||
+163
-62
@@ -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 }}
|
||||
|
||||
+7
-5
@@ -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;
|
||||
}
|
||||
|
||||
+20
-2
@@ -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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+635
-608
File diff suppressed because it is too large
Load Diff
+9
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+624
-515
File diff suppressed because it is too large
Load Diff
+420
-137
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
-2
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+243
-231
@@ -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>
|
||||
|
||||
+9
@@ -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%;
|
||||
|
||||
Reference in New Issue
Block a user