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
@@ -6,53 +6,73 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="container mat-elevation-z8">
<mat-card>
<form [formGroup]="activateSavingsAccountForm" (ngSubmit)="submit()">
<mat-card-content>
<div class="layout-column">
<mat-form-field class="flex-fill" (click)="activatedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Activated On Date' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="activatedOnDatePicker"
required
formControlName="activatedOnDate"
/>
<mat-datepicker-toggle matSuffix [for]="activatedOnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #activatedOnDatePicker></mat-datepicker>
@if (activateSavingsAccountForm.controls.activatedOnDate.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Activated On Date' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
<div class="act-savings">
<!-- Hero -->
<div class="act-savings__hero">
<div class="act-savings__hero-icon">
<fa-icon icon="check-circle"></fa-icon>
</div>
<div>
<h2 class="act-savings__title">Activate Savings Account</h2>
<p class="act-savings__sub">Set the activation date and opening balance to bring this account live.</p>
</div>
</div>
<mat-form-field class="flex-fill">
<mat-label>Opening Balance</mat-label>
<input matInput type="number" [min]="minRequiredOpeningBalance ?? 0" formControlName="openingBalance" />
<span matPrefix>₹&nbsp;</span>
@if (minRequiredOpeningBalance) {
<mat-hint>Minimum required: ₹{{ minRequiredOpeningBalance }}</mat-hint>
}
@if (activateSavingsAccountForm.controls.openingBalance.hasError('min')) {
<mat-error>Opening balance must be at least ₹{{ minRequiredOpeningBalance ?? 0 }}</mat-error>
}
</mat-form-field>
</div>
</mat-card-content>
<!-- Form card -->
<form class="act-savings__card" [formGroup]="activateSavingsAccountForm" (ngSubmit)="submit()">
<div class="act-savings__card-head">
<fa-icon icon="info-circle" class="act-savings__card-ico"></fa-icon>
<h3 class="act-savings__card-title">Activation Details</h3>
</div>
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
<button type="button" mat-raised-button [routerLink]="['../../transactions']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" [disabled]="!activateSavingsAccountForm.valid">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-card-actions>
</form>
</mat-card>
<div class="act-savings__card-body">
<mat-form-field appearance="outline" class="act-savings__field" (click)="activatedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Activated On Date' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="activatedOnDatePicker"
required
formControlName="activatedOnDate"
/>
<mat-datepicker-toggle matSuffix [for]="activatedOnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #activatedOnDatePicker></mat-datepicker>
@if (activateSavingsAccountForm.controls.activatedOnDate.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Activated On Date' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="act-savings__field">
<mat-label>Opening Balance</mat-label>
<input matInput type="number" [min]="minRequiredOpeningBalance ?? 0" formControlName="openingBalance" />
<span matPrefix class="act-savings__prefix">₹&nbsp;</span>
@if (minRequiredOpeningBalance) {
<mat-hint>Minimum required: ₹{{ minRequiredOpeningBalance }}</mat-hint>
}
@if (activateSavingsAccountForm.controls.openingBalance.hasError('min')) {
<mat-error>Opening balance must be at least ₹{{ minRequiredOpeningBalance ?? 0 }}</mat-error>
}
</mat-form-field>
</div>
<div class="act-savings__actions">
<button type="button" mat-stroked-button class="act-savings__cancel" [routerLink]="['../../transactions']">
<fa-icon icon="times" class="m-r-6"></fa-icon>
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button
mat-raised-button
type="submit"
class="act-savings__confirm"
[disabled]="!activateSavingsAccountForm.valid"
>
<fa-icon icon="check" class="m-r-6"></fa-icon>
{{ 'labels.buttons.Confirm' | translate }}
</button>
</div>
</form>
</div>
@@ -6,6 +6,190 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
.container {
max-width: 37rem;
$navy: #0d1b2a;
$blue: #1e88e5;
$blue-d: #1e40af;
$green: #10b981;
$green-d: #047857;
$border: #e2e8f0;
$soft: #f8fafc;
.act-savings {
max-width: 720px;
margin: 0 auto;
padding: 20px 24px;
/* ─── Hero ───────────────────────────────────────────────────────── */
&__hero {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 18px;
}
&__hero-icon {
width: 46px;
height: 46px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 12px;
background: linear-gradient(135deg, $green 0%, $green-d 100%);
color: #fff;
font-size: 18px;
box-shadow: 0 4px 14px -4px rgb(4 120 87 / 45%);
flex-shrink: 0;
}
&__title {
margin: 0;
font-size: 20px;
font-weight: 800;
color: $navy;
line-height: 1.15;
letter-spacing: 0.01em;
}
&__sub {
margin: 4px 0 0;
font-size: 13px;
color: #64748b;
max-width: 540px;
}
/* ─── Form card ─────────────────────────────────────────────────── */
&__card {
background: #fff;
border: 1px solid $border;
border-radius: 14px;
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
overflow: hidden;
}
&__card-head {
display: flex;
align-items: center;
gap: 10px;
padding: 13px 20px;
background: $soft;
border-bottom: 1px solid $border;
}
&__card-ico {
width: 26px;
height: 26px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 8px;
background: linear-gradient(135deg, $blue 0%, $blue-d 100%);
color: #fff;
font-size: 12px;
}
&__card-title {
margin: 0;
font-size: 13.5px;
font-weight: 700;
color: $navy;
letter-spacing: 0.01em;
}
&__card-body {
padding: 22px 22px 4px;
display: grid;
gap: 6px;
}
/* Form field tweaks — slightly more compact density. */
&__field {
width: 100%;
::ng-deep .mat-mdc-text-field-wrapper {
background: #fff;
border-radius: 10px;
}
::ng-deep .mat-mdc-form-field-infix {
min-height: 48px;
padding-top: 12px !important;
padding-bottom: 12px !important;
}
::ng-deep .mdc-notched-outline__leading,
::ng-deep .mdc-notched-outline__notch,
::ng-deep .mdc-notched-outline__trailing {
border-color: #cbd5e1 !important;
}
::ng-deep &.mat-focused {
.mdc-notched-outline__leading,
.mdc-notched-outline__notch,
.mdc-notched-outline__trailing {
border-color: $blue-d !important;
border-width: 2px !important;
}
}
}
&__prefix {
color: #64748b;
font-weight: 600;
margin-right: 4px;
}
/* ─── Action row ────────────────────────────────────────────────── */
&__actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 10px;
padding: 14px 20px 18px;
border-top: 1px solid #f1f5f9;
background: #fff;
button {
height: 42px !important;
border-radius: 10px !important;
font-weight: 600 !important;
padding: 0 22px !important;
letter-spacing: 0.01em;
}
}
&__cancel {
color: #475569 !important;
border-color: $border !important;
}
&__confirm {
background: linear-gradient(135deg, $green 0%, $green-d 100%) !important;
color: #fff !important;
box-shadow: 0 4px 12px -2px rgb(4 120 87 / 35%) !important;
&:disabled {
opacity: 0.55;
filter: grayscale(0.3);
background: linear-gradient(135deg, #9ca3af 0%, #6b7280 100%) !important;
box-shadow: none !important;
}
}
}
.m-r-6 {
margin-right: 6px;
}
@media (width <= 600px) {
.act-savings {
padding: 14px;
&__actions {
flex-direction: column-reverse;
button {
width: 100%;
}
}
}
}
@@ -11,6 +11,7 @@ import { Component, OnInit, Input, inject } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
import { Dates } from 'app/core/utils/dates';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
/** Custom Services */
import { SavingsService } from 'app/savings/savings.service';
@@ -19,13 +20,17 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
* Activate Savings Account Component
*
* Nexora redesign: gradient green hero + section card with form fields and
* a primary/secondary action row.
*/
@Component({
selector: 'mifosx-activate-savings-account',
templateUrl: './activate-savings-account.component.html',
styleUrls: ['./activate-savings-account.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS
...STANDALONE_SHARED_IMPORTS,
FaIconComponent
]
})
export class ActivateSavingsAccountComponent implements OnInit {