feat(investment): add Treasury Investment module (FD, Govt Sec/Non-SLR/Bond/MF, Share)
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.
This commit is contained in:
@@ -0,0 +1,276 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
|
||||
<div class="pg">
|
||||
<header class="pg-head">
|
||||
<div>
|
||||
<div class="pg-eyebrow">TREASURY · INVESTMENT · SHARE</div>
|
||||
<h1 class="pg-title">Share Entry</h1>
|
||||
<p class="pg-sub">
|
||||
Equity and cooperative-society share holdings — Purchase, Dividend, Sell and Write-off entries.
|
||||
</p>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="cctr-tabs">
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'holdings'"
|
||||
(click)="setTab('holdings')"
|
||||
>
|
||||
Holdings
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'purchase'"
|
||||
(click)="setTab('purchase')"
|
||||
>
|
||||
01 Share Purchase Entry
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'dividend'"
|
||||
(click)="setTab('dividend')"
|
||||
>
|
||||
02 Share Interest / Dividend Entry
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'sell'"
|
||||
(click)="setTab('sell')"
|
||||
>
|
||||
03 Share Sell Entry
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="pg-btn pg-btn--ghost"
|
||||
[class.pg-btn--active]="activeTab === 'writeoff'"
|
||||
(click)="setTab('writeoff')"
|
||||
>
|
||||
04 Share Write Off Entry
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- HOLDINGS -->
|
||||
@if (activeTab === 'holdings') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Share Holdings Register</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Cert No</th>
|
||||
<th>Script</th>
|
||||
<th>Party</th>
|
||||
<th class="num">Units</th>
|
||||
<th class="num">Rate</th>
|
||||
<th class="num">Face Value</th>
|
||||
<th>Purchase Date</th>
|
||||
<th>Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (h of holdings; track h.certNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<span class="pg-chip">{{ h.certNo }}</span>
|
||||
</td>
|
||||
<td>{{ h.scriptName }}</td>
|
||||
<td>{{ h.party }}</td>
|
||||
<td class="num">{{ h.units | number }}</td>
|
||||
<td class="num">₹{{ h.purchaseRate | number: '1.2-2' }}</td>
|
||||
<td class="num">₹{{ h.faceValue | number: '1.2-2' }}</td>
|
||||
<td>{{ h.purchaseDate }}</td>
|
||||
<td>
|
||||
<span [class]="statusClass(h)">{{ h.status }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- PURCHASE ENTRY -->
|
||||
@if (activeTab === 'purchase') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Investment - Share Purchase Entry</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field"
|
||||
><span>Cert No</span><input type="text" [(ngModel)]="purchaseForm.certNo" readonly
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Script Code</span><input type="text" [(ngModel)]="purchaseForm.scriptCode"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Script/Company Name *</span><input type="text" [(ngModel)]="purchaseForm.scriptName"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Party / Exchange</span><input type="text" [(ngModel)]="purchaseForm.party"
|
||||
/></label>
|
||||
<label class="inv-form-field">
|
||||
<span>Sub Type</span>
|
||||
<select [(ngModel)]="purchaseForm.subType">
|
||||
<option value="EQUITY">EQUITY</option>
|
||||
<option value="CO-OP SHARE">CO-OP SHARE</option>
|
||||
</select>
|
||||
</label>
|
||||
<label class="inv-form-field"
|
||||
><span>No. of Units *</span><input type="number" [(ngModel)]="purchaseForm.units" min="0"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Purchase Rate *</span><input type="number" [(ngModel)]="purchaseForm.purchaseRate" min="0" step="0.01"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Purchase Date *</span
|
||||
><input type="text" [(ngModel)]="purchaseForm.purchaseDate" placeholder="DD-MM-YYYY"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Broker Code</span><input type="text" [(ngModel)]="purchaseForm.brokerCode"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>CR To A/c</span><input type="text" [(ngModel)]="purchaseForm.crToAc"
|
||||
/></label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--ghost" (click)="cancelPurchase()">Cancel</button>
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="savePurchase()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- DIVIDEND ENTRY -->
|
||||
@if (activeTab === 'dividend') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Investment - Share Interest / Dividend Entry</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field">
|
||||
<span>Cert No *</span>
|
||||
<select [(ngModel)]="dividendCertNo">
|
||||
<option value="">-- Select --</option>
|
||||
@for (h of activeHoldings; track h.certNo) {
|
||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.scriptName }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
@if (dividendHolding; as h) {
|
||||
<label class="inv-form-field"><span>Units Held</span><input type="text" [value]="h.units" readonly /></label>
|
||||
}
|
||||
<label class="inv-form-field"
|
||||
><span>Dividend per Share *</span><input type="number" [(ngModel)]="dividendPerShare" min="0" step="0.01"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Total Dividend (computed)</span
|
||||
><input type="text" [value]="'₹' + (dividendAmount | number: '1.2-2')" readonly
|
||||
/></label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="saveDividend()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- SELL ENTRY -->
|
||||
@if (activeTab === 'sell') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Investment - Share Sell Entry</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field">
|
||||
<span>Cert No *</span>
|
||||
<select [(ngModel)]="sellCertNo">
|
||||
<option value="">-- Select --</option>
|
||||
@for (h of activeHoldings; track h.certNo) {
|
||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.scriptName }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
@if (sellHolding; as h) {
|
||||
<label class="inv-form-field"><span>Units Held</span><input type="text" [value]="h.units" readonly /></label>
|
||||
}
|
||||
<label class="inv-form-field"
|
||||
><span>Units to Sell *</span><input type="number" [(ngModel)]="sellUnits" min="0"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Sell Rate *</span><input type="number" [(ngModel)]="sellRate" min="0" step="0.01"
|
||||
/></label>
|
||||
<label class="inv-form-field"
|
||||
><span>Consideration (computed)</span
|
||||
><input type="text" [value]="'₹' + (sellConsideration | number: '1.2-2')" readonly
|
||||
/></label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="saveSell()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- WRITE OFF ENTRY -->
|
||||
@if (activeTab === 'writeoff') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Investment - Share Write Off Entry</div>
|
||||
<div class="inv-form-grid">
|
||||
<label class="inv-form-field">
|
||||
<span>Cert No *</span>
|
||||
<select [(ngModel)]="writeoffCertNo">
|
||||
<option value="">-- Select --</option>
|
||||
@for (h of activeHoldings; track h.certNo) {
|
||||
<option [value]="h.certNo">{{ h.certNo }} — {{ h.scriptName }}</option>
|
||||
}
|
||||
</select>
|
||||
</label>
|
||||
@if (writeoffHolding; as h) {
|
||||
<label class="inv-form-field"
|
||||
><span>Face Value to be Written Off</span
|
||||
><input type="text" [value]="'₹' + (h.faceValue | number: '1.2-2')" readonly
|
||||
/></label>
|
||||
}
|
||||
<label class="inv-form-field"><span>Reason *</span><input type="text" [(ngModel)]="writeoffReason" /></label>
|
||||
</div>
|
||||
<div class="inv-form-actions">
|
||||
<button type="button" class="pg-btn pg-btn--primary" (click)="saveWriteoff()">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (activeTab !== 'holdings') {
|
||||
<div class="pg-card">
|
||||
<div class="pos-section-label">Share Transaction Log</div>
|
||||
<table class="pg-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Date</th>
|
||||
<th>Voucher No</th>
|
||||
<th>Cert No</th>
|
||||
<th>Type</th>
|
||||
<th>Particulars</th>
|
||||
<th class="num">Amount</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (t of txns; track t.voucherNo) {
|
||||
<tr>
|
||||
<td>{{ t.txnDate }}</td>
|
||||
<td>
|
||||
<span class="pg-chip">{{ t.voucherNo }}</span>
|
||||
</td>
|
||||
<td>{{ t.certNo }}</td>
|
||||
<td>{{ t.txnType }}</td>
|
||||
<td>{{ t.particulars }}</td>
|
||||
<td class="num">₹{{ t.amount | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="inv-toast" [class.show]="toastVisible">{{ toastMsg }}</div>
|
||||
@@ -0,0 +1,263 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user