letter-mgmt (new module): - Notice page: overdue loan list with filters, KPI strip, bulk actions, call disposition logging (Promise-to-Pay, Not Reachable, etc.), Speed Post dispatch dialog with consignment + post-office tracking, PDF preview + download, call-contact dialog with primary / alt / family / nominee numbers and per-contact disposition history. - Certificates: Loan Interest, Share, Balance, Form 60 (no-PAN flow), Form 15G/H (auto age-based), Hypothecation, NOC - each with shared preview dialog (six tone variants) and jsPDF generator using a common letterhead / summary-grid / signature-seal helper. digital: - IMPS module: report (P2A/P2P/Aadhaar tabs) + entry form with MMID/mobile lookup, NPCI switch resolve-to-A/C+IFSC BANL flow, sender autocomplete, hide sender field for P2P/Aadhaar, charges + 18% GST + waive checkbox, per-sender today-only history dialog. - UPI report with app-mix tab and full transaction filtering. - ATM/POS/ECOM hub with three ticket cards routing to today/status/cycle. - NEFT/RTGS entry: sender-account autocomplete, charges + GST + waive, field reorder (sender before amount), NEFT cap / RTGS floor validation, History button opening per-sender today's transactions. - NEFT/RTGS report: horizontal-bar trend leaderboard replacing tall bars. shell + theme: - Header redesigned with IBM Plex Sans 12/500 typography, 11-column grid per row, top-accent hover indicator, bordered cells via grid-gap. - Live KPI ticker below menu (CASH, TXN TODAY, CLEARING IN/OUT, NPA, CASA, CD RATIO, RBI CIRC, REPO) with marquee scroll, pause-on-hover, brand-blue gradient, amber bottom strip, pulsing red LIVE chip. - Sidebar 'KYC Management' -> 'KYC & ReKYC Management'. - Letter Mgmt / certificate menu items wired with green icon hints for items that have a working page. access-control: - MFA card layout compacted; method header inlines back button + title. icons: - Register faTruck/TruckFast, faEllipsisV/H, faCommentDots. - Add IBM Plex Sans Google Fonts import to index.html. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
300 lines
9.9 KiB
HTML
300 lines
9.9 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="ml-dialog">
|
|
<!-- Header -->
|
|
<div class="ml-header">
|
|
<div class="ml-header-ic">
|
|
<fa-icon icon="history"></fa-icon>
|
|
</div>
|
|
<div class="ml-header-text">
|
|
<h2>MFA Activity Logs</h2>
|
|
<p>Enrolled factors and 2FA / MFA sign-in attempts</p>
|
|
</div>
|
|
<button type="button" class="ml-close" (click)="close()" matTooltip="Close">
|
|
<fa-icon icon="times"></fa-icon>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Tab strip -->
|
|
<div class="ml-tabs">
|
|
<button class="ml-tab" [class.active]="activeTab === 'enrollment'" (click)="setTab('enrollment')" type="button">
|
|
<fa-icon icon="cog"></fa-icon>
|
|
<span>Enrolment</span>
|
|
@if (enrollment.state === 'loaded') {
|
|
<span class="ml-tab-count">{{ enrollment.total }}</span>
|
|
}
|
|
</button>
|
|
<button class="ml-tab" [class.active]="activeTab === 'login'" (click)="setTab('login')" type="button">
|
|
<fa-icon icon="shield-alt"></fa-icon>
|
|
<span>2FA / MFA Login</span>
|
|
@if (login.state === 'loaded') {
|
|
<span class="ml-tab-count">{{ login.total }}</span>
|
|
}
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Filter bar — shared layout, status options switch per tab -->
|
|
<div class="ml-filters">
|
|
<div class="ml-filter-search">
|
|
<fa-icon icon="search" class="ml-filter-search-ic"></fa-icon>
|
|
<input
|
|
type="text"
|
|
[ngModel]="current.searchInput"
|
|
(ngModelChange)="onSearchInput(activeTab, $event)"
|
|
placeholder="Search username, destination, OTP reference…"
|
|
/>
|
|
</div>
|
|
|
|
<select
|
|
class="ml-filter-select"
|
|
[ngModel]="current.filters.factorType || ''"
|
|
(ngModelChange)="setFilter(activeTab, 'factorType', $event)"
|
|
matTooltip="Filter by factor type"
|
|
>
|
|
<option value="">All factors</option>
|
|
@for (f of factorTypes; track f) {
|
|
<option [value]="f">{{ factorLabel(f) }}</option>
|
|
}
|
|
</select>
|
|
|
|
<select
|
|
class="ml-filter-select"
|
|
[ngModel]="current.filters.status || ''"
|
|
(ngModelChange)="setFilter(activeTab, 'status', $event)"
|
|
matTooltip="Filter by status"
|
|
>
|
|
<option value="">All statuses</option>
|
|
@if (activeTab === 'enrollment') {
|
|
@for (s of enrollmentStatuses; track s) {
|
|
<option [value]="s">{{ s }}</option>
|
|
}
|
|
} @else {
|
|
@for (s of loginStatuses; track s) {
|
|
<option [value]="s">{{ s }}</option>
|
|
}
|
|
}
|
|
</select>
|
|
|
|
<div class="ml-filter-dates">
|
|
<label>
|
|
<span>From</span>
|
|
<input
|
|
type="date"
|
|
[ngModel]="current.filters.dateFrom || ''"
|
|
(ngModelChange)="setFilter(activeTab, 'dateFrom', $event)"
|
|
/>
|
|
</label>
|
|
<label>
|
|
<span>To</span>
|
|
<input
|
|
type="date"
|
|
[ngModel]="current.filters.dateTo || ''"
|
|
(ngModelChange)="setFilter(activeTab, 'dateTo', $event)"
|
|
/>
|
|
</label>
|
|
</div>
|
|
|
|
@if (hasActiveFilters(activeTab)) {
|
|
<button type="button" class="ml-filter-clear" (click)="clearFilters(activeTab)">
|
|
<fa-icon icon="times"></fa-icon> Clear
|
|
</button>
|
|
}
|
|
|
|
<button type="button" class="ml-filter-refresh" (click)="refresh(activeTab)" matTooltip="Refresh">
|
|
<fa-icon icon="redo"></fa-icon>
|
|
</button>
|
|
</div>
|
|
|
|
<!-- Body -->
|
|
<div class="ml-body">
|
|
<!-- Loading -->
|
|
@if (current.state === 'loading') {
|
|
<div class="ml-skeleton">
|
|
@for (i of skeletonRows(); track i) {
|
|
<div class="ml-skel-row">
|
|
<span class="ml-skel ml-skel--xs"></span>
|
|
<span class="ml-skel ml-skel--md"></span>
|
|
<span class="ml-skel ml-skel--sm"></span>
|
|
<span class="ml-skel ml-skel--lg"></span>
|
|
<span class="ml-skel ml-skel--sm"></span>
|
|
</div>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
<!-- Empty -->
|
|
@if (current.state === 'empty') {
|
|
<div class="ml-empty">
|
|
<div class="ml-empty-ic">
|
|
<fa-icon [icon]="activeTab === 'enrollment' ? 'clipboard-list' : 'shield-alt'"></fa-icon>
|
|
</div>
|
|
@if (activeTab === 'enrollment') {
|
|
<h3>No enrolled factors</h3>
|
|
<p>
|
|
Activity here will populate as factors are verified or removed. Try clearing filters if you're looking for
|
|
an older record.
|
|
</p>
|
|
} @else {
|
|
<h3>No sign-in attempts logged</h3>
|
|
<p>
|
|
2FA / MFA challenges raised at sign-in will appear here — successful verifications, failed codes, expired
|
|
sessions, and account locks.
|
|
</p>
|
|
}
|
|
@if (hasActiveFilters(activeTab)) {
|
|
<button type="button" class="ml-empty-btn" (click)="clearFilters(activeTab)">
|
|
<fa-icon icon="times"></fa-icon> Clear filters
|
|
</button>
|
|
} @else {
|
|
<button type="button" class="ml-empty-btn" (click)="refresh(activeTab)">
|
|
<fa-icon icon="redo"></fa-icon> Refresh
|
|
</button>
|
|
}
|
|
</div>
|
|
}
|
|
|
|
<!-- Error -->
|
|
@if (current.state === 'error') {
|
|
<div class="ml-error">
|
|
<fa-icon icon="exclamation-triangle"></fa-icon>
|
|
<div>
|
|
<strong>Couldn't load logs</strong>
|
|
<p>{{ current.error }}</p>
|
|
</div>
|
|
<button type="button" class="ml-error-btn" (click)="refresh(activeTab)">
|
|
<fa-icon icon="redo"></fa-icon> Retry
|
|
</button>
|
|
</div>
|
|
}
|
|
|
|
<!-- Loaded — Enrolment table -->
|
|
@if (current.state === 'loaded' && activeTab === 'enrollment') {
|
|
<div class="ml-table-wrap">
|
|
<table class="ml-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Event time</th>
|
|
<th>User</th>
|
|
<th>Factor</th>
|
|
<th>Destination</th>
|
|
<th>Status</th>
|
|
<th>Verified on</th>
|
|
<th>Message</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@for (row of enrollment.rows; track $index) {
|
|
<tr>
|
|
<td class="mono">{{ formatTs(row.eventOn) }}</td>
|
|
<td>
|
|
<div>{{ row.userFullName || row.username }}</div>
|
|
<div class="muted small mono">{{ row.username }}</div>
|
|
</td>
|
|
<td>
|
|
<span class="ml-factor-chip">
|
|
<fa-icon [icon]="factorIcon(row.factorType)"></fa-icon>
|
|
{{ factorLabel(row.factorType) }}
|
|
</span>
|
|
</td>
|
|
<td class="mono small">{{ row.destination || '—' }}</td>
|
|
<td>
|
|
<span class="ml-pill" [class]="'ml-pill--' + statusColor(row.status)">{{ row.status }}</span>
|
|
</td>
|
|
<td class="mono small">{{ formatTs(row.verifiedOn) }}</td>
|
|
<td class="small">{{ row.message || '—' }}</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
|
|
<!-- Loaded — Login table -->
|
|
@if (current.state === 'loaded' && activeTab === 'login') {
|
|
<div class="ml-table-wrap">
|
|
<table class="ml-table">
|
|
<thead>
|
|
<tr>
|
|
<th>Event time</th>
|
|
<th>User</th>
|
|
<th>Factor</th>
|
|
<th>Destination</th>
|
|
<th>Status</th>
|
|
<th class="num">Attempts</th>
|
|
<th>IP / Device</th>
|
|
<th>Message</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
@for (row of login.rows; track $index) {
|
|
<tr>
|
|
<td class="mono">{{ formatTs(row.eventOn) }}</td>
|
|
<td>
|
|
<div>{{ row.userFullName || row.username }}</div>
|
|
<div class="muted small mono">{{ row.username }}</div>
|
|
</td>
|
|
<td>
|
|
<span class="ml-factor-chip">
|
|
<fa-icon [icon]="factorIcon(row.factorType)"></fa-icon>
|
|
{{ factorLabel(row.factorType) }}
|
|
</span>
|
|
</td>
|
|
<td class="mono small">{{ row.destination || '—' }}</td>
|
|
<td>
|
|
<span class="ml-pill" [class]="'ml-pill--' + statusColor(row.status)">{{ row.status }}</span>
|
|
</td>
|
|
<td class="num">{{ row.attempts ?? '—' }}</td>
|
|
<td class="mono small">
|
|
{{ row.ipAddress || '—' }}
|
|
@if (row.userAgent) {
|
|
<div class="muted small">{{ row.userAgent }}</div>
|
|
}
|
|
</td>
|
|
<td class="small">{{ row.message || '—' }}</td>
|
|
</tr>
|
|
}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Pagination footer -->
|
|
@if (current.state === 'loaded' || (current.state === 'empty' && current.total > 0)) {
|
|
<div class="ml-pager">
|
|
<div class="ml-pager-info">{{ currentRangeLabel }}</div>
|
|
<div class="ml-pager-ctrls">
|
|
<label class="ml-pager-pagesize">
|
|
Rows
|
|
<select [ngModel]="current.pageSize" (ngModelChange)="changePageSize(+$event)">
|
|
<option [value]="10">10</option>
|
|
<option [value]="25">25</option>
|
|
<option [value]="50">50</option>
|
|
<option [value]="100">100</option>
|
|
</select>
|
|
</label>
|
|
<button type="button" class="ml-pager-btn" [disabled]="current.page === 0" (click)="prevPage()">
|
|
<fa-icon icon="chevron-left"></fa-icon> Prev
|
|
</button>
|
|
<span class="ml-pager-page">
|
|
Page <strong>{{ current.page + 1 }}</strong> of {{ currentTotalPages }}
|
|
</span>
|
|
<button
|
|
type="button"
|
|
class="ml-pager-btn"
|
|
[disabled]="current.page + 1 >= currentTotalPages"
|
|
(click)="nextPage()"
|
|
>
|
|
Next <fa-icon icon="chevron-right"></fa-icon>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|