feat(clerk): add collection flow — multi-DR savings to CR GL with maker-checker
- Transaction entry: detect collection mode (N DR savings → 1 CR GL) and disbursement mode (1 DR GL → N CR savings); build dbCr-tagged payload for backend - Disbursement review: show 'GL Credit (Target)' / 'Debit Entries' labels for collection; fix double-count by summing only DR entries for collection totals; dynamic approve button text - My disbursement requests: add DR/CR badge column in expanded credit-entries preview - My requests / withdrawal-requests: minor consistency updates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
efb01d5441
commit
6cccb58c04
@@ -13,7 +13,7 @@
|
||||
<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
|
||||
{{ isCollectionMode ? 'Collection Review' : 'Disbursement Review' }}
|
||||
</button>
|
||||
<span class="cashier-nav__esc-hint" (click)="backToInbox()">← Back to Inbox</span>
|
||||
</nav>
|
||||
@@ -63,11 +63,11 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GL Debit row -->
|
||||
<!-- GL card — Source (disbursement) or Target (collection) -->
|
||||
<div class="dri-card">
|
||||
<div class="dri-section-title">
|
||||
<mat-icon>account_balance</mat-icon>
|
||||
GL Debit (Source)
|
||||
{{ isCollectionMode ? 'GL Credit (Target)' : 'GL Debit (Source)' }}
|
||||
</div>
|
||||
<div class="dri-detail-grid">
|
||||
<div class="dri-detail-item">
|
||||
@@ -79,7 +79,7 @@
|
||||
<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-label">{{ isCollectionMode ? 'CR Amount' : 'DR Amount' }}</span>
|
||||
<span class="dri-detail-value dri-amount">₹{{ request.dr_amount | number: '1.2-2' }}</span>
|
||||
</div>
|
||||
<div class="dri-detail-item">
|
||||
@@ -95,11 +95,15 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Credit entries -->
|
||||
<!-- Entries table — DR savings (collection) or CR accounts (disbursement) -->
|
||||
<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' : '' }})
|
||||
@if (isCollectionMode) {
|
||||
Debit Entries ({{ drEntries.length }} account{{ drEntries.length !== 1 ? 's' : '' }} — collect from)
|
||||
} @else {
|
||||
Credit Entries ({{ crEntries.length }} account{{ crEntries.length !== 1 ? 's' : '' }})
|
||||
}
|
||||
</div>
|
||||
<table class="dri-cr-table">
|
||||
<thead>
|
||||
@@ -108,11 +112,11 @@
|
||||
<th>Account No</th>
|
||||
<th>Account Name</th>
|
||||
<th>Type</th>
|
||||
<th class="num">Amount (CR)</th>
|
||||
<th class="num">{{ isCollectionMode ? 'Amount (DR)' : 'Amount (CR)' }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (entry of creditEntries; track entry.accountId; let i = $index) {
|
||||
@for (entry of isCollectionMode ? drEntries : crEntries; track entry.accountNo; let i = $index) {
|
||||
<tr>
|
||||
<td class="dri-cr-idx">{{ i + 1 }}</td>
|
||||
<td class="cell-mono">{{ entry.accountNo }}</td>
|
||||
@@ -124,7 +128,7 @@
|
||||
</tr>
|
||||
}
|
||||
<tr class="dri-cr-total-row">
|
||||
<td colspan="4" class="dri-cr-total-label">Total Credit</td>
|
||||
<td colspan="4" class="dri-cr-total-label">{{ isCollectionMode ? 'Total Debit' : 'Total Credit' }}</td>
|
||||
<td class="num dri-cr-total-amount">₹{{ totalCreditAmount() | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
@@ -167,7 +171,7 @@
|
||||
@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' }}
|
||||
{{ acting ? 'Processing…' : isCollectionMode ? 'Approve Collection' : 'Approve Disbursement' }}
|
||||
</button>
|
||||
<button type="button" class="dri-btn dri-btn--reject" [disabled]="acting" (click)="showRejectForm = true">
|
||||
<mat-icon>cancel</mat-icon>
|
||||
|
||||
@@ -18,11 +18,12 @@ import { of } from 'rxjs';
|
||||
import { ClerkDisbursementService } from '../../cashier/clerk-disbursement.service';
|
||||
|
||||
interface CreditEntry {
|
||||
accountId: number;
|
||||
accountId?: number;
|
||||
accountNo: string;
|
||||
accountName: string;
|
||||
accountType: string;
|
||||
amount: number;
|
||||
dbCr?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
@@ -69,8 +70,9 @@ export class DisbursementReviewComponent implements OnInit {
|
||||
this.loading = false;
|
||||
if (res?.credit_entries) {
|
||||
try {
|
||||
this.creditEntries =
|
||||
const raw: any[] =
|
||||
typeof res.credit_entries === 'string' ? JSON.parse(res.credit_entries) : res.credit_entries;
|
||||
this.creditEntries = raw.map((e) => ({ ...e, amount: parseFloat(e.amount) || 0 }));
|
||||
} catch {
|
||||
this.creditEntries = [];
|
||||
}
|
||||
@@ -112,8 +114,21 @@ export class DisbursementReviewComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
get isCollectionMode(): boolean {
|
||||
return this.creditEntries.some((e) => e.dbCr === 'Dr');
|
||||
}
|
||||
|
||||
get drEntries(): CreditEntry[] {
|
||||
return this.creditEntries.filter((e) => e.dbCr === 'Dr');
|
||||
}
|
||||
|
||||
get crEntries(): CreditEntry[] {
|
||||
return this.creditEntries.filter((e) => e.dbCr !== 'Dr');
|
||||
}
|
||||
|
||||
totalCreditAmount(): number {
|
||||
return this.creditEntries.reduce((sum, e) => sum + (e.amount ?? 0), 0);
|
||||
const entries = this.isCollectionMode ? this.drEntries : this.crEntries;
|
||||
return entries.reduce((sum, e) => sum + (e.amount ?? 0), 0);
|
||||
}
|
||||
|
||||
statusClass(status: string): string {
|
||||
|
||||
@@ -127,7 +127,12 @@
|
||||
</td>
|
||||
<td class="num mdr-amount">₹{{ r.dr_amount | number: '1.2-2' }}</td>
|
||||
<td>
|
||||
<span class="mdr-credit-badge">{{ creditCount(r) }} account{{ creditCount(r) !== 1 ? 's' : '' }}</span>
|
||||
<button type="button" class="mdr-credit-badge mdr-credit-badge--btn" (click)="toggleExpand(r.id)">
|
||||
{{ creditCount(r) }} account{{ creditCount(r) !== 1 ? 's' : '' }}
|
||||
<mat-icon class="mdr-credit-chevron" [class.mdr-credit-chevron--open]="expandedIds.has(r.id)">
|
||||
expand_more
|
||||
</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
<td>{{ r.payment_type_name || 'Transfer' }}</td>
|
||||
<td>{{ r.manager_name || r.manager_username || '—' }}</td>
|
||||
@@ -136,6 +141,46 @@
|
||||
<span class="mdr-status" [ngClass]="statusClass(r.status)">{{ statusLabel(r.status) }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
@if (expandedIds.has(r.id)) {
|
||||
<tr class="mdr-expand-row">
|
||||
<td colspan="8" class="mdr-expand-cell">
|
||||
<div class="mdr-credit-preview">
|
||||
<table class="mdr-credit-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>DR/CR</th>
|
||||
<th>Account No</th>
|
||||
<th>Account Name</th>
|
||||
<th>Type</th>
|
||||
<th class="num">Amount (₹)</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (ce of getCreditEntries(r); track ce.accountNo) {
|
||||
<tr>
|
||||
<td>
|
||||
<span
|
||||
class="mdr-actype"
|
||||
[style.background]="ce.dbCr === 'Dr' ? '#fee2e2' : '#dcfce7'"
|
||||
[style.color]="ce.dbCr === 'Dr' ? '#991b1b' : '#166534'"
|
||||
>
|
||||
{{ ce.dbCr || 'CR' }}
|
||||
</span>
|
||||
</td>
|
||||
<td class="cell-mono">{{ ce.accountNo || '—' }}</td>
|
||||
<td>{{ ce.accountName || '—' }}</td>
|
||||
<td>
|
||||
<span class="mdr-actype">{{ ce.accountType || '—' }}</span>
|
||||
</td>
|
||||
<td class="num mdr-ce-amount">{{ ce.amount | number: '1.2-2' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@if (r.status === 'REJECTED' && r.rejection_reason) {
|
||||
<tr class="mdr-reason-row">
|
||||
<td colspan="8" class="mdr-reason-cell">
|
||||
|
||||
@@ -341,6 +341,91 @@
|
||||
color: #0369a1;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
|
||||
&--btn {
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
gap: 2px;
|
||||
font-family: inherit;
|
||||
transition: background 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #bae6fd;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mdr-credit-chevron {
|
||||
font-size: 14px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
transition: transform 0.2s;
|
||||
|
||||
&--open {
|
||||
transform: rotate(180deg);
|
||||
}
|
||||
}
|
||||
|
||||
.mdr-expand-row td {
|
||||
padding: 0;
|
||||
background: #f0f9ff;
|
||||
border-bottom: 2px solid #bae6fd;
|
||||
}
|
||||
|
||||
.mdr-expand-cell {
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
.mdr-credit-preview {
|
||||
padding: 10px 24px 12px 40px;
|
||||
}
|
||||
|
||||
.mdr-credit-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
|
||||
th,
|
||||
td {
|
||||
padding: 6px 10px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #e0f2fe;
|
||||
}
|
||||
|
||||
th {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: #0369a1;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
background: #e0f2fe;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.num {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
tr:last-child td {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.mdr-ce-amount {
|
||||
font-weight: 700;
|
||||
color: #0f4f3c;
|
||||
}
|
||||
|
||||
.mdr-actype {
|
||||
display: inline-block;
|
||||
padding: 1px 7px;
|
||||
border-radius: 10px;
|
||||
background: #f1f5f9;
|
||||
color: #475569;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.mdr-reason-row td {
|
||||
|
||||
@@ -45,6 +45,7 @@ export class MyDisbursementRequestsComponent implements OnInit {
|
||||
|
||||
requests: any[] = [];
|
||||
totalRecords = 0;
|
||||
expandedIds = new Set<number>();
|
||||
pageSize = 20;
|
||||
pageIndex = 0;
|
||||
pageSizeOptions = [
|
||||
@@ -164,15 +165,27 @@ export class MyDisbursementRequestsComponent implements OnInit {
|
||||
this.router.navigate(['/clerk/dashboard']);
|
||||
}
|
||||
|
||||
creditCount(req: any): number {
|
||||
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;
|
||||
return Array.isArray(arr) ? arr.length : 0;
|
||||
return Array.isArray(arr) ? arr.map((e: any) => ({ ...e, amount: parseFloat(e.amount) || 0 })) : [];
|
||||
} catch {
|
||||
return 0;
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
creditCount(req: any): number {
|
||||
return this.getCreditEntries(req).length;
|
||||
}
|
||||
|
||||
statusClass(status: string): string {
|
||||
const map: Record<string, string> = {
|
||||
PENDING: 'mdr-status--pending',
|
||||
|
||||
@@ -120,7 +120,7 @@
|
||||
[class.myr-row--pending]="r.status === 'PENDING'"
|
||||
[class.myr-row--approved]="r.status === 'APPROVED'"
|
||||
[class.myr-row--rejected]="r.status === 'REJECTED'"
|
||||
[class.myr-row--disbursed]="r.status === 'DISBURSED'"
|
||||
[class.myr-row--disbursed]="r.status === 'WITHDRAWN'"
|
||||
>
|
||||
<td class="cell-mono myr-voucher">{{ r.voucher_no || '—' }}</td>
|
||||
<td class="cell-mono myr-token">{{ r.tok_no || '—' }}</td>
|
||||
@@ -140,7 +140,7 @@
|
||||
<span class="myr-rejection-text" [title]="r.rejection_reason">
|
||||
{{ r.rejection_reason | slice: 0 : 60 }}{{ r.rejection_reason?.length > 60 ? '…' : '' }}
|
||||
</span>
|
||||
} @else if (r.status === 'DISBURSED') {
|
||||
} @else if (r.status === 'WITHDRAWN') {
|
||||
<div class="myr-disbursed-info">
|
||||
<span class="myr-disbursed-cashier">{{ r.disbursed_by || '—' }}</span>
|
||||
<span class="myr-dt">{{ formatDateTime(r.disbursed_at) }}</span>
|
||||
|
||||
@@ -81,7 +81,7 @@ export class MyRequestsComponent implements OnInit {
|
||||
{ value: 'PENDING', label: 'Pending' },
|
||||
{ value: 'APPROVED', label: 'Approved' },
|
||||
{ value: 'REJECTED', label: 'Rejected' },
|
||||
{ value: 'DISBURSED', label: 'Disbursed' }
|
||||
{ value: 'WITHDRAWN', label: 'Disbursed' }
|
||||
];
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ export class MyRequestsComponent implements OnInit {
|
||||
PENDING: 'myr-status--pending',
|
||||
APPROVED: 'myr-status--approved',
|
||||
REJECTED: 'myr-status--rejected',
|
||||
DISBURSED: 'myr-status--disbursed'
|
||||
WITHDRAWN: 'myr-status--disbursed'
|
||||
};
|
||||
return map[status] ?? '';
|
||||
}
|
||||
@@ -201,7 +201,7 @@ export class MyRequestsComponent implements OnInit {
|
||||
PENDING: 'Pending Approval',
|
||||
APPROVED: 'Approved',
|
||||
REJECTED: 'Rejected',
|
||||
DISBURSED: 'Disbursed'
|
||||
WITHDRAWN: 'Withdrawn'
|
||||
};
|
||||
return map[status] ?? status;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user