1037 lines
31 KiB
TypeScript
1037 lines
31 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, 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,
|
|
SUBTYPE_MASTER,
|
|
CONTROL_PARAMETERS,
|
|
MUTUAL_FUND_PRODUCTS,
|
|
SGL_BANK_MASTER,
|
|
SCRIPT_MARKET_DETAIL,
|
|
LIEN_REASON_MASTER,
|
|
ScriptMaster,
|
|
BrokerMaster,
|
|
PartyMaster,
|
|
SubTypeMaster,
|
|
ControlParameter,
|
|
MutualFundProductMaster,
|
|
SglBankMaster,
|
|
ScriptMarketDetail,
|
|
DailyScriptRate,
|
|
LienReason,
|
|
InstrumentType,
|
|
BANK_TYPE_OPTIONS,
|
|
SLR_TYPE_OPTIONS,
|
|
INVESTMENT_GL_LOOKUP,
|
|
ELIGIBLE_RATINGS,
|
|
parseDDMMYYYY,
|
|
RATING_AGENCY_OPTIONS,
|
|
RATING_OPTIONS,
|
|
periodBetween
|
|
} from '../investment-data';
|
|
import { exportWorkbook, ExportSheetSpec } from '../investment-export';
|
|
import {
|
|
InvestmentGlAccount,
|
|
InvestmentService,
|
|
SHARE_INSTITUTION_TYPES,
|
|
apiErrorMessage,
|
|
awaitingApproval,
|
|
sentForApproval,
|
|
toDailyScriptRate
|
|
} from '../investment.service';
|
|
|
|
type MasterTab =
|
|
| 'party'
|
|
| 'script'
|
|
| 'subtype'
|
|
| 'broker'
|
|
| 'control'
|
|
| 'dailyscript'
|
|
| 'lien'
|
|
| 'mfproduct'
|
|
| 'sglbank'
|
|
| 'marketdetail';
|
|
|
|
/**
|
|
* Investment master-data screens (legacy "04-13" Entry submenu masters) — 9 simple dummy
|
|
* lists, each with an inline Add-row form so the demo stays interactive without a backend.
|
|
*/
|
|
/** Detail GLs under the 100700 INVESTMENTS head; 100707 holds premium only, never a party's or script's GL. */
|
|
const INVESTMENT_GL_HEAD_PREFIX = '1007';
|
|
const PREMIUM_ON_INVESTMENTS_GL_CODE = '100707';
|
|
|
|
@Component({
|
|
selector: 'mifosx-investment-masters',
|
|
templateUrl: './investment-masters.component.html',
|
|
styleUrls: [
|
|
'../../cashier/cashier-shared.scss',
|
|
'../investment-shared.scss'
|
|
],
|
|
standalone: true,
|
|
imports: [
|
|
CommonModule,
|
|
FormsModule,
|
|
DateDdmmyyyyPickerComponent
|
|
]
|
|
})
|
|
export class InvestmentMastersComponent implements OnInit {
|
|
activeTab: MasterTab = 'party';
|
|
|
|
scripts: ScriptMaster[] = [];
|
|
brokers: BrokerMaster[] = [...BROKER_MASTER];
|
|
parties: PartyMaster[] = [];
|
|
subTypes: SubTypeMaster[] = [...SUBTYPE_MASTER];
|
|
controlParams: ControlParameter[] = [...CONTROL_PARAMETERS];
|
|
mfProducts: MutualFundProductMaster[] = [...MUTUAL_FUND_PRODUCTS];
|
|
sglBanks: SglBankMaster[] = [...SGL_BANK_MASTER];
|
|
marketDetails: ScriptMarketDetail[] = [...SCRIPT_MARKET_DETAIL];
|
|
/** Loaded from the backend (Daily Script Master) in ngOnInit. */
|
|
dailyScriptRates: DailyScriptRate[] = [];
|
|
lienReasons: LienReason[] = [...LIEN_REASON_MASTER];
|
|
|
|
readonly instrumentTypes: InstrumentType[] = [
|
|
'FD',
|
|
'GOVT_SECURITY',
|
|
'NON_SLR',
|
|
'BOND',
|
|
'MUTUAL_FUND',
|
|
'SHARE'
|
|
];
|
|
|
|
private readonly masterLabels: Record<MasterTab, string> = {
|
|
party: 'Bank / Party Master',
|
|
script: 'Script Master',
|
|
subtype: 'SubType Master',
|
|
broker: 'Broker Master',
|
|
control: 'Control Parameter',
|
|
dailyscript: 'Daily Script Master',
|
|
lien: 'LIEN Reason Master',
|
|
mfproduct: 'Mutual Fund Product Master',
|
|
sglbank: 'SGL Bank Master',
|
|
marketdetail: 'Script Market Detail'
|
|
};
|
|
|
|
generating = false;
|
|
toastMsg = '';
|
|
toastVisible = false;
|
|
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
|
|
|
// Bank / Party Master and Script Master follow the bank's existing CBS screens (one entry form
|
|
// with Save / Search / Cancel / Drop / Exit) and are stored in the backend through
|
|
// InvestmentService; the other master tabs are still sample data.
|
|
private readonly investmentService = inject(InvestmentService);
|
|
partyForm: Partial<PartyMaster> = {};
|
|
partySearchCode = '';
|
|
scriptForm: Partial<ScriptMaster> = {};
|
|
scriptSearchCode = '';
|
|
saving = false;
|
|
readonly bankTypeOptions = BANK_TYPE_OPTIONS;
|
|
readonly slrTypeOptions = SLR_TYPE_OPTIONS;
|
|
readonly ratingOptions = RATING_OPTIONS;
|
|
readonly ratingAgencyOptions = RATING_AGENCY_OPTIONS;
|
|
readonly shareInstitutionTypes = SHARE_INSTITUTION_TYPES;
|
|
/** Investment GLs (under the 100700 INVESTMENTS head) a party or script can be mapped to — not loans, banks or test GLs. */
|
|
investmentGlOptions: InvestmentGlAccount[] = [];
|
|
/** Income GLs under 400400 INCOME FROM INVESTMENTS, for an FD bank's interest. */
|
|
interestGlOptions: InvestmentGlAccount[] = [];
|
|
newSubType: Partial<SubTypeMaster> = {};
|
|
newBroker: Partial<BrokerMaster> = {};
|
|
newControlParam: Partial<ControlParameter> = {};
|
|
newLienReason: Partial<LienReason> = {};
|
|
newMfProduct: Partial<MutualFundProductMaster> = {};
|
|
newSglBank: Partial<SglBankMaster> = {};
|
|
newDailyRate: Partial<DailyScriptRate> = {};
|
|
|
|
ngOnInit(): void {
|
|
this.loadDailyRates();
|
|
this.investmentService.getAssetGlAccounts().subscribe({
|
|
next: (gls) =>
|
|
(this.investmentGlOptions = gls.filter(
|
|
(g) => g.glCode.startsWith(INVESTMENT_GL_HEAD_PREFIX) && g.glCode !== PREMIUM_ON_INVESTMENTS_GL_CODE
|
|
)),
|
|
error: (err) => this.toast(`Could not load GL accounts: ${apiErrorMessage(err)}`)
|
|
});
|
|
this.investmentService.getIncomeGlAccounts().subscribe({
|
|
next: (gls) => (this.interestGlOptions = gls.filter((g) => g.glCode.startsWith('4004'))),
|
|
error: (err) => this.toast(`Could not load income GLs: ${apiErrorMessage(err)}`)
|
|
});
|
|
this.reloadMasters();
|
|
}
|
|
|
|
setTab(tab: MasterTab): void {
|
|
this.activeTab = tab;
|
|
}
|
|
|
|
/** Re-reads both masters from the backend (also refreshes the lists the Purchase screen uses). */
|
|
private reloadMasters(): void {
|
|
this.investmentService.syncSharedMasters().subscribe({
|
|
next: ({ parties, scripts }) => {
|
|
this.parties = parties;
|
|
this.scripts = scripts;
|
|
},
|
|
error: (err) => this.toast(`Could not load masters: ${apiErrorMessage(err)}`)
|
|
});
|
|
}
|
|
|
|
glName(glCode?: string): string {
|
|
return (
|
|
this.investmentGlOptions.find((g) => g.glCode === glCode)?.name ??
|
|
INVESTMENT_GL_LOOKUP.find((g) => g.glCode === glCode)?.glName ??
|
|
''
|
|
);
|
|
}
|
|
|
|
/** FD GLs are the bank-wise children of 100704 (codes 1007040x). */
|
|
private static isFdGl(glCode: string): boolean {
|
|
return glCode.startsWith('1007040');
|
|
}
|
|
|
|
/** An FD bank is mapped to its own FD GL; any other party to a non-FD investment GL. */
|
|
get partyGlOptions(): InvestmentGlAccount[] {
|
|
const fd = this.partyForm.bondType === 'FD';
|
|
return this.investmentGlOptions.filter((g) => InvestmentMastersComponent.isFdGl(g.glCode) === fd);
|
|
}
|
|
|
|
/** Scripts are securities, never an FD with a bank. */
|
|
get scriptGlOptions(): InvestmentGlAccount[] {
|
|
return this.investmentGlOptions.filter((g) => !InvestmentMastersComponent.isFdGl(g.glCode));
|
|
}
|
|
|
|
// -------------------- Bank / Party Master --------------------
|
|
/** Preview of the code the backend will assign; the saved code always comes from the backend. */
|
|
get nextPartyCode(): string {
|
|
const nums = this.parties.map((p) => parseInt(p.partyCode.replace(/\D/g, ''), 10)).filter((n) => !isNaN(n));
|
|
return `PTY-${String((nums.length ? Math.max(...nums) : 0) + 1).padStart(3, '0')}`;
|
|
}
|
|
|
|
saveParty(): void {
|
|
const f = this.partyForm;
|
|
if (!f.bondType || !f.partyName || !f.glAccountId) {
|
|
this.toast('Enter Bond Type, Party Name and GL A/c No.');
|
|
return;
|
|
}
|
|
if (f.bondType === 'FD' && !f.interestGlAccountId) {
|
|
this.toast('An FD bank needs its Interest GL as well.');
|
|
return;
|
|
}
|
|
const tds = f.bondType === 'FD' && !!f.tdsApplicable;
|
|
if (tds && !(Number(f.tdsRate) > 0 && Number(f.tdsRate) < 100)) {
|
|
this.toast('Enter the TDS rate (%) this bank deducts on FD interest.');
|
|
return;
|
|
}
|
|
const share = f.bondType === 'SHARE';
|
|
if (share && !f.institutionType) {
|
|
this.toast('Select the Institution Type of this share institution.');
|
|
return;
|
|
}
|
|
if (share && f.institutionType === 'OTHER_COOP_SOCIETY' && !(Number(f.subscribedCapital) > 0)) {
|
|
this.toast('Enter the subscribed share capital of the society (the 5% limit on one society).');
|
|
return;
|
|
}
|
|
const body = {
|
|
bondType: f.bondType,
|
|
partyName: f.partyName,
|
|
address1: f.address1 ?? '',
|
|
address2: f.address2 ?? '',
|
|
address3: f.address3 ?? '',
|
|
city: f.city ?? '',
|
|
pincode: f.pincode ?? '',
|
|
phone: f.phone ?? '',
|
|
fax: f.fax ?? '',
|
|
bankType: f.bankType ?? '',
|
|
glAccountId: Number(f.glAccountId),
|
|
interestGlAccountId: f.bondType === 'FD' && f.interestGlAccountId ? Number(f.interestGlAccountId) : null,
|
|
tdsApplicable: tds,
|
|
tdsRate: tds ? Number(f.tdsRate) : null,
|
|
rbiCode: f.rbiCode ?? '',
|
|
limit: f.limit ?? null,
|
|
leiNo: f.leiNo ?? '',
|
|
cpBankName: f.cpBankName ?? '',
|
|
cpBranchName: f.cpBranchName ?? '',
|
|
cpAccountNo: f.cpAccountNo ?? '',
|
|
cpIfsc: f.cpIfsc ?? '',
|
|
// Shares only: who issued them (only co-operative / membership shares may be held), and what values them.
|
|
institutionType: share ? f.institutionType : '',
|
|
subscribedCapital: share && f.subscribedCapital ? Number(f.subscribedCapital) : null,
|
|
lastBalanceSheetDate: share ? (f.lastBalanceSheetDate ?? '') : '',
|
|
inLiquidation: share && !!f.inLiquidation,
|
|
active: f.status !== 'INACTIVE',
|
|
dateFormat: 'dd-MM-yyyy',
|
|
locale: 'en'
|
|
};
|
|
const request = f.id ? this.investmentService.updateParty(f.id, body) : this.investmentService.createParty(body);
|
|
this.saving = true;
|
|
request.subscribe({
|
|
next: (res) => {
|
|
this.saving = false;
|
|
this.toast(
|
|
awaitingApproval(res)
|
|
? sentForApproval(f.id ? `Change to party ${f.partyCode}` : `New party ${f.partyName ?? ''}`)
|
|
: `Party ${res.resourceIdentifier ?? f.partyCode} ${f.id ? 'updated' : 'saved'}.`
|
|
);
|
|
this.partyForm = {};
|
|
this.reloadMasters();
|
|
},
|
|
error: (err) => {
|
|
this.saving = false;
|
|
this.toast(apiErrorMessage(err));
|
|
}
|
|
});
|
|
}
|
|
|
|
searchParty(): void {
|
|
const p = this.parties.find((x) => x.partyCode.toUpperCase() === this.partySearchCode.trim().toUpperCase());
|
|
if (!p) {
|
|
this.toast(`Party Code ${this.partySearchCode} not found.`);
|
|
return;
|
|
}
|
|
this.partyForm = { ...p };
|
|
this.toast(`Party ${p.partyCode} loaded.`);
|
|
}
|
|
|
|
cancelParty(): void {
|
|
this.partyForm = {};
|
|
this.partySearchCode = '';
|
|
}
|
|
|
|
dropParty(): void {
|
|
const f = this.partyForm;
|
|
if (!f.id) {
|
|
this.toast('Search a saved party first, then Drop.');
|
|
return;
|
|
}
|
|
this.investmentService.deleteParty(f.id).subscribe({
|
|
next: (res) => {
|
|
this.toast(
|
|
awaitingApproval(res) ? sentForApproval(`Drop of party ${f.partyCode}`) : `Party ${f.partyCode} dropped.`
|
|
);
|
|
this.partyForm = {};
|
|
this.reloadMasters();
|
|
},
|
|
error: (err) => this.toast(apiErrorMessage(err))
|
|
});
|
|
}
|
|
|
|
// -------------------- Script Master --------------------
|
|
/** Coupon / maturity terms only apply to debt securities. */
|
|
get scriptIsDebtSecurity(): boolean {
|
|
return [
|
|
'GOVT_SECURITY',
|
|
'NON_SLR',
|
|
'BOND'
|
|
].includes(this.scriptForm.instrumentType ?? '');
|
|
}
|
|
|
|
/** Corporate bonds, CPs and CDs carry a credit rating and a listed / unlisted flag (RBI non-SLR rules). */
|
|
get scriptIsRated(): boolean {
|
|
return [
|
|
'NON_SLR',
|
|
'BOND'
|
|
].includes(this.scriptForm.instrumentType ?? '');
|
|
}
|
|
|
|
/** Whether the rating entered meets the RBI minimum: "A" for bonds, "A2" for CP / CD. */
|
|
get scriptRatingEligible(): boolean {
|
|
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. */
|
|
get nextScriptCode(): string {
|
|
const prefixByType: Record<InstrumentType, string> = {
|
|
GOVT_SECURITY: 'G',
|
|
NON_SLR: 'N',
|
|
BOND: 'B',
|
|
MUTUAL_FUND: 'M',
|
|
FD: 'F',
|
|
SHARE: 'S'
|
|
};
|
|
const type = this.scriptForm.instrumentType;
|
|
if (!type) {
|
|
return '';
|
|
}
|
|
const prefix = prefixByType[type];
|
|
const pattern = new RegExp(`^${prefix}(\\d{5})$`);
|
|
const nums = this.scripts
|
|
.map((s) => pattern.exec(s.scriptCode)?.[1])
|
|
.filter((n): n is string => !!n)
|
|
.map((n) => parseInt(n, 10));
|
|
return `${prefix}${String((nums.length ? Math.max(...nums) : 0) + 1).padStart(5, '0')}`;
|
|
}
|
|
|
|
get scriptPeriod(): { years: number; months: number; days: number } {
|
|
return periodBetween(this.scriptForm.issueDate, this.scriptForm.maturityDate);
|
|
}
|
|
|
|
saveScript(): void {
|
|
const f = this.scriptForm;
|
|
if (!f.instrumentType || !f.scriptName || !f.glAccountId) {
|
|
this.toast('Enter Bond Type, Script Desc and GL A/c No.');
|
|
return;
|
|
}
|
|
if (this.scriptIsDebtSecurity && (!f.isin || f.couponRate == null || !f.issueDate || !f.maturityDate)) {
|
|
this.toast('Enter ISIN, Int Rate, Issue Date and Maturity Date for this security.');
|
|
return;
|
|
}
|
|
if (this.scriptIsRated && !f.rating) {
|
|
this.toast('Enter the credit rating of this bond / CP / CD.');
|
|
return;
|
|
}
|
|
const body = {
|
|
bondType: f.instrumentType,
|
|
slrType: f.slrType ?? '',
|
|
scriptDesc: f.scriptName,
|
|
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 ?? '',
|
|
address1: f.address1 ?? '',
|
|
address2: f.address2 ?? '',
|
|
address3: f.address3 ?? '',
|
|
city: f.city ?? '',
|
|
pincode: f.pincode ?? '',
|
|
phone: f.phone ?? '',
|
|
fax: f.fax ?? '',
|
|
corpCode: f.corpCode ?? '',
|
|
rating: this.scriptIsRated ? (f.rating ?? '') : '',
|
|
ratingAgency: this.scriptIsRated ? (f.ratingAgency ?? '') : '',
|
|
listed: !this.scriptIsRated || f.listed !== false,
|
|
active: f.active !== false,
|
|
dateFormat: 'dd-MM-yyyy',
|
|
locale: 'en'
|
|
};
|
|
const request = f.id ? this.investmentService.updateScript(f.id, body) : this.investmentService.createScript(body);
|
|
this.saving = true;
|
|
request.subscribe({
|
|
next: (res) => {
|
|
this.saving = false;
|
|
this.toast(
|
|
awaitingApproval(res)
|
|
? sentForApproval(f.id ? `Change to script ${f.scriptCode}` : `New script ${f.scriptName ?? ''}`)
|
|
: `Script ${res.resourceIdentifier ?? f.scriptCode} ${f.id ? 'updated' : 'saved'}.`
|
|
);
|
|
this.scriptForm = {};
|
|
this.reloadMasters();
|
|
},
|
|
error: (err) => {
|
|
this.saving = false;
|
|
this.toast(apiErrorMessage(err));
|
|
}
|
|
});
|
|
}
|
|
|
|
searchScript(): void {
|
|
const s = this.scripts.find((x) => x.scriptCode.toUpperCase() === this.scriptSearchCode.trim().toUpperCase());
|
|
if (!s) {
|
|
this.toast(`Script Code ${this.scriptSearchCode} not found.`);
|
|
return;
|
|
}
|
|
this.scriptForm = { ...s };
|
|
this.toast(`Script ${s.scriptCode} loaded.`);
|
|
}
|
|
|
|
cancelScript(): void {
|
|
this.scriptForm = {};
|
|
this.scriptSearchCode = '';
|
|
}
|
|
|
|
addSubType(): void {
|
|
if (!this.newSubType.subTypeName || !this.newSubType.instrumentType) {
|
|
this.toast('Enter Sub Type Name and Instrument Type.');
|
|
return;
|
|
}
|
|
this.subTypes = [
|
|
{ subTypeCode: String(this.subTypes.length + 1), ...this.newSubType } as SubTypeMaster,
|
|
...this.subTypes
|
|
];
|
|
this.newSubType = {};
|
|
this.toast('Sub Type master added.');
|
|
}
|
|
|
|
addBroker(): void {
|
|
if (!this.newBroker.brokerName) {
|
|
this.toast('Enter Broker Name.');
|
|
return;
|
|
}
|
|
this.brokers = [
|
|
{
|
|
brokerCode: `BRK-${String(this.brokers.length + 1).padStart(3, '0')}`,
|
|
contactNo: '',
|
|
empanelledOn: '',
|
|
status: 'ACTIVE',
|
|
...this.newBroker
|
|
} as BrokerMaster,
|
|
...this.brokers
|
|
];
|
|
this.newBroker = {};
|
|
this.toast('Broker master added.');
|
|
}
|
|
|
|
addControlParam(): void {
|
|
if (!this.newControlParam.paramName || !this.newControlParam.paramValue) {
|
|
this.toast('Enter Parameter Name and Value.');
|
|
return;
|
|
}
|
|
this.controlParams = [
|
|
{ description: '', ...this.newControlParam } as ControlParameter,
|
|
...this.controlParams
|
|
];
|
|
this.newControlParam = {};
|
|
this.toast('Control parameter added.');
|
|
}
|
|
|
|
addLienReason(): void {
|
|
if (!this.newLienReason.reasonDescription) {
|
|
this.toast('Enter Reason Description.');
|
|
return;
|
|
}
|
|
this.lienReasons = [
|
|
{ reasonCode: `LR-${String(this.lienReasons.length + 1).padStart(2, '0')}`, ...this.newLienReason } as LienReason,
|
|
...this.lienReasons
|
|
];
|
|
this.newLienReason = {};
|
|
this.toast('LIEN reason added.');
|
|
}
|
|
|
|
addMfProduct(): void {
|
|
if (!this.newMfProduct.productName || !this.newMfProduct.amcName) {
|
|
this.toast('Enter Product Name and AMC Name.');
|
|
return;
|
|
}
|
|
this.mfProducts = [
|
|
{
|
|
productCode: `MFP-${String(this.mfProducts.length + 1).padStart(2, '0')}`,
|
|
category: '',
|
|
riskLevel: 'MODERATE',
|
|
...this.newMfProduct
|
|
} as MutualFundProductMaster,
|
|
...this.mfProducts
|
|
];
|
|
this.newMfProduct = {};
|
|
this.toast('Mutual Fund product added.');
|
|
}
|
|
|
|
addSglBank(): void {
|
|
if (!this.newSglBank.bankName) {
|
|
this.toast('Enter Bank Name.');
|
|
return;
|
|
}
|
|
this.sglBanks = [
|
|
{
|
|
sglCode: `SGL-${String(this.sglBanks.length + 1).padStart(2, '0')}`,
|
|
branch: '',
|
|
...this.newSglBank
|
|
} as SglBankMaster,
|
|
...this.sglBanks
|
|
];
|
|
this.newSglBank = {};
|
|
this.toast('SGL Bank master added.');
|
|
}
|
|
|
|
private loadDailyRates(): void {
|
|
this.investmentService.getDailyRates().subscribe({
|
|
next: (rows) => (this.dailyScriptRates = rows.map(toDailyScriptRate)),
|
|
error: (err) => this.toast(`Could not load daily script rates: ${apiErrorMessage(err)}`)
|
|
});
|
|
}
|
|
|
|
addDailyRate(): void {
|
|
const r = this.newDailyRate;
|
|
if (!r.scriptCode || !r.rateDate || r.closeRate == null) {
|
|
this.toast('Enter Rate Date, Script Code and Close Rate.');
|
|
return;
|
|
}
|
|
const script = this.scripts.find((s) => s.scriptCode === r.scriptCode);
|
|
if (!script?.id) {
|
|
this.toast(`Script ${r.scriptCode} is not saved in Script Master.`);
|
|
return;
|
|
}
|
|
const body = {
|
|
scriptId: script.id,
|
|
rateDate: r.rateDate,
|
|
closeRate: r.closeRate,
|
|
...(r.openRate != null ? { openRate: r.openRate } : {}),
|
|
...(r.highRate != null ? { highRate: r.highRate } : {}),
|
|
...(r.lowRate != null ? { lowRate: r.lowRate } : {}),
|
|
dateFormat: 'dd-MM-yyyy',
|
|
locale: 'en'
|
|
};
|
|
this.investmentService.createDailyRate(body).subscribe({
|
|
next: () => {
|
|
this.toast(`Rate for ${r.scriptCode} on ${r.rateDate} saved.`);
|
|
this.newDailyRate = {};
|
|
this.loadDailyRates();
|
|
},
|
|
error: (err) => this.toast(apiErrorMessage(err))
|
|
});
|
|
}
|
|
|
|
// -------------------- 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<void> {
|
|
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<string[][]> {
|
|
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) {
|
|
return;
|
|
}
|
|
this.investmentService.deleteDailyRate(r.id).subscribe({
|
|
next: () => {
|
|
this.toast(`Rate for ${r.scriptCode} on ${r.rateDate} dropped.`);
|
|
this.loadDailyRates();
|
|
},
|
|
error: (err) => this.toast(apiErrorMessage(err))
|
|
});
|
|
}
|
|
|
|
// -------------------- Export to Excel --------------------
|
|
private exportSheets(): ExportSheetSpec[] {
|
|
const title = this.masterLabels[this.activeTab];
|
|
const one = (headers: string[], widths: number[], rows: (string | number | null)[][]): ExportSheetSpec[] => [
|
|
{ sheetName: title, title, headers, widths, rows }];
|
|
|
|
switch (this.activeTab) {
|
|
case 'party':
|
|
return one(
|
|
[
|
|
'Party Code',
|
|
'Bond Type',
|
|
'Party Name',
|
|
'Address',
|
|
'City',
|
|
'Pincode',
|
|
'Phone',
|
|
'Fax',
|
|
'Bank Type',
|
|
'GL A/c No',
|
|
'RBI Code',
|
|
'Limit',
|
|
'LEI No',
|
|
'Counterparty Bank',
|
|
'Branch',
|
|
'Account No',
|
|
'IFSC',
|
|
'Status'
|
|
],
|
|
[
|
|
12,
|
|
14,
|
|
30,
|
|
36,
|
|
14,
|
|
10,
|
|
14,
|
|
14,
|
|
24,
|
|
12,
|
|
12,
|
|
14,
|
|
22,
|
|
24,
|
|
20,
|
|
18,
|
|
14,
|
|
10
|
|
],
|
|
this.parties.map((p) => [
|
|
p.partyCode,
|
|
p.bondType ?? '',
|
|
p.partyName,
|
|
[
|
|
p.address1,
|
|
p.address2,
|
|
p.address3
|
|
]
|
|
.filter((x) => !!x)
|
|
.join(', '),
|
|
p.city ?? '',
|
|
p.pincode ?? '',
|
|
p.phone ?? '',
|
|
p.fax ?? '',
|
|
p.bankType ?? '',
|
|
p.glCode ?? '',
|
|
p.rbiCode ?? '',
|
|
p.limit ?? null,
|
|
p.leiNo ?? '',
|
|
p.cpBankName ?? '',
|
|
p.cpBranchName ?? '',
|
|
p.cpAccountNo ?? '',
|
|
p.cpIfsc ?? '',
|
|
p.status ?? 'ACTIVE'
|
|
])
|
|
);
|
|
case 'script':
|
|
return one(
|
|
[
|
|
'Script Code',
|
|
'Bond Type',
|
|
'SLR',
|
|
'Script Desc',
|
|
'GL A/c No',
|
|
'ISIN',
|
|
'Int Rate %',
|
|
'Issue Date',
|
|
'Maturity Date',
|
|
'Period (Y/M/D)',
|
|
'Face Value/Unit',
|
|
'Company Name',
|
|
'Corp Code'
|
|
],
|
|
[
|
|
14,
|
|
16,
|
|
12,
|
|
30,
|
|
12,
|
|
16,
|
|
10,
|
|
14,
|
|
14,
|
|
14,
|
|
14,
|
|
24,
|
|
12
|
|
],
|
|
this.scripts.map((s) => {
|
|
const p = periodBetween(s.issueDate, s.maturityDate);
|
|
return [
|
|
s.scriptCode,
|
|
s.instrumentType,
|
|
s.slrType ?? '',
|
|
s.scriptName,
|
|
s.glCode ?? '',
|
|
s.isin,
|
|
s.couponRate ?? null,
|
|
s.issueDate ?? '',
|
|
s.maturityDate ?? '',
|
|
s.issueDate && s.maturityDate ? `${p.years}/${p.months}/${p.days}` : '',
|
|
s.faceValuePerUnit,
|
|
s.companyName ?? '',
|
|
s.corpCode ?? ''
|
|
];
|
|
})
|
|
);
|
|
case 'subtype':
|
|
return one(
|
|
[
|
|
'Code',
|
|
'Sub Type',
|
|
'Instrument'
|
|
],
|
|
[
|
|
10,
|
|
24,
|
|
16
|
|
],
|
|
this.subTypes.map((s) => [
|
|
s.subTypeCode,
|
|
s.subTypeName,
|
|
s.instrumentType
|
|
])
|
|
);
|
|
case 'broker':
|
|
return one(
|
|
[
|
|
'Broker Code',
|
|
'Broker Name',
|
|
'Contact',
|
|
'Empanelled On',
|
|
'Status'
|
|
],
|
|
[
|
|
14,
|
|
30,
|
|
16,
|
|
16,
|
|
12
|
|
],
|
|
this.brokers.map((b) => [
|
|
b.brokerCode,
|
|
b.brokerName,
|
|
b.contactNo,
|
|
b.empanelledOn,
|
|
b.status
|
|
])
|
|
);
|
|
case 'control':
|
|
return one(
|
|
[
|
|
'Parameter',
|
|
'Value',
|
|
'Description'
|
|
],
|
|
[
|
|
26,
|
|
14,
|
|
40
|
|
],
|
|
this.controlParams.map((c) => [
|
|
c.paramName,
|
|
c.paramValue,
|
|
c.description
|
|
])
|
|
);
|
|
case 'dailyscript':
|
|
return one(
|
|
[
|
|
'Rate Date',
|
|
'Script Code',
|
|
'Script Name',
|
|
'Open',
|
|
'High',
|
|
'Low',
|
|
'Close'
|
|
],
|
|
[
|
|
14,
|
|
16,
|
|
26,
|
|
12,
|
|
12,
|
|
12,
|
|
12
|
|
],
|
|
this.dailyScriptRates.map((r) => [
|
|
r.rateDate,
|
|
r.scriptCode,
|
|
r.scriptName,
|
|
r.openRate,
|
|
r.highRate,
|
|
r.lowRate,
|
|
r.closeRate
|
|
])
|
|
);
|
|
case 'lien':
|
|
return one(
|
|
[
|
|
'Reason Code',
|
|
'Description'
|
|
],
|
|
[
|
|
14,
|
|
40
|
|
],
|
|
this.lienReasons.map((l) => [
|
|
l.reasonCode,
|
|
l.reasonDescription
|
|
])
|
|
);
|
|
case 'mfproduct':
|
|
return one(
|
|
[
|
|
'Product Code',
|
|
'Product Name',
|
|
'AMC',
|
|
'Category',
|
|
'Risk'
|
|
],
|
|
[
|
|
14,
|
|
30,
|
|
24,
|
|
20,
|
|
12
|
|
],
|
|
this.mfProducts.map((m) => [
|
|
m.productCode,
|
|
m.productName,
|
|
m.amcName,
|
|
m.category,
|
|
m.riskLevel
|
|
])
|
|
);
|
|
case 'sglbank':
|
|
return one(
|
|
[
|
|
'SGL Code',
|
|
'Bank Name',
|
|
'Branch'
|
|
],
|
|
[
|
|
14,
|
|
30,
|
|
24
|
|
],
|
|
this.sglBanks.map((s) => [
|
|
s.sglCode,
|
|
s.bankName,
|
|
s.branch
|
|
])
|
|
);
|
|
case 'marketdetail':
|
|
return one(
|
|
[
|
|
'Script Code',
|
|
'Market Price',
|
|
'As On',
|
|
'YTM %'
|
|
],
|
|
[
|
|
16,
|
|
16,
|
|
14,
|
|
12
|
|
],
|
|
this.marketDetails.map((m) => [
|
|
m.scriptCode,
|
|
m.marketPrice,
|
|
m.asOnDate,
|
|
m.ytm
|
|
])
|
|
);
|
|
default:
|
|
return one([], [], []);
|
|
}
|
|
}
|
|
|
|
async exportExcel(): Promise<void> {
|
|
this.generating = true;
|
|
try {
|
|
await exportWorkbook(this.exportSheets(), `Investment-${this.masterLabels[this.activeTab]}`);
|
|
this.toast(`${this.masterLabels[this.activeTab]} exported.`);
|
|
} catch (err) {
|
|
console.error('Investment Masters export failed', err);
|
|
this.toast('Could not generate the workbook — please retry.');
|
|
} finally {
|
|
this.generating = false;
|
|
}
|
|
}
|
|
|
|
private toast(msg: string): void {
|
|
this.toastMsg = msg;
|
|
this.toastVisible = true;
|
|
if (this.toastTimer) {
|
|
clearTimeout(this.toastTimer);
|
|
}
|
|
this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200);
|
|
}
|
|
}
|