diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-account-view.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-account-view.component.ts
index 55c4deb69..24ca434e9 100644
--- a/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-account-view.component.ts
+++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-view/recurring-deposits-account-view.component.ts
@@ -27,6 +27,7 @@ import { TranslateService } from '@ngx-translate/core';
import { AccountsService } from 'app/accounts/accounts.service';
import { SettingsService } from 'app/settings/settings.service';
import { Dates } from 'app/core/utils/dates';
+import { AuthenticationService } from '../../../core/authentication/authentication.service';
import { MatTooltip } from '@angular/material/tooltip';
import { NgClass, DecimalPipe, CurrencyPipe } from '@angular/common';
import { AccountNumberComponent } from '../../../shared/account-number/account-number.component';
@@ -37,6 +38,7 @@ import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs';
import { StatusLookupPipe } from '../../../pipes/status-lookup.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
+import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dormancy-alert-panel.component';
/**
* RecurringDeposits Account View Component
@@ -63,7 +65,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
RouterOutlet,
DecimalPipe,
CurrencyPipe,
- StatusLookupPipe
+ StatusLookupPipe,
+ DormancyAlertPanelComponent
]
})
export class RecurringDepositsAccountViewComponent implements OnInit {
@@ -74,6 +77,7 @@ export class RecurringDepositsAccountViewComponent implements OnInit {
private accountsService = inject(AccountsService);
private settingsService = inject(SettingsService);
private dateUtils = inject(Dates);
+ private authenticationService = inject(AuthenticationService);
dialog = inject(MatDialog);
private translateService = inject(TranslateService);
@@ -245,6 +249,14 @@ export class RecurringDepositsAccountViewComponent implements OnInit {
}
}
}
+ if (!this.authenticationService.hasCashierRole()) {
+ this.buttonConfig.buttonsArray = this.buttonConfig.buttonsArray.filter(
+ (btn) => btn.name !== 'Deposit' && btn.name !== 'Withdrawal'
+ );
+ this.buttonConfig.optionArray = this.buttonConfig.optionArray.filter(
+ (opt) => opt.name !== 'Deposit' && opt.name !== 'Withdrawal'
+ );
+ }
}
/**
diff --git a/src/app/letter-mgmt/certificates/certificate-pdf.ts b/src/app/letter-mgmt/certificates/certificate-pdf.ts
index 939a556e5..5467eadfd 100644
--- a/src/app/letter-mgmt/certificates/certificate-pdf.ts
+++ b/src/app/letter-mgmt/certificates/certificate-pdf.ts
@@ -544,82 +544,315 @@ export function downloadLoanInterestPdf(record: LoanInterestRecord, fy: FyIntere
// ───────────────────── Share Certificate ─────────────────────
export function buildShareCertificatePdf(r: ShareCertificateRecord): jsPDF {
- const c = newCtx(ACCENT_AMBER);
+ const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
+ const pageW = doc.internal.pageSize.getWidth();
+ const pageH = doc.internal.pageSize.getHeight();
+ const margin = 20;
+ const contentW = pageW - 40;
+ const gold: [number, number, number] = [
+ 197,
+ 160,
+ 89
+ ]; // #c5a059
+ const darkBlue: [number, number, number] = [
+ 11,
+ 60,
+ 93
+ ]; // #0b3c5d
+ const textDark: [number, number, number] = [
+ 44,
+ 37,
+ 32
+ ]; // #2c2520
+ const textMuted: [number, number, number] = [
+ 123,
+ 111,
+ 99
+ ]; // #7b6f63
+ const lightBg: [number, number, number] = [
+ 253,
+ 252,
+ 247
+ ]; // #fdfcf7
+ const containerBg: [number, number, number] = [
+ 250,
+ 248,
+ 245
+ ]; // #faf8f5
+
+ // 1. Draw Cream Background (page body)
+ doc.setFillColor(containerBg[0], containerBg[1], containerBg[2]);
+ doc.rect(0, 0, pageW, pageH, 'F');
+
+ // 2. Draw Textured cream background inside the outer border
+ doc.setFillColor(lightBg[0], lightBg[1], lightBg[2]);
+ doc.rect(12, 12, pageW - 24, pageH - 24, 'F');
+
+ // 3. Draw Gold Double Borders (Outer and Inner)
+ doc.setDrawColor(gold[0], gold[1], gold[2]);
+
+ // Outer double line
+ doc.setLineWidth(1.2);
+ doc.rect(12, 12, pageW - 24, pageH - 24, 'D');
+
+ doc.setLineWidth(0.3);
+ doc.rect(13.8, 13.8, pageW - 27.6, pageH - 27.6, 'D');
+
+ // Thin interior border line
+ doc.setDrawColor(220, 209, 189); // light brown/grey
+ doc.setLineWidth(0.2);
+ doc.rect(17, 17, pageW - 34, pageH - 34, 'D');
+
+ // 4. Certificate Header
+ let y = 28;
+ doc.setFont('times', 'bold');
+ doc.setFontSize(22);
+ doc.setTextColor(darkBlue[0], darkBlue[1], darkBlue[2]);
+ doc.text('NEXORA CO-OPERATIVE BANK LTD.', pageW / 2, y, { align: 'center' });
+
+ y += 5;
+ doc.setFont('times', 'italic');
+ doc.setFontSize(10.5);
+ doc.setTextColor(textMuted[0], textMuted[1], textMuted[2]);
+ doc.text('Mumbai, Maharashtra, India', pageW / 2, y, { align: 'center' });
+
+ y += 4;
+ doc.setDrawColor(242, 237, 213); // light gold separator
+ doc.setLineWidth(1.5);
+ doc.line(22, y, pageW - 22, y);
+
+ y += 9;
+ doc.setFont('times', 'bold');
+ doc.setFontSize(24);
+ doc.setTextColor(gold[0], gold[1], gold[2]);
+ doc.text('SHARE CERTIFICATE', pageW / 2, y, { align: 'center' });
+
+ y += 5.5;
+ doc.setFont('times', 'bold');
+ doc.setFontSize(7.5);
+ doc.setTextColor(textMuted[0], textMuted[1], textMuted[2]);
+ doc.text('ISSUED UNDER THE COMMON SEAL OF THE BANK', pageW / 2, y, { align: 'center' });
+
+ y += 4;
+ doc.setDrawColor(242, 237, 213); // separator
+ doc.setLineWidth(0.3);
+ doc.line(22, y, pageW - 22, y);
+
+ // 5. Meta Row (Certificate No, Folio No, Date of Issue)
+ y += 7;
+
+ // Column 1: Certificate No
+ doc.setFont('helvetica', 'bold');
+ doc.setFontSize(7.5);
+ doc.setTextColor(textMuted[0], textMuted[1], textMuted[2]);
+ doc.text('CERTIFICATE NO.', 25, y);
+ doc.setFont('times', 'bold');
+ doc.setFontSize(10);
+ doc.setTextColor(textDark[0], textDark[1], textDark[2]);
+ doc.text(r.certificateNo, 25, y + 4.5);
+
+ // Column 2: Folio No (Centered)
+ doc.setFont('helvetica', 'bold');
+ doc.setFontSize(7.5);
+ doc.setTextColor(textMuted[0], textMuted[1], textMuted[2]);
+ doc.text('FOLIO NO.', pageW / 2, y, { align: 'center' });
+ doc.setFont('times', 'bold');
+ doc.setFontSize(10);
+ doc.setTextColor(textDark[0], textDark[1], textDark[2]);
+ doc.text(r.folioNo, pageW / 2, y + 4.5, { align: 'center' });
+
+ // Column 3: Date of Issue (Right Aligned)
+ doc.setFont('helvetica', 'bold');
+ doc.setFontSize(7.5);
+ doc.setTextColor(textMuted[0], textMuted[1], textMuted[2]);
+ doc.text('DATE OF ISSUE', pageW - 25, y, { align: 'right' });
+ doc.setFont('times', 'bold');
+ doc.setFontSize(10);
+ doc.setTextColor(textDark[0], textDark[1], textDark[2]);
+ doc.text(formatLongDate(r.issueDate), pageW - 25, y + 4.5, { align: 'right' });
+
+ y += 8;
+ doc.setDrawColor(242, 237, 213);
+ doc.setLineWidth(0.3);
+ doc.line(22, y, pageW - 22, y);
+
+ // 6. Centered Body Section
+ y += 9;
const classLabel = r.shareClass === 'PREFERENCE' ? 'Preference' : 'Ordinary';
- let y = paintLetterhead(c, 'Share Certificate', 'CERTIFICATE REFERENCE', r.certificateNo, 'Folio No.', r.folioNo);
+ doc.setFont('times', 'normal');
+ doc.setFontSize(11);
+ doc.setTextColor(textDark[0], textDark[1], textDark[2]);
- y = paintAddressee(c, y, 'Issued To,', r.memberName, r.memberId, r.memberPan, r.memberAddress);
-
- // Centered, dramatic body paragraph (mirrors the highlighted "centered" style in the preview)
- if (y > c.pageH - c.margin - 80) {
- c.doc.addPage();
- setFill(c, c.accent);
- c.doc.rect(0, 0, c.pageW, 4, 'F');
- y = c.margin;
+ const bodyText = `This is to certify that ${r.memberName} is the registered holder of ${r.shareCount.toLocaleString('en-IN')} (${classLabel}) shares of Rs. ${r.faceValue.toLocaleString('en-IN')} each, fully paid up, in Nexora Co-operative Bank Ltd., subject to the memorandum, articles of association, and bye-laws of the bank.`;
+ const bodyLines = doc.splitTextToSize(bodyText, pageW - 50);
+ for (const line of bodyLines) {
+ doc.text(line, pageW / 2, y, { align: 'center' });
+ y += 5.5;
}
- const ctaText = `This is to certify that ${r.memberName} is the registered holder of ${r.shareCount.toLocaleString('en-IN')} (${classLabel}) shares of Rs. ${r.faceValue.toLocaleString('en-IN')} each, fully paid up, in ${BANK_NAME}, subject to the bye-laws of the bank.`;
- const ctaLines = c.doc.splitTextToSize(ctaText, c.contentW - 10);
- const ctaH = ctaLines.length * 5 + 6;
- setFill(c, c.accentTint);
- c.doc.rect(c.margin, y, c.contentW, ctaH, 'F');
- setFill(c, c.accent);
- c.doc.rect(c.margin, y, 1.8, ctaH, 'F');
- c.doc.setFont('times', 'bold');
- c.doc.setFontSize(11);
- setText(c, C_TEXT);
- c.doc.text(ctaLines, c.margin + 6, y + 5, { align: 'justify', maxWidth: c.contentW - 10 });
- y += ctaH + 6;
- y = paintSummary(
- c,
- y,
- [
- [
- 'CERTIFICATE NO.',
- r.certificateNo,
- 'FOLIO NO.',
- r.folioNo
- ],
- [
- 'BRANCH',
- r.branch,
- 'MEMBER SINCE',
- formatLongDate(r.joiningDate)
- ],
- [
- 'SHARE CLASS',
- classLabel,
- 'DATE OF ISSUE',
- formatLongDate(r.issueDate)
- ],
- [
- 'SHARE NUMBERS',
- `${r.shareNoFrom.toLocaleString('en-IN')} – ${r.shareNoTo.toLocaleString('en-IN')}`,
- 'DIVIDEND PAID',
- `Rs. ${r.dividendYtd.toLocaleString('en-IN')}`
- ]
- ],
- {
- label: 'TOTAL FACE VALUE',
- value: `Rs. ${(r.shareCount * r.faceValue).toLocaleString('en-IN')}`
+ // 7. ISSUED TO Box
+ y += 3;
+ const addrLines = doc.splitTextToSize(r.memberAddress || '', pageW - 60);
+ const boxH = 26 + addrLines.length * 4;
+
+ doc.setFillColor(253, 252, 247);
+ doc.setDrawColor(230, 225, 210);
+ doc.rect(25, y, pageW - 50, boxH, 'FD');
+
+ let boxY = y + 6;
+ doc.setFont('helvetica', 'bold');
+ doc.setFontSize(8);
+ doc.setTextColor(gold[0], gold[1], gold[2]);
+ doc.text('ISSUED TO:', 30, boxY);
+
+ boxY += 5.5;
+ doc.setFont('times', 'bold');
+ doc.setFontSize(13);
+ doc.setTextColor(darkBlue[0], darkBlue[1], darkBlue[2]);
+ doc.text(r.memberName, 30, boxY);
+
+ boxY += 5.5;
+ doc.setFont('helvetica', 'normal');
+ doc.setFontSize(9);
+ doc.setTextColor(textDark[0], textDark[1], textDark[2]);
+ let idLine = `Member ID: ${r.memberId}`;
+ if (r.memberPan && r.memberPan !== 'Not Available' && r.memberPan !== '—') {
+ idLine += ` · PAN: ${r.memberPan}`;
+ }
+ doc.text(idLine, 30, boxY);
+
+ boxY += 5;
+ doc.setFontSize(8.5);
+ doc.setTextColor(textMuted[0], textMuted[1], textMuted[2]);
+ doc.text(addrLines, 30, boxY);
+
+ y += boxH + 8;
+
+ // 8. Details Grid
+ const tableW = pageW - 50;
+ const tableColW = tableW / 4;
+ const tableRowH = 8.5;
+
+ const drawTableCell = (
+ x: number,
+ tblY: number,
+ w: number,
+ h: number,
+ text: string,
+ isLabel: boolean,
+ align: 'left' | 'right' = 'left'
+ ) => {
+ if (isLabel) {
+ doc.setFillColor(253, 252, 247);
+ doc.rect(x, tblY, w, h, 'F');
+ doc.setFont('helvetica', 'bold');
+ doc.setFontSize(8);
+ doc.setTextColor(gold[0], gold[1], gold[2]);
+ } else {
+ doc.setFont('times', 'bold');
+ doc.setFontSize(9.5);
+ doc.setTextColor(darkBlue[0], darkBlue[1], darkBlue[2]);
}
- );
+ doc.setDrawColor(220, 209, 189);
+ doc.setLineWidth(0.25);
+ doc.rect(x, tblY, w, h, 'D');
+ doc.text(text, align === 'right' ? x + w - 3 : x + 3, tblY + h / 2 + 1.2, { align });
+ };
- y = paintPara(
- c,
+ // Row 1
+ drawTableCell(25, y, tableColW, tableRowH, 'Branch', true);
+ drawTableCell(25 + tableColW, y, tableColW, tableRowH, r.branch || 'Mumbai Main Branch', false);
+ drawTableCell(25 + tableColW * 2, y, tableColW, tableRowH, 'Member Since', true);
+ drawTableCell(25 + tableColW * 3, y, tableColW, tableRowH, formatLongDate(r.joiningDate), false);
+ y += tableRowH;
+
+ // Row 2
+ drawTableCell(25, y, tableColW, tableRowH, 'Share Class', true);
+ drawTableCell(25 + tableColW, y, tableColW, tableRowH, classLabel, false);
+ drawTableCell(25 + tableColW * 2, y, tableColW, tableRowH, 'Date of Issue', true);
+ drawTableCell(25 + tableColW * 3, y, tableColW, tableRowH, formatLongDate(r.issueDate), false);
+ y += tableRowH;
+
+ // Row 3
+ drawTableCell(25, y, tableColW, tableRowH, 'Share Numbers', true);
+ drawTableCell(
+ 25 + tableColW,
y,
- `Given under the common seal of the bank this ${new Date().toLocaleDateString('en-IN', {
- day: 'numeric',
- month: 'long',
- year: 'numeric'
- })}.`
+ tableColW,
+ tableRowH,
+ `${r.shareNoFrom.toLocaleString('en-IN')} – ${r.shareNoTo.toLocaleString('en-IN')}`,
+ false
);
+ drawTableCell(25 + tableColW * 2, y, tableColW, tableRowH, 'Dividend Paid', true);
+ drawTableCell(25 + tableColW * 3, y, tableColW, tableRowH, `Rs. ${r.dividendYtd.toLocaleString('en-IN')}`, false);
+ y += tableRowH;
- y = paintSignoff(c, y + 6, r.branch);
- paintFooter(c, y + 4);
- return c.doc;
+ // Row 4 (Highlight)
+ doc.setFillColor(darkBlue[0], darkBlue[1], darkBlue[2]);
+ doc.rect(25, y, tableColW * 2, tableRowH, 'F');
+ doc.rect(25 + tableColW * 2, y, tableColW * 2, tableRowH, 'F');
+
+ doc.setFont('helvetica', 'bold');
+ doc.setFontSize(8.5);
+ doc.setTextColor(255, 255, 255);
+ doc.text('TOTAL FACE VALUE', 28, y + tableRowH / 2 + 1.2);
+
+ doc.setFont('times', 'bold');
+ doc.setFontSize(11);
+ doc.text(`Rs. ${(r.shareCount * r.faceValue).toLocaleString('en-IN')}`, pageW - 28, y + tableRowH / 2 + 1.2, {
+ align: 'right'
+ });
+ y += tableRowH + 8;
+
+ // 9. Footer Text (Given under common seal...)
+ doc.setFont('times', 'italic');
+ doc.setFontSize(10.5);
+ doc.setTextColor(textDark[0], textDark[1], textDark[2]);
+
+ const sealDay = new Date().getDate();
+ const sealMonthYear = new Date().toLocaleDateString('en-IN', { month: 'long', year: 'numeric' });
+ const sealPhrase = `Given under the common seal of the bank this ${sealDay} day of ${sealMonthYear}.`;
+ doc.text(sealPhrase, pageW / 2, y, { align: 'center' });
+
+ y += 8;
+
+ // 10. Seal Circle (left) and Signatory block (right)
+ const sealX = 50;
+ const sealY = y + 14;
+ doc.setDrawColor(gold[0], gold[1], gold[2]); // Gold
+ doc.setLineWidth(0.6);
+ doc.circle(sealX, sealY, 14, 'D');
+
+ doc.setLineWidth(0.3);
+ doc.setLineDashPattern(
+ [
+ 1,
+ 1
+ ],
+ 0
+ );
+ doc.circle(sealX, sealY, 11, 'D');
+ doc.setLineDashPattern([], 0);
+
+ doc.setFont('helvetica', 'bold');
+ doc.setFontSize(7.5);
+ doc.setTextColor(gold[0], gold[1], gold[2]);
+ doc.text('COMMON', sealX, sealY - 1, { align: 'center' });
+ doc.text('SEAL', sealX, sealY + 3.5, { align: 'center' });
+
+ // Signatory Line
+ doc.setDrawColor(textMuted[0], textMuted[1], textMuted[2]);
+ doc.setLineWidth(0.3);
+ doc.line(pageW - 75, y + 20, pageW - 25, y + 20);
+
+ doc.setFont('helvetica', 'bold');
+ doc.setFontSize(9.5);
+ doc.setTextColor(textDark[0], textDark[1], textDark[2]);
+ doc.text('Authorised Signatory', pageW - 50, y + 25, { align: 'center' });
+
+ return doc;
}
export function downloadShareCertificatePdf(r: ShareCertificateRecord, dsc?: BankUserDsc): void {
diff --git a/src/app/letter-mgmt/certificates/certificates.shared.scss b/src/app/letter-mgmt/certificates/certificates.shared.scss
index c93a0bdc7..ed8bea4d2 100644
--- a/src/app/letter-mgmt/certificates/certificates.shared.scss
+++ b/src/app/letter-mgmt/certificates/certificates.shared.scss
@@ -89,6 +89,43 @@
line-height: 1.5;
}
+.cert-head-r {
+ margin-left: auto;
+ flex-shrink: 0;
+}
+
+.cert-cta-btn {
+ height: 38px;
+ padding: 0 16px;
+ border: 1px solid var(--cert-blue-d);
+ border-radius: 8px;
+ background: var(--cert-blue-d);
+ color: #fff;
+ font-size: 12.5px;
+ font-weight: 700;
+ cursor: pointer;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ white-space: nowrap;
+
+ &:hover {
+ background: var(--cert-blue);
+ border-color: var(--cert-blue);
+ }
+
+ &--muted {
+ background: #fff;
+ color: var(--cert-g700);
+ border-color: var(--cert-g200);
+
+ &:hover {
+ background: var(--cert-g50);
+ border-color: var(--cert-g300);
+ }
+ }
+}
+
/* ───────── KPI strip ───────── */
.cert-kpis {
display: grid;
diff --git a/src/app/letter-mgmt/certificates/share-certificate/real-share-accounts-view/real-share-accounts-view.component.html b/src/app/letter-mgmt/certificates/share-certificate/real-share-accounts-view/real-share-accounts-view.component.html
new file mode 100644
index 000000000..c05fc904b
--- /dev/null
+++ b/src/app/letter-mgmt/certificates/share-certificate/real-share-accounts-view/real-share-accounts-view.component.html
@@ -0,0 +1,174 @@
+
+
+
+ @if (loading) {
+
+
+ Loading real share accounts…
+
+ } @else if (loadError) {
+
+
+ Could not load share accounts from the server.
+ Retry
+
+ } @else if (rows.length === 0) {
+
+
+ No share accounts found in m_share_account yet.
+
+ } @else {
+
+
+
+
+
+
{{ filteredRows.length | number }}
+
Certificates
+
+
+
+
+
+
{{ totalShares() | number }}
+
Shares Held
+
+
+
+
+
+
₹{{ totalShareValue() | number: '1.0-0' }}
+
Total Face Value
+
+
+
+
+
+
+
+
Certificate / Folio / Member / PAN
+
+
+
+
+
+
+
+ Share class
+
+ All classes
+ @for (o of classOptions; track o.value) {
+ {{ o.label }}
+ }
+
+
+
+
+ Branch
+
+ All branches
+ @for (b of branchOptions; track b) {
+ {{ b }}
+ }
+
+
+
+ @if (search || classFilter || branchFilter) {
+
+ Clear
+
+ }
+
+
+
+
+
+
+
+
+ Member · Folio
+ Certificate No.
+ Class
+ Branch
+ Shares
+ Face Value
+ Total
+ Share Nos.
+ Issued
+ Actions
+
+
+
+ @for (row of filteredRows; track row.record.certificateNo) {
+
+
+ {{ row.record.memberName }}
+
+ {{ row.record.memberId }} · Folio {{ row.record.folioNo }} · PAN
+ {{ row.record.memberPan }}
+
+
+ {{ row.record.certificateNo }}
+
+
+ {{ classLabel(row.record.shareClass) }}
+
+
+ {{ row.record.branch || '—' }}
+
+ {{ row.record.shareCount | number }}
+
+ ₹{{ row.record.faceValue | number: '1.0-0' }}
+
+ ₹{{ row.record.shareCount * row.record.faceValue | number: '1.0-0' }}
+
+
+ {{ row.record.shareNoFrom | number }}–{{ row.record.shareNoTo | number }}
+
+ {{ row.record.issueDate | date: 'd MMM y' }}
+
+
+
+
+
+
+
+
+
+
+
+ }
+ @if (filteredRows.length === 0) {
+
+
+
+ No share accounts match the current filters.
+
+
+ }
+
+
+
+
+ }
+
diff --git a/src/app/letter-mgmt/certificates/share-certificate/real-share-accounts-view/real-share-accounts-view.component.scss b/src/app/letter-mgmt/certificates/share-certificate/real-share-accounts-view/real-share-accounts-view.component.scss
new file mode 100644
index 000000000..e76ed1e58
--- /dev/null
+++ b/src/app/letter-mgmt/certificates/share-certificate/real-share-accounts-view/real-share-accounts-view.component.scss
@@ -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;
+}
diff --git a/src/app/letter-mgmt/certificates/share-certificate/real-share-accounts-view/real-share-accounts-view.component.ts b/src/app/letter-mgmt/certificates/share-certificate/real-share-accounts-view/real-share-accounts-view.component.ts
new file mode 100644
index 000000000..655d42238
--- /dev/null
+++ b/src/app/letter-mgmt/certificates/share-certificate/real-share-accounts-view/real-share-accounts-view.component.ts
@@ -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
>();
+ private clientIdentifiersCache = new Map>();
+
+ 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 {
+ 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 {
+ 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 {
+ 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)
+ );
+ });
+ }
+}
diff --git a/src/app/letter-mgmt/certificates/share-certificate/share-certificate.component.html b/src/app/letter-mgmt/certificates/share-certificate/share-certificate.component.html
index a5e1818ae..b2c7a8f71 100644
--- a/src/app/letter-mgmt/certificates/share-certificate/share-certificate.component.html
+++ b/src/app/letter-mgmt/certificates/share-certificate/share-certificate.component.html
@@ -14,151 +14,184 @@
Share Certificate
- 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.
+ }
+