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:
@@ -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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user