DDS: approvals tab, vouchers, payout & close, Sundry GL search, closure tab, fixes
This commit is contained in:
@@ -20,11 +20,67 @@
|
||||
<span class="po-summary__label">Matured on</span>
|
||||
<span class="po-summary__value">{{ data.maturityDate | dateFormat }}</span>
|
||||
</div>
|
||||
<div class="po-summary__row po-summary__row--amount">
|
||||
<span class="po-summary__label">Amount payable</span>
|
||||
<span class="po-summary__value">{{ data.currentBalance | number: '1.2-2' }}</span>
|
||||
<div class="po-summary__row">
|
||||
<span class="po-summary__label">Opened on</span>
|
||||
<span class="po-summary__value">{{ data.openingDate | dateFormat }}</span>
|
||||
</div>
|
||||
|
||||
@if (breakdown) {
|
||||
<div class="po-breakdown">
|
||||
<div class="po-summary__row">
|
||||
<span class="po-summary__label">Deposits collected</span>
|
||||
<span class="po-summary__value">{{ breakdown.deposits | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
@if (breakdown.transfersIn > 0) {
|
||||
<div class="po-summary__row">
|
||||
<span class="po-summary__label">Transferred in</span>
|
||||
<span class="po-summary__value">+ {{ breakdown.transfersIn | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (breakdown.transfersOut > 0) {
|
||||
<div class="po-summary__row">
|
||||
<span class="po-summary__label">Transferred out</span>
|
||||
<span class="po-summary__value">− {{ breakdown.transfersOut | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (breakdown.other !== 0) {
|
||||
<div class="po-summary__row">
|
||||
<span class="po-summary__label">Other (opening balance / adjustments)</span>
|
||||
<span class="po-summary__value">{{ breakdown.other | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="po-summary__row">
|
||||
<span class="po-summary__label">
|
||||
Interest @ {{ data.interestRate | number: '1.2-2' }}%
|
||||
<span class="po-summary__note"
|
||||
>{{ data.openingDate | dateFormat }} to {{ data.interestDate | dateFormat }}</span
|
||||
>
|
||||
</span>
|
||||
<span class="po-summary__value">+ {{ breakdown.interest | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
@if (breakdown.interestToPost > 0) {
|
||||
<div class="po-summary__row">
|
||||
<span class="po-summary__label"> Interest calculated, posted with this payout </span>
|
||||
<span class="po-summary__value">+ {{ breakdown.interestToPost | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="po-summary__row po-summary__row--amount">
|
||||
<span class="po-summary__label">Total payable</span>
|
||||
<span class="po-summary__value">{{ (breakdown ? breakdown.total : data.currentBalance) | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
@if (interestStillToCome) {
|
||||
<p class="po-summary__note">
|
||||
Interest from {{ data.interestDate | dateFormat }} to {{ data.maturityDate | dateFormat }} is not worked out yet
|
||||
- it is added when this is paid out, so the amount paid will be a little more.
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
<p class="po-summary__note">
|
||||
Paying out closes {{ data.partyCode }} - the balance is nil afterwards and it takes nothing more.
|
||||
</p>
|
||||
|
||||
@if (loading) {
|
||||
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
|
||||
@@ -46,28 +102,57 @@
|
||||
<mat-label>Account</mat-label>
|
||||
<mat-select [(ngModel)]="selectedAccountNo" panelWidth="min(28rem, 90vw)">
|
||||
@for (account of ownAccounts; track account.id) {
|
||||
<mat-option [value]="account.accountNo">
|
||||
{{ account.accountNo }} · {{ account.productName }}
|
||||
</mat-option>
|
||||
<mat-option [value]="account.accountNo"> {{ account.accountNo }} · {{ account.productName }} </mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
<mat-radio-button value="other" class="po-choice">
|
||||
<span class="po-choice__title">To an account at another bank</span>
|
||||
<span class="po-choice__note">
|
||||
Held in Matured Deposits Payable, then sent by NEFT or IMPS from Fund Transfers
|
||||
</span>
|
||||
<span class="po-choice__title">Park in Sundry</span>
|
||||
<span class="po-choice__note"
|
||||
>When the customer has no account here - held in the bank's Sundry account in their name</span
|
||||
>
|
||||
</mat-radio-button>
|
||||
</mat-radio-group>
|
||||
|
||||
@if (destination === 'other') {
|
||||
<mat-form-field appearance="outline" class="po-account">
|
||||
<mat-label>Park in GL account</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[ngModel]="sundrySearch"
|
||||
(ngModelChange)="onSundryTyped($event)"
|
||||
[matAutocomplete]="sundryAuto"
|
||||
placeholder="Type GL code or name…"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<mat-autocomplete
|
||||
#sundryAuto="matAutocomplete"
|
||||
panelWidth="min(28rem, 90vw)"
|
||||
[displayWith]="sundryDisplay"
|
||||
(optionSelected)="pickSundry($event.option.value)"
|
||||
>
|
||||
@for (account of sundryOptions; track account.id) {
|
||||
<mat-option [value]="account">{{ account.glCode }} · {{ account.name }}</mat-option>
|
||||
}
|
||||
@if (sundryOptions.length === 0) {
|
||||
<mat-option disabled>No liability GL matches</mat-option>
|
||||
}
|
||||
</mat-autocomplete>
|
||||
@if (!sundryGlAccountId) {
|
||||
<mat-hint class="po-hint-warn">Search and pick your bank's Sundry GL account</mat-hint>
|
||||
} @else {
|
||||
<mat-hint>The balance is held here in the customer's name until it is paid to them</mat-hint>
|
||||
}
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
@if (destination === 'other') {
|
||||
<!-- Said plainly, because the officer will be asked by the customer when the money arrives. -->
|
||||
<div class="po-callout">
|
||||
The money comes off {{ data.partyCode }} today and sits in the bank's Matured Deposits Payable
|
||||
account until the transfer is made from Fund Transfers — so it is accounted for the whole
|
||||
time, and a failed transfer loses nothing.
|
||||
The money comes off {{ data.partyCode }} today and is held in the Sundry account chosen above against
|
||||
{{ data.partyName }} until it is paid to them - so it is accounted for the whole time and nothing is lost.
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -75,5 +160,5 @@
|
||||
|
||||
<mat-dialog-actions align="end">
|
||||
<button mat-stroked-button mat-dialog-close>Cancel</button>
|
||||
<button mat-raised-button color="primary" [disabled]="!canPayOut" (click)="payOut()">Pay out</button>
|
||||
<button mat-raised-button color="primary" [disabled]="!canPayOut" (click)="payOut()">Pay out & Close</button>
|
||||
</mat-dialog-actions>
|
||||
|
||||
@@ -38,6 +38,22 @@
|
||||
font-weight: 500;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
&__note {
|
||||
display: block;
|
||||
margin: 0;
|
||||
font-size: 0.75rem;
|
||||
color: rgb(0 0 0 / 45%);
|
||||
}
|
||||
}
|
||||
|
||||
/* How the amount payable is made up - one line per source, adding up to the total below. */
|
||||
.po-breakdown {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px dashed rgb(0 0 0 / 10%);
|
||||
}
|
||||
|
||||
.po-question {
|
||||
@@ -90,3 +106,7 @@
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.po-hint-warn {
|
||||
color: #c62828;
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/materia
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatRadioModule } from '@angular/material/radio';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatAutocompleteModule } from '@angular/material/autocomplete';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
@@ -46,6 +47,7 @@ import { DateFormatPipe } from 'app/pipes/date-format.pipe';
|
||||
MatButtonModule,
|
||||
MatRadioModule,
|
||||
MatSelectModule,
|
||||
MatAutocompleteModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatProgressBarModule,
|
||||
@@ -72,9 +74,90 @@ export class PayoutDialogComponent implements OnInit {
|
||||
*/
|
||||
ownAccounts: any[] = [];
|
||||
selectedAccountNo = '';
|
||||
|
||||
/** Where the balance is parked when the customer has no account here. */
|
||||
sundryAccounts: any[] = [];
|
||||
sundryGlAccountId: number | null = null;
|
||||
/** What is typed in the GL box - a code or part of a name. */
|
||||
sundrySearch = '';
|
||||
|
||||
/** The accounts matching what is typed, by GL code or name, in any case. */
|
||||
get sundryOptions(): any[] {
|
||||
const typed = this.sundrySearch.trim().toLowerCase();
|
||||
if (!typed || typed === this.sundryLabel(this.chosenSundry).toLowerCase()) {
|
||||
return this.sundryAccounts;
|
||||
}
|
||||
return this.sundryAccounts.filter(
|
||||
(account: any) =>
|
||||
String(account.glCode).toLowerCase().includes(typed) ||
|
||||
String(account.name || '')
|
||||
.toLowerCase()
|
||||
.includes(typed)
|
||||
);
|
||||
}
|
||||
|
||||
get chosenSundry(): any {
|
||||
return this.sundryAccounts.find((account: any) => account.id === this.sundryGlAccountId);
|
||||
}
|
||||
|
||||
/** Shows a picked account in the box as its code and name. */
|
||||
readonly sundryDisplay = (value: any): string =>
|
||||
value && typeof value === 'object' ? this.sundryLabel(value) : value || '';
|
||||
|
||||
sundryLabel(account: any): string {
|
||||
return account ? `${account.glCode} · ${account.name}` : '';
|
||||
}
|
||||
|
||||
pickSundry(account: any): void {
|
||||
this.sundryGlAccountId = account ? account.id : null;
|
||||
this.sundrySearch = this.sundryLabel(account);
|
||||
}
|
||||
|
||||
/** Typing again clears the choice until an account is picked, so a half-typed code is never sent. */
|
||||
onSundryTyped(value: any): void {
|
||||
// Picking from the list hands over the account itself rather than text.
|
||||
if (value && typeof value === 'object') {
|
||||
this.pickSundry(value);
|
||||
return;
|
||||
}
|
||||
value = String(value ?? '');
|
||||
this.sundrySearch = value;
|
||||
if (value !== this.sundryLabel(this.chosenSundry)) {
|
||||
const exact = this.sundryAccounts.find(
|
||||
(account: any) => String(account.glCode).toLowerCase() === value.trim().toLowerCase()
|
||||
);
|
||||
this.sundryGlAccountId = exact ? exact.id : null;
|
||||
}
|
||||
}
|
||||
loading = true;
|
||||
|
||||
/**
|
||||
* How the amount payable was made up - what was deposited, what came in or went out by transfer,
|
||||
* and the interest - read from the account's own transactions so the officer can explain the
|
||||
* figure to the customer line by line. Null until the transactions have arrived.
|
||||
*/
|
||||
breakdown: {
|
||||
deposits: number;
|
||||
transfersIn: number;
|
||||
transfersOut: number;
|
||||
interest: number;
|
||||
other: number;
|
||||
interestToPost: number;
|
||||
total: number;
|
||||
} | null = null;
|
||||
|
||||
ngOnInit(): void {
|
||||
// The accounts the balance can be parked in. None is chosen: each bank has its own Sundry account,
|
||||
// and the clerk searches for it and picks it.
|
||||
this.organizationService.getDdsSundryAccounts().subscribe({
|
||||
next: (accounts: any) => (this.sundryAccounts = accounts || []),
|
||||
error: () => (this.sundryAccounts = [])
|
||||
});
|
||||
this.organizationService.getPartyTransactionsOfParty(this.data.id).subscribe({
|
||||
next: (transactions: any) => (this.breakdown = this.breakdownOf(Array.isArray(transactions) ? transactions : [])),
|
||||
error: () => (this.breakdown = null)
|
||||
});
|
||||
|
||||
this.organizationService.getCreditableAccounts(this.data.clientId).subscribe({
|
||||
next: (accounts: any) => {
|
||||
this.ownAccounts = accounts || [];
|
||||
@@ -96,14 +179,52 @@ export class PayoutDialogComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/** Interest is still to be worked out for part of the term, and the payout will add it. */
|
||||
get interestStillToCome(): boolean {
|
||||
return (
|
||||
!!this.data.interestDate &&
|
||||
!!this.data.maturityDate &&
|
||||
String(this.data.interestDate) < String(this.data.maturityDate)
|
||||
);
|
||||
}
|
||||
|
||||
private breakdownOf(transactions: any[]) {
|
||||
const sum = (test: (txn: any) => boolean) =>
|
||||
transactions.filter(test).reduce((total: number, txn: any) => total + Number(txn.amount || 0), 0);
|
||||
const signed = (type: string) =>
|
||||
sum((txn) => txn.transactionType === type && txn.direction === 'CREDIT') -
|
||||
sum((txn) => txn.transactionType === type && txn.direction === 'DEBIT');
|
||||
const isTransfer = (txn: any) => txn.transactionType === 'WITHDRAW_DEPOSITE' || txn.transactionType === 'TRANSFER';
|
||||
|
||||
const deposits = signed('PARTY_TRAN');
|
||||
const transfersIn = sum((txn) => isTransfer(txn) && txn.direction === 'CREDIT');
|
||||
const transfersOut = sum((txn) => isTransfer(txn) && txn.direction === 'DEBIT');
|
||||
const interest =
|
||||
signed('INTEREST_POST') +
|
||||
signed('OD_INTEREST_POST') +
|
||||
signed('PREMATURE_AGENT_TOP_UP') +
|
||||
signed('PREMATURE_INT_RECOVERY');
|
||||
const balance = Number(this.data.currentBalance || 0);
|
||||
// Anything the four lines above do not cover - an opening balance brought over, say - so the lines
|
||||
// still add up to the balance rather than leaving the officer a gap to explain.
|
||||
const other = Math.round((balance - (deposits + transfersIn - transfersOut + interest)) * 100) / 100;
|
||||
// Interest already worked out but not yet credited is posted by the payout itself, first.
|
||||
const interestToPost = Number(this.data.interestCalc || 0);
|
||||
return { deposits, transfersIn, transfersOut, interest, other, interestToPost, total: balance + interestToPost };
|
||||
}
|
||||
|
||||
get canPayOut(): boolean {
|
||||
if (this.loading) {
|
||||
return false;
|
||||
}
|
||||
return this.destination === 'other' || !!this.selectedAccountNo;
|
||||
return this.destination === 'other' ? !!this.sundryGlAccountId : !!this.selectedAccountNo;
|
||||
}
|
||||
|
||||
payOut(): void {
|
||||
this.dialogRef.close(this.destination === 'own' ? { toSavingsAccountNo: this.selectedAccountNo } : {});
|
||||
this.dialogRef.close(
|
||||
this.destination === 'own'
|
||||
? { toSavingsAccountNo: this.selectedAccountNo }
|
||||
: { sundryGlAccountId: this.sundryGlAccountId }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user