feat(investment): due date diary, script status, deal no by deal date, matured-only redemption, keep typed rate and business-rule error title
This commit is contained in:
@@ -109,7 +109,15 @@ export class ErrorHandlerInterceptor implements HttpInterceptor {
|
|||||||
} else if (status === 400) {
|
} else if (status === 400) {
|
||||||
alert = { type: 'Bad Request', message: errorMessage || 'Invalid parameters were passed in the request!' };
|
alert = { type: 'Bad Request', message: errorMessage || 'Invalid parameters were passed in the request!' };
|
||||||
} else if (status === 403) {
|
} else if (status === 403) {
|
||||||
alert = { type: 'Unauthorized Request', message: errorMessage || 'You are not authorized for this request!' };
|
// A 403 is either a missing permission or a business rule the record does not meet yet ("post the coupon
|
||||||
|
// first", "matures on …"). Only the first is an authorisation problem; the rule is told by its code.
|
||||||
|
const errorCode: string =
|
||||||
|
errorBody?.errors?.[0]?.userMessageGlobalisationCode || errorBody?.userMessageGlobalisationCode || '';
|
||||||
|
const isPermission = !errorCode || /not\.authori[sz]ed|unauthori[sz]ed|access\.denied|forbidden/i.test(errorCode);
|
||||||
|
alert = {
|
||||||
|
type: isPermission ? 'Unauthorized Request' : 'Action Not Allowed',
|
||||||
|
message: errorMessage || 'You are not authorized for this request!'
|
||||||
|
};
|
||||||
} else if (status === 404) {
|
} else if (status === 404) {
|
||||||
alert = {
|
alert = {
|
||||||
type: this.translate.instant('error.resource.not.found'),
|
type: this.translate.instant('error.resource.not.found'),
|
||||||
|
|||||||
@@ -1499,7 +1499,7 @@
|
|||||||
<!-- Investment -->
|
<!-- Investment -->
|
||||||
<div class="deposit-menu-group">
|
<div class="deposit-menu-group">
|
||||||
<div class="deposit-group-title">Investment</div>
|
<div class="deposit-group-title">Investment</div>
|
||||||
<button mat-menu-item routerLink="/investment/dashboard" tabindex="0">
|
<button mat-menu-item routerLink="/investment/diary" tabindex="0">
|
||||||
<fa-icon icon="calendar-alt" class="menu-item-icon"></fa-icon> Due Date Diary for All Investment
|
<fa-icon icon="calendar-alt" class="menu-item-icon"></fa-icon> Due Date Diary for All Investment
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item routerLink="/investment/reports" tabindex="0">
|
<button mat-menu-item routerLink="/investment/reports" tabindex="0">
|
||||||
@@ -2369,7 +2369,7 @@
|
|||||||
<a
|
<a
|
||||||
class="gl-link"
|
class="gl-link"
|
||||||
[class.hidden]="!matchesInvestmentSearch('Due Date Diary for All Investment')"
|
[class.hidden]="!matchesInvestmentSearch('Due Date Diary for All Investment')"
|
||||||
routerLink="/investment/dashboard"
|
routerLink="/investment/diary"
|
||||||
(click)="investmentMenuOpen = false"
|
(click)="investmentMenuOpen = false"
|
||||||
>
|
>
|
||||||
<fa-icon icon="calendar-check" class="gl-icon"></fa-icon> Due Date Diary for All Investment
|
<fa-icon icon="calendar-check" class="gl-icon"></fa-icon> Due Date Diary for All Investment
|
||||||
|
|||||||
@@ -664,10 +664,13 @@
|
|||||||
<span>* Deal No</span>
|
<span>* Deal No</span>
|
||||||
<select [(ngModel)]="redeemPurchaseId" (ngModelChange)="onRedeemDealChange()">
|
<select [(ngModel)]="redeemPurchaseId" (ngModelChange)="onRedeemDealChange()">
|
||||||
<option [ngValue]="null">-- Select --</option>
|
<option [ngValue]="null">-- Select --</option>
|
||||||
@for (h of saleableHoldings; track h.certNo) {
|
@for (h of redeemableHoldings; track h.certNo) {
|
||||||
<option [ngValue]="h.id">{{ h.dealNo }} — {{ h.scriptName }} (matures {{ h.maturityDate }})</option>
|
<option [ngValue]="h.id">{{ h.dealNo }} — {{ h.scriptName }} (matures {{ h.maturityDate }})</option>
|
||||||
}
|
}
|
||||||
</select>
|
</select>
|
||||||
|
@if (!redeemableHoldings.length) {
|
||||||
|
<small>No holding has reached its maturity date yet.</small>
|
||||||
|
}
|
||||||
</label>
|
</label>
|
||||||
@if (redeemQuote; as q) {
|
@if (redeemQuote; as q) {
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
|
|||||||
@@ -190,9 +190,10 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
|
|
||||||
/** Script and party dropdowns offer the masters saved in the backend. */
|
/** Script and party dropdowns offer the masters saved in the backend. */
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
// The home-screen interest alert links here with ?tab=interest.
|
// The home-screen alert and the Due Date Diary link here with ?tab=interest / redemption / amortize.
|
||||||
if (this.route.snapshot.queryParamMap.get('tab') === 'interest') {
|
const tab = this.route.snapshot.queryParamMap.get('tab');
|
||||||
this.activeTab = 'interest';
|
if (tab === 'interest' || tab === 'redemption' || tab === 'amortize') {
|
||||||
|
this.activeTab = tab;
|
||||||
}
|
}
|
||||||
this.investmentService.syncSharedMasters().subscribe({
|
this.investmentService.syncSharedMasters().subscribe({
|
||||||
error: (err) => this.toast(`Could not load Script / Party masters: ${apiErrorMessage(err)}`)
|
error: (err) => this.toast(`Could not load Script / Party masters: ${apiErrorMessage(err)}`)
|
||||||
@@ -470,9 +471,9 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
return INSTRUMENT_LABELS[type];
|
return INSTRUMENT_LABELS[type];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Preview of the deal number the backend will assign: FY prefix (e.g. 202627) + running number. */
|
/** Preview of the deal number the backend will assign: FY of the deal date (e.g. 202627) + running number. */
|
||||||
private nextDealNo(): string {
|
private nextDealNo(dealDate?: string): string {
|
||||||
const d = parseDDMMYYYY(this.asOnDate);
|
const d = parseDDMMYYYY(dealDate || this.asOnDate);
|
||||||
const fyStart = d.getMonth() >= 3 ? d.getFullYear() : d.getFullYear() - 1;
|
const fyStart = d.getMonth() >= 3 ? d.getFullYear() : d.getFullYear() - 1;
|
||||||
const prefix = `${fyStart}${String((fyStart + 1) % 100).padStart(2, '0')}`;
|
const prefix = `${fyStart}${String((fyStart + 1) % 100).padStart(2, '0')}`;
|
||||||
const nums = (this.holdings ?? [])
|
const nums = (this.holdings ?? [])
|
||||||
@@ -668,6 +669,11 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
|
|
||||||
/** Re-suggests the last coupon date when the deal date changes. */
|
/** Re-suggests the last coupon date when the deal date changes. */
|
||||||
onDealDateChange(): void {
|
onDealDateChange(): void {
|
||||||
|
const f = this.purchaseForm;
|
||||||
|
// A new deal takes its number from the deal date's financial year; a saved deal keeps its own.
|
||||||
|
if (f.dealDate && !this.holdings.some((h) => h.dealNo === f.dealNo)) {
|
||||||
|
f.dealNo = this.nextDealNo(f.dealDate);
|
||||||
|
}
|
||||||
this.suggestLastIntDate();
|
this.suggestLastIntDate();
|
||||||
this.loadMarketRate();
|
this.loadMarketRate();
|
||||||
}
|
}
|
||||||
@@ -754,6 +760,9 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
purchaseMarketRate: InvestmentDailyRateDto | null | undefined;
|
purchaseMarketRate: InvestmentDailyRateDto | null | undefined;
|
||||||
|
|
||||||
|
/** The rate this screen last filled in from the Daily Script Master, to tell it apart from one the user typed. */
|
||||||
|
private autoFilledRate: number | null = null;
|
||||||
|
|
||||||
private loadMarketRate(): void {
|
private loadMarketRate(): void {
|
||||||
const s = this.purchaseScript;
|
const s = this.purchaseScript;
|
||||||
const f = this.purchaseForm;
|
const f = this.purchaseForm;
|
||||||
@@ -764,12 +773,16 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
this.investmentService.getLatestDailyRate(s.id, f.dealDate).subscribe({
|
this.investmentService.getLatestDailyRate(s.id, f.dealDate).subscribe({
|
||||||
next: (rate) => {
|
next: (rate) => {
|
||||||
this.purchaseMarketRate = rate;
|
this.purchaseMarketRate = rate;
|
||||||
// No market rate: clear the field rather than leave another script's rate in it.
|
const current = this.isMutualFund ? f.nav : f.purchaseRate;
|
||||||
|
// A market rate fills the field. Without one, only a rate this screen filled in earlier (another script's or
|
||||||
|
// another day's) is cleared; a rate the user typed stays.
|
||||||
|
const next = rate ? rate.closeRate : current === this.autoFilledRate ? null : current;
|
||||||
if (this.isMutualFund) {
|
if (this.isMutualFund) {
|
||||||
f.nav = rate ? rate.closeRate : null;
|
f.nav = next;
|
||||||
} else {
|
} else {
|
||||||
f.purchaseRate = rate ? rate.closeRate : null;
|
f.purchaseRate = next;
|
||||||
}
|
}
|
||||||
|
this.autoFilledRate = rate ? rate.closeRate : null;
|
||||||
},
|
},
|
||||||
error: (err) => this.toast(`Could not read the market rate: ${apiErrorMessage(err)}`)
|
error: (err) => this.toast(`Could not read the market rate: ${apiErrorMessage(err)}`)
|
||||||
});
|
});
|
||||||
@@ -1080,6 +1093,12 @@ export class BondInvestmentComponent implements OnInit {
|
|||||||
return this.activeHoldingsForType.filter((h) => !!h.id);
|
return this.activeHoldingsForType.filter((h) => !!h.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Redemption is at maturity, so only holdings that have reached it are offered. */
|
||||||
|
get redeemableHoldings(): InvestmentHolding[] {
|
||||||
|
const today = parseDDMMYYYY(this.asOnDate);
|
||||||
|
return this.saleableHoldings.filter((h) => !!h.maturityDate && parseDDMMYYYY(h.maturityDate) <= today);
|
||||||
|
}
|
||||||
|
|
||||||
/** Shows what the redemption will post, or why it cannot be done yet (before maturity, coupon not posted). */
|
/** Shows what the redemption will post, or why it cannot be done yet (before maturity, coupon not posted). */
|
||||||
onRedeemDealChange(): void {
|
onRedeemDealChange(): void {
|
||||||
this.redeemQuote = null;
|
this.redeemQuote = null;
|
||||||
|
|||||||
@@ -0,0 +1,126 @@
|
|||||||
|
<!--
|
||||||
|
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 · DUE DATE DIARY</div>
|
||||||
|
<h1 class="pg-title">Due Date Diary</h1>
|
||||||
|
<p class="pg-sub">
|
||||||
|
Coupons, 31 Mar / 30 Sep provisions, premium amortisation, maturities and FD interest due in a period, with what
|
||||||
|
is posted and what is still pending.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div class="pg-card">
|
||||||
|
<div class="inv-form-grid">
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>From</span><mifosx-date-ddmmyyyy [(ngModel)]="fromDate"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
|
<label class="inv-form-field"
|
||||||
|
><span>To</span><mifosx-date-ddmmyyyy [(ngModel)]="toDate"></mifosx-date-ddmmyyyy
|
||||||
|
></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Investment</span>
|
||||||
|
<select [(ngModel)]="instrument">
|
||||||
|
<option value="">All</option>
|
||||||
|
<option value="SECURITY">Govt Security / Bond</option>
|
||||||
|
<option value="FD">FD with banks</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Status</span>
|
||||||
|
<select [(ngModel)]="status">
|
||||||
|
<option value="">All</option>
|
||||||
|
<option value="PENDING">Pending</option>
|
||||||
|
<option value="POSTED">Posted</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="diary-actions">
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="setRange(7)">Next 7 days</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="setRange(30)">Next 30 days</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="setRange(90)">Next 90 days</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--ghost" (click)="showOverdue()">Overdue</button>
|
||||||
|
<button type="button" class="pg-btn pg-btn--primary" [disabled]="loading" (click)="load()">
|
||||||
|
{{ loading ? 'Loading…' : 'Show' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
@if (error) {
|
||||||
|
<p class="diary-error">{{ error }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (diary; as d) {
|
||||||
|
<div class="diary-summary">
|
||||||
|
<div class="diary-tile">
|
||||||
|
<span class="diary-tile-l">Entries</span><span class="diary-tile-v">{{ rows.length }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="diary-tile">
|
||||||
|
<span class="diary-tile-l">Pending</span><span class="diary-tile-v">{{ d.pendingCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="diary-tile" [class.diary-tile--bad]="d.overdueCount > 0">
|
||||||
|
<span class="diary-tile-l">Overdue</span><span class="diary-tile-v">{{ d.overdueCount }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="diary-tile">
|
||||||
|
<span class="diary-tile-l">Money expected (not yet received)</span
|
||||||
|
><span class="diary-tile-v">₹{{ d.expectedInflow | number: '1.2-2' }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="pg-card">
|
||||||
|
<div class="pos-section-label">{{ d.fromDate }} to {{ d.toDate }} · as on {{ d.asOnDate }}</div>
|
||||||
|
<table class="pg-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Date</th>
|
||||||
|
<th>Due</th>
|
||||||
|
<th>Investment</th>
|
||||||
|
<th>Deal No</th>
|
||||||
|
<th>Particulars</th>
|
||||||
|
<th class="num">Amount</th>
|
||||||
|
<th>Status</th>
|
||||||
|
<th></th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@for (r of rows; track r.instrument + r.refId + r.kind + r.date) {
|
||||||
|
<tr>
|
||||||
|
<td>{{ r.date }}</td>
|
||||||
|
<td>{{ kindLabels[r.kind] }}</td>
|
||||||
|
<td>{{ r.instrument === 'FD' ? 'FD' : 'Govt Security / Bond' }}</td>
|
||||||
|
<td>
|
||||||
|
<span class="pg-chip">{{ r.dealNo }}</span>
|
||||||
|
</td>
|
||||||
|
<td>{{ r.particulars }}</td>
|
||||||
|
<td class="num">
|
||||||
|
{{ r.amount !== null && r.amount !== undefined ? '₹' + (r.amount | number: '1.2-2') : '—' }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="inv-badge" [ngClass]="'diary-badge--' + r.status.toLowerCase()">{{ statusLabel(r) }}</span>
|
||||||
|
@if (r.voucherNo) {
|
||||||
|
<br /><small>{{ r.voucherNo }}</small>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@if (r.status !== 'POSTED') {
|
||||||
|
<a class="diary-open" [routerLink]="link(r).path" [queryParams]="link(r).query">Open →</a>
|
||||||
|
}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
} @empty {
|
||||||
|
<tr>
|
||||||
|
<td colspan="8" class="pg-empty">Nothing falls due in this period.</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,84 @@
|
|||||||
|
/**
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
.diary-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diary-error {
|
||||||
|
margin: 8px 0 0;
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diary-summary {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diary-tile {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 14px 16px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 10px;
|
||||||
|
background: #fff;
|
||||||
|
|
||||||
|
&--bad {
|
||||||
|
border-color: #fecaca;
|
||||||
|
background: #fef2f2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.diary-tile-l {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #64748b;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diary-tile-v {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diary-badge--posted {
|
||||||
|
background: #f0fdf4;
|
||||||
|
color: #15803d;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diary-badge--due {
|
||||||
|
background: #fff7ed;
|
||||||
|
color: #c2410c;
|
||||||
|
border: 1px solid #fed7aa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diary-badge--overdue {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #b91c1c;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diary-badge--upcoming {
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #1d4ed8;
|
||||||
|
border: 1px solid #bfdbfe;
|
||||||
|
}
|
||||||
|
|
||||||
|
.diary-open {
|
||||||
|
font-weight: 600;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
/**
|
||||||
|
* 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 { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
|
||||||
|
import { addPeriod, todayDDMMYYYY } from '../investment-data';
|
||||||
|
import { InvestmentDiaryDto, InvestmentDiaryRowDto, InvestmentService, apiErrorMessage } from '../investment.service';
|
||||||
|
|
||||||
|
type InstrumentFilter = '' | 'SECURITY' | 'FD';
|
||||||
|
type StatusFilter = '' | 'PENDING' | 'POSTED';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Due Date Diary for All Investment: coupons, 31 Mar / 30 Sep provisions, premium amortisation, maturities and FD
|
||||||
|
* payouts in a period, with what is posted, due or overdue, and a link to the screen that posts each one.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-investment-diary',
|
||||||
|
templateUrl: './investment-diary.component.html',
|
||||||
|
styleUrls: [
|
||||||
|
'../../cashier/cashier-shared.scss',
|
||||||
|
'../investment-shared.scss',
|
||||||
|
'./investment-diary.component.scss'
|
||||||
|
],
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
RouterModule,
|
||||||
|
DateDdmmyyyyPickerComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class InvestmentDiaryComponent implements OnInit {
|
||||||
|
private readonly investmentService = inject(InvestmentService);
|
||||||
|
|
||||||
|
// Opens on everything still to be done: what is pending from the past year and what falls due in the next 30 days.
|
||||||
|
fromDate = addPeriod(todayDDMMYYYY(), -1, 0, 0);
|
||||||
|
toDate = addPeriod(todayDDMMYYYY(), 0, 0, 30);
|
||||||
|
instrument: InstrumentFilter = '';
|
||||||
|
status: StatusFilter = 'PENDING';
|
||||||
|
diary: InvestmentDiaryDto | null = null;
|
||||||
|
loading = false;
|
||||||
|
error = '';
|
||||||
|
|
||||||
|
readonly kindLabels: Record<InvestmentDiaryRowDto['kind'], string> = {
|
||||||
|
COUPON: 'Coupon',
|
||||||
|
PROVISION: 'Interest provision',
|
||||||
|
MATURITY: 'Maturity / redemption',
|
||||||
|
AMORTISATION: 'Premium amortisation',
|
||||||
|
FD_PROVISION: 'FD interest provision',
|
||||||
|
FD_PAYOUT: 'FD interest payout',
|
||||||
|
FD_MATURITY: 'FD maturity'
|
||||||
|
};
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Quick ranges from today. */
|
||||||
|
setRange(days: number): void {
|
||||||
|
this.status = '';
|
||||||
|
this.fromDate = todayDDMMYYYY();
|
||||||
|
this.toDate = addPeriod(this.fromDate, 0, 0, days);
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** What is still pending from the past (up to a year back) and today. */
|
||||||
|
showOverdue(): void {
|
||||||
|
this.toDate = todayDDMMYYYY();
|
||||||
|
this.fromDate = addPeriod(this.toDate, -1, 0, 0);
|
||||||
|
this.status = 'PENDING';
|
||||||
|
this.load();
|
||||||
|
}
|
||||||
|
|
||||||
|
load(): void {
|
||||||
|
if (!this.fromDate || !this.toDate) {
|
||||||
|
this.error = 'Enter the From and To dates.';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.loading = true;
|
||||||
|
this.error = '';
|
||||||
|
this.investmentService.getDiary(this.fromDate, this.toDate).subscribe({
|
||||||
|
next: (d) => {
|
||||||
|
this.diary = d;
|
||||||
|
this.loading = false;
|
||||||
|
},
|
||||||
|
error: (err) => {
|
||||||
|
this.diary = null;
|
||||||
|
this.loading = false;
|
||||||
|
this.error = apiErrorMessage(err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
get rows(): InvestmentDiaryRowDto[] {
|
||||||
|
return (this.diary?.rows ?? []).filter(
|
||||||
|
(r) =>
|
||||||
|
(!this.instrument || r.instrument === this.instrument) &&
|
||||||
|
(!this.status || (this.status === 'POSTED' ? r.status === 'POSTED' : r.status !== 'POSTED'))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The screen that posts this entry. */
|
||||||
|
link(r: InvestmentDiaryRowDto): { path: string; query: Record<string, string | number> } {
|
||||||
|
if (r.instrument === 'FD') {
|
||||||
|
return { path: '/investment/fd', query: { tab: 'interest', fd: r.refId } };
|
||||||
|
}
|
||||||
|
const tab = r.kind === 'MATURITY' ? 'redemption' : r.kind === 'AMORTISATION' ? 'amortize' : 'interest';
|
||||||
|
return { path: '/investment/bonds', query: { tab } };
|
||||||
|
}
|
||||||
|
|
||||||
|
statusLabel(r: InvestmentDiaryRowDto): string {
|
||||||
|
return r.status === 'DUE' ? 'Due today' : r.status.charAt(0) + r.status.slice(1).toLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -349,6 +349,16 @@
|
|||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
><span>Corp. Code</span><input type="text" [(ngModel)]="scriptForm.corpCode"
|
><span>Corp. Code</span><input type="text" [(ngModel)]="scriptForm.corpCode"
|
||||||
/></label>
|
/></label>
|
||||||
|
<label class="inv-form-field">
|
||||||
|
<span>Status</span>
|
||||||
|
<select
|
||||||
|
[ngModel]="scriptForm.active === false ? 'INACTIVE' : 'ACTIVE'"
|
||||||
|
(ngModelChange)="scriptForm.active = $event !== 'INACTIVE'"
|
||||||
|
>
|
||||||
|
<option value="ACTIVE">ACTIVE</option>
|
||||||
|
<option value="INACTIVE">INACTIVE</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="inv-form-actions">
|
<div class="inv-form-actions">
|
||||||
<label class="inv-form-field"
|
<label class="inv-form-field"
|
||||||
@@ -371,6 +381,7 @@
|
|||||||
<th class="num">Int Rate %</th>
|
<th class="num">Int Rate %</th>
|
||||||
<th>Issue Date</th>
|
<th>Issue Date</th>
|
||||||
<th>Maturity</th>
|
<th>Maturity</th>
|
||||||
|
<th>Status</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -387,6 +398,7 @@
|
|||||||
<td class="num">{{ s.couponRate ?? '—' }}</td>
|
<td class="num">{{ s.couponRate ?? '—' }}</td>
|
||||||
<td>{{ s.issueDate || '—' }}</td>
|
<td>{{ s.issueDate || '—' }}</td>
|
||||||
<td>{{ s.maturityDate || '—' }}</td>
|
<td>{{ s.maturityDate || '—' }}</td>
|
||||||
|
<td>{{ s.active === false ? 'INACTIVE' : 'ACTIVE' }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
}
|
}
|
||||||
</tbody>
|
</tbody>
|
||||||
|
|||||||
@@ -350,6 +350,7 @@ export class InvestmentMastersComponent implements OnInit {
|
|||||||
phone: f.phone ?? '',
|
phone: f.phone ?? '',
|
||||||
fax: f.fax ?? '',
|
fax: f.fax ?? '',
|
||||||
corpCode: f.corpCode ?? '',
|
corpCode: f.corpCode ?? '',
|
||||||
|
active: f.active !== false,
|
||||||
dateFormat: 'dd-MM-yyyy',
|
dateFormat: 'dd-MM-yyyy',
|
||||||
locale: 'en'
|
locale: 'en'
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { ShareInvestmentComponent } from './share-investment/share-investment.co
|
|||||||
import { InvestmentMastersComponent } from './investment-masters/investment-masters.component';
|
import { InvestmentMastersComponent } from './investment-masters/investment-masters.component';
|
||||||
import { InvestmentReportsComponent } from './investment-reports/investment-reports.component';
|
import { InvestmentReportsComponent } from './investment-reports/investment-reports.component';
|
||||||
import { InvestmentInterestComponent } from './investment-interest/investment-interest.component';
|
import { InvestmentInterestComponent } from './investment-interest/investment-interest.component';
|
||||||
|
import { InvestmentDiaryComponent } from './investment-diary/investment-diary.component';
|
||||||
|
|
||||||
const routes: Routes = [
|
const routes: Routes = [
|
||||||
Route.withShell([
|
Route.withShell([
|
||||||
@@ -54,6 +55,11 @@ const routes: Routes = [
|
|||||||
component: InvestmentInterestComponent,
|
component: InvestmentInterestComponent,
|
||||||
data: { title: 'Investment Interest', breadcrumb: 'Investment Interest' }
|
data: { title: 'Investment Interest', breadcrumb: 'Investment Interest' }
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'diary',
|
||||||
|
component: InvestmentDiaryComponent,
|
||||||
|
data: { title: 'Due Date Diary', breadcrumb: 'Due Date Diary' }
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: 'reports',
|
path: 'reports',
|
||||||
component: InvestmentReportsComponent,
|
component: InvestmentReportsComponent,
|
||||||
|
|||||||
@@ -143,6 +143,30 @@ export interface InvestmentFdScheduleDto {
|
|||||||
rows: InvestmentFdInterestRowDto[];
|
rows: InvestmentFdInterestRowDto[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** One dated event in the Due Date Diary. */
|
||||||
|
export interface InvestmentDiaryRowDto {
|
||||||
|
date: string;
|
||||||
|
kind: 'COUPON' | 'PROVISION' | 'MATURITY' | 'AMORTISATION' | 'FD_PROVISION' | 'FD_PAYOUT' | 'FD_MATURITY';
|
||||||
|
instrument: 'SECURITY' | 'FD';
|
||||||
|
refId: number;
|
||||||
|
dealNo: string;
|
||||||
|
particulars: string;
|
||||||
|
amount?: number;
|
||||||
|
status: 'POSTED' | 'OVERDUE' | 'DUE' | 'UPCOMING';
|
||||||
|
voucherNo?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Due Date Diary for a period, with what is still pending and the money expected in. */
|
||||||
|
export interface InvestmentDiaryDto {
|
||||||
|
fromDate: string;
|
||||||
|
toDate: string;
|
||||||
|
asOnDate: string;
|
||||||
|
pendingCount: number;
|
||||||
|
overdueCount: number;
|
||||||
|
expectedInflow: number;
|
||||||
|
rows: InvestmentDiaryRowDto[];
|
||||||
|
}
|
||||||
|
|
||||||
/** The next interest voucher (or the maturity) of an FD that is due. */
|
/** The next interest voucher (or the maturity) of an FD that is due. */
|
||||||
export interface InvestmentFdDueDto {
|
export interface InvestmentFdDueDto {
|
||||||
fdId: number;
|
fdId: number;
|
||||||
@@ -477,6 +501,11 @@ export class InvestmentService {
|
|||||||
return this.http.get<InvestmentFdDueDto[]>('/investment/fds/due');
|
return this.http.get<InvestmentFdDueDto[]>('/investment/fds/due');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Due Date Diary: dated events of all active investments between two dates (DD-MM-YYYY). */
|
||||||
|
getDiary(fromDate: string, toDate: string): Observable<InvestmentDiaryDto> {
|
||||||
|
return this.http.get<InvestmentDiaryDto>('/investment/diary', { params: { fromDate, toDate } });
|
||||||
|
}
|
||||||
|
|
||||||
/** Records total deposits as on a 31 March, the base of the RBI limits. */
|
/** Records total deposits as on a 31 March, the base of the RBI limits. */
|
||||||
setDepositBase(body: object): Observable<unknown> {
|
setDepositBase(body: object): Observable<unknown> {
|
||||||
return this.http.post('/investment/fds/deposit-base', body);
|
return this.http.post('/investment/fds/deposit-base', body);
|
||||||
|
|||||||
Reference in New Issue
Block a user