feat(share): add Share Collection cashier flow and fix Share Product accounting
- Add Share Collection screen under Cashier menu for cash pre-collection before share account activation; posts Dr Cash Vault, Cr Share Sundry, Cr Income GL entries with duplicate-collection guard - Restore all 4 GL mappings (Assets, Liabilities, Income, Equity) in Share Product accounting step with pill-style savings-product UI - Remove Multiples section from Share Product currency step; default Decimal Places to 2 - GET /cashier/sharecollection/:id endpoint for collection status check Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
5f7de01ba9
commit
c8e9ab8f84
@@ -0,0 +1,199 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { RouterModule } from '@angular/router';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { of } from 'rxjs';
|
||||
import { CashierService } from '../cashier.service';
|
||||
|
||||
type PageState = 'idle' | 'searching' | 'loaded' | 'submitting' | 'success';
|
||||
|
||||
interface ShareCharge {
|
||||
id: number;
|
||||
name: string;
|
||||
amount: number;
|
||||
currencySymbol: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-share-collection',
|
||||
templateUrl: './share-collection.component.html',
|
||||
styleUrls: [
|
||||
'../cashier-shared.scss',
|
||||
'./share-collection.component.scss'
|
||||
],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
RouterModule,
|
||||
MatIconModule,
|
||||
MatSnackBarModule,
|
||||
MatFormFieldModule,
|
||||
MatSelectModule,
|
||||
MatInputModule
|
||||
]
|
||||
})
|
||||
export class ShareCollectionComponent implements OnInit {
|
||||
private service = inject(CashierService);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
|
||||
pageState: PageState = 'idle';
|
||||
accountInput = '';
|
||||
transactionDate = '';
|
||||
paymentTypeId: number | null = null;
|
||||
paymentTypes: any[] = [];
|
||||
note = '';
|
||||
errorMsg = '';
|
||||
saving = false;
|
||||
|
||||
account: any = null;
|
||||
charges: ShareCharge[] = [];
|
||||
collectionResult: any = null; // filled after successful POST
|
||||
|
||||
// Editable fields — cashier can override if API returns 0
|
||||
requestedSharesInput = 0;
|
||||
pricePerShare = 0;
|
||||
|
||||
get shareAmount(): number {
|
||||
return this.requestedSharesInput * this.pricePerShare;
|
||||
}
|
||||
|
||||
get totalChargeAmount(): number {
|
||||
return this.charges.reduce((s, c) => s + c.amount, 0);
|
||||
}
|
||||
|
||||
get totalAmount(): number {
|
||||
return this.shareAmount + this.totalChargeAmount;
|
||||
}
|
||||
|
||||
get currencySymbol(): string {
|
||||
return this.account?.currency?.displaySymbol ?? '₹';
|
||||
}
|
||||
|
||||
get isValid(): boolean {
|
||||
return !!this.account && this.totalAmount > 0 && !!this.transactionDate;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
const d = new Date();
|
||||
this.transactionDate = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`;
|
||||
this.service
|
||||
.getPaymentTypes()
|
||||
.pipe(catchError(() => of([])))
|
||||
.subscribe((types: any[]) => (this.paymentTypes = types || []));
|
||||
}
|
||||
|
||||
searchAccount(): void {
|
||||
const query = this.accountInput.trim();
|
||||
if (!query) return;
|
||||
this.pageState = 'searching';
|
||||
this.account = null;
|
||||
this.charges = [];
|
||||
this.errorMsg = '';
|
||||
|
||||
this.service
|
||||
.getShareAccountForCollection(query)
|
||||
.pipe(
|
||||
catchError((err) => {
|
||||
this.errorMsg =
|
||||
err?.error?.errors?.[0]?.defaultUserMessage ||
|
||||
err?.error?.developerMessage ||
|
||||
'Account not found. Please check the account ID and try again.';
|
||||
this.pageState = 'idle';
|
||||
return of(null);
|
||||
})
|
||||
)
|
||||
.subscribe((acc: any) => {
|
||||
if (!acc) return;
|
||||
this.account = acc;
|
||||
|
||||
// Auto-detect shares from summary or first pending purchasedShares entry
|
||||
const pendingTx = (acc.purchasedShares || []).find(
|
||||
(t: any) =>
|
||||
t.status?.code === 'purchasedSharesStatusType.applied' && t.type?.code === 'purchasedSharesType.purchased'
|
||||
);
|
||||
this.requestedSharesInput = Number(
|
||||
acc.summary?.totalPendingForApprovalShares || pendingTx?.numberOfShares || acc.requestedShares || 0
|
||||
);
|
||||
this.pricePerShare = Number(pendingTx?.purchasedPrice || acc.currentMarketPrice || acc.unitPrice || 0);
|
||||
|
||||
// Show ALL charges on the account — no type filtering
|
||||
this.charges = (acc.charges || []).map((c: any) => ({
|
||||
id: c.chargeId ?? c.id,
|
||||
name: c.name,
|
||||
amount: Number(c.amount ?? c.amountOutstanding ?? 0),
|
||||
currencySymbol: c.currency?.displaySymbol ?? '₹'
|
||||
}));
|
||||
|
||||
this.pageState = 'loaded';
|
||||
});
|
||||
}
|
||||
|
||||
recalc(): void {
|
||||
// triggered by input changes — nothing extra needed, getters auto-recalculate
|
||||
}
|
||||
|
||||
submit(): void {
|
||||
if (!this.isValid || this.saving) return;
|
||||
this.saving = true;
|
||||
const payload = {
|
||||
shareAccountId: this.account.id,
|
||||
accountNo: this.account.accountNo,
|
||||
clientName: this.account.clientName,
|
||||
transactionDate: this.transactionDate,
|
||||
requestedShares: this.requestedSharesInput,
|
||||
pricePerShare: this.pricePerShare,
|
||||
shareAmount: this.shareAmount,
|
||||
chargeAmount: this.totalChargeAmount,
|
||||
totalAmount: this.totalAmount,
|
||||
paymentTypeId: this.paymentTypeId || null,
|
||||
note: this.note || null
|
||||
};
|
||||
|
||||
this.service.collectSharePayment(payload).subscribe({
|
||||
next: (result: any) => {
|
||||
this.collectionResult = result;
|
||||
this.snackBar.open('Share collection posted successfully.', 'Close', { duration: 3000 });
|
||||
this.pageState = 'success';
|
||||
this.saving = false;
|
||||
},
|
||||
error: (err: any) => {
|
||||
const msg =
|
||||
err?.error?.errors?.[0]?.defaultUserMessage ||
|
||||
err?.error?.developerMessage ||
|
||||
err?.error?.defaultUserMessage ||
|
||||
'Collection failed. Please try again.';
|
||||
this.snackBar.open(msg, 'Close', { duration: 8000 });
|
||||
this.saving = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reset(): void {
|
||||
this.pageState = 'idle';
|
||||
this.accountInput = '';
|
||||
this.account = null;
|
||||
this.charges = [];
|
||||
this.errorMsg = '';
|
||||
this.note = '';
|
||||
this.paymentTypeId = null;
|
||||
this.requestedSharesInput = 0;
|
||||
this.pricePerShare = 0;
|
||||
this.collectionResult = null;
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user