Adds a shared Export to Excel utility (investment-export.ts) and wires it into every listing/report screen in the Investment module — Dashboard (break-up + allocation), Reports (all ~20 report shapes), Interest (per active tab), the FD/Bond/Share Holdings registers, and Masters (per active master tab). Entry/transaction forms intentionally have no export button. No sign-off is included here, matching the request to keep that feature reconciliation-only. Also fixes the Reconciliation export flow: the Prepared/Checked/Approved sign-off is now an optional one-off popup shown when "Export to Excel" is clicked (fields are blank by default and downloading works whether or not they're filled), replacing the earlier always-visible sign-off card. Also fixes three reconciliation reports (Sundry Dr/Cr, NEFT/RTGS, UDIR) whose labels contain "/" — an Excel-forbidden worksheet-name character — which was silently aborting their export.
272 lines
10 KiB
HTML
272 lines
10 KiB
HTML
<!--
|
||
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>
|
||
@if (!isRestricted) {
|
||
<button type="button" class="pg-btn pg-btn--ghost" [disabled]="generating" (click)="openExportModal()">
|
||
{{ generating ? 'Exporting…' : 'Export to Excel' }}
|
||
</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>
|
||
|
||
@if (showExportModal) {
|
||
<div class="recon-modal-backdrop" (click)="closeExportModal()">
|
||
<div class="recon-modal" (click)="$event.stopPropagation()">
|
||
<div class="recon-modal__header">
|
||
<span>Export to Excel — Sign-off (Optional)</span>
|
||
<button type="button" class="recon-modal__close" (click)="closeExportModal()" aria-label="Close">
|
||
×
|
||
</button>
|
||
</div>
|
||
<p class="pg-sub" style="padding: 0 20px">
|
||
Fill in Prepared / Checked / Approved details if this copy needs them for the record — or leave them blank and
|
||
download straight away.
|
||
</p>
|
||
<div class="recon-signoff-grid">
|
||
<div class="recon-signoff-col">
|
||
<span class="recon-signoff-role">Prepared By</span>
|
||
<input type="text" placeholder="Name" [(ngModel)]="exportSignOff.preparedByName" />
|
||
<input type="text" placeholder="Designation" [(ngModel)]="exportSignOff.preparedByDesg" />
|
||
</div>
|
||
<div class="recon-signoff-col">
|
||
<span class="recon-signoff-role">Checked By</span>
|
||
<input type="text" placeholder="Name" [(ngModel)]="exportSignOff.checkedByName" />
|
||
<input type="text" placeholder="Designation" [(ngModel)]="exportSignOff.checkedByDesg" />
|
||
</div>
|
||
<div class="recon-signoff-col">
|
||
<span class="recon-signoff-role">Approved By</span>
|
||
<input type="text" placeholder="Name" [(ngModel)]="exportSignOff.approvedByName" />
|
||
<input type="text" placeholder="Designation" [(ngModel)]="exportSignOff.approvedByDesg" />
|
||
</div>
|
||
</div>
|
||
<div class="inv-form-actions" style="padding: 0 20px 20px">
|
||
<button type="button" class="pg-btn pg-btn--ghost" (click)="closeExportModal()">Cancel</button>
|
||
<button type="button" class="pg-btn pg-btn--primary" (click)="confirmExport()">Download</button>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<div class="inv-toast" [class.show]="toastVisible">{{ toastMsg }}</div>
|