share certificate and share product maker checker

This commit is contained in:
2026-07-27 13:57:49 +05:30
parent d0536dbb11
commit 0c99dc3686
47 changed files with 2790 additions and 245 deletions
@@ -0,0 +1,174 @@
<!--
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="rsad-root">
@if (loading) {
<div class="rsad-state">
<fa-icon icon="spinner" class="rsad-state-ic fa-spin"></fa-icon>
Loading real share accounts&hellip;
</div>
} @else if (loadError) {
<div class="rsad-state rsad-state--error">
<fa-icon icon="exclamation-triangle" class="rsad-state-ic"></fa-icon>
Could not load share accounts from the server.
<button type="button" class="cert-clear" (click)="retry()"><fa-icon icon="redo"></fa-icon> Retry</button>
</div>
} @else if (rows.length === 0) {
<div class="rsad-state">
<fa-icon icon="info-circle" class="rsad-state-ic"></fa-icon>
No share accounts found in m_share_account yet.
</div>
} @else {
<!-- KPI STRIP -->
<div class="cert-kpis">
<div class="cert-kpi cert-kpi--blue">
<fa-icon icon="certificate" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">{{ filteredRows.length | number }}</div>
<div class="cert-kpi-l">Certificates</div>
</div>
</div>
<div class="cert-kpi cert-kpi--teal">
<fa-icon icon="cube" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">{{ totalShares() | number }}</div>
<div class="cert-kpi-l">Shares Held</div>
</div>
</div>
<div class="cert-kpi cert-kpi--green">
<fa-icon icon="coins" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">₹{{ totalShareValue() | number: '1.0-0' }}</div>
<div class="cert-kpi-l">Total Face Value</div>
</div>
</div>
</div>
<!-- FILTERS -->
<div class="cert-filters">
<div class="cert-filter">
<label>Certificate / Folio / Member / PAN</label>
<div class="cert-input-ic">
<fa-icon icon="search"></fa-icon>
<input type="text" [(ngModel)]="search" placeholder="e.g. member name" />
</div>
</div>
<div class="cert-filter">
<label>Share class</label>
<select [(ngModel)]="classFilter">
<option value="">All classes</option>
@for (o of classOptions; track o.value) {
<option [value]="o.value">{{ o.label }}</option>
}
</select>
</div>
<div class="cert-filter">
<label>Branch</label>
<select [(ngModel)]="branchFilter">
<option value="">All branches</option>
@for (b of branchOptions; track b) {
<option [value]="b">{{ b }}</option>
}
</select>
</div>
@if (search || classFilter || branchFilter) {
<button type="button" class="cert-clear" (click)="clearFilters()">
<fa-icon icon="times"></fa-icon> Clear
</button>
}
</div>
<!-- TABLE -->
<div class="cert-card">
<div class="cert-table-wrap">
<table class="cert-table">
<thead>
<tr>
<th>Member · Folio</th>
<th>Certificate No.</th>
<th>Class</th>
<th>Branch</th>
<th class="num">Shares</th>
<th class="num">Face Value</th>
<th class="num">Total</th>
<th>Share Nos.</th>
<th>Issued</th>
<th class="col-actions">Actions</th>
</tr>
</thead>
<tbody>
@for (row of filteredRows; track row.record.certificateNo) {
<tr>
<td>
<div class="cert-cust-name">{{ row.record.memberName }}</div>
<div class="mono small muted">
{{ row.record.memberId }} · Folio <span class="cert-folio">{{ row.record.folioNo }}</span> · PAN
{{ row.record.memberPan }}
</div>
</td>
<td class="mono">{{ row.record.certificateNo }}</td>
<td>
<span
class="cert-pill"
[class.cert-pill--blue]="row.record.shareClass === 'ORDINARY'"
[class.cert-pill--amber]="row.record.shareClass === 'PREFERENCE'"
>
{{ classLabel(row.record.shareClass) }}
</span>
</td>
<td class="small">{{ row.record.branch || '—' }}</td>
<td class="num">
<strong class="cert-shares">{{ row.record.shareCount | number }}</strong>
</td>
<td class="num">₹{{ row.record.faceValue | number: '1.0-0' }}</td>
<td class="num">
<strong>₹{{ row.record.shareCount * row.record.faceValue | number: '1.0-0' }}</strong>
</td>
<td class="mono small">
{{ row.record.shareNoFrom | number }}&ndash;{{ row.record.shareNoTo | number }}
</td>
<td class="small">{{ row.record.issueDate | date: 'd MMM y' }}</td>
<td class="col-actions">
<div class="cert-row-actions">
<button
type="button"
class="cert-icon-btn primary"
(click)="viewCertificate(row)"
matTooltip="View / Print certificate"
>
<fa-icon icon="eye"></fa-icon>
</button>
<button
type="button"
class="cert-icon-btn"
(click)="downloadCertificate(row)"
matTooltip="Download PDF"
>
<fa-icon icon="download"></fa-icon>
</button>
</div>
</td>
</tr>
}
@if (filteredRows.length === 0) {
<tr>
<td colspan="10" class="cert-empty">
<fa-icon icon="info-circle"></fa-icon>
No share accounts match the current filters.
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
</div>
@@ -0,0 +1,25 @@
/**
* 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/.
*/
.rsad-state {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
padding: 48px 20px;
color: var(--cert-g500);
font-size: 13.5px;
&--error {
color: var(--cert-red);
}
}
.rsad-state-ic {
font-size: 16px;
}
@@ -0,0 +1,312 @@
/**
* 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, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatDialog } from '@angular/material/dialog';
import { MatTooltip } from '@angular/material/tooltip';
import { forkJoin, of, Observable } from 'rxjs';
import { catchError, map, shareReplay, switchMap } from 'rxjs/operators';
import { SharesService } from '../../../../shares/shares.service';
import { ClientsService } from '../../../../clients/clients.service';
import { ShareCertificateDialogComponent } from '../../../../shares/shares-account-view/share-certificate-dialog/share-certificate-dialog.component';
import { downloadShareCertificatePdf } from '../../certificate-pdf';
import { DscSignService } from '../../../dsc-sign-dialog/dsc-sign.service';
import type { ShareClass, ShareCertificateRecord } from '../share-certificate.component';
/** A real share account, kept alongside the raw API payload needed to open the real certificate dialog. */
export interface RealShareAccountRow {
record: ShareCertificateRecord;
sharesAccountData: any;
clientId: string;
}
@Component({
selector: 'mifosx-real-share-accounts-view',
templateUrl: './real-share-accounts-view.component.html',
styleUrls: [
'../../certificates.shared.scss',
'./real-share-accounts-view.component.scss'
],
imports: [
CommonModule,
FormsModule,
FaIconComponent,
MatTooltip
]
})
export class RealShareAccountsViewComponent implements OnInit {
private dialog = inject(MatDialog);
private sharesService = inject(SharesService);
private clientsService = inject(ClientsService);
private dscSign = inject(DscSignService);
loading = true;
loadError = false;
rows: RealShareAccountRow[] = [];
// ── Filters (mirrors the demo list above) ──
search = '';
classFilter: '' | ShareClass = '';
branchFilter = '';
readonly classOptions: { value: ShareClass; label: string }[] = [
{ value: 'ORDINARY', label: 'Ordinary' },
{ value: 'PREFERENCE', label: 'Preference' }
];
// Per-client caches so accounts sharing the same client (a common case — one
// member can hold several share accounts) only fetch that client once.
private clientDataCache = new Map<string, Observable<any>>();
private clientIdentifiersCache = new Map<string, Observable<any>>();
ngOnInit(): void {
this.loadRealAccounts();
}
private loadRealAccounts(): void {
this.loading = true;
this.loadError = false;
this.clientDataCache.clear();
this.clientIdentifiersCache.clear();
this.sharesService
.getAllShareAccounts(0, 200)
.pipe(
map((page: any) => page?.pageItems || page?.content || (Array.isArray(page) ? page : [])),
switchMap((items: any[]) => {
if (!items.length) {
return of([] as RealShareAccountRow[]);
}
return forkJoin(items.map((item) => this.buildRow(item))).pipe(
map((rows) => rows.filter((r): r is RealShareAccountRow => !!r))
);
}),
catchError(() => {
this.loadError = true;
return of([] as RealShareAccountRow[]);
})
)
.subscribe((rows) => {
this.rows = rows;
this.loading = false;
});
}
/**
* The share-account list already carries clientId/clientName (same mapper as the
* single-account read), so the account detail call and the client calls can fire
* together instead of waiting on each other — halving the round trips on the
* critical path.
*/
private buildRow(listItem: any): Observable<RealShareAccountRow | null> {
const clientId = listItem?.clientId != null ? String(listItem.clientId) : null;
return forkJoin({
sa: this.sharesService.getSharesAccountData(String(listItem.id), false),
client: clientId ? this.getClientDataCached(clientId) : of(null),
identifiers: clientId ? this.getClientIdentifiersCached(clientId) : of([])
}).pipe(
switchMap(({ sa, client, identifiers }) => {
// Defensive fallback if the list payload didn't carry clientId for some reason.
if (clientId || !sa?.clientId) {
return of({ sa, client, identifiers });
}
const fallbackClientId = String(sa.clientId);
return forkJoin({
sa: of(sa),
client: this.getClientDataCached(fallbackClientId),
identifiers: this.getClientIdentifiersCached(fallbackClientId)
});
}),
map(({ sa, client, identifiers }: any) => ({
record: this.mapToRecord(sa, client, identifiers),
sharesAccountData: sa,
clientId: String(sa.clientId)
})),
catchError(() => of(null))
);
}
private getClientDataCached(clientId: string): Observable<any> {
if (!this.clientDataCache.has(clientId)) {
this.clientDataCache.set(
clientId,
this.clientsService.getClientData(clientId).pipe(
catchError(() => of(null)),
shareReplay(1)
)
);
}
return this.clientDataCache.get(clientId)!;
}
private getClientIdentifiersCached(clientId: string): Observable<any> {
if (!this.clientIdentifiersCache.has(clientId)) {
this.clientIdentifiersCache.set(
clientId,
this.clientsService.getClientIdentifiers(clientId).pipe(
catchError(() => of([])),
shareReplay(1)
)
);
}
return this.clientIdentifiersCache.get(clientId)!;
}
private mapToRecord(sa: any, client: any, identifiers: any): ShareCertificateRecord {
const approvedShares = sa.summary?.totalApprovedShares || 0;
const faceValue = sa.currentMarketPrice || 0;
const issueDate = this.toIsoDate(sa.timeline?.activatedDate) || this.toIsoDate(sa.timeline?.approvedDate) || '';
const joiningDate =
this.toIsoDate(client?.activationDate) || this.toIsoDate(client?.timeline?.submittedOnDate) || issueDate;
const startNum = sa.id * 5000 + 201;
const endNum = startNum + approvedShares - 1;
const idList = Array.isArray(identifiers) ? identifiers : identifiers?.pageItems || [];
const panId = idList.find((i: any) => /pan/i.test(i?.documentType?.name || ''));
const memberPan = panId ? panId.documentKey : '—';
const dividendYtd = (sa.dividends || []).reduce((sum: number, d: any) => sum + (d.amount || 0), 0);
return {
certificateNo: `SC-2026-${sa.accountNo || String(sa.id).padStart(6, '0')}`,
folioNo: `F-${client?.accountNo || String(client?.id ?? sa.clientId).padStart(5, '0')}`,
memberId: client?.accountNo || '',
memberName: sa.clientName || client?.displayName || '',
memberPan,
// Address isn't shown in this table — fetched on demand when a PDF is downloaded.
memberAddress: '',
branch: client?.officeName || '',
joiningDate,
shareClass: /prefer/i.test(sa.productName || '') ? 'PREFERENCE' : 'ORDINARY',
shareCount: approvedShares,
faceValue,
issueDate,
shareNoFrom: startNum,
shareNoTo: approvedShares > 0 ? endNum : startNum,
dividendYtd
};
}
private toIsoDate(value: any): string {
if (!value) {
return '';
}
if (Array.isArray(value)) {
const [
y,
m,
d
] = value;
if (!y) {
return '';
}
return `${y}-${String(m || 1).padStart(2, '0')}-${String(d || 1).padStart(2, '0')}`;
}
return String(value);
}
private formatAddress(addr: any): string {
if (!addr) {
return '';
}
return [
addr.addressLine1,
addr.addressLine2,
addr.townVillage,
addr.city,
addr.stateName,
addr.countryName,
addr.postalCode
]
.filter((p) => !!p && String(p).trim())
.join(', ');
}
// ── Derived list (identical shape to the demo table above) ──
get filteredRows(): RealShareAccountRow[] {
const q = this.search.trim().toLowerCase();
const branch = this.branchFilter.trim().toLowerCase();
return this.rows.filter(({ record: r }) => {
if (this.classFilter && r.shareClass !== this.classFilter) return false;
if (branch && !r.branch.toLowerCase().includes(branch)) return false;
if (!q) return true;
return (
r.certificateNo.toLowerCase().includes(q) ||
r.folioNo.toLowerCase().includes(q) ||
r.memberId.toLowerCase().includes(q) ||
r.memberName.toLowerCase().includes(q) ||
r.memberPan.toLowerCase().includes(q)
);
});
}
get branchOptions(): string[] {
return Array.from(new Set(this.rows.map((row) => row.record.branch).filter(Boolean))).sort();
}
classLabel(c: ShareClass): string {
return this.classOptions.find((o) => o.value === c)?.label || c;
}
totalShares(): number {
return this.filteredRows.reduce((s, { record: r }) => s + r.shareCount, 0);
}
totalShareValue(): number {
return this.filteredRows.reduce((s, { record: r }) => s + r.shareCount * r.faceValue, 0);
}
clearFilters(): void {
this.search = '';
this.classFilter = '';
this.branchFilter = '';
}
retry(): void {
this.loadRealAccounts();
}
/** Opens the same certificate dialog used from Client → Share Account → Share Certificate. */
viewCertificate(row: RealShareAccountRow): void {
this.dialog.open(ShareCertificateDialogComponent, {
data: {
sharesAccountData: row.sharesAccountData,
clientId: row.clientId
},
width: '850px',
maxWidth: '95vw'
});
}
/** Downloads the certificate as a PDF. Address is fetched here (only place it's needed). */
downloadCertificate(row: RealShareAccountRow): void {
this.clientsService
.getClientAddressData(row.clientId)
.pipe(catchError(() => of([])))
.subscribe((addresses: any) => {
const addrList = Array.isArray(addresses) ? addresses : [];
const permanent = addrList.find((a: any) => /permanent/i.test(a?.addressType || '')) || addrList[0];
const r: ShareCertificateRecord = {
...row.record,
memberAddress: permanent ? this.formatAddress(permanent) : row.record.memberAddress
};
this.dscSign.promptThenDownload(
{
title: 'Share Certificate',
reference: `${r.certificateNo} · Folio ${r.folioNo} · ${r.memberName}`,
filename: `Share-Certificate-${r.certificateNo}.pdf`
},
(dsc) => downloadShareCertificatePdf(r, dsc)
);
});
}
}
@@ -14,151 +14,184 @@
<div>
<h2 class="cert-head-t">Share Certificate</h2>
<div class="cert-head-s">
Issue, preview and download member share certificates with folio + share-number range.
@if (viewMode === 'demo') {
Issue, preview and download member share certificates with folio + share-number range.
} @else {
Live holder details and certificates, sourced from m_share_account.
}
</div>
</div>
</div>
<div class="cert-head-r">
@if (viewMode === 'demo') {
<button
type="button"
class="cert-cta-btn"
(click)="toggleViewMode()"
matTooltip="Same fields and certificate, using real m_share_account data"
>
<fa-icon icon="database"></fa-icon> Real Share Accounts
</button>
} @else {
<button
type="button"
class="cert-cta-btn cert-cta-btn--muted"
(click)="toggleViewMode()"
matTooltip="Back to the sample data page"
>
<fa-icon icon="table"></fa-icon> Sample Data
</button>
}
</div>
</div>
@if (viewMode === 'real') {
<mifosx-real-share-accounts-view></mifosx-real-share-accounts-view>
}
<!-- KPI STRIP -->
<div class="cert-kpis">
<div class="cert-kpi cert-kpi--blue">
<fa-icon icon="certificate" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">{{ filteredRecords.length | number }}</div>
<div class="cert-kpi-l">Certificates</div>
@if (viewMode === 'demo') {
<div class="cert-kpis">
<div class="cert-kpi cert-kpi--blue">
<fa-icon icon="certificate" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">{{ filteredRecords.length | number }}</div>
<div class="cert-kpi-l">Certificates</div>
</div>
</div>
</div>
<div class="cert-kpi cert-kpi--teal">
<fa-icon icon="cube" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">{{ totalShares() | number }}</div>
<div class="cert-kpi-l">Shares Held</div>
<div class="cert-kpi cert-kpi--teal">
<fa-icon icon="cube" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">{{ totalShares() | number }}</div>
<div class="cert-kpi-l">Shares Held</div>
</div>
</div>
</div>
<div class="cert-kpi cert-kpi--green">
<fa-icon icon="coins" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">₹{{ totalShareValue() | number: '1.0-0' }}</div>
<div class="cert-kpi-l">Total Face Value</div>
</div>
</div>
</div>
<!-- FILTERS -->
<div class="cert-filters">
<div class="cert-filter">
<label>Certificate / Folio / Member / PAN</label>
<div class="cert-input-ic">
<fa-icon icon="search"></fa-icon>
<input type="text" [(ngModel)]="search" placeholder="e.g. SC-2024-000123 or member name" />
<div class="cert-kpi cert-kpi--green">
<fa-icon icon="coins" class="cert-kpi-ic"></fa-icon>
<div>
<div class="cert-kpi-n">₹{{ totalShareValue() | number: '1.0-0' }}</div>
<div class="cert-kpi-l">Total Face Value</div>
</div>
</div>
</div>
<div class="cert-filter">
<label>Share class</label>
<select [(ngModel)]="classFilter">
<option value="">All classes</option>
@for (o of classOptions; track o.value) {
<option [value]="o.value">{{ o.label }}</option>
}
</select>
</div>
<!-- FILTERS -->
<div class="cert-filters">
<div class="cert-filter">
<label>Certificate / Folio / Member / PAN</label>
<div class="cert-input-ic">
<fa-icon icon="search"></fa-icon>
<input type="text" [(ngModel)]="search" placeholder="e.g. SC-2024-000123 or member name" />
</div>
</div>
<div class="cert-filter">
<label>Branch</label>
<select [(ngModel)]="branchFilter">
<option value="">All branches</option>
@for (b of branchOptions; track b) {
<option [value]="b">{{ b }}</option>
}
</select>
</div>
@if (search || classFilter || branchFilter) {
<button type="button" class="cert-clear" (click)="clearFilters()"><fa-icon icon="times"></fa-icon> Clear</button>
}
</div>
<!-- TABLE -->
<div class="cert-card">
<div class="cert-table-wrap">
<table class="cert-table">
<thead>
<tr>
<th>Member · Folio</th>
<th>Certificate No.</th>
<th>Class</th>
<th>Branch</th>
<th class="num">Shares</th>
<th class="num">Face Value</th>
<th class="num">Total</th>
<th>Share Nos.</th>
<th>Issued</th>
<th class="col-actions">Actions</th>
</tr>
</thead>
<tbody>
@for (r of filteredRecords; track r.certificateNo) {
<tr>
<td>
<div class="cert-cust-name">{{ r.memberName }}</div>
<div class="mono small muted">
{{ r.memberId }} · Folio <span class="cert-folio">{{ r.folioNo }}</span> · PAN {{ r.memberPan }}
</div>
</td>
<td class="mono">{{ r.certificateNo }}</td>
<td>
<span
class="cert-pill"
[class.cert-pill--blue]="r.shareClass === 'ORDINARY'"
[class.cert-pill--amber]="r.shareClass === 'PREFERENCE'"
>
{{ classLabel(r.shareClass) }}
</span>
</td>
<td class="small">{{ r.branch }}</td>
<td class="num">
<strong class="cert-shares">{{ r.shareCount | number }}</strong>
</td>
<td class="num">₹{{ r.faceValue | number: '1.0-0' }}</td>
<td class="num">
<strong>₹{{ r.shareCount * r.faceValue | number: '1.0-0' }}</strong>
</td>
<td class="mono small">{{ r.shareNoFrom }}&ndash;{{ r.shareNoTo }}</td>
<td class="small">{{ r.issueDate | date: 'd MMM y' }}</td>
<td class="col-actions">
<div class="cert-row-actions">
<button
type="button"
class="cert-icon-btn primary"
(click)="viewCertificate(r)"
matTooltip="View / Print certificate"
>
<fa-icon icon="eye"></fa-icon>
</button>
<button
type="button"
class="cert-icon-btn"
(click)="downloadCertificate(r)"
matTooltip="Download PDF"
>
<fa-icon icon="download"></fa-icon>
</button>
</div>
</td>
</tr>
<div class="cert-filter">
<label>Share class</label>
<select [(ngModel)]="classFilter">
<option value="">All classes</option>
@for (o of classOptions; track o.value) {
<option [value]="o.value">{{ o.label }}</option>
}
@if (filteredRecords.length === 0) {
<tr>
<td colspan="10" class="cert-empty">
<fa-icon icon="info-circle"></fa-icon>
No share certificates match the current filters.
</td>
</tr>
</select>
</div>
<div class="cert-filter">
<label>Branch</label>
<select [(ngModel)]="branchFilter">
<option value="">All branches</option>
@for (b of branchOptions; track b) {
<option [value]="b">{{ b }}</option>
}
</tbody>
</table>
</select>
</div>
@if (search || classFilter || branchFilter) {
<button type="button" class="cert-clear" (click)="clearFilters()">
<fa-icon icon="times"></fa-icon> Clear
</button>
}
</div>
</div>
<!-- TABLE -->
<div class="cert-card">
<div class="cert-table-wrap">
<table class="cert-table">
<thead>
<tr>
<th>Member · Folio</th>
<th>Certificate No.</th>
<th>Class</th>
<th>Branch</th>
<th class="num">Shares</th>
<th class="num">Face Value</th>
<th class="num">Total</th>
<th>Share Nos.</th>
<th>Issued</th>
<th class="col-actions">Actions</th>
</tr>
</thead>
<tbody>
@for (r of filteredRecords; track r.certificateNo) {
<tr>
<td>
<div class="cert-cust-name">{{ r.memberName }}</div>
<div class="mono small muted">
{{ r.memberId }} · Folio <span class="cert-folio">{{ r.folioNo }}</span> · PAN {{ r.memberPan }}
</div>
</td>
<td class="mono">{{ r.certificateNo }}</td>
<td>
<span
class="cert-pill"
[class.cert-pill--blue]="r.shareClass === 'ORDINARY'"
[class.cert-pill--amber]="r.shareClass === 'PREFERENCE'"
>
{{ classLabel(r.shareClass) }}
</span>
</td>
<td class="small">{{ r.branch }}</td>
<td class="num">
<strong class="cert-shares">{{ r.shareCount | number }}</strong>
</td>
<td class="num">₹{{ r.faceValue | number: '1.0-0' }}</td>
<td class="num">
<strong>₹{{ r.shareCount * r.faceValue | number: '1.0-0' }}</strong>
</td>
<td class="mono small">{{ r.shareNoFrom }}&ndash;{{ r.shareNoTo }}</td>
<td class="small">{{ r.issueDate | date: 'd MMM y' }}</td>
<td class="col-actions">
<div class="cert-row-actions">
<button
type="button"
class="cert-icon-btn primary"
(click)="viewCertificate(r)"
matTooltip="View / Print certificate"
>
<fa-icon icon="eye"></fa-icon>
</button>
<button
type="button"
class="cert-icon-btn"
(click)="downloadCertificate(r)"
matTooltip="Download PDF"
>
<fa-icon icon="download"></fa-icon>
</button>
</div>
</td>
</tr>
}
@if (filteredRecords.length === 0) {
<tr>
<td colspan="10" class="cert-empty">
<fa-icon icon="info-circle"></fa-icon>
No share certificates match the current filters.
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
</div>
@@ -21,6 +21,7 @@ import {
} from '../certificate-preview-dialog/certificate-preview-dialog.component';
import { downloadShareCertificatePdf } from '../certificate-pdf';
import { DscSignService } from '../../dsc-sign-dialog/dsc-sign.service';
import { RealShareAccountsViewComponent } from './real-share-accounts-view/real-share-accounts-view.component';
export type ShareClass = 'ORDINARY' | 'PREFERENCE';
@@ -53,7 +54,8 @@ export interface ShareCertificateRecord {
CommonModule,
FormsModule,
FaIconComponent,
MatTooltip
MatTooltip,
RealShareAccountsViewComponent
]
})
export class ShareCertificateComponent implements OnInit {
@@ -61,6 +63,9 @@ export class ShareCertificateComponent implements OnInit {
private snack = inject(MatSnackBar);
private dscSign = inject(DscSignService);
/** Toggles between the static sample table and the live m_share_account view. */
viewMode: 'demo' | 'real' = 'demo';
// ── Filters ──
search = '';
classFilter: '' | ShareClass = '';
@@ -143,6 +148,11 @@ export class ShareCertificateComponent implements OnInit {
(dsc) => downloadShareCertificatePdf(r, dsc)
);
}
/** Switches between the static sample table and the live m_share_account view. */
toggleViewMode(): void {
this.viewMode = this.viewMode === 'demo' ? 'real' : 'demo';
}
}
// ───────────────────── Mock seed (top-level export) ─────────────────────