/** * 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(); 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 = { 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 = { 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 = { 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); } } }