feat(cashier): add Cash/Clearing/Transfer summary report with branch-wise drill-down

Adds a dummy-data summary report (Opening/Credit/Debit/Closing) under Sub
Ledger > Cash / Clearing / Transfer, with Cash, Transfer (GL-wise), Clearing
and Digital (RTGS/NEFT/UPI/ATM/NACH) tabs plus a transaction-details drill
down. Credit/Debit/Closing are always derived from the transaction lines so
the summary and detail views can never drift apart.

Head Office logins (officeId 1 or office name containing "head office") get
a branch switcher across two demo branches plus a consolidated view with a
branch-wise break-up; branch-level logins see only their own office's data.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-31 13:39:13 +05:30
co-authored by Claude Sonnet 5
parent f687e1808d
commit 1dc378c2dd
7 changed files with 1900 additions and 1 deletions
@@ -0,0 +1,267 @@
<!--
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="pg">
<header class="pg-head">
<div>
<div class="pg-eyebrow">SUB LEDGER · TRANSACTIONS · {{ scopeName | uppercase }}</div>
<h1 class="pg-title">Cash / Clearing / Transfer - Summary Report</h1>
<p class="pg-sub">
Opening, credit, debit and closing position across Cash, Transfer, Clearing and Digital channels as on
{{ asOnDate }}.
</p>
</div>
<div class="pg-toolbar">
@if (isHeadOffice) {
<label class="pg-field">
<span>Branch</span>
<select class="pg-select" [ngModel]="selectedBranchCode" (ngModelChange)="setBranch($event)">
<option value="ALL">All Branches (Consolidated)</option>
@for (b of branches; track b.code) {
<option [value]="b.code">{{ b.name }}</option>
}
</select>
</label>
}
<button type="button" class="pg-btn" [class.pg-btn--active]="showDetails" (click)="toggleDetails()">
{{ showDetails ? 'Summary View' : 'Transaction Details' }}
</button>
</div>
</header>
<div class="cctr-grand-summary">
<div class="cctr-grand-summary__item">
<span class="cctr-grand-summary__label">Opening</span>
<span class="cctr-grand-summary__value">₹{{ grandOpening | number: '1.2-2' }}</span>
</div>
<div class="cctr-grand-summary__item">
<span class="cctr-grand-summary__label">Credit</span>
<span class="cctr-grand-summary__value col-in">₹{{ grandCredit | number: '1.2-2' }}</span>
</div>
<div class="cctr-grand-summary__item">
<span class="cctr-grand-summary__label">Debit</span>
<span class="cctr-grand-summary__value col-out">₹{{ grandDebit | number: '1.2-2' }}</span>
</div>
<div class="cctr-grand-summary__item">
<span class="cctr-grand-summary__label">Closing</span>
<span class="cctr-grand-summary__value">₹{{ grandClosing | number: '1.2-2' }}</span>
</div>
</div>
@if (isHeadOffice && selectedBranchCode === 'ALL') {
<div class="pg-card">
<div class="pos-section-label">Branch-wise Break-up — click a branch to drill in</div>
<table class="pg-table">
<thead>
<tr>
<th>Branch</th>
<th class="num">Opening</th>
<th class="num col-in">Credit</th>
<th class="num col-out">Debit</th>
<th class="num col-net">Closing</th>
</tr>
</thead>
<tbody>
@for (b of branchBreakup; track b.code) {
<tr class="cctr-branch-row" (click)="setBranch(b.code)">
<td>{{ b.name }}</td>
<td class="num">₹{{ b.opening | number: '1.2-2' }}</td>
<td class="num col-in">₹{{ b.credit | number: '1.2-2' }}</td>
<td class="num col-out">₹{{ b.debit | number: '1.2-2' }}</td>
<td class="num col-net">₹{{ b.closing | number: '1.2-2' }}</td>
</tr>
}
</tbody>
<tfoot>
<tr class="cctr-total-row">
<td>All Branches</td>
<td class="num">₹{{ grandOpening | number: '1.2-2' }}</td>
<td class="num">₹{{ grandCredit | number: '1.2-2' }}</td>
<td class="num">₹{{ grandDebit | number: '1.2-2' }}</td>
<td class="num">₹{{ grandClosing | number: '1.2-2' }}</td>
</tr>
</tfoot>
</table>
</div>
}
<div class="cctr-tabs">
<button
type="button"
class="pg-btn pg-btn--ghost"
[class.pg-btn--active]="activeTab === 'cash'"
(click)="setTab('cash')"
>
Cash
</button>
<button
type="button"
class="pg-btn pg-btn--ghost"
[class.pg-btn--active]="activeTab === 'transfer'"
(click)="setTab('transfer')"
>
Transfer
</button>
<button
type="button"
class="pg-btn pg-btn--ghost"
[class.pg-btn--active]="activeTab === 'clearing'"
(click)="setTab('clearing')"
>
Clearing
</button>
<button
type="button"
class="pg-btn pg-btn--ghost"
[class.pg-btn--active]="activeTab === 'digital'"
(click)="setTab('digital')"
>
Digital
</button>
</div>
<!-- Summary (Opening | Credit | Debit | Closing) -->
@if (!showDetails) {
<div class="pg-card">
@if (activeTab === 'digital') {
<div class="pos-section-label">RTGS · NEFT · UPI · ATM · NACH — Overall Position</div>
}
<table class="pg-table">
<thead>
<tr>
@if (activeTab !== 'digital') {
<th>GL Code</th>
}
<th>Particulars</th>
<th class="num">Opening</th>
<th class="num col-in">Credit</th>
<th class="num col-out">Debit</th>
<th class="num col-net">Closing</th>
</tr>
</thead>
<tbody>
@for (r of rowsForActiveTab; track r.code) {
<tr>
@if (activeTab !== 'digital') {
<td>
<span class="pg-chip">{{ r.code }}</span>
</td>
}
<td>{{ r.name }}</td>
<td class="num">₹{{ r.opening | number: '1.2-2' }}</td>
<td class="num col-in">₹{{ rowCredit(r, txnsForActiveTab) | number: '1.2-2' }}</td>
<td class="num col-out">₹{{ rowDebit(r, txnsForActiveTab) | number: '1.2-2' }}</td>
<td class="num col-net">₹{{ closing(r, txnsForActiveTab) | number: '1.2-2' }}</td>
</tr>
}
</tbody>
<tfoot>
<tr class="cctr-total-row">
<td [attr.colspan]="activeTab !== 'digital' ? 2 : 1">Total</td>
<td class="num">₹{{ total(rowsForActiveTab, txnsForActiveTab, 'opening') | number: '1.2-2' }}</td>
<td class="num">₹{{ total(rowsForActiveTab, txnsForActiveTab, 'credit') | number: '1.2-2' }}</td>
<td class="num">₹{{ total(rowsForActiveTab, txnsForActiveTab, 'debit') | number: '1.2-2' }}</td>
<td class="num">₹{{ totalClosing(rowsForActiveTab, txnsForActiveTab) | number: '1.2-2' }}</td>
</tr>
</tfoot>
</table>
</div>
}
<!-- Transaction Details (all txns for the selected category, optionally filtered by GL/channel) -->
@if (showDetails) {
@if (rowsForActiveTab.length > 1) {
<div class="cctr-tabs">
<button
type="button"
class="pg-btn pg-btn--ghost"
[class.pg-btn--active]="glFilter === null"
(click)="setGlFilter(null)"
>
All
</button>
@for (r of rowsForActiveTab; track r.code) {
<button
type="button"
class="pg-btn pg-btn--ghost"
[class.pg-btn--active]="glFilter === r.code"
(click)="setGlFilter(r.code)"
>
{{ filterLabel(r) }}
</button>
}
</div>
}
<div class="pg-card">
<div class="pos-section-label">
{{
activeTab === 'cash'
? 'Cash'
: activeTab === 'transfer'
? 'Transfer'
: activeTab === 'clearing'
? 'Clearing'
: 'Digital'
}}
— Transaction Details
@if (glFilter !== null) {
· {{ glFilter }}
}
</div>
<table class="pg-table">
<thead>
<tr>
<th>Date</th>
<th>Voucher / Ref No</th>
<th>Account / Reference</th>
<th>Particulars</th>
<th class="num col-out">Debit</th>
<th class="num col-in">Credit</th>
</tr>
</thead>
<tbody>
@for (t of filteredTxns; track t.voucherNo) {
<tr>
<td>{{ asOnDate }}</td>
<td>
<span class="pg-chip">{{ t.voucherNo }}</span>
</td>
<td>
<div>{{ t.accountName }}</div>
<div class="cctr-subtext">{{ t.accountNo }}</div>
</td>
<td>{{ t.particulars }}</td>
<td class="num col-out">
@if (t.debit > 0) {
₹{{ t.debit | number: '1.2-2' }}
}
</td>
<td class="num col-in">
@if (t.credit > 0) {
₹{{ t.credit | number: '1.2-2' }}
}
</td>
</tr>
} @empty {
<tr>
<td colspan="6" class="pg-empty">No transactions for this filter.</td>
</tr>
}
</tbody>
<tfoot>
<tr class="cctr-total-row">
<td colspan="4">Total</td>
<td class="num">₹{{ txnTotal(filteredTxns, 'debit') | number: '1.2-2' }}</td>
<td class="num">₹{{ txnTotal(filteredTxns, 'credit') | number: '1.2-2' }}</td>
</tr>
</tfoot>
</table>
</div>
}
</div>
@@ -0,0 +1,73 @@
/**
* 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/.
*/
.cctr-tabs {
display: flex;
gap: 8px;
flex-wrap: wrap;
}
.cctr-total-row {
td {
background: #f1f5f9;
font-weight: 800;
border-top: 2px solid #cbd5e1;
border-bottom: none;
color: #0f172a;
}
}
.cctr-grand-summary {
display: flex;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 12px;
overflow: hidden;
&__item {
flex: 1;
padding: 14px 20px;
display: flex;
flex-direction: column;
gap: 4px;
border-right: 1px solid #e2e8f0;
&:last-child {
border-right: none;
}
}
&__label {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.07em;
color: #64748b;
}
&__value {
font-size: 20px;
font-weight: 800;
font-variant-numeric: tabular-nums;
color: #0f172a;
}
}
.cctr-subtext {
font-size: 11px;
color: #94a3b8;
margin-top: 2px;
}
.cctr-branch-row {
cursor: pointer;
&:hover td {
background: #eff6ff;
}
}
+11
View File
@@ -179,6 +179,17 @@ const routes: Routes = [
loadComponent: () =>
import('./my-deposit-requests/my-deposit-requests.component').then((m) => m.MyDepositRequestsComponent),
data: { title: 'My Deposit Requests', breadcrumb: 'My Deposit Requests' }
},
{
path: 'cash-clearing-transfer-summary',
loadComponent: () =>
import('./cash-clearing-transfer-summary/cash-clearing-transfer-summary.component').then(
(m) => m.CashClearingTransferSummaryComponent
),
data: {
title: 'Cash / Clearing / Transfer - Summary Report',
breadcrumb: 'Cash / Clearing / Transfer - Summary Report'
}
}
]
}
@@ -424,6 +424,18 @@ export class AuthenticationService {
return roleNames.some((n) => n === 'cashier');
}
/**
* Returns true when the logged-in user belongs to the Head Office.
* Fineract seeds Head Office as officeId 1 with hierarchy "."; office name is
* checked too since some deployments rename/reseed the root office.
*/
isHeadOfficeUser(): boolean {
const creds = this.getCredentials();
if (!creds) return false;
if (creds.officeId === 1) return true;
return (creds.officeName || '').toLowerCase().includes('head office');
}
/**
* Sets the user credentials.
*
+1
View File
@@ -653,6 +653,7 @@
<a
class="gl-link"
[class.hidden]="!matchesSubLedgerSearch('Cash / Clearing / Transfer')"
routerLink="/cashier/cash-clearing-transfer-summary"
(click)="subLedgerOpen = false"
><fa-icon icon="money-bill" class="gl-icon"></fa-icon> Cash / Clearing / Transfer</a
>
@@ -266,7 +266,7 @@
<a
class="subledger-link"
[class.hidden]="!matchesSearch('Cash / Clearing / Transfer')"
(click)="subLedgerOpen = false"
(click)="navigateMenu('/cashier/cash-clearing-transfer-summary'); subLedgerOpen = false"
>
<fa-icon icon="money-bill" class="subledger-icon"></fa-icon> Cash / Clearing / Transfer
</a>