saving account,current account and rd account modify based on new update

This commit is contained in:
Nitesh Yadav
2026-04-27 10:55:03 +05:30
parent 5fb25edca5
commit e4516934a7
36 changed files with 4302 additions and 663 deletions
@@ -96,6 +96,9 @@
[currentAccountSettingsForm]="currentAccountSettingsForm"
[currentAccountTermsForm]="currentAccountTermsForm"
[currentAccount]="currentAccount"
[currentAccountDigital]="currentAccountDigital"
[currentAccountNominee]="currentAccountNominee"
[currentAccountCurrencyFlags]="currentAccountCurrencyFlags"
(submitEvent)="submit()"
>
</mifosx-current-account-preview-step>
@@ -66,14 +66,15 @@ export class CreateCurrentAccountComponent {
/** Current Account Product Template */
currentAccountProductTemplate: any;
/** Current Account Details Step */
@ViewChild(CurrentAccountDetailsStepComponent, { static: true })
currentAccountDetailsStep: CurrentAccountDetailsStepComponent;
/** Current Account Currency Step */
@ViewChild(CurrentAccountCurrencyStepComponent, { static: true })
@ViewChild(CurrentAccountCurrencyStepComponent)
currentAccountCurrencyStep: CurrentAccountCurrencyStepComponent;
@ViewChild(CurrentAccountDigitalStepComponent)
currentAccountDigitalStep: CurrentAccountDigitalStepComponent;
constructor() {
this.route.data.subscribe((data: { currentAccountTemplate: any }) => {
this.currentAccountTemplate = data.currentAccountTemplate;
@@ -107,12 +108,23 @@ export class CreateCurrentAccountComponent {
return new FormGroup({});
}
/**
* Retrieves current account object.
*/
get currentAccount() {
return { ...this.currentAccountDetailsStep.currentAccountDetails };
}
get currentAccountDigital() {
return this.currentAccountDigitalStep?.currentAccountDigital ?? null;
}
get currentAccountNominee() {
return this.currentAccountCurrencyStep?.nomineeData ?? null;
}
get currentAccountCurrencyFlags() {
return {
...this.currentAccountDetailsStep.currentAccountDetails
balanceSwapOption: this.currentAccountCurrencyStep?.balanceSwapOption ?? false,
msme: this.currentAccountCurrencyStep?.msme ?? false,
autoDebitFacility: this.currentAccountCurrencyStep?.autoDebitFacility ?? false
};
}
@@ -59,8 +59,9 @@
{{ currentAccountProductTemplate?.dormancyAlert ? 'Yes' : 'No' }}
</span>
</div>
<mifosx-dormancy-alert-panel [productTemplate]="currentAccountProductTemplate"></mifosx-dormancy-alert-panel>
@if (currentAccountProductTemplate?.dormancyAlert) {
<mifosx-dormancy-alert-panel [productTemplate]="currentAccountProductTemplate"></mifosx-dormancy-alert-panel>
}
<div class="sac-alert">
<div class="sac-alert__content">
@@ -71,7 +72,6 @@
{{ currentAccountProductTemplate?.nominationFacility ? 'Yes' : 'No' }}
</span>
</div>
@if (currentAccountProductTemplate?.nominationFacility) {
<mifosx-nominee-form (nomineeChange)="onNomineeChange($event)"></mifosx-nominee-form>
}
@@ -0,0 +1,190 @@
<!--
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/.
-->
<!-- Header -->
<div class="abd-header">
<div class="abd-header__icon">
<mat-icon>person_pin</mat-icon>
</div>
<div class="abd-header__text">
<h2 class="abd-header__title">Add Beneficiary / Nominee</h2>
<p class="abd-header__sub">Register a nominee for this account as per Indian banking norms</p>
</div>
<button mat-icon-button class="abd-header__close" type="button" (click)="cancel()">
<mat-icon>close</mat-icon>
</button>
</div>
<mat-dialog-content>
<form [formGroup]="form" class="abd-form">
<!-- Mode Toggle -->
<div class="abd-mode-toggle">
<button
type="button"
class="abd-mode-btn"
[class.abd-mode-btn--active]="mode === 'search'"
(click)="switchMode('search')"
>
<mat-icon>manage_search</mat-icon> Linked Client
</button>
<button
type="button"
class="abd-mode-btn"
[class.abd-mode-btn--active]="mode === 'manual'"
(click)="switchMode('manual')"
>
<mat-icon>edit_note</mat-icon> Manual Entry
</button>
</div>
<!-- Search Mode -->
@if (mode === 'search') {
<div class="abd-search-section">
<mat-form-field appearance="outline" class="abd-full-width">
<mat-label>Search Client by Name / CIF</mat-label>
<mat-icon matPrefix class="abd-prefix-icon">search</mat-icon>
<input
matInput
formControlName="searchText"
(input)="onSearchInput($event.target.value)"
autocomplete="off"
placeholder="Type at least 2 characters..."
/>
@if (isSearching) {
<mat-progress-spinner matSuffix diameter="18" mode="indeterminate"></mat-progress-spinner>
}
@if (selectedClient) {
<button matSuffix mat-icon-button type="button" (click)="clearSelection()">
<mat-icon>close</mat-icon>
</button>
}
</mat-form-field>
@if (searchResults.length > 0) {
<div class="abd-search-results">
@for (client of searchResults; track client) {
<div class="abd-result-item" (click)="selectClient(client)">
<div class="abd-result-avatar">{{ (client.displayName || '?')[0].toUpperCase() }}</div>
<div class="abd-result-body">
<span class="abd-result-name">{{ client.displayName }}</span>
<span class="abd-result-meta">
@if (client.cifNumber) {
<span class="abd-chip">{{ client.cifNumber }}</span>
}
@if (client.officeName) {
<span class="abd-chip">{{ client.officeName }}</span>
}
</span>
</div>
<mat-icon class="abd-result-arrow">chevron_right</mat-icon>
</div>
}
</div>
}
@if (selectedClient) {
<div class="abd-selected-card">
<div class="abd-selected-avatar">{{ (selectedClient.displayName || '?')[0].toUpperCase() }}</div>
<div class="abd-selected-info">
<span class="abd-selected-name">{{ selectedClient.displayName }}</span>
<span class="abd-selected-sub">{{ selectedClient.cifNumber || selectedClient.accountNumber }}</span>
</div>
<span class="abd-badge abd-badge--linked">Linked</span>
</div>
}
</div>
}
<!-- Name field (manual or auto-filled) -->
<mat-form-field appearance="outline" class="abd-full-width abd-mt">
<mat-label>Nominee / Beneficiary Name <span class="abd-required">*</span></mat-label>
<input matInput formControlName="name" [readonly]="mode === 'search' && !!selectedClient" />
<mat-error>Name is required</mat-error>
</mat-form-field>
<!-- Row: Relationship + DOB -->
<div class="abd-row">
<mat-form-field appearance="outline" class="abd-flex">
<mat-label>Relationship <span class="abd-required">*</span></mat-label>
<mat-select formControlName="relationship">
@for (r of relationships; track r) {
<mat-option [value]="r">{{ r }}</mat-option>
}
</mat-select>
<mat-error>Relationship is required</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="abd-flex" (click)="dobPicker.open()">
<mat-label>Date of Birth</mat-label>
<input matInput [matDatepicker]="dobPicker" formControlName="dateOfBirth" readonly />
<mat-datepicker-toggle matSuffix [for]="dobPicker"></mat-datepicker-toggle>
<mat-datepicker #dobPicker></mat-datepicker>
</mat-form-field>
</div>
<!-- Share % -->
<div class="abd-share-row">
<mat-form-field appearance="outline" class="abd-share-field">
<mat-label>Share % <span class="abd-required">*</span></mat-label>
<input matInput type="number" formControlName="sharePercentage" min="1" max="100" />
<span matSuffix class="abd-pct">%</span>
<mat-error>Enter 1–100</mat-error>
</mat-form-field>
<div class="abd-quick-pct">
@for (p of [25, 33, 50, 100]; track p) {
<button type="button" class="abd-pct-chip" (click)="form.get('sharePercentage').setValue(p)">{{ p }}%</button>
}
</div>
</div>
<!-- Minor Toggle -->
<div class="abd-minor-toggle">
<mat-slide-toggle formControlName="isMinor" color="primary">
Nominee is a Minor (below 18 years)
</mat-slide-toggle>
</div>
<!-- Guardian fields (if minor) -->
@if (isMinor) {
<div class="abd-guardian-box">
<div class="abd-guardian-label">
<mat-icon>supervisor_account</mat-icon>
Guardian Details
</div>
<div class="abd-row">
<mat-form-field appearance="outline" class="abd-flex">
<mat-label>Guardian Name</mat-label>
<input matInput formControlName="guardianName" />
</mat-form-field>
<mat-form-field appearance="outline" class="abd-flex">
<mat-label>Guardian Relationship</mat-label>
<mat-select formControlName="guardianRelationship">
@for (r of guardianRelationships; track r) {
<mat-option [value]="r">{{ r }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
</div>
}
</form>
</mat-dialog-content>
<mat-dialog-actions class="abd-actions">
<button mat-stroked-button type="button" (click)="cancel()">Cancel</button>
<button
mat-flat-button
color="primary"
type="button"
class="abd-btn-add"
[disabled]="form.get('name').invalid || form.get('relationship').invalid || form.get('sharePercentage').invalid"
(click)="confirm()"
>
<mat-icon>check</mat-icon> Add Nominee
</button>
</mat-dialog-actions>
@@ -0,0 +1,415 @@
/**
* 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/.
*/
$primary: #1a56db;
$primary-light: #eff6ff;
$primary-border: #bfdbfe;
$success: #057a55;
$success-light: #f3faf7;
$success-border: #84e1bc;
$orange: #d03801;
$orange-light: #fff8f1;
$orange-border: #fcd9bd;
$border: #e5e7eb;
$text: #111827;
$muted: #6b7280;
$radius: 10px;
mifosx-add-beneficiary-dialog {
display: block;
}
// Header
.abd-header {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 18px 20px 14px;
border-bottom: 1px solid $border;
background: linear-gradient(135deg, #f0f7ff 0%, #fff 60%);
&__icon {
width: 42px;
height: 42px;
border-radius: 10px;
background: $primary;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
mat-icon {
color: #fff;
font-size: 22px;
width: 22px;
height: 22px;
}
}
&__text {
flex: 1;
}
&__title {
margin: 0;
font-size: 15px;
font-weight: 700;
color: $text;
}
&__sub {
margin: 3px 0 0;
font-size: 11.5px;
color: $muted;
}
&__close {
color: $muted;
margin: -4px -8px 0 0;
}
}
// Form
.abd-form {
display: flex;
flex-direction: column;
gap: 0;
width: 500px;
max-width: 100%;
padding: 4px 0;
}
.abd-full-width {
width: 100%;
}
.abd-mt {
margin-top: 8px;
}
// Mode toggle
.abd-mode-toggle {
display: flex;
gap: 8px;
margin: 14px 0 10px;
background: #f9fafb;
border: 1px solid $border;
border-radius: 10px;
padding: 5px;
}
.abd-mode-btn {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 8px 14px;
border-radius: 8px;
border: 1.5px solid transparent;
background: transparent;
font-size: 13px;
font-weight: 600;
color: $muted;
cursor: pointer;
transition: all 0.15s;
mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
}
&--active {
background: #fff;
border-color: $primary-border;
color: $primary;
box-shadow: 0 1px 4px rgb(0 0 0 / 8%);
}
}
// Search
.abd-search-section {
position: relative;
}
.abd-prefix-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: $muted;
margin-right: 4px;
}
.abd-search-results {
position: absolute;
z-index: 200;
top: calc(100% - 8px);
left: 0;
right: 0;
background: #fff;
border: 1px solid $border;
border-radius: $radius;
box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
max-height: 220px;
overflow-y: auto;
}
.abd-result-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
cursor: pointer;
border-bottom: 1px solid #f3f4f6;
transition: background 0.1s;
&:last-child {
border-bottom: none;
}
&:hover {
background: $primary-light;
}
}
.abd-result-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: linear-gradient(135deg, #6366f1, $primary);
color: #fff;
font-size: 13px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.abd-result-body {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
}
.abd-result-name {
font-size: 13px;
font-weight: 600;
color: $text;
}
.abd-result-meta {
display: flex;
gap: 5px;
flex-wrap: wrap;
}
.abd-chip {
font-size: 10px;
padding: 1px 7px;
border-radius: 999px;
background: #f3f4f6;
color: $muted;
font-weight: 500;
}
.abd-result-arrow {
font-size: 18px;
width: 18px;
height: 18px;
color: $primary;
opacity: 0.4;
transition: all 0.1s;
.abd-result-item:hover & {
opacity: 1;
transform: translateX(2px);
}
}
// Selected card
.abd-selected-card {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
background: $success-light;
border: 1.5px solid $success-border;
border-radius: $radius;
margin-top: 6px;
}
.abd-selected-avatar {
width: 36px;
height: 36px;
border-radius: 50%;
background: linear-gradient(135deg, #10b981, $success);
color: #fff;
font-size: 14px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.abd-selected-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
}
.abd-selected-name {
font-size: 13px;
font-weight: 700;
color: #065f46;
}
.abd-selected-sub {
font-size: 11px;
color: #047857;
}
.abd-badge {
font-size: 10px;
font-weight: 700;
padding: 3px 10px;
border-radius: 999px;
text-transform: uppercase;
letter-spacing: 0.04em;
&--linked {
background: $success;
color: #fff;
}
}
// Row layout
.abd-row {
display: flex;
gap: 12px;
margin-top: 8px;
@media (width <= 500px) {
flex-direction: column;
gap: 0;
}
}
.abd-flex {
flex: 1;
}
// Share %
.abd-share-row {
display: flex;
align-items: flex-start;
gap: 12px;
margin-top: 8px;
}
.abd-share-field {
flex: 0 0 150px;
}
.abd-pct {
font-size: 15px;
font-weight: 600;
color: $muted;
margin-right: 4px;
}
.abd-quick-pct {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding-top: 10px;
}
.abd-pct-chip {
padding: 5px 12px;
border-radius: 8px;
border: 1.5px solid $border;
background: #fff;
font-size: 12px;
font-weight: 600;
color: $muted;
cursor: pointer;
transition: all 0.12s;
&:hover {
border-color: $primary;
background: $primary-light;
color: $primary;
}
}
// Required star
.abd-required {
color: #e02424;
}
// Minor toggle
.abd-minor-toggle {
margin-top: 14px;
padding: 12px 14px;
background: #fffbeb;
border: 1px solid #fde68a;
border-radius: 8px;
}
// Guardian box
.abd-guardian-box {
margin-top: 10px;
padding: 14px;
background: $orange-light;
border: 1.5px solid $orange-border;
border-radius: $radius;
}
.abd-guardian-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 700;
color: $orange;
text-transform: uppercase;
letter-spacing: 0.04em;
margin-bottom: 10px;
mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
color: $orange;
}
}
// Actions
.abd-actions {
padding: 12px 20px 16px !important;
border-top: 1px solid $border;
gap: 10px;
justify-content: flex-end !important;
}
.abd-btn-add {
border-radius: 8px !important;
padding: 0 18px !important;
gap: 4px;
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
}
@@ -0,0 +1,167 @@
/**
* 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/.
*/
import { Component, inject, OnDestroy, ViewEncapsulation } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { MatDialogRef, MatDialogContent, MatDialogActions } from '@angular/material/dialog';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { MatIcon } from '@angular/material/icon';
import { MatSlideToggle } from '@angular/material/slide-toggle';
import { ClientsService } from 'app/clients/clients.service';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-add-beneficiary-dialog',
templateUrl: './add-beneficiary-dialog.component.html',
styleUrl: './add-beneficiary-dialog.component.scss',
encapsulation: ViewEncapsulation.None,
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDialogContent,
MatDialogActions,
MatProgressSpinner,
MatIcon,
MatSlideToggle,
ReactiveFormsModule
]
})
export class AddBeneficiaryDialogComponent implements OnDestroy {
private fb = inject(UntypedFormBuilder);
private clientsService = inject(ClientsService);
dialogRef = inject(MatDialogRef<AddBeneficiaryDialogComponent>);
/** Toggle between linked client and manual entry */
mode: 'search' | 'manual' = 'search';
form: UntypedFormGroup = this.fb.group({
searchText: [''],
name: [
'',
Validators.required
],
relationship: [
'',
Validators.required
],
dateOfBirth: [null],
sharePercentage: [
null,
[
Validators.required,
Validators.min(1),
Validators.max(100)
]
],
isMinor: [false],
guardianName: [''],
guardianRelationship: ['']
});
searchResults: any[] = [];
selectedClient: any = null;
isSearching = false;
private searchTimer: any = null;
readonly relationships = [
'Spouse',
'Son',
'Daughter',
'Father',
'Mother',
'Brother',
'Sister',
'Grandson',
'Granddaughter',
'Father-in-law',
'Mother-in-law',
'Other'
];
readonly guardianRelationships = [
'Father',
'Mother',
'Guardian',
'Natural Guardian'
];
get isMinor(): boolean {
return this.form.get('isMinor').value;
}
switchMode(m: 'search' | 'manual'): void {
this.mode = m;
this.selectedClient = null;
this.searchResults = [];
this.form.patchValue({ searchText: '', name: '' });
}
onSearchInput(value: string): void {
if (this.searchTimer) clearTimeout(this.searchTimer);
this.selectedClient = null;
if (!value || value.trim().length < 2) {
this.searchResults = [];
return;
}
this.isSearching = true;
this.searchTimer = setTimeout(() => {
this.clientsService.searchByText(value.trim(), 0, 10).subscribe({
next: (res: any) => {
this.searchResults = res?.content || [];
this.isSearching = false;
},
error: () => {
this.searchResults = [];
this.isSearching = false;
}
});
}, 400);
}
selectClient(client: any): void {
this.selectedClient = client;
this.form.patchValue({
searchText: client.displayName,
name: client.displayName
});
this.searchResults = [];
}
clearSelection(): void {
this.selectedClient = null;
this.form.patchValue({ searchText: '', name: '' });
this.searchResults = [];
}
confirm(): void {
if (
this.form.get('name').invalid ||
this.form.get('relationship').invalid ||
this.form.get('sharePercentage').invalid
)
return;
const v = this.form.getRawValue();
this.dialogRef.close({
clientId: this.selectedClient?.id || null,
name: v.name,
relationship: v.relationship,
dateOfBirth: v.dateOfBirth,
sharePercentage: v.sharePercentage,
isMinor: v.isMinor,
guardianName: v.isMinor ? v.guardianName : null,
guardianRelationship: v.isMinor ? v.guardianRelationship : null
});
}
cancel(): void {
this.dialogRef.close(null);
}
ngOnDestroy(): void {
if (this.searchTimer) clearTimeout(this.searchTimer);
}
}
@@ -0,0 +1,135 @@
<!--
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/.
-->
<!-- Dialog Header -->
<div class="arc-header">
<div class="arc-header__icon">
<mat-icon>person_add</mat-icon>
</div>
<div class="arc-header__text">
<h2 class="arc-header__title">Add Related Client / Partner</h2>
<p class="arc-header__sub">Search and link an existing client with their ownership share</p>
</div>
<button mat-icon-button class="arc-header__close" (click)="cancel()">
<mat-icon>close</mat-icon>
</button>
</div>
<mat-dialog-content>
<form [formGroup]="form" class="arc-dialog-form">
<!-- Step 1: Client Search -->
<div class="arc-step-label">
<span class="arc-step-badge">1</span>
<span>Select Client</span>
</div>
<div class="arc-search-section">
<mat-form-field appearance="outline" class="arc-full-width">
<mat-label>Search by Name / CIF / Account No</mat-label>
<mat-icon matPrefix class="arc-search-icon">search</mat-icon>
<input
matInput
formControlName="searchText"
(input)="onSearchInput($event.target.value)"
autocomplete="off"
placeholder="Type at least 2 characters..."
/>
@if (isSearching) {
<mat-progress-spinner matSuffix diameter="18" mode="indeterminate"></mat-progress-spinner>
}
@if (selectedClient) {
<button matSuffix mat-icon-button type="button" (click)="clearSelection()">
<mat-icon>close</mat-icon>
</button>
}
</mat-form-field>
<!-- Search Results -->
@if (searchResults.length > 0) {
<div class="arc-search-results">
@for (client of searchResults; track client) {
<div class="arc-search-result-item" (click)="selectClient(client)">
<div class="arc-result-avatar">
{{ (client.displayName || '?')[0].toUpperCase() }}
</div>
<div class="arc-result-body">
<span class="arc-result-name">{{ client.displayName }}</span>
<span class="arc-result-meta">
@if (client.cifNumber) {
<span class="arc-chip">{{ client.cifNumber }}</span>
}
@if (client.officeName) {
<span class="arc-chip">{{ client.officeName }}</span>
}
@if (client.status?.value) {
<span class="arc-chip arc-chip--status" [class.arc-chip--active]="client.status?.id === 300">
{{ client.status.value }}
</span>
}
</span>
</div>
<mat-icon class="arc-result-arrow">chevron_right</mat-icon>
</div>
}
</div>
}
</div>
<!-- Selected Client Card -->
@if (selectedClient) {
<div class="arc-selected-client">
<div class="arc-selected-avatar">
{{ (selectedClient.displayName || '?')[0].toUpperCase() }}
</div>
<div class="arc-client-info">
<span class="arc-client-name">{{ selectedClient.displayName }}</span>
<span class="arc-client-sub">{{ selectedClient.cifNumber || selectedClient.accountNumber || 'N/A' }}</span>
</div>
<span class="arc-selected-badge">Selected</span>
</div>
}
<!-- Step 2: Share Percentage -->
<div class="arc-step-label arc-step-label--mt">
<span class="arc-step-badge">2</span>
<span>Ownership Share</span>
</div>
<div class="arc-share-row">
<mat-form-field appearance="outline" class="arc-share-field">
<mat-label>Share Percentage</mat-label>
<input matInput type="number" formControlName="sharePercentage" min="1" max="100" />
<span matSuffix class="arc-percent-suffix">%</span>
<mat-error>Enter a value between 1 and 100</mat-error>
</mat-form-field>
<div class="arc-share-hints">
@for (hint of [25, 33, 50, 100]; track hint) {
<button type="button" class="arc-hint-chip" (click)="form.get('sharePercentage').setValue(hint)">
{{ hint }}%
</button>
}
</div>
</div>
</form>
</mat-dialog-content>
<mat-dialog-actions class="arc-actions">
<button mat-stroked-button type="button" (click)="cancel()" class="arc-btn-cancel">Cancel</button>
<button
mat-flat-button
color="primary"
type="button"
class="arc-btn-add"
[disabled]="!selectedClient || form.get('sharePercentage').invalid"
(click)="confirm()"
>
<mat-icon>add</mat-icon>
Add Partner
</button>
</mat-dialog-actions>
@@ -0,0 +1,359 @@
/**
* 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/.
*/
// ── Tokens ───────────────────────────────────────────────────────────────────
$primary: #2563eb;
$primary-light: #eff6ff;
$primary-border: #bfdbfe;
$success: #16a34a;
$success-light: #f0fdf4;
$success-border: #bbf7d0;
$border: #e5e7eb;
$text: #111827;
$text-muted: #6b7280;
$radius: 10px;
// ── Dialog wrapper ────────────────────────────────────────────────────────────
mifosx-add-related-client-dialog {
display: block;
}
// ── Header ────────────────────────────────────────────────────────────────────
.arc-header {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 20px 20px 14px;
border-bottom: 1px solid $border;
&__icon {
width: 40px;
height: 40px;
border-radius: 10px;
background: $primary-light;
border: 1px solid $primary-border;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
mat-icon {
color: $primary;
font-size: 20px;
width: 20px;
height: 20px;
}
}
&__text {
flex: 1;
}
&__title {
margin: 0;
font-size: 15px;
font-weight: 700;
color: $text;
line-height: 1.3;
}
&__sub {
margin: 3px 0 0;
font-size: 12px;
color: $text-muted;
}
&__close {
margin-top: -4px;
margin-right: -8px;
color: $text-muted;
}
}
// ── Dialog content ────────────────────────────────────────────────────────────
.arc-dialog-form {
display: flex;
flex-direction: column;
gap: 0;
width: 440px;
max-width: 100%;
padding-top: 4px;
}
// ── Step labels ───────────────────────────────────────────────────────────────
.arc-step-label {
display: flex;
align-items: center;
gap: 8px;
margin: 16px 0 10px;
font-size: 12px;
font-weight: 700;
color: $text-muted;
text-transform: uppercase;
letter-spacing: 0.04em;
&--mt {
margin-top: 20px;
}
}
.arc-step-badge {
width: 20px;
height: 20px;
border-radius: 50%;
background: $primary;
color: #fff;
font-size: 11px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
// ── Search ────────────────────────────────────────────────────────────────────
.arc-full-width {
width: 100%;
}
.arc-search-section {
position: relative;
}
.arc-search-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: $text-muted;
margin-right: 4px;
}
.arc-search-results {
position: absolute;
z-index: 200;
top: calc(100% - 8px);
left: 0;
right: 0;
background: #fff;
border: 1px solid $border;
border-radius: $radius;
box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
max-height: 240px;
overflow-y: auto;
}
.arc-search-result-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
cursor: pointer;
border-bottom: 1px solid #f3f4f6;
transition: background 0.12s;
&:last-child {
border-bottom: none;
}
&:hover {
background: $primary-light;
.arc-result-arrow {
opacity: 1;
transform: translateX(2px);
}
}
}
.arc-result-avatar {
width: 34px;
height: 34px;
border-radius: 50%;
background: linear-gradient(135deg, #6366f1, #2563eb);
color: #fff;
font-size: 13px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.arc-result-body {
flex: 1;
display: flex;
flex-direction: column;
gap: 3px;
}
.arc-result-name {
font-size: 13px;
font-weight: 600;
color: $text;
}
.arc-result-meta {
display: flex;
gap: 6px;
flex-wrap: wrap;
}
.arc-chip {
font-size: 10px;
padding: 1px 7px;
border-radius: 999px;
background: #f3f4f6;
color: $text-muted;
font-weight: 500;
&--status {
background: #fef3c7;
color: #92400e;
}
&--active {
background: #dcfce7;
color: #166534;
}
}
.arc-result-arrow {
font-size: 18px;
width: 18px;
height: 18px;
color: $primary;
opacity: 0;
transition:
opacity 0.12s,
transform 0.12s;
}
// ── Selected client ───────────────────────────────────────────────────────────
.arc-selected-client {
display: flex;
align-items: center;
gap: 12px;
padding: 12px 14px;
background: $success-light;
border: 1.5px solid $success-border;
border-radius: $radius;
margin-top: 6px;
}
.arc-selected-avatar {
width: 38px;
height: 38px;
border-radius: 50%;
background: linear-gradient(135deg, #22c55e, #16a34a);
color: #fff;
font-size: 14px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.arc-client-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 2px;
}
.arc-client-name {
font-size: 13px;
font-weight: 700;
color: #166534;
}
.arc-client-sub {
font-size: 11px;
color: #15803d;
}
.arc-selected-badge {
font-size: 10px;
font-weight: 700;
padding: 3px 10px;
border-radius: 999px;
background: $success;
color: #fff;
text-transform: uppercase;
letter-spacing: 0.05em;
}
// ── Share percentage ──────────────────────────────────────────────────────────
.arc-share-row {
display: flex;
align-items: flex-start;
gap: 12px;
}
.arc-share-field {
flex: 0 0 160px;
}
.arc-percent-suffix {
font-size: 15px;
font-weight: 600;
color: $text-muted;
margin-right: 4px;
}
.arc-share-hints {
display: flex;
flex-wrap: wrap;
gap: 6px;
padding-top: 10px;
}
.arc-hint-chip {
padding: 5px 12px;
border-radius: 8px;
border: 1.5px solid $border;
background: #fff;
font-size: 12px;
font-weight: 600;
color: $text-muted;
cursor: pointer;
transition:
border-color 0.12s,
background 0.12s,
color 0.12s;
&:hover {
border-color: $primary;
background: $primary-light;
color: $primary;
}
}
// ── Actions ───────────────────────────────────────────────────────────────────
.arc-actions {
padding: 12px 20px 16px !important;
border-top: 1px solid $border;
gap: 10px;
justify-content: flex-end !important;
}
.arc-btn-cancel {
border-radius: 8px !important;
}
.arc-btn-add {
border-radius: 8px !important;
padding: 0 18px !important;
gap: 4px;
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
}
@@ -0,0 +1,104 @@
/**
* 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/.
*/
import { Component, inject, OnDestroy, ViewEncapsulation } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators, ReactiveFormsModule } from '@angular/forms';
import { MatDialogRef, MatDialogContent, MatDialogActions } from '@angular/material/dialog';
import { MatProgressSpinner } from '@angular/material/progress-spinner';
import { MatIcon } from '@angular/material/icon';
import { ClientsService } from 'app/clients/clients.service';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@Component({
selector: 'mifosx-add-related-client-dialog',
templateUrl: './add-related-client-dialog.component.html',
styleUrl: './add-related-client-dialog.component.scss',
encapsulation: ViewEncapsulation.None,
imports: [
...STANDALONE_SHARED_IMPORTS,
MatDialogContent,
MatDialogActions,
MatProgressSpinner,
MatIcon,
ReactiveFormsModule
]
})
export class AddRelatedClientDialogComponent implements OnDestroy {
private fb = inject(UntypedFormBuilder);
private clientsService = inject(ClientsService);
dialogRef = inject(MatDialogRef<AddRelatedClientDialogComponent>);
form: UntypedFormGroup = this.fb.group({
searchText: [''],
sharePercentage: [
null,
[
Validators.required,
Validators.min(1),
Validators.max(100)
]
]
});
searchResults: any[] = [];
selectedClient: any = null;
isSearching = false;
private searchTimer: any = null;
onSearchInput(value: string): void {
if (this.searchTimer) clearTimeout(this.searchTimer);
this.selectedClient = null;
if (!value || value.trim().length < 2) {
this.searchResults = [];
return;
}
this.isSearching = true;
this.searchTimer = setTimeout(() => {
this.clientsService.searchByText(value.trim(), 0, 10).subscribe({
next: (res: any) => {
this.searchResults = res?.content || [];
this.isSearching = false;
},
error: () => {
this.searchResults = [];
this.isSearching = false;
}
});
}, 400);
}
selectClient(client: any): void {
this.selectedClient = client;
this.form.patchValue({ searchText: client.displayName });
this.searchResults = [];
}
clearSelection(): void {
this.selectedClient = null;
this.form.patchValue({ searchText: '' });
this.searchResults = [];
}
confirm(): void {
if (!this.selectedClient || this.form.get('sharePercentage').invalid) return;
this.dialogRef.close({
clientId: this.selectedClient.id,
clientName: this.selectedClient.displayName,
clientCif: this.selectedClient.cifNumber || this.selectedClient.accountNumber || '',
sharePercentage: this.form.get('sharePercentage').value
});
}
cancel(): void {
this.dialogRef.close(null);
}
ngOnDestroy(): void {
if (this.searchTimer) clearTimeout(this.searchTimer);
}
}
@@ -131,6 +131,208 @@
}
}
<!-- ══ Non-Individual Account Details (Indian Banking Style) ═════ -->
@if (isNonIndividualClient && currentAccountProductSelected) {
<div class="ni-card">
<!-- Card Header -->
<div class="ni-card__header">
<div class="ni-card__header-left">
<div class="ni-card__header-icon">
<i class="fas fa-building"></i>
</div>
<div>
<h3 class="ni-card__title">Non-Individual Account Details</h3>
<span class="ni-card__sub">As per RBI / Indian banking norms for non-individual entities</span>
</div>
</div>
<span class="ni-pill ni-pill--entity">Entity Account</span>
</div>
<!-- ── A. Partners / Members ─────────────────────────────── -->
<div class="ni-block">
<div class="ni-block__head">
<div class="ni-block__head-left">
<span class="ni-block__letter">A</span>
<div>
<span class="ni-block__title">Partners / Members</span>
<span class="ni-block__hint">Authorised persons linked to this entity account</span>
</div>
</div>
<button
mat-flat-button
color="primary"
type="button"
class="ni-add-btn"
(click)="openAddRelatedClientDialog()"
>
<mat-icon>person_add</mat-icon> Add Partner
</button>
</div>
@if (relatedClients.length > 0) {
<div class="ni-table-wrap">
<table class="ni-tbl">
<thead>
<tr>
<th class="ni-th ni-th--sr">Sr.</th>
<th class="ni-th">Partner Name</th>
<th class="ni-th">CIF No.</th>
<th class="ni-th ni-th--center">Ownership %</th>
<th class="ni-th ni-th--center">Signing Authority</th>
<th class="ni-th ni-th--center">Action</th>
</tr>
</thead>
<tbody>
@for (c of relatedClients; track c; let i = $index) {
<tr class="ni-tr" [class.ni-tr--mandatory]="c.isMandatory">
<td class="ni-td ni-td--sr">{{ i + 1 }}</td>
<td class="ni-td">
<div class="ni-client-cell">
<div class="ni-avatar" [class.ni-avatar--mandatory]="c.isMandatory">
{{ c.clientName[0].toUpperCase() }}
</div>
<div>
<span class="ni-client-name">{{ c.clientName }}</span>
@if (c.isMandatory) {
<span class="ni-mandatory-tag">Mandatory Signatory</span>
}
</div>
</div>
</td>
<td class="ni-td">
<span class="ni-mono">{{ c.clientCif || '—' }}</span>
</td>
<td class="ni-td ni-td--center">
<span class="ni-pct-badge" [class.ni-pct-badge--full]="c.sharePercentage === 100">
{{ c.sharePercentage }}%
</span>
</td>
<td class="ni-td ni-td--center">
<button
type="button"
class="ni-sign-toggle"
[class.ni-sign-toggle--mandatory]="c.isMandatory"
[class.ni-sign-toggle--optional]="!c.isMandatory"
(click)="toggleMandatory(c)"
[title]="c.isMandatory ? 'Click to set Optional' : 'Click to set Mandatory'"
>
<mat-icon>{{ c.isMandatory ? 'lock' : 'lock_open' }}</mat-icon>
{{ c.isMandatory ? 'Mandatory' : 'Optional' }}
</button>
</td>
<td class="ni-td ni-td--center">
<button
mat-icon-button
color="warn"
type="button"
(click)="removeRelatedClient(i)"
class="ni-del-btn"
>
<mat-icon>delete_outline</mat-icon>
</button>
</td>
</tr>
}
</tbody>
<tfoot>
<tr class="ni-tr-total">
<td colspan="3" class="ni-td ni-td--total-label">Total Ownership</td>
<td class="ni-td ni-td--center">
<span
class="ni-pct-badge"
[class.ni-pct-badge--ok]="totalSharePercentage === 100"
[class.ni-pct-badge--warn]="totalSharePercentage !== 100"
>
{{ totalSharePercentage }}%
</span>
</td>
<td class="ni-td ni-td--center">
<span class="ni-mandatory-count-badge">
<mat-icon>lock</mat-icon> {{ mandatoryCount }} Mandatory
</span>
</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
@if (totalSharePercentage !== 100) {
<div class="ni-alert ni-alert--warn">
<mat-icon>warning_amber</mat-icon>
Total ownership is {{ totalSharePercentage }}%. It must equal 100%.
</div>
} @else {
<div class="ni-alert ni-alert--ok">
<mat-icon>check_circle</mat-icon>
Ownership allocation is complete (100%).
</div>
}
} @else {
<div class="ni-empty-state">
<mat-icon>group_add</mat-icon>
<span>No partners added yet. Click <strong>Add Partner</strong> to begin.</span>
</div>
}
</div>
<!-- ── B. Mode of Operation / Signatory Rights (auto from partners) ── -->
<div class="ni-block ni-block--sep">
<div class="ni-block__head">
<div class="ni-block__head-left">
<span class="ni-block__letter ni-block__letter--b">B</span>
<div>
<span class="ni-block__title">Mode of Operation / Signatory Rights</span>
<span class="ni-block__hint">Auto-computed from Mandatory partners — you can override manually</span>
</div>
</div>
@if (signatoryRights) {
<span class="ni-auto-badge"> <mat-icon>auto_awesome</mat-icon> Auto </span>
}
</div>
@if (relatedClients.length === 0) {
<div class="ni-empty-state">
<mat-icon>how_to_vote</mat-icon>
<span>Add partners in Section A and set Mandatory/Optional to auto-compute the mode.</span>
</div>
} @else {
<div class="ni-signatory-grid">
@for (opt of signatoryRightsOptions; track opt.value) {
<button
type="button"
class="ni-sig-card"
[class.ni-sig-card--selected]="signatoryRights === opt.value"
(click)="signatoryRights = opt.value"
>
<mat-icon class="ni-sig-card__icon">{{ opt.icon }}</mat-icon>
<div class="ni-sig-card__code">{{ opt.label }}</div>
<div class="ni-sig-card__desc">{{ opt.sublabel }}</div>
@if (signatoryRights === opt.value) {
<mat-icon class="ni-sig-card__check">check_circle</mat-icon>
}
</button>
}
</div>
<div class="ni-mode-summary" [class.ni-mode-summary--set]="signatoryRights">
@if (signatoryRights) {
<mat-icon>how_to_vote</mat-icon>
<span
>Mode of Operation: <strong>{{ selectedSignatoryLabel }}</strong></span
>
<span class="ni-mode-summary__from">
({{ mandatoryCount }} Mandatory + {{ relatedClients.length - mandatoryCount }} Optional partner(s))
</span>
} @else {
<mat-icon>info</mat-icon>
<span>Set Mandatory/Optional for partners above to auto-select mode.</span>
}
</div>
}
</div>
</div>
}
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious disabled>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
@@ -104,6 +104,614 @@ $gray-900: #202124;
}
}
// ── Non-Individual Section (Indian Banking Style) ─────────────────────────
$ni-primary: #1a56db;
$ni-green: #057a55;
$ni-orange: #d03801;
$ni-border: #e5e7eb;
$ni-bg: #f9fafb;
.ni-card {
margin-top: 16px;
border: 1.5px solid #c3dafe;
border-radius: 12px;
overflow: hidden;
background: #fff;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 20px;
background: linear-gradient(135deg, #1e3a8a 0%, #1a56db 100%);
}
&__header-left {
display: flex;
align-items: center;
gap: 12px;
}
&__header-icon {
width: 38px;
height: 38px;
border-radius: 8px;
background: rgba(255 255 255 / 20%);
display: flex;
align-items: center;
justify-content: center;
i {
color: #fff;
font-size: 17px;
}
}
&__title {
margin: 0;
font-size: 14px;
font-weight: 700;
color: #fff;
}
&__sub {
margin: 2px 0 0;
font-size: 11px;
color: rgba(255 255 255 / 75%);
}
}
.ni-pill {
padding: 4px 12px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
&--entity {
background: rgba(255 255 255 / 20%);
color: #fff;
border: 1px solid rgba(255 255 255 / 30%);
}
}
// Block
.ni-block {
padding: 16px 20px 4px;
&--sep {
border-top: 1.5px dashed #e5e7eb;
padding-top: 16px;
}
&__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 14px;
}
&__head-left {
display: flex;
align-items: flex-start;
gap: 10px;
&--alone {
margin-bottom: 14px;
}
}
&__letter {
width: 26px;
height: 26px;
border-radius: 6px;
background: $ni-primary;
color: #fff;
font-size: 13px;
font-weight: 800;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
margin-top: 1px;
&--b {
background: $ni-green;
}
&--c {
background: #7c3aed;
}
}
&__title {
display: block;
font-size: 13px;
font-weight: 700;
color: #111827;
}
&__hint {
display: block;
font-size: 11px;
color: #6b7280;
margin-top: 1px;
}
}
.ni-add-btn {
border-radius: 8px !important;
font-size: 12px !important;
padding: 0 14px !important;
height: 34px !important;
gap: 4px;
mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
}
&--accent {
background: $ni-green !important;
color: #fff !important;
}
}
// Table
.ni-table-wrap {
overflow-x: auto;
border: 1px solid $ni-border;
border-radius: 8px;
margin-bottom: 8px;
}
.ni-tbl {
width: 100%;
border-collapse: collapse;
font-size: 13px;
thead {
background: #f8faff;
}
}
.ni-th {
padding: 9px 12px;
font-size: 11px;
font-weight: 700;
color: #374151;
text-transform: uppercase;
letter-spacing: 0.04em;
border-bottom: 1.5px solid $ni-border;
text-align: left;
white-space: nowrap;
&--sr {
width: 40px;
text-align: center;
}
&--center {
text-align: center;
}
}
.ni-tr {
transition: background 0.1s;
&:hover {
background: #f9fafb;
}
&--mandatory {
background: #fffbf0;
&:hover {
background: #fff8e6;
}
}
}
.ni-tr-total {
background: #f0f4ff;
border-top: 1.5px solid #c3dafe;
}
.ni-td {
padding: 10px 12px;
color: #111827;
border-bottom: 1px solid #f3f4f6;
&--sr {
text-align: center;
color: #6b7280;
font-size: 12px;
}
&--center {
text-align: center;
}
&--total-label {
font-weight: 700;
font-size: 12px;
color: #374151;
}
}
.ni-client-cell {
display: flex;
align-items: center;
gap: 8px;
}
.ni-avatar {
width: 30px;
height: 30px;
border-radius: 50%;
background: linear-gradient(135deg, #6366f1, $ni-primary);
color: #fff;
font-size: 12px;
font-weight: 700;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&--green {
background: linear-gradient(135deg, #10b981, $ni-green);
}
&--mandatory {
background: linear-gradient(135deg, #f59e0b, #d97706);
}
}
.ni-client-name {
display: block;
font-weight: 600;
color: #111827;
}
.ni-mandatory-tag {
display: inline-block;
font-size: 9.5px;
font-weight: 700;
color: #b45309;
background: #fef3c7;
border: 1px solid #fde68a;
border-radius: 4px;
padding: 1px 6px;
margin-top: 2px;
text-transform: uppercase;
letter-spacing: 0.04em;
}
.ni-mandatory-count-badge {
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 11px;
font-weight: 700;
color: #92400e;
background: #fef3c7;
border: 1px solid #fde68a;
border-radius: 6px;
padding: 3px 8px;
mat-icon {
font-size: 12px;
width: 12px;
height: 12px;
}
}
// Signing authority toggle in table
.ni-sign-toggle {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 12px;
border-radius: 20px;
border: 1.5px solid;
font-size: 11.5px;
font-weight: 700;
cursor: pointer;
transition: all 0.15s;
white-space: nowrap;
mat-icon {
font-size: 13px;
width: 13px;
height: 13px;
}
&--mandatory {
border-color: #d97706;
background: #fffbeb;
color: #92400e;
}
&--optional {
border-color: #d1d5db;
background: #f9fafb;
color: #6b7280;
&:hover {
border-color: #d97706;
color: #92400e;
background: #fffbeb;
}
}
}
.ni-nom-info {
display: flex;
flex-direction: column;
gap: 1px;
}
.ni-guardian-hint {
font-size: 10.5px;
color: #6b7280;
}
.ni-mono {
font-family: monospace;
font-size: 12px;
color: #374151;
}
.ni-pct-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 3px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
background: #e0e7ff;
color: $ni-primary;
min-width: 52px;
&--ok {
background: #d1fae5;
color: $ni-green;
}
&--warn {
background: #fef3c7;
color: #92400e;
}
&--full {
background: #d1fae5;
color: $ni-green;
}
}
.ni-rel-badge {
font-size: 11px;
padding: 2px 8px;
border-radius: 6px;
background: #f3f4f6;
color: #374151;
font-weight: 500;
}
.ni-minor-badge {
font-size: 10px;
padding: 2px 8px;
border-radius: 999px;
background: #fef3c7;
color: #92400e;
font-weight: 700;
text-transform: uppercase;
}
.ni-td-dash {
color: #d1d5db;
}
.ni-del-btn {
width: 28px !important;
height: 28px !important;
line-height: 28px !important;
mat-icon {
font-size: 17px;
width: 17px;
height: 17px;
}
}
// Alerts
.ni-alert {
display: flex;
align-items: center;
gap: 7px;
padding: 8px 12px;
border-radius: 8px;
font-size: 12px;
font-weight: 500;
margin-bottom: 12px;
mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
}
&--warn {
background: #fffbeb;
color: #92400e;
border: 1px solid #fde68a;
}
&--ok {
background: #f0fdf4;
color: $ni-green;
border: 1px solid #bbf7d0;
}
&--sm {
margin-top: 8px;
margin-bottom: 14px;
}
}
// Empty state
.ni-empty-state {
display: flex;
align-items: center;
gap: 8px;
padding: 16px 12px;
font-size: 13px;
color: #9ca3af;
font-style: italic;
margin-bottom: 10px;
mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
color: #d1d5db;
}
}
// Auto badge on section header
.ni-auto-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
background: #fffbeb;
color: #92400e;
border: 1px solid #fde68a;
mat-icon {
font-size: 12px;
width: 12px;
height: 12px;
}
}
// Mode summary bar below signatory cards
.ni-mode-summary {
display: flex;
align-items: center;
gap: 8px;
padding: 9px 14px;
border-radius: 8px;
font-size: 12.5px;
font-weight: 500;
margin-top: 4px;
background: #f3f4f6;
color: #6b7280;
border: 1px solid #e5e7eb;
mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
}
&--set {
background: #f0fdf4;
color: $ni-green;
border-color: #bbf7d0;
}
&__from {
font-size: 11px;
color: #9ca3af;
font-style: italic;
}
}
// ── Signatory / Mode of Operation cards ──────────────────────────────────
.ni-signatory-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
margin-bottom: 10px;
@media (width <= 700px) {
grid-template-columns: repeat(2, 1fr);
}
}
.ni-sig-card {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 6px;
padding: 18px 10px 14px;
border-radius: 12px;
border: 1.5px solid $ni-border;
background: #fff;
cursor: pointer;
transition: all 0.15s;
text-align: center;
&:hover:not(.ni-sig-card--selected) {
border-color: #93c5fd;
background: #f0f9ff;
transform: translateY(-1px);
}
&--selected {
border-color: $ni-primary;
background: #eff6ff;
box-shadow: 0 0 0 3px rgba(37 99 235 / 12%);
transform: translateY(-1px);
}
&__icon {
font-size: 22px;
width: 22px;
height: 22px;
color: #6b7280;
.ni-sig-card--selected & {
color: $ni-primary;
}
}
&__code {
font-size: 15px;
font-weight: 800;
color: #1e3a8a;
font-family: monospace;
letter-spacing: 0.02em;
}
&__desc {
font-size: 10.5px;
font-weight: 500;
color: #6b7280;
line-height: 1.3;
}
&__check {
position: absolute;
top: 7px;
right: 7px;
font-size: 15px;
width: 15px;
height: 15px;
color: $ni-primary;
}
}
// ── Account Feature Toggles ────────────────────────────────────────────────
.sad-toggles {
display: flex;
@@ -14,10 +14,27 @@ import { Dates } from 'app/core/utils/dates';
/** Custom Services */
import { CurrentAccountsService } from 'app/current-accounts/current-accounts.service';
import { SettingsService } from 'app/settings/settings.service';
import { ClientsService } from 'app/clients/clients.service';
import { MatDialog } from '@angular/material/dialog';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import {
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow
} from '@angular/material/table';
import { MatIcon } from '@angular/material/icon';
import { FormsModule } from '@angular/forms';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { IntroducerSearchComponent } from 'app/shared/introducer-search/introducer-search.component';
import { AddRelatedClientDialogComponent } from './add-related-client-dialog/add-related-client-dialog.component';
/**
* Current Account Details Step
@@ -31,13 +48,27 @@ import { IntroducerSearchComponent } from 'app/shared/introducer-search/introduc
MatStepperPrevious,
FaIconComponent,
MatStepperNext,
IntroducerSearchComponent
IntroducerSearchComponent,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
MatIcon,
FormsModule
]
})
export class CurrentAccountDetailsStepComponent implements OnInit {
private formBuilder = inject(UntypedFormBuilder);
private currentAccountsService = inject(CurrentAccountsService);
private settingsService = inject(SettingsService);
private clientsService = inject(ClientsService);
private dialog = inject(MatDialog);
private dateUtils = inject(Dates);
private cdr = inject(ChangeDetectorRef);
@@ -59,6 +90,26 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
currentAccountProductSelected = false;
introducerId: number | null = null;
introducerName: string | null = null;
introducerCif: string | null = null;
/** Non-Individual section */
isNonIndividualClient = false;
relatedClients: Array<{
clientId: number;
clientName: string;
clientCif: string;
sharePercentage: number;
isMandatory: boolean;
}> = [];
signatoryRights = '';
readonly signatoryRightsOptions = [
{ value: 'ANYONE', label: 'E or S', sublabel: 'Either or Survivor', icon: 'how_to_vote' },
{ value: 'ONE_MANDATORY', label: 'J/One', sublabel: 'Joint – One Mandatory', icon: 'group' },
{ value: 'TWO_MANDATORY', label: 'J/Two', sublabel: 'Joint – Two Mandatory', icon: 'groups' },
{ value: 'ONLY_ONE', label: 'Single', sublabel: 'Only 1 Person', icon: 'person' }
];
/** Current Account Template with product data */
@Output() currentAccountProductTemplate = new EventEmitter();
@@ -73,6 +124,13 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
ngOnInit() {
this.maxDate = this.settingsService.businessDate;
this.buildDependencies();
if (this.currentAccountTemplate?.clientId) {
this.clientsService.getClientData(String(this.currentAccountTemplate.clientId)).subscribe((client: any) => {
this.isNonIndividualClient = client?.legalForm?.id === 2;
this.cdr.detectChanges();
});
}
if (this.currentAccountTemplate) {
this.productData = this.currentAccountTemplate.productOptions;
if (this.currentAccountTemplate.currentAccountProductId) {
@@ -178,14 +236,84 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
/**
* Returns current account form value.
*/
onIntroducerChange(event: { clientId: number; displayName: string } | null): void {
onIntroducerChange(event: { clientId: number; displayName: string; cifNumber?: string } | null): void {
this.introducerId = event?.clientId ?? null;
this.introducerName = event?.displayName ?? null;
this.introducerCif = event?.cifNumber ?? null;
}
openAddRelatedClientDialog(): void {
const ref = this.dialog.open(AddRelatedClientDialogComponent, {
width: '500px',
panelClass: 'arc-dialog-panel',
disableClose: true
});
ref.afterClosed().subscribe((result) => {
if (result) {
const duplicate = this.relatedClients.some((c) => c.clientCif && c.clientCif === result.clientCif);
if (duplicate) {
alert(`Partner with CIF ${result.clientCif} is already added.`);
return;
}
this.relatedClients = [
...this.relatedClients,
{ ...result, isMandatory: false }];
this.autoSetSignatoryRights();
}
});
}
removeRelatedClient(index: number): void {
this.relatedClients = this.relatedClients.filter((_, i) => i !== index);
this.autoSetSignatoryRights();
}
toggleMandatory(client: any): void {
client.isMandatory = !client.isMandatory;
this.autoSetSignatoryRights();
}
/** Auto-compute Mode of Operation based on mandatory count */
autoSetSignatoryRights(): void {
const mandatory = this.relatedClients.filter((c) => c.isMandatory).length;
if (mandatory === 0) {
this.signatoryRights = 'ANYONE';
} else if (mandatory === 1) {
this.signatoryRights = 'ONE_MANDATORY';
} else {
this.signatoryRights = 'TWO_MANDATORY';
}
}
get totalSharePercentage(): number {
return this.relatedClients.reduce((sum, c) => sum + (c.sharePercentage || 0), 0);
}
get mandatoryCount(): number {
return this.relatedClients.filter((c) => c.isMandatory).length;
}
get selectedSignatoryLabel(): string {
const opt = this.signatoryRightsOptions.find((x) => x.value === this.signatoryRights);
return opt ? opt.sublabel : '';
}
get currentAccountDetails() {
const value = this.currentAccountDetailsForm.getRawValue();
if (this.introducerId) {
value.introducerId = this.introducerId;
value.introducerName = this.introducerName;
value.introducerCif = this.introducerCif;
}
if (this.isNonIndividualClient) {
value.signatoryRights = this.signatoryRights || null;
value.partners = this.relatedClients.map((c) => ({
clientId: c.clientId,
clientName: c.clientName,
clientCif: c.clientCif,
sharePercentage: c.sharePercentage,
isMandatory: c.isMandatory
}));
}
return value;
}
@@ -7,10 +7,11 @@
-->
<div class="preview-wrapper">
<!-- Details -->
<!-- ══ 1. Account Details ════════════════════════════════════════ -->
<div class="preview-section">
<div class="preview-section__header">
<span class="preview-section__title">{{ 'labels.heading.Details' | translate }}</span>
<i class="fas fa-info-circle preview-section__icon"></i>
<span class="preview-section__title">Account Details</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
@@ -38,27 +39,28 @@
<mifosx-external-identifier externalId="{{ currentAccount.externalId }}"></mifosx-external-identifier>
</span>
} @else {
<span class="preview-value">{{ 'labels.inputs.Unassigned' | translate }}</span>
<span class="preview-value preview-muted">Unassigned</span>
}
</div>
</div>
</div>
</div>
<!-- Product Classification -->
<!-- ══ 2. Product Classification ════════════════════════════════ -->
@if (
currentAccountProductTemplate?.accountType ||
currentAccountProductTemplate?.customerSegment ||
currentAccountProductTemplate?.productCode ||
currentAccountProductTemplate?.previewAccountNo ||
currentAccountProductTemplate?.effectiveDate ||
currentAccountProductTemplate?.effectiveTo ||
currentAccountProductTemplate?.endDate ||
currentAccountProductTemplate?.introducerRequired !== undefined ||
currentAccountProductTemplate?.shareLinkingEnabled !== undefined
currentAccountProductTemplate?.endDate
) {
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-tags preview-section__icon"></i>
<span class="preview-section__title">Product Classification</span>
<span class="preview-section__badge">From Product</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
@@ -77,9 +79,15 @@
@if (currentAccountProductTemplate?.productCode) {
<div class="preview-field">
<span class="preview-label">Product Code</span>
<span class="preview-value">{{ currentAccountProductTemplate?.productCode }}</span>
<span class="preview-value preview-mono">{{ currentAccountProductTemplate?.productCode }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">Account No</span>
<span class="preview-value preview-mono">{{
currentAccountProductTemplate?.previewAccountNo || currentAccountTemplate?.accountNo || 'Auto-generated'
}}</span>
</div>
@if (currentAccountProductTemplate?.effectiveDate) {
<div class="preview-field">
<span class="preview-label">Effective Date</span>
@@ -98,141 +106,348 @@
<span class="preview-value">{{ currentAccountProductTemplate?.endDate | dateFormat }}</span>
</div>
}
@if (
currentAccountProductTemplate?.introducerRequired !== undefined &&
currentAccountProductTemplate?.introducerRequired !== null
) {
<div class="preview-field">
<span class="preview-label">Introducer Required</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="currentAccountProductTemplate?.introducerRequired"
>
{{ currentAccountProductTemplate?.introducerRequired | yesNo }}
</span>
</div>
<div class="preview-field">
<span class="preview-label">Share Linking Enabled</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="currentAccountProductTemplate?.shareLinkingEnabled"
>
{{ currentAccountProductTemplate?.shareLinkingEnabled | yesNo }}
</span>
</div>
</div>
</div>
</div>
}
<!-- ══ 3. Introducer Details (if selected) ══════════════════════ -->
@if (currentAccount?.introducerId) {
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-user-tie preview-section__icon"></i>
<span class="preview-section__title">Introducer Details</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
<div class="preview-field">
<span class="preview-label">Name</span>
<span class="preview-value">{{ currentAccount?.introducerName || '—' }}</span>
</div>
@if (currentAccount?.introducerCif) {
<div class="preview-field">
<span class="preview-label">Introducer Required</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="currentAccountProductTemplate?.introducerRequired"
>
{{ currentAccountProductTemplate?.introducerRequired | yesNo }}
</span>
<span class="preview-label">CIF No</span>
<span class="preview-value preview-mono">{{ currentAccount.introducerCif }}</span>
</div>
}
@if (
currentAccountProductTemplate?.shareLinkingEnabled !== undefined &&
currentAccountProductTemplate?.shareLinkingEnabled !== null
) {
<div class="preview-field">
<span class="preview-label">Share Linking Enabled</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="currentAccountProductTemplate?.shareLinkingEnabled"
>
{{ currentAccountProductTemplate?.shareLinkingEnabled | yesNo }}
<div class="preview-field">
<span class="preview-label">Client ID</span>
<span class="preview-value preview-mono">{{ currentAccount.introducerId }}</span>
</div>
</div>
</div>
</div>
}
<!-- ══ 4. Currency & Account Flags ══════════════════════════════ -->
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-coins preview-section__icon"></i>
<span class="preview-section__title">Currency &amp; Account Flags</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
<div class="preview-field">
<span class="preview-label">Currency</span>
<span class="preview-value">{{ currencyName }}</span>
</div>
<div class="preview-field">
<span class="preview-label">Decimal Places</span>
<span class="preview-value">{{ decimalPlaces }}</span>
</div>
<div class="preview-field">
<span class="preview-label">Currency In Multiples Of</span>
<span class="preview-value">{{ inMultiplesOf }}</span>
</div>
<div class="preview-field">
<span class="preview-label">Balance Swap Option</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="currentAccountCurrencyFlags?.balanceSwapOption"
>
{{ currentAccountCurrencyFlags?.balanceSwapOption | yesNo }}
</span>
</div>
<div class="preview-field">
<span class="preview-label">MSME</span>
<span class="preview-value preview-badge" [class.preview-badge--yes]="currentAccountCurrencyFlags?.msme">
{{ currentAccountCurrencyFlags?.msme | yesNo }}
</span>
</div>
<div class="preview-field">
<span class="preview-label">Auto Debit Facility</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="currentAccountCurrencyFlags?.autoDebitFacility"
>
{{ currentAccountCurrencyFlags?.autoDebitFacility | yesNo }}
</span>
</div>
<div class="preview-field">
<span class="preview-label">Dormancy Alert</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="currentAccountProductTemplate?.dormancyAlert"
>
{{ currentAccountProductTemplate?.dormancyAlert | yesNo }}
</span>
</div>
<div class="preview-field">
<span class="preview-label">Nomination Facility</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="currentAccountProductTemplate?.nominationFacility"
>
{{ currentAccountProductTemplate?.nominationFacility | yesNo }}
</span>
</div>
</div>
</div>
</div>
<!-- ══ 5. Dormancy Monitoring ════════════════════════════════════ -->
@if (currentAccountProductTemplate?.dormancyAlert || currentAccountProductTemplate?.isDormancyTrackingActive) {
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-moon preview-section__icon"></i>
<span class="preview-section__title">Dormancy Monitoring</span>
<span
class="preview-section__badge"
[class.preview-section__badge--active]="currentAccountProductTemplate?.isDormancyTrackingActive"
>
{{ currentAccountProductTemplate?.isDormancyTrackingActive ? 'Active' : 'Inactive' }}
</span>
</div>
<div class="preview-body">
<div class="preview-dormancy-timeline">
<div class="preview-dorm-step">
<div class="preview-dorm-step__dot preview-dorm-step__dot--inactive"></div>
<div class="preview-dorm-step__body">
<span class="preview-dorm-step__label">Inactive</span>
<span class="preview-dorm-step__days">
@if (currentAccountProductTemplate?.daysToInactive != null) {
After {{ currentAccountProductTemplate.daysToInactive }} days of no transaction
} @else {
Not configured
}
</span>
</div>
</div>
<div class="preview-dorm-connector"></div>
<div class="preview-dorm-step">
<div class="preview-dorm-step__dot preview-dorm-step__dot--dormant"></div>
<div class="preview-dorm-step__body">
<span class="preview-dorm-step__label">Dormant</span>
<span class="preview-dorm-step__days">
@if (currentAccountProductTemplate?.daysToDormancy != null) {
{{ currentAccountProductTemplate.daysToDormancy }} days after inactive
} @else {
Not configured
}
</span>
</div>
</div>
<div class="preview-dorm-connector"></div>
<div class="preview-dorm-step">
<div class="preview-dorm-step__dot preview-dorm-step__dot--escheat"></div>
<div class="preview-dorm-step__body">
<span class="preview-dorm-step__label">Escheat</span>
<span class="preview-dorm-step__days">
@if (currentAccountProductTemplate?.daysToEscheat != null) {
{{ currentAccountProductTemplate.daysToEscheat }} days after dormant
} @else {
Not configured
}
</span>
</div>
</div>
</div>
<div class="preview-dorm-flags">
<span class="preview-dorm-flag" [class.preview-dorm-flag--on]="currentAccountProductTemplate?.dormancyAlert">
<i class="fas fa-bell"></i> Alert
{{ currentAccountProductTemplate?.dormancyAlert ? 'Enabled' : 'Disabled' }}
</span>
<span
class="preview-dorm-flag"
[class.preview-dorm-flag--on]="currentAccountProductTemplate?.isDormancyTrackingActive"
>
<i class="fas fa-eye"></i> Tracking
{{ currentAccountProductTemplate?.isDormancyTrackingActive ? 'Active' : 'Inactive' }}
</span>
</div>
</div>
</div>
}
<!-- ══ 6. Nominee Details (if filled) ═══════════════════════════ -->
@if (currentAccountNominee) {
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-user-shield preview-section__icon"></i>
<span class="preview-section__title">Nominee Details</span>
<span class="preview-section__badge">
{{ currentAccountNominee?.mode === 'linked' ? 'Linked Client' : 'Manual Entry' }}
</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
<div class="preview-field">
<span class="preview-label">Name</span>
<span class="preview-value">{{
currentAccountNominee?.fullName || currentAccountNominee?.displayName || '—'
}}</span>
</div>
@if (currentAccountNominee?.cifNumber) {
<div class="preview-field">
<span class="preview-label">CIF No</span>
<span class="preview-value preview-mono">{{ currentAccountNominee.cifNumber }}</span>
</div>
}
<div class="preview-field">
<span class="preview-label">Relationship</span>
<span class="preview-value">{{ currentAccountNominee?.relationship || '—' }}</span>
</div>
<div class="preview-field">
<span class="preview-label">Share %</span>
<span class="preview-value">{{ currentAccountNominee?.nomineeShare ?? '—' }}%</span>
</div>
@if (currentAccountNominee?.dateOfBirth) {
<div class="preview-field">
<span class="preview-label">Date of Birth</span>
<span class="preview-value">{{ currentAccountNominee?.dateOfBirth | dateFormat }}</span>
</div>
}
@if (currentAccountNominee?.nationality) {
<div class="preview-field">
<span class="preview-label">Nationality</span>
<span class="preview-value">{{ currentAccountNominee?.nationality }}</span>
</div>
}
@if (currentAccountNominee?.mobileNo) {
<div class="preview-field">
<span class="preview-label">Mobile No</span>
<span class="preview-value preview-mono">{{ currentAccountNominee?.mobileNo }}</span>
</div>
}
@if (currentAccountNominee?.aadhaarNumber) {
<div class="preview-field">
<span class="preview-label">Aadhaar No</span>
<span class="preview-value preview-mono">{{ currentAccountNominee?.aadhaarNumber }}</span>
</div>
}
@if (currentAccountNominee?.panNumber) {
<div class="preview-field">
<span class="preview-label">PAN No</span>
<span class="preview-value preview-mono">{{ currentAccountNominee?.panNumber }}</span>
</div>
}
@if (currentAccountNominee?.guardianName) {
<div class="preview-field">
<span class="preview-label">Guardian Name</span>
<span class="preview-value">{{ currentAccountNominee?.guardianName }}</span>
</div>
}
@if (currentAccountNominee?.guardianRelationship) {
<div class="preview-field">
<span class="preview-label">Guardian Relationship</span>
<span class="preview-value">{{ currentAccountNominee?.guardianRelationship }}</span>
</div>
}
@if (currentAccountNominee?.remarks) {
<div class="preview-field">
<span class="preview-label">Remarks</span>
<span class="preview-value">{{ currentAccountNominee?.remarks }}</span>
</div>
}
</div>
</div>
</div>
}
<!-- Currency -->
<div class="preview-section">
<div class="preview-section__header">
<span class="preview-section__title">{{ 'labels.heading.Currency' | translate }}</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Currency' | translate }}</span>
<span class="preview-value">{{ currentAccountTermsForm?.get('currencyCode')?.value }}</span>
<!-- ══ 7. Non-Individual Details (if applicable) ════════════════ -->
@if (currentAccount?.partners?.length > 0) {
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-building preview-section__icon"></i>
<span class="preview-section__title">Non-Individual Account Details</span>
</div>
<div class="preview-body">
<!-- Partners -->
<div class="preview-ni-label">Partners / Members</div>
<div class="preview-ni-table-wrap">
<table class="preview-ni-tbl">
<thead>
<tr>
<th>Partner Name</th>
<th>CIF No</th>
<th>Ownership %</th>
<th>Signing Authority</th>
</tr>
</thead>
<tbody>
@for (p of currentAccount.partners; track p.clientId) {
<tr>
<td>{{ p.clientName || '—' }}</td>
<td class="preview-mono">{{ p.clientCif || '—' }}</td>
<td>{{ p.sharePercentage }}%</td>
<td>
<span class="preview-badge" [class.preview-badge--yes]="p.isMandatory">
{{ p.isMandatory ? 'Mandatory' : 'Optional' }}
</span>
</td>
</tr>
}
</tbody>
</table>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Decimal Places' | translate }}</span>
<span class="preview-value">{{ currentAccountProductTemplate?.currency?.decimalPlaces }}</span>
</div>
<div class="preview-field">
<span class="preview-label">{{ 'labels.inputs.Currency In Multiples Of' | translate }}</span>
<span class="preview-value">{{ currentAccountProductTemplate?.currency?.inMultiplesOf }}</span>
</div>
@if (
currentAccountProductTemplate?.dormancyAlert !== undefined &&
currentAccountProductTemplate?.dormancyAlert !== null
) {
<div class="preview-field">
<span class="preview-label">Dormancy Alert</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="currentAccountProductTemplate?.dormancyAlert"
>
{{ currentAccountProductTemplate?.dormancyAlert | yesNo }}
</span>
</div>
}
@if (
currentAccountProductTemplate?.balanceSwapOption !== undefined &&
currentAccountProductTemplate?.balanceSwapOption !== null
) {
<div class="preview-field">
<span class="preview-label">Balance Swap Option</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="currentAccountProductTemplate?.balanceSwapOption"
>
{{ currentAccountProductTemplate?.balanceSwapOption | yesNo }}
</span>
</div>
}
@if (currentAccountProductTemplate?.msme !== undefined && currentAccountProductTemplate?.msme !== null) {
<div class="preview-field">
<span class="preview-label">MSME</span>
<span class="preview-value preview-badge" [class.preview-badge--yes]="currentAccountProductTemplate?.msme">
{{ currentAccountProductTemplate?.msme | yesNo }}
</span>
</div>
}
@if (
currentAccountProductTemplate?.nominationFacility !== undefined &&
currentAccountProductTemplate?.nominationFacility !== null
) {
<div class="preview-field">
<span class="preview-label">Nomination Facility</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="currentAccountProductTemplate?.nominationFacility"
>
{{ currentAccountProductTemplate?.nominationFacility | yesNo }}
</span>
</div>
}
@if (
currentAccountProductTemplate?.autoDebitFacility !== undefined &&
currentAccountProductTemplate?.autoDebitFacility !== null
) {
<div class="preview-field">
<span class="preview-label">Auto Debit Facility</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="currentAccountProductTemplate?.autoDebitFacility"
>
{{ currentAccountProductTemplate?.autoDebitFacility | yesNo }}
</span>
<!-- Signatory Rights -->
@if (currentAccount?.signatoryRights) {
<div class="preview-ni-mode">
<i class="fas fa-how_to_vote"></i>
<span class="preview-ni-mode__label">Mode of Operation:</span>
<strong>{{ currentAccount.signatoryRights }}</strong>
</div>
}
</div>
</div>
</div>
}
<!-- ══ 8. Digital Facilities ════════════════════════════════════ -->
@if (hasDigitalSection) {
<div class="preview-section">
<div class="preview-section__header">
<span class="preview-section__title">Digital</span>
<i class="fas fa-mobile-alt preview-section__icon"></i>
<span class="preview-section__title">Digital Facilities</span>
<span class="preview-section__badge preview-section__badge--digital">User Configured</span>
</div>
<div class="preview-body">
<div class="preview-grid preview-grid--three">
<div class="preview-digital-grid">
@for (facility of digitalFacilityLabels; track facility.key) {
<div class="preview-field">
<span class="preview-label">{{ facility.label }}</span>
<div class="preview-digital-card" [class.preview-digital-card--on]="digitalFacilityConfig?.[facility.key]">
<span class="preview-digital-label">{{ facility.label }}</span>
<span
class="preview-value preview-badge"
[class.preview-badge--yes]="digitalPreview?.accountFacilityConfig?.[facility.key]"
class="preview-digital-status"
[class.preview-digital-status--on]="digitalFacilityConfig?.[facility.key]"
>
{{ digitalPreview?.accountFacilityConfig?.[facility.key] | yesNo }}
{{ digitalFacilityConfig?.[facility.key] ? 'Enabled' : 'Disabled' }}
</span>
</div>
}
@@ -9,29 +9,65 @@
.preview-wrapper {
display: flex;
flex-direction: column;
gap: 1.25rem;
gap: 1rem;
padding: 0.5rem 0;
}
.preview-section {
border: 1px solid #e0e0e0;
border-radius: 8px;
border: 1px solid #e2e8f0;
border-radius: 10px;
overflow: hidden;
background: #fff;
box-shadow: 0 1px 3px rgb(0 0 0 / 6%);
box-shadow: 0 1px 4px rgb(0 0 0 / 5%);
&__header {
background: #f5f5f5;
border-bottom: 1px solid #e0e0e0;
display: flex;
align-items: center;
gap: 8px;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
padding: 0.6rem 1rem;
}
&__icon {
font-size: 13px;
color: #0ea5e9;
width: 16px;
text-align: center;
}
&__title {
font-size: 0.95rem;
font-weight: 600;
color: #333;
font-size: 0.82rem;
font-weight: 700;
color: #1e293b;
text-transform: uppercase;
letter-spacing: 0.04em;
letter-spacing: 0.06em;
flex: 1;
}
&__badge {
display: inline-flex;
align-items: center;
padding: 2px 10px;
border-radius: 999px;
font-size: 10px;
font-weight: 700;
background: #dbeafe;
color: #1d4ed8;
border: 1px solid #bfdbfe;
text-transform: uppercase;
&--digital {
background: #fef3c7;
color: #92400e;
border-color: #fde68a;
}
&--active {
background: #dcfce7 !important;
color: #15803d !important;
border-color: #bbf7d0 !important;
}
}
}
@@ -41,7 +77,7 @@
.preview-grid {
display: grid;
gap: 0.75rem 1.5rem;
gap: 0.85rem 1.5rem;
&--three {
grid-template-columns: repeat(3, 1fr);
@@ -59,36 +95,276 @@
.preview-field {
display: flex;
flex-direction: column;
gap: 0.15rem;
gap: 0.2rem;
}
.preview-label {
font-size: 0.72rem;
color: #888;
font-weight: 500;
font-size: 0.68rem;
color: #94a3b8;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
letter-spacing: 0.05em;
}
.preview-value {
font-size: 0.92rem;
color: #222;
font-size: 0.9rem;
color: #1e293b;
font-weight: 500;
}
.preview-muted {
color: #94a3b8;
font-style: italic;
}
.preview-mono {
font-family: monospace;
font-size: 0.88rem;
color: #334155;
}
.preview-badge {
display: inline-block;
padding: 0.1rem 0.55rem;
padding: 0.15rem 0.6rem;
border-radius: 12px;
font-size: 0.78rem;
font-size: 0.75rem;
font-weight: 600;
background: #ffebee;
color: #c62828;
background: #fef2f2;
color: #dc2626;
width: fit-content;
&--yes {
background: #e8f5e9;
color: #2e7d32;
background: #f0fdf4;
color: #15803d;
}
&--warn {
background: #fef3c7;
color: #92400e;
}
}
// Digital facilities grid
.preview-digital-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
@media (width <= 900px) {
grid-template-columns: repeat(3, 1fr);
}
@media (width <= 600px) {
grid-template-columns: repeat(2, 1fr);
}
}
.preview-digital-card {
display: flex;
flex-direction: column;
gap: 4px;
padding: 10px 12px;
border: 1.5px solid #e2e8f0;
border-radius: 8px;
background: #f8fafc;
&--on {
border-color: #86efac;
background: #f0fdf4;
}
}
.preview-digital-label {
font-size: 11px;
font-weight: 600;
color: #475569;
}
.preview-digital-status {
font-size: 11px;
font-weight: 700;
color: #dc2626;
&--on {
color: #15803d;
}
}
// ── Dormancy Monitoring ───────────────────────────────────────────
.preview-dormancy-timeline {
display: flex;
align-items: flex-start;
margin-bottom: 14px;
}
.preview-dorm-step {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
flex: 1;
&__dot {
width: 36px;
height: 36px;
border-radius: 50%;
border: 3px solid;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&--inactive {
border-color: #f59e0b;
background: #fffbeb;
&::after {
content: '';
width: 10px;
height: 10px;
border-radius: 50%;
background: #f59e0b;
}
}
&--dormant {
border-color: #ef4444;
background: #fef2f2;
&::after {
content: '';
width: 10px;
height: 10px;
border-radius: 50%;
background: #ef4444;
}
}
&--escheat {
border-color: #7c3aed;
background: #f5f3ff;
&::after {
content: '';
width: 10px;
height: 10px;
border-radius: 50%;
background: #7c3aed;
}
}
}
&__body {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
gap: 2px;
}
&__label {
font-size: 12px;
font-weight: 700;
color: #1e293b;
}
&__days {
font-size: 10.5px;
color: #64748b;
line-height: 1.3;
}
}
.preview-dorm-connector {
flex: 0 0 40px;
height: 3px;
background: linear-gradient(90deg, #e2e8f0, #cbd5e1);
margin-top: 16px;
border-radius: 2px;
}
.preview-dorm-flags {
display: flex;
gap: 10px;
flex-wrap: wrap;
}
.preview-dorm-flag {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 4px 12px;
border-radius: 20px;
font-size: 11.5px;
font-weight: 600;
background: #f1f5f9;
color: #94a3b8;
border: 1px solid #e2e8f0;
i {
font-size: 11px;
}
&--on {
background: #f0fdf4;
color: #15803d;
border-color: #bbf7d0;
}
}
// Non-Individual section
.preview-ni-label {
font-size: 11px;
font-weight: 700;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 8px;
}
.preview-ni-table-wrap {
overflow-x: auto;
border: 1px solid #e2e8f0;
border-radius: 8px;
margin-bottom: 12px;
}
.preview-ni-tbl {
width: 100%;
border-collapse: collapse;
font-size: 13px;
th {
padding: 8px 12px;
font-size: 11px;
font-weight: 700;
color: #374151;
text-transform: uppercase;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
text-align: left;
}
td {
padding: 9px 12px;
color: #1e293b;
border-bottom: 1px solid #f3f4f6;
}
}
.preview-ni-mode {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #374151;
padding: 8px 12px;
background: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: 8px;
&__label {
font-weight: 600;
}
}
@@ -83,18 +83,15 @@ export class CurrentAccountPreviewStepComponent implements OnChanges {
private translateService = inject(TranslateService);
/** Current Account Product Template */
@Input() currentAccountProductTemplate: any;
/** Current Account Template */
@Input() currentAccountTemplate: any;
/** Current Account Terms Form */
@Input() currentAccountTermsForm: any;
/** Current Account Term Form */
@Input() currentAccountTermForm: any;
/** Current Account Settings Form */
@Input() currentAccountSettingsForm: any;
/** Current Account */
@Input() currentAccount: any;
@Input() currentAccountDigital: any;
@Input() currentAccountNominee: any;
@Input() currentAccountCurrencyFlags: any;
@Input() activeClientMembers?: any;
/** Table Data Source */
@@ -136,15 +133,31 @@ export class CurrentAccountPreviewStepComponent implements OnChanges {
return this.currentAccountSettingsForm?.getRawValue?.() || this.currentAccount || {};
}
get digitalPreview(): any {
return this.currentAccountProductTemplate || {};
get digitalFacilityConfig(): any {
return (
this.currentAccountDigital?.accountFacilityConfig ??
this.currentAccountProductTemplate?.accountFacilityConfig ??
null
);
}
get hasDigitalSection(): boolean {
return !!(
this.digitalPreview?.accountFacilityConfig ||
this.digitalPreview?.accountFacilityLimits?.length ||
this.digitalPreview?.depositLimitKyc
return !!this.digitalFacilityConfig;
}
get currencyName(): string {
return (
this.currentAccountProductTemplate?.currency?.name ||
this.currentAccountTermsForm?.get?.('currencyCode')?.value ||
'—'
);
}
get decimalPlaces(): string | number {
return this.currentAccountProductTemplate?.currency?.decimalPlaces ?? '—';
}
get inMultiplesOf(): string | number {
return this.currentAccountProductTemplate?.currency?.inMultiplesOf ?? '—';
}
}