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>
1507 lines
68 KiB
HTML
1507 lines
68 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]="createClientForm">
|
||
<div class="step-header">
|
||
<div class="step-header__left">
|
||
<fa-icon icon="user-plus" class="step-header__icon"></fa-icon>
|
||
<div>
|
||
<h3 class="step-header__title">General Details</h3>
|
||
<p class="step-header__sub">Client registration — personal & identification details.</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Section: Basic Information -->
|
||
<div class="section-block">
|
||
<h4 class="section-title">
|
||
<fa-icon icon="briefcase" class="section-icon"></fa-icon>
|
||
Basic Information
|
||
</h4>
|
||
<div class="form-grid">
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>{{ 'labels.inputs.Office' | translate }}</mat-label>
|
||
<mat-select required formControlName="officeId">
|
||
@for (office of officeOptions; track office) {
|
||
<mat-option [value]="office.id">{{ office.name }}</mat-option>
|
||
}
|
||
</mat-select>
|
||
@if (createClientForm.controls.officeId.hasError('required')) {
|
||
<mat-error>Office is <strong>required</strong></mat-error>
|
||
}
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>{{ 'labels.inputs.Legal Form' | translate }}</mat-label>
|
||
<mat-select required formControlName="legalFormId">
|
||
<mat-select-trigger>
|
||
@if (createClientForm.get('legalFormId')?.value) {
|
||
<span class="legal-form-option">
|
||
<mat-icon class="legal-form-option-icon">{{
|
||
createClientForm.get('legalFormId')?.value === 1 ? 'person' : 'business'
|
||
}}</mat-icon>
|
||
{{ getSelectedLegalFormLabel() }}
|
||
</span>
|
||
}
|
||
</mat-select-trigger>
|
||
@for (legalForm of legalFormOptions; track legalForm) {
|
||
<mat-option [value]="legalForm.id">
|
||
<span class="legal-form-option">
|
||
<mat-icon class="legal-form-option-icon">{{ legalForm.id === 1 ? 'person' : 'business' }}</mat-icon>
|
||
{{ legalForm.id === 1 ? 'Individual' : 'Non-Individual' }}
|
||
</span>
|
||
</mat-option>
|
||
}
|
||
</mat-select>
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Nationality</mat-label>
|
||
<mat-select formControlName="nationalityId">
|
||
<mat-select-trigger>
|
||
@if (getSelectedNationality()) {
|
||
<span class="nat-option">
|
||
@if (getSelectedNationality().flagType === 'india') {
|
||
<span class="nat-flag">
|
||
<svg viewBox="0 0 36 24" class="nat-flag-svg">
|
||
<rect width="36" height="8" fill="#FF9933" />
|
||
<rect y="8" width="36" height="8" fill="#FFFFFF" />
|
||
<rect y="16" width="36" height="8" fill="#138808" />
|
||
<circle cx="18" cy="12" r="3" fill="#000080" />
|
||
<circle cx="18" cy="12" r="2.5" fill="#FFFFFF" />
|
||
<circle cx="18" cy="12" r="0.7" fill="#000080" />
|
||
</svg>
|
||
</span>
|
||
} @else {
|
||
<mat-icon class="nat-globe-icon">{{ getSelectedNationality().icon }}</mat-icon>
|
||
}
|
||
{{ getSelectedNationality().label }}
|
||
</span>
|
||
}
|
||
</mat-select-trigger>
|
||
@for (nat of nationalityOptions; track nat) {
|
||
<mat-option [value]="nat.value">
|
||
<span class="nat-option">
|
||
@if (nat.flagType === 'india') {
|
||
<span class="nat-flag">
|
||
<svg viewBox="0 0 36 24" class="nat-flag-svg">
|
||
<rect width="36" height="8" fill="#FF9933" />
|
||
<rect y="8" width="36" height="8" fill="#FFFFFF" />
|
||
<rect y="16" width="36" height="8" fill="#138808" />
|
||
<circle cx="18" cy="12" r="3" fill="#000080" />
|
||
<circle cx="18" cy="12" r="2.5" fill="#FFFFFF" />
|
||
<circle cx="18" cy="12" r="0.7" fill="#000080" />
|
||
</svg>
|
||
</span>
|
||
} @else {
|
||
<mat-icon class="nat-globe-icon">{{ nat.icon }}</mat-icon>
|
||
}
|
||
{{ nat.label }}
|
||
</span>
|
||
</mat-option>
|
||
}
|
||
</mat-select>
|
||
</mat-form-field>
|
||
|
||
@if (createClientForm.get('legalFormId')?.value === LegalFormId.PERSON) {
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>{{ 'labels.inputs.Is staff' | translate }}?</mat-label>
|
||
<mat-select formControlName="isStaff">
|
||
<mat-option [value]="false">No</mat-option>
|
||
<mat-option [value]="true">Yes</mat-option>
|
||
</mat-select>
|
||
</mat-form-field>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Section: Client Name & Personal Details -->
|
||
<div class="section-block">
|
||
<h4 class="section-title">
|
||
<fa-icon icon="user" class="section-icon"></fa-icon>
|
||
{{ 'labels.inputs.Client Details' | translate }}
|
||
</h4>
|
||
<div class="form-grid">
|
||
<!-- Entity: Full Name -->
|
||
@if (createClientForm.get('fullname')) {
|
||
<mat-form-field appearance="outline" class="form-field full-width">
|
||
<mat-label>
|
||
{{
|
||
'labels.inputs.' + getDateLabel(createClientForm.get('legalFormId')?.value, ['Name', 'Entity Name'])
|
||
| translate
|
||
}}
|
||
</mat-label>
|
||
<input
|
||
matInput
|
||
required
|
||
formControlName="fullname"
|
||
autocomplete="off"
|
||
(blur)="capitalizeControl('fullname')"
|
||
style="text-transform: capitalize"
|
||
/>
|
||
@if (createClientForm.controls.fullname.hasError('required')) {
|
||
<mat-error>Client name is <strong>required</strong></mat-error>
|
||
} @else if (createClientForm.controls.fullname.hasError('pattern')) {
|
||
<mat-error>Only letters, spaces, dots, hyphens and apostrophes allowed</mat-error>
|
||
} @else if (createClientForm.controls.fullname.hasError('maxlength')) {
|
||
<mat-error>Name cannot exceed 100 characters</mat-error>
|
||
}
|
||
</mat-form-field>
|
||
|
||
<!-- Entity Name Verify -->
|
||
@if (createClientForm.get('fullname')?.value) {
|
||
<div class="full-name-row full-width">
|
||
<div class="full-name-preview">
|
||
<span class="full-name-label">Entity Name:</span>
|
||
<span class="full-name-value" style="text-transform: capitalize">{{
|
||
createClientForm.get('fullname')?.value
|
||
}}</span>
|
||
</div>
|
||
<div class="verify-inline">
|
||
@if (!sanctionsVerified && (!sanctionsBlocked || sanctionsNeedsRescreen)) {
|
||
<button
|
||
mat-stroked-button
|
||
color="primary"
|
||
type="button"
|
||
class="verify-name-btn"
|
||
[disabled]="readOnly"
|
||
(click)="verifySanctions()"
|
||
>
|
||
<fa-icon icon="shield-alt" class="verify-name-icon"></fa-icon>
|
||
{{ sanctionsNeedsRescreen ? 'Re-verify Entity' : 'Verify Entity' }}
|
||
</button>
|
||
}
|
||
@if (sanctionsVerified) {
|
||
<span class="sanctions-citation">{{ sanctionsCitation }}</span>
|
||
<span class="verify-status verified"> <fa-icon icon="check-circle"></fa-icon> Verified </span>
|
||
<mat-icon
|
||
class="sanctions-info-icon"
|
||
[matTooltip]="sanctionsTooltip"
|
||
matTooltipClass="sanctions-tooltip"
|
||
>info</mat-icon
|
||
>
|
||
}
|
||
@if (sanctionsBlocked) {
|
||
<span class="sanctions-citation">{{ sanctionsCitation }}</span>
|
||
<span class="verify-status blocked">
|
||
<fa-icon icon="exclamation-triangle"></fa-icon> {{ sanctionsStatusLabel }}
|
||
</span>
|
||
<mat-icon
|
||
class="sanctions-info-icon blocked-info"
|
||
[matTooltip]="sanctionsTooltip"
|
||
matTooltipClass="sanctions-tooltip"
|
||
>info</mat-icon
|
||
>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
}
|
||
|
||
<!-- Person: Salutation + First / Middle / Last Name -->
|
||
@if (
|
||
createClientForm.get('firstname') || createClientForm.get('middlename') || createClientForm.get('lastname')
|
||
) {
|
||
<div class="name-fields-row">
|
||
@if (createClientForm.get('salutationId') && salutationOptions?.length > 0) {
|
||
<mat-form-field appearance="outline" class="name-field salutation-field-wide">
|
||
<mat-label>Salutation</mat-label>
|
||
<span matPrefix class="field-prefix-icon"><mat-icon>person</mat-icon></span>
|
||
<mat-select formControlName="salutationId">
|
||
@for (salutation of salutationOptions; track salutation) {
|
||
<mat-option [value]="salutation.id">{{ salutation.name }}</mat-option>
|
||
}
|
||
</mat-select>
|
||
</mat-form-field>
|
||
}
|
||
@if (createClientForm.get('firstname')) {
|
||
<mat-form-field appearance="outline" class="name-field first-name">
|
||
<mat-label>{{ 'labels.inputs.First Name' | translate }}</mat-label>
|
||
<span matPrefix class="field-prefix-icon"><mat-icon>badge</mat-icon></span>
|
||
<input
|
||
matInput
|
||
required
|
||
formControlName="firstname"
|
||
autocomplete="off"
|
||
(blur)="capitalizeControl('firstname')"
|
||
style="text-transform: capitalize"
|
||
/>
|
||
@if (createClientForm.controls.firstname.hasError('required')) {
|
||
<mat-error>First name is <strong>required</strong></mat-error>
|
||
} @else if (createClientForm.controls.firstname.hasError('pattern')) {
|
||
<mat-error>Only letters, spaces, dots and hyphens allowed</mat-error>
|
||
} @else if (createClientForm.controls.firstname.hasError('maxlength')) {
|
||
<mat-error>Cannot exceed 50 characters</mat-error>
|
||
}
|
||
</mat-form-field>
|
||
}
|
||
@if (createClientForm.get('middlename')) {
|
||
<mat-form-field appearance="outline" class="name-field middle-name">
|
||
<mat-label>{{ 'labels.inputs.Middle Name' | translate }}</mat-label>
|
||
<span matPrefix class="field-prefix-icon"><mat-icon>person_outline</mat-icon></span>
|
||
<input
|
||
matInput
|
||
formControlName="middlename"
|
||
autocomplete="off"
|
||
(blur)="capitalizeControl('middlename')"
|
||
style="text-transform: capitalize"
|
||
/>
|
||
@if (
|
||
createClientForm.controls.middlename?.hasError('pattern') &&
|
||
createClientForm.controls.middlename?.touched
|
||
) {
|
||
<mat-error>Only letters, spaces, dots and hyphens allowed</mat-error>
|
||
}
|
||
</mat-form-field>
|
||
}
|
||
@if (createClientForm.get('lastname')) {
|
||
<mat-form-field appearance="outline" class="name-field last-name">
|
||
<mat-label>{{ 'labels.inputs.Last Name' | translate }}</mat-label>
|
||
<span matPrefix class="field-prefix-icon"><mat-icon>person</mat-icon></span>
|
||
<input
|
||
matInput
|
||
required
|
||
formControlName="lastname"
|
||
autocomplete="off"
|
||
(blur)="capitalizeControl('lastname')"
|
||
style="text-transform: capitalize"
|
||
/>
|
||
@if (createClientForm.controls.lastname.hasError('required')) {
|
||
<mat-error>Last name is <strong>required</strong></mat-error>
|
||
} @else if (createClientForm.controls.lastname.hasError('pattern')) {
|
||
<mat-error>Only letters, spaces, dots and hyphens allowed</mat-error>
|
||
} @else if (createClientForm.controls.lastname.hasError('maxlength')) {
|
||
<mat-error>Cannot exceed 50 characters</mat-error>
|
||
}
|
||
</mat-form-field>
|
||
}
|
||
</div>
|
||
|
||
<!-- Full Name Preview + Verify -->
|
||
@if (fullNamePreview) {
|
||
<div class="full-name-row">
|
||
<div class="full-name-preview">
|
||
<span class="full-name-label">Full Name:</span>
|
||
<span class="full-name-value">{{ fullNamePreview }}</span>
|
||
</div>
|
||
<div class="verify-inline">
|
||
@if (!sanctionsVerified && (!sanctionsBlocked || sanctionsNeedsRescreen)) {
|
||
<button
|
||
mat-stroked-button
|
||
color="primary"
|
||
type="button"
|
||
class="verify-name-btn"
|
||
[disabled]="readOnly"
|
||
(click)="verifySanctions()"
|
||
>
|
||
<fa-icon icon="shield-alt" class="verify-name-icon"></fa-icon>
|
||
{{ sanctionsNeedsRescreen ? 'Re-verify Client' : 'Verify Client' }}
|
||
</button>
|
||
}
|
||
@if (sanctionsVerified) {
|
||
<span class="sanctions-citation">{{ sanctionsCitation }}</span>
|
||
<span class="verify-status verified"> <fa-icon icon="check-circle"></fa-icon> Verified </span>
|
||
<mat-icon
|
||
class="sanctions-info-icon"
|
||
[matTooltip]="sanctionsTooltip"
|
||
matTooltipClass="sanctions-tooltip"
|
||
>info</mat-icon
|
||
>
|
||
}
|
||
@if (sanctionsBlocked) {
|
||
<span class="sanctions-citation">{{ sanctionsCitation }}</span>
|
||
<span class="verify-status blocked">
|
||
<fa-icon icon="exclamation-triangle"></fa-icon> {{ sanctionsStatusLabel }}
|
||
</span>
|
||
<mat-icon
|
||
class="sanctions-info-icon blocked-info"
|
||
[matTooltip]="sanctionsTooltip"
|
||
matTooltipClass="sanctions-tooltip"
|
||
>info</mat-icon
|
||
>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
}
|
||
|
||
<!-- DOB (Person only) -->
|
||
@if (createClientForm.get('legalFormId')?.value === LegalFormId.PERSON) {
|
||
<mat-form-field appearance="outline" class="form-field" (click)="dateOfBirthDatePicker.open()">
|
||
<mat-label>Date of Birth</mat-label>
|
||
<span matPrefix class="field-prefix-icon"><mat-icon>calendar_today</mat-icon></span>
|
||
<input
|
||
matInput
|
||
required
|
||
[max]="maxDate"
|
||
[matDatepicker]="dateOfBirthDatePicker"
|
||
formControlName="dateOfBirth"
|
||
/>
|
||
<mat-datepicker-toggle matSuffix [for]="dateOfBirthDatePicker"></mat-datepicker-toggle>
|
||
<mat-datepicker #dateOfBirthDatePicker></mat-datepicker>
|
||
@if (
|
||
createClientForm.get('dateOfBirth')?.hasError('required') && createClientForm.get('dateOfBirth')?.touched
|
||
) {
|
||
<mat-error>Date of Birth is <strong>required</strong></mat-error>
|
||
}
|
||
</mat-form-field>
|
||
}
|
||
|
||
@if (createClientForm.get('legalFormId')?.value === LegalFormId.PERSON) {
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>{{ 'labels.inputs.Gender' | translate }}</mat-label>
|
||
<mat-select required formControlName="genderId">
|
||
<mat-select-trigger>
|
||
@if (getSelectedGender()) {
|
||
<span class="icon-option">
|
||
<mat-icon class="icon-option-icon">{{ getGenderIcon(getSelectedGender().name) }}</mat-icon>
|
||
{{ getSelectedGender().name | titlecase }}
|
||
</span>
|
||
}
|
||
</mat-select-trigger>
|
||
@for (gender of genderOptions; track gender) {
|
||
<mat-option [value]="gender.id">
|
||
<span class="icon-option">
|
||
<mat-icon class="icon-option-icon">{{ getGenderIcon(gender.name) }}</mat-icon>
|
||
{{ gender.name | titlecase }}
|
||
</span>
|
||
</mat-option>
|
||
}
|
||
</mat-select>
|
||
@if (createClientForm.get('genderId')?.hasError('required') && createClientForm.get('genderId')?.touched) {
|
||
<mat-error>Gender is <strong>required</strong></mat-error>
|
||
}
|
||
</mat-form-field>
|
||
}
|
||
|
||
<!-- Age display below DOB row -->
|
||
@if (createClientForm.get('legalFormId')?.value === LegalFormId.PERSON && calculatedAgeDetail) {
|
||
<div class="age-inline full-width" [class.age-minor]="isMinor">
|
||
<fa-icon [icon]="isMinor ? 'child' : 'clock'" class="age-inline-icon"></fa-icon>
|
||
<span>{{ calculatedAgeDetail }}</span>
|
||
@if (isMinor) {
|
||
<span class="minor-badge">Minor</span>
|
||
}
|
||
@if (isSeniorCitizen) {
|
||
<span class="senior-citizen-badge">Senior Citizen</span>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<!-- Minor / Guardian Mapping (mandatory for a minor -- see MINOR_AGE_THRESHOLD) -->
|
||
@if (isMinor && createClientForm.get('legalFormId')?.value === LegalFormId.PERSON) {
|
||
<div class="guardian-section">
|
||
<!-- Header bar -->
|
||
<div class="guardian-section__header">
|
||
<div class="guardian-section__header-left">
|
||
<mat-icon class="guardian-section__header-icon">family_restroom</mat-icon>
|
||
<div>
|
||
<h4 class="guardian-section__title">Guardian Details</h4>
|
||
<span class="guardian-section__sub">Minor account — guardian is mandatory per RBI KYC norms</span>
|
||
</div>
|
||
</div>
|
||
<div class="guardian-section__header-right">
|
||
<div class="guardian-section__age-chips">
|
||
<span class="guardian-age-chip"
|
||
><strong>{{ ageBreakdown.years }}</strong> yrs</span
|
||
>
|
||
<span class="guardian-age-chip"
|
||
><strong>{{ ageBreakdown.months }}</strong> mos</span
|
||
>
|
||
<span class="guardian-age-chip"
|
||
><strong>{{ ageBreakdown.days }}</strong> days</span
|
||
>
|
||
<span class="guardian-minor-badge">MINOR</span>
|
||
</div>
|
||
@if (!selectedGuardian && !guardianLinking) {
|
||
<button mat-flat-button type="button" class="guardian-add-btn" (click)="guardianLinking = true">
|
||
<mat-icon>person_add</mat-icon>
|
||
Link Guardian
|
||
</button>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Body -->
|
||
<div class="guardian-section__body" formGroupName="guardianDetails">
|
||
<!-- Empty state: no guardian linked, not linking -->
|
||
@if (!selectedGuardian && !guardianLinking) {
|
||
<div class="guardian-empty guardian-empty--required">
|
||
<mat-icon class="guardian-empty__icon">person_off</mat-icon>
|
||
<div class="guardian-empty__content">
|
||
<span class="guardian-empty__text"
|
||
>No guardian linked yet. Click <strong>"Link Guardian"</strong> to search and add.</span
|
||
>
|
||
<span class="guardian-empty__required">Guardian is mandatory for minor accounts</span>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- Linking mode: search + relationship -->
|
||
@if (!selectedGuardian && guardianLinking) {
|
||
<div class="guardian-search-row">
|
||
<mat-form-field appearance="outline" class="guardian-rel-field">
|
||
<mat-label>Relationship</mat-label>
|
||
<mat-select required formControlName="guardianRelation">
|
||
<mat-option value="FATHER"><mat-icon class="icon-option-icon">man</mat-icon> Father</mat-option>
|
||
<mat-option value="MOTHER"><mat-icon class="icon-option-icon">woman</mat-icon> Mother</mat-option>
|
||
<mat-option value="LEGAL_GUARDIAN"
|
||
><mat-icon class="icon-option-icon">gavel</mat-icon> Legal Guardian</mat-option
|
||
>
|
||
<mat-option value="GRANDPARENT"
|
||
><mat-icon class="icon-option-icon">elderly</mat-icon> Grandparent</mat-option
|
||
>
|
||
<mat-option value="OTHER"><mat-icon class="icon-option-icon">group</mat-icon> Other</mat-option>
|
||
</mat-select>
|
||
</mat-form-field>
|
||
|
||
<div class="guardian-search-wrapper guardian-search-col">
|
||
<mat-form-field
|
||
appearance="outline"
|
||
class="guardian-search-field"
|
||
[class.guardian-search-disabled]="!createClientForm.get('guardianDetails.guardianRelation')?.value"
|
||
>
|
||
<mat-label>Search Guardian by Name or Client ID</mat-label>
|
||
<span matPrefix class="field-prefix-icon"><mat-icon>person_search</mat-icon></span>
|
||
<input
|
||
matInput
|
||
[value]="guardianSearchText"
|
||
(input)="onGuardianSearch($any($event.target).value)"
|
||
(blur)="dismissGuardianSearch()"
|
||
[disabled]="!createClientForm.get('guardianDetails.guardianRelation')?.value"
|
||
placeholder="Select relationship first, then search..."
|
||
autocomplete="off"
|
||
/>
|
||
</mat-form-field>
|
||
|
||
@if (guardianSearching) {
|
||
<div class="guardian-results">
|
||
<div class="guardian-results__loading">
|
||
<mat-progress-spinner diameter="20" mode="indeterminate"></mat-progress-spinner>
|
||
<span>Searching...</span>
|
||
</div>
|
||
</div>
|
||
}
|
||
@if (!guardianSearching && guardianSearchResults.length > 0) {
|
||
<div class="guardian-results">
|
||
@for (result of guardianSearchResults; track result.id) {
|
||
<div class="guardian-result-item" (mousedown)="selectGuardian(result); $event.preventDefault()">
|
||
<div class="guardian-result-item__avatar">
|
||
<fa-icon icon="user"></fa-icon>
|
||
</div>
|
||
<div class="guardian-result-item__info">
|
||
<span class="guardian-result-item__name">{{ result.displayName }}</span>
|
||
<span class="guardian-result-item__meta">
|
||
ID: {{ result.id }} · A/C: {{ result.accountNumber }} ·
|
||
{{ result.officeName }}
|
||
@if (result.dateOfBirth) {
|
||
· DOB: {{ result.dateOfBirth | dateFormat }}
|
||
}
|
||
@if (result.fatherSpouseName) {
|
||
· {{ result.fatherSpouseName }}
|
||
}
|
||
</span>
|
||
</div>
|
||
<span class="guardian-result-item__status" [class.active]="result.status?.value === 'Active'">
|
||
{{ result.status?.value }}
|
||
</span>
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
@if (!guardianSearching && guardianSearchText.length >= 2 && guardianSearchResults.length === 0) {
|
||
<div class="search-no-results-hint">No clients found</div>
|
||
}
|
||
</div>
|
||
|
||
<button
|
||
mat-icon-button
|
||
type="button"
|
||
class="guardian-cancel-link"
|
||
(click)="guardianLinking = false"
|
||
matTooltip="Cancel"
|
||
>
|
||
<mat-icon>close</mat-icon>
|
||
</button>
|
||
</div>
|
||
}
|
||
|
||
<!-- Linked Guardian: compact row -->
|
||
@if (selectedGuardian) {
|
||
<div class="guardian-row-table">
|
||
<!-- Table header -->
|
||
<div class="guardian-row__header">
|
||
<span class="guardian-row__col guardian-row__col--photo">Photo</span>
|
||
<span class="guardian-row__col guardian-row__col--name">Name</span>
|
||
<span class="guardian-row__col guardian-row__col--rel">Relationship</span>
|
||
<span class="guardian-row__col guardian-row__col--cif">CIF / A/C</span>
|
||
<span class="guardian-row__col guardian-row__col--status">Status</span>
|
||
<span class="guardian-row__col guardian-row__col--actions">Actions</span>
|
||
</div>
|
||
|
||
<!-- Data row -->
|
||
<div class="guardian-row__data">
|
||
<span class="guardian-row__col guardian-row__col--photo">
|
||
<div class="guardian-row__avatar" [class.has-photo]="selectedGuardian.profileImageUrl">
|
||
@if (selectedGuardian.profileImageUrl) {
|
||
<img
|
||
[src]="selectedGuardian.profileImageUrl"
|
||
alt="Photo"
|
||
class="avatar-photo guardian-row__zoomable"
|
||
(click)="zoomGuardianImage(selectedGuardian.profileImageUrl, 'Guardian Photo')"
|
||
/>
|
||
} @else {
|
||
<fa-icon icon="user-shield"></fa-icon>
|
||
}
|
||
</div>
|
||
</span>
|
||
<span class="guardian-row__col guardian-row__col--name">
|
||
<div class="guardian-row__name-info">
|
||
<span class="guardian-row__name-text">{{ selectedGuardian.displayName }}</span>
|
||
<span class="guardian-row__linked-tag">
|
||
<mat-icon class="guardian-row__link-icon">link</mat-icon>
|
||
Linked Client #{{ selectedGuardian.cifNumber || selectedGuardian.id }}
|
||
</span>
|
||
</div>
|
||
</span>
|
||
<span class="guardian-row__col guardian-row__col--rel">
|
||
<span class="guardian-row__rel-badge">{{
|
||
createClientForm.get('guardianDetails.guardianRelation')?.value || '-'
|
||
}}</span>
|
||
</span>
|
||
<span class="guardian-row__col guardian-row__col--cif">
|
||
<span class="guardian-row__mono">{{
|
||
selectedGuardian.accountNo || selectedGuardian.accountNumber || '-'
|
||
}}</span>
|
||
</span>
|
||
<span class="guardian-row__col guardian-row__col--status">
|
||
<span
|
||
class="guardian-row__status-badge"
|
||
[class.guardian-row__status-badge--active]="selectedGuardian.status?.value === 'Active'"
|
||
>
|
||
{{ selectedGuardian.status?.value || '-' }}
|
||
</span>
|
||
</span>
|
||
<span class="guardian-row__col guardian-row__col--actions">
|
||
<button
|
||
mat-icon-button
|
||
type="button"
|
||
class="guardian-action-btn"
|
||
(click)="guardianExpanded = !guardianExpanded"
|
||
[matTooltip]="guardianExpanded ? 'Collapse' : 'View Details'"
|
||
>
|
||
<mat-icon>{{ guardianExpanded ? 'expand_less' : 'visibility' }}</mat-icon>
|
||
</button>
|
||
<button
|
||
mat-icon-button
|
||
type="button"
|
||
class="guardian-action-btn guardian-action-btn--delete"
|
||
(click)="removeGuardian()"
|
||
matTooltip="Delete"
|
||
>
|
||
<mat-icon>delete</mat-icon>
|
||
</button>
|
||
</span>
|
||
</div>
|
||
|
||
<!-- Expandable detail panel -->
|
||
@if (guardianExpanded) {
|
||
<div class="guardian-row__expand">
|
||
<div class="guardian-expand__grid">
|
||
<div class="guardian-expand__item">
|
||
<span class="guardian-expand__label">Account Type</span>
|
||
<span class="guardian-expand__value">{{ selectedGuardian.accountType || '-' }}</span>
|
||
</div>
|
||
@if (selectedGuardian.panNumber) {
|
||
<div class="guardian-expand__item">
|
||
<span class="guardian-expand__label">PAN</span>
|
||
<span class="guardian-expand__value"
|
||
>{{ selectedGuardian.panNumber }}
|
||
@if (selectedGuardian.panVerified) {
|
||
<mat-icon class="verify-tick-sm">check_circle</mat-icon>
|
||
}
|
||
</span>
|
||
</div>
|
||
}
|
||
@if (selectedGuardian.aadhaarMasked) {
|
||
<div class="guardian-expand__item">
|
||
<span class="guardian-expand__label">Aadhaar</span>
|
||
<span class="guardian-expand__value"
|
||
>{{ selectedGuardian.aadhaarMasked }}
|
||
@if (selectedGuardian.aadhaarVerified) {
|
||
<mat-icon class="verify-tick-sm">check_circle</mat-icon>
|
||
}
|
||
</span>
|
||
</div>
|
||
}
|
||
@if (selectedGuardian.mobileNo) {
|
||
<div class="guardian-expand__item">
|
||
<span class="guardian-expand__label">Mobile</span>
|
||
<span class="guardian-expand__value">{{ selectedGuardian.mobileNo }}</span>
|
||
</div>
|
||
}
|
||
@if (selectedGuardian.fullAddress) {
|
||
<div class="guardian-expand__item guardian-expand__item--wide">
|
||
<span class="guardian-expand__label">Address</span>
|
||
<span class="guardian-expand__value">{{ selectedGuardian.fullAddress }}</span>
|
||
</div>
|
||
}
|
||
</div>
|
||
@if (selectedGuardian.signatureUrl) {
|
||
<div class="guardian-expand__sig">
|
||
<span class="guardian-expand__label">Specimen Signature</span>
|
||
<img
|
||
[src]="selectedGuardian.signatureUrl"
|
||
alt="Signature"
|
||
class="guardian-expand__sig-img guardian-row__zoomable"
|
||
(click)="zoomGuardianImage(selectedGuardian.signatureUrl, 'Specimen Signature')"
|
||
/>
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- Non-Individual Details -->
|
||
@if (createClientForm.get('clientNonPersonDetails')) {
|
||
<div class="form-grid" formGroupName="clientNonPersonDetails" style="margin-top: 16px">
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Constitution Type</mat-label>
|
||
<mat-select formControlName="constitutionId" required>
|
||
<mat-select-trigger>
|
||
@if (selectedConstitution) {
|
||
<mat-icon class="constitution-icon">{{ selectedConstitution.icon }}</mat-icon>
|
||
{{ selectedConstitution.label }}
|
||
}
|
||
</mat-select-trigger>
|
||
@for (ct of enrichedConstitutionOptions; track ct.id) {
|
||
<mat-option [value]="ct.id">
|
||
<mat-icon class="constitution-icon">{{ ct.icon }}</mat-icon>
|
||
{{ ct.label }}
|
||
</mat-option>
|
||
}
|
||
</mat-select>
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Nature of Business / Activity</mat-label>
|
||
<mat-select formControlName="mainBusinessLineId">
|
||
<mat-select-trigger>
|
||
@if (getSelectedBusiness()) {
|
||
<span class="biz-option">
|
||
<mat-icon class="biz-option-icon">{{ getBusinessIcon(getSelectedBusiness().name) }}</mat-icon>
|
||
{{ getSelectedBusiness().name }}
|
||
</span>
|
||
}
|
||
</mat-select-trigger>
|
||
@for (business of businessLineOptions; track business) {
|
||
<mat-option [value]="business.id">
|
||
<span class="biz-option">
|
||
<mat-icon class="biz-option-icon">{{ getBusinessIcon(business.name) }}</mat-icon>
|
||
{{ business.name }}
|
||
</span>
|
||
</mat-option>
|
||
}
|
||
</mat-select>
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Registration / Incorporation Number</mat-label>
|
||
<input matInput formControlName="incorpNumber" placeholder="{{ selectedConstitution?.regHint || '' }}" />
|
||
@if (selectedConstitution?.regHint) {
|
||
<mat-hint>e.g. {{ selectedConstitution.regHint }}</mat-hint>
|
||
}
|
||
</mat-form-field>
|
||
|
||
<!-- Date of Incorporation is captured on the KYC step, next to the entity PAN it is
|
||
verified against — see client-kyc-step's `incorporationDate` control. -->
|
||
|
||
<!-- Authorized Signatories (multiple, registered clients) -->
|
||
<div class="form-field full-width signatory-section">
|
||
<h4 class="section-title signatory-title">
|
||
<fa-icon icon="user-shield" class="section-icon"></fa-icon>
|
||
Authorized Signatories
|
||
<span class="sig-count-badge">{{ signatories.length }}</span>
|
||
</h4>
|
||
<div class="minor-info-bar">
|
||
<fa-icon icon="info-circle" class="minor-info-icon"></fa-icon>
|
||
<span
|
||
>Select registered clients as authorized signatories — as per Board Resolution / Partnership Deed. Max
|
||
10.</span
|
||
>
|
||
</div>
|
||
|
||
<!-- Signatory List Table -->
|
||
@if (signatories.length > 0) {
|
||
<div class="sig-table">
|
||
<div class="sig-table__header">
|
||
<span class="sig-table__col sig-table__col--sno">#</span>
|
||
<span class="sig-table__col sig-table__col--photo"></span>
|
||
<span class="sig-table__col sig-table__col--name">Name</span>
|
||
<span class="sig-table__col sig-table__col--id">Client ID</span>
|
||
<span class="sig-table__col sig-table__col--pan">PAN</span>
|
||
<span class="sig-table__col sig-table__col--din">DIN</span>
|
||
<span class="sig-table__col sig-table__col--status">Status</span>
|
||
<span class="sig-table__col sig-table__col--actions">Actions</span>
|
||
</div>
|
||
@for (sig of signatories; track sig.id; let i = $index) {
|
||
<div class="sig-table__row">
|
||
<span class="sig-table__col sig-table__col--sno">{{ i + 1 }}</span>
|
||
<span class="sig-table__col sig-table__col--photo">
|
||
<div class="sig-table__avatar" [class.has-photo]="sig.profileImageUrl">
|
||
@if (sig.profileImageUrl) {
|
||
<img [src]="sig.profileImageUrl" alt="" class="avatar-photo" />
|
||
} @else {
|
||
<fa-icon icon="user"></fa-icon>
|
||
}
|
||
</div>
|
||
</span>
|
||
<span class="sig-table__col sig-table__col--name">
|
||
<strong>{{ sig.displayName }}</strong>
|
||
@if (sig._loading) {
|
||
<mat-progress-spinner
|
||
diameter="14"
|
||
mode="indeterminate"
|
||
class="sig-inline-spinner"
|
||
></mat-progress-spinner>
|
||
}
|
||
</span>
|
||
<span class="sig-table__col sig-table__col--id">{{ sig.cifNumber || sig.id }}</span>
|
||
<span class="sig-table__col sig-table__col--pan">
|
||
{{ sig.panNumber || '-' }}
|
||
@if (sig.panVerified) {
|
||
<mat-icon class="verify-tick-sm">check_circle</mat-icon>
|
||
}
|
||
</span>
|
||
<span class="sig-table__col sig-table__col--din">
|
||
@if (sig.dinNumber) {
|
||
{{ sig.dinNumber }}
|
||
@if (sig.dinVerified) {
|
||
<mat-icon class="verify-tick-sm">check_circle</mat-icon>
|
||
}
|
||
@if (!sig.dinVerified) {
|
||
<mat-icon class="verify-cross-sm">cancel</mat-icon>
|
||
}
|
||
} @else {
|
||
<button
|
||
mat-stroked-button
|
||
class="din-verify-btn"
|
||
type="button"
|
||
matTooltip="Verify DIN"
|
||
(click)="openDinVerifyDialog(sig)"
|
||
>
|
||
<mat-icon class="din-verify-icon">badge</mat-icon> Verify DIN
|
||
</button>
|
||
}
|
||
</span>
|
||
<span class="sig-table__col sig-table__col--status">
|
||
<span class="sig-status-chip" [class.active]="sig.status?.value === 'Active'">{{
|
||
sig.status?.value || '-'
|
||
}}</span>
|
||
</span>
|
||
<span class="sig-table__col sig-table__col--actions">
|
||
<button mat-icon-button type="button" matTooltip="View Details" (click)="viewSignatoryDetails(sig)">
|
||
<mat-icon class="sig-action-icon">visibility</mat-icon>
|
||
</button>
|
||
<button mat-icon-button type="button" matTooltip="Remove" (click)="removeSignatoryAt(i)">
|
||
<mat-icon class="sig-action-icon remove">close</mat-icon>
|
||
</button>
|
||
</span>
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
|
||
<!-- Add Signatory Search -->
|
||
@if (signatories.length < 10) {
|
||
<div class="sig-add-wrapper">
|
||
<div class="guardian-search-wrapper">
|
||
<mat-form-field appearance="outline" class="guardian-search-field">
|
||
<mat-label>Add Signatory — Search by Name or Client ID</mat-label>
|
||
<span matPrefix class="field-prefix-icon"><mat-icon>person_add</mat-icon></span>
|
||
<input
|
||
matInput
|
||
[value]="signatorySearchText"
|
||
(input)="onSignatorySearch($any($event.target).value)"
|
||
(blur)="dismissSignatorySearch()"
|
||
placeholder="Type name or client ID..."
|
||
autocomplete="off"
|
||
/>
|
||
</mat-form-field>
|
||
@if (signatorySearching) {
|
||
<div class="guardian-results">
|
||
<div class="guardian-results__loading">
|
||
<mat-progress-spinner diameter="20" mode="indeterminate"></mat-progress-spinner>
|
||
<span>Searching...</span>
|
||
</div>
|
||
</div>
|
||
}
|
||
@if (!signatorySearching && signatorySearchResults.length > 0) {
|
||
<div class="guardian-results">
|
||
@for (result of signatorySearchResults; track result.id) {
|
||
<div class="guardian-result-item" (mousedown)="addSignatory(result); $event.preventDefault()">
|
||
<div class="guardian-result-item__avatar"><fa-icon icon="user"></fa-icon></div>
|
||
<div class="guardian-result-item__info">
|
||
<span class="guardian-result-item__name">{{ result.displayName }}</span>
|
||
<span class="guardian-result-item__meta"
|
||
>ID: {{ result.id }} · A/C: {{ result.accountNumber }}
|
||
·
|
||
{{ result.officeName }}
|
||
@if (result.dateOfBirth) {
|
||
· DOB: {{ result.dateOfBirth | dateFormat }}
|
||
}
|
||
@if (result.fatherSpouseName) {
|
||
· {{ result.fatherSpouseName }}
|
||
}
|
||
</span>
|
||
</div>
|
||
<span class="guardian-result-item__status" [class.active]="result.status?.value === 'Active'">{{
|
||
result.status?.value
|
||
}}</span>
|
||
</div>
|
||
}
|
||
</div>
|
||
}
|
||
@if (!signatorySearching && signatorySearchText.length >= 2 && signatorySearchResults.length === 0) {
|
||
<div class="search-no-results-hint">No clients found</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<!-- Additional field based on constitution -->
|
||
@if (selectedConstitution?.additionalField) {
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>{{ selectedConstitution.additionalField }}</mat-label>
|
||
<input matInput formControlName="additionalInfo" />
|
||
</mat-form-field>
|
||
}
|
||
|
||
<mat-form-field appearance="outline" class="form-field full-width">
|
||
<mat-label>Remarks</mat-label>
|
||
<textarea matInput formControlName="remarks" cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
|
||
</mat-form-field>
|
||
</div>
|
||
}
|
||
</div>
|
||
|
||
<!-- Section: Contact Details -->
|
||
<div class="section-block">
|
||
<h4 class="section-title">
|
||
<fa-icon icon="mobile-alt" class="section-icon"></fa-icon>
|
||
Contact Details
|
||
<mat-icon
|
||
class="dip-info-icon"
|
||
matTooltip="Digital Intelligence Platform (DIP) refers to the fraud-monitoring and intelligence ecosystem created by the Department of Telecommunications (DoT)"
|
||
matTooltipPosition="above"
|
||
>info</mat-icon
|
||
>
|
||
</h4>
|
||
|
||
<div class="form-grid">
|
||
<!-- Row 1: Mobile No | DIP Verify (primary) -->
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>{{ 'labels.inputs.Mobile No' | translate }}</mat-label>
|
||
<span matPrefix class="field-prefix-icon"><mat-icon>phone_android</mat-icon></span>
|
||
<input
|
||
matInput
|
||
required
|
||
type="text"
|
||
formControlName="mobileNo"
|
||
placeholder="e.g. 9876543210"
|
||
maxlength="10"
|
||
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()"
|
||
(paste)="$event.preventDefault()"
|
||
/>
|
||
@if (createClientForm.get('mobileNo')?.hasError('required') && createClientForm.get('mobileNo')?.touched) {
|
||
<mat-error>Mobile number is <strong>required</strong></mat-error>
|
||
} @else if (
|
||
createClientForm.get('mobileNo')?.hasError('pattern') && createClientForm.get('mobileNo')?.touched
|
||
) {
|
||
<mat-error>Enter valid 10-digit number (starts with 6-9)</mat-error>
|
||
}
|
||
</mat-form-field>
|
||
|
||
<div class="form-field mobile-verify-row">
|
||
@if (!mobileVerified) {
|
||
<button
|
||
mat-stroked-button
|
||
color="primary"
|
||
type="button"
|
||
class="verify-btn"
|
||
[disabled]="readOnly || createClientForm.get('mobileNo')?.value?.length !== 10"
|
||
(click)="verifyWithDIP()"
|
||
>
|
||
<fa-icon icon="shield-alt" class="m-r-10"></fa-icon>
|
||
Verify with DIP
|
||
</button>
|
||
} @else {
|
||
<span class="text-success verified-label">✔ Verified</span>
|
||
}
|
||
</div>
|
||
|
||
<!-- Row 2: Alternate Mobile No | Alternate DIP Verify -->
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Alternate Mobile No</mat-label>
|
||
<span matPrefix class="field-prefix-icon"><mat-icon>phone_android</mat-icon></span>
|
||
<input
|
||
matInput
|
||
type="text"
|
||
formControlName="alternateMobileNo"
|
||
placeholder="e.g. 9876543210"
|
||
maxlength="10"
|
||
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()"
|
||
(paste)="$event.preventDefault()"
|
||
/>
|
||
@if (
|
||
createClientForm.get('alternateMobileNo')?.hasError('pattern') &&
|
||
createClientForm.get('alternateMobileNo')?.touched
|
||
) {
|
||
<mat-error>Enter valid 10-digit number (starts with 6-9)</mat-error>
|
||
}
|
||
</mat-form-field>
|
||
|
||
<div class="form-field mobile-verify-row">
|
||
@if (!alternateMobileVerified) {
|
||
<button
|
||
mat-stroked-button
|
||
color="primary"
|
||
type="button"
|
||
class="verify-btn"
|
||
[disabled]="readOnly || createClientForm.get('alternateMobileNo')?.value?.length !== 10"
|
||
(click)="verifyAlternateDIP()"
|
||
>
|
||
<fa-icon icon="shield-alt" class="m-r-10"></fa-icon>
|
||
Verify with DIP
|
||
</button>
|
||
} @else {
|
||
<span class="text-success verified-label">✔ Verified</span>
|
||
}
|
||
</div>
|
||
|
||
<!-- Row 3: Email | Marital Status -->
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>{{ 'labels.inputs.Email Address' | translate }}</mat-label>
|
||
<span matPrefix class="field-prefix-icon"><mat-icon>email</mat-icon></span>
|
||
<input matInput required formControlName="emailAddress" placeholder="e.g. name@example.com" />
|
||
@if (
|
||
createClientForm.get('emailAddress')?.hasError('required') && createClientForm.get('emailAddress')?.touched
|
||
) {
|
||
<mat-error>Email is <strong>required</strong></mat-error>
|
||
} @else if (createClientForm.get('emailAddress')?.invalid && createClientForm.get('emailAddress')?.touched) {
|
||
<mat-error>Enter a valid email (e.g. name@example.com)</mat-error>
|
||
}
|
||
</mat-form-field>
|
||
|
||
@if (
|
||
createClientForm.get('legalFormId')?.value === LegalFormId.PERSON && maritalStatusOptions.length > 0 && !isMinor
|
||
) {
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Marital Status</mat-label>
|
||
<mat-select formControlName="maritalStatusId">
|
||
<mat-select-trigger>
|
||
@if (getSelectedMaritalStatus()) {
|
||
<span class="icon-option">
|
||
<mat-icon class="icon-option-icon">{{
|
||
getMaritalStatusIcon(getSelectedMaritalStatus().name)
|
||
}}</mat-icon>
|
||
{{ getSelectedMaritalStatus().name }}
|
||
</span>
|
||
}
|
||
</mat-select-trigger>
|
||
@for (ms of maritalStatusOptions; track ms) {
|
||
<mat-option [value]="ms.id">
|
||
<span class="icon-option">
|
||
<mat-icon class="icon-option-icon">{{ getMaritalStatusIcon(ms.name) }}</mat-icon>
|
||
{{ ms.name }}
|
||
</span>
|
||
</mat-option>
|
||
}
|
||
</mat-select>
|
||
</mat-form-field>
|
||
}
|
||
|
||
<!-- CLIENT CREATION contact-OTP verification — create mode only. Edit mode already has
|
||
its own dual old/new OTP panel in EditClientComponent's template; this single
|
||
"new value" block only makes sense for a brand-new client (no "old" destination to
|
||
verify against). Each block only appears once its own field passes validation, so
|
||
staff enter a plausible-looking number/email before being asked to verify it. -->
|
||
@if (!clientData) {
|
||
<div class="contact-otp-row">
|
||
@if (createClientForm.get('mobileNo')?.valid) {
|
||
<mifosx-client-contact-otp-block
|
||
channel="MOBILE"
|
||
[destination]="createClientForm.get('mobileNo')?.value"
|
||
label="Verify Mobile Number"
|
||
(stateChange)="onMobileOtpState($event)"
|
||
></mifosx-client-contact-otp-block>
|
||
}
|
||
@if (createClientForm.get('emailAddress')?.valid) {
|
||
<mifosx-client-contact-otp-block
|
||
channel="EMAIL"
|
||
[destination]="createClientForm.get('emailAddress')?.value"
|
||
label="Verify Email Address"
|
||
(stateChange)="onEmailOtpState($event)"
|
||
></mifosx-client-contact-otp-block>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
|
||
<!-- Section: Education & Social Details (Individual only) -->
|
||
@if (createClientForm.get('legalFormId')?.value === LegalFormId.PERSON) {
|
||
<div class="section-block">
|
||
<h4 class="section-title">
|
||
<fa-icon icon="book" class="section-icon"></fa-icon>
|
||
Education & Social Details
|
||
</h4>
|
||
<div class="form-grid">
|
||
<!-- Education Level -->
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Education Level</mat-label>
|
||
<mat-select formControlName="educationLevelId">
|
||
<mat-select-trigger>
|
||
@if (getSelectedEducationLevel()) {
|
||
<span class="icon-option">
|
||
<mat-icon class="icon-option-icon">{{
|
||
getEducationLevelIcon(getSelectedEducationLevel().name)
|
||
}}</mat-icon>
|
||
{{ getSelectedEducationLevel().name }}
|
||
</span>
|
||
}
|
||
</mat-select-trigger>
|
||
@for (el of educationLevelOptions; track el) {
|
||
<mat-option [value]="el.id">
|
||
<span class="icon-option">
|
||
<mat-icon class="icon-option-icon">{{ getEducationLevelIcon(el.name) }}</mat-icon>
|
||
{{ el.name }}
|
||
</span>
|
||
</mat-option>
|
||
}
|
||
</mat-select>
|
||
</mat-form-field>
|
||
|
||
<!-- Degree / Qualification (depends on Education Level) -->
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Degree / Qualification</mat-label>
|
||
<mat-select
|
||
formControlName="degreeQualificationId"
|
||
[placeholder]="getSelectedEducationLevel() ? 'Select Degree' : 'Select Education Level first'"
|
||
>
|
||
<mat-select-trigger>
|
||
@if (getSelectedDegreeQualification()) {
|
||
<span class="icon-option">
|
||
<mat-icon class="icon-option-icon">{{
|
||
getDegreeQualificationIcon(getSelectedDegreeQualification().name)
|
||
}}</mat-icon>
|
||
{{ getSelectedDegreeQualification().name }}
|
||
</span>
|
||
}
|
||
</mat-select-trigger>
|
||
@for (dq of filteredDegreeQualificationOptions; track dq) {
|
||
<mat-option [value]="dq.id">
|
||
<span class="icon-option">
|
||
<mat-icon class="icon-option-icon">{{ getDegreeQualificationIcon(dq.name) }}</mat-icon>
|
||
{{ dq.name }}
|
||
</span>
|
||
</mat-option>
|
||
}
|
||
@if (!getSelectedEducationLevel()) {
|
||
<mat-option disabled>
|
||
<em>Please select Education Level first</em>
|
||
</mat-option>
|
||
} @else if (filteredDegreeQualificationOptions.length === 0) {
|
||
<mat-option disabled>
|
||
<em>No options available</em>
|
||
</mat-option>
|
||
}
|
||
</mat-select>
|
||
</mat-form-field>
|
||
|
||
<!-- Religion -->
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Religion</mat-label>
|
||
<mat-select formControlName="religionId">
|
||
<mat-select-trigger>
|
||
@if (getSelectedReligion()) {
|
||
<span class="icon-option">
|
||
<mat-icon class="icon-option-icon">{{ getReligionIcon(getSelectedReligion().name) }}</mat-icon>
|
||
{{ getSelectedReligion().name }}
|
||
</span>
|
||
}
|
||
</mat-select-trigger>
|
||
@for (r of religionOptions; track r) {
|
||
<mat-option [value]="r.id">
|
||
<span class="icon-option">
|
||
<mat-icon class="icon-option-icon">{{ getReligionIcon(r.name) }}</mat-icon>
|
||
{{ r.name }}
|
||
</span>
|
||
</mat-option>
|
||
}
|
||
</mat-select>
|
||
</mat-form-field>
|
||
|
||
<!-- Social Category -->
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Social Category</mat-label>
|
||
<mat-select formControlName="socialCategoryId">
|
||
<mat-select-trigger>
|
||
@if (getSelectedSocialCategory()) {
|
||
<span class="icon-option">
|
||
<mat-icon class="icon-option-icon">{{
|
||
getSocialCategoryIcon(getSelectedSocialCategory().name)
|
||
}}</mat-icon>
|
||
{{ getSelectedSocialCategory().name }}
|
||
</span>
|
||
}
|
||
</mat-select-trigger>
|
||
@for (sc of socialCategoryOptions; track sc) {
|
||
<mat-option [value]="sc.id">
|
||
<span class="icon-option">
|
||
<mat-icon class="icon-option-icon">{{ getSocialCategoryIcon(sc.name) }}</mat-icon>
|
||
{{ sc.name }}
|
||
</span>
|
||
</mat-option>
|
||
}
|
||
</mat-select>
|
||
</mat-form-field>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!--
|
||
Section: Turnover & Risk (Non-Individual only)
|
||
|
||
An entity has no salary to declare, so the individual Financial & Classification section below
|
||
never applied to it — which left a company with no turnover on file at all, and its risk stuck
|
||
on the flat entity default. Turnover is the size signal the branch actually has at onboarding,
|
||
so it is captured here and the risk band it produces is shown right beside it: the officer sees
|
||
the grade move as they type, instead of first meeting it on the KYC tab.
|
||
-->
|
||
@if (createClientForm.get('legalFormId')?.value === LegalFormId.ENTITY) {
|
||
<div class="section-block">
|
||
<h4 class="section-title">
|
||
<fa-icon icon="chart-line" class="section-icon"></fa-icon>
|
||
Turnover & Risk Profile
|
||
</h4>
|
||
<div class="form-grid">
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Monthly Turnover</mat-label>
|
||
<span matPrefix class="rupee-prefix">₹</span>
|
||
<input matInput type="number" formControlName="monthlyTurnover" min="0" />
|
||
<mat-hint>Yearly turnover fills in automatically (× 12)</mat-hint>
|
||
@if (
|
||
createClientForm.get('monthlyTurnover')?.hasError('pattern') &&
|
||
createClientForm.get('monthlyTurnover')?.touched
|
||
) {
|
||
<mat-error>Enter a valid amount</mat-error>
|
||
} @else if (
|
||
createClientForm.get('monthlyTurnover')?.hasError('min') && createClientForm.get('monthlyTurnover')?.touched
|
||
) {
|
||
<mat-error>Amount cannot be negative</mat-error>
|
||
}
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Annual Turnover</mat-label>
|
||
<span matPrefix class="rupee-prefix">₹</span>
|
||
<input matInput type="number" formControlName="annualTurnover" min="0" />
|
||
<mat-hint>Drives the risk grade below</mat-hint>
|
||
@if (
|
||
createClientForm.get('annualTurnover')?.hasError('pattern') &&
|
||
createClientForm.get('annualTurnover')?.touched
|
||
) {
|
||
<mat-error>Enter a valid amount</mat-error>
|
||
} @else if (
|
||
createClientForm.get('annualTurnover')?.hasError('min') && createClientForm.get('annualTurnover')?.touched
|
||
) {
|
||
<mat-error>Amount cannot be negative</mat-error>
|
||
}
|
||
</mat-form-field>
|
||
|
||
<div
|
||
class="full-width entity-risk-card"
|
||
[class.entity-risk-card--low]="entityTurnoverRisk.category === 1"
|
||
[class.entity-risk-card--medium]="entityTurnoverRisk.category === 2"
|
||
[class.entity-risk-card--high]="entityTurnoverRisk.category === 3"
|
||
>
|
||
<div class="entity-risk-card__head">
|
||
<fa-icon icon="shield-alt" class="entity-risk-card__icon"></fa-icon>
|
||
<span class="entity-risk-card__title">Auto-calculated Risk Category</span>
|
||
<span class="entity-risk-card__badge">{{ riskCategoryLabel(entityTurnoverRisk.category) }}</span>
|
||
</div>
|
||
<p class="entity-risk-card__reason">{{ entityTurnoverRisk.reason }}</p>
|
||
<p class="entity-risk-card__scale">
|
||
Up to ₹1 crore — Low · ₹1 crore to ₹10 crore — Medium · Above ₹10 crore — High
|
||
</p>
|
||
<p class="entity-risk-card__note">
|
||
Carried into the KYC tab, where it can still be overridden with a recorded reason.
|
||
</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- Section: Financial & Classification (Individual only) -->
|
||
@if (createClientForm.get('legalFormId')?.value === LegalFormId.PERSON) {
|
||
<div class="section-block">
|
||
<h4 class="section-title">
|
||
<fa-icon icon="chart-line" class="section-icon"></fa-icon>
|
||
Financial & Classification
|
||
</h4>
|
||
<div class="form-grid">
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>{{ 'labels.inputs.Occupation Category' | translate }}</mat-label>
|
||
<mat-select formControlName="occupationCategoryId">
|
||
<mat-select-trigger>
|
||
@if (getSelectedOccupationCategory()) {
|
||
<span class="occ-option">
|
||
<mat-icon class="occ-option-icon">{{
|
||
getOccupationCategoryIcon(getSelectedOccupationCategory().name)
|
||
}}</mat-icon>
|
||
{{ getSelectedOccupationCategory().name }}
|
||
</span>
|
||
}
|
||
</mat-select-trigger>
|
||
@for (cat of occupationCategoryOptions; track cat) {
|
||
<mat-option [value]="cat.id">
|
||
<span class="occ-option">
|
||
<mat-icon class="occ-option-icon">{{ getOccupationCategoryIcon(cat.name) }}</mat-icon>
|
||
{{ cat.name }}
|
||
</span>
|
||
</mat-option>
|
||
}
|
||
</mat-select>
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>{{ 'labels.inputs.Occupation' | translate }}</mat-label>
|
||
<mat-select formControlName="occupationId">
|
||
<mat-select-trigger>
|
||
@if (getSelectedOccupation()) {
|
||
<span class="occ-option">
|
||
<mat-icon class="occ-option-icon">{{ getOccupationIcon(getSelectedOccupation().name) }}</mat-icon>
|
||
{{ getSelectedOccupation().name }}
|
||
</span>
|
||
}
|
||
</mat-select-trigger>
|
||
@for (occ of occupationOptions; track occ) {
|
||
<mat-option [value]="occ.id">
|
||
<span class="occ-option">
|
||
<mat-icon class="occ-option-icon">{{ getOccupationIcon(occ.name) }}</mat-icon>
|
||
{{ occ.name }}
|
||
</span>
|
||
</mat-option>
|
||
}
|
||
</mat-select>
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Monthly Income</mat-label>
|
||
<span matPrefix class="rupee-prefix">₹</span>
|
||
<input matInput type="number" formControlName="monthlyIncome" min="0" />
|
||
@if (
|
||
createClientForm.get('monthlyIncome')?.hasError('pattern') && createClientForm.get('monthlyIncome')?.touched
|
||
) {
|
||
<mat-error>Enter a valid amount</mat-error>
|
||
} @else if (
|
||
createClientForm.get('monthlyIncome')?.hasError('min') && createClientForm.get('monthlyIncome')?.touched
|
||
) {
|
||
<mat-error>Amount cannot be negative</mat-error>
|
||
}
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Annual Income</mat-label>
|
||
<span matPrefix class="rupee-prefix">₹</span>
|
||
<input matInput type="number" formControlName="annualIncome" min="0" />
|
||
@if (
|
||
createClientForm.get('annualIncome')?.hasError('pattern') && createClientForm.get('annualIncome')?.touched
|
||
) {
|
||
<mat-error>Enter a valid amount</mat-error>
|
||
} @else if (
|
||
createClientForm.get('annualIncome')?.hasError('min') && createClientForm.get('annualIncome')?.touched
|
||
) {
|
||
<mat-error>Amount cannot be negative</mat-error>
|
||
}
|
||
</mat-form-field>
|
||
|
||
<div class="full-width subsection-title">
|
||
<fa-icon icon="coins" class="subsection-icon"></fa-icon>
|
||
Other Income
|
||
</div>
|
||
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Agriculture Income</mat-label>
|
||
<span matPrefix class="rupee-prefix">₹</span>
|
||
<input matInput type="number" formControlName="agricultureIncome" min="0" />
|
||
@if (
|
||
createClientForm.get('agricultureIncome')?.hasError('pattern') &&
|
||
createClientForm.get('agricultureIncome')?.touched
|
||
) {
|
||
<mat-error>Enter a valid amount</mat-error>
|
||
} @else if (
|
||
createClientForm.get('agricultureIncome')?.hasError('min') &&
|
||
createClientForm.get('agricultureIncome')?.touched
|
||
) {
|
||
<mat-error>Amount cannot be negative</mat-error>
|
||
}
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Non-Agriculture Income</mat-label>
|
||
<span matPrefix class="rupee-prefix">₹</span>
|
||
<input matInput type="number" formControlName="nonAgricultureIncome" min="0" />
|
||
@if (
|
||
createClientForm.get('nonAgricultureIncome')?.hasError('pattern') &&
|
||
createClientForm.get('nonAgricultureIncome')?.touched
|
||
) {
|
||
<mat-error>Enter a valid amount</mat-error>
|
||
} @else if (
|
||
createClientForm.get('nonAgricultureIncome')?.hasError('min') &&
|
||
createClientForm.get('nonAgricultureIncome')?.touched
|
||
) {
|
||
<mat-error>Amount cannot be negative</mat-error>
|
||
}
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Monthly Turnover</mat-label>
|
||
<span matPrefix class="rupee-prefix">₹</span>
|
||
<input matInput type="number" formControlName="monthlyTurnover" min="0" />
|
||
@if (
|
||
createClientForm.get('monthlyTurnover')?.hasError('pattern') &&
|
||
createClientForm.get('monthlyTurnover')?.touched
|
||
) {
|
||
<mat-error>Enter a valid amount</mat-error>
|
||
} @else if (
|
||
createClientForm.get('monthlyTurnover')?.hasError('min') && createClientForm.get('monthlyTurnover')?.touched
|
||
) {
|
||
<mat-error>Amount cannot be negative</mat-error>
|
||
}
|
||
</mat-form-field>
|
||
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Yearly Turnover</mat-label>
|
||
<span matPrefix class="rupee-prefix">₹</span>
|
||
<input matInput type="number" formControlName="annualTurnover" min="0" />
|
||
@if (
|
||
createClientForm.get('annualTurnover')?.hasError('pattern') &&
|
||
createClientForm.get('annualTurnover')?.touched
|
||
) {
|
||
<mat-error>Enter a valid amount</mat-error>
|
||
} @else if (
|
||
createClientForm.get('annualTurnover')?.hasError('min') && createClientForm.get('annualTurnover')?.touched
|
||
) {
|
||
<mat-error>Amount cannot be negative</mat-error>
|
||
}
|
||
</mat-form-field>
|
||
|
||
<div class="full-width subsection-title">
|
||
<fa-icon icon="user-shield" class="subsection-icon"></fa-icon>
|
||
Senior Citizen / Person with Disability
|
||
</div>
|
||
|
||
@if (isSeniorCitizen) {
|
||
<div class="full-width senior-citizen-auto-note">
|
||
<fa-icon icon="check-circle"></fa-icon>
|
||
Senior Citizen (60+ years) — detected automatically from Date of Birth
|
||
</div>
|
||
|
||
<div class="full-width doc-upload-field">
|
||
<label class="doc-upload-label">
|
||
<input type="file" accept=".jpg,.jpeg,.png,.pdf" hidden (change)="onSeniorCitizenProofSelected($event)" />
|
||
<fa-icon icon="file-upload"></fa-icon>
|
||
@if (seniorCitizenProofFile) {
|
||
<span>{{ seniorCitizenProofFile.name }}</span>
|
||
} @else if (existingSeniorCitizenProofDoc) {
|
||
<span>{{ existingSeniorCitizenProofDoc.name }} (uploaded)</span>
|
||
} @else {
|
||
<span>Upload Senior Citizen Proof (Age / ID Proof) — optional</span>
|
||
}
|
||
</label>
|
||
@if (seniorCitizenProofFile) {
|
||
<button type="button" mat-icon-button (click)="removeSeniorCitizenProof()" matTooltip="Remove">
|
||
<mat-icon>close</mat-icon>
|
||
</button>
|
||
} @else if (existingSeniorCitizenProofDoc) {
|
||
<button type="button" mat-icon-button (click)="viewExistingSeniorCitizenProof()" matTooltip="View">
|
||
<mat-icon>visibility</mat-icon>
|
||
</button>
|
||
<button type="button" mat-icon-button (click)="removeExistingSeniorCitizenProof()" matTooltip="Remove">
|
||
<mat-icon>close</mat-icon>
|
||
</button>
|
||
}
|
||
</div>
|
||
}
|
||
|
||
<div class="checkbox-field full-width">
|
||
<mat-checkbox formControlName="isPwd">Person with Disability (PWD)</mat-checkbox>
|
||
</div>
|
||
|
||
@if (createClientForm.get('isPwd')?.value) {
|
||
<mat-form-field appearance="outline" class="form-field">
|
||
<mat-label>Disability Type</mat-label>
|
||
<mat-select formControlName="pwdCategoryId">
|
||
@for (category of pwdCategoryOptions; track category.id) {
|
||
<mat-option [value]="category.id">{{ category.name }}</mat-option>
|
||
}
|
||
</mat-select>
|
||
@if (seniorPwdDocsTouched && !createClientForm.get('pwdCategoryId')?.value) {
|
||
<mat-error>Disability type is required</mat-error>
|
||
}
|
||
</mat-form-field>
|
||
|
||
<div class="full-width doc-upload-field">
|
||
<label
|
||
class="doc-upload-label"
|
||
[class.doc-upload-label--missing]="
|
||
seniorPwdDocsTouched && !pwdCertificateFile && !existingPwdCertificateDoc
|
||
"
|
||
>
|
||
<input type="file" accept=".jpg,.jpeg,.png,.pdf" hidden (change)="onPwdCertificateSelected($event)" />
|
||
<fa-icon icon="file-upload"></fa-icon>
|
||
@if (pwdCertificateFile) {
|
||
<span>{{ pwdCertificateFile.name }}</span>
|
||
} @else if (existingPwdCertificateDoc) {
|
||
<span>{{ existingPwdCertificateDoc.name }} (uploaded)</span>
|
||
} @else {
|
||
<span>Upload Disability Certificate <span class="doc-upload-req">*</span></span>
|
||
}
|
||
</label>
|
||
@if (pwdCertificateFile) {
|
||
<button type="button" mat-icon-button (click)="removePwdCertificate()" matTooltip="Remove">
|
||
<mat-icon>close</mat-icon>
|
||
</button>
|
||
} @else if (existingPwdCertificateDoc) {
|
||
<button type="button" mat-icon-button (click)="viewExistingPwdCertificate()" matTooltip="View">
|
||
<mat-icon>visibility</mat-icon>
|
||
</button>
|
||
<button type="button" mat-icon-button (click)="removeExistingPwdCertificate()" matTooltip="Remove">
|
||
<mat-icon>close</mat-icon>
|
||
</button>
|
||
}
|
||
@if (seniorPwdDocsTouched && !pwdCertificateFile && !existingPwdCertificateDoc) {
|
||
<span class="doc-upload-error">Required when PWD is checked</span>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
}
|
||
|
||
<!-- submittedOnDate is set automatically via form default (business date) -->
|
||
</form>
|