FD with banks: {{ pendingFd.length }} entry(ies){{ overdueNote(pendingFd) }}FD with banks: {{ pendingFd.length }} FD(s) with entries pending{{ overdueNote(pendingFd) }}
+
+
+ File columns: Script Code (or ISIN), Rate Date (DD-MM-YYYY), Close, Open, High, Low — first row is the header.
+ Use it for the day's market rates and mutual fund NAVs.
+
+ @if (uploadResults.length) {
+
+ @for (r of uploadResults; track r.row) {
+
Row {{ r.row }}: {{ r.text }}
+ }
+
+ }
@@ -640,10 +659,11 @@
{{ r.scriptCode }}
{{ r.scriptName }}
-
{{ r.openRate | number: '1.2-2' }}
-
{{ r.highRate | number: '1.2-2' }}
-
{{ r.lowRate | number: '1.2-2' }}
-
{{ r.closeRate | number: '1.2-2' }}
+
+
{{ r.openRate | number: '1.2-4' }}
+
{{ r.highRate | number: '1.2-4' }}
+
{{ r.lowRate | number: '1.2-4' }}
+
{{ r.closeRate | number: '1.2-4' }}
diff --git a/src/app/investment/investment-masters/investment-masters.component.ts b/src/app/investment/investment-masters/investment-masters.component.ts
index 13c2f137c..c706aa571 100644
--- a/src/app/investment/investment-masters/investment-masters.component.ts
+++ b/src/app/investment/investment-masters/investment-masters.component.ts
@@ -9,6 +9,7 @@
import { Component, OnInit, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
+import { firstValueFrom } from 'rxjs';
import { DateDdmmyyyyPickerComponent } from '../date-ddmmyyyy-picker/date-ddmmyyyy-picker.component';
import {
BROKER_MASTER,
@@ -33,6 +34,7 @@ import {
SLR_TYPE_OPTIONS,
INVESTMENT_GL_LOOKUP,
ELIGIBLE_RATINGS,
+ parseDDMMYYYY,
RATING_AGENCY_OPTIONS,
RATING_OPTIONS,
periodBetween
@@ -310,7 +312,19 @@ export class InvestmentMastersComponent implements OnInit {
/** Whether the rating entered meets the RBI minimum: "A" for bonds, "A2" for CP / CD. */
get scriptRatingEligible(): boolean {
- return !!this.scriptForm.rating && ELIGIBLE_RATINGS.includes(this.scriptForm.rating);
+ const r = this.scriptForm.rating;
+ return !!r && ELIGIBLE_RATINGS.includes(r) && this.scriptIsShortTerm === /^A[1-4]\+?$/.test(r);
+ }
+
+ /** A CP / CD runs one year or less and takes a short-term rating (A1+ to A4); a bond takes a long-term one. */
+ get scriptIsShortTerm(): boolean {
+ const f = this.scriptForm;
+ if (!f.issueDate || !f.maturityDate) {
+ return false;
+ }
+ const issue = parseDDMMYYYY(f.issueDate);
+ const limit = new Date(issue.getFullYear() + 1, issue.getMonth(), issue.getDate());
+ return parseDDMMYYYY(f.maturityDate) <= limit;
}
/** Preview of the code the backend will assign — a type letter + running number, e.g. G00001. */
@@ -361,6 +375,8 @@ export class InvestmentMastersComponent implements OnInit {
glAccountId: Number(f.glAccountId),
isin: f.isin ?? '',
intRate: f.couponRate ?? null,
+ // Units = face value / face value per unit (100 for G-Secs by default; bonds can be 1,000 or 10,00,000).
+ ...(f.faceValuePerUnit ? { faceValuePerUnit: Number(f.faceValuePerUnit) } : {}),
issueDate: f.issueDate ?? '',
maturityDate: f.maturityDate ?? '',
companyName: f.companyName ?? '',
@@ -541,6 +557,113 @@ export class InvestmentMastersComponent implements OnInit {
});
}
+ // -------------------- Daily rate upload (CSV / Excel) --------------------
+ /** Result of the last upload, one line per row of the file. */
+ uploadResults: Array<{ row: number; text: string; ok: boolean }> = [];
+ uploading = false;
+
+ /**
+ * Uploads the day's rates / NAVs from a CSV or Excel file (Jitendra Sir): columns Script Code (or ISIN), Rate Date
+ * (DD-MM-YYYY), Close, and optionally Open, High, Low; the first row is the header. Each row is saved on its own,
+ * so one bad row does not stop the rest.
+ */
+ async uploadDailyRates(event: Event): Promise {
+ const input = event.target as HTMLInputElement;
+ const file = input.files?.[0];
+ input.value = '';
+ if (!file) {
+ return;
+ }
+ this.uploadResults = [];
+ let rows: string[][];
+ try {
+ rows = file.name.toLowerCase().endsWith('.xlsx') ? await this.readXlsx(file) : this.readCsv(await file.text());
+ } catch {
+ this.toast('Could not read the file. Use CSV or Excel (.xlsx).');
+ return;
+ }
+ const data = rows.slice(1).filter((r) => r.some((c) => c.trim() !== ''));
+ if (!data.length) {
+ this.toast('The file has no rate rows.');
+ return;
+ }
+ this.uploading = true;
+ for (let i = 0; i < data.length; i++) {
+ const [
+ code,
+ rateDate,
+ close,
+ open,
+ high,
+ low
+ ] = data[i].map((c) => c.trim());
+ const script = this.scripts.find(
+ (s) =>
+ s.scriptCode.toUpperCase() === code.toUpperCase() || (!!s.isin && s.isin.toUpperCase() === code.toUpperCase())
+ );
+ const num = (v?: string) => (v ? Number(v.replace(/,/g, '')) : null);
+ if (!script?.id || !rateDate || num(close) == null || isNaN(num(close) as number)) {
+ this.uploadResults.push({
+ row: i + 2,
+ text: `${code || '?'}: script not found or date / close missing`,
+ ok: false
+ });
+ continue;
+ }
+ const body = {
+ scriptId: script.id,
+ rateDate,
+ closeRate: num(close),
+ ...(num(open) != null ? { openRate: num(open) } : {}),
+ ...(num(high) != null ? { highRate: num(high) } : {}),
+ ...(num(low) != null ? { lowRate: num(low) } : {}),
+ dateFormat: 'dd-MM-yyyy',
+ locale: 'en'
+ };
+ try {
+ await firstValueFrom(this.investmentService.createDailyRate(body, true));
+ this.uploadResults.push({ row: i + 2, text: `${script.scriptCode} ${rateDate}: saved`, ok: true });
+ } catch (err) {
+ this.uploadResults.push({
+ row: i + 2,
+ text: `${script.scriptCode} ${rateDate}: ${apiErrorMessage(err)}`,
+ ok: false
+ });
+ }
+ }
+ this.uploading = false;
+ const saved = this.uploadResults.filter((r) => r.ok).length;
+ this.toast(`${saved} of ${data.length} rate(s) saved.`);
+ this.loadDailyRates();
+ }
+
+ private readCsv(text: string): string[][] {
+ return text.split(/\r?\n/).map((line) => line.split(',').map((c) => c.replace(/^"|"$/g, '')));
+ }
+
+ private async readXlsx(file: File): Promise {
+ const ExcelJS = await import('exceljs');
+ const wb = new ExcelJS.Workbook();
+ await wb.xlsx.load(await file.arrayBuffer());
+ const sheet = wb.worksheets[0];
+ const rows: string[][] = [];
+ sheet.eachRow({ includeEmpty: false }, (row) => {
+ const cells: string[] = [];
+ for (let c = 1; c <= 6; c++) {
+ const v = row.getCell(c).value;
+ cells.push(
+ v instanceof Date
+ ? `${String(v.getDate()).padStart(2, '0')}-${String(v.getMonth() + 1).padStart(2, '0')}-${v.getFullYear()}`
+ : v == null
+ ? ''
+ : String(v)
+ );
+ }
+ rows.push(cells);
+ });
+ return rows;
+ }
+
/** A wrong rate is corrected by dropping it and entering it again. */
dropDailyRate(r: DailyScriptRate): void {
if (!r.id) {
diff --git a/src/app/investment/investment-reports/investment-reports.component.html b/src/app/investment/investment-reports/investment-reports.component.html
index 29c989aee..b3a502cbb 100644
--- a/src/app/investment/investment-reports/investment-reports.component.html
+++ b/src/app/investment/investment-reports/investment-reports.component.html
@@ -122,7 +122,8 @@