Adds full frontend-only Investment module mirroring the legacy AutoBank CBS Investment menu: Dashboard, FD/Bond/Share entry screens (purchase, interest, sell/redeem, write-off, amortize and more), 10 master screens, an Interest calculation/register screen, and ~21 reports. All figures are derived from a single holdings/transactions dataset so entry, interest, and reports can never drift out of sync. Wires the new "Investment" mega-menu into the shell and routes the previously inert Investment stub buttons.
264 lines
7.4 KiB
TypeScript
264 lines
7.4 KiB
TypeScript
/**
|
|
* 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 } from '@angular/core';
|
|
import { CommonModule } from '@angular/common';
|
|
import { FormsModule } from '@angular/forms';
|
|
import { SHARE_HOLDINGS, ALL_TXNS, InvestmentHolding, InvestmentTxn, todayDDMMYYYY } from '../investment-data';
|
|
|
|
type ShareTab = 'holdings' | 'purchase' | 'dividend' | 'sell' | 'writeoff';
|
|
|
|
interface SharePurchaseForm {
|
|
certNo: string;
|
|
scriptCode: string;
|
|
scriptName: string;
|
|
party: string;
|
|
subType: string;
|
|
units: number | null;
|
|
purchaseRate: number | null;
|
|
purchaseDate: string;
|
|
brokerCode: string;
|
|
crToAc: string;
|
|
}
|
|
|
|
/**
|
|
* Share Entry treasury screen — Holdings register + Purchase / Dividend / Sell / Write-off
|
|
* entry tabs (legacy "02 Share Entry" menu). Dummy in-memory data; Save appends locally.
|
|
*/
|
|
@Component({
|
|
selector: 'mifosx-share-investment',
|
|
templateUrl: './share-investment.component.html',
|
|
styleUrls: [
|
|
'../../cashier/cashier-shared.scss',
|
|
'../investment-shared.scss'
|
|
],
|
|
standalone: true,
|
|
imports: [
|
|
CommonModule,
|
|
FormsModule
|
|
]
|
|
})
|
|
export class ShareInvestmentComponent {
|
|
activeTab: ShareTab = 'holdings';
|
|
asOnDate = todayDDMMYYYY();
|
|
|
|
holdings: InvestmentHolding[] = [...SHARE_HOLDINGS];
|
|
txns: InvestmentTxn[] = ALL_TXNS.filter((t) => t.instrumentType === 'SHARE');
|
|
|
|
toastMsg = '';
|
|
toastVisible = false;
|
|
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
|
|
|
purchaseForm: SharePurchaseForm = this.blankPurchaseForm();
|
|
dividendCertNo = '';
|
|
dividendPerShare: number | null = null;
|
|
sellCertNo = '';
|
|
sellUnits: number | null = null;
|
|
sellRate: number | null = null;
|
|
writeoffCertNo = '';
|
|
writeoffReason = '';
|
|
|
|
setTab(tab: ShareTab): void {
|
|
this.activeTab = tab;
|
|
}
|
|
|
|
get activeHoldings(): InvestmentHolding[] {
|
|
return this.holdings.filter((h) => h.status === 'ACTIVE');
|
|
}
|
|
|
|
private blankPurchaseForm(): SharePurchaseForm {
|
|
return {
|
|
certNo: `SH-${6000 + this.holdings.length + 1}`,
|
|
scriptCode: '',
|
|
scriptName: '',
|
|
party: 'NSE/BSE',
|
|
subType: 'EQUITY',
|
|
units: null,
|
|
purchaseRate: null,
|
|
purchaseDate: this.asOnDate,
|
|
brokerCode: '',
|
|
crToAc: '310070'
|
|
};
|
|
}
|
|
|
|
savePurchase(): void {
|
|
const f = this.purchaseForm;
|
|
if (!f.scriptName || !f.units || !f.purchaseRate) {
|
|
this.toast('Please fill Script Name, No. of Units and Purchase Rate.');
|
|
return;
|
|
}
|
|
const faceValue = f.units * f.purchaseRate;
|
|
const holding: InvestmentHolding = {
|
|
certNo: f.certNo,
|
|
instrumentType: 'SHARE',
|
|
scriptCode: f.scriptCode || f.scriptName,
|
|
scriptName: f.scriptName,
|
|
party: f.party,
|
|
subType: f.subType,
|
|
faceValue,
|
|
purchaseRate: f.purchaseRate,
|
|
units: f.units,
|
|
purchaseDate: f.purchaseDate,
|
|
maturityDate: f.purchaseDate,
|
|
interestRate: 0,
|
|
interestFreq: 'YEARLY',
|
|
glCode: '310060',
|
|
glName: 'INVESTMENT IN SHARES',
|
|
crToAc: f.crToAc,
|
|
brokerCode: f.brokerCode,
|
|
status: 'ACTIVE'
|
|
};
|
|
this.holdings = [
|
|
holding,
|
|
...this.holdings
|
|
];
|
|
this.txns = [
|
|
{
|
|
certNo: holding.certNo,
|
|
instrumentType: 'SHARE',
|
|
txnType: 'PURCHASE',
|
|
voucherNo: `INV/PUR/${holding.certNo}`,
|
|
txnDate: f.purchaseDate,
|
|
amount: faceValue,
|
|
particulars: `Purchase of ${f.units} shares - ${holding.scriptName}`
|
|
},
|
|
...this.txns
|
|
];
|
|
this.toast(`Share holding ${holding.certNo} saved successfully.`);
|
|
this.purchaseForm = this.blankPurchaseForm();
|
|
this.activeTab = 'holdings';
|
|
}
|
|
|
|
cancelPurchase(): void {
|
|
this.purchaseForm = this.blankPurchaseForm();
|
|
}
|
|
|
|
// -------------------- Dividend --------------------
|
|
get dividendHolding(): InvestmentHolding | undefined {
|
|
return this.holdings.find((h) => h.certNo === this.dividendCertNo);
|
|
}
|
|
|
|
get dividendAmount(): number {
|
|
const h = this.dividendHolding;
|
|
if (!h || !this.dividendPerShare) {
|
|
return 0;
|
|
}
|
|
return Math.round(h.units * this.dividendPerShare);
|
|
}
|
|
|
|
saveDividend(): void {
|
|
const h = this.dividendHolding;
|
|
if (!h || !this.dividendPerShare) {
|
|
this.toast('Select a holding and enter dividend per share.');
|
|
return;
|
|
}
|
|
const amt = this.dividendAmount;
|
|
this.txns = [
|
|
{
|
|
certNo: h.certNo,
|
|
instrumentType: 'SHARE',
|
|
txnType: 'DIVIDEND',
|
|
voucherNo: `INV/DIV/${h.certNo}-${this.txns.length + 1}`,
|
|
txnDate: this.asOnDate,
|
|
amount: amt,
|
|
particulars: `Dividend received - ${h.scriptName} (₹${this.dividendPerShare}/share)`
|
|
},
|
|
...this.txns
|
|
];
|
|
this.toast(`Dividend of ₹${amt.toLocaleString('en-IN')} recorded for ${h.certNo}.`);
|
|
this.dividendCertNo = '';
|
|
this.dividendPerShare = null;
|
|
}
|
|
|
|
// -------------------- Sell --------------------
|
|
get sellHolding(): InvestmentHolding | undefined {
|
|
return this.holdings.find((h) => h.certNo === this.sellCertNo);
|
|
}
|
|
|
|
get sellConsideration(): number {
|
|
if (!this.sellUnits || !this.sellRate) {
|
|
return 0;
|
|
}
|
|
return Math.round(this.sellUnits * this.sellRate);
|
|
}
|
|
|
|
saveSell(): void {
|
|
const h = this.sellHolding;
|
|
if (!h || !this.sellUnits || !this.sellRate || this.sellUnits > h.units) {
|
|
this.toast('Select a holding and enter valid units (≤ held) and sell rate.');
|
|
return;
|
|
}
|
|
const consideration = this.sellConsideration;
|
|
h.units -= this.sellUnits;
|
|
h.faceValue = Math.round(h.units * h.purchaseRate);
|
|
if (h.units === 0) {
|
|
h.status = 'SOLD';
|
|
}
|
|
this.txns = [
|
|
{
|
|
certNo: h.certNo,
|
|
instrumentType: 'SHARE',
|
|
txnType: 'SELL',
|
|
voucherNo: `INV/SEL/${h.certNo}-${this.txns.length + 1}`,
|
|
txnDate: this.asOnDate,
|
|
amount: consideration,
|
|
particulars: `Sold ${this.sellUnits} shares of ${h.scriptName} @ ${this.sellRate}`
|
|
},
|
|
...this.txns
|
|
];
|
|
this.toast(`Sold ${this.sellUnits} shares. Consideration ₹${consideration.toLocaleString('en-IN')}.`);
|
|
this.sellCertNo = '';
|
|
this.sellUnits = null;
|
|
this.sellRate = null;
|
|
this.activeTab = 'holdings';
|
|
}
|
|
|
|
// -------------------- Write-off --------------------
|
|
get writeoffHolding(): InvestmentHolding | undefined {
|
|
return this.holdings.find((h) => h.certNo === this.writeoffCertNo);
|
|
}
|
|
|
|
saveWriteoff(): void {
|
|
const h = this.writeoffHolding;
|
|
if (!h || !this.writeoffReason) {
|
|
this.toast('Select a holding and enter a write-off reason.');
|
|
return;
|
|
}
|
|
h.status = 'WRITTEN_OFF';
|
|
this.txns = [
|
|
{
|
|
certNo: h.certNo,
|
|
instrumentType: 'SHARE',
|
|
txnType: 'WRITE_OFF',
|
|
voucherNo: `INV/WOF/${h.certNo}`,
|
|
txnDate: this.asOnDate,
|
|
amount: h.faceValue,
|
|
particulars: `Written off - ${this.writeoffReason}`
|
|
},
|
|
...this.txns
|
|
];
|
|
this.toast(`${h.certNo} written off.`);
|
|
this.writeoffCertNo = '';
|
|
this.writeoffReason = '';
|
|
this.activeTab = 'holdings';
|
|
}
|
|
|
|
statusClass(h: InvestmentHolding): string {
|
|
return `inv-badge inv-badge--${h.status.toLowerCase()}`;
|
|
}
|
|
|
|
private toast(msg: string): void {
|
|
this.toastMsg = msg;
|
|
this.toastVisible = true;
|
|
if (this.toastTimer) {
|
|
clearTimeout(this.toastTimer);
|
|
}
|
|
this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200);
|
|
}
|
|
}
|