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,36 +6,92 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="container mat-elevation-z8">
<mat-card>
@if (transactionType.holdamount) {
<mat-card-title>{{ 'labels.heading.Hold Amount' | translate }}</mat-card-title>
}
@if (transactionType.blockaccount) {
<mat-card-title>{{ 'labels.heading.Block Savings Account' | translate }}</mat-card-title>
}
<div
class="msa"
[class.msa--hold]="transactionType.holdamount"
[class.msa--block-account]="transactionType.blockaccount"
[class.msa--block-deposit]="transactionType.blockdeposit"
[class.msa--block-withdrawal]="transactionType.blockwithdrawal"
>
<!-- Hero -->
<div class="msa__hero">
<button type="button" mat-stroked-button class="msa__back" [routerLink]="['../../transactions']">
<fa-icon icon="arrow-left"></fa-icon>
</button>
<form [formGroup]="manageSavingsAccountForm" (ngSubmit)="submit()">
<mat-card-content class="layout-row">
<mat-form-field class="flex-100">
<mat-label>{{ 'labels.inputs.Reason' | translate }}</mat-label>
<mat-select required formControlName="reasonForBlock">
@for (codeValue of reasonOptions; track codeValue) {
<mat-option [value]="codeValue.id">
{{ codeValue.name }}
</mat-option>
}
</mat-select>
@if (manageSavingsAccountForm.controls.reasonForBlock.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Reason' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
<div class="msa__hero-ico">
<fa-icon
[icon]="
transactionType.holdamount
? 'pause'
: transactionType.blockaccount
? 'lock'
: transactionType.blockdeposit
? 'lock'
: transactionType.blockwithdrawal
? 'lock'
: 'lock'
"
></fa-icon>
</div>
<div class="msa__hero-text">
<h2 class="msa__title">
@if (transactionType.holdamount) {
<mat-form-field class="flex-100" (click)="transactionDatePicker.open()">
{{ 'labels.heading.Hold Amount' | translate }}
} @else if (transactionType.blockaccount) {
Block Savings Account
} @else if (transactionType.blockdeposit) {
Block Deposits
} @else if (transactionType.blockwithdrawal) {
Block Withdrawals
}
</h2>
<p class="msa__sub">
@if (transactionType.holdamount) {
Place a hold on a portion of the available balance with a reason and amount.
} @else if (transactionType.blockaccount) {
Freezes all account activity. Select a reason to record the action in the audit log.
} @else if (transactionType.blockdeposit) {
Stops incoming credits to this account. Existing withdrawals continue to work.
} @else if (transactionType.blockwithdrawal) {
Stops outgoing debits from this account. Deposits continue to be accepted.
}
</p>
</div>
<span class="msa__hero-pill">
<fa-icon icon="exclamation-triangle"></fa-icon>
Action recorded
</span>
</div>
<!-- Form card -->
<form class="msa__card" [formGroup]="manageSavingsAccountForm" (ngSubmit)="submit()">
<div class="msa__card-head">
<fa-icon icon="info-circle" class="msa__card-ico"></fa-icon>
<h3 class="msa__card-title">Action Details</h3>
</div>
<div class="msa__card-body">
<mat-form-field appearance="outline" class="msa__field">
<mat-label>{{ 'labels.inputs.Reason' | translate }}</mat-label>
<mat-select required formControlName="reasonForBlock">
@for (codeValue of reasonOptions; track codeValue.id) {
<mat-option [value]="codeValue.id">{{ codeValue.name }}</mat-option>
}
</mat-select>
@if (manageSavingsAccountForm.controls.reasonForBlock.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Reason' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
@if (transactionType.holdamount) {
<div class="msa__field-row">
<mat-form-field appearance="outline" class="msa__field" (click)="transactionDatePicker.open()">
<mat-label>{{ 'labels.inputs.Transaction Date' | translate }}</mat-label>
<input
matInput
@@ -54,28 +110,28 @@
</mat-error>
}
</mat-form-field>
}
@if (transactionType.holdamount) {
<mifosx-input-amount
class="flex-100"
class="msa__field"
[currency]="currency"
[isRequired]="true"
[inputFormControl]="manageSavingsAccountForm.controls.transactionAmount"
[inputLabel]="'Transaction Amount'"
>
</mifosx-input-amount>
}
</mat-card-content>
</div>
}
</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]="!manageSavingsAccountForm.valid">
{{ 'labels.buttons.Confirm' | translate }}
</button>
</mat-card-actions>
</form>
</mat-card>
<div class="msa__actions">
<button type="button" mat-stroked-button class="msa__cancel" [routerLink]="['../../transactions']">
<fa-icon icon="times" class="m-r-6"></fa-icon>
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-flat-button type="submit" class="msa__confirm" [disabled]="!manageSavingsAccountForm.valid">
<fa-icon icon="check" class="m-r-6"></fa-icon>
{{ 'labels.buttons.Confirm' | translate }}
</button>
</div>
</form>
</div>
@@ -6,6 +6,305 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
.container {
width: 37rem;
/* Manage-Savings-Account screen — drives four flows: Hold Amount (blue),
Block Deposit (orange), Block Withdrawal (orange), Block Account (red).
Hero gradient + Confirm button tint shift per modifier class. */
$navy: #0d1b2a;
$slate: #475569;
$muted: #64748b;
$border: #e2e8f0;
$soft: #f8fafc;
$blue: #1e88e5;
$blue-d: #1e40af;
$amber: #d97706;
$amber-d: #b45309;
$red: #b91c1c;
$red-d: #7f1d1d;
.msa {
max-width: 720px;
margin: 0 auto;
padding: 14px 18px 22px;
display: flex;
flex-direction: column;
gap: 12px;
}
/* ─── Hero ─────────────────────────────────────────────────────────── */
.msa__hero {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 18px;
border-radius: 12px;
color: #fff;
position: relative;
overflow: hidden;
/* Default (Hold Amount) — calm blue. Modifier classes override below. */
background: linear-gradient(135deg, #0f172a 0%, $blue-d 45%, $blue 100%);
box-shadow: 0 6px 18px -10px rgb(30 64 175 / 50%);
&::before {
content: '';
position: absolute;
inset: 0;
background: radial-gradient(circle at top right, rgb(255 255 255 / 12%), transparent 55%);
pointer-events: none;
}
}
/* Hero gradient variants */
.msa--block-deposit .msa__hero,
.msa--block-withdrawal .msa__hero {
background: linear-gradient(135deg, #7c2d12 0%, $amber-d 45%, $amber 100%);
box-shadow: 0 6px 18px -10px rgb(180 83 9 / 50%);
}
.msa--block-account .msa__hero {
background: linear-gradient(135deg, $red-d 0%, $red 45%, #dc2626 100%);
box-shadow: 0 6px 18px -10px rgb(153 27 27 / 55%);
}
.msa__back {
height: 30px !important;
min-width: 30px !important;
width: 30px !important;
padding: 0 !important;
border-radius: 50% !important;
background: rgb(255 255 255 / 14%) !important;
color: #fff !important;
border: 1px solid rgb(255 255 255 / 28%) !important;
font-size: 12px !important;
flex-shrink: 0;
position: relative;
z-index: 1;
&:hover {
background: rgb(255 255 255 / 26%) !important;
}
}
.msa__hero-ico {
width: 36px;
height: 36px;
border-radius: 9px;
background: rgb(255 255 255 / 18%);
border: 1px solid rgb(255 255 255 / 28%);
display: inline-flex;
align-items: center;
justify-content: center;
font-size: 15px;
flex-shrink: 0;
position: relative;
z-index: 1;
}
.msa__hero-text {
flex: 1 1 auto;
min-width: 0;
position: relative;
z-index: 1;
}
.msa__title {
margin: 0;
font-size: 14.5px;
font-weight: 800;
letter-spacing: 0.01em;
line-height: 1.15;
}
.msa__sub {
margin: 2px 0 0;
font-size: 11.5px;
color: rgb(255 255 255 / 82%);
line-height: 1.4;
}
.msa__hero-pill {
display: inline-flex;
align-items: center;
gap: 5px;
height: 22px;
padding: 0 10px;
border-radius: 999px;
background: rgb(255 255 255 / 16%);
border: 1px solid rgb(255 255 255 / 30%);
font-size: 10.5px;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.07em;
color: #fff;
flex-shrink: 0;
position: relative;
z-index: 1;
fa-icon {
font-size: 10px;
opacity: 0.95;
}
}
/* ─── Form card ────────────────────────────────────────────────────── */
.msa__card {
background: #fff;
border: 1px solid $border;
border-radius: 12px;
box-shadow: 0 1px 3px rgb(15 23 42 / 4%);
overflow: hidden;
}
.msa__card-head {
display: flex;
align-items: center;
gap: 8px;
padding: 9px 14px;
background: $soft;
border-bottom: 1px solid $border;
}
.msa__card-ico {
width: 24px;
height: 24px;
display: inline-flex;
align-items: center;
justify-content: center;
border-radius: 7px;
background: linear-gradient(135deg, $blue 0%, $blue-d 100%);
color: #fff;
font-size: 11px;
}
.msa--block-deposit .msa__card-ico,
.msa--block-withdrawal .msa__card-ico {
background: linear-gradient(135deg, $amber 0%, $amber-d 100%);
}
.msa--block-account .msa__card-ico {
background: linear-gradient(135deg, $red 0%, $red-d 100%);
}
.msa__card-title {
margin: 0;
font-size: 12.5px;
font-weight: 700;
color: $navy;
letter-spacing: 0.01em;
}
.msa__card-body {
padding: 16px 18px 4px;
display: flex;
flex-direction: column;
gap: 4px;
}
.msa__field {
width: 100%;
::ng-deep .mat-mdc-text-field-wrapper {
background: #fff;
border-radius: 10px;
}
::ng-deep .mat-mdc-form-field-infix {
min-height: 46px;
padding-top: 11px !important;
padding-bottom: 11px !important;
}
::ng-deep .mdc-notched-outline__leading,
::ng-deep .mdc-notched-outline__notch,
::ng-deep .mdc-notched-outline__trailing {
border-color: #cbd5e1 !important;
}
}
.msa__field-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 10px;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
/* ─── Actions ─────────────────────────────────────────────────────── */
.msa__actions {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 12px 18px 14px;
border-top: 1px solid #f1f5f9;
button {
height: 38px !important;
border-radius: 9px !important;
font-weight: 700 !important;
padding: 0 18px !important;
letter-spacing: 0.01em;
}
}
.msa__cancel {
color: $slate !important;
border-color: $border !important;
}
.msa__confirm {
/* Default — blue (Hold Amount) */
background: linear-gradient(135deg, $blue 0%, $blue-d 100%) !important;
color: #fff !important;
box-shadow: 0 3px 10px -2px rgb(30 64 175 / 35%) !important;
&:disabled {
opacity: 0.55;
filter: grayscale(0.3);
background: linear-gradient(135deg, #9ca3af 0%, #6b7280 100%) !important;
box-shadow: none !important;
}
}
.msa--block-deposit .msa__confirm,
.msa--block-withdrawal .msa__confirm {
background: linear-gradient(135deg, $amber 0%, $amber-d 100%) !important;
box-shadow: 0 3px 10px -2px rgb(180 83 9 / 35%) !important;
&:disabled {
background: linear-gradient(135deg, #9ca3af 0%, #6b7280 100%) !important;
}
}
.msa--block-account .msa__confirm {
background: linear-gradient(135deg, $red 0%, $red-d 100%) !important;
box-shadow: 0 3px 10px -2px rgb(153 27 27 / 40%) !important;
&:disabled {
background: linear-gradient(135deg, #9ca3af 0%, #6b7280 100%) !important;
}
}
.m-r-6 {
margin-right: 6px;
}
@media (width <= 600px) {
.msa {
padding: 12px;
}
.msa__hero {
flex-wrap: wrap;
}
.msa__actions {
flex-direction: column-reverse;
button {
width: 100%;
}
}
}
@@ -14,8 +14,10 @@ import { SavingsService } from 'app/savings/savings.service';
import { SettingsService } from 'app/settings/settings.service';
import { Currency } from 'app/shared/models/general.model';
import { SystemService } from 'app/system/system.service';
import { MatCard, MatCardTitle, MatCardContent, MatCardActions } from '@angular/material/card';
import { MatDialog } from '@angular/material/dialog';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { InputAmountComponent } from '../../../shared/input-amount/input-amount.component';
import { SuccessDialogComponent } from 'app/shared/success-dialog/success-dialog.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
type TransactionCommandType = 'holdamount' | 'blockaccount' | 'blockdeposit' | 'blockwithdrawal';
@@ -33,7 +35,7 @@ interface TransactionType {
styleUrls: ['./manage-savings-account.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatCardTitle,
FaIconComponent,
InputAmountComponent
]
})
@@ -45,6 +47,7 @@ export class ManageSavingsAccountComponent implements OnInit {
private router = inject(Router);
private systemService = inject(SystemService);
private settingsService = inject(SettingsService);
private dialog = inject(MatDialog);
@Input() currency: Currency;
/** Minimum date allowed. */
@@ -170,9 +173,7 @@ export class ManageSavingsAccountComponent implements OnInit {
this.savingsService
.executeSavingsAccountTransactionsCommand(this.savingAccountId, command, payload)
.subscribe((response: any) => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
});
.subscribe(() => this.showSuccessAndExit());
} else {
payload = {
...this.manageSavingsAccountForm.value
@@ -186,9 +187,39 @@ export class ManageSavingsAccountComponent implements OnInit {
this.savingsService
.executeSavingsAccountCommand(this.savingAccountId, command, payload)
.subscribe((response: any) => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
});
.subscribe(() => this.showSuccessAndExit());
}
}
/**
* Shows the shared success pop-up with copy tuned to the current
* transactionType, then navigates back to the transactions tab when
* the cashier dismisses it.
*/
private showSuccessAndExit(): void {
let title = 'Action Completed';
let message = 'The action has been recorded on the account.';
if (this.transactionType.holdamount) {
title = 'Amount Held Successfully';
message = 'The selected amount has been placed on hold on this savings account.';
} else if (this.transactionType.blockaccount) {
title = 'Account Blocked Successfully';
message = 'All activity on this savings account has been frozen.';
} else if (this.transactionType.blockdeposit) {
title = 'Deposits Blocked Successfully';
message = 'Incoming credits to this savings account have been blocked.';
} else if (this.transactionType.blockwithdrawal) {
title = 'Withdrawals Blocked Successfully';
message = 'Outgoing debits from this savings account have been blocked.';
}
const ref = this.dialog.open(SuccessDialogComponent, {
width: '420px',
maxWidth: '92vw',
autoFocus: false,
data: { title, message, buttonLabel: 'Done' }
});
ref.afterClosed().subscribe(() => {
this.router.navigate(['../../transactions'], { relativeTo: this.route });
});
}
}