feat: implement cashier dashboard, dividend dashboards, share accounts views, and checker tasks

This commit is contained in:
2026-08-25 11:20:42 +05:30
parent 7ef75c902d
commit 0b79b0b0d3
77 changed files with 7396 additions and 426 deletions
@@ -54,6 +54,24 @@
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
<mat-form-field class="flex-48" (click)="resolutionDatePicker.open()">
<mat-label>{{ 'labels.inputs.Resolution Date' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="resolutionDatePicker"
formControlName="resolutionDate"
required
/>
<mat-datepicker-toggle matSuffix [for]="resolutionDatePicker"></mat-datepicker-toggle>
<mat-datepicker #resolutionDatePicker></mat-datepicker>
<mat-error>
{{ 'labels.inputs.Resolution Date' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
</div>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
@@ -68,7 +86,8 @@
!(
sharesAccountDetailsForm.get('productId')?.value &&
sharesAccountDetailsForm.get('submittedDate')?.value &&
sharesAccountDetailsForm.get('resolutionNumber')?.value
sharesAccountDetailsForm.get('resolutionNumber')?.value &&
sharesAccountDetailsForm.get('resolutionDate')?.value
)
"
>
@@ -76,7 +76,9 @@ export class SharesAccountDetailsStepComponent implements OnInit {
this.sharesAccountTemplate.timeline.submittedOnDate &&
new Date(this.sharesAccountTemplate.timeline.submittedOnDate),
externalId: this.sharesAccountTemplate.externalId,
resolutionNumber: this.sharesAccountTemplate.resolutionNumber
resolutionNumber: this.sharesAccountTemplate.resolutionNumber,
resolutionDate:
this.sharesAccountTemplate.resolutionDate && new Date(this.sharesAccountTemplate.resolutionDate)
});
}
}
@@ -102,6 +104,10 @@ export class SharesAccountDetailsStepComponent implements OnInit {
resolutionNumber: [
'',
Validators.required
],
resolutionDate: [
'',
Validators.required
]
});
}
@@ -41,6 +41,11 @@
<span class="flex-60">{{ sharesAccount.resolutionNumber }}</span>
</div>
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Resolution Date' | translate }}</span>
<span class="flex-60">{{ sharesAccount.resolutionDate | dateFormat }}</span>
</div>
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Terms' | translate }}</h3>
<mat-divider class="flex-fill"></mat-divider>
@@ -71,9 +76,15 @@
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Default Savings Account' | translate }}</span>
<span class="flex-60">{{
sharesAccount.savingsAccountId | find: sharesAccountProductTemplate.clientSavingsAccounts : 'id' : 'accountNo'
}}</span>
<span class="flex-60">
@if (sharesAccount.savingsAccountId) {
{{
sharesAccount.savingsAccountId | find: sharesAccountProductTemplate.clientSavingsAccounts : 'id' : 'accountNo'
}}
} @else {
{{ 'labels.inputs.Unassigned' | translate }}
}
</span>
</div>
@if (sharesAccount.minimumActivePeriod) {
@@ -30,6 +30,9 @@
<span class="flex-48">{{ calculateCurrenValue() | currency: currency.code : 'symbol-narrow' : '1.2-2' }}</span>
}
<!-- No "None" option: the server rejects a share account without a linked savings account, and
offering the choice only produced a failure after the whole stepper was filled in. The link
is also where an over-payment at the counter is credited back to the member. -->
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Default Savings Account' | translate }}</mat-label>
<mat-select formControlName="savingsAccountId" required>
@@ -42,6 +45,12 @@
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
</mat-form-field>
@if (savingsAccountsData?.length === 0) {
<p class="sats-field-note flex-98">
This client has no active savings account in the share product's currency. Open one first — a share account
cannot be created without it.
</p>
}
<mat-form-field class="flex-48" (click)="applicationDatePicker.open()">
<mat-label>{{ 'labels.inputs.Application Date' | translate }}</mat-label>
@@ -67,6 +76,15 @@
<h4 class="mat-h4 flex-98">{{ 'labels.heading.Minimum Active Period' | translate }}</h4>
<!-- Spelled out because the field name does not say what it decides. This is the holding period
that qualifies a member for a dividend: shares bought fewer days before the period end than
this are skipped entirely by the dividend allocation, and the member is paid nothing on
them. Setting it wrong is silent — nobody sees it until a declaration pays out. -->
<p class="sats-field-note flex-98">
Enter the minimum number of days a member must hold these shares before they qualify for a dividend. Shares held
for fewer days than this are excluded from the dividend calculation and earn nothing for that period.
</p>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<input type="number" matInput formControlName="minimumActivePeriod" />
@@ -88,6 +106,15 @@
<h4 class="mat-h4 flex-98">{{ 'labels.heading.Lock-in Period' | translate }}</h4>
<!-- Sits next to Minimum Active Period and is easily confused with it, so the difference is
stated outright: that one decides whether a dividend is earned, this one decides whether
shares can be sold back. Counted per purchase, not per account — a later purchase carries
its own lock-in from its own date, which is not obvious from a single "Frequency" box. -->
<p class="sats-field-note flex-98">
Enter the minimum number of days a member must wait before they can redeem (sell back) these shares. This is
counted separately from each purchase, and does not affect dividends — for that, use Minimum Active Period above.
</p>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Frequency' | translate }}</mat-label>
<input type="number" matInput formControlName="lockinPeriodFrequency" />
@@ -14,3 +14,14 @@ h4 {
.margin-t {
margin-top: 1em;
}
/* Red because both fields it marks fail silently — a wrong Minimum Active Period pays the member
nothing months later, a wrong Lock-in Period only surfaces when a redemption is refused. Neither
raises a validation error at the time. Sits under the heading and above the inputs, so it is read
before the number is typed rather than after. */
.sats-field-note {
margin: -0.5em 0 1em;
color: #b42318;
font-size: 12.5px;
line-height: 1.5;
}
@@ -128,6 +128,13 @@ export class SharesAccountTermsStepComponent implements OnChanges, OnInit {
],
unitPrice: [{ value: '', disabled: true }],
currencyMultiple: [{ value: '', disabled: true }],
// Required to match the server, which has always rejected a share account without a linked
// savings account. The form allowed one through, so the failure surfaced as a validation
// error on submit after the whole stepper had been filled in.
//
// It also gives an over-payment somewhere to go: a member who hands over more cash than the
// application costs has the difference credited to this account, rather than being left in a
// suspense GL that nobody can attribute back to them.
savingsAccountId: [
'',
Validators.required