feat(reconciliation): build full 16-screen Reconciliation module

Wires every item in the Reconciliation mega-menu (Core & Books, Bank &
Clearing, Digital & Payments, Instruments & Members, Compliance &
Financials) to a working screen, mirroring the NPA module's pattern: one
route-param-driven component over a shared two-sided (Side A vs Side B)
matching model, with pagination and a "Mark as Reconciled" action on every
screen.

Access is split by real-world scope: activity that happens at a branch
(vouchers, cheques, digital transactions, share members, fixed assets) is
branch-wise with the same Head-Office/branch login behavior as NPA; activity
with no branch dimension in real banking (GL/P&L consolidation, treasury's
own bank accounts, statutory tax filing, vendor procurement, inter-branch
netting) is Head-Office-only, with branch logins shown a restricted notice
instead of the data.

Also adds the .menu-item-live "working" indicator to mat-menu-item buttons
(same convention as .gl-link--live) and applies it across the reconciliation
menu and the pre-existing DD/Pay Order Reconciliation link.
This commit is contained in:
2026-08-01 13:15:17 +05:30
parent 4261f9659e
commit c935e2a62a
9 changed files with 1386 additions and 17 deletions
@@ -0,0 +1,228 @@
<!--
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">
RECONCILIATION · {{ groupLabels[activeGroup] | uppercase }}
<span class="recon-scope-chip">{{ meta.scope === 'HEAD_OFFICE' ? 'Head Office Only' : 'Branch-wise' }}</span>
</div>
<h1 class="pg-title">{{ meta.label }}</h1>
<p class="pg-sub">
{{ meta.sideALabel }} vs {{ meta.sideBLabel }}
@if (meta.extraLabel) {
vs {{ meta.extraLabel }}
}
— as on {{ asOnDate }}.
</p>
</div>
<div class="pg-toolbar">
@if (meta.scope === 'BRANCH') {
@if (isHeadOffice) {
<label class="pg-field">
<span>Branch</span>
<select class="pg-select" [ngModel]="branchFilter" (ngModelChange)="setBranch($event)">
<option value="">-- All Branches --</option>
@for (b of branches; track b.code) {
<option [value]="b.code">{{ b.name }}</option>
}
</select>
</label>
} @else {
<label class="pg-field">
<span>Branch</span>
<span class="pg-chip">{{ scopeName }}</span>
</label>
}
} @else {
<label class="pg-field">
<span>Scope</span>
<span class="pg-chip">Head Office (Whole Bank)</span>
</label>
}
<label class="pg-field">
<span>As On</span>
<input type="text" [(ngModel)]="asOnDate" placeholder="DD-MM-YYYY" />
</label>
<button type="button" class="pg-btn pg-btn--primary" (click)="generate()">Generate</button>
</div>
</header>
@if (!isRestricted) {
<div class="inv-kpis">
<div class="inv-kpi">
<span class="inv-kpi__label">{{ meta.sideALabel }}</span>
<span class="inv-kpi__value">₹{{ totalA | number: '1.0-0' }}</span>
<span class="inv-kpi__sub">{{ items.length }} items</span>
</div>
<div class="inv-kpi">
<span class="inv-kpi__label">{{ meta.sideBLabel }}</span>
<span class="inv-kpi__value">₹{{ totalB | number: '1.0-0' }}</span>
<span class="inv-kpi__sub">{{ scopeName }}</span>
</div>
<div class="inv-kpi">
<span class="inv-kpi__label">Absolute Difference</span>
<span class="inv-kpi__value" [class.npa-warn]="totalDiff > 0">₹{{ totalDiff | number: '1.0-0' }}</span>
<span class="inv-kpi__sub">{{ matchPct | number: '1.1-1' }}% matched</span>
</div>
<div class="inv-kpi">
<span class="inv-kpi__label">Open Items</span>
<span class="inv-kpi__value npa-warn">{{ unmatchedCount + pendingCount }}</span>
<span class="inv-kpi__sub"
>{{ unmatchedCount }} unmatched · {{ pendingCount }} pending · {{ resolvedCount }} resolved</span
>
</div>
</div>
}
<div class="cctr-tabs">
@for (g of groups; track g) {
<button
type="button"
class="pg-btn pg-btn--ghost"
[class.pg-btn--active]="activeGroup === g"
(click)="goToReport(groupOf(g)[0].id)"
>
{{ groupLabels[g] }}
</button>
}
</div>
<div class="cctr-tabs">
@for (m of groupOf(activeGroup); track m.id) {
<button
type="button"
class="pg-btn pg-btn--ghost"
[class.pg-btn--active]="activeReport === m.id"
(click)="goToReport(m.id)"
>
{{ m.label }}
</button>
}
</div>
@if (isRestricted) {
<div class="pg-card">
<div class="recon-restricted">
<fa-icon icon="university" class="recon-restricted__icon"></fa-icon>
<div class="recon-restricted__title">Managed Centrally by Head Office</div>
<p class="recon-restricted__body">
{{ meta.label }} has no branch-level dimension in real-world banking — it is reconciled centrally by the Head
Office Accounts/Treasury team. Your login ({{ currentOfficeName }}) does not have access to this screen.
Contact Head Office if you need a figure from this reconciliation.
</p>
</div>
</div>
} @else {
<div class="pg-card">
<div class="pos-section-label">{{ meta.label }} — {{ scopeName }}</div>
<div class="pg-table-scroll">
<table class="pg-table">
<thead>
<tr>
<th>{{ meta.referenceLabel }}</th>
<th>Description</th>
@if (meta.scope === 'BRANCH') {
<th>Branch</th>
}
<th>Date</th>
<th class="num">{{ meta.sideALabel }}</th>
<th class="num">{{ meta.sideBLabel }}</th>
@if (meta.extraLabel) {
<th class="num">{{ meta.extraLabel }}</th>
}
<th class="num">Difference</th>
<th>Status</th>
<th>Remarks / Action</th>
</tr>
</thead>
<tbody>
@for (item of paged(items); track item.id) {
<tr>
<td>
<span class="pg-chip">{{ item.reference }}</span>
</td>
<td>{{ item.description }}</td>
@if (meta.scope === 'BRANCH') {
<td>{{ item.branchName }}</td>
}
<td>{{ item.date }}</td>
<td class="num">₹{{ item.sideAAmount | number: '1.0-0' }}</td>
<td class="num">₹{{ item.sideBAmount | number: '1.0-0' }}</td>
@if (meta.extraLabel) {
<td class="num">₹{{ item.extraAmount | number: '1.0-0' }}</td>
}
<td class="num" [class.col-out]="diff(item) !== 0" [class.col-in]="diff(item) === 0">
₹{{ diff(item) | number: '1.0-0' }}
</td>
<td>
<span [class]="badgeClass(item.status)">{{ item.status }}</span>
</td>
<td>
@if (isItemOpen(item)) {
<div class="recon-action-row">
<input
type="text"
class="recon-remark-input"
[(ngModel)]="remarksDraft[item.id]"
placeholder="Remarks (optional)"
/>
<button type="button" class="pg-btn pg-btn--ghost" (click)="markReconciled(item)">
Mark as Reconciled
</button>
</div>
} @else if (item.status === 'RESOLVED') {
<span class="pg-sub">{{ item.remarks }}</span>
}
</td>
</tr>
} @empty {
<tr>
<td
[attr.colspan]="meta.scope === 'BRANCH' ? (meta.extraLabel ? 10 : 9) : meta.extraLabel ? 9 : 8"
class="pg-empty"
>
No {{ meta.label }} items for this branch.
</td>
</tr>
}
</tbody>
</table>
</div>
@if (items.length > pageSize) {
<div class="pg-pagination">
<span class="pg-pagination__info"
>Showing {{ rangeStart(items) }}–{{ rangeEnd(items) }} of {{ items.length }}</span
>
<div class="pg-pagination__controls">
<button
type="button"
class="pg-btn pg-btn--ghost"
[disabled]="currentPage === 1"
(click)="goToPage(currentPage - 1, items)"
>
‹ Prev
</button>
<span class="pg-pagination__page">Page {{ currentPage }} of {{ pageCount(items) }}</span>
<button
type="button"
class="pg-btn pg-btn--ghost"
[disabled]="currentPage === pageCount(items)"
(click)="goToPage(currentPage + 1, items)"
>
Next ›
</button>
</div>
</div>
}
</div>
}
</div>
<div class="inv-toast" [class.show]="toastVisible">{{ toastMsg }}</div>