/** * 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 { MatIconModule } from '@angular/material/icon'; import { MatPaginatorModule, PageEvent } from '@angular/material/paginator'; import { MatDialog } from '@angular/material/dialog'; import { MatNativeDateModule } from '@angular/material/core'; import { MatDatepickerModule } from '@angular/material/datepicker'; import { catchError } from 'rxjs/operators'; import { of } from 'rxjs'; import { OrganizationService } from 'app/organization/organization.service'; import { SettingsService } from 'app/settings/settings.service'; import { AuthenticationService } from 'app/core/authentication/authentication.service'; import { DdsApprovalReviewComponent } from './dds-approval-review/dds-approval-review.component'; /** * Approval Center › DDS Approvals: every DDS entry waiting on a checker, in the clerk's terms - what * it does, on which accounts, for how much - and, once approved, the voucher it moved under. * * Laid out like Disbursement & Transfers beside it, and sharing its styles, so the two queues read the * same way. The entries are the platform's own maker-checker entries; approving one here carries it out. */ @Component({ selector: 'mifosx-dds-approvals', templateUrl: './dds-approvals.component.html', styleUrls: ['../../tasks/checker-inbox-and-tasks-tabs/disbursement-requests/disbursement-requests.component.scss'], standalone: true, imports: [ CommonModule, FormsModule, MatIconModule, MatPaginatorModule, MatNativeDateModule, MatDatepickerModule ] }) export class DdsApprovalsComponent implements OnInit { private organizationService = inject(OrganizationService); private settingsService = inject(SettingsService); private authenticationService = inject(AuthenticationService); private dialog = inject(MatDialog); entries: any[] = []; pageSize = 20; pageIndex = 0; readonly pageSizeOptions = [ 10, 20, 50 ]; /** The page of entries on screen. */ get pagedEntries(): any[] { const start = this.pageIndex * this.pageSize; return this.entries.slice(start, start + this.pageSize); } onPageChange(event: PageEvent): void { this.pageSize = event.pageSize; this.pageIndex = event.pageIndex; } filterStatus = 'PENDING'; fromDate: Date | null = null; toDate: Date | null = null; maxDate!: Date; activePreset = 'today'; loading = false; loaded = false; readonly currentUserId = this.authenticationService.getCredentials()?.userId; readonly statusOptions = [ { value: 'PENDING', label: 'Pending' }, { value: 'APPROVED', label: 'Approved' }, { value: 'REJECTED', label: 'Rejected' } ]; readonly datePresets = [ { key: 'today', label: 'Today' }, { key: 'week', label: 'This Week' }, { key: 'month', label: 'This Month' }, { key: 'all', label: 'All' } ]; ngOnInit(): void { const today = new Date(this.settingsService.businessDate || new Date()); today.setHours(0, 0, 0, 0); this.maxDate = today; this.applyPreset('today'); } load(): void { this.loading = true; this.organizationService .getDdsApprovals(this.filterStatus, this.isoDate(this.fromDate), this.isoDate(this.toDate)) .pipe(catchError(() => of([]))) .subscribe((rows: any) => { this.entries = Array.isArray(rows) ? rows : []; this.pageIndex = 0; this.loading = false; this.loaded = true; }); } onFilterChange(): void { this.load(); } onFromDateChange(): void { this.activePreset = ''; if (this.fromDate && this.toDate && this.fromDate > this.toDate) { this.toDate = new Date(this.fromDate); } this.load(); } onToDateChange(): void { this.activePreset = ''; this.load(); } applyPreset(key: string): void { this.activePreset = key; const today = new Date(this.settingsService.businessDate || new Date()); 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.load(); } review(entry: any): void { this.dialog .open(DdsApprovalReviewComponent, { data: { entry, currentUserId: this.currentUserId }, autoFocus: false }) .afterClosed() // However the dialog was closed - its Close button, a click outside, Esc - the list is read // again, so an entry just approved or rejected leaves the Pending list at once. .subscribe(() => this.load()); } /** Whose entry it is - a checker cannot pass their own. */ isMine(entry: any): boolean { return !!this.currentUserId && Number(entry.makerId) === Number(this.currentUserId); } statusClass(status: string): string { const map: Record = { PENDING: 'wri-status--pending', APPROVED: 'wri-status--approved', REJECTED: 'wri-status--rejected' }; return map[status] ?? ''; } typeColor(entry: any): string { const map: Record = { Transfer: '#e3f2fd', 'Apply Receipt': '#fce4ec' }; if (String(entry.type).startsWith('Premature')) { return '#fff3e0'; } if (String(entry.type).startsWith('Maturity')) { return '#e8f5e9'; } if (String(entry.type).startsWith('Post')) { return '#ede7f6'; } return map[entry.type] ?? '#f5f5f5'; } formatDateTime(val: any): string { if (!val) { return '—'; } const d = new Date(String(val).replace(' ', 'T')); return isNaN(d.getTime()) ? String(val) : d.toLocaleString('en-IN', { day: '2-digit', month: 'short', year: 'numeric', hour: '2-digit', minute: '2-digit' }); } private isoDate(d: Date | null): string | undefined { if (!d) { return undefined; } return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; } }