fix: update sponsor DD approvals, cashier transaction entry, FIU and dividend declaration

This commit is contained in:
2026-09-21 10:24:08 +05:30
parent 3c363d0372
commit a6e6220100
14 changed files with 1745 additions and 234 deletions
@@ -21,14 +21,31 @@
<!-- The outcome, product by product. Which products were skipped and why matters as much as
which succeeded, so both are shown rather than just a success count. -->
<div class="pl-card dd-panel">
<!-- A run that skipped a product is not a clean success: those members get nothing, and saying
only "declared" is how that goes unnoticed. The heading states it either way. -->
<div class="dd-result-head">
<fa-icon icon="check-circle" class="dd-result-ic"></fa-icon>
<fa-icon
[icon]="hasSkipped ? 'exclamation-triangle' : 'check-circle'"
class="dd-result-ic"
[class.dd-result-ic--warn]="hasSkipped"
></fa-icon>
<div>
<h3>Dividend declared for {{ result.financialYear }}</h3>
<h3>
@if (hasSkipped) {
Declared for {{ result.financialYear }} — {{ result.skipped.length }}
{{ result.skipped.length === 1 ? 'product was' : 'products were' }} skipped
} @else {
Dividend declared for {{ result.financialYear }}
}
</h3>
<p>
{{ result.declarationsCreated }}
{{ result.declarationsCreated === 1 ? 'declaration' : 'declarations' }} created. Each still needs approval
before members can claim.
@if (hasSkipped) {
Members of the skipped {{ result.skipped.length === 1 ? 'product' : 'products' }} below will receive
nothing for this year — check the reason and declare separately if that is not intended.
}
</p>
</div>
</div>
@@ -128,7 +145,22 @@
<span class="dd-amount">− {{ preview.totalAmount | number: '1.2-2' }}</span>
</div>
@if (isUnderfunded) {
@if (exceedsMaximumRate) {
<!-- Checked before the year and funding notes: a rate over the cap is not payable at
all, so what it would cost is beside the point until it is lowered. -->
<div class="dd-note dd-note--error">
{{ declareForm.value.ratePercentage }}% is above the society's maximum of
{{ preview.maximumRatePercentage }}%. Lower the rate, or raise the limit in Configurations if the board
has resolved a higher one.
</div>
} @else if (isPeriodNotFinished) {
<!-- Shown ahead of the funding check: a year still running cannot be declared at any
rate, so the shortfall below it would be advice on a choice that is not available. -->
<div class="dd-note dd-note--error">
{{ preview.financialYear }} has not finished yet, so its profit is not final. A dividend can only be
declared once the financial year is over.
</div>
} @else 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.
@@ -146,16 +178,42 @@
{{ declareForm.value.ratePercentage || 0 }}% on each member’s share investment
</div>
<!-- The member count sits beside each amount because an amount alone does not say who it
reaches. A product paying a plausible total to one member is the case worth catching
here, while the rate can still be changed. -->
@for (product of eligibleProducts; track product.productId) {
<div class="dd-preview-row">
<span>{{ product.productName }}</span>
<span>
{{ product.productName }}
<span class="dd-members">
{{ product.memberCount }} {{ product.memberCount === 1 ? 'member' : 'members' }}
@if (product.accountCount !== product.memberCount) {
· {{ product.accountCount }} accounts
}
</span>
</span>
<span class="dd-amount">{{ product.amount | number: '1.2-2' }}</span>
</div>
}
@if (eligibleProducts.length > 0) {
<div class="dd-preview-row dd-preview-row--total">
<span>Total to declare</span>
<span>
Total to declare
@if (preview.totalMemberCount) {
<!-- Members are distinct people, accounts are what actually gets paid: someone
holding shares in two products is one member with two entitlements, each
claimed on its own draft. Both are shown so the difference reads as the
split it is rather than a miscount. -->
<span class="dd-members">
{{ preview.totalMemberCount }}
{{ preview.totalMemberCount === 1 ? 'member' : 'members' }}
@if (preview.totalAccountCount !== preview.totalMemberCount) {
· {{ preview.totalAccountCount }} accounts
}
</span>
}
</span>
<span class="dd-amount">{{ payableTotal | number: '1.2-2' }}</span>
</div>
}
@@ -148,6 +148,14 @@ form {
text-align: right;
}
// Who a product's amount reaches, set quietly beside the product name so it reads as a
// qualifier on the amount rather than competing with it.
.dd-members {
margin-left: 8px;
font-size: 12px;
color: #94a3b8;
}
.dd-note {
margin-top: 12px;
padding: 10px 14px;
@@ -194,6 +202,11 @@ form {
font-size: 26px;
color: #16a34a;
margin-top: 2px;
// A run that left a product out is amber, not green: it succeeded, but not for everyone.
&--warn {
color: #d97706;
}
}
.dd-table {
@@ -73,6 +73,17 @@ export class DeclareDividendComponent implements OnInit {
/** Set once the declaration has gone through, so the outcome per product can be shown. */
result: any = null;
/**
* Whether any product was left out of the declaration that just ran.
*
* Drives the outcome heading. A run that skipped a product still returns success — one product
* being ineligible is not a reason to lose the others — but those members receive nothing for the
* year, so the screen has to say so rather than report a plain "declared".
*/
get hasSkipped(): boolean {
return (this.result?.skipped?.length ?? 0) > 0;
}
ngOnInit(): void {
this.declareForm = this.formBuilder.group({
financialYear: [
@@ -96,9 +107,13 @@ export class DeclareDividendComponent implements OnInit {
/**
* The financial years a dividend can be declared for: the current one and the five before it.
*
* The current year is included because a society may declare an interim dividend part-way through,
* and five back covers a declaration delayed by an AGM that ran late. Built from the business date
* rather than the browser's clock, so the list matches the year the branch is operating in.
* The year in progress is listed but cannot be declared — the server rejects it, and the screen
* says so as soon as it is picked. It stays on the list so an officer looking for it finds it with
* the reason attached, rather than wondering why the year they are in is missing. Five back covers
* a declaration delayed by an AGM that ran late.
*
* Built from the business date rather than the browser's clock, so the list matches the year the
* branch is operating in.
*/
private buildFinancialYears(): void {
const businessDate: Date = this.settingsService.businessDate ?? new Date();
@@ -171,8 +186,36 @@ export class DeclareDividendComponent implements OnInit {
return !!this.preview?.exceedsDistributableProfit;
}
/**
* Whether the chosen year is still running.
*
* A dividend is a share of the year's profit, so it cannot be declared until the year is over and
* that profit is final. The server enforces this; the flag simply lets the screen say so before the
* officer commits to a rate.
*/
get isPeriodNotFinished(): boolean {
return !!this.preview?.periodNotFinished;
}
/**
* Whether the entered rate is above the society's configured maximum.
*
* The bye-laws cap what may be paid in a year, so a rate above it needs a board resolution rather
* than a typed figure. The server enforces the same limit.
*/
get exceedsMaximumRate(): boolean {
return !!this.preview?.exceedsMaximumRate;
}
get canSubmit(): boolean {
return this.declareForm.valid && !this.submitting && this.eligibleProducts.length > 0 && !this.isUnderfunded;
return (
this.declareForm.valid &&
!this.submitting &&
this.eligibleProducts.length > 0 &&
!this.isUnderfunded &&
!this.isPeriodNotFinished &&
!this.exceedsMaximumRate
);
}
submit(): void {