feat: merge updates - current account, savings, recurring deposits and shared components

This commit is contained in:
Nitesh Yadav
2026-04-29 15:20:31 +05:30
25 changed files with 6816 additions and 176 deletions
@@ -135,20 +135,37 @@ export class CreateCurrentAccountComponent {
const locale = this.settingsService.language.code;
const dateFormat = this.settingsService.dateFormat;
const monthDayFormat = 'dd MMMM';
const currentAccount = {
...this.currentAccount,
const details = this.currentAccount;
const currentAccount: any = {
...details,
...this.currentAccountCurrencyStep.currencyStepData,
...(this.currentAccountDigitalStep?.currentAccountDigital ?? {}),
charges: [],
submittedOnDate: this.dateUtils.formatDate(this.currentAccount.submittedOnDate, dateFormat),
submittedOnDate: this.dateUtils.formatDate(details.submittedOnDate, dateFormat),
dateFormat,
monthDayFormat,
locale
};
if (details.establishmentDate) {
currentAccount.establishmentDate = this.dateUtils.formatDate(details.establishmentDate, dateFormat);
}
if (details.startOperationDate) {
currentAccount.startOperationDate = this.dateUtils.formatDate(details.startOperationDate, dateFormat);
}
// strip UI-only flags not needed by backend
delete currentAccount.isNonIndividualClient;
if (this.currentAccountTemplate.clientId) {
currentAccount.clientId = this.currentAccountTemplate.clientId;
} else {
currentAccount.groupId = this.currentAccountTemplate.groupId;
}
console.log('Current Account Payload:', JSON.stringify(currentAccount, null, 2));
this.currentAccountsService.createCurrentAccount(currentAccount).subscribe((response: any) => {
this.router.navigate(
[
@@ -42,9 +42,23 @@ export class AddRelatedClientDialogComponent implements OnDestroy {
Validators.min(1),
Validators.max(100)
]
]
],
signingAuthority: [null],
role: [null]
});
readonly signingAuthorityOptions = [
{ value: 'SINGLE', label: 'Single' },
{ value: 'JOINTLY', label: 'Jointly' },
{ value: 'EITHER_OR_SURVIVOR', label: 'Either or Survivor' }
];
readonly roleOptions = [
{ value: 'PRIMARY', label: 'Primary' },
{ value: 'JOINT', label: 'Joint' },
{ value: 'NOMINEE', label: 'Nominee' }
];
searchResults: any[] = [];
selectedClient: any = null;
isSearching = false;
@@ -89,8 +103,10 @@ export class AddRelatedClientDialogComponent implements OnDestroy {
this.dialogRef.close({
clientId: this.selectedClient.id,
clientName: this.selectedClient.displayName,
clientCif: this.selectedClient.cifNumber || this.selectedClient.accountNumber || '',
sharePercentage: this.form.get('sharePercentage').value
clientCif: this.selectedClient.cifNumber || this.selectedClient.cifNo || '',
sharePercentage: this.form.get('sharePercentage').value,
signingAuthority: this.form.get('signingAuthority').value || null,
role: this.form.get('role').value || null
});
}
@@ -59,6 +59,22 @@
<mat-label>{{ 'labels.inputs.External ID' | translate }}</mat-label>
<input matInput formControlName="externalId" />
</mat-form-field>
<mat-form-field appearance="outline" class="sad-field" (click)="establishmentDatePicker.open()">
<mat-label>Establishment Date</mat-label>
<input matInput [matDatepicker]="establishmentDatePicker" formControlName="establishmentDate" />
<mat-datepicker-toggle matSuffix [for]="establishmentDatePicker"></mat-datepicker-toggle>
<mat-datepicker #establishmentDatePicker></mat-datepicker>
<mat-hint>Date of entity / business establishment</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="sad-field" (click)="startOperationDatePicker.open()">
<mat-label>Start of Operation Date</mat-label>
<input matInput [matDatepicker]="startOperationDatePicker" formControlName="startOperationDate" />
<mat-datepicker-toggle matSuffix [for]="startOperationDatePicker"></mat-datepicker-toggle>
<mat-datepicker #startOperationDatePicker></mat-datepicker>
<mat-hint>Date when operations commenced</mat-hint>
</mat-form-field>
}
</div>
</div>
@@ -74,12 +90,20 @@
<div class="sad-grid sad-grid--classification">
<mat-form-field appearance="outline" class="sad-field">
<mat-label>Account Type</mat-label>
<input matInput [value]="productTemplate.accountType || ''" readonly />
<mat-select [(ngModel)]="selectedAccountType" [ngModelOptions]="{ standalone: true }">
@for (opt of accountTypeOptions; track opt) {
<mat-option [value]="opt">{{ opt }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="sad-field">
<mat-label>Customer Segment</mat-label>
<input matInput [value]="productTemplate.customerSegment || ''" readonly />
<mat-select [(ngModel)]="selectedCustomerSegment" [ngModelOptions]="{ standalone: true }">
@for (opt of customerSegmentOptions; track opt) {
<mat-option [value]="opt">{{ opt }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="sad-field">
@@ -131,6 +155,560 @@
}
}
<!-- ══ Status of Account (RBI) — Individual Clients Only ════════ -->
@if (currentAccountProductSelected) {
<div class="soa-card">
<div class="soa-card__header">
<div class="soa-card__header-left">
<div class="soa-card__icon">
<i class="fas fa-id-card"></i>
</div>
<div>
<h3 class="soa-card__title">Status of Account</h3>
<span class="soa-card__sub">As per RBI guidelines — select account holder type</span>
</div>
</div>
<span class="soa-pill">RBI Compliant</span>
</div>
<!-- Auto-detected client info bar -->
@if (clientDob) {
<div class="soa-client-info-bar">
<div class="soa-client-info-bar__item">
<mat-icon>cake</mat-icon>
<span
>Date of Birth: <strong>{{ formatTemplateDate(clientDob) }}</strong></span
>
</div>
<div class="soa-client-info-bar__item">
<mat-icon>person_pin</mat-icon>
<span
>Age: <strong>{{ clientAge }} years</strong></span
>
</div>
@if (clientAge !== null && clientAge < 18) {
<span class="soa-auto-pill soa-auto-pill--minor">
<mat-icon>child_care</mat-icon> Auto-detected: Minor
</span>
} @else if (clientAge !== null && clientAge >= 60) {
<span class="soa-auto-pill soa-auto-pill--senior">
<mat-icon>elderly</mat-icon> Auto-detected: Senior Citizen
</span>
} @else {
<span class="soa-auto-pill soa-auto-pill--ok"><mat-icon>check_circle</mat-icon> Age Verified</span>
}
</div>
}
<!-- Status selector tiles -->
<div class="soa-tiles">
@for (opt of accountStatusOptions; track opt.value) {
<button
type="button"
class="soa-tile"
[class.soa-tile--selected]="selectedAccountStatus === opt.value"
[class.soa-tile--disabled]="isTileDisabled(opt.value)"
[disabled]="isTileDisabled(opt.value)"
(click)="!isTileDisabled(opt.value) && (selectedAccountStatus = opt.value)"
[title]="getTileTooltip(opt.value)"
>
<mat-icon class="soa-tile__icon" [style.color]="isTileDisabled(opt.value) ? '#cbd5e1' : opt.color">
{{ opt.icon }}
</mat-icon>
<span class="soa-tile__label">{{ opt.label }}</span>
@if (isTileDisabled(opt.value)) {
<span class="soa-tile__lock"><mat-icon>lock</mat-icon></span>
}
@if (selectedAccountStatus === opt.value) {
<mat-icon class="soa-tile__check">check_circle</mat-icon>
}
</button>
}
</div>
<!-- ── JOINT ACCOUNT ─────────────────────────────────────────── -->
@if (selectedAccountStatus === 'JOINT') {
<div class="soa-block">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #8b5cf6">A</span>
<div>
<span class="soa-block__title">Joint Account Holders</span>
<span class="soa-block__hint">Add all co-applicants linked to this joint current account</span>
</div>
</div>
<button
mat-flat-button
color="primary"
type="button"
class="soa-add-btn"
(click)="openAddJointHolderDialog()"
>
<mat-icon>person_add</mat-icon> Add Holder
</button>
</div>
<div class="soa-table-wrap">
<table class="soa-tbl">
<thead>
<tr>
<th class="soa-th soa-th--sr">Sr.</th>
<th class="soa-th">Holder Name</th>
<th class="soa-th">CIF No.</th>
<th class="soa-th soa-th--center">Ownership %</th>
<th class="soa-th soa-th--center">Signing Authority</th>
<th class="soa-th soa-th--center">Role</th>
<th class="soa-th soa-th--center">Action</th>
</tr>
</thead>
<tbody>
<!-- Primary account holder row (non-removable) -->
<tr class="soa-tr soa-tr--primary">
<td class="soa-td soa-td--sr">1</td>
<td class="soa-td">
<div class="soa-client-cell">
<div class="soa-avatar soa-avatar--primary">
{{ primaryClientName ? primaryClientName[0].toUpperCase() : '?' }}
</div>
<div>
<span class="soa-client-name">{{ primaryClientName || 'Primary Holder' }}</span>
<span class="soa-primary-tag">Primary Account Holder</span>
</div>
</div>
</td>
<td class="soa-td">
<span class="soa-mono">{{ primaryClientCif || '—' }}</span>
</td>
<td class="soa-td soa-td--center">
<input
class="soa-pct-input"
type="number"
min="0"
max="100"
[(ngModel)]="primaryHolderShare"
[ngModelOptions]="{ standalone: true }"
placeholder="0"
/>
<span class="soa-pct-unit">%</span>
</td>
<td class="soa-td soa-td--center">
<span class="soa-sign-toggle soa-sign-toggle--mandatory">
<mat-icon>lock</mat-icon> Mandatory
</span>
</td>
<td class="soa-td soa-td--center">Primary Account Holder</td>
<td class="soa-td soa-td--center"><span class="soa-no-action">—</span></td>
</tr>
<!-- Co-holders -->
@for (h of jointHolders; track h; let i = $index) {
<tr class="soa-tr" [class.soa-tr--mandatory]="h.isMandatory">
<td class="soa-td soa-td--sr">{{ i + 2 }}</td>
<td class="soa-td">
<div class="soa-client-cell">
<div class="soa-avatar" [class.soa-avatar--mandatory]="h.isMandatory">
{{ h.clientName[0].toUpperCase() }}
</div>
<div>
<span class="soa-client-name">{{ h.clientName }}</span>
@if (h.isMandatory) {
<span class="soa-mandatory-tag">Mandatory Signatory</span>
}
</div>
</div>
</td>
<td class="soa-td">
<span class="soa-mono">{{ h.clientCif || '—' }}</span>
</td>
<td class="soa-td soa-td--center">
<span class="soa-pct-badge" [class.soa-pct-badge--full]="h.sharePercentage === 100">
{{ h.sharePercentage }}%
</span>
</td>
<td class="soa-td soa-td--center">
<button
type="button"
class="soa-sign-toggle"
[class.soa-sign-toggle--mandatory]="h.isMandatory"
[class.soa-sign-toggle--optional]="!h.isMandatory"
(click)="toggleJointHolderMandatory(h)"
>
<mat-icon>{{ h.isMandatory ? 'lock' : 'lock_open' }}</mat-icon>
{{ h.isMandatory ? 'Mandatory' : 'Optional' }}
</button>
</td>
<td class="soa-td soa-td--center">Co-Holder</td>
<td class="soa-td soa-td--center">
<button mat-icon-button color="warn" type="button" (click)="removeJointHolder(i)">
<mat-icon>delete_outline</mat-icon>
</button>
</td>
</tr>
}
</tbody>
<tfoot>
<tr class="soa-tr-total">
<td colspan="3" class="soa-td soa-td--total-label">Total Ownership</td>
<td class="soa-td soa-td--center">
<span
class="soa-pct-badge"
[class.soa-pct-badge--ok]="totalJointSharePercentage === 100"
[class.soa-pct-badge--warn]="totalJointSharePercentage !== 100"
>
{{ totalJointSharePercentage }}%
</span>
</td>
<td class="soa-td soa-td--center">
<span class="soa-mandatory-count">
<mat-icon>lock</mat-icon> {{ mandatoryJointCount + 1 }} Mandatory
</span>
</td>
<td></td>
<td></td>
</tr>
</tfoot>
</table>
</div>
@if (jointHolders.length === 0) {
<div class="soa-empty" style="margin-top: 8px">
<mat-icon>group_add</mat-icon>
<span>Click <strong>Add Holder</strong> to add joint account holders.</span>
</div>
}
@if (totalJointSharePercentage !== 100) {
<div class="soa-alert soa-alert--warn" style="margin-top: 8px">
<mat-icon>warning_amber</mat-icon>
Total ownership is {{ totalJointSharePercentage }}%. It must equal 100%.
</div>
} @else {
<div class="soa-alert soa-alert--ok" style="margin-top: 8px">
<mat-icon>check_circle</mat-icon>
Ownership allocation is complete (100%).
</div>
}
</div>
<!-- Mode of Operation -->
<div class="soa-block soa-block--sep">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #7c3aed">B</span>
<div>
<span class="soa-block__title">Mode of Operation</span>
<span class="soa-block__hint">Auto-set from mandatory holders — override manually if required</span>
</div>
</div>
@if (modeOfOperation) {
<span class="soa-auto-badge"><mat-icon>auto_awesome</mat-icon> Auto</span>
}
</div>
@if (jointHolders.length === 0) {
<div class="soa-empty">
<mat-icon>how_to_vote</mat-icon>
<span>Add joint holders in Section A first.</span>
</div>
} @else {
<div class="soa-mode-grid">
@for (opt of modeOfOperationOptions; track opt.value) {
<button
type="button"
class="soa-mode-card"
[class.soa-mode-card--selected]="modeOfOperation === opt.value"
(click)="modeOfOperation = opt.value"
>
<mat-icon class="soa-mode-card__icon">{{ opt.icon }}</mat-icon>
<div class="soa-mode-card__code">{{ opt.label }}</div>
<div class="soa-mode-card__desc">{{ opt.sublabel }}</div>
@if (modeOfOperation === opt.value) {
<mat-icon class="soa-mode-card__check">check_circle</mat-icon>
}
</button>
}
</div>
<div class="soa-mode-summary" [class.soa-mode-summary--set]="modeOfOperation">
@if (modeOfOperation) {
<mat-icon>how_to_vote</mat-icon>
<span
>Mode of Operation: <strong>{{ selectedModeLabel }}</strong></span
>
<span class="soa-mode-summary__from">
({{ mandatoryJointCount }} Mandatory + {{ jointHolders.length - mandatoryJointCount }} Optional)
</span>
} @else {
<mat-icon>info</mat-icon>
<span>Set Mandatory/Optional for holders above to auto-select mode.</span>
}
</div>
}
</div>
}
<!-- ── MINOR ACCOUNT ─────────────────────────────────────────── -->
@if (selectedAccountStatus === 'MINOR') {
<div class="soa-block">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #f59e0b">
<mat-icon style="font-size: 14px; height: 14px; width: 14px">child_care</mat-icon>
</span>
<div>
<span class="soa-block__title">Minor Account Details</span>
<span class="soa-block__hint">As per RBI — guardian must be an existing registered client</span>
</div>
</div>
<span class="soa-pill soa-pill--minor">Guardian Required</span>
</div>
<div class="soa-minor-grid">
<mat-form-field appearance="outline" class="soa-field" (click)="minorDobPicker.open()">
<mat-label>Minor's Date of Birth</mat-label>
<input
matInput
[matDatepicker]="minorDobPicker"
[max]="maxDate"
[(ngModel)]="minorDob"
[ngModelOptions]="{ standalone: true }"
/>
<mat-datepicker-toggle matSuffix [for]="minorDobPicker"></mat-datepicker-toggle>
<mat-datepicker #minorDobPicker></mat-datepicker>
@if (clientDob) {
<mat-hint>Auto-filled from client profile</mat-hint>
}
</mat-form-field>
<mat-form-field appearance="outline" class="soa-field">
<mat-label>Age</mat-label>
<input matInput [value]="clientAge !== null ? clientAge + ' years' : '—'" readonly />
<mat-hint>Must be below 18 years</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="soa-field">
<mat-label>Date of Majority (Auto)</mat-label>
<input matInput [value]="majorityDate" readonly />
<mat-hint>Auto-calculated: DOB + 18 years</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="soa-field">
<mat-label>Guardian Relationship</mat-label>
<mat-select [(ngModel)]="guardianRelationship" [ngModelOptions]="{ standalone: true }">
@for (rel of guardianRelationshipOptions; track rel) {
<mat-option [value]="rel">{{ rel }}</mat-option>
}
</mat-select>
</mat-form-field>
</div>
<div class="soa-guardian-search">
<div class="soa-guardian-search__label">
<mat-icon>manage_accounts</mat-icon>
<span>Search &amp; Link Guardian (existing client)</span>
</div>
<mifosx-introducer-search (introducerChange)="onGuardianChange($event)"></mifosx-introducer-search>
@if (guardianName) {
<div class="soa-guardian-selected">
<mat-icon class="soa-guardian-selected__icon">verified_user</mat-icon>
<div>
<div class="soa-guardian-selected__name">{{ guardianName }}</div>
<div class="soa-guardian-selected__cif">CIF: {{ guardianCif || 'N/A' }}</div>
</div>
<span class="soa-pill soa-pill--ok">Guardian Linked</span>
</div>
}
</div>
<div class="soa-info-banner soa-info-banner--minor">
<mat-icon>info</mat-icon>
<ul>
<li>Minor below 10 years — Guardian must operate the account.</li>
<li>Minor above 10 years — Can operate independently (bank discretion).</li>
<li>Account must be converted to regular savings on attaining majority.</li>
<li>Fresh KYC of the minor must be done on majority.</li>
</ul>
</div>
</div>
}
<!-- ── SENIOR CITIZEN ────────────────────────────────────────── -->
@if (selectedAccountStatus === 'SENIOR_CITIZEN') {
<div class="soa-block">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #10b981">
<mat-icon style="font-size: 14px; height: 14px; width: 14px">elderly</mat-icon>
</span>
<div>
<span class="soa-block__title">Senior Citizen Account</span>
<span class="soa-block__hint">Age ≥ 60 years — eligible for preferential banking services</span>
</div>
</div>
<span class="soa-pill soa-pill--senior">Age ≥ 60</span>
</div>
<div class="soa-minor-grid" style="margin-bottom: 16px">
<mat-form-field appearance="outline" class="soa-field">
<mat-label>Date of Birth</mat-label>
<input matInput [value]="formatTemplateDate(clientDob)" readonly />
<mat-hint>From client profile</mat-hint>
</mat-form-field>
<mat-form-field appearance="outline" class="soa-field">
<mat-label>Age</mat-label>
<input matInput [value]="clientAge !== null ? clientAge + ' years' : '—'" readonly />
<mat-hint>Age ≥ 60 required for Senior Citizen</mat-hint>
</mat-form-field>
</div>
<div class="soa-sc-benefits">
<div class="soa-sc-benefit">
<mat-icon>priority_high</mat-icon>
<div>
<strong>Priority Service</strong>
<span>Dedicated counters / assisted banking at branches</span>
</div>
</div>
<div class="soa-sc-benefit">
<mat-icon>percent</mat-icon>
<div>
<strong>Preferential Interest Rate</strong>
<span>Additional 0.25%–0.50% on deposits (as per product)</span>
</div>
</div>
<div class="soa-sc-benefit">
<mat-icon>health_and_safety</mat-icon>
<div>
<strong>Doorstep Banking</strong>
<span>Eligible for home banking services (RBI circular)</span>
</div>
</div>
<div class="soa-sc-benefit">
<mat-icon>gavel</mat-icon>
<div>
<strong>Nomination Mandatory</strong>
<span>RBI mandate — ensure nomination is registered</span>
</div>
</div>
</div>
<div class="soa-info-banner soa-info-banner--senior">
<mat-icon>info</mat-icon>
<ul>
<li>Age proof (DOB document) mandatory at time of account opening.</li>
<li>Nomination registration is compulsory (Form DA-1).</li>
<li>Bank must offer dedicated helpline and priority grievance redressal.</li>
<li>Door-step banking services must be offered (RBI Master Direction 2023).</li>
</ul>
</div>
</div>
}
<!-- ── INDIVIDUAL MALE ───────────────────────────────────────── -->
@if (selectedAccountStatus === 'INDIVIDUAL_MALE') {
<div class="soa-block">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #3b82f6">
<mat-icon style="font-size: 14px; height: 14px; width: 14px">person</mat-icon>
</span>
<div>
<span class="soa-block__title">Individual Account (Male)</span>
<span class="soa-block__hint"
>Select special category if applicable — governs applicable schemes &amp; benefits</span
>
</div>
</div>
<span class="soa-pill" style="background: #dbeafe; color: #1d4ed8; border-color: #bfdbfe"
>Special Category</span
>
</div>
<div class="soa-category-grid">
@for (opt of specialCategoryOptions; track opt.value) {
<button
type="button"
class="soa-cat-tile"
[class.soa-cat-tile--selected]="selectedSpecialCategory === opt.value"
[class.soa-cat-tile--male]="true"
(click)="selectedSpecialCategory = opt.value"
>
<mat-icon class="soa-cat-tile__icon">{{ opt.icon }}</mat-icon>
<span class="soa-cat-tile__label">{{ opt.label }}</span>
@if (selectedSpecialCategory === opt.value) {
<mat-icon class="soa-cat-tile__check">check_circle</mat-icon>
}
</button>
}
</div>
<div class="soa-info-banner soa-info-banner--male">
<mat-icon>info</mat-icon>
<ul>
<li>Valid KYC documents required (Aadhaar / PAN / Passport).</li>
<li>Nomination registration is mandatory (Form DA-1).</li>
<li>Defence / Ex-Serviceman — eligible for special savings schemes (DSSP).</li>
<li>NRI — FCNR / NRE / NRO account rules apply as per FEMA guidelines.</li>
</ul>
</div>
</div>
}
<!-- ── INDIVIDUAL FEMALE ──────────────────────────────────────── -->
@if (selectedAccountStatus === 'INDIVIDUAL_FEMALE') {
<div class="soa-block">
<div class="soa-block__head">
<div class="soa-block__head-left">
<span class="soa-block__letter" style="background: #ec4899">
<mat-icon style="font-size: 14px; height: 14px; width: 14px">person_outline</mat-icon>
</span>
<div>
<span class="soa-block__title">Individual Account (Female)</span>
<span class="soa-block__hint"
>Select weaker section category if applicable — governs PMJDY &amp; priority schemes</span
>
</div>
</div>
<span class="soa-pill" style="background: #fce7f3; color: #be185d; border-color: #f9a8d4"
>Weaker Section</span
>
</div>
<div class="soa-category-grid">
@for (opt of weakerSectionOptions; track opt.value) {
<button
type="button"
class="soa-cat-tile"
[class.soa-cat-tile--selected]="selectedWeakerSection === opt.value"
[class.soa-cat-tile--female]="true"
(click)="selectedWeakerSection = opt.value"
>
<mat-icon class="soa-cat-tile__icon">{{ opt.icon }}</mat-icon>
<span class="soa-cat-tile__label">{{ opt.label }}</span>
@if (selectedWeakerSection === opt.value) {
<mat-icon class="soa-cat-tile__check">check_circle</mat-icon>
}
</button>
}
</div>
<div class="soa-info-banner soa-info-banner--female">
<mat-icon>info</mat-icon>
<ul>
<li>Valid KYC documents required (Aadhaar / PAN / Passport).</li>
<li>Nomination registration is mandatory (Form DA-1).</li>
<li>BPL / SC-ST / Minority — eligible for zero-balance accounts (RBI Priority Sector norms).</li>
<li>PMJDY beneficiary — ₹10,000 overdraft facility &amp; accident insurance cover applicable.</li>
<li>SHG members — group linkage lending &amp; special deposit rates applicable.</li>
</ul>
</div>
</div>
}
</div>
}
<!-- ══ Non-Individual Account Details (Indian Banking Style) ═════ -->
@if (isNonIndividualClient && currentAccountProductSelected) {
<div class="ni-card">
@@ -712,6 +712,941 @@ $ni-bg: #f9fafb;
}
}
// ── Status of Account (RBI) ───────────────────────────────────────────────
.soa-card {
margin-top: 16px;
border: 1px solid #e2e8f0;
border-radius: 10px;
background: #fff;
overflow: visible;
&__header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 24px;
background: #f8f9fa;
border-bottom: 1px solid #e8eaed;
border-radius: 10px 10px 0 0;
}
&__header-left {
display: flex;
align-items: center;
gap: 12px;
}
&__icon {
width: 36px;
height: 36px;
border-radius: 8px;
background: #ede9fe;
display: flex;
align-items: center;
justify-content: center;
i {
font-size: 16px;
color: #7c3aed;
}
}
&__title {
margin: 0;
font-size: 14px;
font-weight: 600;
color: #202124;
}
&__sub {
font-size: 11px;
color: #6b7280;
display: block;
}
}
.soa-pill {
display: inline-flex;
align-items: center;
padding: 3px 12px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
background: #ede9fe;
color: #7c3aed;
border: 1px solid #ddd6fe;
&--minor {
background: #fef3c7;
color: #b45309;
border-color: #fde68a;
}
&--senior {
background: #d1fae5;
color: #065f46;
border-color: #a7f3d0;
}
&--ok {
background: #dcfce7;
color: #15803d;
border-color: #bbf7d0;
}
}
.soa-client-info-bar {
display: flex;
align-items: center;
gap: 20px;
padding: 10px 24px;
background: #f0f9ff;
border-bottom: 1px solid #bae6fd;
flex-wrap: wrap;
&__item {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #0369a1;
mat-icon {
font-size: 16px;
height: 16px;
width: 16px;
}
strong {
font-weight: 700;
}
}
}
.soa-auto-pill {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
margin-left: auto;
mat-icon {
font-size: 13px;
height: 13px;
width: 13px;
}
&--minor {
background: #fef3c7;
color: #b45309;
border: 1px solid #fde68a;
}
&--senior {
background: #d1fae5;
color: #065f46;
border: 1px solid #a7f3d0;
}
&--ok {
background: #dcfce7;
color: #15803d;
border: 1px solid #bbf7d0;
}
}
.soa-tiles {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 12px;
padding: 16px 24px;
border-bottom: 1px solid #f1f5f9;
@media (width <= 900px) {
grid-template-columns: repeat(3, 1fr);
}
@media (width <= 600px) {
grid-template-columns: repeat(2, 1fr);
}
}
.soa-tile {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 14px 10px;
border: 2px solid #e2e8f0;
border-radius: 10px;
background: #fff;
cursor: pointer;
transition:
border-color 0.15s,
box-shadow 0.15s;
position: relative;
&:hover {
border-color: #a5b4fc;
box-shadow: 0 2px 8px rgb(99 102 241 / 12%);
}
&--selected {
border-color: #6366f1;
background: #eef2ff;
box-shadow: 0 2px 12px rgb(99 102 241 / 20%);
}
&__icon {
font-size: 28px;
height: 28px;
width: 28px;
}
&__label {
font-size: 12px;
font-weight: 600;
color: #374151;
text-align: center;
}
&__check {
position: absolute;
top: 6px;
right: 6px;
font-size: 16px;
height: 16px;
width: 16px;
color: #6366f1;
}
&__lock {
position: absolute;
top: 4px;
right: 4px;
display: flex;
align-items: center;
mat-icon {
font-size: 13px;
height: 13px;
width: 13px;
color: #94a3b8;
}
}
&--disabled {
opacity: 0.45;
cursor: not-allowed;
background: #f8fafc;
border-color: #e2e8f0;
box-shadow: none;
&:hover {
border-color: #e2e8f0;
box-shadow: none;
}
}
}
.soa-block {
padding: 16px 24px;
&--sep {
border-top: 1px dashed #e2e8f0;
}
&__head {
display: flex;
align-items: flex-start;
justify-content: space-between;
margin-bottom: 14px;
}
&__head-left {
display: flex;
align-items: flex-start;
gap: 12px;
}
&__letter {
width: 28px;
height: 28px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 13px;
font-weight: 700;
color: #fff;
flex-shrink: 0;
margin-top: 2px;
}
&__title {
font-size: 13px;
font-weight: 600;
color: #1f2937;
display: block;
}
&__hint {
font-size: 11px;
color: #6b7280;
display: block;
}
}
.soa-table-wrap {
overflow-x: auto;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
.soa-tbl {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.soa-th {
padding: 10px 14px;
background: #f8fafc;
text-align: left;
font-weight: 600;
font-size: 11px;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.04em;
border-bottom: 1px solid #e2e8f0;
&--sr {
width: 44px;
}
&--center {
text-align: center;
}
}
.soa-td {
padding: 10px 14px;
border-bottom: 1px solid #f1f5f9;
vertical-align: middle;
&--sr {
text-align: center;
font-size: 12px;
color: #94a3b8;
font-weight: 600;
}
&--center {
text-align: center;
}
&--total-label {
font-size: 12px;
font-weight: 600;
color: #475569;
text-align: right;
}
}
.soa-tr {
transition: background 0.1s;
&:hover {
background: #f8fafc;
}
&--mandatory {
background: #fdf4ff;
}
&--primary {
background: #f0f9ff;
border-left: 3px solid #0891b2;
}
}
.soa-avatar--primary {
background: #cffafe;
color: #0e7490;
}
.soa-primary-tag {
font-size: 10px;
color: #0e7490;
font-weight: 600;
background: #cffafe;
padding: 1px 7px;
border-radius: 999px;
display: inline-block;
}
.soa-pct-input {
width: 56px;
padding: 4px 6px;
border: 1px solid #cbd5e1;
border-radius: 6px;
font-size: 13px;
font-weight: 600;
text-align: center;
color: #1f2937;
&:focus {
outline: none;
border-color: #0891b2;
box-shadow: 0 0 0 2px rgb(8 145 178 / 15%);
}
&::-webkit-inner-spin-button,
&::-webkit-outer-spin-button {
opacity: 1;
}
}
.soa-pct-unit {
font-size: 13px;
font-weight: 600;
color: #64748b;
margin-left: 2px;
}
.soa-no-action {
color: #cbd5e1;
font-size: 16px;
}
.soa-tr-total {
background: #f8fafc;
}
.soa-client-cell {
display: flex;
align-items: center;
gap: 10px;
}
.soa-avatar {
width: 32px;
height: 32px;
border-radius: 50%;
background: #dbeafe;
color: #1d4ed8;
font-weight: 700;
font-size: 14px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
&--mandatory {
background: #ede9fe;
color: #7c3aed;
}
}
.soa-client-name {
font-weight: 600;
font-size: 13px;
color: #1f2937;
display: block;
}
.soa-mandatory-tag {
font-size: 10px;
color: #7c3aed;
font-weight: 600;
background: #ede9fe;
padding: 1px 7px;
border-radius: 999px;
}
.soa-mono {
font-family: 'Roboto Mono', monospace;
font-size: 12px;
color: #64748b;
}
.soa-pct-badge {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 2px 10px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
background: #f1f5f9;
color: #475569;
&--full,
&--ok {
background: #dcfce7;
color: #15803d;
}
&--warn {
background: #fef3c7;
color: #b45309;
}
}
.soa-sign-toggle {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: 6px;
font-size: 12px;
font-weight: 600;
border: 1px solid #e2e8f0;
cursor: pointer;
transition: all 0.15s;
mat-icon {
font-size: 14px;
height: 14px;
width: 14px;
}
&--mandatory {
background: #ede9fe;
color: #7c3aed;
border-color: #ddd6fe;
}
&--optional {
background: #f8fafc;
color: #94a3b8;
}
}
.soa-mandatory-count {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
font-weight: 600;
color: #7c3aed;
mat-icon {
font-size: 14px;
height: 14px;
width: 14px;
}
}
.soa-alert {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
padding: 8px 14px;
border-radius: 8px;
font-size: 13px;
font-weight: 500;
mat-icon {
font-size: 18px;
height: 18px;
width: 18px;
}
&--warn {
background: #fef3c7;
color: #b45309;
}
&--ok {
background: #dcfce7;
color: #15803d;
}
}
.soa-empty {
display: flex;
align-items: center;
gap: 10px;
padding: 20px;
background: #f8fafc;
border-radius: 8px;
font-size: 13px;
color: #64748b;
mat-icon {
color: #94a3b8;
font-size: 22px;
height: 22px;
width: 22px;
}
}
.soa-add-btn {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
}
.soa-mode-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
@media (width <= 700px) {
grid-template-columns: repeat(2, 1fr);
}
}
.soa-mode-card {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 12px 8px;
border: 2px solid #e2e8f0;
border-radius: 10px;
background: #fff;
cursor: pointer;
transition: all 0.15s;
&:hover {
border-color: #a5b4fc;
}
&--selected {
border-color: #7c3aed;
background: #faf5ff;
}
&__icon {
font-size: 24px;
height: 24px;
width: 24px;
color: #7c3aed;
}
&__code {
font-size: 13px;
font-weight: 700;
color: #1f2937;
}
&__desc {
font-size: 11px;
color: #6b7280;
text-align: center;
}
&__check {
position: absolute;
top: 6px;
right: 6px;
font-size: 16px;
height: 16px;
width: 16px;
color: #7c3aed;
}
}
.soa-mode-summary {
display: flex;
align-items: center;
gap: 8px;
margin-top: 12px;
padding: 8px 14px;
border-radius: 8px;
font-size: 13px;
background: #f8fafc;
color: #64748b;
mat-icon {
font-size: 18px;
height: 18px;
width: 18px;
}
&--set {
background: #faf5ff;
color: #7c3aed;
}
&__from {
font-size: 11px;
color: #94a3b8;
}
}
.soa-auto-badge {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 3px 10px;
border-radius: 999px;
font-size: 11px;
font-weight: 700;
background: #fef9c3;
color: #854d0e;
border: 1px solid #fde68a;
mat-icon {
font-size: 13px;
height: 13px;
width: 13px;
}
}
.soa-minor-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px 16px;
margin-bottom: 16px;
@media (width <= 700px) {
grid-template-columns: 1fr;
}
}
.soa-field {
width: 100%;
}
.soa-guardian-search {
margin-bottom: 12px;
&__label {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
font-weight: 600;
color: #374151;
margin-bottom: 8px;
mat-icon {
font-size: 18px;
height: 18px;
width: 18px;
color: #7c3aed;
}
}
}
.soa-guardian-selected {
display: flex;
align-items: center;
gap: 12px;
padding: 10px 14px;
background: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: 8px;
margin-top: 10px;
&__icon {
font-size: 24px;
height: 24px;
width: 24px;
color: #16a34a;
}
&__name {
font-size: 13px;
font-weight: 600;
color: #1f2937;
}
&__cif {
font-size: 11px;
color: #6b7280;
}
}
.soa-sc-benefits {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-bottom: 14px;
@media (width <= 600px) {
grid-template-columns: 1fr;
}
}
.soa-sc-benefit {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px;
background: #f0fdf4;
border: 1px solid #bbf7d0;
border-radius: 8px;
mat-icon {
color: #16a34a;
font-size: 20px;
height: 20px;
width: 20px;
flex-shrink: 0;
margin-top: 2px;
}
strong {
display: block;
font-size: 13px;
color: #1f2937;
font-weight: 600;
}
span {
font-size: 11px;
color: #6b7280;
}
}
.soa-category-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 16px;
@media (width <= 700px) {
grid-template-columns: repeat(2, 1fr);
}
}
.soa-cat-tile {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 14px 10px;
border: 2px solid #e2e8f0;
border-radius: 10px;
background: #fff;
cursor: pointer;
transition: all 0.15s;
&:hover {
border-color: #a5b4fc;
box-shadow: 0 2px 8px rgb(99 102 241 / 12%);
}
&--selected {
&.soa-cat-tile--male {
border-color: #3b82f6;
background: #eff6ff;
box-shadow: 0 2px 12px rgb(59 130 246 / 20%);
}
&.soa-cat-tile--female {
border-color: #ec4899;
background: #fdf2f8;
box-shadow: 0 2px 12px rgb(236 72 153 / 20%);
}
}
&__icon {
font-size: 26px;
height: 26px;
width: 26px;
color: #64748b;
}
&--selected.soa-cat-tile--male &__icon {
color: #3b82f6;
}
&--selected.soa-cat-tile--female &__icon {
color: #ec4899;
}
&__label {
font-size: 11px;
font-weight: 600;
color: #374151;
text-align: center;
line-height: 1.3;
}
&__check {
position: absolute;
top: 6px;
right: 6px;
font-size: 16px;
height: 16px;
width: 16px;
.soa-cat-tile--male & {
color: #3b82f6;
}
.soa-cat-tile--female & {
color: #ec4899;
}
}
}
.soa-info-banner {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px 14px;
border-radius: 8px;
font-size: 12px;
background: #eff6ff;
border: 1px solid #bfdbfe;
color: #1e40af;
mat-icon {
font-size: 18px;
height: 18px;
width: 18px;
flex-shrink: 0;
margin-top: 2px;
}
ul {
margin: 4px 0 0;
padding-left: 18px;
li {
margin-bottom: 3px;
}
}
&--minor {
background: #fefce8;
border-color: #fde68a;
color: #92400e;
}
&--senior {
background: #f0fdf4;
border-color: #bbf7d0;
color: #065f46;
}
&--male {
background: #eff6ff;
border-color: #bfdbfe;
color: #1e40af;
}
&--female {
background: #fdf2f8;
border-color: #f9a8d4;
color: #9d174d;
}
}
// ── Account Feature Toggles ────────────────────────────────────────────────
.sad-toggles {
display: flex;
@@ -8,7 +8,7 @@
/** Angular Imports */
import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorRef } from '@angular/core';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule, FormsModule } from '@angular/forms';
import { Dates } from 'app/core/utils/dates';
/** Custom Services */
@@ -17,20 +17,7 @@ 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';
@@ -49,16 +36,6 @@ import { AddRelatedClientDialogComponent } from './add-related-client-dialog/add
FaIconComponent,
MatStepperNext,
IntroducerSearchComponent,
MatTable,
MatColumnDef,
MatHeaderCellDef,
MatHeaderCell,
MatCellDef,
MatCell,
MatHeaderRowDef,
MatHeaderRow,
MatRowDef,
MatRow,
MatIcon,
FormsModule
]
@@ -72,20 +49,13 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
private dateUtils = inject(Dates);
private cdr = inject(ChangeDetectorRef);
/** Current Account Template */
@Input() currentAccountTemplate: any;
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
maxDate = new Date();
/** Product Data */
productData: any;
/** Field Officer Data */
fieldOfficerData: any;
/** For edit current account form */
isFieldOfficerPatched = false;
/** Current Account Details Form */
currentAccountDetailsForm: UntypedFormGroup;
currentAccountProductSelected = false;
@@ -93,7 +63,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
introducerName: string | null = null;
introducerCif: string | null = null;
/** Non-Individual section */
// ── Non-Individual (existing) ──────────────────────────────────────────
isNonIndividualClient = false;
relatedClients: Array<{
clientId: number;
@@ -111,11 +81,92 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
{ value: 'ONLY_ONE', label: 'Single', sublabel: 'Only 1 Person', icon: 'person' }
];
/** Current Account Template with product data */
@Output() currentAccountProductTemplate = new EventEmitter();
/** Local copy of product template for displaying classification info */
productTemplate: any = null;
accountTypeOptions: string[] = [];
customerSegmentOptions: string[] = [];
selectedAccountType: string | null = null;
selectedCustomerSegment: string | null = null;
// ── Client info (auto-fetched) ─────────────────────────────────────────
clientDob: Date | null = null;
clientAge: number | null = null;
clientGender: string | null = null;
primaryClientName = '';
primaryClientCif = '';
primaryClientId: number | null = null;
primaryHolderShare = 0;
// ── Status of Account (RBI) ────────────────────────────────────────────
readonly accountStatusOptions = [
{ value: 'INDIVIDUAL_MALE', label: 'Individual (Male)', icon: 'person', color: '#3b82f6' },
{ value: 'INDIVIDUAL_FEMALE', label: 'Individual (Female)', icon: 'person_outline', color: '#ec4899' },
{ value: 'SINGLE', label: 'Single Account', icon: 'account_circle', color: '#0891b2' },
{ value: 'JOINT', label: 'Joint Account', icon: 'group', color: '#8b5cf6' },
{ value: 'MINOR', label: 'Minor Account', icon: 'child_care', color: '#f59e0b' },
{ value: 'SENIOR_CITIZEN', label: 'Senior Citizen', icon: 'elderly', color: '#10b981' }
];
selectedAccountStatus = '';
// ── Joint Account (Status of Account) ─────────────────────────────────
jointHolders: Array<{
clientId: number;
clientName: string;
clientCif: string;
sharePercentage: number;
isMandatory: boolean;
signingAuthority?: string;
role?: string;
}> = [];
modeOfOperation = '';
readonly modeOfOperationOptions = [
{ value: 'EITHER_OR_SURVIVOR', label: 'E or S', sublabel: 'Either or Survivor', icon: 'how_to_vote' },
{ value: 'FORMER_OR_SURVIVOR', label: 'F or S', sublabel: 'Former or Survivor', icon: 'person' },
{ value: 'JOINTLY', label: 'Jointly', sublabel: 'All Must Sign', icon: 'groups' },
{ value: 'ANYONE_OR_SURVIVOR', label: 'Anyone', sublabel: 'Anyone or Survivor', icon: 'group' }
];
// ── Individual Male — Special Category ────────────────────────────────
selectedSpecialCategory = '';
readonly specialCategoryOptions = [
{ value: 'DEFENCE', label: 'Defence Personnel', icon: 'military_tech' },
{ value: 'EX_SERVICEMAN', label: 'Ex-Serviceman', icon: 'shield' },
{ value: 'GOVT_EMPLOYEE', label: 'Government Employee', icon: 'account_balance' },
{ value: 'PHYSICALLY_CHALLENGED', label: 'Physically Challenged', icon: 'accessible' },
{ value: 'NRI', label: 'NRI (Non-Resident Indian)', icon: 'flight' },
{ value: 'NONE', label: 'None / General', icon: 'person' }
];
// ── Individual Female — Weaker Section ────────────────────────────────
selectedWeakerSection = '';
readonly weakerSectionOptions = [
{ value: 'BPL', label: 'BPL (Below Poverty Line)', icon: 'volunteer_activism' },
{ value: 'SC_ST', label: 'SC / ST Category', icon: 'groups' },
{ value: 'MINORITY', label: 'Minority Community', icon: 'diversity_3' },
{ value: 'SHG', label: 'Self Help Group Member', icon: 'handshake' },
{ value: 'PMJDY', label: 'PMJDY Beneficiary', icon: 'savings' },
{ value: 'NONE', label: 'None / General', icon: 'person_outline' }
];
// ── Minor Account ──────────────────────────────────────────────────────
guardianClientId: number | null = null;
guardianName = '';
guardianCif = '';
guardianRelationship = '';
minorDob: Date | null = null;
readonly guardianRelationshipOptions = [
'Father',
'Mother',
'Legal Guardian',
'Court Appointed Guardian'
];
constructor() {
this.createCurrentAccountDetailsForm();
@@ -124,9 +175,23 @@ 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) => {
const clientId = this.currentAccountTemplate?.clientId;
if (clientId) {
this.primaryClientId = clientId;
this.clientsService.getClientData(String(clientId)).subscribe((client: any) => {
this.isNonIndividualClient = client?.legalForm?.id === 2;
this.primaryClientName = client?.displayName ?? client?.fullname ?? '';
this.primaryClientCif = client?.cifNumber ?? client?.cifNo ?? '';
if (client?.dateOfBirth) {
const dob = this.parseClientDob(client.dateOfBirth);
if (dob) {
this.clientDob = dob;
this.clientAge = this.calculateAge(dob);
this.clientGender = client.gender?.name?.toLowerCase() ?? null;
this.autoDetectAccountStatus();
}
}
this.cdr.detectChanges();
});
}
@@ -149,9 +214,6 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
}
}
/**
* Creates current account details form.
*/
createCurrentAccountDetailsForm() {
this.currentAccountDetailsForm = this.formBuilder.group({
productId: [
@@ -166,22 +228,19 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
externalId: [''],
balanceSwapOption: [false],
msme: [false],
autoDebitFacility: [false]
autoDebitFacility: [false],
establishmentDate: [''],
startOperationDate: ['']
});
}
/**
* Fetches current account product template on productId value changes
*/
buildDependencies() {
const entityId = this.currentAccountTemplate.groupId || this.currentAccountTemplate.clientId;
this.currentAccountDetailsForm.get('productId').valueChanges.subscribe((productId: string) => {
this.currentAccountsService
.getCurrentAccountTemplate(entityId, productId, this.currentAccountTemplate.groupId ? true : false)
.subscribe((response: any) => {
// Ensure currency is included in the product template
if (!response.currency) {
// Try to get currency from the selected product
const selectedProduct = this.currentAccountTemplate.productOptions?.find(
(product: any) => product.id == productId
);
@@ -192,10 +251,13 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
}
}
this.productTemplate = response;
this.accountTypeOptions = this.extractOptions(response.accountTypeOptions, response.accountType);
this.customerSegmentOptions = this.extractOptions(response.customerSegmentOptions, response.customerSegment);
this.selectedAccountType = response.accountType ?? null;
this.selectedCustomerSegment = response.customerSegment ?? null;
this.currentAccountProductTemplate.emit(response);
this.fieldOfficerData = response.fieldOfficerOptions;
this.currentAccountProductSelected = true;
// Pre-populate toggles from product defaults (user can override)
this.currentAccountDetailsForm.patchValue({
balanceSwapOption: !!response.balanceSwapOption,
msme: !!response.msme,
@@ -212,13 +274,15 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
});
}
/**
* Formats template dates into the current CBS date format.
*/
formatTemplateDate(value: any): string {
if (!value) {
return '';
private extractOptions(options: any[], fallback?: string): string[] {
if (Array.isArray(options) && options.length > 0) {
return options.map((o: any) => o.value ?? o.name ?? o);
}
return fallback ? [fallback] : [];
}
formatTemplateDate(value: any): string {
if (!value) return '';
let dateValue: Date | null = null;
if (value instanceof Date) {
dateValue = value;
@@ -233,15 +297,125 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
return this.dateUtils.formatDate(dateValue, 'M/d/yyyy') || '';
}
/**
* Returns current account form value.
*/
// ── Client DOB helpers ─────────────────────────────────────────────────
private parseClientDob(dob: any): Date | null {
if (!dob) return null;
if (Array.isArray(dob) && dob.length >= 3) return new Date(dob[0], dob[1] - 1, dob[2]);
const d = new Date(dob);
return isNaN(d.getTime()) ? null : d;
}
private calculateAge(dob: Date): number {
const today = this.settingsService.businessDate ?? new Date();
let age = today.getFullYear() - dob.getFullYear();
const m = today.getMonth() - dob.getMonth();
if (m < 0 || (m === 0 && today.getDate() < dob.getDate())) age--;
return age;
}
private autoDetectAccountStatus(): void {
if (this.clientAge === null) return;
if (this.clientAge < 18) {
this.selectedAccountStatus = 'MINOR';
this.minorDob = this.clientDob;
} else if (this.clientAge >= 60) {
this.selectedAccountStatus = 'SENIOR_CITIZEN';
} else if (this.clientGender === 'female') {
this.selectedAccountStatus = 'INDIVIDUAL_FEMALE';
} else {
this.selectedAccountStatus = 'INDIVIDUAL_MALE';
}
}
isTileDisabled(value: string): boolean {
if (value === 'MINOR') return this.clientAge === null || this.clientAge >= 18;
if (value === 'SENIOR_CITIZEN') return this.clientAge === null || this.clientAge < 60;
return false;
}
getTileTooltip(value: string): string {
if (value === 'MINOR' && this.clientAge !== null && this.clientAge >= 18)
return `Age ${this.clientAge} yrs — Minor only allowed below 18 years`;
if (value === 'SENIOR_CITIZEN' && this.clientAge !== null && this.clientAge < 60)
return `Age ${this.clientAge} yrs — Senior Citizen only allowed at 60+ years`;
if (value === 'MINOR' && this.clientAge === null) return 'DOB not available — cannot verify age';
if (value === 'SENIOR_CITIZEN' && this.clientAge === null) return 'DOB not available — cannot verify age';
return '';
}
// ── Introducer ─────────────────────────────────────────────────────────
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;
}
onGuardianChange(event: { clientId: number; displayName: string; cifNumber?: string } | null): void {
this.guardianClientId = event?.clientId ?? null;
this.guardianName = event?.displayName ?? '';
this.guardianCif = event?.cifNumber ?? '';
}
// ── Joint Account (Status of Account) ─────────────────────────────────
openAddJointHolderDialog(): void {
const ref = this.dialog.open(AddRelatedClientDialogComponent, {
width: '500px',
panelClass: 'arc-dialog-panel',
disableClose: true
});
ref.afterClosed().subscribe((result) => {
if (result) {
const duplicate = this.jointHolders.some((h) => h.clientCif && h.clientCif === result.clientCif);
if (duplicate) {
alert(`Account holder with CIF ${result.clientCif} is already added.`);
return;
}
this.jointHolders = [
...this.jointHolders,
{ ...result, isMandatory: false }];
this.autoSetModeOfOperation();
}
});
}
removeJointHolder(index: number): void {
this.jointHolders = this.jointHolders.filter((_, i) => i !== index);
this.autoSetModeOfOperation();
}
toggleJointHolderMandatory(holder: any): void {
holder.isMandatory = !holder.isMandatory;
this.autoSetModeOfOperation();
}
autoSetModeOfOperation(): void {
const mandatory = this.jointHolders.filter((h) => h.isMandatory).length;
if (mandatory === 0) this.modeOfOperation = 'EITHER_OR_SURVIVOR';
else if (mandatory === 1) this.modeOfOperation = 'FORMER_OR_SURVIVOR';
else this.modeOfOperation = 'JOINTLY';
}
get totalJointSharePercentage(): number {
return (this.primaryHolderShare || 0) + this.jointHolders.reduce((sum, h) => sum + (h.sharePercentage || 0), 0);
}
get mandatoryJointCount(): number {
return this.jointHolders.filter((h) => h.isMandatory).length;
}
get selectedModeLabel(): string {
return this.modeOfOperationOptions.find((o) => o.value === this.modeOfOperation)?.sublabel ?? '';
}
get majorityDate(): string {
const dob = this.minorDob ?? this.clientDob;
if (!dob) return '';
const d = new Date(dob);
d.setFullYear(d.getFullYear() + 18);
return this.dateUtils.formatDate(d, 'M/d/yyyy') || '';
}
// ── Non-Individual (existing) helpers ──────────────────────────────────
openAddRelatedClientDialog(): void {
const ref = this.dialog.open(AddRelatedClientDialogComponent, {
width: '500px',
@@ -273,16 +447,11 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
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';
}
if (mandatory === 0) this.signatoryRights = 'ANYONE';
else if (mandatory === 1) this.signatoryRights = 'ONE_MANDATORY';
else this.signatoryRights = 'TWO_MANDATORY';
}
get totalSharePercentage(): number {
@@ -305,6 +474,48 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
value.introducerName = this.introducerName;
value.introducerCif = this.introducerCif;
}
value.isNonIndividualClient = this.isNonIndividualClient;
// accountStatus applies to all client types
value.accountStatus = this.selectedAccountStatus || null;
if (this.selectedAccountStatus === 'INDIVIDUAL_MALE' && this.selectedSpecialCategory)
value.specialCategory = this.selectedSpecialCategory;
if (this.selectedAccountStatus === 'INDIVIDUAL_FEMALE' && this.selectedWeakerSection)
value.weakerSection = this.selectedWeakerSection;
if (this.selectedAccountStatus === 'JOINT') {
value.modeOfOperation = this.modeOfOperation || null;
value.jointHolders = [
{
clientId: this.primaryClientId,
clientName: this.primaryClientName,
clientCif: this.primaryClientCif,
sharePercentage: this.primaryHolderShare,
isMandatory: true,
isPrimary: true,
signingAuthority: 'Mandatory',
role: 'Primary Account Holder'
},
...this.jointHolders.map((h) => ({
clientId: h.clientId,
clientName: h.clientName,
clientCif: h.clientCif,
sharePercentage: h.sharePercentage,
isMandatory: h.isMandatory,
isPrimary: false,
signingAuthority: h.signingAuthority || (h.isMandatory ? 'Mandatory' : 'Optional'),
role: h.role || 'Co-Holder'
}))
];
}
if (this.selectedAccountStatus === 'MINOR') {
value.guardianClientId = this.guardianClientId;
value.guardianName = this.guardianName;
value.guardianCif = this.guardianCif;
value.guardianRelationship = this.guardianRelationship;
value.minorDob = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'yyyy-MM-dd') : null;
}
// non-individual: partners are a separate dataset
if (this.isNonIndividualClient) {
value.signatoryRights = this.signatoryRights || null;
value.partners = this.relatedClients.map((c) => ({
@@ -42,6 +42,18 @@
<span class="preview-value preview-muted">Unassigned</span>
}
</div>
@if (currentAccount?.establishmentDate) {
<div class="preview-field">
<span class="preview-label">Establishment Date</span>
<span class="preview-value">{{ currentAccount.establishmentDate | dateFormat }}</span>
</div>
}
@if (currentAccount?.startOperationDate) {
<div class="preview-field">
<span class="preview-label">Start of Operation Date</span>
<span class="preview-value">{{ currentAccount.startOperationDate | dateFormat }}</span>
</div>
}
</div>
</div>
</div>
@@ -157,7 +169,262 @@
</div>
}
<!-- ══ 4. Currency & Account Flags ══════════════════════════════ -->
<!-- ══ 4. Status of Account ══════════════════════════════════════ -->
@if (currentAccount?.accountStatus) {
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-id-card preview-section__icon"></i>
<span class="preview-section__title">Status of Account</span>
<span class="preview-section__badge">RBI Guidelines</span>
</div>
<div class="preview-body">
<!-- Status Tile -->
<div class="preview-soa-status-row">
<span class="preview-soa-tile">
@switch (currentAccount.accountStatus) {
@case ('INDIVIDUAL_MALE') {
<i class="fas fa-male"></i> Individual (Male)
}
@case ('INDIVIDUAL_FEMALE') {
<i class="fas fa-female"></i> Individual (Female)
}
@case ('SINGLE') {
<i class="fas fa-user-circle"></i> Single Account
}
@case ('JOINT') {
<i class="fas fa-users"></i> Joint Account
}
@case ('MINOR') {
<i class="fas fa-child"></i> Minor Account
}
@case ('SENIOR_CITIZEN') {
<i class="fas fa-user-clock"></i> Senior Citizen
}
@default {
{{ currentAccount.accountStatus }}
}
}
</span>
</div>
<!-- JOINT: Mode of Operation + Holders Table -->
@if (currentAccount.accountStatus === 'JOINT') {
<div class="preview-grid preview-grid--three" style="margin-top: 12px">
<div class="preview-field">
<span class="preview-label">Mode of Operation</span>
<span class="preview-value">
@switch (currentAccount.modeOfOperation) {
@case ('EITHER_OR_SURVIVOR') {
Either or Survivor
}
@case ('FORMER_OR_SURVIVOR') {
Former or Survivor
}
@case ('JOINTLY') {
Jointly
}
@case ('ANYONE_OR_SURVIVOR') {
Anyone or Survivor
}
@default {
{{ currentAccount.modeOfOperation || '—' }}
}
}
</span>
</div>
</div>
@if (currentAccount.jointHolders?.length) {
<div class="preview-soa-table-wrap">
<table class="preview-soa-tbl">
<thead>
<tr>
<th class="preview-soa-th">Sr.</th>
<th class="preview-soa-th">Holder Name</th>
<th class="preview-soa-th">CIF No.</th>
<th class="preview-soa-th">Ownership %</th>
<th class="preview-soa-th">Signing</th>
<th class="preview-soa-th">Role</th>
</tr>
</thead>
<tbody>
@for (holder of currentAccount.jointHolders; track holder.clientId; let i = $index) {
<tr [class.preview-soa-tr--primary]="holder.isPrimary">
<td class="preview-soa-td">{{ i + 1 }}</td>
<td class="preview-soa-td">
{{ holder.clientName || holder.displayName || '—' }}
@if (holder.isPrimary) {
<span class="preview-soa-primary-tag">Primary</span>
}
</td>
<td class="preview-soa-td preview-mono">{{ holder.clientCif || holder.cifNumber || '—' }}</td>
<td class="preview-soa-td">{{ holder.sharePercentage ?? '—' }}%</td>
<td class="preview-soa-td">
<span class="preview-badge" [class.preview-badge--yes]="holder.isMandatory">
{{ holder.signingAuthority || (holder.isMandatory ? 'Mandatory' : 'Optional') }}
</span>
</td>
<td class="preview-soa-td">
{{ holder.role || (holder.isPrimary ? 'Primary Account Holder' : 'Co-Holder') }}
</td>
</tr>
}
</tbody>
</table>
</div>
}
}
<!-- INDIVIDUAL_MALE: Special Category -->
@if (currentAccount.accountStatus === 'INDIVIDUAL_MALE' && currentAccount.specialCategory) {
<div class="preview-grid preview-grid--three" style="margin-top: 12px">
<div class="preview-field">
<span class="preview-label">Special Category</span>
<span class="preview-value">
@switch (currentAccount.specialCategory) {
@case ('DEFENCE') {
Defence Personnel
}
@case ('EX_SERVICEMAN') {
Ex-Serviceman
}
@case ('GOVT_EMPLOYEE') {
Government Employee
}
@case ('PHYSICALLY_CHALLENGED') {
Physically Challenged
}
@case ('NRI') {
NRI (Non-Resident Indian)
}
@case ('NONE') {
None / General
}
@default {
{{ currentAccount.specialCategory }}
}
}
</span>
</div>
</div>
}
<!-- INDIVIDUAL_FEMALE: Weaker Section -->
@if (currentAccount.accountStatus === 'INDIVIDUAL_FEMALE' && currentAccount.weakerSection) {
<div class="preview-grid preview-grid--three" style="margin-top: 12px">
<div class="preview-field">
<span class="preview-label">Weaker Section</span>
<span class="preview-value">
@switch (currentAccount.weakerSection) {
@case ('BPL') {
BPL (Below Poverty Line)
}
@case ('SC_ST') {
SC / ST Category
}
@case ('MINORITY') {
Minority Community
}
@case ('SHG') {
Self Help Group Member
}
@case ('PMJDY') {
PMJDY Beneficiary
}
@case ('NONE') {
None / General
}
@default {
{{ currentAccount.weakerSection }}
}
}
</span>
</div>
</div>
}
<!-- MINOR: Guardian Details -->
@if (currentAccount.accountStatus === 'MINOR') {
<div class="preview-grid preview-grid--three" style="margin-top: 12px">
@if (currentAccount.guardianName) {
<div class="preview-field">
<span class="preview-label">Guardian Name</span>
<span class="preview-value">{{ currentAccount.guardianName }}</span>
</div>
}
@if (currentAccount.guardianCif) {
<div class="preview-field">
<span class="preview-label">Guardian CIF</span>
<span class="preview-value preview-mono">{{ currentAccount.guardianCif }}</span>
</div>
}
@if (currentAccount.guardianRelationship) {
<div class="preview-field">
<span class="preview-label">Relationship</span>
<span class="preview-value">{{ currentAccount.guardianRelationship }}</span>
</div>
}
@if (currentAccount.minorDob) {
<div class="preview-field">
<span class="preview-label">Minor DOB</span>
<span class="preview-value">{{ currentAccount.minorDob | dateFormat }}</span>
</div>
}
</div>
}
</div>
</div>
}
<!-- ══ 4b. Partners / Related Clients (Non-Individual only) ═════ -->
@if (currentAccount?.isNonIndividualClient) {
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-handshake preview-section__icon"></i>
<span class="preview-section__title">Partners / Related Clients</span>
@if (currentAccount?.signatoryRights) {
<span class="preview-section__badge preview-section__badge--digital">{{
currentAccount.signatoryRights
}}</span>
}
</div>
<div class="preview-body">
@if (currentAccount?.partners?.length) {
<div class="preview-soa-table-wrap">
<table class="preview-soa-tbl">
<thead>
<tr>
<th class="preview-soa-th">Sr.</th>
<th class="preview-soa-th">Partner / Client Name</th>
<th class="preview-soa-th">CIF No.</th>
<th class="preview-soa-th">Ownership %</th>
<th class="preview-soa-th">Signing</th>
</tr>
</thead>
<tbody>
@for (p of currentAccount.partners; track p.clientId; let i = $index) {
<tr>
<td class="preview-soa-td">{{ i + 1 }}</td>
<td class="preview-soa-td">{{ p.clientName || '—' }}</td>
<td class="preview-soa-td preview-mono">{{ p.clientCif || '—' }}</td>
<td class="preview-soa-td">{{ p.sharePercentage ?? '—' }}%</td>
<td class="preview-soa-td">
<span class="preview-badge" [class.preview-badge--yes]="p.isMandatory">
{{ p.isMandatory ? 'Mandatory' : 'Optional' }}
</span>
</td>
</tr>
}
</tbody>
</table>
</div>
} @else {
<p class="preview-muted">No related clients / partners added.</p>
}
</div>
</div>
}
<!-- ══ 5. Currency & Account Flags ══════════════════════════════ -->
<div class="preview-section">
<div class="preview-section__header">
<i class="fas fa-coins preview-section__icon"></i>
@@ -112,11 +112,6 @@
font-weight: 500;
}
.preview-muted {
color: #94a3b8;
font-style: italic;
}
.preview-mono {
font-family: monospace;
font-size: 0.88rem;
@@ -376,3 +371,91 @@
.margin-t {
margin-top: 1em;
}
// ── Status of Account ─────────────────────────────────────────────
.preview-soa-status-row {
display: flex;
align-items: center;
gap: 10px;
}
.preview-soa-tile {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 8px 20px;
border-radius: 999px;
font-size: 14px;
font-weight: 700;
background: #eff6ff;
color: #1d4ed8;
border: 2px solid #bfdbfe;
i {
font-size: 16px;
}
&--entity {
background: #fef3c7;
color: #92400e;
border-color: #fde68a;
}
}
.preview-soa-table-wrap {
margin-top: 14px;
overflow-x: auto;
border-radius: 8px;
border: 1px solid #e2e8f0;
}
.preview-soa-tbl {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.preview-soa-th {
background: #f8fafc;
padding: 8px 12px;
text-align: left;
font-weight: 600;
color: #475569;
border-bottom: 1px solid #e2e8f0;
white-space: nowrap;
}
.preview-soa-td {
padding: 10px 12px;
border-bottom: 1px solid #f1f5f9;
color: #0f172a;
vertical-align: middle;
}
.preview-soa-tr--primary {
background: #f0f9ff;
.preview-soa-td {
font-weight: 600;
}
}
.preview-soa-primary-tag {
display: inline-block;
margin-left: 6px;
padding: 1px 8px;
border-radius: 999px;
font-size: 10px;
font-weight: 700;
background: #dbeafe;
color: #1d4ed8;
border: 1px solid #bfdbfe;
vertical-align: middle;
}
.preview-muted {
color: #94a3b8;
font-size: 13px;
font-style: italic;
margin: 0;
}
@@ -68,6 +68,11 @@ export class CurrentAccountsService {
return this.http.get(`/currentaccounts/${accountId}`, { params: httpParams });
}
getClientCurrentAccounts(clientId: string): Observable<any> {
const httpParams = new HttpParams().set('clientId', clientId);
return this.http.get('/currentaccounts', { params: httpParams });
}
/**
* @param accountId Current Account Id of account to get data for.
* @returns {Observable<any>} Current account and template.