Files
cbsnexora/src/app/clerk/my-disbursement-requests/my-disbursement-requests.component.ts
T
nitesh f0c06988c0
build-run / Run Lint, Build and Deploy (push) Canceled after 0s
Publish Image in Docker Hub / build (push) Canceled after 0s
Playwright E2E Tests / Run Playwright Tests (push) Canceled after 0s
Tests / testRigor (push) Canceled after 0s
fix(cashier): My Vouchers filters, Fund Transfer GL support, and disbursement-list rename
my-work.component (My Vouchers):
- The five summary tiles (Received/Payments/Journal/Entered/Approved) were
  bound directly to the server's day-total fields, so they stayed fixed at
  the combined "entered + approved" figure no matter what the Side/My part
  dropdowns were set to. They're now pinned to this user's own entered rows
  only (enteredRows), independent of both filters, so "how much did I
  personally put through today" reads correctly regardless of what the table
  below happens to be filtered to. The table and its dropdowns are unchanged
  and still default to showing everything.

transaction-entry.component (Fund Transfer):
- onSave()'s plain bank-transfer branch (SD/CA/CB only) was catching every
  'Fund Transfer' row before the GL-aware Disbursement/Collection branch
  below it could see them, so a Fund Transfer against a GL account was always
  rejected with "Invalid Account Type" even though that branch already
  supports it under the same label. Fund Transfer rows that include a GL leg
  now skip the plain-transfer branch and fall through correctly.

my-disbursement-requests.component (Disbursements & Transfer list):
- getCreditEntries() only read the stored credit_entries JSON, which holds
  the full picture for Collection (its GL leg is one of the entries) but not
  for Disbursement/Fund Transfer, whose GL is the request's own top-level
  source (gl_account_no/dr_amount) and was never in that array. The DR/CR
  breakdown for those two types showed only the credit side, looking like
  half the entry was missing. The GL leg is now synthesised back in as the
  first row for those two types, matching what actually posted.
- Renamed the clerk's "My Disbursements" / "My Disbursement Requests" screen
  to "Disbursements & Transfer" throughout (nav tab, dropdown menu, route
  title/breadcrumb, cashier counter link, page header) — the register covers
  Disbursement, Collection, Fund Transfer and Bank Transfer requests, and the
  old name read as disbursement-only. "My Requests" (Withdrawal Slip
  requests, a separate register) is renamed to "Withdrawal Request" for the
  same reason: neither name said what was actually inside.
2026-09-10 12:40:32 +05:30

267 lines
7.8 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 { Router } from '@angular/router';
import { MatIconModule } from '@angular/material/icon';
import { MatPaginatorModule, PageEvent } from '@angular/material/paginator';
import { MatNativeDateModule } from '@angular/material/core';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatDatepickerModule } from '@angular/material/datepicker';
import { catchError } from 'rxjs/operators';
import { of } from 'rxjs';
import { ClerkDisbursementService } from '../../cashier/clerk-disbursement.service';
import { SettingsService } from '../../settings/settings.service';
@Component({
selector: 'mifosx-my-disbursement-requests',
templateUrl: './my-disbursement-requests.component.html',
styleUrls: ['./my-disbursement-requests.component.scss'],
standalone: true,
imports: [
CommonModule,
FormsModule,
MatIconModule,
MatPaginatorModule,
MatNativeDateModule,
MatFormFieldModule,
MatInputModule,
MatDatepickerModule
]
})
export class MyDisbursementRequestsComponent implements OnInit {
private service = inject(ClerkDisbursementService);
private router = inject(Router);
private settingsService = inject(SettingsService);
requests: any[] = [];
totalRecords = 0;
expandedIds = new Set<number>();
pageSize = 20;
pageIndex = 0;
pageSizeOptions = [
10,
20,
50
];
filterStatus = '';
fromDate: Date | null = null;
toDate: Date | null = null;
maxDate!: Date;
activePreset = 'today';
loading = false;
loaded = false;
readonly datePresets = [
{ key: 'today', label: 'Today' },
{ key: 'week', label: 'This Week' },
{ key: 'month', label: 'This Month' },
{ key: 'all', label: 'All' }
];
get statusOptions(): { value: string; label: string }[] {
return [
{ value: '', label: 'All' },
{ value: 'PENDING', label: 'Pending' },
{ value: 'APPROVED', label: 'Approved' },
{ value: 'REJECTED', label: 'Rejected' },
{ value: 'FAILED', label: 'Failed' }
];
}
ngOnInit(): void {
const today = new Date(this.settingsService.businessDate);
today.setHours(0, 0, 0, 0);
this.maxDate = today;
this.applyPreset('today');
}
load(): void {
this.loading = true;
this.service
.getMyRequests(
this.filterStatus || undefined,
this.pageIndex * this.pageSize,
this.pageSize,
this.isoDate(this.fromDate),
this.isoDate(this.toDate)
)
.pipe(catchError(() => of({ pageItems: [], totalFilteredRecords: 0 })))
.subscribe((res: any) => {
this.requests = res?.pageItems ?? [];
this.totalRecords = Number(res?.totalFilteredRecords ?? 0);
this.loading = false;
this.loaded = true;
});
}
onPageChange(event: PageEvent): void {
this.pageSize = event.pageSize;
this.pageIndex = event.pageIndex;
this.load();
}
onFilterChange(): void {
this.pageIndex = 0;
this.load();
}
onFromDateChange(): void {
this.activePreset = '';
if (this.fromDate && this.toDate && this.fromDate > this.toDate) {
this.toDate = new Date(this.fromDate);
}
this.pageIndex = 0;
this.load();
}
onToDateChange(): void {
this.activePreset = '';
this.pageIndex = 0;
this.load();
}
applyPreset(key: string): void {
this.activePreset = key;
const today = new Date(this.settingsService.businessDate);
today.setHours(0, 0, 0, 0);
if (key === 'today') {
this.fromDate = new Date(today);
this.toDate = new Date(today);
} else if (key === 'week') {
const start = new Date(today);
start.setDate(today.getDate() - today.getDay());
this.fromDate = start;
this.toDate = new Date(today);
} else if (key === 'month') {
this.fromDate = new Date(today.getFullYear(), today.getMonth(), 1);
this.toDate = new Date(today);
} else {
this.fromDate = null;
this.toDate = null;
}
this.pageIndex = 0;
this.load();
}
private isoDate(d: Date | null): string | undefined {
if (!d) return undefined;
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return `${y}-${m}-${day}`;
}
newDisbursement(): void {
this.router.navigate(['/clerk/dashboard']);
}
toggleExpand(id: number): void {
if (this.expandedIds.has(id)) {
this.expandedIds.delete(id);
} else {
this.expandedIds.add(id);
}
}
getCreditEntries(req: any): any[] {
try {
const arr = typeof req.credit_entries === 'string' ? JSON.parse(req.credit_entries) : req.credit_entries;
const entries: any[] = Array.isArray(arr)
? arr.map((e: any) => ({ ...e, amount: parseFloat(e.amount) || 0 }))
: [];
// Collection embeds its GL leg inside credit_entries (it can collect from several
// accounts into the one GL, so the GL has to be one row among them). Disbursement and
// Fund Transfer instead name their GL as the request's own top-level source — dr_amount
// is its amount — because there is exactly one, so it never needed a row of its own. That
// meant the DR/CR breakdown here only ever showed the credit side for those two types,
// looking like half the slip was missing. Adding it back as the first row here, rather
// than in credit_entries itself, keeps the stored JSON exactly what approve() posts from.
if ((req.request_type === 'DISBURSEMENT' || req.request_type === 'FUND_TRANSFER') && req.gl_account_no) {
return [
{
dbCr: 'Dr',
accountNo: req.gl_account_no,
accountName: req.gl_account_name,
accountType: 'GL',
amount: parseFloat(req.dr_amount) || 0
},
...entries
];
}
return entries;
} catch {
return [];
}
}
creditCount(req: any): number {
return this.getCreditEntries(req).length;
}
/**
* Human-readable account type for the badge. The stored value is the backend's own code
* (SAVINGS / CURRENT / FIXED_DEPOSIT / RECURRING_DEPOSIT / SHARE / LOAN / GL); presentation only.
*/
accountTypeLabel(accountType: string): string {
const labels: Record<string, string> = {
SAVINGS: 'Savings',
CURRENT: 'Current',
FIXED_DEPOSIT: 'Fixed Deposit',
RECURRING_DEPOSIT: 'Recurring Deposit',
SHARE: 'Share',
LOAN: 'Loan',
GL: 'GL'
};
return labels[(accountType ?? '').toUpperCase()] ?? accountType;
}
statusClass(status: string): string {
const map: Record<string, string> = {
PENDING: 'mdr-status--pending',
PROCESSING: 'mdr-status--processing',
APPROVED: 'mdr-status--approved',
REJECTED: 'mdr-status--rejected',
FAILED: 'mdr-status--failed'
};
return map[status] ?? '';
}
statusLabel(status: string): string {
const map: Record<string, string> = {
PENDING: 'Pending Approval',
PROCESSING: 'Processing…',
APPROVED: 'Approved',
REJECTED: 'Rejected',
FAILED: 'Processing Failed'
};
return map[status] ?? status;
}
formatDateTime(val: any): string {
if (!val) return '—';
try {
const d = new Date(val);
return d.toLocaleString('en-IN', {
day: '2-digit',
month: 'short',
year: 'numeric',
hour: '2-digit',
minute: '2-digit'
});
} catch {
return String(val);
}
}
}