/** * 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 Denomination { id: number; value: number; kind: 'note' | 'coin'; label: string; } 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; // ── Denomination ──────────────────────────────────────────────────────────── notes: Denomination[] = []; coins: Denomination[] = []; denominationCounts: Record = {}; 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 isCashPayment(): boolean { if (this.paymentTypeId === null) return true; const pt = this.paymentTypes.find((p: any) => p.id === this.paymentTypeId); return !!pt?.isCashPayment; } get allDenominations(): Denomination[] { const desc = (a: Denomination, b: Denomination) => b.value - a.value; return [ ...[...this.notes].sort(desc), ...[...this.coins].sort(desc) ]; } get denominationTotal(): number { return this.allDenominations.reduce((s, d) => s + this.subtotalFor(d), 0); } get denomMatchesTotal(): boolean { return this.denominationTotal === this.totalAmount; } get isValid(): boolean { if (!this.account || this.totalAmount <= 0 || !this.transactionDate) return false; if (this.isCashPayment && this.allDenominations.length > 0) { return this.denomMatchesTotal; } return true; } 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 || [])); this.loadDenominations(); } private loadDenominations(): void { this.service .getActiveDenominations() .pipe(catchError(() => of([]))) .subscribe((list: any[]) => { const rows: Denomination[] = (list || []).map((d: any) => ({ id: d.id, value: d.denomination ?? d.value, kind: d.type === 'coin' ? 'coin' : 'note', label: '₹' + (d.denomination ?? d.value) })); const order = (a: Denomination, b: Denomination) => b.value - a.value; this.notes = rows.filter((r) => r.kind === 'note').sort(order); this.coins = rows.filter((r) => r.kind === 'coin').sort(order); }); } // ── Denomination helpers ────────────────────────────────────────────────── denomKey(d: Denomination): string { return `${d.kind}:${d.value}`; } countFor(d: Denomination): number { return this.denominationCounts[this.denomKey(d)] || 0; } subtotalFor(d: Denomination): number { return this.countFor(d) * d.value; } onDenomChange(d: Denomination, raw: any): void { this.denominationCounts[this.denomKey(d)] = Math.max(0, Math.floor(Number(raw) || 0)); } bumpDenom(d: Denomination, delta: number): void { this.denominationCounts[this.denomKey(d)] = Math.max(0, this.countFor(d) + delta); } clearDenominations(): void { this.denominationCounts = {}; } 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; this.clearDenominations(); } }