share certificate and share product maker checker
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
+174
@@ -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…
|
||||
</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 }}–{{ 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>
|
||||
+25
@@ -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;
|
||||
}
|
||||
+312
@@ -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)
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
+165
-132
@@ -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 }}–{{ 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 }}–{{ 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) ─────────────────────
|
||||
|
||||
Reference in New Issue
Block a user