feat: implement share account actions, view components, and pending checker tasks
This commit is contained in:
+42
@@ -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">
|
||||
|
||||
+48
@@ -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;
|
||||
|
||||
+75
@@ -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 [
|
||||
|
||||
Reference in New Issue
Block a user