Files
cbsnexora/src/app/access-control/mfa-logs-dialog/mfa-logs-dialog.component.html
T
AdityaandClaude Opus 4.7 325228c514 feat: letter-mgmt module + digital UPI/IMPS/ATM-POS reports + header redesign
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>
2026-05-19 10:09:06 +05:30

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>