216 lines
6.6 KiB
TypeScript
216 lines
6.6 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 { 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<string, string> = {
|
||
PENDING: 'wri-status--pending',
|
||
APPROVED: 'wri-status--approved',
|
||
REJECTED: 'wri-status--rejected'
|
||
};
|
||
return map[status] ?? '';
|
||
}
|
||
|
||
typeColor(entry: any): string {
|
||
const map: Record<string, string> = {
|
||
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')}`;
|
||
}
|
||
}
|