From 1cebb84906740cfbe6adabbe81d604a97db053e8 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Thu, 8 Oct 2026 22:38:56 +0530 Subject: [PATCH] feat(fixed-deposits): FD Maturity Reports component (missed from prior commit) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The component, service and template for the FD Maturity Reports screen itself — the previous commit wired up its route, nav entry and the day-close/product-summary integration, but the new directory was never staged alongside them. Co-Authored-By: Claude Sonnet 5 --- .../fd-maturity-report.component.html | 406 ++++++++++++++++++ .../fd-maturity-report.component.ts | 367 ++++++++++++++++ .../fd-maturity-report.service.ts | 163 +++++++ 3 files changed, 936 insertions(+) create mode 100644 src/app/fd-maturity-report/fd-maturity-report.component.html create mode 100644 src/app/fd-maturity-report/fd-maturity-report.component.ts create mode 100644 src/app/fd-maturity-report/fd-maturity-report.service.ts diff --git a/src/app/fd-maturity-report/fd-maturity-report.component.html b/src/app/fd-maturity-report/fd-maturity-report.component.html new file mode 100644 index 000000000..184098972 --- /dev/null +++ b/src/app/fd-maturity-report/fd-maturity-report.component.html @@ -0,0 +1,406 @@ + + +
+
+
+

FD Maturity Reports

+
+ Every branch gets its own report of the fixed deposits maturing with no closure instruction of their own, + generated automatically at 12:00 noon. One user puts the figures up and a second approves them — whoever + generated a report cannot approve it. Approving sets in motion the whole event: status, final interest, and any + renewal, all together, when the branch closes its day. A deposit that already named Transfer to Savings or a + Reinvest option settles itself the moment it matures, exactly as it always has — see "Matured automatically" for + those. +
+
+
+ + + +
+
+ + @if (!approvalEnabled) { +
+ +
+ Fixed deposit maturities are currently settled directly by the nightly job, so there is nothing to approve here. + Turn on fd-maturity-interest-posting-approval under System → Configurations to put + maturities behind this report. +
+
+ } + + @if (isHeadOffice) { +
+ + +
+ @if (tab === 'others') { +
+ +
+ Each branch generates and approves its own report. The head office can act here only for a branch that has not + opened the day, and only once every other open branch has closed. Anything it approves is marked (HO). +
+
+ } + } + + @if (errorMessage) { +
+ +
{{ errorMessage }}
+
+ } + @if (notice) { +
+ +
{{ notice }}
+
+ } + @if (pendingCount > 0) { +
+ +
+ {{ pendingCount }} report(s) waiting for approval. The day cannot be closed until they are + approved, and the user who generated a report cannot be the one to approve it. +
+
+ } + + @if (due.length > 0) { +
+ +
+
Fixed deposits are maturing but no report has been generated yet for:
+ @for (d of due; track d.officeId) { +
+ {{ d.officeName }} + + · {{ d.postingDate }} · {{ d.accountCount }} deposit(s) · est. ₹{{ + d.totalInterest | number: '1.2-2' + }} + + @if (d.canAct) { + + } @else if (d.blockedReason) { + {{ d.blockedReason }} + } +
+ } +
+
+ } + + +
+
+
Maturing, no report
+
₹{{ dueTotal | number: '1.2-2' }}
+
{{ dueAccountTotal }} deposit(s)
+
+
+
Awaiting approval
+
₹{{ awaitingTotal | number: '1.2-2' }}
+
{{ awaitingReports.length }} report(s) · blocking day close
+
+
+
Approved — runs at close
+
₹{{ approvedTotal | number: '1.2-2' }}
+
{{ approvedReports.length }} report(s)
+
+
+
Matured today
+
₹{{ postedTotal | number: '1.2-2' }}
+
{{ postedReports.length }} report(s)
+
+
+ + @if (showAutoMatured) { +
+
+
+

Fixed deposits matured automatically

+
+ A deposit that named Transfer to Savings or a Reinvest option on its own closure needs no approval here — it + settles the moment it matures, as it always has. Listed so nothing that moved is invisible, approved or not. +
+
+
+ Deposits + {{ autoMaturedRows.length }} +
+
+
+ + + + +
+ @if (autoMaturedLoading) { +
Looking for deposits that matured outside this report…
+ } @else { +
+ + + + + + + + + + + @for (g of autoMaturedGroups; track g.productName) { + + + + + + + } @empty { + + + + } + +
ProductSchemeDepositsPrincipal
+ {{ g.productName }} + + {{ fdTypeShort(g.fdType) }} + {{ g.accounts }}₹{{ g.deposit | number: '1.2-2' }}
No fixed deposit matured automatically in this period.
+
+ + @if (autoMaturedRows.length > 0) { +
+ + + + + + + + + + + + + + + @for (r of autoMaturedRows; track r.fdAccountId) { + + + + + + + + + + + } + +
Account noCustomerProductSchemeMaturity dateDepositOn closureMatured on
{{ r.accountNo }}{{ r.clientName }}{{ r.productName }} + {{ fdTypeShort(r.fdType) }} + {{ r.maturityDate }}₹{{ r.depositAmount | number: '1.2-2' }}{{ closureLabel(r.closureInstruction) }}{{ r.maturedOn }}
+
+ } + } +
+ } + +
+
+

Reports{{ postingDate ? ' for ' + postingDate : '' }}

+ One report per branch +
+
+ + + + + + + + + + + + + + + @for (r of reports; track r.id) { + + + + + + + + + + + } @empty { + + + + } + +
BranchPosting dateDepositsEst. interestStatusMakerChecker
{{ r.officeName }}{{ r.postingDate }}{{ r.accountCount }}₹{{ r.totalInterest | number: '1.2-2' }} + {{ r.status }} + @if (r.failureReason) { +
Some deposits could not mature — see detail
+ } +
+ {{ r.generatedBy || 'System' }} · {{ r.generationMode }}
{{ + r.generatedAt | date: 'dd MMM, HH:mm' + }} +
+ @if (r.approvedAt) { + {{ r.approvedBy }} + @if (r.approvedOnBehalf) { + (HO) + } +
{{ r.approvedAt | date: 'dd MMM, HH:mm' }} + } @else { + — + } +
+
+ @if (canRegenerate(r)) { + + } + @if (canApprove(r)) { + + } @else if (r.status === 'GENERATED' && r.approveBlockedReason) { + {{ r.approveBlockedReason }} + } +
+
+ @if (loading) { + Loading… + } @else { + No fixed deposit maturity report for {{ postingDate || 'this date' }}. Reports are generated at 12:00 + noon on the posting date. + } +
+
+
+ + @if (selected) { +
+
+
+

+ {{ selected.officeName }} — {{ selected.postingDate }} + {{ selected.status }} +

+
+ Approving runs the real maturity settlement for every deposit below — status, final interest and closure, + together. +
+
+
+ Total estimated interest + ₹{{ selected.totalInterest | number: '1.2-2' }} + {{ selected.accountCount }} deposit(s) +
+
+
+ + + + + + + + + + + + + + + + @for (l of selected.lines; track l.fdAccountId) { + + + + + + + + + + + + } + + + + + + + + +
Account noAccount holderSchemeMaturity dateDepositRate %Est. interestOn closureMatured
{{ l.accountNo }}{{ l.clientName }} + {{ fdTypeShort(l.fdType) }} + {{ l.maturityDate }}₹{{ l.depositAmount | number: '1.2-2' }}{{ l.interestRate | number: '1.2-2' }}₹{{ l.estimatedInterest | number: '1.2-2' }} + {{ closureLabel(l.closureInstruction) }} + @if (l.closureAccountNo) { +
{{ l.closureAccountNo }} + } +
+ @if (l.postedAt) { + {{ l.postedAt | date: 'dd MMM, HH:mm' }} + } @else if (l.failureReason) { + Failed + } @else { + — + } +
Total + ₹{{ selected.totalInterest | number: '1.2-2' }} +
+
+
+ } +
diff --git a/src/app/fd-maturity-report/fd-maturity-report.component.ts b/src/app/fd-maturity-report/fd-maturity-report.component.ts new file mode 100644 index 000000000..62f9f1ac0 --- /dev/null +++ b/src/app/fd-maturity-report/fd-maturity-report.component.ts @@ -0,0 +1,367 @@ +/** + * 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 { FormsModule } from '@angular/forms'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { SettingsService } from 'app/settings/settings.service'; +import { + FdAutoMaturedRow, + FdMaturityDue, + FdMaturityReport, + FdMaturityReportDetail, + FdMaturityReportService, + FdReportScope, + FdType +} from './fd-maturity-report.service'; + +/** + * Fixed deposit maturity reports. + * + * Until a report here is approved, a deposit whose term has ended simply waits — ACTIVE, not MATURED. + * Approving it is authorising the whole event: its final interest, the move to MATURED, and any + * renewal or transfer-to-savings instruction, all of which run together, unchanged, the moment the + * branch closes the day. The maker cannot be the checker, for the same reason as the interest report: + * this moves a customer's principal, not only its interest. + */ +@Component({ + selector: 'mifosx-fd-maturity-report', + templateUrl: './fd-maturity-report.component.html', + styleUrls: ['../fd-interest-posting-report/fd-interest-posting-report.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FormsModule, + FaIconComponent + ] +}) +export class FdMaturityReportComponent implements OnInit { + private reportService = inject(FdMaturityReportService); + private settingsService = inject(SettingsService); + + postingDate = ''; + loading = false; + busy = false; + errorMessage = ''; + notice = ''; + + reports: FdMaturityReport[] = []; + selected: FdMaturityReportDetail | null = null; + due: FdMaturityDue[] = []; + + tab: FdReportScope = 'own'; + isHeadOffice = false; + myBranch = ''; + approvalEnabled = true; + + ngOnInit(): void { + this.postingDate = this.businessDateIso(); + this.reportService.context().subscribe({ + next: (ctx) => { + this.isHeadOffice = !!ctx?.headOffice; + this.myBranch = ctx?.officeName ?? ''; + this.approvalEnabled = ctx?.approvalEnabled !== false; + }, + error: () => (this.isHeadOffice = false) + }); + this.load(); + } + + switchTab(tab: FdReportScope): void { + if (this.tab === tab) { + return; + } + this.tab = tab; + this.selected = null; + this.notice = ''; + this.load(); + } + + private confirmOnBehalf(officeName: string, action: string): boolean { + if (this.tab !== 'others') { + return true; + } + return confirm( + `${officeName} has not opened today. You are about to ${action} its fixed deposit maturity report on its ` + + 'behalf, from the head office. Continue?' + ); + } + + load(): void { + this.loading = true; + this.errorMessage = ''; + this.reportService.list(this.postingDate || undefined, undefined, undefined, this.tab).subscribe({ + next: (reports) => { + this.reports = reports ?? []; + this.loading = false; + const stillThere = this.selected && this.reports.find((r) => r.id === this.selected?.id); + if (stillThere) { + this.open(stillThere); + } else { + this.selected = null; + } + }, + error: (err) => { + this.loading = false; + this.errorMessage = this.messageOf(err, 'Could not load the fixed deposit maturity reports.'); + } + }); + this.reportService.dueWithoutReport(this.postingDate || undefined, this.tab).subscribe({ + next: (due) => (this.due = due ?? []), + error: () => (this.due = []) + }); + } + + generate(item: FdMaturityDue): void { + if (!this.confirmOnBehalf(item.officeName, 'generate')) { + return; + } + this.busy = true; + this.notice = ''; + this.errorMessage = ''; + this.reportService.regenerate(item.officeId, item.postingDate).subscribe({ + next: (res) => { + this.busy = false; + this.notice = `Report generated for ${item.officeName}. Another user of the branch has to approve it.`; + this.load(); + this.reportService.detail(res.reportId).subscribe((detail) => (this.selected = detail)); + }, + error: (err) => { + this.busy = false; + this.errorMessage = this.messageOf(err, 'Could not generate the report.'); + } + }); + } + + open(report: FdMaturityReport): void { + this.errorMessage = ''; + this.reportService.detail(report.id).subscribe({ + next: (detail) => (this.selected = detail), + error: (err) => (this.errorMessage = this.messageOf(err, 'Could not load the report.')) + }); + } + + regenerate(report: FdMaturityReport): void { + if ( + !confirm( + `Regenerate the fixed deposit maturity report for ${report.officeName}? It will need approving again.` + ) || + !this.confirmOnBehalf(report.officeName, 'regenerate') + ) { + return; + } + this.busy = true; + this.notice = ''; + this.errorMessage = ''; + this.reportService.regenerate(report.officeId, report.postingDate).subscribe({ + next: (res) => { + this.busy = false; + this.notice = `Report regenerated for ${report.officeName}. Another user of the branch has to approve it.`; + this.selected = null; + this.load(); + this.reportService.detail(res.reportId).subscribe((detail) => (this.selected = detail)); + }, + error: (err) => { + this.busy = false; + this.errorMessage = this.messageOf(err, 'Could not regenerate the report.'); + } + }); + } + + approve(report: FdMaturityReport): void { + if ( + !confirm( + `Approve ${report.accountCount} deposit(s) maturing in ${report.officeName}, for a final interest of ` + + `₹${report.totalInterest}? Status, interest and any renewal or transfer instruction all run when the ` + + 'branch closes the day.' + ) || + !this.confirmOnBehalf(report.officeName, 'approve') + ) { + return; + } + this.busy = true; + this.notice = ''; + this.errorMessage = ''; + this.reportService.approve(report.id).subscribe({ + next: () => { + this.busy = false; + this.notice = `Approved. ${report.officeName}'s maturities will run when the branch closes the day.`; + this.load(); + }, + error: (err) => { + this.busy = false; + this.errorMessage = this.messageOf(err, 'Could not approve the report.'); + } + }); + } + + canRegenerate(report: FdMaturityReport): boolean { + return report.canAct && (report.status === 'GENERATED' || report.status === 'APPROVED'); + } + + canApprove(report: FdMaturityReport): boolean { + return !!report.canApprove; + } + + statusClass(status: string): string { + return ( + { + GENERATED: 'fd-badge--pending', + APPROVED: 'fd-badge--approved', + POSTED: 'fd-badge--posted', + SUPERSEDED: 'fd-badge--muted' + }[status] ?? 'fd-badge--muted' + ); + } + + fdTypeShort(fdType: FdType | string): string { + return ( + { + SINGLE: 'Single', + DOUBLE: 'Double', + SPECIAL: 'Special' + }[fdType] ?? String(fdType ?? '') + ); + } + + fdTypeClass(fdType: FdType | string): string { + return ( + { + SINGLE: 'fd-chip--single', + DOUBLE: 'fd-chip--double', + SPECIAL: 'fd-chip--special' + }[fdType] ?? 'fd-chip--single' + ); + } + + /** What approving this line sets in motion beyond the interest, spelt out rather than left as a code. */ + closureLabel(closure: string): string { + return ( + { + NONE: 'Sits matured — renew from the FD Dashboard', + WITHDRAW_DEPOSIT: 'Paid out on closure', + TRANSFER_TO_SAVINGS: 'Transfers to linked savings', + REINVEST_PRINCIPAL_AND_INTEREST: 'Reinvests principal + interest', + REINVEST_PRINCIPAL_ONLY: 'Reinvests principal only', + PARTIAL_REINVEST: 'Partially reinvests' + }[closure] ?? closure + ); + } + + get pendingCount(): number { + return this.tab === 'own' ? this.reports.filter((r) => r.status === 'GENERATED').length : 0; + } + + get dueTotal(): number { + return this.due.reduce((sum, d) => sum + Number(d.totalInterest || 0), 0); + } + + get dueAccountTotal(): number { + return this.due.reduce((sum, d) => sum + Number(d.accountCount || 0), 0); + } + + private reportsWithStatus(status: string): FdMaturityReport[] { + return this.reports.filter((r) => r.status === status); + } + + private totalOf(list: FdMaturityReport[]): number { + return list.reduce((sum, r) => sum + Number(r.totalInterest || 0), 0); + } + + get awaitingReports(): FdMaturityReport[] { + return this.reportsWithStatus('GENERATED'); + } + + get approvedReports(): FdMaturityReport[] { + return this.reportsWithStatus('APPROVED'); + } + + get postedReports(): FdMaturityReport[] { + return this.reportsWithStatus('POSTED'); + } + + get awaitingTotal(): number { + return this.totalOf(this.awaitingReports); + } + + get approvedTotal(): number { + return this.totalOf(this.approvedReports); + } + + get postedTotal(): number { + return this.totalOf(this.postedReports); + } + + // ── Matured automatically: a deposit with its own closure instruction, settled by the scheduler ── + + showAutoMatured = false; + autoMaturedLoading = false; + autoMaturedRows: FdAutoMaturedRow[] = []; + autoMaturedFrom = ''; + autoMaturedTo = ''; + + openAutoMatured(): void { + this.showAutoMatured = true; + if (!this.autoMaturedTo) { + this.autoMaturedTo = this.businessDateIso(); + this.autoMaturedFrom = `${this.autoMaturedTo.slice(0, 7)}-01`; + } + this.loadAutoMatured(); + } + + loadAutoMatured(): void { + this.autoMaturedLoading = true; + this.errorMessage = ''; + this.reportService + .autoMatured(this.autoMaturedFrom || undefined, this.autoMaturedTo || undefined, undefined, this.tab) + .subscribe({ + next: (rows) => { + this.autoMaturedRows = rows ?? []; + this.autoMaturedLoading = false; + }, + error: (err) => { + this.autoMaturedLoading = false; + this.autoMaturedRows = []; + this.errorMessage = this.messageOf(err, 'Could not load deposits that matured automatically.'); + } + }); + } + + closeAutoMatured(): void { + this.showAutoMatured = false; + } + + /** Totals by product, so a branch can see at a glance which schemes settled themselves and how. */ + get autoMaturedGroups(): { productName: string; fdType: string; accounts: number; deposit: number }[] { + const map = new Map(); + this.autoMaturedRows.forEach((r) => { + const g = map.get(r.productName) ?? { productName: r.productName, fdType: r.fdType, accounts: 0, deposit: 0 }; + g.accounts += 1; + g.deposit += Number(r.depositAmount || 0); + map.set(r.productName, g); + }); + return [...map.values()]; + } + + private businessDateIso(): string { + const businessDate = this.settingsService.businessDate as any; + const parsed = businessDate ? new Date(businessDate) : new Date(); + if (isNaN(parsed.getTime())) { + return new Date().toISOString().slice(0, 10); + } + const month = String(parsed.getMonth() + 1).padStart(2, '0'); + const day = String(parsed.getDate()).padStart(2, '0'); + return `${parsed.getFullYear()}-${month}-${day}`; + } + + private messageOf(err: any, fallback: string): string { + return ( + err?.error?.errors?.[0]?.defaultUserMessage ?? err?.error?.defaultUserMessage ?? err?.error?.message ?? fallback + ); + } +} diff --git a/src/app/fd-maturity-report/fd-maturity-report.service.ts b/src/app/fd-maturity-report/fd-maturity-report.service.ts new file mode 100644 index 000000000..1459acccf --- /dev/null +++ b/src/app/fd-maturity-report/fd-maturity-report.service.ts @@ -0,0 +1,163 @@ +/** + * 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 { Injectable, inject } from '@angular/core'; +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Observable } from 'rxjs'; + +export type FdMaturityReportStatus = 'GENERATED' | 'APPROVED' | 'POSTED' | 'SUPERSEDED'; +export type FdReportScope = 'own' | 'others'; +export type FdType = 'SINGLE' | 'DOUBLE' | 'SPECIAL'; + +export interface FdMaturityReport { + id: number; + officeId: number; + officeName: string; + postingDate: string; + status: FdMaturityReportStatus; + totalInterest: number; + accountCount: number; + generationMode: 'AUTO' | 'MANUAL'; + generatedAt: string; + generatedBy: string | null; + approvedAt: string | null; + approvedBy: string | null; + approvedOnBehalf: boolean; + postedAt: string | null; + failureReason: string | null; + canAct: boolean; + blockedReason: string | null; + canApprove: boolean; + approveBlockedReason: string | null; +} + +export interface FdMaturityReportLine { + fdAccountId: number; + accountNo: string; + clientName: string; + productName: string; + fdType: FdType; + maturityDate: string; + depositAmount: number | null; + interestRate: number | null; + /** What postMaturityInterest will still credit — zero where the last period was already posted. */ + estimatedInterest: number; + /** NONE, WITHDRAW_DEPOSIT, TRANSFER_TO_SAVINGS, REINVEST_PRINCIPAL_AND_INTEREST, REINVEST_PRINCIPAL_ONLY, PARTIAL_REINVEST. */ + closureInstruction: string; + closureAccountNo: string | null; + /** Set once this deposit's maturity has actually run. */ + postedAt: string | null; + /** Set where this one deposit's maturity failed — the rest of the report still posts. */ + failureReason: string | null; +} + +export interface FdMaturityReportDetail extends FdMaturityReport { + lines: FdMaturityReportLine[]; +} + +/** + * One fixed deposit that matured without this report's approval — a deposit carrying its own closure + * instruction, settled directly by the scheduler exactly as it always has been. View only. + */ +export interface FdAutoMaturedRow { + fdAccountId: number; + accountNo: string; + clientName: string; + officeId: number; + officeName: string; + productId: number; + productName: string; + fdType: FdType; + maturityDate: string; + depositAmount: number | null; + maturedOn: string; + closureInstruction: string; +} + +export interface FdMaturityDue { + officeId: number; + officeName: string; + postingDate: string; + accountCount: number; + totalInterest: number; + canAct: boolean; + blockedReason: string | null; +} + +/** + * Fixed deposit maturities, one report per branch per date: every deposit maturing that day, what its + * last posting would still come to, and how it closes, approved by a second user before the branch + * closes the day and the real maturity — status, interest and any renewal or transfer — actually runs. + */ +@Injectable({ providedIn: 'root' }) +export class FdMaturityReportService { + private http = inject(HttpClient); + private readonly base = '/fixeddepositmaturityreports'; + + list( + postingDate?: string, + status?: string, + officeId?: number, + scope?: FdReportScope + ): Observable { + let params = new HttpParams(); + if (scope) params = params.set('scope', scope); + if (postingDate) params = params.set('postingDate', postingDate); + if (status) params = params.set('status', status); + if (officeId) params = params.set('officeId', String(officeId)); + return this.http.get(this.base, { params }); + } + + dueWithoutReport(postingDate?: string, scope?: FdReportScope): Observable { + let params = new HttpParams(); + if (scope) params = params.set('scope', scope); + if (postingDate) params = params.set('postingDate', postingDate); + return this.http.get(`${this.base}/due-without-report`, { params }); + } + + /** Deposits that matured without this report — a closure instruction, settled by the scheduler directly. */ + autoMatured( + fromDate?: string, + toDate?: string, + officeId?: number, + scope?: FdReportScope + ): Observable { + let params = new HttpParams(); + if (scope) params = params.set('scope', scope); + if (fromDate) params = params.set('fromDate', fromDate); + if (toDate) params = params.set('toDate', toDate); + if (officeId) params = params.set('officeId', String(officeId)); + return this.http.get(`${this.base}/outside-approval`, { params }); + } + + context(): Observable<{ officeId: number; officeName: string; headOffice: boolean; approvalEnabled: boolean }> { + return this.http.get<{ officeId: number; officeName: string; headOffice: boolean; approvalEnabled: boolean }>( + `${this.base}/context` + ); + } + + detail(reportId: number): Observable { + return this.http.get(`${this.base}/${reportId}`); + } + + regenerate(officeId: number, postingDate: string): Observable<{ reportId: number; status: string }> { + return this.http.post<{ reportId: number; status: string }>( + this.base, + { officeId, postingDate }, + { params: new HttpParams().set('command', 'regenerate') } + ); + } + + approve(reportId: number): Observable<{ reportId: number; status: string }> { + return this.http.post<{ reportId: number; status: string }>( + `${this.base}/${reportId}`, + {}, + { params: new HttpParams().set('command', 'approve') } + ); + } +}