Four independent, self-contained changes bundled from this session's work:
1. Auto-derive Senior Citizen / Minor / Customer Segment instead of manual entry
Senior Citizen was a manual, staff-editable checkbox on FD/RD account
creation with no validation against the client's actual age -- this let
non-senior clients get the senior-citizen interest rate by mistake (found
twice on live test accounts, where m_savings_account.is_senior_citizen
was true despite the client being confirmed non-senior at the client
record level). Likewise "minor" client classification was hardcoded to
the age-18 threshold with no single source of truth, and FD/RD account
creation's "Customer Segment" (Individual / Non-Individual) had no
connection to the Individual/Non-Individual Legal Form already captured
at client creation.
- New src/app/clients/shared/client-age.util.ts centralises age/DOB
derivation: MINOR_AGE_THRESHOLD (now 10, was 18), SENIOR_CITIZEN_AGE_
THRESHOLD (60), AGE_OF_MAJORITY (18, kept separate from the minor
threshold -- majority date math is unrelated to the minor/adult
classification threshold), calculateAge/isMinorAge/isSeniorCitizenAge/
isIndividualClient/deriveSeniorCitizenStatus.
- FD/RD interest-rate-chart steps: Senior Citizen checkbox is now derived
from the client's DOB (age >= 60, individual clients only) and
disabled for manual editing; a hint explains why it is checked/
unchecked.
- Client general step and all four account-details-steps (savings,
current, FD, RD): replaced hardcoded 18/60 literals with the shared
helpers; "Must be below 18" hints now read the actual threshold.
- FD/RD account-details-steps: Customer Segment auto-selects to match
the client's Legal Form (exact match, not substring -- a substring
match would misclassify "Individual" as matching "Non-Individual").
- RD preview step: fixed a pre-existing display bug that always showed
the product's default Customer Segment instead of what was actually
selected on the account.
2. Fix wrong Account Name / Account Type on Disbursement Review screens
Clerk disbursement review and "My Disbursement Requests" showed the
account NUMBER instead of the holder's name, and always labelled the
account type "SAVINGS" regardless of the real account type (FD/RD/
Share/Loan), because transaction-entry.component.ts's account-type
mapping only checked for CA/CB and its account-name fallback chain
never tried clientName. Added creditEntryAccountType() covering
GL/CA/CB/FD/RD/SHR/LA, and accountTypeLabel() display helpers on both
review screens.
3. Stop reversed transactions from distorting displayed running balances
Passbook and Transactions tabs were including reversed transactions in
their client-side running-balance recalculation, so a reversed posting
(visible struck-through) still shifted the balance shown for every
transaction after it. Both now filter out txn.reversed before computing
running balances; Transactions tab also defaults "Hide Reversed" to on.
4. New Interest Schedule dialog on the FD account view
Added custom-dialogs/interest-schedule-dialog, opened from the FD
General tab, showing the account's period-by-period interest schedule.
Excludes src/environments/.env.ts (auto-generated build version/hash
stamp, regenerated on every build -- not meaningful to hand-commit).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
987 lines
43 KiB
HTML
987 lines
43 KiB
HTML
<!--
|
||
Copyright since 2025 Mifos Initiative
|
||
|
||
This Source Code Form is subject to the terms of the Mozilla Public
|
||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||
-->
|
||
|
||
<form [formGroup]="currentAccountDetailsForm">
|
||
<!-- ══ Account Details ══════════════════════════════════════════ -->
|
||
<div class="sad-section">
|
||
<div class="sad-section__header">
|
||
<i class="fas fa-info-circle"></i>
|
||
<h3>Account Details</h3>
|
||
</div>
|
||
<div class="sad-grid">
|
||
<mat-form-field appearance="outline" class="sad-field">
|
||
<mat-label>{{ 'labels.inputs.Product Name' | translate }}</mat-label>
|
||
<mat-select formControlName="productId" required>
|
||
@for (product of productData; track product) {
|
||
<mat-option [value]="product.id">{{ product.name }}</mat-option>
|
||
}
|
||
</mat-select>
|
||
<mat-error>
|
||
{{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }}
|
||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||
</mat-error>
|
||
</mat-form-field>
|
||
|
||
@if (currentAccountProductSelected) {
|
||
<mat-form-field appearance="outline" class="sad-field" (click)="submittedOnDatePicker.open()">
|
||
<mat-label>{{ 'labels.inputs.Submitted On' | translate }}</mat-label>
|
||
<input
|
||
matInput
|
||
[min]="minDate"
|
||
[max]="maxDate"
|
||
[matDatepicker]="submittedOnDatePicker"
|
||
formControlName="submittedOnDate"
|
||
required
|
||
/>
|
||
<mat-datepicker-toggle matSuffix [for]="submittedOnDatePicker"></mat-datepicker-toggle>
|
||
<mat-datepicker #submittedOnDatePicker></mat-datepicker>
|
||
<mat-error>
|
||
{{ 'labels.inputs.Submitted On' | translate }} {{ 'labels.commons.is' | translate }}
|
||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||
</mat-error>
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="sad-field">
|
||
<mat-label>{{ 'labels.inputs.Field Officer' | translate }}</mat-label>
|
||
<mat-select formControlName="fieldOfficerId">
|
||
@for (officer of fieldOfficerData; track officer) {
|
||
<mat-option [value]="officer.id">{{ officer.displayName }}</mat-option>
|
||
}
|
||
</mat-select>
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="sad-field">
|
||
<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" [max]="today" />
|
||
<mat-datepicker-toggle matSuffix [for]="establishmentDatePicker"></mat-datepicker-toggle>
|
||
<mat-datepicker #establishmentDatePicker></mat-datepicker>
|
||
<mat-hint>Date of entity / business establishment</mat-hint>
|
||
@if (currentAccountDetailsForm.get('establishmentDate')?.hasError('matDatepickerMax')) {
|
||
<mat-error>Establishment Date cannot be a future date.</mat-error>
|
||
}
|
||
</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"
|
||
[max]="today"
|
||
[min]="minStartOperationDate"
|
||
/>
|
||
<mat-datepicker-toggle matSuffix [for]="startOperationDatePicker"></mat-datepicker-toggle>
|
||
<mat-datepicker #startOperationDatePicker></mat-datepicker>
|
||
<mat-hint>Date when operations commenced</mat-hint>
|
||
@if (currentAccountDetailsForm.get('startOperationDate')?.hasError('matDatepickerMax')) {
|
||
<mat-error>Start of Operation Date cannot be a future date.</mat-error>
|
||
} @else if (currentAccountDetailsForm.get('startOperationDate')?.hasError('matDatepickerMin')) {
|
||
<mat-error>Start of Operation Date cannot be before Establishment Date.</mat-error>
|
||
}
|
||
</mat-form-field>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
@if (currentAccountProductSelected && productTemplate) {
|
||
<!-- ══ Product Classification (read-only) ══════════════════════ -->
|
||
<div class="sad-section sad-section--mt">
|
||
<div class="sad-section__header">
|
||
<i class="fas fa-tags"></i>
|
||
<h3>Product Classification</h3>
|
||
<span class="sad-badge">From Product</span>
|
||
</div>
|
||
<div class="sad-grid sad-grid--classification">
|
||
<mat-form-field appearance="outline" class="sad-field">
|
||
<mat-label>Account Type</mat-label>
|
||
<mat-select [(ngModel)]="selectedAccountType" [ngModelOptions]="{ standalone: true }" [disabled]="readOnly">
|
||
@for (opt of accountTypeOptions; track opt) {
|
||
<mat-option [value]="opt">{{ opt }}</mat-option>
|
||
}
|
||
</mat-select>
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="sad-field">
|
||
<mat-label>Customer Segment</mat-label>
|
||
<mat-select
|
||
[(ngModel)]="selectedCustomerSegment"
|
||
[ngModelOptions]="{ standalone: true }"
|
||
[disabled]="readOnly"
|
||
>
|
||
@for (opt of customerSegmentOptions; track opt) {
|
||
<mat-option [value]="opt">{{ opt }}</mat-option>
|
||
}
|
||
</mat-select>
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="sad-field">
|
||
<mat-label>Account No</mat-label>
|
||
<input
|
||
matInput
|
||
[value]="currentAccountTemplate?.accountNo || productTemplate?.previewAccountNo || 'Auto-generated'"
|
||
readonly
|
||
/>
|
||
</mat-form-field>
|
||
|
||
@if (productTemplate.effectiveDate) {
|
||
<mat-form-field appearance="outline" class="sad-field">
|
||
<mat-label>Effective Date</mat-label>
|
||
<input matInput [value]="formatTemplateDate(productTemplate.effectiveDate)" readonly />
|
||
</mat-form-field>
|
||
}
|
||
|
||
@if (productTemplate.effectiveTo) {
|
||
<mat-form-field appearance="outline" class="sad-field">
|
||
<mat-label>Effective To</mat-label>
|
||
<input matInput [value]="formatTemplateDate(productTemplate.effectiveTo)" readonly />
|
||
</mat-form-field>
|
||
}
|
||
|
||
@if (productTemplate.endDate) {
|
||
<mat-form-field appearance="outline" class="sad-field">
|
||
<mat-label>End Date</mat-label>
|
||
<input matInput [value]="formatTemplateDate(productTemplate.endDate)" readonly />
|
||
</mat-form-field>
|
||
}
|
||
|
||
<mat-form-field appearance="outline" class="sad-field">
|
||
<mat-label>Introducer Required</mat-label>
|
||
<input matInput [value]="productTemplate.introducerRequired ? 'Yes' : 'No'" readonly />
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="sad-field">
|
||
<mat-label>Share Linking Enabled</mat-label>
|
||
<mat-select [(ngModel)]="shareLinkingEnabled" [ngModelOptions]="{ standalone: true }" [disabled]="readOnly">
|
||
<mat-option [value]="true">Yes</mat-option>
|
||
<mat-option [value]="false">No</mat-option>
|
||
</mat-select>
|
||
</mat-form-field>
|
||
</div>
|
||
</div>
|
||
|
||
@if (productTemplate?.introducerRequired) {
|
||
<mifosx-introducer-search
|
||
[excludedClientIds]="primaryClientId ? [primaryClientId] : []"
|
||
[preselectedIntroducer]="
|
||
introducerId ? { clientId: introducerId, displayName: introducerName, cifNumber: introducerCif } : null
|
||
"
|
||
(introducerChange)="onIntroducerChange($event)"
|
||
[disabled]="readOnly"
|
||
></mifosx-introducer-search>
|
||
}
|
||
}
|
||
|
||
<!-- ══ 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"
|
||
[disabled]="totalJointSharePercentage >= 100 || readOnly"
|
||
(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="1"
|
||
max="100"
|
||
[(ngModel)]="primaryHolderShare"
|
||
[ngModelOptions]="{ standalone: true }"
|
||
[disabled]="readOnly"
|
||
placeholder="0"
|
||
(input)="onPrimaryShareInput($event)"
|
||
(blur)="onPrimaryShareBlur($event)"
|
||
/>
|
||
<span class="soa-pct-unit">%</span>
|
||
@if (primaryHolderShare !== null && primaryHolderShare !== undefined && primaryHolderShare < 1) {
|
||
<div class="soa-pct-error">Min 1%</div>
|
||
}
|
||
</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"
|
||
[disabled]="readOnly"
|
||
(click)="!readOnly && 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"
|
||
[disabled]="readOnly"
|
||
(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]="isJointOwnershipValid"
|
||
[class.soa-pct-badge--warn]="!isJointOwnershipValid"
|
||
>
|
||
{{ 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 (!isJointOwnershipValid) {
|
||
<div class="soa-alert soa-alert--warn" style="margin-top: 8px">
|
||
<mat-icon>warning_amber</mat-icon>
|
||
@if (totalJointSharePercentage !== 100) {
|
||
Total ownership is {{ totalJointSharePercentage }}%. It must equal 100%.
|
||
} @else {
|
||
Each holder must have at least 1% ownership share.
|
||
}
|
||
</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"
|
||
[disabled]="readOnly"
|
||
(click)="!readOnly && (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 }"
|
||
[disabled]="readOnly"
|
||
/>
|
||
<mat-datepicker-toggle matSuffix [for]="minorDobPicker" [disabled]="readOnly"></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 {{ minorAgeThreshold }} 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 }"
|
||
[disabled]="readOnly"
|
||
>
|
||
@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
|
||
[excludedClientIds]="primaryClientId ? [primaryClientId] : []"
|
||
(introducerChange)="onGuardianChange($event)"
|
||
[disabled]="readOnly"
|
||
></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"
|
||
[disabled]="readOnly"
|
||
(click)="!readOnly && (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"
|
||
[disabled]="readOnly"
|
||
(click)="!readOnly && (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">
|
||
<!-- Card Header -->
|
||
<div class="ni-card__header">
|
||
<div class="ni-card__header-left">
|
||
<div class="ni-card__header-icon">
|
||
<i class="fas fa-building"></i>
|
||
</div>
|
||
<div>
|
||
<h3 class="ni-card__title">Non-Individual Account Details</h3>
|
||
<span class="ni-card__sub">As per RBI / Indian banking norms for non-individual entities</span>
|
||
</div>
|
||
</div>
|
||
<span class="ni-pill ni-pill--entity">Entity Account</span>
|
||
</div>
|
||
|
||
<!-- ── A. Partners / Members ─────────────────────────────── -->
|
||
<div class="ni-block">
|
||
<div class="ni-block__head">
|
||
<div class="ni-block__head-left">
|
||
<span class="ni-block__letter">A</span>
|
||
<div>
|
||
<span class="ni-block__title">Partners / Members</span>
|
||
<span class="ni-block__hint">Authorised persons linked to this entity account</span>
|
||
</div>
|
||
</div>
|
||
<button
|
||
mat-flat-button
|
||
color="primary"
|
||
type="button"
|
||
class="ni-add-btn"
|
||
[disabled]="totalSharePercentage >= 100 || readOnly"
|
||
(click)="openAddRelatedClientDialog()"
|
||
>
|
||
<mat-icon>person_add</mat-icon> Add Partner
|
||
</button>
|
||
</div>
|
||
|
||
@if (relatedClients.length > 0) {
|
||
<div class="ni-table-wrap">
|
||
<table class="ni-tbl">
|
||
<thead>
|
||
<tr>
|
||
<th class="ni-th ni-th--sr">Sr.</th>
|
||
<th class="ni-th">Partner Name</th>
|
||
<th class="ni-th">CIF No.</th>
|
||
<th class="ni-th ni-th--center">Ownership %</th>
|
||
<th class="ni-th ni-th--center">Signing Authority</th>
|
||
<th class="ni-th ni-th--center">Action</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
@for (c of relatedClients; track c; let i = $index) {
|
||
<tr class="ni-tr" [class.ni-tr--mandatory]="c.isMandatory">
|
||
<td class="ni-td ni-td--sr">{{ i + 1 }}</td>
|
||
<td class="ni-td">
|
||
<div class="ni-client-cell">
|
||
<div class="ni-avatar" [class.ni-avatar--mandatory]="c.isMandatory">
|
||
{{ c.clientName[0].toUpperCase() }}
|
||
</div>
|
||
<div>
|
||
<span class="ni-client-name">{{ c.clientName }}</span>
|
||
@if (c.isMandatory) {
|
||
<span class="ni-mandatory-tag">Mandatory Signatory</span>
|
||
}
|
||
</div>
|
||
</div>
|
||
</td>
|
||
<td class="ni-td">
|
||
<span class="ni-mono">{{ c.clientCif || '—' }}</span>
|
||
</td>
|
||
<td class="ni-td ni-td--center">
|
||
<span class="ni-pct-badge" [class.ni-pct-badge--full]="c.sharePercentage === 100">
|
||
{{ c.sharePercentage }}%
|
||
</span>
|
||
</td>
|
||
<td class="ni-td ni-td--center">
|
||
<button
|
||
type="button"
|
||
class="ni-sign-toggle"
|
||
[class.ni-sign-toggle--mandatory]="c.isMandatory"
|
||
[class.ni-sign-toggle--optional]="!c.isMandatory"
|
||
[disabled]="readOnly"
|
||
(click)="!readOnly && toggleMandatory(c)"
|
||
[title]="c.isMandatory ? 'Click to set Optional' : 'Click to set Mandatory'"
|
||
>
|
||
<mat-icon>{{ c.isMandatory ? 'lock' : 'lock_open' }}</mat-icon>
|
||
{{ c.isMandatory ? 'Mandatory' : 'Optional' }}
|
||
</button>
|
||
</td>
|
||
<td class="ni-td ni-td--center">
|
||
<button
|
||
mat-icon-button
|
||
color="warn"
|
||
type="button"
|
||
[disabled]="readOnly"
|
||
(click)="removeRelatedClient(i)"
|
||
class="ni-del-btn"
|
||
>
|
||
<mat-icon>delete_outline</mat-icon>
|
||
</button>
|
||
</td>
|
||
</tr>
|
||
}
|
||
</tbody>
|
||
<tfoot>
|
||
<tr class="ni-tr-total">
|
||
<td colspan="3" class="ni-td ni-td--total-label">Total Ownership</td>
|
||
<td class="ni-td ni-td--center">
|
||
<span
|
||
class="ni-pct-badge"
|
||
[class.ni-pct-badge--ok]="totalSharePercentage === 100"
|
||
[class.ni-pct-badge--warn]="totalSharePercentage !== 100"
|
||
>
|
||
{{ totalSharePercentage }}%
|
||
</span>
|
||
</td>
|
||
<td class="ni-td ni-td--center">
|
||
<span class="ni-mandatory-count-badge">
|
||
<mat-icon>lock</mat-icon> {{ mandatoryCount }} Mandatory
|
||
</span>
|
||
</td>
|
||
<td></td>
|
||
</tr>
|
||
</tfoot>
|
||
</table>
|
||
</div>
|
||
@if (totalSharePercentage !== 100) {
|
||
<div class="ni-alert ni-alert--warn">
|
||
<mat-icon>warning_amber</mat-icon>
|
||
Total ownership is {{ totalSharePercentage }}%. It must equal 100%.
|
||
</div>
|
||
} @else {
|
||
<div class="ni-alert ni-alert--ok">
|
||
<mat-icon>check_circle</mat-icon>
|
||
Ownership allocation is complete (100%).
|
||
</div>
|
||
}
|
||
} @else {
|
||
<div class="ni-empty-state">
|
||
<mat-icon>group_add</mat-icon>
|
||
<span>No partners added yet. Click <strong>Add Partner</strong> to begin.</span>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<!-- ── B. Mode of Operation / Signatory Rights (auto from partners) ── -->
|
||
<div class="ni-block ni-block--sep">
|
||
<div class="ni-block__head">
|
||
<div class="ni-block__head-left">
|
||
<span class="ni-block__letter ni-block__letter--b">B</span>
|
||
<div>
|
||
<span class="ni-block__title">Mode of Operation / Signatory Rights</span>
|
||
<span class="ni-block__hint">Auto-computed from Mandatory partners — you can override manually</span>
|
||
</div>
|
||
</div>
|
||
@if (signatoryRights) {
|
||
<span class="ni-auto-badge"> <mat-icon>auto_awesome</mat-icon> Auto </span>
|
||
}
|
||
</div>
|
||
|
||
@if (relatedClients.length === 0) {
|
||
<div class="ni-empty-state">
|
||
<mat-icon>how_to_vote</mat-icon>
|
||
<span>Add partners in Section A and set Mandatory/Optional to auto-compute the mode.</span>
|
||
</div>
|
||
} @else {
|
||
<div class="ni-signatory-grid">
|
||
@for (opt of signatoryRightsOptions; track opt.value) {
|
||
<button
|
||
type="button"
|
||
class="ni-sig-card"
|
||
[class.ni-sig-card--selected]="signatoryRights === opt.value"
|
||
[disabled]="readOnly"
|
||
(click)="!readOnly && (signatoryRights = opt.value)"
|
||
>
|
||
<mat-icon class="ni-sig-card__icon">{{ opt.icon }}</mat-icon>
|
||
<div class="ni-sig-card__code">{{ opt.label }}</div>
|
||
<div class="ni-sig-card__desc">{{ opt.sublabel }}</div>
|
||
@if (signatoryRights === opt.value) {
|
||
<mat-icon class="ni-sig-card__check">check_circle</mat-icon>
|
||
}
|
||
</button>
|
||
}
|
||
</div>
|
||
|
||
<div class="ni-mode-summary" [class.ni-mode-summary--set]="signatoryRights">
|
||
@if (signatoryRights) {
|
||
<mat-icon>how_to_vote</mat-icon>
|
||
<span
|
||
>Mode of Operation: <strong>{{ selectedSignatoryLabel }}</strong></span
|
||
>
|
||
<span class="ni-mode-summary__from">
|
||
({{ mandatoryCount }} Mandatory + {{ relatedClients.length - mandatoryCount }} Optional partner(s))
|
||
</span>
|
||
} @else {
|
||
<mat-icon>info</mat-icon>
|
||
<span>Set Mandatory/Optional for partners above to auto-select mode.</span>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
@if (!hideButtons) {
|
||
<div class="layout-row align-center gap-2percent margin-t responsive-column">
|
||
<button mat-raised-button matStepperPrevious disabled>
|
||
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
|
||
{{ 'labels.buttons.Previous' | translate }}
|
||
</button>
|
||
<button mat-raised-button matStepperNext [disabled]="currentAccountDetailsForm.invalid">
|
||
{{ 'labels.buttons.Next' | translate }}
|
||
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
|
||
</button>
|
||
</div>
|
||
}
|
||
</form>
|