feat: implement share account actions, view components, and pending checker tasks

This commit is contained in:
2026-08-27 10:23:25 +05:30
parent 1273bdbe0e
commit 83e58376cb
32 changed files with 1699 additions and 93 deletions
@@ -99,6 +99,48 @@
{{ cumulativeDividend | currency: sharesAccountData.currency.code : 'symbol-narrow' }}
</td>
</tr>
</tbody>
</table>
<!-- The allotments this certificate covers, one row each. A holding is built up over time —
the opening subscription, then each further tranche under its own board resolution — and
a certificate has to show when each block was allotted, not just the total. -->
@if (allotments.length > 0) {
<table class="allotment-table">
<thead>
<tr>
<th class="ta-left">Date of Allotment</th>
<th class="ta-right">Shares</th>
<th class="ta-right">Face Value</th>
<th class="ta-left">Share Numbers</th>
<th class="ta-left">Resolution No. / Meeting Date</th>
</tr>
</thead>
<tbody>
@for (row of allotments; track $index) {
<tr>
<td class="ta-left">{{ row.date | dateFormat }}</td>
<td class="ta-right">{{ row.shares }}</td>
<td class="ta-right">
{{ row.faceValue | currency: sharesAccountData.currency.code : 'symbol-narrow' }}
</td>
<td class="ta-left">
{{ row.shareNumbersFrom.toLocaleString() }} – {{ row.shareNumbersTo.toLocaleString() }}
</td>
<td class="ta-left">
{{ row.resolutionNumber || '—' }}
@if (row.meetingDate) {
<span class="allotment-meeting">({{ row.meetingDate | dateFormat }})</span>
}
</td>
</tr>
}
</tbody>
</table>
}
<table class="certificate-details-table">
<tbody>
<tr class="highlight-row">
<td colspan="2" class="lbl-col-highlight">TOTAL FACE VALUE</td>
<td colspan="2" class="val-col-highlight">
@@ -206,6 +206,13 @@
}
/* Certificate Data Table */
/* The details grid and the allotment schedule read as one block, so the gap that used to sit under
the details table now sits under whichever table comes last. */
.certificate-details-table:not(:last-of-type) {
margin-bottom: 0;
}
.certificate-details-table {
width: 100%;
border-collapse: collapse;
@@ -250,6 +257,47 @@
}
}
/* The allotment schedule — one row per block of shares allotted, in the same paper-and-ink palette
as the details grid so it reads as part of the certificate rather than a report pasted into it. */
.allotment-table {
position: relative;
z-index: 2; /* Above the watermark, like the other certificate content. */
width: 100%;
border-collapse: collapse;
margin-bottom: 40px;
th {
padding: 8px 12px;
border: 1px solid #ede8db;
background-color: #f7f5ee;
font-size: 11px;
font-weight: bold;
color: #7b6f63;
text-transform: uppercase;
letter-spacing: 0.6px;
}
td {
padding: 9px 12px;
border: 1px solid #ede8db;
font-size: 13px;
color: #322f2b;
}
.allotment-meeting {
color: #7b6f63;
font-size: 12px;
}
.ta-left {
text-align: left;
}
.ta-right {
text-align: right;
}
}
/* Certificate Footer Seal / Signatures */
.cert-footer {
display: flex;
@@ -61,6 +61,27 @@ export class ShareCertificateDialogComponent implements OnInit {
issueDate: Date = new Date();
todayDate: Date = new Date();
/**
* One row per approved purchase, oldest first — the allotments this certificate covers.
*
* A member's holding is built up over time: the opening subscription, then each further tranche,
* every one authorised by its own board resolution on its own date. Collapsing that into a single
* total and one "Date of Issue" loses exactly what a share certificate exists to record — when
* each block of shares was allotted, under which resolution, and which share numbers it covers.
* The running range is what lets a member's 20 shares be read as 10 allotted in August and 10 in
* September rather than as one undated block.
*/
allotments: {
date: any;
shares: number;
unitPrice: number;
faceValue: number;
resolutionNumber: string;
meetingDate: any;
shareNumbersFrom: number;
shareNumbersTo: number;
}[] = [];
ngOnInit() {
this.sharesAccountData = this.data.sharesAccountData;
this.clientId = String(this.data.clientId);
@@ -82,6 +103,18 @@ export class ShareCertificateDialogComponent implements OnInit {
const endNum = startNum + approvedShares - 1;
this.shareNumbersRange = approvedShares > 0 ? `${startNum.toLocaleString()} – ${endNum.toLocaleString()}` : 'None';
this.buildAllotments(startNum);
// The certificate is issued as of the most recent allotment, not the account's activation: a
// certificate reprinted after a further tranche covers those shares too, and dating it from
// activation would say the later shares were issued before they existed.
if (this.allotments.length > 0) {
const latest = this.allotments[this.allotments.length - 1];
if (latest.date) {
this.issueDate = new Date(latest.date);
}
}
// Calculate cumulative dividends
if (this.sharesAccountData.dividends) {
this.cumulativeDividend = this.sharesAccountData.dividends.reduce(
@@ -124,6 +157,48 @@ export class ShareCertificateDialogComponent implements OnInit {
});
}
/**
* Builds the allotment rows from the account's approved purchases.
*
* Only approved purchases count. A pending tranche has not been allotted — the board has not
* approved it and, on the cash and savings routes alike, the money is still sitting in Share
* Sundry — so putting it on a certificate would certify shares the member does not yet hold.
* Rejected ones and charge payments are not allotments at all.
*
* Share numbers run consecutively in purchase order, so each row's range picks up where the last
* left off and the ranges together reconcile to the account's total holding.
*
* @param startNum the first share number of the account's holding.
*/
private buildAllotments(startNum: number): void {
const purchases = (this.sharesAccountData.purchasedShares || [])
.filter(
(txn: any) => txn?.type?.value === 'Purchase' && txn?.status?.code === 'purchasedSharesStatusType.approved'
)
.slice()
.sort((a: any, b: any) => new Date(a.purchasedDate).getTime() - new Date(b.purchasedDate).getTime());
let nextShareNumber = startNum;
this.allotments = purchases.map((txn: any) => {
const shares = Number(txn.numberOfShares) || 0;
const unitPrice = Number(txn.purchasedPrice) || 0;
const from = nextShareNumber;
nextShareNumber += shares;
return {
date: txn.purchasedDate,
shares,
unitPrice,
faceValue: shares * unitPrice,
// The account's own resolution stands in for the opening subscription, which carries none of
// its own; each additional tranche has the resolution captured when it was applied for.
resolutionNumber: txn.resolutionNumber || this.sharesAccountData.resolutionNumber || '',
meetingDate: txn.meetingDate || this.sharesAccountData.resolutionDate || null,
shareNumbersFrom: from,
shareNumbersTo: from + shares - 1
};
});
}
formatAddress(addr: any): string {
if (!addr) return '';
return [