feat(clerk): add GL disbursement maker-checker flow
- Add ClerkDisbursementService for submit/approve/reject HTTP calls - Add DisbursementRequestsComponent (manager inbox tab under checker tasks) - Add DisbursementReviewComponent (manager approve/reject detail page) - Add MyDisbursementRequestsComponent (clerk's own disbursement history) - Add 'My Disbursements' nav tab to clerk shell navigation header - Wire routes in cashier-routing and tasks-routing modules - Add 'My Disbursements' button to transaction-entry clerk footer - Include accountName in creditEntries payload for manager review display Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
89d1167145
commit
efb01d5441
@@ -0,0 +1,210 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
|
||||
<div class="dri">
|
||||
<!-- ── NAV BAR ── -->
|
||||
<nav class="cashier-nav cashier-nav--clerk">
|
||||
<button type="button" class="cashier-nav__item cashier-nav__item--primary cashier-nav__item--active">
|
||||
<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<path d="M12 2v20M2 12h20" />
|
||||
</svg>
|
||||
Disbursement Review
|
||||
</button>
|
||||
<span class="cashier-nav__esc-hint" (click)="backToInbox()">← Back to Inbox</span>
|
||||
</nav>
|
||||
|
||||
<!-- ── LOADING ── -->
|
||||
@if (loading) {
|
||||
<div class="dri-loading">
|
||||
<mat-icon class="dri-loading__icon">hourglass_empty</mat-icon>
|
||||
<span>Loading request…</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ── NOT FOUND ── -->
|
||||
@if (!loading && !request) {
|
||||
<div class="dri-loading">
|
||||
<mat-icon class="dri-loading__icon">error_outline</mat-icon>
|
||||
<span>Request not found.</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ── MAIN CONTENT ── -->
|
||||
@if (!loading && request) {
|
||||
<div class="dri-body">
|
||||
<!-- Header card -->
|
||||
<div class="dri-card dri-card--header">
|
||||
<div class="dri-card-header">
|
||||
<div class="dri-card-header__left">
|
||||
<span class="dri-voucher">{{ request.voucher_no || '—' }}</span>
|
||||
<span class="dri-status" [ngClass]="statusClass(request.status)">{{ request.status }}</span>
|
||||
</div>
|
||||
<div class="dri-card-header__meta">
|
||||
<span class="dri-meta-item">
|
||||
<mat-icon>person</mat-icon>
|
||||
{{ request.clerk_name }} ({{ request.clerk_username }})
|
||||
</span>
|
||||
<span class="dri-meta-item">
|
||||
<mat-icon>schedule</mat-icon>
|
||||
{{ formatDateTime(request.submitted_at) }}
|
||||
</span>
|
||||
@if (request.actioned_at) {
|
||||
<span class="dri-meta-item">
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
Actioned: {{ formatDateTime(request.actioned_at) }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GL Debit row -->
|
||||
<div class="dri-card">
|
||||
<div class="dri-section-title">
|
||||
<mat-icon>account_balance</mat-icon>
|
||||
GL Debit (Source)
|
||||
</div>
|
||||
<div class="dri-detail-grid">
|
||||
<div class="dri-detail-item">
|
||||
<span class="dri-detail-label">GL Account No</span>
|
||||
<span class="dri-detail-value cell-mono">{{ request.gl_account_no }}</span>
|
||||
</div>
|
||||
<div class="dri-detail-item">
|
||||
<span class="dri-detail-label">GL Account Name</span>
|
||||
<span class="dri-detail-value">{{ request.gl_account_name }}</span>
|
||||
</div>
|
||||
<div class="dri-detail-item">
|
||||
<span class="dri-detail-label">DR Amount</span>
|
||||
<span class="dri-detail-value dri-amount">₹{{ request.dr_amount | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="dri-detail-item">
|
||||
<span class="dri-detail-label">Payment Mode</span>
|
||||
<span class="dri-detail-value">{{ request.payment_type_name || 'Transfer' }}</span>
|
||||
</div>
|
||||
@if (request.description) {
|
||||
<div class="dri-detail-item dri-detail-item--full">
|
||||
<span class="dri-detail-label">Description</span>
|
||||
<span class="dri-detail-value">{{ request.description }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Credit entries -->
|
||||
<div class="dri-card">
|
||||
<div class="dri-section-title">
|
||||
<mat-icon>account_balance_wallet</mat-icon>
|
||||
Credit Entries ({{ creditEntries.length }} account{{ creditEntries.length !== 1 ? 's' : '' }})
|
||||
</div>
|
||||
<table class="dri-cr-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>#</th>
|
||||
<th>Account No</th>
|
||||
<th>Account Name</th>
|
||||
<th>Type</th>
|
||||
<th class="num">Amount (CR)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (entry of creditEntries; track entry.accountId; let i = $index) {
|
||||
<tr>
|
||||
<td class="dri-cr-idx">{{ i + 1 }}</td>
|
||||
<td class="cell-mono">{{ entry.accountNo }}</td>
|
||||
<td>{{ entry.accountName }}</td>
|
||||
<td>
|
||||
<span class="dri-type-badge">{{ entry.accountType }}</span>
|
||||
</td>
|
||||
<td class="num dri-cr-amount">₹{{ entry.amount | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
<tr class="dri-cr-total-row">
|
||||
<td colspan="4" class="dri-cr-total-label">Total Credit</td>
|
||||
<td class="num dri-cr-total-amount">₹{{ totalCreditAmount() | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Rejection reason (if rejected) -->
|
||||
@if (request.status === 'REJECTED' && request.rejection_reason) {
|
||||
<div class="dri-card dri-card--rejected">
|
||||
<div class="dri-section-title">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
Rejection Reason
|
||||
</div>
|
||||
<p class="dri-rejection-text">{{ request.rejection_reason }}</p>
|
||||
<div class="dri-meta-item" style="margin-top: 8px">
|
||||
<mat-icon>manage_accounts</mat-icon>
|
||||
{{ request.manager_name || request.manager_username || '—' }}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Txn refs (if approved) -->
|
||||
@if (request.status === 'APPROVED' && request.txn_refs) {
|
||||
<div class="dri-card dri-card--approved">
|
||||
<div class="dri-section-title">
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
Transaction References
|
||||
</div>
|
||||
<p class="dri-txn-refs cell-mono">{{ request.txn_refs }}</p>
|
||||
<div class="dri-meta-item" style="margin-top: 8px">
|
||||
<mat-icon>manage_accounts</mat-icon>
|
||||
Approved by: {{ request.manager_name || request.manager_username || '—' }}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Action buttons (only for PENDING) -->
|
||||
@if (request.status === 'PENDING') {
|
||||
<div class="dri-actions">
|
||||
@if (!showRejectForm) {
|
||||
<button type="button" class="dri-btn dri-btn--approve" [disabled]="acting" (click)="approve()">
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
{{ acting ? 'Processing…' : 'Approve Disbursement' }}
|
||||
</button>
|
||||
<button type="button" class="dri-btn dri-btn--reject" [disabled]="acting" (click)="showRejectForm = true">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
Reject
|
||||
</button>
|
||||
} @else {
|
||||
<div class="dri-reject-form">
|
||||
<label class="dri-reject-label">Rejection Reason</label>
|
||||
<textarea
|
||||
class="dri-reject-textarea"
|
||||
[(ngModel)]="rejectionReason"
|
||||
placeholder="State the reason for rejection…"
|
||||
rows="3"
|
||||
></textarea>
|
||||
<div class="dri-reject-actions">
|
||||
<button
|
||||
type="button"
|
||||
class="dri-btn dri-btn--reject"
|
||||
[disabled]="acting || !rejectionReason.trim()"
|
||||
(click)="reject()"
|
||||
>
|
||||
<mat-icon>cancel</mat-icon>
|
||||
{{ acting ? 'Processing…' : 'Confirm Rejection' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="dri-btn dri-btn--ghost"
|
||||
[disabled]="acting"
|
||||
(click)="showRejectForm = false"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,374 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.dri {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.dri-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
justify-content: center;
|
||||
padding: 60px 24px;
|
||||
font-size: 15px;
|
||||
color: #64748b;
|
||||
|
||||
&__icon {
|
||||
font-size: 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
.dri-body {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dri-card {
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #e2e8f0;
|
||||
overflow: hidden;
|
||||
|
||||
&--header {
|
||||
border-top: 3px solid #0f4f3c;
|
||||
}
|
||||
|
||||
&--rejected {
|
||||
border-top: 3px solid #ef4444;
|
||||
}
|
||||
|
||||
&--approved {
|
||||
border-top: 3px solid #22c55e;
|
||||
}
|
||||
}
|
||||
|
||||
.dri-card-header {
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.dri-voucher {
|
||||
font-family: 'Courier New', monospace;
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: #1e40af;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
|
||||
.dri-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 3px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
|
||||
&--pending {
|
||||
background: #fef3c7;
|
||||
color: #92400e;
|
||||
}
|
||||
|
||||
&--approved {
|
||||
background: #dcfce7;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
&--rejected {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
}
|
||||
}
|
||||
|
||||
.dri-meta-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 14px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
.dri-section-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #475569;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
background: #f8fafc;
|
||||
|
||||
mat-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
color: #0f4f3c;
|
||||
}
|
||||
}
|
||||
|
||||
.dri-detail-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
padding: 16px 20px;
|
||||
gap: 12px 24px;
|
||||
}
|
||||
|
||||
.dri-detail-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
|
||||
&--full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
|
||||
.dri-detail-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.dri-detail-value {
|
||||
font-size: 14px;
|
||||
color: #0d1b2a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.dri-amount {
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
.dri-cr-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 13px;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 10px 16px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
th {
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
color: #64748b;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.num {
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.dri-cr-idx {
|
||||
width: 36px;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.dri-type-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
background: #f1f5f9;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.dri-cr-amount {
|
||||
font-weight: 700;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.dri-cr-total-row {
|
||||
background: #f0fdf4;
|
||||
|
||||
td {
|
||||
border-top: 2px solid #bbf7d0;
|
||||
padding: 10px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.dri-cr-total-label {
|
||||
font-weight: 700;
|
||||
color: #166534;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.dri-cr-total-amount {
|
||||
font-weight: 700;
|
||||
color: #166534;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.dri-rejection-text {
|
||||
padding: 14px 20px;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
color: #dc2626;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.dri-txn-refs {
|
||||
padding: 14px 20px;
|
||||
margin: 0;
|
||||
font-size: 12px;
|
||||
color: #166534;
|
||||
}
|
||||
|
||||
.dri-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: flex-start;
|
||||
padding: 4px 0 8px;
|
||||
}
|
||||
|
||||
.dri-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-radius: 8px;
|
||||
font-family: inherit;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
border: none;
|
||||
transition: background 0.15s;
|
||||
padding: 10px 20px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
opacity: 0.5;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
&--approve {
|
||||
background: #0f4f3c;
|
||||
color: #fff;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #0a3d2e;
|
||||
}
|
||||
}
|
||||
|
||||
&--reject {
|
||||
background: #fee2e2;
|
||||
color: #991b1b;
|
||||
border: 1px solid #fca5a5;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #fecaca;
|
||||
}
|
||||
}
|
||||
|
||||
&--ghost {
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: #e2e8f0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.dri-reject-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
width: 100%;
|
||||
max-width: 480px;
|
||||
}
|
||||
|
||||
.dri-reject-label {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #991b1b;
|
||||
}
|
||||
|
||||
.dri-reject-textarea {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid #fca5a5;
|
||||
border-radius: 8px;
|
||||
font-family: inherit;
|
||||
font-size: 13px;
|
||||
resize: vertical;
|
||||
color: #0d1b2a;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
|
||||
&:focus {
|
||||
border-color: #ef4444;
|
||||
}
|
||||
}
|
||||
|
||||
.dri-reject-actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.cell-mono {
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* 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 { ActivatedRoute, Router } from '@angular/router';
|
||||
import { MatSnackBar, MatSnackBarModule } from '@angular/material/snack-bar';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { ClerkDisbursementService } from '../../cashier/clerk-disbursement.service';
|
||||
|
||||
interface CreditEntry {
|
||||
accountId: number;
|
||||
accountNo: string;
|
||||
accountName: string;
|
||||
accountType: string;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-disbursement-review',
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
FormsModule,
|
||||
MatSnackBarModule,
|
||||
MatIconModule
|
||||
],
|
||||
templateUrl: './disbursement-review.component.html',
|
||||
styleUrl: './disbursement-review.component.scss'
|
||||
})
|
||||
export class DisbursementReviewComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
private router = inject(Router);
|
||||
private service = inject(ClerkDisbursementService);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
|
||||
requestId: number | null = null;
|
||||
request: any = null;
|
||||
creditEntries: CreditEntry[] = [];
|
||||
|
||||
loading = false;
|
||||
acting = false;
|
||||
showRejectForm = false;
|
||||
rejectionReason = '';
|
||||
|
||||
ngOnInit(): void {
|
||||
const id = this.route.snapshot.params['id'];
|
||||
this.requestId = id != null ? Number(id) : null;
|
||||
if (this.requestId) this.loadRequest();
|
||||
}
|
||||
|
||||
loadRequest(): void {
|
||||
if (!this.requestId) return;
|
||||
this.loading = true;
|
||||
this.service
|
||||
.getById(this.requestId)
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe((res: any) => {
|
||||
this.request = res;
|
||||
this.loading = false;
|
||||
if (res?.credit_entries) {
|
||||
try {
|
||||
this.creditEntries =
|
||||
typeof res.credit_entries === 'string' ? JSON.parse(res.credit_entries) : res.credit_entries;
|
||||
} catch {
|
||||
this.creditEntries = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
approve(): void {
|
||||
if (!this.requestId || this.acting) return;
|
||||
this.acting = true;
|
||||
this.service
|
||||
.approve(this.requestId)
|
||||
.pipe(catchError((err) => of({ error: err?.error?.errors?.[0]?.defaultUserMessage ?? 'Approval failed.' })))
|
||||
.subscribe((res: any) => {
|
||||
this.acting = false;
|
||||
if (res?.error) {
|
||||
this.snackBar.open(res.error, 'Close', { duration: 5000 });
|
||||
} else {
|
||||
this.snackBar.open('Disbursement approved successfully.', 'Close', { duration: 3000 });
|
||||
this.router.navigate(['/checker-inbox-and-tasks/disbursement-requests']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reject(): void {
|
||||
if (!this.requestId || this.acting || !this.rejectionReason.trim()) return;
|
||||
this.acting = true;
|
||||
this.service
|
||||
.reject(this.requestId, this.rejectionReason.trim())
|
||||
.pipe(catchError((err) => of({ error: err?.error?.errors?.[0]?.defaultUserMessage ?? 'Rejection failed.' })))
|
||||
.subscribe((res: any) => {
|
||||
this.acting = false;
|
||||
if (res?.error) {
|
||||
this.snackBar.open(res.error, 'Close', { duration: 5000 });
|
||||
} else {
|
||||
this.snackBar.open('Request rejected.', 'Close', { duration: 3000 });
|
||||
this.router.navigate(['/checker-inbox-and-tasks/disbursement-requests']);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
totalCreditAmount(): number {
|
||||
return this.creditEntries.reduce((sum, e) => sum + (e.amount ?? 0), 0);
|
||||
}
|
||||
|
||||
statusClass(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
PENDING: 'dri-status--pending',
|
||||
APPROVED: 'dri-status--approved',
|
||||
REJECTED: 'dri-status--rejected'
|
||||
};
|
||||
return map[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);
|
||||
}
|
||||
}
|
||||
|
||||
backToInbox(): void {
|
||||
this.router.navigate(['/checker-inbox-and-tasks/disbursement-requests']);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user