feat: KYC re-verify UX, family/nominee tab gating, savings dialogs, cfcfrms shell

- KYC tab: fix Verify (PAN) and Send OTP (Aadhaar) being disabled in edit mode;
  unmask Aadhaar input and show on-file masked value as hint; resolve kycId
  from id/kycId/resourceId fallbacks.
- Client stepper/view: add familyAllowed / nomineeAllowed predicates +
  CanActivate route guards. Family tab hidden for non-individuals, Nominee
  tab hidden for non-proprietor entities. Family-members step accepts a
  nomineeOnly input for proprietorship clients. Add/Edit/Delete buttons
  inside family-members and nominee tabs gated by canAdd.
- cfcfrms: add cfcfrms-shell wrapper, restyle complaint/fraud/mule/escalate
  pages and dashboard.
- Savings: identity-verify and transaction-confirm dialogs, manage / activate
  / add-charge flows, account preview styling.
- Tasks, core shell, account-nominees, icons: minor style and behaviour
  updates.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-05-21 18:49:43 +05:30
co-authored by Claude Opus 4.7
parent bf3e26a0d0
commit b149fef72e
99 changed files with 8575 additions and 2574 deletions
@@ -7,40 +7,45 @@
-->
<div class="cv-tab-content">
<div class="cv-action-bar">
<mat-form-field appearance="outline" class="cv-search-field">
<mat-icon matPrefix>search</mat-icon>
<input
matInput
placeholder="{{ 'labels.text.Search by user' | translate }}"
(keyup)="applyFilter($event.target.value)"
/>
</mat-form-field>
<button
mat-icon-button
(click)="toggleAdvancedSearch()"
class="cv-toggle-btn"
[attr.aria-label]="
showAdvancedSearch
? ('labels.buttons.Hide advanced search' | translate)
: ('labels.buttons.Show advanced search' | translate)
"
[attr.aria-expanded]="showAdvancedSearch"
>
<mat-icon>{{ showAdvancedSearch ? 'arrow_drop_up' : 'arrow_drop_down' }}</mat-icon>
</button>
<div class="cv-action-buttons">
<button mat-stroked-button class="cv-btn cv-btn-approve" (click)="approveChecker()">
<fa-icon icon="check" class="m-r-10"></fa-icon>{{ 'labels.buttons.Approve' | translate }}
</button>
<button mat-stroked-button class="cv-btn cv-btn-delete" (click)="deleteChecker()">
<fa-icon icon="trash" class="m-r-10"></fa-icon>{{ 'labels.buttons.Delete' | translate }}
</button>
<button mat-stroked-button class="cv-btn cv-btn-reject" (click)="rejectChecker()">
<fa-icon icon="times" class="m-r-10"></fa-icon>{{ 'labels.buttons.Reject' | translate }}
<!-- Action bar (search + advanced toggle + Approve/Delete/Reject) — only
rendered when there's actual data to work with. An empty inbox shows
the friendly empty-state block below without these controls. -->
@if (checkerData) {
<div class="cv-action-bar">
<mat-form-field appearance="outline" class="cv-search-field">
<mat-icon matPrefix>search</mat-icon>
<input
matInput
placeholder="{{ 'labels.text.Search by user' | translate }}"
(keyup)="applyFilter($event.target.value)"
/>
</mat-form-field>
<button
mat-icon-button
(click)="toggleAdvancedSearch()"
class="cv-toggle-btn"
[attr.aria-label]="
showAdvancedSearch
? ('labels.buttons.Hide advanced search' | translate)
: ('labels.buttons.Show advanced search' | translate)
"
[attr.aria-expanded]="showAdvancedSearch"
>
<mat-icon>{{ showAdvancedSearch ? 'arrow_drop_up' : 'arrow_drop_down' }}</mat-icon>
</button>
<div class="cv-action-buttons">
<button mat-stroked-button class="cv-btn cv-btn-approve" (click)="approveChecker()">
<fa-icon icon="check" class="m-r-10"></fa-icon>{{ 'labels.buttons.Approve' | translate }}
</button>
<button mat-stroked-button class="cv-btn cv-btn-delete" (click)="deleteChecker()">
<fa-icon icon="trash" class="m-r-10"></fa-icon>{{ 'labels.buttons.Delete' | translate }}
</button>
<button mat-stroked-button class="cv-btn cv-btn-reject" (click)="rejectChecker()">
<fa-icon icon="times" class="m-r-10"></fa-icon>{{ 'labels.buttons.Reject' | translate }}
</button>
</div>
</div>
</div>
}
@if (showAdvancedSearch) {
<form [formGroup]="makerCheckerSearchForm">
@@ -161,16 +166,20 @@
}
@if (checkerData && noSearchedData) {
<div class="empty-state">
<div class="empty-state empty-state--filter">
<i class="fas fa-inbox empty-icon"></i>
<p class="empty-text">{{ 'labels.text.No checker inbox data available for this search' | translate }}</p>
<p class="empty-title">No matches</p>
<p class="empty-text">No checker inbox items match the current search. Try clearing the filters.</p>
</div>
}
@if (!checkerData) {
<div class="empty-state">
<i class="fas fa-inbox empty-icon"></i>
<p class="empty-text">{{ 'labels.text.No checker inbox data available for this account' | translate }}</p>
<p class="empty-title">Inbox is empty</p>
<p class="empty-text">
Currently no items in the Checker Inbox. New maker actions awaiting review will appear here.
</p>
</div>
}
</div>
@@ -210,3 +210,44 @@ $text-muted: #64748b;
grid-template-columns: 1fr;
}
}
/* Empty state — shown when the inbox has no items at all (search/action bar
is hidden in that case) or when the current search has no matches. */
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 24px;
background: #fff;
border: 1px dashed #e2e8f0;
border-radius: 14px;
text-align: center;
margin: 16px 0;
.empty-icon {
font-size: 42px;
color: #cbd5e1;
margin-bottom: 14px;
}
.empty-title {
margin: 0 0 4px;
font-size: 15px;
font-weight: 700;
color: #0d1b2a;
}
.empty-text {
margin: 0;
font-size: 12.5px;
color: #64748b;
max-width: 380px;
line-height: 1.5;
}
&--filter {
background: #f8fafc;
border-style: solid;
}
}
@@ -6,66 +6,81 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="container">
<div class="table-header">
<div class="table-title-group">
<h2 class="table-title">
<span class="t-logomark"><span></span><span></span><span></span><span></span></span>
{{ 'labels.heading.Pending Tasks' | translate }}
</h2>
<div class="ac-container">
<!-- Hero header -->
<div class="ac-header">
<div class="ac-header__left">
<fa-icon icon="check-double" class="ac-header__icon"></fa-icon>
<div>
<h2 class="ac-header__title">Approval Center</h2>
<div class="ac-header__sub">
Review pending checker actions, client &amp; loan approvals, disbursals, and reschedules.
</div>
</div>
</div>
</div>
<nav mat-tab-nav-bar class="cv-tab-nav" [tabPanel]="tabPanel">
<!-- Pill-style tab navigation -->
<nav mat-tab-nav-bar class="ac-tabs" [tabPanel]="tabPanel">
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./checker-inbox']"
routerLinkActive
#checkerInbox="routerLinkActive"
[active]="checkerInbox.isActive"
*mifosxHasPermission="'ALL_FUNCTIONS_READ'"
>
<i class="fas fa-inbox cv-tab-icon"></i> {{ 'labels.inputs.Checker Inbox' | translate }}
<fa-icon icon="inbox" class="ac-tab__ic"></fa-icon>
<span>Checker Inbox</span>
</a>
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./client-approval']"
routerLinkActive
#clientApproval="routerLinkActive"
[active]="clientApproval.isActive"
*mifosxHasPermission="'READ_CLIENT'"
>
<i class="fas fa-user-check cv-tab-icon"></i> {{ 'labels.inputs.Client Approval' | translate }}
<fa-icon icon="user-check" class="ac-tab__ic"></fa-icon>
<span>Client Approval</span>
</a>
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./loan-approval']"
routerLinkActive
#loanApproval="routerLinkActive"
[active]="loanApproval.isActive"
*mifosxHasPermission="'READ_LOAN'"
>
<i class="fas fa-hand-holding-usd cv-tab-icon"></i> {{ 'labels.inputs.Loan Approval' | translate }}
<fa-icon icon="hand-holding-usd" class="ac-tab__ic"></fa-icon>
<span>Loan Approval</span>
</a>
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./loan-disbursal']"
routerLinkActive
#loanDisbursal="routerLinkActive"
[active]="loanDisbursal.isActive"
*mifosxHasPermission="'READ_LOAN'"
>
<i class="fas fa-money-bill-wave cv-tab-icon"></i> {{ 'labels.inputs.Loan Disbursal' | translate }}
<fa-icon icon="money-bill-wave" class="ac-tab__ic"></fa-icon>
<span>Loan Disbursal</span>
</a>
<a
mat-tab-link
class="ac-tab"
[routerLink]="['./reschedule-loan']"
routerLinkActive
#rescheduleLoan="routerLinkActive"
[active]="rescheduleLoan.isActive"
*mifosxHasPermission="'RESCHEDULE_LOAN'"
>
<i class="fas fa-calendar-alt cv-tab-icon"></i> {{ 'labels.inputs.Reschedule Loan' | translate }}
<fa-icon icon="calendar-alt" class="ac-tab__ic"></fa-icon>
<span>Reschedule Loan</span>
</a>
</nav>
@@ -8,24 +8,151 @@
@use '../../shared/styles/list-layout' as *;
.cv-tab-icon {
margin-right: 5px;
font-size: 12px;
opacity: 0.7;
$navy: #0d1b2a;
$blue: #1e88e5;
$blue-d: #1e40af;
$purple: #7c3aed;
$green: #10b981;
$amber: #f59e0b;
.ac-container {
padding: 20px 24px;
max-width: 1440px;
margin: 0 auto;
}
.cv-tab-nav {
.ac-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
gap: 12px;
flex-wrap: wrap;
&__left {
display: flex;
align-items: center;
gap: 12px;
}
&__icon {
width: 44px;
height: 44px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 12px;
background: linear-gradient(135deg, $blue 0%, $purple 100%);
color: #fff;
font-size: 18px;
box-shadow: 0 2px 8px rgb(29 78 216 / 25%);
}
&__title {
margin: 0;
font-size: 20px;
font-weight: 800;
color: $navy;
line-height: 1.1;
}
&__sub {
margin-top: 2px;
font-size: 12.5px;
color: #64748b;
}
}
/* ───────── Pill-style tab navigation ───────── */
.ac-tabs {
margin-bottom: 20px;
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 14px;
padding: 6px;
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
overflow: hidden;
::ng-deep {
.mat-mdc-tab-link {
font-family: 'DM Sans', Roboto, sans-serif;
font-size: 13px;
font-weight: 500;
letter-spacing: 0.02em;
min-width: 0;
padding: 0 16px;
height: 40px;
.mat-mdc-tab-link-container {
border-bottom: none !important;
}
.mat-mdc-tab-links {
gap: 4px;
}
/* Material draws the active-tab underline indicator with a 2px height —
hide it; our pill background already signals the active tab. */
.mdc-tab-indicator__content--underline,
.mat-mdc-tab-header-pagination-controls-enabled .mdc-tab-indicator__content--underline {
display: none !important;
}
}
}
.ac-tab {
display: inline-flex !important;
align-items: center;
gap: 8px;
height: 38px !important;
min-width: 0 !important;
padding: 0 18px !important;
border-radius: 10px !important;
font-family: Inter, 'DM Sans', Roboto, sans-serif !important;
font-size: 13px !important;
font-weight: 600 !important;
letter-spacing: 0.01em !important;
color: #64748b !important;
background: transparent !important;
transition: all 0.15s ease;
white-space: nowrap;
border: 1px solid transparent !important;
opacity: 1 !important;
&__ic {
font-size: 13px;
color: inherit;
}
&:hover:not(.mdc-tab--active) {
background: #f1f5f9 !important;
color: $navy !important;
}
/* Material 3 adds `.mdc-tab--active` (and `.mat-mdc-tab-link-active`) to the
selected link. Both target the same pill state — bold blue gradient. */
&.mdc-tab--active,
&.mat-mdc-tab-link-active {
background: linear-gradient(135deg, $blue 0%, $blue-d 100%) !important;
color: #fff !important;
box-shadow: 0 2px 6px -1px rgb(30 64 175 / 35%);
border-color: $blue-d !important;
/* Material 3 wraps the label in its own span with a color rule that
beats the parent. Force white on every descendant so the active-tab
label is always legible against the dark gradient pill. */
::ng-deep .mdc-tab__text-label,
::ng-deep .mat-mdc-tab-link-content,
::ng-deep span,
.ac-tab__ic {
color: #fff !important;
opacity: 1 !important;
}
}
}
@media (width <= 768px) {
.ac-tabs {
overflow-x: auto;
::ng-deep .mat-mdc-tab-links {
flex-wrap: nowrap;
}
}
.ac-tab {
padding: 0 14px !important;
font-size: 12px !important;
}
}
@@ -10,10 +10,11 @@
import { Component } from '@angular/core';
import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs';
import { RouterLinkActive, RouterLink, RouterOutlet } from '@angular/router';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Checker Inbox and Tasks Component
* Approval Center — checker inbox + client/loan approval workflows.
*/
@Component({
selector: 'mifosx-checker-inbox-and-tasks',
@@ -25,7 +26,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatTabLink,
RouterLinkActive,
MatTabNavPanel,
RouterOutlet
RouterOutlet,
FaIconComponent
]
})
export class CheckerInboxAndTasksComponent {