DDS: approvals tab, vouchers, payout & close, Sundry GL search, closure tab, fixes
This commit is contained in:
+265
@@ -0,0 +1,265 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
|
||||
<!-- Laid out as Disbursement & Transfers reviews its requests, and styled by the same sheet. -->
|
||||
<div class="dri dri--dialog">
|
||||
<nav class="cashier-nav cashier-nav--clerk">
|
||||
<span class="dri-header-title">DDS {{ entry.type }} Review</span>
|
||||
<span class="cashier-nav__esc-hint" (click)="close()">✕ Close</span>
|
||||
</nav>
|
||||
|
||||
@if (loading) {
|
||||
<div class="dri-loading">
|
||||
<mat-icon class="dri-loading__icon">hourglass_empty</mat-icon>
|
||||
<span>Loading entry…</span>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="dri-body">
|
||||
<!-- Header card: the voucher it moves under, its status, who made it and when -->
|
||||
<div class="dri-card dri-card--header">
|
||||
<div class="dri-card-header">
|
||||
<div class="dri-card-header__left">
|
||||
<span class="dri-voucher">{{ voucherNo || '—' }}</span>
|
||||
<span class="dri-status" [ngClass]="statusClass(entry.status)">
|
||||
{{ entry.status }}{{ entry.voucherCancelled ? ' · VOUCHER CANCELLED' : '' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="dri-card-header__meta">
|
||||
<span class="dri-meta-item">
|
||||
<mat-icon>person</mat-icon>
|
||||
{{ entry.makerName || entry.maker }} ({{ entry.maker }})
|
||||
</span>
|
||||
<span class="dri-meta-item">
|
||||
<mat-icon>schedule</mat-icon>
|
||||
{{ formatDateTime(entry.madeOn) }}
|
||||
</span>
|
||||
@if (entry.status !== 'PENDING' && entry.checker) {
|
||||
<span class="dri-meta-item">
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
Actioned by: {{ entry.checkerName || entry.checker }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Summary -->
|
||||
<div class="dri-card">
|
||||
<div class="dri-section-title">
|
||||
<mat-icon>swap_horiz</mat-icon>
|
||||
{{ entry.type }} Summary
|
||||
</div>
|
||||
<div class="dri-detail-grid">
|
||||
<div class="dri-detail-item">
|
||||
<span class="dri-detail-label">Total Amount</span>
|
||||
<span class="dri-detail-value dri-amount">
|
||||
{{ entry.amount !== null && entry.amount !== undefined ? '₹' + (entry.amount | number: '1.2-2') : '—' }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="dri-detail-item">
|
||||
<span class="dri-detail-label">DR Accounts</span>
|
||||
<span class="dri-detail-value"
|
||||
>{{ debitLines.length }} account{{ debitLines.length !== 1 ? 's' : '' }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="dri-detail-item">
|
||||
<span class="dri-detail-label">CR Accounts</span>
|
||||
<span class="dri-detail-value"
|
||||
>{{ creditLines.length }} account{{ creditLines.length !== 1 ? 's' : '' }}</span
|
||||
>
|
||||
</div>
|
||||
<div class="dri-detail-item dri-detail-item--full">
|
||||
<span class="dri-detail-label">Details</span>
|
||||
<span class="dri-detail-value">{{ entry.details || '—' }}</span>
|
||||
</div>
|
||||
@if (detail?.description) {
|
||||
<div class="dri-detail-item dri-detail-item--full">
|
||||
<span class="dri-detail-label">Description</span>
|
||||
<span class="dri-detail-value">{{ detail.description }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Debit entries -->
|
||||
@if (debitLines.length) {
|
||||
<div class="dri-card">
|
||||
<div class="dri-section-title">
|
||||
<mat-icon>arrow_upward</mat-icon>
|
||||
Debit Entries ({{ debitLines.length }} account{{ debitLines.length !== 1 ? 's' : '' }} — funds out)
|
||||
</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 (DR)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (line of debitLines; track $index; let i = $index) {
|
||||
<tr>
|
||||
<td class="dri-cr-idx">{{ i + 1 }}</td>
|
||||
<td class="cell-mono">{{ line.accountNo || '—' }}</td>
|
||||
<td>{{ line.accountName || '—' }}</td>
|
||||
<td>
|
||||
<span class="dri-type-badge">{{ line.type || '—' }}</span>
|
||||
</td>
|
||||
<td class="num dri-cr-amount">₹{{ line.amount | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
<tr class="dri-cr-total-row">
|
||||
<td colspan="4" class="dri-cr-total-label">Total Debit</td>
|
||||
<td class="num dri-cr-total-amount">₹{{ total(debitLines) | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Credit entries -->
|
||||
@if (creditLines.length) {
|
||||
<div class="dri-card">
|
||||
<div class="dri-section-title">
|
||||
<mat-icon>arrow_downward</mat-icon>
|
||||
Credit Entries ({{ creditLines.length }} account{{ creditLines.length !== 1 ? 's' : '' }} — funds in)
|
||||
</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 (line of creditLines; track $index; let i = $index) {
|
||||
<tr>
|
||||
<td class="dri-cr-idx">{{ i + 1 }}</td>
|
||||
<td class="cell-mono">{{ line.accountNo || '—' }}</td>
|
||||
<td>{{ line.accountName || '—' }}</td>
|
||||
<td>
|
||||
<span class="dri-type-badge">{{ line.type || '—' }}</span>
|
||||
</td>
|
||||
<td class="num dri-cr-amount">₹{{ line.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">₹{{ total(creditLines) | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (entry.status === 'PENDING' && isMine && !done) {
|
||||
<div class="dri-card dri-card--processing">
|
||||
<div class="dri-section-title">
|
||||
<mat-icon>info</mat-icon>
|
||||
Your own entry
|
||||
</div>
|
||||
<p class="dri-processing-text">
|
||||
You made this entry, so another user with checker rights has to approve or reject it.
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (entry.status === 'REJECTED') {
|
||||
<div class="dri-card dri-card--rejected">
|
||||
<div class="dri-section-title">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
Rejected
|
||||
</div>
|
||||
<p class="dri-rejection-text">
|
||||
Nothing was done{{ voucherNo ? '. Voucher ' + voucherNo + ' is cancelled and will not be used' : '' }}.
|
||||
</p>
|
||||
@if (entry.checker) {
|
||||
<div class="dri-meta-item" style="margin-top: 8px">
|
||||
<mat-icon>manage_accounts</mat-icon>
|
||||
{{ entry.checker }}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (entry.status === 'APPROVED') {
|
||||
<div class="dri-card dri-card--approved">
|
||||
<div class="dri-section-title">
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
Approved and carried out
|
||||
</div>
|
||||
<p class="dri-txn-refs cell-mono">
|
||||
{{
|
||||
voucherNo
|
||||
? 'Voucher ' + voucherNo
|
||||
: entry.entity === 'DDSBATCH'
|
||||
? 'Posting is running - refresh for its voucher'
|
||||
: '—'
|
||||
}}
|
||||
</p>
|
||||
<div class="dri-meta-item" style="margin-top: 8px">
|
||||
<mat-icon>manage_accounts</mat-icon>
|
||||
Approved by: {{ entry.checker || '—' }}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (error) {
|
||||
<div class="dri-card dri-card--failed">
|
||||
<div class="dri-section-title">
|
||||
<mat-icon>error</mat-icon>
|
||||
Could not complete
|
||||
</div>
|
||||
<p class="dri-rejection-text">{{ error }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (entry.status === 'PENDING' && !done) {
|
||||
<div class="dri-actions">
|
||||
@if (!confirmReject) {
|
||||
<button type="button" class="dri-btn dri-btn--approve" [disabled]="!canAct" (click)="act('approve')">
|
||||
<mat-icon>check_circle</mat-icon>
|
||||
{{ working ? 'Processing…' : 'Approve ' + entry.type }}
|
||||
</button>
|
||||
<button type="button" class="dri-btn dri-btn--reject" [disabled]="!canAct" (click)="confirmReject = true">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
Reject
|
||||
</button>
|
||||
} @else {
|
||||
<div class="dri-reject-form">
|
||||
<label class="dri-reject-label">
|
||||
Reject this entry? Nothing will be done{{
|
||||
voucherNo ? ' and voucher ' + voucherNo + ' will be cancelled' : ''
|
||||
}}.
|
||||
</label>
|
||||
<div class="dri-reject-actions">
|
||||
<button type="button" class="dri-btn dri-btn--reject" [disabled]="!canAct" (click)="act('reject')">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
{{ working ? 'Processing…' : 'Confirm Rejection' }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="dri-btn dri-btn--ghost"
|
||||
[disabled]="working"
|
||||
(click)="confirmReject = false"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,148 @@
|
||||
/**
|
||||
* 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 { MAT_DIALOG_DATA, MatDialogModule, MatDialogRef } from '@angular/material/dialog';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
|
||||
import { OrganizationService } from 'app/organization/organization.service';
|
||||
|
||||
/**
|
||||
* One DDS entry, laid out the way Disbursement & Transfers reviews a request - voucher and status on
|
||||
* top, who made it and when, then the debit and credit entries it moves - and styled by the same
|
||||
* sheet, so a checker reads both queues the same way.
|
||||
*
|
||||
* The checker approves it, which carries it out under the voucher numbered when it was sent, or
|
||||
* rejects it, which leaves nothing done and cancels that number. Neither is offered to the user who
|
||||
* made the entry, and the server refuses it as well.
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-dds-approval-review',
|
||||
templateUrl: './dds-approval-review.component.html',
|
||||
styleUrls: ['../../../clerk/disbursement-review/disbursement-review.component.scss'],
|
||||
standalone: true,
|
||||
imports: [
|
||||
CommonModule,
|
||||
MatDialogModule,
|
||||
MatIconModule
|
||||
]
|
||||
})
|
||||
export class DdsApprovalReviewComponent implements OnInit {
|
||||
private organizationService = inject(OrganizationService);
|
||||
private dialogRef = inject(MatDialogRef<DdsApprovalReviewComponent>);
|
||||
data = inject<any>(MAT_DIALOG_DATA);
|
||||
|
||||
entry: any = this.data.entry;
|
||||
/** The entry in full, with the debit and credit sides it moves. */
|
||||
detail: any = null;
|
||||
loading = true;
|
||||
working = false;
|
||||
confirmReject = false;
|
||||
done = false;
|
||||
error = '';
|
||||
/** Set by an approval, in case the list row did not carry the number yet. */
|
||||
approvedVoucher = '';
|
||||
|
||||
ngOnInit(): void {
|
||||
this.load();
|
||||
}
|
||||
|
||||
private load(): void {
|
||||
this.organizationService.getDdsApproval(this.entry.id).subscribe({
|
||||
next: (detail: any) => {
|
||||
this.detail = detail;
|
||||
this.entry = { ...this.entry, ...detail };
|
||||
this.loading = false;
|
||||
},
|
||||
error: () => (this.loading = false)
|
||||
});
|
||||
}
|
||||
|
||||
get voucherNo(): string {
|
||||
return this.approvedVoucher || this.entry?.voucherNo || '';
|
||||
}
|
||||
|
||||
get debitLines(): any[] {
|
||||
return (this.detail?.lines || []).filter((line: any) => line.side === 'DR');
|
||||
}
|
||||
|
||||
get creditLines(): any[] {
|
||||
return (this.detail?.lines || []).filter((line: any) => line.side === 'CR');
|
||||
}
|
||||
|
||||
total(lines: any[]): number {
|
||||
return lines.reduce((sum: number, line: any) => sum + Number(line.amount || 0), 0);
|
||||
}
|
||||
|
||||
get isMine(): boolean {
|
||||
return !!this.data.currentUserId && Number(this.entry.makerId) === Number(this.data.currentUserId);
|
||||
}
|
||||
|
||||
get canAct(): boolean {
|
||||
return this.entry.status === 'PENDING' && !this.isMine && !this.done && !this.working;
|
||||
}
|
||||
|
||||
statusClass(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
PENDING: 'dri-status--pending',
|
||||
APPROVED: 'dri-status--approved',
|
||||
REJECTED: 'dri-status--rejected'
|
||||
};
|
||||
return map[status] ?? '';
|
||||
}
|
||||
|
||||
act(command: 'approve' | 'reject'): void {
|
||||
this.working = true;
|
||||
this.error = '';
|
||||
this.organizationService.actOnDdsApproval(this.entry.id, command).subscribe({
|
||||
next: (result: any) => {
|
||||
this.working = false;
|
||||
this.done = true;
|
||||
this.approvedVoucher = result?.voucherNo || '';
|
||||
this.entry = {
|
||||
...this.entry,
|
||||
status: command === 'approve' ? 'APPROVED' : 'REJECTED',
|
||||
voucherCancelled: command === 'reject' && !!this.voucherNo
|
||||
};
|
||||
// The approved entry's sides are now the voucher's own lines.
|
||||
this.load();
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.working = false;
|
||||
const body = err?.error;
|
||||
this.error =
|
||||
body?.errors?.[0]?.defaultUserMessage ||
|
||||
body?.defaultUserMessage ||
|
||||
body?.developerMessage ||
|
||||
'Could not complete this.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** The same form Disbursement & Transfers prints its times in: 29 Sept 2026, 01:25 pm. */
|
||||
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'
|
||||
});
|
||||
}
|
||||
|
||||
close(): void {
|
||||
this.dialogRef.close(this.done);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user