done
This commit is contained in:
@@ -213,19 +213,11 @@ export class ProductsService {
|
||||
* @param ratePercentage Rate per annum.
|
||||
* @returns {Observable<any>} `{ totalAmount, products: [{ productName, amount, eligible, reason }] }`.
|
||||
*/
|
||||
previewAllProductsDividend(
|
||||
fromDate: string,
|
||||
toDate: string,
|
||||
ratePercentage: number,
|
||||
dividendBasis: string
|
||||
): Observable<any> {
|
||||
previewAllProductsDividend(fromDate: string, toDate: string, ratePercentage: number): Observable<any> {
|
||||
const params: any = { fromDate, toDate };
|
||||
if (ratePercentage != null) {
|
||||
params.ratePercentage = ratePercentage;
|
||||
}
|
||||
if (dividendBasis) {
|
||||
params.dividendBasis = dividendBasis;
|
||||
}
|
||||
return this.http.get('/shareproducts/dividends/preview', { params });
|
||||
}
|
||||
|
||||
@@ -305,6 +297,15 @@ export class ProductsService {
|
||||
return this.http.get(`/shareproduct/${shareProductId}/dividend/${dividendId}/rounding-residue`);
|
||||
}
|
||||
|
||||
/**
|
||||
* The heading of a Share Dividend Distribution Report: the rate the board resolved, what it came
|
||||
* to, and the date it became payable. Those live on the declaration rather than on any member
|
||||
* row, so they are fetched once rather than repeated across the breakdown.
|
||||
*/
|
||||
getDividendReportHeader(dividendId: any): Observable<any> {
|
||||
return this.http.get(`/clerk/dividend-dashboard/declaration/${dividendId}/report-header`);
|
||||
}
|
||||
|
||||
/** Every declaration's rounding residue for one product, newest first, with a running total. */
|
||||
getDividendRoundingResidueHistory(shareProductId: any): Observable<any> {
|
||||
return this.http.get(`/shareproduct/${shareProductId}/dividend/rounding-residue`);
|
||||
|
||||
+21
-3
@@ -182,10 +182,28 @@ export class AllProductsDividendsComponent implements OnInit {
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens a declaration in the existing per-product view, which holds the member-by-member
|
||||
* allocation and the approve action.
|
||||
* Opens a declaration where the officer can actually act on it.
|
||||
*
|
||||
* One still awaiting approval goes to the Approval Centre, because approving is what the officer
|
||||
* came to do and that is where the approve action lives alongside every other pending
|
||||
* declaration — landing them on a read-only page they then have to navigate away from is a
|
||||
* detour. The declaration is named in the query string so the Centre can open its member-by-member
|
||||
* breakdown straight away rather than making them find the row again.
|
||||
*
|
||||
* One already approved has nothing to act on, so it goes to the per-product view to be read.
|
||||
*/
|
||||
viewDividend(row: any): void {
|
||||
if (!row.approved) {
|
||||
this.router.navigate(
|
||||
[
|
||||
'/checker-inbox-and-tasks',
|
||||
'dividend-requests'
|
||||
],
|
||||
{ queryParams: { productId: row.productId, dividendId: row.dividendId } }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.router.navigate(
|
||||
[
|
||||
'/products',
|
||||
@@ -194,7 +212,7 @@ export class AllProductsDividendsComponent implements OnInit {
|
||||
'dividends',
|
||||
row.dividendId
|
||||
],
|
||||
{ queryParams: { status: row.approved ? 'Approved' : 'Initiated' } }
|
||||
{ queryParams: { status: 'Approved' } }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+44
-66
@@ -12,7 +12,7 @@
|
||||
<div class="pl-header-ic"><fa-icon icon="hand-holding-usd"></fa-icon></div>
|
||||
<div class="pl-header-text">
|
||||
<h2>Declare Dividend</h2>
|
||||
<p>One rate for the year, applied to every share product at once.</p>
|
||||
<p>One rate for the year, applied to every member’s share investment.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -70,34 +70,15 @@
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div class="dd-basis">
|
||||
<span class="dd-basis-label">Dividend Basis</span>
|
||||
<mat-radio-group formControlName="dividendBasis">
|
||||
<mat-radio-button value="RATE_ON_CAPITAL">Rate on share capital</mat-radio-button>
|
||||
<mat-radio-button value="PERCENT_OF_PROFIT">Percentage of distributable profit</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
<p class="dd-hint">
|
||||
@if (declareForm.value.dividendBasis === 'PERCENT_OF_PROFIT') {
|
||||
A share of the year's distributable profit, split across products in proportion to the capital each
|
||||
carries.
|
||||
} @else {
|
||||
Each member receives this rate on their own paid-up capital, pro-rated for how long they held it.
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="dd-fields">
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-label>
|
||||
{{
|
||||
declareForm.value.dividendBasis === 'PERCENT_OF_PROFIT'
|
||||
? 'Percentage of Profit'
|
||||
: 'Dividend Rate (% per annum)'
|
||||
}}
|
||||
</mat-label>
|
||||
<mat-label>Dividend Rate (% per annum)</mat-label>
|
||||
<input matInput type="number" step="0.01" min="0.01" max="100" formControlName="ratePercentage" />
|
||||
<span matSuffix> % </span>
|
||||
</mat-form-field>
|
||||
<p class="dd-hint">
|
||||
Applied to each member's own share investment, pro-rated for how long they held it.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if (loadingPreview) {
|
||||
@@ -109,51 +90,62 @@
|
||||
}
|
||||
|
||||
@if (preview && !loadingPreview) {
|
||||
<!-- The appropriation waterfall. Shown in full because the officer is approving a
|
||||
division of the society's profit, not a single number: ₹2,22,000 on its own gives no
|
||||
way to tell a small profit from a large one already heavily appropriated, and the
|
||||
reserve and education rates are configurable so their amounts cannot be inferred. -->
|
||||
<!-- The calculation itself: share investment x rate, and nothing else. Profit does not
|
||||
size the dividend, so it does not appear here — it is reported below only as the
|
||||
account the payout is drawn from. -->
|
||||
<div class="dd-preview">
|
||||
<div class="dd-preview-head">Where the money comes from</div>
|
||||
<div class="dd-preview-head">How the dividend is worked out</div>
|
||||
|
||||
<div class="dd-preview-row">
|
||||
<span>
|
||||
Net profit for {{ preview.financialYear }}
|
||||
<span class="dd-gl">{{ preview.profitGlCode }}</span>
|
||||
</span>
|
||||
<span class="dd-amount">{{ preview.netProfit | number: '1.2-2' }}</span>
|
||||
<span>Share investment (weighted for holding period)</span>
|
||||
<span class="dd-amount">{{ preview.totalWeightedCapital | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="dd-preview-row dd-preview-row--deduction">
|
||||
<span>Less statutory reserve ({{ preview.statutoryReservePercentage | number: '1.0-2' }}%)</span>
|
||||
<span class="dd-amount">− {{ preview.statutoryReserveAmount | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="dd-preview-row dd-preview-row--deduction">
|
||||
<span>Less education fund ({{ preview.educationFundPercentage | number: '1.0-2' }}%)</span>
|
||||
<span class="dd-amount">− {{ preview.educationFundAmount | number: '1.2-2' }}</span>
|
||||
<div class="dd-preview-row dd-preview-row--muted">
|
||||
<span>Dividend rate</span>
|
||||
<span class="dd-amount">{{ declareForm.value.ratePercentage || 0 }}%</span>
|
||||
</div>
|
||||
|
||||
<div class="dd-preview-row dd-preview-row--subtotal">
|
||||
<span>Distributable profit</span>
|
||||
<span class="dd-amount">{{ preview.distributableProfit | number: '1.2-2' }}</span>
|
||||
<span>Dividend payable</span>
|
||||
<span class="dd-amount">{{ preview.totalAmount | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Where the money is drawn from. A funding position, not a basis: the amount above is
|
||||
already fixed by capital, and this only says whether the profit account can pay it. -->
|
||||
<div class="dd-preview">
|
||||
<div class="dd-preview-head">Funded from</div>
|
||||
|
||||
<div class="dd-preview-row">
|
||||
<span>
|
||||
Bank profit for {{ preview.financialYear }}
|
||||
<span class="dd-gl">{{ preview.profitGlCode }}</span>
|
||||
</span>
|
||||
<span class="dd-amount">{{ preview.fundingAvailable | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
|
||||
@if (declareForm.value.dividendBasis === 'RATE_ON_CAPITAL') {
|
||||
<div class="dd-preview-row dd-preview-row--deduction">
|
||||
<span>Less dividend payable</span>
|
||||
<span class="dd-amount">− {{ preview.totalAmount | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
|
||||
@if (isUnderfunded) {
|
||||
<div class="dd-note dd-note--error">
|
||||
Short by {{ preview.fundingShortfall | number: '1.2-2' }}. There is not enough in
|
||||
{{ preview.profitGlCode }} to fund this dividend — lower the rate, or close the year's profit first.
|
||||
</div>
|
||||
} @else {
|
||||
<div class="dd-preview-row dd-preview-row--muted">
|
||||
<span>Weighted share capital</span>
|
||||
<span class="dd-amount">{{ preview.totalWeightedCapital | number: '1.2-2' }}</span>
|
||||
<span>Left in profit (swept to Bank Profit on approval)</span>
|
||||
<span class="dd-amount">{{ preview.undistributedRemainder | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="dd-preview">
|
||||
<div class="dd-preview-head">
|
||||
{{
|
||||
declareForm.value.dividendBasis === 'PERCENT_OF_PROFIT'
|
||||
? (declareForm.value.ratePercentage || 0) + '% of distributable profit, split by capital'
|
||||
: (declareForm.value.ratePercentage || 0) + '% on each member’s capital'
|
||||
}}
|
||||
{{ declareForm.value.ratePercentage || 0 }}% on each member’s share investment
|
||||
</div>
|
||||
|
||||
@for (product of eligibleProducts; track product.productId) {
|
||||
@@ -168,13 +160,6 @@
|
||||
<span>Total to declare</span>
|
||||
<span class="dd-amount">{{ payableTotal | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
|
||||
<!-- What stays behind. Called out because on approval it leaves the distributable
|
||||
pool for good — no later year's dividend can draw on it. -->
|
||||
<div class="dd-preview-row dd-preview-row--muted">
|
||||
<span>Stays in profit (swept to Bank Profit on approval)</span>
|
||||
<span class="dd-amount">{{ preview.undistributedRemainder | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@for (product of skippedProducts; track product.productId) {
|
||||
@@ -186,13 +171,6 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (preview.exceedsDistributableProfit) {
|
||||
<div class="dd-note dd-note--error">
|
||||
This is more than the distributable profit for {{ preview.financialYear }}. The declaration will be
|
||||
rejected.
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (eligibleProducts.length === 0) {
|
||||
<div class="dd-note dd-note--error">
|
||||
No share product is eligible for this year, so there is nothing to declare.
|
||||
|
||||
+2
-24
@@ -47,36 +47,14 @@ form {
|
||||
}
|
||||
}
|
||||
|
||||
/* Sits directly under the rate field, where it explains what the number just typed applies to. */
|
||||
.dd-hint {
|
||||
flex: 1 1 100%;
|
||||
margin: 0 0 16px;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
/* Which basis is chosen changes what the percentage means, so it sits above the rate field rather
|
||||
than below it — the officer picks the meaning before typing the number. */
|
||||
.dd-basis {
|
||||
margin: 4px 0 8px;
|
||||
|
||||
mat-radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.dd-hint {
|
||||
margin: 6px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dd-basis-label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #334155;
|
||||
}
|
||||
|
||||
/* The cost of the rate, per product. The officer is committing to a rate, but what leaves the
|
||||
society is an amount — so the amount is shown before the commit, not after. */
|
||||
.dd-preview {
|
||||
|
||||
+29
-22
@@ -16,7 +16,6 @@ import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatOptionModule } from '@angular/material/core';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
|
||||
@@ -31,6 +30,10 @@ import { SettingsService } from '../../../../settings/settings.service';
|
||||
* much capital each product's members held and for how long. So the officer enters the rate and the
|
||||
* year, and the preview shows what it works out to per product before anything is committed. That
|
||||
* preview is the point of the screen: a rate looks harmless until you see the amount it implies.
|
||||
*
|
||||
* The rate applies to members' share investment and to nothing else. Profit does not size the
|
||||
* dividend — it only funds it, so the screen leads with capital x rate and reports the profit
|
||||
* account only as the source the payout is drawn from.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-declare-dividend',
|
||||
@@ -44,7 +47,6 @@ import { SettingsService } from '../../../../settings/settings.service';
|
||||
MatInputModule,
|
||||
MatSelectModule,
|
||||
MatOptionModule,
|
||||
MatRadioModule,
|
||||
MatProgressBarModule,
|
||||
FaIconComponent
|
||||
]
|
||||
@@ -77,9 +79,6 @@ export class DeclareDividendComponent implements OnInit {
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
// Rate on capital is the default because it is what a dividend rate means in an AGM
|
||||
// resolution: each member receives that rate on their own capital, not a slice of a pool.
|
||||
dividendBasis: ['RATE_ON_CAPITAL'],
|
||||
ratePercentage: [
|
||||
'',
|
||||
[
|
||||
@@ -131,21 +130,19 @@ export class DeclareDividendComponent implements OnInit {
|
||||
|
||||
this.loadingPreview = true;
|
||||
this.previewError = null;
|
||||
this.productsService
|
||||
.previewAllProductsDividend(year.start, year.end, rate, this.declareForm.value.dividendBasis)
|
||||
.subscribe({
|
||||
next: (response: any) => {
|
||||
this.preview = response;
|
||||
this.loadingPreview = false;
|
||||
},
|
||||
error: (error: any) => {
|
||||
// Cleared rather than left stale, so a failed refresh can never be read as the cost of the
|
||||
// rate now on screen.
|
||||
this.preview = null;
|
||||
this.previewError = error?.error?.defaultUserMessage ?? 'Could not work out what this rate would cost.';
|
||||
this.loadingPreview = false;
|
||||
}
|
||||
});
|
||||
this.productsService.previewAllProductsDividend(year.start, year.end, rate).subscribe({
|
||||
next: (response: any) => {
|
||||
this.preview = response;
|
||||
this.loadingPreview = false;
|
||||
},
|
||||
error: (error: any) => {
|
||||
// Cleared rather than left stale, so a failed refresh can never be read as the cost of the
|
||||
// rate now on screen.
|
||||
this.preview = null;
|
||||
this.previewError = error?.error?.defaultUserMessage ?? 'Could not work out what this rate would cost.';
|
||||
this.loadingPreview = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Products that will actually receive a declaration. */
|
||||
@@ -163,8 +160,19 @@ export class DeclareDividendComponent implements OnInit {
|
||||
return this.eligibleProducts.reduce((sum: number, p: any) => sum + Number(p.amount || 0), 0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the profit account can cover what this rate works out to.
|
||||
*
|
||||
* The rate decides the amount; profit only has to be able to pay it. Surfaced here so the officer
|
||||
* sees the shortfall on screen rather than having the command layer reject the declaration after
|
||||
* they have already committed to it — the same ceiling is enforced there either way.
|
||||
*/
|
||||
get isUnderfunded(): boolean {
|
||||
return !!this.preview?.exceedsDistributableProfit;
|
||||
}
|
||||
|
||||
get canSubmit(): boolean {
|
||||
return this.declareForm.valid && !this.submitting && this.eligibleProducts.length > 0;
|
||||
return this.declareForm.valid && !this.submitting && this.eligibleProducts.length > 0 && !this.isUnderfunded;
|
||||
}
|
||||
|
||||
submit(): void {
|
||||
@@ -179,7 +187,6 @@ export class DeclareDividendComponent implements OnInit {
|
||||
.createAllProductsDividend({
|
||||
dividendPeriodStartDate: year.start,
|
||||
dividendPeriodEndDate: year.end,
|
||||
dividendBasis: this.declareForm.value.dividendBasis,
|
||||
ratePercentage: String(this.declareForm.value.ratePercentage)
|
||||
})
|
||||
.subscribe({
|
||||
|
||||
@@ -80,30 +80,6 @@
|
||||
}
|
||||
<span></span>
|
||||
</div>
|
||||
@if (profitBreakdown.statutoryReservePercentage > 0) {
|
||||
<div class="dividend-waterfall__row dividend-waterfall__row--deduction">
|
||||
<span
|
||||
>− {{ 'labels.inputs.Statutory Reserve Fund' | translate }} ({{
|
||||
profitBreakdown.statutoryReservePercentage
|
||||
}}%)</span
|
||||
>
|
||||
<span>{{ profitBreakdown.statutoryReserveAmount | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (profitBreakdown.educationFundPercentage > 0) {
|
||||
<div class="dividend-waterfall__row dividend-waterfall__row--deduction">
|
||||
<span
|
||||
>− {{ 'labels.inputs.Education Fund' | translate }} ({{
|
||||
profitBreakdown.educationFundPercentage
|
||||
}}%)</span
|
||||
>
|
||||
<span>{{ profitBreakdown.educationFundAmount | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="dividend-waterfall__row dividend-waterfall__row--subtotal">
|
||||
<span>{{ 'labels.inputs.Distributable Profit' | translate }}</span>
|
||||
<span>{{ profitBreakdown.distributableProfit | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
@if (profitBreakdown.alreadyDeclared > 0) {
|
||||
<div class="dividend-waterfall__row dividend-waterfall__row--deduction">
|
||||
<span>− {{ 'labels.inputs.Already declared for other products' | translate }}</span>
|
||||
@@ -126,40 +102,9 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Which question the percentage below answers. RATE_ON_CAPITAL is the default because
|
||||
it is what a rate in an AGM resolution promises each member. -->
|
||||
<div class="dividend-mode">
|
||||
<span class="dividend-mode__label">{{ 'labels.inputs.Dividend Basis' | translate }}</span>
|
||||
<mat-radio-group [value]="dividendMode" (change)="onModeChange($event.value)">
|
||||
<mat-radio-button value="RATE_ON_CAPITAL">
|
||||
{{ 'labels.inputs.Rate on share capital' | translate }}
|
||||
</mat-radio-button>
|
||||
<mat-radio-button value="PERCENT_OF_PROFIT">
|
||||
{{ 'labels.inputs.Percentage of distributable profit' | translate }}
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
<p class="dividend-mode__hint">
|
||||
@if (dividendMode === 'RATE_ON_CAPITAL') {
|
||||
{{
|
||||
'labels.inputs.Each member receives this rate on their own paid-up capital, pro-rated for how long they held it'
|
||||
| translate
|
||||
}}.
|
||||
} @else {
|
||||
{{
|
||||
'labels.inputs.This share of distributable profit is split across members by holding period — it is not the rate any individual member receives'
|
||||
| translate
|
||||
}}.
|
||||
}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<mat-form-field>
|
||||
<mat-label>
|
||||
@if (dividendMode === 'RATE_ON_CAPITAL') {
|
||||
{{ 'labels.inputs.Dividend Rate' | translate }} (% {{ 'labels.inputs.per annum' | translate }})
|
||||
} @else {
|
||||
{{ 'labels.inputs.Dividend Percentage' | translate }} (%)
|
||||
}
|
||||
{{ 'labels.inputs.Dividend Rate' | translate }} (% {{ 'labels.inputs.per annum' | translate }})
|
||||
</mat-label>
|
||||
<input matInput type="number" min="0" max="100" step="0.01" required formControlName="dividendPercentage" />
|
||||
@if (createDividendForm.controls.dividendPercentage.hasError('required')) {
|
||||
@@ -170,41 +115,44 @@
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<p class="dividend-mode__hint">
|
||||
{{
|
||||
'labels.inputs.Each member receives this rate on their own paid-up capital, pro-rated for how long they held it'
|
||||
| translate
|
||||
}}.
|
||||
</p>
|
||||
|
||||
@if (rateCeilingError) {
|
||||
<p class="dividend-profit-note dividend-profit-note--error">{{ rateCeilingError }}</p>
|
||||
}
|
||||
|
||||
@if (dividendMode === 'RATE_ON_CAPITAL') {
|
||||
@if (loadingCapital) {
|
||||
<p class="dividend-profit-note">
|
||||
{{ 'labels.inputs.Calculating eligible share capital' | translate }}...
|
||||
</p>
|
||||
} @else if (capitalLookupError) {
|
||||
<p class="dividend-profit-note dividend-profit-note--error">
|
||||
{{ 'labels.inputs.Could not calculate eligible share capital for this period' | translate }}.
|
||||
</p>
|
||||
} @else if (capitalPreview !== null) {
|
||||
<div class="dividend-waterfall">
|
||||
<div class="dividend-waterfall__row">
|
||||
<span>{{ 'labels.inputs.Eligible share capital (weighted)' | translate }}</span>
|
||||
<span>{{ capitalPreview.weightedCapital | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="dividend-waterfall__row dividend-waterfall__row--detail">
|
||||
<span>{{ 'labels.inputs.Eligible accounts' | translate }}</span>
|
||||
<span>{{ capitalPreview.eligibleAccounts }}</span>
|
||||
</div>
|
||||
@if (capitalPreview.eligibleAccounts === 0 && capitalPreview.minimumActivePeriodDays > 0) {
|
||||
<div class="dividend-waterfall__row dividend-waterfall__row--detail">
|
||||
<span
|
||||
>{{ 'labels.inputs.No member has held shares for the minimum active period of' | translate }}
|
||||
{{ capitalPreview.minimumActivePeriodDays }}
|
||||
{{ 'labels.inputs.days' | translate }}</span
|
||||
>
|
||||
<span></span>
|
||||
</div>
|
||||
}
|
||||
@if (loadingCapital) {
|
||||
<p class="dividend-profit-note">{{ 'labels.inputs.Calculating eligible share capital' | translate }}...</p>
|
||||
} @else if (capitalLookupError) {
|
||||
<p class="dividend-profit-note dividend-profit-note--error">
|
||||
{{ 'labels.inputs.Could not calculate eligible share capital for this period' | translate }}.
|
||||
</p>
|
||||
} @else if (capitalPreview !== null) {
|
||||
<div class="dividend-waterfall">
|
||||
<div class="dividend-waterfall__row">
|
||||
<span>{{ 'labels.inputs.Eligible share capital (weighted)' | translate }}</span>
|
||||
<span>{{ capitalPreview.weightedCapital | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="dividend-waterfall__row dividend-waterfall__row--detail">
|
||||
<span>{{ 'labels.inputs.Eligible accounts' | translate }}</span>
|
||||
<span>{{ capitalPreview.eligibleAccounts }}</span>
|
||||
</div>
|
||||
@if (capitalPreview.eligibleAccounts === 0 && capitalPreview.minimumActivePeriodDays > 0) {
|
||||
<div class="dividend-waterfall__row dividend-waterfall__row--detail">
|
||||
<span
|
||||
>{{ 'labels.inputs.No member has held shares for the minimum active period of' | translate }}
|
||||
{{ capitalPreview.minimumActivePeriodDays }}
|
||||
{{ 'labels.inputs.days' | translate }}</span
|
||||
>
|
||||
<span></span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<mat-form-field>
|
||||
|
||||
@@ -57,28 +57,11 @@
|
||||
}
|
||||
}
|
||||
|
||||
.dividend-mode {
|
||||
/* Sits under the rate field, saying what the number just typed applies to. */
|
||||
.dividend-mode__hint {
|
||||
margin: 0 0 12px;
|
||||
|
||||
&__label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #344054;
|
||||
}
|
||||
|
||||
mat-radio-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
&__hint {
|
||||
margin: 6px 0 0;
|
||||
font-size: 12px;
|
||||
color: #667085;
|
||||
}
|
||||
font-size: 12px;
|
||||
color: #667085;
|
||||
}
|
||||
|
||||
.dividend-profit-note {
|
||||
|
||||
@@ -10,7 +10,6 @@
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { MatRadioGroup, MatRadioButton } from '@angular/material/radio';
|
||||
import { DecimalPipe } from '@angular/common';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
|
||||
@@ -29,8 +28,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
styleUrls: ['./create-dividend.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatRadioGroup,
|
||||
MatRadioButton,
|
||||
DecimalPipe
|
||||
]
|
||||
})
|
||||
@@ -53,9 +50,10 @@ export class CreateDividendComponent implements OnInit {
|
||||
maxDate = new Date();
|
||||
|
||||
/**
|
||||
* The full appropriation waterfall for the selected period, from the backend: net profit (posted
|
||||
* GL journal entries), less the statutory appropriations that legally precede a dividend, less
|
||||
* dividends already declared for other share products over an overlapping period.
|
||||
* The profit available to fund a dividend for the selected period, from the backend: the profit
|
||||
* standing in the GL, less dividends already declared for other share products over an
|
||||
* overlapping period. The statutory reserve and education fund are not part of this — they are
|
||||
* appropriated on the profit-distribution screen before a dividend is ever declared.
|
||||
*/
|
||||
profitBreakdown: {
|
||||
financialYear: string;
|
||||
@@ -64,10 +62,6 @@ export class CreateDividendComponent implements OnInit {
|
||||
yearClosed: boolean;
|
||||
profitSource: 'BANK_PROFIT_GL' | 'OPEN_PROFIT_AND_LOSS';
|
||||
netProfit: number;
|
||||
statutoryReservePercentage: number;
|
||||
statutoryReserveAmount: number;
|
||||
educationFundPercentage: number;
|
||||
educationFundAmount: number;
|
||||
distributableProfit: number;
|
||||
alreadyDeclared: number;
|
||||
availableForDeclaration: number;
|
||||
@@ -85,8 +79,8 @@ export class CreateDividendComponent implements OnInit {
|
||||
profitLookupError = false;
|
||||
|
||||
/**
|
||||
* Weighted paid-up capital for the period, for RATE_ON_CAPITAL mode. `requiredAmount` is what the
|
||||
* entered rate actually costs to honour.
|
||||
* Weighted paid-up capital for the period — the only thing the dividend is calculated from.
|
||||
* `requiredAmount` is what the entered rate actually costs to honour.
|
||||
*/
|
||||
capitalPreview: {
|
||||
weightedCapital: number;
|
||||
@@ -98,13 +92,15 @@ export class CreateDividendComponent implements OnInit {
|
||||
capitalLookupError = false;
|
||||
|
||||
/**
|
||||
* RATE_ON_CAPITAL is the default because it is what a dividend rate means in an AGM resolution:
|
||||
* each member receives the declared rate on their own paid-up capital, pro-rated for how long
|
||||
* they held it. PERCENT_OF_PROFIT sizes the pool as a share of profit instead — legitimate when
|
||||
* the committee decides "distribute this much of the profit", but the resulting per-member rate
|
||||
* then depends on how many other members hold shares, so it is not the rate members are promised.
|
||||
* A dividend is a rate on each member's own share investment, and nothing else.
|
||||
*
|
||||
* This is what a rate means in an AGM resolution: each member receives the declared rate on their
|
||||
* own paid-up capital, pro-rated for how long they held it. Sizing the pool as a share of profit
|
||||
* instead was dropped — the resulting per-member rate depends on how many other members hold
|
||||
* shares, so it is not the rate members are promised. Profit no longer sizes the dividend; it
|
||||
* only funds it, which is what {@link ceilingError} checks.
|
||||
*/
|
||||
dividendMode: 'RATE_ON_CAPITAL' | 'PERCENT_OF_PROFIT' = 'RATE_ON_CAPITAL';
|
||||
readonly dividendMode = 'RATE_ON_CAPITAL';
|
||||
|
||||
/**
|
||||
* Financial years a dividend can be declared for, newest first.
|
||||
@@ -302,10 +298,6 @@ export class CreateDividendComponent implements OnInit {
|
||||
yearClosed: !!response.yearClosed,
|
||||
profitSource: response.profitSource,
|
||||
netProfit: Number(response.netProfit) || 0,
|
||||
statutoryReservePercentage: Number(response.statutoryReservePercentage) || 0,
|
||||
statutoryReserveAmount: Number(response.statutoryReserveAmount) || 0,
|
||||
educationFundPercentage: Number(response.educationFundPercentage) || 0,
|
||||
educationFundAmount: Number(response.educationFundAmount) || 0,
|
||||
distributableProfit: Number(response.distributableProfit) || 0,
|
||||
alreadyDeclared: Number(response.alreadyDeclared) || 0,
|
||||
availableForDeclaration: Number(response.availableForDeclaration) || 0,
|
||||
@@ -358,22 +350,13 @@ export class CreateDividendComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/** Switches between the two ways of sizing the pool and recomputes under the new one. */
|
||||
onModeChange(mode: 'RATE_ON_CAPITAL' | 'PERCENT_OF_PROFIT'): void {
|
||||
this.dividendMode = mode;
|
||||
this.recomputeDividendAmount();
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the dividend amount under whichever mode is selected, then checks it against both
|
||||
* ceilings — the rate cap in the governing Act, and the profit still available to distribute.
|
||||
* Computes the dividend amount — weighted paid-up capital x rate — then checks it against both
|
||||
* ceilings: the rate cap in the governing Act, and the profit still available to fund it.
|
||||
*
|
||||
* RATE_ON_CAPITAL: amount = weighted paid-up capital x rate. Every member then receives that rate
|
||||
* on their own capital, pro-rated by holding period.
|
||||
*
|
||||
* PERCENT_OF_PROFIT: amount = distributable profit x percentage. Note this is applied to the
|
||||
* DISTRIBUTABLE figure, not raw net profit — the statutory appropriations come out first either
|
||||
* way.
|
||||
* Every member receives the declared rate on their own capital, pro-rated by holding period, so
|
||||
* the amount follows entirely from what members invested. Profit does not enter the arithmetic;
|
||||
* it only has to be large enough to pay the result.
|
||||
*/
|
||||
recomputeDividendAmount(): void {
|
||||
const percentage = Number(this.createDividendForm.get('dividendPercentage').value);
|
||||
@@ -388,36 +371,27 @@ export class CreateDividendComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
// The rate cap governs a rate promised on capital. Under PERCENT_OF_PROFIT the number entered
|
||||
// is a share of profit, not a rate any member receives, so the cap does not apply to it.
|
||||
// The rate cap governs the rate promised on capital, which is exactly what is entered here.
|
||||
const maximumRate = this.profitBreakdown?.maximumRatePercentage ?? 0;
|
||||
if (this.dividendMode === 'RATE_ON_CAPITAL' && maximumRate > 0 && percentage > maximumRate) {
|
||||
if (maximumRate > 0 && percentage > maximumRate) {
|
||||
this.rateCeilingError = `The maximum dividend rate permitted is ${maximumRate}%. Reduce the rate to declare this dividend.`;
|
||||
}
|
||||
|
||||
let amount: number | null = null;
|
||||
if (this.dividendMode === 'RATE_ON_CAPITAL') {
|
||||
if (this.capitalPreview === null) {
|
||||
this.createDividendForm.get('dividendAmount').setValue('');
|
||||
return;
|
||||
}
|
||||
amount = (this.capitalPreview.weightedCapital * percentage) / 100;
|
||||
this.capitalPreview.requiredAmount = Math.round(amount * 100) / 100;
|
||||
} else {
|
||||
if (this.profitBreakdown === null) {
|
||||
this.createDividendForm.get('dividendAmount').setValue('');
|
||||
return;
|
||||
}
|
||||
amount = (this.profitBreakdown.distributableProfit * percentage) / 100;
|
||||
if (this.capitalPreview === null) {
|
||||
this.createDividendForm.get('dividendAmount').setValue('');
|
||||
return;
|
||||
}
|
||||
|
||||
const amount = (this.capitalPreview.weightedCapital * percentage) / 100;
|
||||
const rounded = Math.round(amount * 100) / 100;
|
||||
this.capitalPreview.requiredAmount = rounded;
|
||||
this.createDividendForm.get('dividendAmount').setValue(rounded);
|
||||
|
||||
// Not a limit on the calculation — a limit on what the profit account can fund. The amount is
|
||||
// already fixed by capital; this only says whether there is enough profit to pay it.
|
||||
if (this.profitBreakdown && rounded > this.profitBreakdown.availableForDeclaration) {
|
||||
this.ceilingError =
|
||||
`This dividend of ${rounded.toFixed(2)} exceeds the ${this.profitBreakdown.availableForDeclaration.toFixed(2)} ` +
|
||||
`still available to distribute for this period. Reduce the rate, or declare for a period with more profit.`;
|
||||
`still available to fund it for this period. Reduce the rate, or declare for a period with more profit.`;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -17,18 +17,34 @@
|
||||
>
|
||||
</div>
|
||||
<div class="vd-header__actions">
|
||||
<!-- Standalone, this is a read-only breakdown and the button hands off to the Approval
|
||||
Centre, so it is labelled for where it goes and needs no approve permission to press.
|
||||
Embedded, this IS the Approval Centre's breakdown, so it approves in place. -->
|
||||
@if (!isdividendPosted) {
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
class="vd-approve-btn"
|
||||
*mifosxHasPermission="'POST_DIVIDENDS'"
|
||||
(click)="postDividends()"
|
||||
>
|
||||
<fa-icon icon="check" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Approve' | translate }}
|
||||
</button>
|
||||
@if (embedded) {
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
class="vd-approve-btn"
|
||||
*mifosxHasPermission="'POST_DIVIDENDS'"
|
||||
(click)="postDividends()"
|
||||
>
|
||||
<fa-icon icon="check" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Approve' | translate }}
|
||||
</button>
|
||||
} @else {
|
||||
<button mat-raised-button color="primary" class="vd-approve-btn" (click)="postDividends()">
|
||||
<fa-icon icon="arrow-right" class="m-r-10"></fa-icon>
|
||||
Go to Approval Centre
|
||||
</button>
|
||||
}
|
||||
}
|
||||
<!-- The distribution report: the heading the board resolved plus the member-wise split it
|
||||
produced, in one sheet so the two never have to be reconciled by hand. -->
|
||||
<button mat-stroked-button class="vd-export-btn" [disabled]="exporting" (click)="exportToExcel()">
|
||||
<fa-icon icon="file-excel" class="m-r-10"></fa-icon>
|
||||
{{ exporting ? 'Preparing…' : 'Export to Excel' }}
|
||||
</button>
|
||||
@if (embedded) {
|
||||
<button type="button" class="vd-close-btn" (click)="close()" aria-label="Close">
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
@@ -38,6 +54,15 @@
|
||||
</div>
|
||||
|
||||
<div class="vd-summary">
|
||||
<!-- The rate and the payment date are what a distribution report is quoted by — an AGM
|
||||
resolution says "9% for 2025-26", not "₹11,300" — so they sit alongside the total rather
|
||||
than being left to be worked out from it. -->
|
||||
@if (reportHeader?.ratePercentage != null) {
|
||||
<div class="vd-summary__stat">
|
||||
<span class="vd-summary__label">{{ 'labels.inputs.Dividend Rate' | translate }}</span>
|
||||
<span class="vd-summary__value">{{ reportHeader.ratePercentage | number: '1.0-2' }}%</span>
|
||||
</div>
|
||||
}
|
||||
<div class="vd-summary__stat">
|
||||
<span class="vd-summary__label">{{ 'labels.inputs.Total Amount' | translate }}</span>
|
||||
<span class="vd-summary__value">{{ totalAmount | number: '1.2-2' }}</span>
|
||||
@@ -46,6 +71,18 @@
|
||||
<span class="vd-summary__label">{{ 'labels.inputs.Members' | translate }}</span>
|
||||
<span class="vd-summary__value">{{ dividendData.pageItems.length }}</span>
|
||||
</div>
|
||||
@if (reportHeader?.financialYear) {
|
||||
<div class="vd-summary__stat">
|
||||
<span class="vd-summary__label">{{ 'labels.inputs.Financial Year' | translate }}</span>
|
||||
<span class="vd-summary__value">FY {{ reportHeader.financialYear }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (reportHeader?.paymentDate) {
|
||||
<div class="vd-summary__stat">
|
||||
<span class="vd-summary__label">{{ 'labels.inputs.Payment Date' | translate }}</span>
|
||||
<span class="vd-summary__value">{{ reportHeader.paymentDate | slice: 0 : 10 }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="vd-summary__stat">
|
||||
<span class="vd-summary__label">{{ 'labels.inputs.Status' | translate }}</span>
|
||||
<span [class]="isdividendPosted ? 'vd-badge vd-badge--posted' : 'vd-badge vd-badge--pending'">
|
||||
|
||||
@@ -278,3 +278,15 @@ table {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Export sits beside the primary action rather than under the table: a clerk asked for the
|
||||
distribution report wants the file, not the screen, so it belongs where the eye lands first. */
|
||||
.vd-export-btn {
|
||||
margin-right: 8px;
|
||||
color: #1e7f4f;
|
||||
border-color: #cfe3d8;
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,6 +84,15 @@ export class ViewDividendComponent implements OnInit {
|
||||
glTransactionId: string | null;
|
||||
} | null = null;
|
||||
|
||||
/**
|
||||
* What a distribution report has to state besides the member list: the rate the board resolved
|
||||
* and the date the dividend became payable. Null for a declaration made before the rate was
|
||||
* recorded — the heading simply omits what it does not know rather than guessing.
|
||||
*/
|
||||
reportHeader: any = null;
|
||||
|
||||
exporting = false;
|
||||
|
||||
@ViewChild(MatPaginator, { static: true }) paginator: MatPaginator;
|
||||
@ViewChild(MatSort, { static: true }) sort: MatSort;
|
||||
displayedColumns: string[] = [
|
||||
@@ -100,16 +109,34 @@ export class ViewDividendComponent implements OnInit {
|
||||
.getDividendData(this.overrideProductId, this.overrideDividendId)
|
||||
.subscribe((dividendData: any) => this.setDividendData(dividendData));
|
||||
this.loadResidue(this.overrideProductId, this.overrideDividendId);
|
||||
this.loadReportHeader(this.overrideDividendId);
|
||||
} else {
|
||||
this.status = this.route.snapshot.queryParams['status'];
|
||||
this.route.data.subscribe((data: { dividendData: any }) => this.setDividendData(data.dividendData));
|
||||
this.loadResidue(
|
||||
this.route.parent?.parent?.snapshot.paramMap.get('productId'),
|
||||
this.route.snapshot.paramMap.get('dividendId')
|
||||
);
|
||||
const dividendId = this.route.snapshot.paramMap.get('dividendId');
|
||||
this.loadResidue(this.route.parent?.parent?.snapshot.paramMap.get('productId'), dividendId);
|
||||
this.loadReportHeader(dividendId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The rate the board resolved and the date the dividend became payable.
|
||||
*
|
||||
* Neither is on the member breakdown — a rate is a property of the declaration, and the payment
|
||||
* date is when it was approved — but a distribution report has to quote both, so they are pulled
|
||||
* once here. Silently skipped on failure: the breakdown is still perfectly readable without a
|
||||
* heading, and an error banner over a working page helps nobody.
|
||||
*/
|
||||
private loadReportHeader(dividendId: any): void {
|
||||
if (!dividendId) {
|
||||
return;
|
||||
}
|
||||
this.productsService
|
||||
.getDividendReportHeader(dividendId)
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe((res: any) => (this.reportHeader = res));
|
||||
}
|
||||
|
||||
/**
|
||||
* Declarations made before residue was tracked have no record, and the endpoint returns null for
|
||||
* them — the panel simply stays hidden rather than showing zeros that would misrepresent them.
|
||||
@@ -161,27 +188,118 @@ export class ViewDividendComponent implements OnInit {
|
||||
return 'vd-badge vd-badge--pending';
|
||||
}
|
||||
|
||||
/**
|
||||
* Approving happens in the Approval Centre, not here.
|
||||
*
|
||||
* This page is a read-only breakdown reached from the product's dividend register, and a
|
||||
* declaration approved from here would bypass the queue every other pending declaration goes
|
||||
* through — a checker would have no single place that shows what is still outstanding. So the
|
||||
* button hands off to the Approval Centre with this declaration already open, rather than
|
||||
* approving in place.
|
||||
*
|
||||
* The embedded copy IS the Approval Centre's own breakdown, so there it approves directly.
|
||||
*/
|
||||
postDividends() {
|
||||
const shareProductId = this.embedded
|
||||
? this.overrideProductId
|
||||
: this.route.parent.parent.snapshot.paramMap.get('productId');
|
||||
const dividendId = this.embedded ? this.overrideDividendId : this.route.snapshot.paramMap.get('dividendId');
|
||||
|
||||
if (!this.embedded) {
|
||||
this.router.navigate(
|
||||
[
|
||||
'/checker-inbox-and-tasks',
|
||||
'dividend-requests'
|
||||
],
|
||||
{ queryParams: { productId: shareProductId, dividendId: dividendId } }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only the embedded copy reaches here — the standalone page returned above — so this is always
|
||||
// the Approval Centre approving from its own modal, which closes and refreshes its list.
|
||||
this.productsService
|
||||
.approveDividend(shareProductId, dividendId, { productId: shareProductId, dividendId: dividendId })
|
||||
.subscribe(() => {
|
||||
if (this.embedded) {
|
||||
this.actionCompleted.emit();
|
||||
} else {
|
||||
// Back to the society-wide dividend register. Not '../', which used to be this product's
|
||||
// own dividend list — that page is gone now that a dividend is declared for every product
|
||||
// at once, and relative navigation would land on a route that no longer exists.
|
||||
this.router.navigate([
|
||||
'/products',
|
||||
'share-products',
|
||||
'dividends'
|
||||
]);
|
||||
}
|
||||
.subscribe(() => this.actionCompleted.emit());
|
||||
}
|
||||
|
||||
/**
|
||||
* Share Dividend Distribution Report as an Excel workbook.
|
||||
*
|
||||
* Two parts in one sheet, because that is what the report is: a heading stating what the board
|
||||
* resolved — the rate, the amount, the payment date — and beneath it the member-wise distribution
|
||||
* that heading produced. Split across two files they would have to be reconciled by hand.
|
||||
*
|
||||
* Every member is exported, including those whose entitlement rounded to nothing: a distribution
|
||||
* report that silently omits them cannot be checked against the member register.
|
||||
*/
|
||||
async exportToExcel(): Promise<void> {
|
||||
this.exporting = true;
|
||||
try {
|
||||
const ExcelJS = await import('exceljs');
|
||||
const wb = new ExcelJS.Workbook();
|
||||
const ws = wb.addWorksheet('Dividend Distribution');
|
||||
const h = this.reportHeader;
|
||||
const fy = h?.financialYear ?? '';
|
||||
|
||||
ws.mergeCells('A1:D1');
|
||||
ws.getCell('A1').value = 'SHARE DIVIDEND DISTRIBUTION REPORT';
|
||||
ws.getCell('A1').font = { bold: true, size: 14 };
|
||||
ws.getCell('A1').alignment = { horizontal: 'center' };
|
||||
|
||||
const heading: [string, any][] = [
|
||||
['Share Product', h?.productName ?? ''],
|
||||
['Financial Year', fy],
|
||||
['Dividend Period', h ? `${h.periodStartDate} to ${h.periodEndDate}` : ''],
|
||||
['Dividend Rate (% per annum)', h?.ratePercentage != null ? Number(h.ratePercentage) : 'Not recorded'],
|
||||
['Declared Amount', h?.declaredAmount != null ? Number(h.declaredAmount) : ''],
|
||||
['Distributed to Members', h?.allocatedAmount != null ? Number(h.allocatedAmount) : ''],
|
||||
['Payment Date', h?.paymentDate ? String(h.paymentDate).slice(0, 10) : 'Not yet approved'],
|
||||
['Status', h?.status === 'APPROVED' ? 'Approved' : 'Awaiting Approval'],
|
||||
['Members Paid', h?.paidMembers ?? ''],
|
||||
['Members Below Minimum', h?.unpaidMembers ?? '']
|
||||
];
|
||||
heading.forEach(([label, value], i) => {
|
||||
const row = ws.getRow(i + 3);
|
||||
row.getCell(1).value = label;
|
||||
row.getCell(1).font = { bold: true };
|
||||
row.getCell(2).value = value;
|
||||
});
|
||||
|
||||
const tableStart = heading.length + 5;
|
||||
const header = ws.getRow(tableStart);
|
||||
['Client Name', 'Share Account', 'Dividend Amount', 'Status'].forEach((t, i) => {
|
||||
header.getCell(i + 1).value = t;
|
||||
header.getCell(i + 1).font = { bold: true };
|
||||
header.getCell(i + 1).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFE8ECF0' } };
|
||||
});
|
||||
|
||||
(this.dataSource?.data ?? []).forEach((d: any, i: number) => {
|
||||
const row = ws.getRow(tableStart + 1 + i);
|
||||
row.getCell(1).value = d?.accountData?.clientName ?? '';
|
||||
row.getCell(2).value = d?.accountData?.accountNo ?? '';
|
||||
row.getCell(3).value = Number(d?.amount) || 0;
|
||||
row.getCell(3).numFmt = '#,##0.00';
|
||||
row.getCell(4).value = d?.status?.value ?? '';
|
||||
});
|
||||
|
||||
const totalRow = ws.getRow(tableStart + 1 + (this.dataSource?.data?.length ?? 0));
|
||||
totalRow.getCell(2).value = 'Total';
|
||||
totalRow.getCell(2).font = { bold: true };
|
||||
totalRow.getCell(3).value = (this.dataSource?.data ?? []).reduce((s: number, d: any) => s + (Number(d?.amount) || 0), 0);
|
||||
totalRow.getCell(3).numFmt = '#,##0.00';
|
||||
totalRow.getCell(3).font = { bold: true };
|
||||
|
||||
ws.getColumn(1).width = 34;
|
||||
ws.getColumn(2).width = 20;
|
||||
ws.getColumn(3).width = 18;
|
||||
ws.getColumn(4).width = 20;
|
||||
|
||||
const { downloadWorkbook } = await import('../../../rbi-circulars/oss-workbook');
|
||||
await downloadWorkbook(wb, `Dividend Distribution Report${fy ? ' - FY ' + fy : ''}.xlsx`);
|
||||
} finally {
|
||||
this.exporting = false;
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
|
||||
Reference in New Issue
Block a user