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:
@@ -67,6 +67,7 @@ import { FiuModule } from './fiu/fiu.module';
|
|||||||
import { SftModule } from './sft/sft.module';
|
import { SftModule } from './sft/sft.module';
|
||||||
import { InvestmentModule } from './investment/investment.module';
|
import { InvestmentModule } from './investment/investment.module';
|
||||||
import { NpaModule } from './npa/npa.module';
|
import { NpaModule } from './npa/npa.module';
|
||||||
|
import { ReconciliationModule } from './reconciliation/reconciliation.module';
|
||||||
import { DigitalModule } from './digital/digital.module';
|
import { DigitalModule } from './digital/digital.module';
|
||||||
import { LetterMgmtModule } from './letter-mgmt/letter-mgmt.module';
|
import { LetterMgmtModule } from './letter-mgmt/letter-mgmt.module';
|
||||||
import { AccountsModule } from './accounts/accounts.module';
|
import { AccountsModule } from './accounts/accounts.module';
|
||||||
@@ -176,6 +177,7 @@ export function HttpLoaderFactory(http: HttpClient) {
|
|||||||
SftModule,
|
SftModule,
|
||||||
InvestmentModule,
|
InvestmentModule,
|
||||||
NpaModule,
|
NpaModule,
|
||||||
|
ReconciliationModule,
|
||||||
DigitalModule,
|
DigitalModule,
|
||||||
LetterMgmtModule,
|
LetterMgmtModule,
|
||||||
AccountsModule,
|
AccountsModule,
|
||||||
|
|||||||
@@ -1299,27 +1299,27 @@
|
|||||||
<!-- Core / Books -->
|
<!-- Core / Books -->
|
||||||
<div class="deposit-menu-group">
|
<div class="deposit-menu-group">
|
||||||
<div class="deposit-group-title">Core & Books</div>
|
<div class="deposit-group-title">Core & Books</div>
|
||||||
<button mat-menu-item tabindex="0">
|
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/voucher">
|
||||||
<fa-icon icon="receipt" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
<fa-icon icon="receipt" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
||||||
Voucher Reconciliation
|
Voucher Reconciliation
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item tabindex="0">
|
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/sundry-dr-cr">
|
||||||
<fa-icon icon="random" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
<fa-icon icon="random" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
||||||
Sundry Dr / Cr Reconciliation
|
Sundry Dr / Cr Reconciliation
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item tabindex="0">
|
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/pnl-impact">
|
||||||
<fa-icon icon="chart-line" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
<fa-icon icon="chart-line" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
||||||
P&L Impact Reconciliation
|
P&L Impact Reconciliation
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item tabindex="0">
|
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/gl-reconciliation">
|
||||||
<fa-icon icon="book-open" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
<fa-icon icon="book-open" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
||||||
GL Reconciliation
|
GL Reconciliation
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item tabindex="0">
|
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/fixed-assets">
|
||||||
<fa-icon icon="cube" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
<fa-icon icon="cube" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
||||||
Fixed Assets Reconciliation
|
Fixed Assets Reconciliation
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item tabindex="0">
|
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/inter-company">
|
||||||
<fa-icon icon="sitemap" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
<fa-icon icon="sitemap" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
||||||
Inter-Company Reconciliation
|
Inter-Company Reconciliation
|
||||||
</button>
|
</button>
|
||||||
@@ -1328,19 +1328,19 @@
|
|||||||
<!-- Bank & Clearing -->
|
<!-- Bank & Clearing -->
|
||||||
<div class="deposit-menu-group">
|
<div class="deposit-menu-group">
|
||||||
<div class="deposit-group-title">Bank & Clearing</div>
|
<div class="deposit-group-title">Bank & Clearing</div>
|
||||||
<button mat-menu-item tabindex="0">
|
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/bank-reco">
|
||||||
<fa-icon icon="university" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
<fa-icon icon="university" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
||||||
Bank Reconciliation
|
Bank Reconciliation
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item tabindex="0">
|
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/api-bank-reco">
|
||||||
<fa-icon icon="code-branch" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
<fa-icon icon="code-branch" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
||||||
API Based Bank Reconciliation
|
API Based Bank Reconciliation
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item tabindex="0">
|
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/neft-rtgs">
|
||||||
<fa-icon icon="exchange" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
<fa-icon icon="exchange" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
||||||
NEFT / RTGS Reconciliation
|
NEFT / RTGS Reconciliation
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item tabindex="0">
|
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/clearing-reco">
|
||||||
<fa-icon icon="hand-holding-usd" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
<fa-icon icon="hand-holding-usd" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
||||||
Clearing Reconciliation
|
Clearing Reconciliation
|
||||||
</button>
|
</button>
|
||||||
@@ -1349,15 +1349,15 @@
|
|||||||
<!-- Digital / Payments -->
|
<!-- Digital / Payments -->
|
||||||
<div class="deposit-menu-group">
|
<div class="deposit-menu-group">
|
||||||
<div class="deposit-group-title">Digital & Payments</div>
|
<div class="deposit-group-title">Digital & Payments</div>
|
||||||
<button mat-menu-item tabindex="0">
|
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/udir">
|
||||||
<fa-icon icon="mobile-alt" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
<fa-icon icon="mobile-alt" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
||||||
UDIR (IMPS / UPI / ATM)
|
UDIR (IMPS / UPI / ATM)
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item tabindex="0">
|
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/three-way-digital">
|
||||||
<fa-icon icon="project-diagram" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
<fa-icon icon="project-diagram" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
||||||
Three-way Digital Reconciliation
|
Three-way Digital Reconciliation
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item tabindex="0">
|
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/nach-reco">
|
||||||
<fa-icon icon="sync-alt" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
<fa-icon icon="sync-alt" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
||||||
NACH Reconciliation
|
NACH Reconciliation
|
||||||
</button>
|
</button>
|
||||||
@@ -1366,11 +1366,11 @@
|
|||||||
<!-- Instruments & Members -->
|
<!-- Instruments & Members -->
|
||||||
<div class="deposit-menu-group">
|
<div class="deposit-menu-group">
|
||||||
<div class="deposit-group-title">Instruments & Members</div>
|
<div class="deposit-group-title">Instruments & Members</div>
|
||||||
<button mat-menu-item [routerLink]="['/sponsor-dd-reconciliation']" tabindex="0">
|
<button mat-menu-item [routerLink]="['/sponsor-dd-reconciliation']" tabindex="0" class="menu-item-live">
|
||||||
<fa-icon icon="money-check-alt" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
<fa-icon icon="money-check-alt" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
||||||
DD / Pay Order Reconciliation
|
DD / Pay Order Reconciliation
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item tabindex="0">
|
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/share-member">
|
||||||
<fa-icon icon="coins" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
<fa-icon icon="coins" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
||||||
Share Member Reconciliation
|
Share Member Reconciliation
|
||||||
</button>
|
</button>
|
||||||
@@ -1379,11 +1379,11 @@
|
|||||||
<!-- Compliance & Financials -->
|
<!-- Compliance & Financials -->
|
||||||
<div class="deposit-menu-group">
|
<div class="deposit-menu-group">
|
||||||
<div class="deposit-group-title">Compliance & Financials</div>
|
<div class="deposit-group-title">Compliance & Financials</div>
|
||||||
<button mat-menu-item tabindex="0">
|
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/invoice-payment">
|
||||||
<fa-icon icon="file-invoice" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
<fa-icon icon="file-invoice" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
||||||
Invoice Payment
|
Invoice Payment
|
||||||
</button>
|
</button>
|
||||||
<button mat-menu-item tabindex="0">
|
<button mat-menu-item tabindex="0" class="menu-item-live" routerLink="/reconciliation/tax-reco">
|
||||||
<fa-icon icon="percentage" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
<fa-icon icon="percentage" class="menu-item-icon" style="color: #0d9488"></fa-icon>
|
||||||
Tax Reconciliation
|
Tax Reconciliation
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -677,6 +677,26 @@ $gray-700: #334155;
|
|||||||
opacity: 0.7;
|
opacity: 0.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Marks a `mat-menu-item` as actually wired to a working screen/dialog (vs a still-unwired
|
||||||
|
// placeholder) — same "green dot" convention as .gl-link--live, positioned absolutely since
|
||||||
|
// mat-menu-item's internal layout isn't a plain flex row we control directly.
|
||||||
|
[mat-menu-item].menu-item-live {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
right: 14px;
|
||||||
|
top: 50%;
|
||||||
|
transform: translateY(-50%);
|
||||||
|
width: 6px;
|
||||||
|
height: 6px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: #16a34a;
|
||||||
|
box-shadow: 0 0 0 2px rgb(22 163 74 / 15%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ===== Nav Dropdown Menus =====
|
// ===== Nav Dropdown Menus =====
|
||||||
::ng-deep .nav-dropdown {
|
::ng-deep .nav-dropdown {
|
||||||
border-radius: 8px !important;
|
border-radius: 8px !important;
|
||||||
|
|||||||
@@ -0,0 +1,703 @@
|
|||||||
|
/**
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Dummy data layer for the "Reconciliation" mega-menu — 16 screens across 5 groups (Core &
|
||||||
|
// Books / Bank & Clearing / Digital & Payments / Instruments & Members / Compliance &
|
||||||
|
// Financials). Every reconciliation type shares one generic two-sided model (Side A vs Side B,
|
||||||
|
// difference, match status) so the UI, KPIs and "Mark as Reconciled" action are all driven by
|
||||||
|
// the same pure functions below — no report can duplicate/contradict another's numbers.
|
||||||
|
//
|
||||||
|
// Branches are the SAME 8 used by the NPA module (Branch 1/Branch 2 + 6 named branches) so a
|
||||||
|
// real bank demo never shows two different branch lists in two different menus.
|
||||||
|
|
||||||
|
import { BRANCHES, BranchMaster } from '../npa/npa-data';
|
||||||
|
|
||||||
|
export { BRANCHES };
|
||||||
|
export type { BranchMaster };
|
||||||
|
|
||||||
|
export type ReconGroupId = 'core' | 'bank' | 'digital' | 'instruments' | 'compliance';
|
||||||
|
export type ReconScope = 'BRANCH' | 'HEAD_OFFICE';
|
||||||
|
export type ReconStatus = 'MATCHED' | 'UNMATCHED' | 'PENDING' | 'RESOLVED';
|
||||||
|
|
||||||
|
export const GROUP_LABELS: Record<ReconGroupId, string> = {
|
||||||
|
core: 'Core & Books',
|
||||||
|
bank: 'Bank & Clearing',
|
||||||
|
digital: 'Digital & Payments',
|
||||||
|
instruments: 'Instruments & Members',
|
||||||
|
compliance: 'Compliance & Financials'
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ReconTypeMeta {
|
||||||
|
id: string;
|
||||||
|
group: ReconGroupId;
|
||||||
|
label: string;
|
||||||
|
/**
|
||||||
|
* BRANCH — the underlying activity is booked at a specific branch (vouchers, cheques,
|
||||||
|
* customer digital transactions, share members, fixed assets), so Head Office sees a branch
|
||||||
|
* selector + consolidated view and a branch login is locked to its own branch — same pattern
|
||||||
|
* as the NPA module.
|
||||||
|
* HEAD_OFFICE — the activity has no branch dimension in real life (bank-wide P&L/GL control,
|
||||||
|
* treasury's own bank accounts, statutory tax filing, vendor procurement, inter-branch
|
||||||
|
* consolidation) — only Head Office can open these; a branch login sees a restricted notice.
|
||||||
|
*/
|
||||||
|
scope: ReconScope;
|
||||||
|
sideALabel: string;
|
||||||
|
sideBLabel: string;
|
||||||
|
referenceLabel: string;
|
||||||
|
/** Only set for the one genuinely three-sided report (Three-way Digital Reconciliation). */
|
||||||
|
extraLabel?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RECON_TYPES: ReconTypeMeta[] = [
|
||||||
|
{
|
||||||
|
id: 'voucher',
|
||||||
|
group: 'core',
|
||||||
|
label: 'Voucher Reconciliation',
|
||||||
|
scope: 'BRANCH',
|
||||||
|
sideALabel: 'System Voucher Amount',
|
||||||
|
sideBLabel: 'Physical / Scanned Voucher Amount',
|
||||||
|
referenceLabel: 'Voucher No'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'sundry-dr-cr',
|
||||||
|
group: 'core',
|
||||||
|
label: 'Sundry Dr / Cr Reconciliation',
|
||||||
|
scope: 'BRANCH',
|
||||||
|
sideALabel: 'GL Balance',
|
||||||
|
sideBLabel: 'Party Ledger Balance',
|
||||||
|
referenceLabel: 'Party Code'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'pnl-impact',
|
||||||
|
group: 'core',
|
||||||
|
label: 'P&L Impact Reconciliation',
|
||||||
|
scope: 'HEAD_OFFICE',
|
||||||
|
sideALabel: 'P&L as per GL',
|
||||||
|
sideBLabel: 'P&L as per MIS Schedule',
|
||||||
|
referenceLabel: 'Income / Expense Head'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'gl-reconciliation',
|
||||||
|
group: 'core',
|
||||||
|
label: 'GL Reconciliation',
|
||||||
|
scope: 'HEAD_OFFICE',
|
||||||
|
sideALabel: 'Total Debit',
|
||||||
|
sideBLabel: 'Total Credit',
|
||||||
|
referenceLabel: 'GL Code'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'fixed-assets',
|
||||||
|
group: 'core',
|
||||||
|
label: 'Fixed Assets Reconciliation',
|
||||||
|
scope: 'BRANCH',
|
||||||
|
sideALabel: 'GL Asset Value (WDV)',
|
||||||
|
sideBLabel: 'Physical Verification Value',
|
||||||
|
referenceLabel: 'Asset Tag No'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'inter-company',
|
||||||
|
group: 'core',
|
||||||
|
label: 'Inter-Company Reconciliation',
|
||||||
|
scope: 'HEAD_OFFICE',
|
||||||
|
sideALabel: 'HO Books (Inter-Branch A/c)',
|
||||||
|
sideBLabel: 'Branch Books (HO A/c)',
|
||||||
|
referenceLabel: 'Branch'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'bank-reco',
|
||||||
|
group: 'bank',
|
||||||
|
label: 'Bank Reconciliation',
|
||||||
|
scope: 'HEAD_OFFICE',
|
||||||
|
sideALabel: 'Balance per Bank GL (Books)',
|
||||||
|
sideBLabel: 'Balance per Bank Statement',
|
||||||
|
referenceLabel: 'Bank Account'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'api-bank-reco',
|
||||||
|
group: 'bank',
|
||||||
|
label: 'API Based Bank Reconciliation',
|
||||||
|
scope: 'HEAD_OFFICE',
|
||||||
|
sideALabel: 'Core Banking Balance',
|
||||||
|
sideBLabel: 'API Feed Balance',
|
||||||
|
referenceLabel: 'Bank Account (API)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'neft-rtgs',
|
||||||
|
group: 'bank',
|
||||||
|
label: 'NEFT / RTGS Reconciliation',
|
||||||
|
scope: 'BRANCH',
|
||||||
|
sideALabel: 'Amount per CBS',
|
||||||
|
sideBLabel: 'Amount per RBI Settlement File',
|
||||||
|
referenceLabel: 'UTR No'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'clearing-reco',
|
||||||
|
group: 'bank',
|
||||||
|
label: 'Clearing Reconciliation',
|
||||||
|
scope: 'BRANCH',
|
||||||
|
sideALabel: 'Cheque Amount per CBS',
|
||||||
|
sideBLabel: 'Amount per Clearing House (CTS)',
|
||||||
|
referenceLabel: 'Cheque No'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'udir',
|
||||||
|
group: 'digital',
|
||||||
|
label: 'UDIR (IMPS / UPI / ATM)',
|
||||||
|
scope: 'BRANCH',
|
||||||
|
sideALabel: 'Disputed Amount (Customer Claim)',
|
||||||
|
sideBLabel: 'NPCI UDIR Resolution Amount',
|
||||||
|
referenceLabel: 'UDIR Ticket No'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'three-way-digital',
|
||||||
|
group: 'digital',
|
||||||
|
label: 'Three-way Digital Reconciliation',
|
||||||
|
scope: 'HEAD_OFFICE',
|
||||||
|
sideALabel: 'Core Banking Record',
|
||||||
|
sideBLabel: 'Settlement Bank Record',
|
||||||
|
extraLabel: 'Switch / NPCI Record',
|
||||||
|
referenceLabel: 'Transaction Ref (RRN)'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'nach-reco',
|
||||||
|
group: 'digital',
|
||||||
|
label: 'NACH Reconciliation',
|
||||||
|
scope: 'BRANCH',
|
||||||
|
sideALabel: 'Mandate Amount per CBS',
|
||||||
|
sideBLabel: 'Amount per NPCI Response File',
|
||||||
|
referenceLabel: 'UMRN'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'share-member',
|
||||||
|
group: 'instruments',
|
||||||
|
label: 'Share Member Reconciliation',
|
||||||
|
scope: 'BRANCH',
|
||||||
|
sideALabel: 'Share GL Balance',
|
||||||
|
sideBLabel: 'Member Share Ledger Total',
|
||||||
|
referenceLabel: 'Folio No'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'invoice-payment',
|
||||||
|
group: 'compliance',
|
||||||
|
label: 'Invoice Payment',
|
||||||
|
scope: 'HEAD_OFFICE',
|
||||||
|
sideALabel: 'Invoice Amount (Vendor Bill)',
|
||||||
|
sideBLabel: 'Amount Paid per GL',
|
||||||
|
referenceLabel: 'Invoice No'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'tax-reco',
|
||||||
|
group: 'compliance',
|
||||||
|
label: 'Tax Reconciliation',
|
||||||
|
scope: 'HEAD_OFFICE',
|
||||||
|
sideALabel: 'Deducted / Collected per Books',
|
||||||
|
sideBLabel: 'Remitted to Government (Challan)',
|
||||||
|
referenceLabel: 'Challan No'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
export const DEFAULT_RECON_ID = 'voucher';
|
||||||
|
|
||||||
|
export function reconMeta(id: string): ReconTypeMeta {
|
||||||
|
return RECON_TYPES.find((t) => t.id === id) || RECON_TYPES[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ReconItem {
|
||||||
|
id: string;
|
||||||
|
reconTypeId: string;
|
||||||
|
branchCode: string;
|
||||||
|
branchName: string;
|
||||||
|
groupLabel: string;
|
||||||
|
reference: string;
|
||||||
|
description: string;
|
||||||
|
date: string;
|
||||||
|
sideAAmount: number;
|
||||||
|
sideBAmount: number;
|
||||||
|
extraAmount?: number;
|
||||||
|
status: ReconStatus;
|
||||||
|
remarks: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function difference(item: ReconItem): number {
|
||||||
|
return +(item.sideAAmount - item.sideBAmount).toFixed(2);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isOpen(item: ReconItem): boolean {
|
||||||
|
return item.status === 'UNMATCHED' || item.status === 'PENDING';
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Date helpers (same shape as npa-data.ts, kept local so this module has no
|
||||||
|
// runtime dependency beyond the shared BRANCHES list)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
function fmt(d: Date): string {
|
||||||
|
return `${String(d.getDate()).padStart(2, '0')}-${String(d.getMonth() + 1).padStart(2, '0')}-${d.getFullYear()}`;
|
||||||
|
}
|
||||||
|
function addDays(d: Date, days: number): Date {
|
||||||
|
const r = new Date(d);
|
||||||
|
r.setDate(r.getDate() + days);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
export function todayDDMMYYYY(): string {
|
||||||
|
return fmt(new Date());
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// Seeded RNG — deterministic dataset across reloads (own seed, independent of npa-data.ts)
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
function mulberry32(seed: number): () => number {
|
||||||
|
return function random(): number {
|
||||||
|
seed |= 0;
|
||||||
|
seed = (seed + 0x6d2b79f5) | 0;
|
||||||
|
let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
|
||||||
|
t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
|
||||||
|
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const rng = mulberry32(20260213);
|
||||||
|
const pick = <T>(arr: T[]): T => arr[Math.floor(rng() * arr.length)];
|
||||||
|
const between = (min: number, max: number): number => min + rng() * (max - min);
|
||||||
|
const round = (n: number): number => Math.round(n);
|
||||||
|
|
||||||
|
const VENDOR_NAMES = [
|
||||||
|
'Om Stationery Suppliers',
|
||||||
|
'Shubham Printers',
|
||||||
|
'Shree Balaji AMC Services',
|
||||||
|
'Metro Security Services',
|
||||||
|
'Sai Computers & Networking',
|
||||||
|
'Deccan Furniture Works',
|
||||||
|
'Unity Housekeeping Services',
|
||||||
|
'Precision Fire & Safety',
|
||||||
|
'Konark Electricals',
|
||||||
|
'Vasant Stationery Mart'
|
||||||
|
];
|
||||||
|
const PARTY_NAMES = [
|
||||||
|
'Kiran Hardware',
|
||||||
|
'Vishal Electronics',
|
||||||
|
'Nilesh Electricals',
|
||||||
|
'Ramdas Plumbing Works',
|
||||||
|
'Anita Traders',
|
||||||
|
'Ganesh Enterprises',
|
||||||
|
'Om Textiles',
|
||||||
|
'Balaji Agro Traders',
|
||||||
|
'Sunil Auto Parts',
|
||||||
|
'Meera Fabricators'
|
||||||
|
];
|
||||||
|
const ASSET_NAMES = [
|
||||||
|
'Office Furniture Set',
|
||||||
|
'Desktop Computer (Branch Counter)',
|
||||||
|
'UPS 3 KVA',
|
||||||
|
'Currency Counting Machine',
|
||||||
|
'Air Conditioner 1.5 Ton',
|
||||||
|
'CCTV Camera System',
|
||||||
|
'Branch Generator 15 KVA',
|
||||||
|
'Server Rack Cabinet',
|
||||||
|
'Biometric Attendance Device',
|
||||||
|
'Two-Wheeler (Recovery Officer)'
|
||||||
|
];
|
||||||
|
const GL_HEADS = [
|
||||||
|
'Interest on Loans & Advances',
|
||||||
|
'Interest on Investments',
|
||||||
|
'Interest Paid on Deposits',
|
||||||
|
'Commission & Exchange',
|
||||||
|
'Locker Rent Income',
|
||||||
|
'Establishment Charges',
|
||||||
|
'Rent, Taxes & Lighting',
|
||||||
|
'Printing & Stationery',
|
||||||
|
'Depreciation on Fixed Assets',
|
||||||
|
'Bad Debts Written Off',
|
||||||
|
'Audit Fees',
|
||||||
|
'Sundry Income'
|
||||||
|
];
|
||||||
|
const BANK_ACCOUNTS = [
|
||||||
|
'SBI Current Account - 40012345678',
|
||||||
|
'HDFC Bank Current Account - 5020012345',
|
||||||
|
'ICICI Bank Current Account - 000305012345',
|
||||||
|
'Axis Bank Current Account - 91020012345',
|
||||||
|
'RBI Current Account (Banking Dept)'
|
||||||
|
];
|
||||||
|
|
||||||
|
interface GenOptions {
|
||||||
|
reconTypeId: string;
|
||||||
|
refPrefix: string;
|
||||||
|
refDigits: number;
|
||||||
|
descriptions: string[];
|
||||||
|
minAmt: number;
|
||||||
|
maxAmt: number;
|
||||||
|
unmatchedRate: number;
|
||||||
|
branchScoped: boolean;
|
||||||
|
countPerBranch?: number;
|
||||||
|
totalCount?: number;
|
||||||
|
today: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
function statusFor(unmatchedRate: number): ReconStatus {
|
||||||
|
const r = rng();
|
||||||
|
if (r < unmatchedRate * 0.6) {
|
||||||
|
return 'UNMATCHED';
|
||||||
|
}
|
||||||
|
if (r < unmatchedRate) {
|
||||||
|
return 'PENDING';
|
||||||
|
}
|
||||||
|
return 'MATCHED';
|
||||||
|
}
|
||||||
|
|
||||||
|
function amountsFor(status: ReconStatus, minAmt: number, maxAmt: number): { a: number; b: number } {
|
||||||
|
const a = round(between(minAmt, maxAmt));
|
||||||
|
if (status === 'MATCHED') {
|
||||||
|
return { a, b: a };
|
||||||
|
}
|
||||||
|
const gapPct = between(0.02, 0.35);
|
||||||
|
const sign = rng() < 0.5 ? 1 : -1;
|
||||||
|
const b = round(a * (1 - sign * gapPct));
|
||||||
|
return { a, b };
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateGeneric(opts: GenOptions): ReconItem[] {
|
||||||
|
const items: ReconItem[] = [];
|
||||||
|
const meta = reconMeta(opts.reconTypeId);
|
||||||
|
const branchLoop = opts.branchScoped ? BRANCHES : [{ code: 'HO', name: 'Head Office' }];
|
||||||
|
const perBranch = opts.countPerBranch ?? opts.totalCount ?? 20;
|
||||||
|
|
||||||
|
branchLoop.forEach((branch, branchIdx) => {
|
||||||
|
const count = opts.branchScoped ? perBranch : (opts.totalCount ?? perBranch);
|
||||||
|
for (let i = 0; i < count; i++) {
|
||||||
|
const status = statusFor(opts.unmatchedRate);
|
||||||
|
const { a, b } = amountsFor(status, opts.minAmt, opts.maxAmt);
|
||||||
|
const daysAgo = round(between(1, 75));
|
||||||
|
const seq = String(branchIdx * 1000 + i + 1).padStart(opts.refDigits, '0');
|
||||||
|
items.push({
|
||||||
|
id: `${opts.reconTypeId}-${branch.code}-${i}`,
|
||||||
|
reconTypeId: opts.reconTypeId,
|
||||||
|
branchCode: branch.code,
|
||||||
|
branchName: branch.name,
|
||||||
|
groupLabel: GROUP_LABELS[meta.group],
|
||||||
|
reference: `${opts.refPrefix}${seq}`,
|
||||||
|
description: pick(opts.descriptions),
|
||||||
|
date: fmt(addDays(opts.today, -daysAgo)),
|
||||||
|
sideAAmount: a,
|
||||||
|
sideBAmount: status === 'MATCHED' ? a : b,
|
||||||
|
status,
|
||||||
|
remarks: status === 'MATCHED' ? '' : ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateInterCompany(today: Date): ReconItem[] {
|
||||||
|
return BRANCHES.map((b): ReconItem => {
|
||||||
|
const status = statusFor(0.25);
|
||||||
|
const { a, b: bAmt } = amountsFor(status, 200000, 3500000);
|
||||||
|
return {
|
||||||
|
id: `inter-company-${b.code}`,
|
||||||
|
reconTypeId: 'inter-company',
|
||||||
|
branchCode: 'HO',
|
||||||
|
branchName: 'Head Office',
|
||||||
|
groupLabel: GROUP_LABELS.core,
|
||||||
|
reference: b.name,
|
||||||
|
description: `Inter-branch settlement account — ${b.name}`,
|
||||||
|
date: fmt(addDays(today, -round(between(1, 10)))),
|
||||||
|
sideAAmount: a,
|
||||||
|
sideBAmount: status === 'MATCHED' ? a : bAmt,
|
||||||
|
status,
|
||||||
|
remarks: ''
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateBankReco(reconTypeId: string, today: Date, refPrefix: string): ReconItem[] {
|
||||||
|
const templates = [
|
||||||
|
'Cheque issued, not yet presented',
|
||||||
|
'Cheque deposited, not yet credited by bank',
|
||||||
|
'Bank charges debited, not yet booked in CBS',
|
||||||
|
'Interest credited by bank, not yet booked in CBS',
|
||||||
|
'Direct credit by remitter, not yet identified',
|
||||||
|
'Standing instruction debited by bank, pending booking',
|
||||||
|
'RTGS outward debited by bank, in-transit in CBS'
|
||||||
|
];
|
||||||
|
const items: ReconItem[] = [];
|
||||||
|
BANK_ACCOUNTS.forEach((acct, idx) => {
|
||||||
|
const lineCount = reconTypeId === 'api-bank-reco' ? 5 : 6;
|
||||||
|
for (let i = 0; i < lineCount; i++) {
|
||||||
|
const status = statusFor(0.55);
|
||||||
|
const { a, b } = amountsFor(status, 15000, 850000);
|
||||||
|
items.push({
|
||||||
|
id: `${reconTypeId}-${idx}-${i}`,
|
||||||
|
reconTypeId,
|
||||||
|
branchCode: 'HO',
|
||||||
|
branchName: 'Head Office',
|
||||||
|
groupLabel: GROUP_LABELS.bank,
|
||||||
|
reference: `${refPrefix}${String(idx + 1).padStart(2, '0')}${String(i + 1).padStart(3, '0')}`,
|
||||||
|
description: `${acct} — ${pick(templates)}`,
|
||||||
|
date: fmt(addDays(today, -round(between(1, 30)))),
|
||||||
|
sideAAmount: a,
|
||||||
|
sideBAmount: status === 'MATCHED' ? a : b,
|
||||||
|
status,
|
||||||
|
remarks: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateThreeWayDigital(today: Date): ReconItem[] {
|
||||||
|
const channels = [
|
||||||
|
'IMPS',
|
||||||
|
'UPI',
|
||||||
|
'NEFT',
|
||||||
|
'RTGS',
|
||||||
|
'ATM (NFS)'
|
||||||
|
];
|
||||||
|
const items: ReconItem[] = [];
|
||||||
|
for (let i = 0; i < 45; i++) {
|
||||||
|
const status = statusFor(0.12);
|
||||||
|
const core = round(between(500, 250000));
|
||||||
|
const switchAmt = status === 'MATCHED' ? core : round(core * (1 - between(0.02, 0.2)));
|
||||||
|
const settlement = status === 'MATCHED' ? core : round(core * (1 - between(0.02, 0.15)));
|
||||||
|
items.push({
|
||||||
|
id: `three-way-digital-${i}`,
|
||||||
|
reconTypeId: 'three-way-digital',
|
||||||
|
branchCode: 'HO',
|
||||||
|
branchName: 'Head Office',
|
||||||
|
groupLabel: GROUP_LABELS.digital,
|
||||||
|
reference: `RRN${String(700000 + i)}`,
|
||||||
|
description: `${pick(channels)} transaction — Core vs Switch vs Settlement Bank`,
|
||||||
|
date: fmt(addDays(today, -round(between(1, 20)))),
|
||||||
|
sideAAmount: core,
|
||||||
|
sideBAmount: settlement,
|
||||||
|
extraAmount: switchAmt,
|
||||||
|
status,
|
||||||
|
remarks: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateAll(): ReconItem[] {
|
||||||
|
const today = new Date();
|
||||||
|
return [
|
||||||
|
...generateGeneric({
|
||||||
|
reconTypeId: 'voucher',
|
||||||
|
refPrefix: 'JV/26/',
|
||||||
|
refDigits: 5,
|
||||||
|
descriptions: [
|
||||||
|
'Cash deposit voucher',
|
||||||
|
'Cash withdrawal voucher',
|
||||||
|
'Journal entry — interest provision',
|
||||||
|
'Journal entry — expense booking',
|
||||||
|
'Transfer voucher — inter-GL',
|
||||||
|
'Debit voucher — charges recovery',
|
||||||
|
'Credit voucher — refund adjustment'
|
||||||
|
],
|
||||||
|
minAmt: 500,
|
||||||
|
maxAmt: 250000,
|
||||||
|
unmatchedRate: 0.08,
|
||||||
|
branchScoped: true,
|
||||||
|
countPerBranch: 15,
|
||||||
|
today
|
||||||
|
}),
|
||||||
|
...generateGeneric({
|
||||||
|
reconTypeId: 'sundry-dr-cr',
|
||||||
|
refPrefix: 'PTY-',
|
||||||
|
refDigits: 4,
|
||||||
|
descriptions: PARTY_NAMES,
|
||||||
|
minAmt: 5000,
|
||||||
|
maxAmt: 180000,
|
||||||
|
unmatchedRate: 0.18,
|
||||||
|
branchScoped: true,
|
||||||
|
countPerBranch: 10,
|
||||||
|
today
|
||||||
|
}),
|
||||||
|
...generateGeneric({
|
||||||
|
reconTypeId: 'pnl-impact',
|
||||||
|
refPrefix: 'GLH-',
|
||||||
|
refDigits: 3,
|
||||||
|
descriptions: GL_HEADS,
|
||||||
|
minAmt: 40000,
|
||||||
|
maxAmt: 4200000,
|
||||||
|
unmatchedRate: 0.15,
|
||||||
|
branchScoped: false,
|
||||||
|
totalCount: 24,
|
||||||
|
today
|
||||||
|
}),
|
||||||
|
...generateGeneric({
|
||||||
|
reconTypeId: 'gl-reconciliation',
|
||||||
|
refPrefix: 'GL-',
|
||||||
|
refDigits: 6,
|
||||||
|
descriptions: [
|
||||||
|
'Suspense account clearance check',
|
||||||
|
'Control account tally — deposits',
|
||||||
|
'Control account tally — advances',
|
||||||
|
'Inter-branch adjustment head',
|
||||||
|
'Sundry assets/liabilities head',
|
||||||
|
...GL_HEADS
|
||||||
|
],
|
||||||
|
minAmt: 20000,
|
||||||
|
maxAmt: 5200000,
|
||||||
|
unmatchedRate: 0.1,
|
||||||
|
branchScoped: false,
|
||||||
|
totalCount: 42,
|
||||||
|
today
|
||||||
|
}),
|
||||||
|
...generateGeneric({
|
||||||
|
reconTypeId: 'fixed-assets',
|
||||||
|
refPrefix: 'FA-',
|
||||||
|
refDigits: 4,
|
||||||
|
descriptions: ASSET_NAMES,
|
||||||
|
minAmt: 8000,
|
||||||
|
maxAmt: 650000,
|
||||||
|
unmatchedRate: 0.12,
|
||||||
|
branchScoped: true,
|
||||||
|
countPerBranch: 8,
|
||||||
|
today
|
||||||
|
}),
|
||||||
|
...generateInterCompany(today),
|
||||||
|
...generateBankReco('bank-reco', today, 'BRS-'),
|
||||||
|
...generateBankReco('api-bank-reco', today, 'API-'),
|
||||||
|
...generateGeneric({
|
||||||
|
reconTypeId: 'neft-rtgs',
|
||||||
|
refPrefix: 'UTR',
|
||||||
|
refDigits: 12,
|
||||||
|
descriptions: [
|
||||||
|
'RTGS outward — beneficiary bank settlement',
|
||||||
|
'RTGS inward — remitting bank settlement',
|
||||||
|
'NEFT outward — beneficiary bank settlement',
|
||||||
|
'NEFT inward — remitting bank settlement'
|
||||||
|
],
|
||||||
|
minAmt: 25000,
|
||||||
|
maxAmt: 1800000,
|
||||||
|
unmatchedRate: 0.06,
|
||||||
|
branchScoped: true,
|
||||||
|
countPerBranch: 12,
|
||||||
|
today
|
||||||
|
}),
|
||||||
|
...generateGeneric({
|
||||||
|
reconTypeId: 'clearing-reco',
|
||||||
|
refPrefix: 'CHQ-',
|
||||||
|
refDigits: 6,
|
||||||
|
descriptions: [
|
||||||
|
'Outward clearing — cheque sent',
|
||||||
|
'Inward clearing — cheque received',
|
||||||
|
'CTS grid settlement adjustment',
|
||||||
|
'Cheque return — insufficient funds'
|
||||||
|
],
|
||||||
|
minAmt: 10000,
|
||||||
|
maxAmt: 620000,
|
||||||
|
unmatchedRate: 0.09,
|
||||||
|
branchScoped: true,
|
||||||
|
countPerBranch: 10,
|
||||||
|
today
|
||||||
|
}),
|
||||||
|
...generateGeneric({
|
||||||
|
reconTypeId: 'udir',
|
||||||
|
refPrefix: 'UDIR-',
|
||||||
|
refDigits: 5,
|
||||||
|
descriptions: [
|
||||||
|
'IMPS failed but debited — customer dispute',
|
||||||
|
'UPI transaction disputed — merchant not credited',
|
||||||
|
'ATM cash-not-dispensed but account debited',
|
||||||
|
'UPI duplicate debit dispute'
|
||||||
|
],
|
||||||
|
minAmt: 500,
|
||||||
|
maxAmt: 45000,
|
||||||
|
unmatchedRate: 0.35,
|
||||||
|
branchScoped: true,
|
||||||
|
countPerBranch: 4,
|
||||||
|
today
|
||||||
|
}),
|
||||||
|
...generateThreeWayDigital(today),
|
||||||
|
...generateGeneric({
|
||||||
|
reconTypeId: 'nach-reco',
|
||||||
|
refPrefix: 'UMRN',
|
||||||
|
refDigits: 10,
|
||||||
|
descriptions: [
|
||||||
|
'NACH debit — loan EMI collection',
|
||||||
|
'NACH debit — gold loan EMI collection',
|
||||||
|
'NACH credit — salary/DBT disbursement',
|
||||||
|
'NACH mandate return — insufficient funds'
|
||||||
|
],
|
||||||
|
minAmt: 1500,
|
||||||
|
maxAmt: 85000,
|
||||||
|
unmatchedRate: 0.14,
|
||||||
|
branchScoped: true,
|
||||||
|
countPerBranch: 8,
|
||||||
|
today
|
||||||
|
}),
|
||||||
|
...generateGeneric({
|
||||||
|
reconTypeId: 'share-member',
|
||||||
|
refPrefix: 'FOL-',
|
||||||
|
refDigits: 5,
|
||||||
|
descriptions: [
|
||||||
|
'Regular member — share balance',
|
||||||
|
'Nominal member — share balance',
|
||||||
|
'Share transfer on account closure',
|
||||||
|
'Share application pending allotment'
|
||||||
|
],
|
||||||
|
minAmt: 500,
|
||||||
|
maxAmt: 25000,
|
||||||
|
unmatchedRate: 0.07,
|
||||||
|
branchScoped: true,
|
||||||
|
countPerBranch: 9,
|
||||||
|
today
|
||||||
|
}),
|
||||||
|
...generateGeneric({
|
||||||
|
reconTypeId: 'invoice-payment',
|
||||||
|
refPrefix: 'INV-',
|
||||||
|
refDigits: 5,
|
||||||
|
descriptions: VENDOR_NAMES,
|
||||||
|
minAmt: 3000,
|
||||||
|
maxAmt: 380000,
|
||||||
|
unmatchedRate: 0.13,
|
||||||
|
branchScoped: false,
|
||||||
|
totalCount: 30,
|
||||||
|
today
|
||||||
|
}),
|
||||||
|
...generateGeneric({
|
||||||
|
reconTypeId: 'tax-reco',
|
||||||
|
refPrefix: 'CHLN-',
|
||||||
|
refDigits: 4,
|
||||||
|
descriptions: [
|
||||||
|
'TDS on FD/RD interest payouts',
|
||||||
|
'TDS on salary payments',
|
||||||
|
'TDS on contractor/vendor payments',
|
||||||
|
'GST on service charges collected',
|
||||||
|
'GST on locker rent collected',
|
||||||
|
'TCS on cash transactions'
|
||||||
|
],
|
||||||
|
minAmt: 8000,
|
||||||
|
maxAmt: 620000,
|
||||||
|
unmatchedRate: 0.11,
|
||||||
|
branchScoped: false,
|
||||||
|
totalCount: 18,
|
||||||
|
today
|
||||||
|
})
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const RECON_ITEMS: ReconItem[] = generateAll();
|
||||||
|
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
// KPI helpers — always computed from the item list handed in, so a filtered
|
||||||
|
// (branch-scoped) view and the full book can never disagree on a total.
|
||||||
|
// -------------------------------------------------------------------------
|
||||||
|
export function totalSideA(items: ReconItem[]): number {
|
||||||
|
return items.reduce((s, i) => s + i.sideAAmount, 0);
|
||||||
|
}
|
||||||
|
export function totalSideB(items: ReconItem[]): number {
|
||||||
|
return items.reduce((s, i) => s + i.sideBAmount, 0);
|
||||||
|
}
|
||||||
|
export function totalDifference(items: ReconItem[]): number {
|
||||||
|
return items.reduce((s, i) => s + Math.abs(difference(i)), 0);
|
||||||
|
}
|
||||||
|
export function countByStatus(items: ReconItem[], status: ReconStatus): number {
|
||||||
|
return items.filter((i) => i.status === status).length;
|
||||||
|
}
|
||||||
|
export function matchRate(items: ReconItem[]): number {
|
||||||
|
return items.length ? (countByStatus(items, 'MATCHED') / items.length) * 100 : 100;
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
/**
|
||||||
|
* 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, inject, OnDestroy, OnInit } from '@angular/core';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { ActivatedRoute, Router, RouterModule } from '@angular/router';
|
||||||
|
import { Subscription } from 'rxjs';
|
||||||
|
import { AuthenticationService } from '../../core/authentication/authentication.service';
|
||||||
|
import {
|
||||||
|
RECON_TYPES,
|
||||||
|
RECON_ITEMS,
|
||||||
|
BRANCHES,
|
||||||
|
GROUP_LABELS,
|
||||||
|
DEFAULT_RECON_ID,
|
||||||
|
ReconGroupId,
|
||||||
|
ReconTypeMeta,
|
||||||
|
ReconItem,
|
||||||
|
ReconStatus,
|
||||||
|
reconMeta,
|
||||||
|
difference,
|
||||||
|
isOpen,
|
||||||
|
totalSideA,
|
||||||
|
totalSideB,
|
||||||
|
totalDifference,
|
||||||
|
countByStatus,
|
||||||
|
matchRate,
|
||||||
|
todayDDMMYYYY
|
||||||
|
} from '../reconciliation-data';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The "Reconciliation" mega-menu — 16 screens across 5 groups (Core & Books / Bank & Clearing /
|
||||||
|
* Digital & Payments / Instruments & Members / Compliance & Financials) — collapsed into one
|
||||||
|
* route-param-driven component, mirroring the NPA module's pattern. Every screen shares the same
|
||||||
|
* generic two-sided (Side A vs Side B) reconciliation model in reconciliation-data.ts, and the
|
||||||
|
* same Head-Office-vs-branch access split used by NPA and Cash/Clearing/Transfer: activity that
|
||||||
|
* genuinely happens at a branch (vouchers, cheques, digital transactions, share members, fixed
|
||||||
|
* assets) is branch-scoped; activity with no branch dimension in real life (bank-wide P&L/GL
|
||||||
|
* control, treasury's own bank accounts, statutory tax filing, vendor procurement, inter-branch
|
||||||
|
* consolidation) is Head-Office-only.
|
||||||
|
*/
|
||||||
|
@Component({
|
||||||
|
selector: 'mifosx-reconciliation-report',
|
||||||
|
templateUrl: './reconciliation-report.component.html',
|
||||||
|
styleUrls: [
|
||||||
|
'../../cashier/cashier-shared.scss',
|
||||||
|
'../../investment/investment-shared.scss',
|
||||||
|
'../../npa/npa-shared.scss',
|
||||||
|
'../reconciliation-shared.scss'
|
||||||
|
],
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
FormsModule,
|
||||||
|
RouterModule
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class ReconciliationReportComponent implements OnInit, OnDestroy {
|
||||||
|
readonly reportMeta = RECON_TYPES;
|
||||||
|
readonly groupLabels = GROUP_LABELS;
|
||||||
|
readonly branches = BRANCHES;
|
||||||
|
readonly groups: ReconGroupId[] = [
|
||||||
|
'core',
|
||||||
|
'bank',
|
||||||
|
'digital',
|
||||||
|
'instruments',
|
||||||
|
'compliance'
|
||||||
|
];
|
||||||
|
asOnDate = todayDDMMYYYY();
|
||||||
|
|
||||||
|
activeGroup: ReconGroupId = 'core';
|
||||||
|
activeReport = DEFAULT_RECON_ID;
|
||||||
|
|
||||||
|
isHeadOffice = false;
|
||||||
|
currentOfficeName = 'Branch Office';
|
||||||
|
private ownBranchCode: string | null = null;
|
||||||
|
|
||||||
|
/** '' = All Branches (Head Office consolidated view); otherwise a BranchMaster.code. */
|
||||||
|
branchFilter = '';
|
||||||
|
|
||||||
|
readonly pageSize = 20;
|
||||||
|
currentPage = 1;
|
||||||
|
|
||||||
|
remarksDraft: Record<string, string> = {};
|
||||||
|
|
||||||
|
toastMsg = '';
|
||||||
|
toastVisible = false;
|
||||||
|
private toastTimer: ReturnType<typeof setTimeout> | undefined;
|
||||||
|
private sub?: Subscription;
|
||||||
|
|
||||||
|
private readonly route = inject(ActivatedRoute);
|
||||||
|
private readonly router = inject(Router);
|
||||||
|
private readonly auth = inject(AuthenticationService);
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.sub = this.route.paramMap.subscribe((params) => {
|
||||||
|
const id = params.get('reportId') || DEFAULT_RECON_ID;
|
||||||
|
const meta = this.reportMeta.find((m) => m.id === id) || this.reportMeta[0];
|
||||||
|
this.activeReport = meta.id;
|
||||||
|
this.activeGroup = meta.group;
|
||||||
|
this.currentPage = 1;
|
||||||
|
});
|
||||||
|
|
||||||
|
const creds = this.auth.getCredentials();
|
||||||
|
this.isHeadOffice = this.auth.isHeadOfficeUser();
|
||||||
|
this.currentOfficeName = creds?.officeName || 'Branch Office';
|
||||||
|
if (this.isHeadOffice) {
|
||||||
|
this.branchFilter = '';
|
||||||
|
} else {
|
||||||
|
this.ownBranchCode =
|
||||||
|
this.branches.find((b) => this.currentOfficeName.toLowerCase().includes(b.name.toLowerCase()))?.code || null;
|
||||||
|
this.branchFilter = this.ownBranchCode || this.branches[0].code;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.sub?.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
goToReport(reportId: string): void {
|
||||||
|
this.router.navigate([
|
||||||
|
'/reconciliation',
|
||||||
|
reportId
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
groupOf(group: ReconGroupId): ReconTypeMeta[] {
|
||||||
|
return this.reportMeta.filter((m) => m.group === group);
|
||||||
|
}
|
||||||
|
|
||||||
|
setBranch(code: string): void {
|
||||||
|
this.branchFilter = code;
|
||||||
|
this.currentPage = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
get meta(): ReconTypeMeta {
|
||||||
|
return reconMeta(this.activeReport);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** True when a branch-level login has opened a Head-Office-only reconciliation type. */
|
||||||
|
get isRestricted(): boolean {
|
||||||
|
return this.meta.scope === 'HEAD_OFFICE' && !this.isHeadOffice;
|
||||||
|
}
|
||||||
|
|
||||||
|
get scopeName(): string {
|
||||||
|
if (this.meta.scope === 'HEAD_OFFICE') {
|
||||||
|
return 'Head Office';
|
||||||
|
}
|
||||||
|
if (!this.isHeadOffice) {
|
||||||
|
return this.currentOfficeName;
|
||||||
|
}
|
||||||
|
return this.branchFilter ? this.branches.find((b) => b.code === this.branchFilter)?.name || '' : 'All Branches';
|
||||||
|
}
|
||||||
|
|
||||||
|
get items(): ReconItem[] {
|
||||||
|
const all = RECON_ITEMS.filter((i) => i.reconTypeId === this.activeReport);
|
||||||
|
if (this.meta.scope !== 'BRANCH' || !this.branchFilter) {
|
||||||
|
return all;
|
||||||
|
}
|
||||||
|
return all.filter((i) => i.branchCode === this.branchFilter);
|
||||||
|
}
|
||||||
|
|
||||||
|
diff(item: ReconItem): number {
|
||||||
|
return difference(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
isItemOpen(item: ReconItem): boolean {
|
||||||
|
return isOpen(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
badgeClass(status: ReconStatus): string {
|
||||||
|
return `recon-badge recon-badge--${status.toLowerCase()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- KPIs (always over the full `items` list, never the paged slice) --------------------
|
||||||
|
get totalA(): number {
|
||||||
|
return totalSideA(this.items);
|
||||||
|
}
|
||||||
|
get totalB(): number {
|
||||||
|
return totalSideB(this.items);
|
||||||
|
}
|
||||||
|
get totalDiff(): number {
|
||||||
|
return totalDifference(this.items);
|
||||||
|
}
|
||||||
|
get matchedCount(): number {
|
||||||
|
return countByStatus(this.items, 'MATCHED');
|
||||||
|
}
|
||||||
|
get unmatchedCount(): number {
|
||||||
|
return countByStatus(this.items, 'UNMATCHED');
|
||||||
|
}
|
||||||
|
get pendingCount(): number {
|
||||||
|
return countByStatus(this.items, 'PENDING');
|
||||||
|
}
|
||||||
|
get resolvedCount(): number {
|
||||||
|
return countByStatus(this.items, 'RESOLVED');
|
||||||
|
}
|
||||||
|
get matchPct(): number {
|
||||||
|
return matchRate(this.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- Pagination --------------------
|
||||||
|
paged<T>(list: T[]): T[] {
|
||||||
|
const start = (this.currentPage - 1) * this.pageSize;
|
||||||
|
return list.slice(start, start + this.pageSize);
|
||||||
|
}
|
||||||
|
pageCount(list: unknown[]): number {
|
||||||
|
return Math.max(1, Math.ceil(list.length / this.pageSize));
|
||||||
|
}
|
||||||
|
rangeStart(list: unknown[]): number {
|
||||||
|
return list.length === 0 ? 0 : (this.currentPage - 1) * this.pageSize + 1;
|
||||||
|
}
|
||||||
|
rangeEnd(list: unknown[]): number {
|
||||||
|
return Math.min(this.currentPage * this.pageSize, list.length);
|
||||||
|
}
|
||||||
|
goToPage(page: number, list: unknown[]): void {
|
||||||
|
this.currentPage = Math.min(Math.max(1, page), this.pageCount(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
// -------------------- Actions --------------------
|
||||||
|
markReconciled(item: ReconItem): void {
|
||||||
|
const remark = (this.remarksDraft[item.id] || '').trim();
|
||||||
|
item.status = 'RESOLVED';
|
||||||
|
item.remarks = remark || 'Reconciled — difference verified and squared off.';
|
||||||
|
delete this.remarksDraft[item.id];
|
||||||
|
this.toast(`${this.meta.referenceLabel} ${item.reference} marked as reconciled.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
generate(): void {
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.toast(`${this.meta.label} refreshed as on ${this.asOnDate}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
private toast(msg: string): void {
|
||||||
|
this.toastMsg = msg;
|
||||||
|
this.toastVisible = true;
|
||||||
|
if (this.toastTimer) {
|
||||||
|
clearTimeout(this.toastTimer);
|
||||||
|
}
|
||||||
|
this.toastTimer = setTimeout(() => (this.toastVisible = false), 3200);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
/**
|
||||||
|
* 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 { NgModule } from '@angular/core';
|
||||||
|
import { Routes, RouterModule } from '@angular/router';
|
||||||
|
|
||||||
|
import { Route } from '../core/route/route.service';
|
||||||
|
import { ReconciliationReportComponent } from './reconciliation-report/reconciliation-report.component';
|
||||||
|
import { DEFAULT_RECON_ID } from './reconciliation-data';
|
||||||
|
|
||||||
|
const routes: Routes = [
|
||||||
|
Route.withShell([
|
||||||
|
{
|
||||||
|
path: 'reconciliation',
|
||||||
|
data: { title: 'Reconciliation', breadcrumb: 'Reconciliation' },
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
redirectTo: DEFAULT_RECON_ID,
|
||||||
|
pathMatch: 'full'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':reportId',
|
||||||
|
component: ReconciliationReportComponent,
|
||||||
|
data: { title: 'Reconciliation', breadcrumb: 'Reconciliation' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
])
|
||||||
|
];
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [RouterModule.forChild(routes)],
|
||||||
|
exports: [RouterModule]
|
||||||
|
})
|
||||||
|
export class ReconciliationRoutingModule {}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
/**
|
||||||
|
* 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/.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Reconciliation-status colouring layered on top of ../cashier/cashier-shared.scss (pg-*,
|
||||||
|
// cctr-*), ../investment/investment-shared.scss (inv-kpis, inv-form-*, inv-toast) and
|
||||||
|
// ../npa/npa-shared.scss (pg-pagination) — reused as-is since they're generic primitives.
|
||||||
|
|
||||||
|
.recon-badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
&--matched {
|
||||||
|
background: #f0fdf4;
|
||||||
|
color: #15803d;
|
||||||
|
border: 1px solid #bbf7d0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--unmatched {
|
||||||
|
background: #fef2f2;
|
||||||
|
color: #dc2626;
|
||||||
|
border: 1px solid #fecaca;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--pending {
|
||||||
|
background: #fefce8;
|
||||||
|
color: #a16207;
|
||||||
|
border: 1px solid #fde68a;
|
||||||
|
}
|
||||||
|
|
||||||
|
&--resolved {
|
||||||
|
background: #eff6ff;
|
||||||
|
color: #1d4ed8;
|
||||||
|
border: 1px solid #bfdbfe;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.recon-scope-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 3px 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
background: #f1f5f9;
|
||||||
|
color: #475569;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recon-action-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recon-remark-input {
|
||||||
|
height: 32px;
|
||||||
|
min-width: 160px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-family: inherit;
|
||||||
|
color: #0d1b2a;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recon-restricted {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 56px 24px;
|
||||||
|
text-align: center;
|
||||||
|
color: #64748b;
|
||||||
|
|
||||||
|
&__icon {
|
||||||
|
font-size: 28px;
|
||||||
|
color: #94a3b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
font-size: 15px;
|
||||||
|
font-weight: 700;
|
||||||
|
color: #334155;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__body {
|
||||||
|
max-width: 440px;
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
/**
|
||||||
|
* 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 { NgModule } from '@angular/core';
|
||||||
|
|
||||||
|
import { SharedModule } from '../shared/shared.module';
|
||||||
|
import { ReconciliationRoutingModule } from './reconciliation-routing.module';
|
||||||
|
import { ReconciliationReportComponent } from './reconciliation-report/reconciliation-report.component';
|
||||||
|
|
||||||
|
@NgModule({
|
||||||
|
imports: [
|
||||||
|
SharedModule,
|
||||||
|
ReconciliationRoutingModule,
|
||||||
|
ReconciliationReportComponent
|
||||||
|
]
|
||||||
|
})
|
||||||
|
export class ReconciliationModule {}
|
||||||
Reference in New Issue
Block a user