feat: update savings/current/rd account UI flows
This commit is contained in:
+1
@@ -116,6 +116,7 @@
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
|
||||
|
||||
+19
-3
@@ -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
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
+546
@@ -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>
|
||||
@@ -139,6 +155,536 @@
|
||||
}
|
||||
}
|
||||
|
||||
<!-- ══ 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 & 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 & 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 & 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 & accident insurance cover applicable.</li>
|
||||
<li>SHG members — group linkage lending & special deposit rates applicable.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ══ Non-Individual Account Details (Indian Banking Style) ═════ -->
|
||||
@if (isNonIndividualClient && currentAccountProductSelected) {
|
||||
<div class="ni-card">
|
||||
|
||||
+935
@@ -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;
|
||||
|
||||
+238
-74
@@ -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,16 +81,86 @@ 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;
|
||||
}> = [];
|
||||
|
||||
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();
|
||||
}
|
||||
@@ -128,9 +168,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();
|
||||
});
|
||||
}
|
||||
@@ -153,39 +207,27 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates current account details form.
|
||||
*/
|
||||
createCurrentAccountDetailsForm() {
|
||||
this.currentAccountDetailsForm = this.formBuilder.group({
|
||||
productId: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
submittedOnDate: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
productId: ['', Validators.required],
|
||||
submittedOnDate: ['', Validators.required],
|
||||
fieldOfficerId: [''],
|
||||
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
|
||||
);
|
||||
@@ -203,7 +245,6 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
|
||||
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,
|
||||
@@ -220,9 +261,6 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats template dates into the current CBS date format.
|
||||
*/
|
||||
private extractOptions(options: any[], fallback?: string): string[] {
|
||||
if (Array.isArray(options) && options.length > 0) {
|
||||
return options.map((o: any) => o.value ?? o.name ?? o);
|
||||
@@ -231,9 +269,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
|
||||
}
|
||||
|
||||
formatTemplateDate(value: any): string {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
if (!value) return '';
|
||||
let dateValue: Date | null = null;
|
||||
if (value instanceof Date) {
|
||||
dateValue = value;
|
||||
@@ -248,15 +284,123 @@ 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',
|
||||
@@ -270,9 +414,7 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
|
||||
alert(`Partner with CIF ${result.clientCif} is already added.`);
|
||||
return;
|
||||
}
|
||||
this.relatedClients = [
|
||||
...this.relatedClients,
|
||||
{ ...result, isMandatory: false }];
|
||||
this.relatedClients = [...this.relatedClients, { ...result, isMandatory: false }];
|
||||
this.autoSetSignatoryRights();
|
||||
}
|
||||
});
|
||||
@@ -288,16 +430,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 {
|
||||
@@ -329,6 +466,33 @@ export class CurrentAccountDetailsStepComponent implements OnInit {
|
||||
sharePercentage: c.sharePercentage,
|
||||
isMandatory: c.isMandatory
|
||||
}));
|
||||
} else {
|
||||
if (this.selectedAccountStatus) value.accountStatus = this.selectedAccountStatus;
|
||||
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
|
||||
},
|
||||
...this.jointHolders.map((h) => ({ ...h, isPrimary: false }))
|
||||
];
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
+647
-8
@@ -115,13 +115,8 @@
|
||||
</mat-form-field>
|
||||
}
|
||||
|
||||
<mat-form-field appearance="outline" class="rad-field">
|
||||
<mat-label>Introducer Required</mat-label>
|
||||
<input matInput [value]="productTemplate.introducerRequired ? 'Yes' : 'No'" readonly />
|
||||
</mat-form-field>
|
||||
|
||||
<div class="rad-field rad-info-row">
|
||||
<span class="rad-info-label">Share Linking Enabled</span>
|
||||
<span class="rad-info-label">Deposit Account Link</span>
|
||||
<span class="rad-badge" [class.rad-badge--yes]="productTemplate.shareLinkingEnabled">
|
||||
{{ productTemplate.shareLinkingEnabled ? 'Yes' : 'No' }}
|
||||
</span>
|
||||
@@ -129,8 +124,652 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (productTemplate?.introducerRequired) {
|
||||
<mifosx-introducer-search (introducerChange)="onIntroducerChange($event)"></mifosx-introducer-search>
|
||||
<!-- ══ Deposit Account Link ══════════════════════════════════════════ -->
|
||||
@if (productTemplate?.shareLinkingEnabled) {
|
||||
<div class="dal-card">
|
||||
<div class="dal-card__header">
|
||||
<div class="dal-card__header-left">
|
||||
<div class="dal-card__icon">
|
||||
<mat-icon>link</mat-icon>
|
||||
</div>
|
||||
<div>
|
||||
<h3 class="dal-card__title">Deposit Account Link</h3>
|
||||
<span class="dal-card__sub">Link an existing Savings or Current account to this RD</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="dal-card__body">
|
||||
|
||||
@if (dalLoading) {
|
||||
<div class="dal-loading">
|
||||
<mat-icon class="dal-loading__icon">hourglass_top</mat-icon>
|
||||
<span>Loading accounts…</span>
|
||||
</div>
|
||||
} @else if (selectedLinkedAccount) {
|
||||
|
||||
<!-- ── Selected Account Card ── -->
|
||||
<div class="dal-selected">
|
||||
<div class="dal-selected__info">
|
||||
<span class="dal-type-badge" [class.dal-type-badge--savings]="selectedLinkedAccount.accountType === 'SAVINGS'"
|
||||
[class.dal-type-badge--current]="selectedLinkedAccount.accountType === 'CURRENT'">
|
||||
<mat-icon>{{ selectedLinkedAccount.accountType === 'SAVINGS' ? 'savings' : 'account_balance' }}</mat-icon>
|
||||
{{ selectedLinkedAccount.accountType === 'SAVINGS' ? 'Savings' : 'Current' }}
|
||||
</span>
|
||||
<div class="dal-selected__details">
|
||||
<span class="dal-selected__accno">{{ selectedLinkedAccount.accountNo }}</span>
|
||||
<span class="dal-selected__product">{{ selectedLinkedAccount.productName }}</span>
|
||||
</div>
|
||||
<div class="dal-selected__balance">
|
||||
<span class="dal-selected__bal-label">Balance</span>
|
||||
<span class="dal-selected__bal-amount">{{ selectedLinkedAccount.currencyCode }} {{ selectedLinkedAccount.balance | number:'1.2-2' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<button mat-stroked-button color="warn" type="button" class="dal-change-btn" (click)="clearLinkedAccount()">
|
||||
<mat-icon>swap_horiz</mat-icon> Change
|
||||
</button>
|
||||
</div>
|
||||
|
||||
} @else {
|
||||
|
||||
<!-- ── Step 1: Choose account type ── -->
|
||||
@if (!dalSelectedType) {
|
||||
<div class="dal-type-step">
|
||||
<p class="dal-type-step__label">Select account type to link:</p>
|
||||
<div class="dal-type-grid">
|
||||
<button type="button" class="dal-type-btn dal-type-btn--savings" (click)="selectDalType('SAVINGS')">
|
||||
<mat-icon>savings</mat-icon>
|
||||
<span class="dal-type-btn__name">Savings Account</span>
|
||||
<span class="dal-type-btn__count">{{ dalAllSavings.length }} account{{ dalAllSavings.length !== 1 ? 's' : '' }}</span>
|
||||
</button>
|
||||
<button type="button" class="dal-type-btn dal-type-btn--current" (click)="selectDalType('CURRENT')">
|
||||
<mat-icon>account_balance</mat-icon>
|
||||
<span class="dal-type-btn__name">Current Account</span>
|
||||
<span class="dal-type-btn__count">{{ dalAllCurrent.length }} account{{ dalAllCurrent.length !== 1 ? 's' : '' }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
} @else {
|
||||
|
||||
<!-- ── Step 2: Search & pick from accounts ── -->
|
||||
<div class="dal-picker">
|
||||
<div class="dal-picker__topbar">
|
||||
<button type="button" mat-icon-button class="dal-back-btn" matTooltip="Back" (click)="dalSelectedType = null">
|
||||
<mat-icon>arrow_back</mat-icon>
|
||||
</button>
|
||||
<span class="dal-picker__title">
|
||||
<mat-icon>{{ dalSelectedType === 'SAVINGS' ? 'savings' : 'account_balance' }}</mat-icon>
|
||||
{{ dalSelectedType === 'SAVINGS' ? 'Savings Accounts' : 'Current Accounts' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<mat-form-field appearance="outline" class="dal-search-field">
|
||||
<mat-label>Search by Account No. or Product</mat-label>
|
||||
<mat-icon matPrefix>search</mat-icon>
|
||||
<input matInput [(ngModel)]="dalSearch" [ngModelOptions]="{standalone: true}" placeholder="Type to filter…" />
|
||||
@if (dalSearch) {
|
||||
<button matSuffix mat-icon-button type="button" (click)="dalSearch = ''">
|
||||
<mat-icon>clear</mat-icon>
|
||||
</button>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
@if (dalFilteredAccounts.length === 0) {
|
||||
<div class="dal-empty">
|
||||
<mat-icon>search_off</mat-icon>
|
||||
<span>No {{ dalSelectedType === 'SAVINGS' ? 'Savings' : 'Current' }} accounts found.</span>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="dal-list">
|
||||
@for (account of dalFilteredAccounts; track account.id) {
|
||||
<div class="dal-item" (click)="selectLinkedAccount(account)">
|
||||
<div class="dal-item__details">
|
||||
<span class="dal-item__accno">{{ account.accountNo }}</span>
|
||||
<span class="dal-item__product">{{ account.productName }}</span>
|
||||
</div>
|
||||
<div class="dal-item__right">
|
||||
<span class="dal-item__status" [class.dal-item__status--active]="account.status === 'Active'">{{ account.status }}</span>
|
||||
<span class="dal-item__balance">{{ account.currencyCode }} {{ account.balance | number:'1.2-2' }}</span>
|
||||
</div>
|
||||
<mat-icon class="dal-item__arrow">chevron_right</mat-icon>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- ══ Status of Account (RBI) ═══════════════════════════════════════ -->
|
||||
@if (isProductSelected) {
|
||||
<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 RD 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>
|
||||
<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>
|
||||
|
||||
@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>
|
||||
|
||||
<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 & 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 & 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 & 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 & accident insurance cover applicable.</li>
|
||||
<li>SHG members — group linkage lending & special deposit rates applicable.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
+1283
File diff suppressed because it is too large
Load Diff
+341
-43
@@ -11,14 +11,17 @@ import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorR
|
||||
import { UntypedFormGroup, UntypedFormBuilder, Validators, FormsModule } from '@angular/forms';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
|
||||
/** Custom Services */
|
||||
import { RecurringDepositsService } from '../../recurring-deposits.service';
|
||||
import { ClientsService } from 'app/clients/clients.service';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
|
||||
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 { MatIcon } from '@angular/material/icon';
|
||||
import { AddRelatedClientDialogComponent } from 'app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component';
|
||||
|
||||
/**
|
||||
* Recurring Deposits Account Details Step
|
||||
@@ -33,8 +36,8 @@ import { IntroducerSearchComponent } from 'app/shared/introducer-search/introduc
|
||||
MatStepperPrevious,
|
||||
FaIconComponent,
|
||||
MatStepperNext,
|
||||
IntroducerSearchComponent,
|
||||
FormsModule
|
||||
FormsModule,
|
||||
MatIcon
|
||||
]
|
||||
})
|
||||
export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
|
||||
@@ -43,6 +46,8 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
|
||||
private settingsService = inject(SettingsService);
|
||||
private dateUtils = inject(Dates);
|
||||
private cdr = inject(ChangeDetectorRef);
|
||||
private clientsService = inject(ClientsService);
|
||||
private dialog = inject(MatDialog);
|
||||
|
||||
/** Recurring Deposits Account Template */
|
||||
@Input() recurringDepositsAccountTemplate: any;
|
||||
@@ -61,9 +66,6 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
|
||||
recurringDepositAccountDetailsForm!: UntypedFormGroup;
|
||||
|
||||
isProductSelected = false;
|
||||
introducerId: number | null = null;
|
||||
introducerName: string | null = null;
|
||||
introducerCif: string | null = null;
|
||||
|
||||
/** Recurring Deposits Account Template with product data */
|
||||
@Output() recurringDepositsAccountProductTemplate = new EventEmitter();
|
||||
@@ -75,6 +77,145 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
|
||||
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 ──────────────────────────────────────────────────────
|
||||
jointHolders: Array<{
|
||||
clientId: number;
|
||||
clientName: string;
|
||||
clientCif: string;
|
||||
sharePercentage: number;
|
||||
isMandatory: boolean;
|
||||
}> = [];
|
||||
|
||||
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'];
|
||||
|
||||
// ── Deposit Account Link ───────────────────────────────────────────────
|
||||
dalSelectedType: 'SAVINGS' | 'CURRENT' | null = null;
|
||||
dalAllSavings: any[] = [];
|
||||
dalAllCurrent: any[] = [];
|
||||
dalSearch = '';
|
||||
dalLoading = false;
|
||||
selectedLinkedAccount: any = null;
|
||||
|
||||
get dalAccounts(): any[] {
|
||||
return this.dalSelectedType === 'SAVINGS' ? this.dalAllSavings : this.dalAllCurrent;
|
||||
}
|
||||
|
||||
get dalFilteredAccounts(): any[] {
|
||||
const q = this.dalSearch.toLowerCase().trim();
|
||||
if (!q) return this.dalAccounts;
|
||||
return this.dalAccounts.filter(
|
||||
(a) => a.accountNo.toLowerCase().includes(q) || a.productName.toLowerCase().includes(q)
|
||||
);
|
||||
}
|
||||
|
||||
selectDalType(type: 'SAVINGS' | 'CURRENT'): void {
|
||||
this.dalSelectedType = type;
|
||||
this.dalSearch = '';
|
||||
this.selectedLinkedAccount = null;
|
||||
}
|
||||
|
||||
selectLinkedAccount(account: any): void {
|
||||
this.selectedLinkedAccount = account;
|
||||
}
|
||||
|
||||
clearLinkedAccount(): void {
|
||||
this.selectedLinkedAccount = null;
|
||||
this.dalSelectedType = null;
|
||||
this.dalSearch = '';
|
||||
}
|
||||
|
||||
private loadClientAccounts(): void {
|
||||
const clientId = String(this.recurringDepositsAccountTemplate?.clientId);
|
||||
if (!clientId) return;
|
||||
this.dalLoading = true;
|
||||
this.clientsService.getClientAccountData(clientId).subscribe({
|
||||
next: (data: any) => {
|
||||
this.dalAllSavings = (data?.savingsAccounts ?? []).map((a: any) => ({
|
||||
id: a.id,
|
||||
accountNo: a.accountNo,
|
||||
productName: a.productName ?? '',
|
||||
accountType: 'SAVINGS' as const,
|
||||
status: a.status?.value ?? '',
|
||||
balance: a.accountBalance ?? 0,
|
||||
currencyCode: a.currency?.code ?? ''
|
||||
}));
|
||||
this.dalAllCurrent = (data?.currentAccounts ?? []).map((a: any) => ({
|
||||
id: a.id,
|
||||
accountNo: a.accountNo,
|
||||
productName: a.productName ?? '',
|
||||
accountType: 'CURRENT' as const,
|
||||
status: a.status?.value ?? '',
|
||||
balance: a.accountBalance ?? 0,
|
||||
currencyCode: a.currency?.code ?? ''
|
||||
}));
|
||||
this.dalLoading = false;
|
||||
this.cdr.detectChanges();
|
||||
},
|
||||
error: () => { this.dalLoading = false; }
|
||||
});
|
||||
}
|
||||
|
||||
constructor() {
|
||||
this.createRecurringDepositsAccountDetailsForm();
|
||||
}
|
||||
@@ -85,7 +226,6 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
|
||||
if (this.recurringDepositsAccountTemplate) {
|
||||
this.productData = this.recurringDepositsAccountTemplate.productOptions;
|
||||
if (this.recurringDepositsAccountTemplate.depositProductId) {
|
||||
// Edit mode: patch from saved data
|
||||
this.recurringDepositAccountDetailsForm.patchValue({
|
||||
productId: this.recurringDepositsAccountTemplate.depositProductId,
|
||||
submittedOnDate:
|
||||
@@ -94,12 +234,84 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
|
||||
externalId: this.recurringDepositsAccountTemplate.externalId
|
||||
});
|
||||
} else {
|
||||
// New account: default submittedOnDate to today's business date
|
||||
this.recurringDepositAccountDetailsForm.patchValue({
|
||||
submittedOnDate: this.settingsService.businessDate
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-detect account status from client DOB + capture primary holder info
|
||||
const clientId = this.recurringDepositsAccountTemplate?.clientId;
|
||||
if (clientId) {
|
||||
this.primaryClientId = clientId;
|
||||
this.clientsService.getClientData(String(clientId)).subscribe((client: any) => {
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Parse DOB from backend — handles array [yyyy, mm, dd] or string */
|
||||
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;
|
||||
}
|
||||
|
||||
/** Calculate full years from a date to today */
|
||||
private calculateAge(dob: Date): number {
|
||||
const today = this.settingsService.businessDate ?? new Date();
|
||||
let age = today.getFullYear() - dob.getFullYear();
|
||||
const monthDiff = today.getMonth() - dob.getMonth();
|
||||
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < dob.getDate())) {
|
||||
age--;
|
||||
}
|
||||
return age;
|
||||
}
|
||||
|
||||
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 '';
|
||||
}
|
||||
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -107,14 +319,8 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
|
||||
*/
|
||||
createRecurringDepositsAccountDetailsForm() {
|
||||
this.recurringDepositAccountDetailsForm = this.formBuilder.group({
|
||||
productId: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
submittedOnDate: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
productId: ['', Validators.required],
|
||||
submittedOnDate: ['', Validators.required],
|
||||
fieldOfficerId: [''],
|
||||
externalId: [''],
|
||||
balanceSwapOption: [false],
|
||||
@@ -132,9 +338,7 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
|
||||
this.recurringDepositsService
|
||||
.getRecurringDepositsAccountTemplate(clientId, productId)
|
||||
.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.recurringDepositsAccountTemplate.productOptions?.find(
|
||||
(product: any) => product.id == productId
|
||||
);
|
||||
@@ -152,7 +356,9 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
|
||||
this.recurringDepositsAccountProductTemplate.emit(response);
|
||||
this.fieldOfficerData = response.fieldOfficerOptions;
|
||||
this.isProductSelected = true;
|
||||
// Pre-populate toggles from product defaults (user can override)
|
||||
if (response.shareLinkingEnabled) {
|
||||
this.loadClientAccounts();
|
||||
}
|
||||
this.recurringDepositAccountDetailsForm.patchValue({
|
||||
balanceSwapOption: !!response.balanceSwapOption,
|
||||
msme: !!response.msme,
|
||||
@@ -171,17 +377,10 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if a value is not null, undefined, or empty string.
|
||||
*/
|
||||
hasValue(val: any): boolean {
|
||||
return val !== null && val !== undefined && val !== '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats template dates into the current CBS date format.
|
||||
* Handles backend date arrays like [2026, 4, 20] as well as strings and Date objects.
|
||||
*/
|
||||
private extractOptions(options: any[], fallback?: string): string[] {
|
||||
if (Array.isArray(options) && options.length > 0) {
|
||||
return options.map((o: any) => o.value ?? o.name ?? o);
|
||||
@@ -190,12 +389,8 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
|
||||
}
|
||||
|
||||
formatTemplateDate(value: any): string {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!value) return '';
|
||||
let dateValue: Date | null = null;
|
||||
|
||||
if (value instanceof Date) {
|
||||
dateValue = value;
|
||||
} else if (Array.isArray(value) && value.length >= 3) {
|
||||
@@ -203,30 +398,133 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit {
|
||||
} else {
|
||||
dateValue = new Date(value);
|
||||
}
|
||||
|
||||
if (!dateValue || Number.isNaN(dateValue.getTime())) {
|
||||
return Array.isArray(value) ? value.join('/') : String(value);
|
||||
}
|
||||
|
||||
return this.dateUtils.formatDate(dateValue, 'M/d/yyyy') || '';
|
||||
}
|
||||
|
||||
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 helpers ──────────────────────────────────────────────
|
||||
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') || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns recurring deposits account details form value.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
get recurringDepositAccountDetails() {
|
||||
const value = this.recurringDepositAccountDetailsForm.value;
|
||||
if (this.introducerId) {
|
||||
value.introducerId = this.introducerId;
|
||||
value.introducerName = this.introducerName;
|
||||
value.introducerCif = this.introducerCif;
|
||||
|
||||
if (this.selectedAccountStatus) {
|
||||
value.accountStatus = this.selectedAccountStatus;
|
||||
}
|
||||
|
||||
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
|
||||
},
|
||||
...this.jointHolders.map((h) => ({
|
||||
clientId: h.clientId,
|
||||
clientName: h.clientName,
|
||||
clientCif: h.clientCif,
|
||||
sharePercentage: h.sharePercentage,
|
||||
isMandatory: h.isMandatory,
|
||||
isPrimary: false
|
||||
}))
|
||||
];
|
||||
}
|
||||
|
||||
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 === '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;
|
||||
}
|
||||
|
||||
if (this.selectedLinkedAccount) {
|
||||
value.linkedAccountId = this.selectedLinkedAccount.id;
|
||||
value.linkedAccountType = this.selectedLinkedAccount.accountType;
|
||||
value.linkedAccountNo = this.selectedLinkedAccount.accountNo;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -112,7 +112,7 @@
|
||||
recurringDepositsAccountProductTemplate?.shareLinkingEnabled !== null
|
||||
) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Share Linking Enabled</span>
|
||||
<span class="preview-label">Deposit Account Link</span>
|
||||
<span
|
||||
class="preview-value preview-badge"
|
||||
[class.preview-badge--yes]="recurringDepositsAccountProductTemplate.shareLinkingEnabled"
|
||||
|
||||
+1
-1
@@ -120,7 +120,7 @@
|
||||
</mat-form-field>
|
||||
|
||||
<div class="rdpd-field rdpd-field--full rdpd-checkbox-row">
|
||||
<mat-checkbox formControlName="shareLinkingEnabled">Share Linking Enabled</mat-checkbox>
|
||||
<mat-checkbox formControlName="shareLinkingEnabled">Deposit Account Link</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+1
-1
@@ -87,7 +87,7 @@
|
||||
|
||||
@if (recurringDepositProduct.shareLinkingEnabled !== undefined) {
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Share Linking Enabled:</span>
|
||||
<span class="flex-40">Deposit Account Link:</span>
|
||||
<span class="flex-60">{{ recurringDepositProduct.shareLinkingEnabled | yesNo }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
+1
-1
@@ -77,7 +77,7 @@
|
||||
<span class="flex-60">{{ recurringDepositProduct.introducerRequired | yesNo }}</span>
|
||||
</div>
|
||||
<div class="flex-100 layout-row">
|
||||
<span class="flex-40">Share Linking Enabled:</span>
|
||||
<span class="flex-40">Deposit Account Link:</span>
|
||||
<span class="flex-60">{{ recurringDepositProduct.shareLinkingEnabled | yesNo }}</span>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -113,7 +113,7 @@ export class CreateSavingsAccountComponent {
|
||||
const locale = this.settingsService.language.code;
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
const monthDayFormat = 'dd MMMM';
|
||||
const savingsAccount = {
|
||||
const savingsAccount: any = {
|
||||
...this.savingsAccount,
|
||||
charges: [],
|
||||
submittedOnDate: this.dateUtils.formatDate(this.savingsAccount.submittedOnDate, dateFormat),
|
||||
@@ -121,6 +121,17 @@ export class CreateSavingsAccountComponent {
|
||||
monthDayFormat,
|
||||
locale
|
||||
};
|
||||
|
||||
// Digital Facilities
|
||||
if (this.savingsAccountDigital?.accountFacilityConfig) {
|
||||
savingsAccount.accountFacilityConfig = this.savingsAccountDigital.accountFacilityConfig;
|
||||
}
|
||||
|
||||
// Nominee
|
||||
if (this.savingsAccountNominee) {
|
||||
savingsAccount.nominee = this.savingsAccountNominee;
|
||||
}
|
||||
|
||||
if (this.savingsAccountTemplate.clientId) {
|
||||
savingsAccount.clientId = this.savingsAccountTemplate.clientId;
|
||||
} else {
|
||||
|
||||
+557
@@ -135,4 +135,561 @@
|
||||
<mifosx-introducer-search (introducerChange)="onIntroducerChange($event)"></mifosx-introducer-search>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- ══ Status of Account (RBI) ═══════════════════════════════════════ -->
|
||||
@if (savingsProductSelected) {
|
||||
<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 savings 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 & 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>
|
||||
|
||||
<!-- DOB + Age from client profile -->
|
||||
<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 & 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 & 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 & accident insurance cover applicable.</li>
|
||||
<li>SHG members — group linkage lending & special deposit rates applicable.</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
|
||||
+1043
File diff suppressed because it is too large
Load Diff
+285
-37
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
@@ -11,13 +11,17 @@ import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorR
|
||||
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule, FormsModule } from '@angular/forms';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
|
||||
/** Custom Services */
|
||||
import { SavingsService } from 'app/savings/savings.service';
|
||||
import { ClientsService } from 'app/clients/clients.service';
|
||||
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
|
||||
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 { MatIcon } from '@angular/material/icon';
|
||||
import { AddRelatedClientDialogComponent } from 'app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component';
|
||||
|
||||
/**
|
||||
* Savings Account Details Step
|
||||
@@ -32,30 +36,27 @@ import { IntroducerSearchComponent } from 'app/shared/introducer-search/introduc
|
||||
FaIconComponent,
|
||||
MatStepperNext,
|
||||
IntroducerSearchComponent,
|
||||
FormsModule
|
||||
FormsModule,
|
||||
MatIcon
|
||||
]
|
||||
})
|
||||
export class SavingsAccountDetailsStepComponent implements OnInit {
|
||||
private formBuilder = inject(UntypedFormBuilder);
|
||||
private savingsService = inject(SavingsService);
|
||||
private clientsService = inject(ClientsService);
|
||||
private settingsService = inject(SettingsService);
|
||||
private dateUtils = inject(Dates);
|
||||
private cdr = inject(ChangeDetectorRef);
|
||||
private dialog = inject(MatDialog);
|
||||
|
||||
/** Savings Account Template */
|
||||
@Input() savingsAccountTemplate: 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 savings form */
|
||||
isFieldOfficerPatched = false;
|
||||
/** Savings Account Details Form */
|
||||
savingsAccountDetailsForm: UntypedFormGroup;
|
||||
|
||||
savingsProductSelected = false;
|
||||
@@ -63,22 +64,93 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
|
||||
introducerName: string | null = null;
|
||||
introducerCif: string | null = null;
|
||||
|
||||
/** Savings Account Template with product data */
|
||||
@Output() savingsAccountProductTemplate = 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;
|
||||
|
||||
/**
|
||||
* Sets share account details form.
|
||||
* @param {FormBuilder} formBuilder Form Builder.
|
||||
* @param {SavingsService} savingsService Savings Service.
|
||||
* @param {SettingsService} settingsService Setting service
|
||||
*/
|
||||
// ── 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 ──────────────────────────────────────────────────────
|
||||
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.createSavingsAccountDetailsForm();
|
||||
}
|
||||
@@ -102,11 +174,83 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-detect account status from client DOB + capture primary holder info
|
||||
const clientId = this.savingsAccountTemplate?.clientId;
|
||||
if (clientId) {
|
||||
this.primaryClientId = clientId;
|
||||
this.clientsService.getClientData(String(clientId)).subscribe((client: any) => {
|
||||
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();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/** Parse DOB from backend — handles array [yyyy, mm, dd] or string */
|
||||
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;
|
||||
}
|
||||
|
||||
/** Calculate full years from a date to today */
|
||||
private calculateAge(dob: Date): number {
|
||||
const today = this.settingsService.businessDate ?? new Date();
|
||||
let age = today.getFullYear() - dob.getFullYear();
|
||||
const monthDiff = today.getMonth() - dob.getMonth();
|
||||
if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < dob.getDate())) {
|
||||
age--;
|
||||
}
|
||||
return age;
|
||||
}
|
||||
|
||||
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 '';
|
||||
}
|
||||
|
||||
/** Auto-set status tile based on age; pre-fill minorDob if minor */
|
||||
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';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates savings account details form.
|
||||
*/
|
||||
createSavingsAccountDetailsForm() {
|
||||
this.savingsAccountDetailsForm = this.formBuilder.group({
|
||||
productId: [
|
||||
@@ -122,9 +266,6 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches savings account product template on productId value changes
|
||||
*/
|
||||
buildDependencies() {
|
||||
const entityId = this.savingsAccountTemplate.groupId || this.savingsAccountTemplate.clientId;
|
||||
this.savingsAccountDetailsForm.get('productId').valueChanges.subscribe((productId: string) => {
|
||||
@@ -150,10 +291,6 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats template dates into the current CBS date format.
|
||||
* Handles backend date arrays like [2026, 4, 20] as well as strings and Date objects.
|
||||
*/
|
||||
private extractOptions(options: any[], fallback?: string): string[] {
|
||||
if (Array.isArray(options) && options.length > 0) {
|
||||
return options.map((o: any) => o.value ?? o.name ?? o);
|
||||
@@ -162,12 +299,8 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
|
||||
}
|
||||
|
||||
formatTemplateDate(value: any): string {
|
||||
if (!value) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (!value) return '';
|
||||
let dateValue: Date | null = null;
|
||||
|
||||
if (value instanceof Date) {
|
||||
dateValue = value;
|
||||
} else if (Array.isArray(value) && value.length >= 3) {
|
||||
@@ -175,30 +308,145 @@ export class SavingsAccountDetailsStepComponent implements OnInit {
|
||||
} else {
|
||||
dateValue = new Date(value);
|
||||
}
|
||||
|
||||
if (!dateValue || Number.isNaN(dateValue.getTime())) {
|
||||
return Array.isArray(value) ? value.join('/') : String(value);
|
||||
}
|
||||
|
||||
return this.dateUtils.formatDate(dateValue, 'M/d/yyyy') || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns savings account form value.
|
||||
*/
|
||||
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 helpers ──────────────────────────────────────────────
|
||||
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 ?? '';
|
||||
}
|
||||
|
||||
// ── Minor Account helpers ──────────────────────────────────────────────
|
||||
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') || '';
|
||||
}
|
||||
|
||||
get savingsAccountDetails() {
|
||||
const value = this.savingsAccountDetailsForm.getRawValue();
|
||||
|
||||
// Product Classification — user-selected values
|
||||
if (this.selectedAccountType) value.accountType = this.selectedAccountType;
|
||||
if (this.selectedCustomerSegment) value.customerSegment = this.selectedCustomerSegment;
|
||||
|
||||
if (this.introducerId) {
|
||||
value.introducerId = this.introducerId;
|
||||
value.introducerName = this.introducerName;
|
||||
value.introducerCif = this.introducerCif;
|
||||
value.introducerCifNumber = this.introducerCif;
|
||||
}
|
||||
|
||||
if (this.selectedAccountStatus) {
|
||||
value.accountStatus = this.selectedAccountStatus;
|
||||
}
|
||||
|
||||
if (this.selectedAccountStatus === 'JOINT') {
|
||||
value.modeOfOperation = this.modeOfOperation || null;
|
||||
value.jointHolders = [
|
||||
{
|
||||
clientId: this.primaryClientId,
|
||||
displayName: this.primaryClientName,
|
||||
cifNumber: this.primaryClientCif,
|
||||
sharePercentage: this.primaryHolderShare,
|
||||
isMandatory: true,
|
||||
isPrimary: true
|
||||
},
|
||||
...this.jointHolders.map((h) => ({
|
||||
clientId: h.clientId,
|
||||
displayName: h.clientName,
|
||||
cifNumber: h.clientCif,
|
||||
sharePercentage: h.sharePercentage,
|
||||
isPrimary: false,
|
||||
isMandatory: h.isMandatory,
|
||||
signingAuthority: h.signingAuthority || null,
|
||||
role: h.role || 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 === 'MINOR') {
|
||||
value.guardianClientId = this.guardianClientId;
|
||||
value.guardianName = this.guardianName;
|
||||
value.guardianCifNumber = this.guardianCif;
|
||||
value.guardianRelationship = this.guardianRelationship;
|
||||
value.minorDateOfBirth = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'yyyy-MM-dd') : null;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
+155
-7
@@ -63,16 +63,16 @@
|
||||
</div>
|
||||
<div class="preview-body">
|
||||
<div class="preview-grid preview-grid--three">
|
||||
@if (savingsAccountProductTemplate?.accountType) {
|
||||
@if (savingsAccount?.accountType || savingsAccountProductTemplate?.accountType) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Account Type</span>
|
||||
<span class="preview-value">{{ savingsAccountProductTemplate?.accountType }}</span>
|
||||
<span class="preview-value">{{ savingsAccount?.accountType || savingsAccountProductTemplate?.accountType }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountProductTemplate?.customerSegment) {
|
||||
@if (savingsAccount?.customerSegment || savingsAccountProductTemplate?.customerSegment) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Customer Segment</span>
|
||||
<span class="preview-value">{{ savingsAccountProductTemplate?.customerSegment }}</span>
|
||||
<span class="preview-value">{{ savingsAccount?.customerSegment || savingsAccountProductTemplate?.customerSegment }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccountProductTemplate?.productCode) {
|
||||
@@ -135,10 +135,10 @@
|
||||
<span class="preview-label">Name</span>
|
||||
<span class="preview-value">{{ savingsAccount?.introducerName || '—' }}</span>
|
||||
</div>
|
||||
@if (savingsAccount?.introducerCif) {
|
||||
@if (savingsAccount?.introducerCifNumber) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">CIF No</span>
|
||||
<span class="preview-value preview-mono">{{ savingsAccount.introducerCif }}</span>
|
||||
<span class="preview-value preview-mono">{{ savingsAccount.introducerCifNumber }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="preview-field">
|
||||
@@ -150,7 +150,155 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ══ 4. Currency & Account Flags ══════════════════════════════ -->
|
||||
<!-- ══ 4. Status of Account ════════════════════════════════════ -->
|
||||
@if (savingsAccount?.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 (savingsAccount.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 { {{ savingsAccount.accountStatus }} }
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- JOINT: Mode of Operation + Holders Table -->
|
||||
@if (savingsAccount.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 (savingsAccount.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 { {{ savingsAccount.modeOfOperation || '—' }} }
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@if (savingsAccount.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 savingsAccount.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.displayName || '—' }}
|
||||
@if (holder.isPrimary) { <span class="preview-soa-primary-tag">Primary</span> }
|
||||
</td>
|
||||
<td class="preview-soa-td preview-mono">{{ holder.cifNumber || '—' }}</td>
|
||||
<td class="preview-soa-td">{{ holder.sharePercentage ?? '—' }}%</td>
|
||||
<td class="preview-soa-td">{{ holder.isMandatory ? 'Mandatory' : 'Optional' }}</td>
|
||||
<td class="preview-soa-td">{{ holder.isPrimary ? 'Primary Account Holder' : 'Co-Holder' }}</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- INDIVIDUAL_MALE: Special Category -->
|
||||
@if (savingsAccount.accountStatus === 'INDIVIDUAL_MALE' && savingsAccount.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 (savingsAccount.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 { {{ savingsAccount.specialCategory }} }
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- INDIVIDUAL_FEMALE: Weaker Section -->
|
||||
@if (savingsAccount.accountStatus === 'INDIVIDUAL_FEMALE' && savingsAccount.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 (savingsAccount.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 { {{ savingsAccount.weakerSection }} }
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- MINOR: Guardian Details -->
|
||||
@if (savingsAccount.accountStatus === 'MINOR') {
|
||||
<div class="preview-grid preview-grid--three" style="margin-top:12px">
|
||||
@if (savingsAccount.guardianName) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Guardian Name</span>
|
||||
<span class="preview-value">{{ savingsAccount.guardianName }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccount.guardianCifNumber) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Guardian CIF</span>
|
||||
<span class="preview-value preview-mono">{{ savingsAccount.guardianCifNumber }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccount.guardianRelationship) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Relationship</span>
|
||||
<span class="preview-value">{{ savingsAccount.guardianRelationship }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (savingsAccount.minorDateOfBirth) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Minor DOB</span>
|
||||
<span class="preview-value">{{ savingsAccount.minorDateOfBirth }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ══ 5. Currency & Account Flags ══════════════════════════════ -->
|
||||
<div class="preview-section">
|
||||
<div class="preview-section__header">
|
||||
<i class="fas fa-coins preview-section__icon"></i>
|
||||
|
||||
+72
@@ -323,3 +323,75 @@
|
||||
.margin-t {
|
||||
margin-top: 1em;
|
||||
}
|
||||
|
||||
// ── Status of Account preview ─────────────────────────────────────────────
|
||||
|
||||
.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; }
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
@@ -64,6 +64,11 @@ export class SavingsService {
|
||||
return this.http.get(`/savingsaccounts/${accountId}`, { params: httpParams });
|
||||
}
|
||||
|
||||
getClientSavingsAccounts(clientId: string): Observable<any> {
|
||||
const httpParams = new HttpParams().set('clientId', clientId);
|
||||
return this.http.get('/savingsaccounts', { params: httpParams });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param accountId Savings Account Id of account to get data for.
|
||||
* @returns {Observable<any>} Savings account and template.
|
||||
|
||||
@@ -84,7 +84,7 @@ export class IntroducerSearchComponent {
|
||||
this.introducerChange.emit({
|
||||
clientId: client.id,
|
||||
displayName: client.displayName,
|
||||
cifNumber: client.cifNumber || client.accountNumber || ''
|
||||
cifNumber: client.cifNumber || client.cifNo || ''
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -183,7 +183,7 @@ export class NomineeFormComponent implements OnInit {
|
||||
mode: 'linked',
|
||||
clientId: client.id,
|
||||
displayName: client.displayName,
|
||||
cifNumber: client.accountNo || client.cifNumber || ''
|
||||
cifNumber: client.cifNumber || client.cifNo || ''
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user