feat: non-individual entity UI improvements — hide individual-only sections, fix legal form display, send extended fields to API
- Hide Family Members tab/section for non-individual entities in client view, create flow, and preview - Hide Financial & Classification section for non-individual entities in personal data and preview - Fix legal form column in clients list — fetch legalForm from client API for correct display - Add person/business icon before client name in list rows - Remove unsupportedNpd filter — send authorizedSignatory, additionalInfo, dateOfRegistration to API - Add registration number format hint (mat-hint) based on constitution type - Add spacing between verification row and constitution type dropdowns - Reduce table cell padding and prevent action buttons from being clipped - Add missing MPL-2.0 license headers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
ab01111424
commit
b890d24bb7
@@ -39,7 +39,7 @@
|
||||
</span>
|
||||
</mat-panel-title>
|
||||
<mat-panel-description class="addr-panel__desc">
|
||||
{{ address.city || address.townVillage || '' }}{{ address.city || address.townVillage ? ', ' : '' }}{{ getSelectedValue('stateProvinceIdOptions', address.stateProvinceId)?.name || '' }}
|
||||
{{ address.city || '' }}{{ address.city && (address.countyDistrict || address.addressLine3) ? ', ' : '' }}{{ address.countyDistrict || address.addressLine3 || '' }}{{ address.postalCode ? ' - ' + address.postalCode : '' }}
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
@@ -56,60 +56,58 @@
|
||||
|
||||
<!-- Address Details Grid -->
|
||||
<div class="addr-grid">
|
||||
@if (isFieldEnabled('street') && address.street) {
|
||||
@if (address.addressLine1) {
|
||||
<div class="addr-field">
|
||||
<div class="addr-field__label">{{ 'labels.inputs.Street' | translate }}</div>
|
||||
<div class="addr-field__value">{{ address.street }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (isFieldEnabled('addressLine1') && address.addressLine1) {
|
||||
<div class="addr-field">
|
||||
<div class="addr-field__label">{{ 'labels.inputs.Address Line' | translate }} 1</div>
|
||||
<div class="addr-field__label">Address Line 1</div>
|
||||
<div class="addr-field__value">{{ address.addressLine1 }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (isFieldEnabled('addressLine2') && address.addressLine2) {
|
||||
@if (address.addressLine2) {
|
||||
<div class="addr-field">
|
||||
<div class="addr-field__label">{{ 'labels.inputs.Address Line' | translate }} 2</div>
|
||||
<div class="addr-field__label">Address Line 2</div>
|
||||
<div class="addr-field__value">{{ address.addressLine2 }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (isFieldEnabled('addressLine3') && address.addressLine3) {
|
||||
@if (address.street) {
|
||||
<div class="addr-field">
|
||||
<div class="addr-field__label">{{ 'labels.inputs.Address Line' | translate }} 3</div>
|
||||
<div class="addr-field__value">{{ address.addressLine3 }}</div>
|
||||
<div class="addr-field__label">Landmark</div>
|
||||
<div class="addr-field__value">{{ address.street }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (isFieldEnabled('townVillage') && address.townVillage) {
|
||||
@if (address.postalCode) {
|
||||
<div class="addr-field">
|
||||
<div class="addr-field__label">{{ 'labels.inputs.Town / Village' | translate }}</div>
|
||||
<div class="addr-field__value">{{ address.townVillage }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (isFieldEnabled('city') && address.city) {
|
||||
<div class="addr-field">
|
||||
<div class="addr-field__label">{{ 'labels.inputs.City' | translate }}</div>
|
||||
<div class="addr-field__value">{{ address.city }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (isFieldEnabled('stateProvinceId')) {
|
||||
<div class="addr-field">
|
||||
<div class="addr-field__label">{{ 'labels.inputs.State / Province' | translate }}</div>
|
||||
<div class="addr-field__value">{{ getSelectedValue('stateProvinceIdOptions', address.stateProvinceId)?.name || '-' }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (isFieldEnabled('countryId')) {
|
||||
<div class="addr-field">
|
||||
<div class="addr-field__label">{{ 'labels.inputs.Country' | translate }}</div>
|
||||
<div class="addr-field__value">{{ getSelectedValue('countryIdOptions', address.countryId)?.name || '-' }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (isFieldEnabled('postalCode') && address.postalCode) {
|
||||
<div class="addr-field">
|
||||
<div class="addr-field__label">{{ 'labels.inputs.Postal Code' | translate }}</div>
|
||||
<div class="addr-field__label">PIN Code</div>
|
||||
<div class="addr-field__value addr-field__value--mono">{{ address.postalCode }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (address.addressLine3) {
|
||||
<div class="addr-field">
|
||||
<div class="addr-field__label">Locality / State</div>
|
||||
<div class="addr-field__value">{{ address.addressLine3 }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (address.city) {
|
||||
<div class="addr-field">
|
||||
<div class="addr-field__label">City</div>
|
||||
<div class="addr-field__value">{{ address.city }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (address.countyDistrict) {
|
||||
<div class="addr-field">
|
||||
<div class="addr-field__label">District</div>
|
||||
<div class="addr-field__value">{{ address.countyDistrict }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (address.stateProvinceId) {
|
||||
<div class="addr-field">
|
||||
<div class="addr-field__label">State</div>
|
||||
<div class="addr-field__value">{{ getSelectedValue('stateProvinceIdOptions', address.stateProvinceId)?.name || '-' }}</div>
|
||||
</div>
|
||||
}
|
||||
<div class="addr-field">
|
||||
<div class="addr-field__label">Country</div>
|
||||
<div class="addr-field__value">{{ getSelectedValue('countryIdOptions', address.countryId)?.name || 'India' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
}
|
||||
|
||||
@@ -296,7 +296,7 @@ export class AddressTabComponent {
|
||||
formfields.push(
|
||||
this.isFieldEnabled('countyDistrict')
|
||||
? new InputBase({
|
||||
controlName: 'countryDistrict',
|
||||
controlName: 'countyDistrict',
|
||||
label: this.translateService.instant('labels.inputs.State / Province'),
|
||||
value: address ? address.countyDistrict : '',
|
||||
type: 'text',
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
|
||||
<div class="container">
|
||||
<div class="layout-row">
|
||||
<mat-form-field class="flex-fill">
|
||||
<mat-form-field appearance="outline" class="flex-fill">
|
||||
<mat-label>{{ 'labels.inputs.Filter' | translate }}</mat-label>
|
||||
<i matPrefix class="fas fa-search"></i>
|
||||
<input matInput (keyup)="applyFilter($event.target.value)" />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
@@ -144,6 +144,54 @@
|
||||
<div class="cv-cell__value">{{ clientViewData.occupation.name }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.mobileNo && clientViewData.dipVerified != null) {
|
||||
<div class="cv-cell">
|
||||
<div class="cv-cell__label">DIP Status</div>
|
||||
<div class="cv-cell__value">
|
||||
<span class="cv-badge"
|
||||
[class.cv-badge--green]="clientViewData.dipVerified === true || clientViewData.dipVerified === 'true' || clientViewData.dipVerified === 1"
|
||||
[class.cv-badge--yellow]="clientViewData.dipVerified !== true && clientViewData.dipVerified !== 'true' && clientViewData.dipVerified !== 1">
|
||||
{{ (clientViewData.dipVerified === true || clientViewData.dipVerified === 'true' || clientViewData.dipVerified === 1) ? 'Verified' : 'Pending' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.nameVerified != null) {
|
||||
<div class="cv-cell">
|
||||
<div class="cv-cell__label">Sanctions Screening</div>
|
||||
<div class="cv-cell__value">
|
||||
<span class="cv-badge"
|
||||
[class.cv-badge--green]="clientViewData.nameVerified === true || clientViewData.nameVerified === 'true' || clientViewData.nameVerified === 1"
|
||||
[class.cv-badge--red]="clientViewData.nameVerified === false || clientViewData.nameVerified === 'false' || clientViewData.nameVerified === 0">
|
||||
{{ (clientViewData.nameVerified === true || clientViewData.nameVerified === 'true' || clientViewData.nameVerified === 1) ? 'Clear' : 'Pending' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.nationality) {
|
||||
<div class="cv-cell">
|
||||
<div class="cv-cell__label">Nationality</div>
|
||||
<div class="cv-cell__value">{{ clientViewData.nationality.name || clientViewData.nationality }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.maritalStatus) {
|
||||
<div class="cv-cell">
|
||||
<div class="cv-cell__label">Marital Status</div>
|
||||
<div class="cv-cell__value">{{ clientViewData.maritalStatus.name }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.monthlyIncome != null && clientViewData.monthlyIncome !== '') {
|
||||
<div class="cv-cell">
|
||||
<div class="cv-cell__label">Monthly Income</div>
|
||||
<div class="cv-cell__value">₹ {{ clientViewData.monthlyIncome | number }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.annualTurnover != null && clientViewData.annualTurnover !== '') {
|
||||
<div class="cv-cell">
|
||||
<div class="cv-cell__label">Annual Turnover</div>
|
||||
<div class="cv-cell__value">₹ {{ clientViewData.annualTurnover | number }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.groups?.length > 0) {
|
||||
<div class="cv-cell">
|
||||
<div class="cv-cell__label">{{ 'labels.inputs.Member Of' | translate }}</div>
|
||||
@@ -355,90 +403,48 @@
|
||||
</span>
|
||||
}
|
||||
</mat-menu>
|
||||
</mat-menu>
|
||||
|
||||
<div>
|
||||
<nav mat-tab-nav-bar class="navigation-tabs" [tabPanel]="tabPanel">
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./general']"
|
||||
routerLinkActive
|
||||
#general="routerLinkActive"
|
||||
[active]="general.isActive"
|
||||
>
|
||||
{{ 'labels.inputs.General' | translate }}
|
||||
<nav mat-tab-nav-bar class="cv-tabs" [tabPanel]="tabPanel">
|
||||
<a mat-tab-link [routerLink]="['./general']" routerLinkActive #general="routerLinkActive" [active]="general.isActive">
|
||||
<i class="fa fa-tachometer cv-tab-icon"></i> {{ 'labels.inputs.General' | translate }}
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./personal-data']"
|
||||
routerLinkActive
|
||||
#personalData="routerLinkActive"
|
||||
[active]="personalData.isActive"
|
||||
>
|
||||
{{ 'labels.inputs.Personal Data' | translate }}
|
||||
<a mat-tab-link [routerLink]="['./personal-data']" routerLinkActive #personalData="routerLinkActive" [active]="personalData.isActive">
|
||||
<i class="fa fa-user cv-tab-icon"></i> {{ 'labels.inputs.Personal Data' | translate }}
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./address']"
|
||||
routerLinkActive
|
||||
#address="routerLinkActive"
|
||||
[active]="address.isActive"
|
||||
>
|
||||
{{ 'labels.inputs.Address' | translate }}
|
||||
<a mat-tab-link [routerLink]="['./address']" routerLinkActive #address="routerLinkActive" [active]="address.isActive">
|
||||
<i class="fa fa-map-marker cv-tab-icon"></i> {{ 'labels.inputs.Address' | translate }}
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./family-members']"
|
||||
routerLinkActive
|
||||
#familyMembers="routerLinkActive"
|
||||
[active]="familyMembers.isActive"
|
||||
>
|
||||
{{ 'labels.inputs.Family Members' | translate }}
|
||||
@if (clientViewData?.legalForm?.id !== 2) {
|
||||
<a mat-tab-link [routerLink]="['./family-members']" routerLinkActive #familyMembers="routerLinkActive" [active]="familyMembers.isActive">
|
||||
<i class="fa fa-users cv-tab-icon"></i> Family Members
|
||||
</a>
|
||||
}
|
||||
<a mat-tab-link [routerLink]="['./nominees']" routerLinkActive #nomineesTab="routerLinkActive" [active]="nomineesTab.isActive">
|
||||
<i class="fa fa-shield cv-tab-icon"></i> Nominee
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./identities']"
|
||||
routerLinkActive
|
||||
#identities="routerLinkActive"
|
||||
[active]="identities.isActive"
|
||||
*mifosxHasPermission="'READ_CLIENTIDENTIFIER'"
|
||||
>
|
||||
{{ 'labels.inputs.Identities' | translate }}
|
||||
<a mat-tab-link [routerLink]="['./identities']" routerLinkActive #identities="routerLinkActive" [active]="identities.isActive" *mifosxHasPermission="'READ_CLIENTIDENTIFIER'">
|
||||
<i class="fa fa-id-card cv-tab-icon"></i> {{ 'labels.inputs.Identities' | translate }}
|
||||
</a>
|
||||
<a mat-tab-link [routerLink]="['./kyc']" routerLinkActive #kyc="routerLinkActive" [active]="kyc.isActive">
|
||||
{{ 'labels.inputs.KYC' | translate }}
|
||||
<i class="fa fa-check-circle cv-tab-icon"></i> {{ 'labels.inputs.KYC' | translate }}
|
||||
</a>
|
||||
@if (clientViewData.legalForm?.id !== 1) {
|
||||
<a mat-tab-link [routerLink]="['./entity-classification']" routerLinkActive #ecTab="routerLinkActive" [active]="ecTab.isActive">
|
||||
<i class="fa fa-building cv-tab-icon"></i> Scheme & Sector
|
||||
</a>
|
||||
}
|
||||
<a mat-tab-link [routerLink]="['./ckyc']" routerLinkActive #ckyc="routerLinkActive" [active]="ckyc.isActive">
|
||||
{{ 'labels.inputs.CKYC' | translate }}
|
||||
<i class="fa fa-shield cv-tab-icon"></i> {{ 'labels.inputs.CKYC' | translate }}
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./form60']"
|
||||
routerLinkActive
|
||||
#form60="routerLinkActive"
|
||||
[active]="form60.isActive"
|
||||
>
|
||||
{{ 'labels.inputs.Form 60' | translate }}
|
||||
<a mat-tab-link [routerLink]="['./form60']" routerLinkActive #form60="routerLinkActive" [active]="form60.isActive">
|
||||
<i class="fa fa-file-text cv-tab-icon"></i> {{ 'labels.inputs.Form 60' | translate }}
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./documents']"
|
||||
routerLinkActive
|
||||
#documents="routerLinkActive"
|
||||
[active]="documents.isActive"
|
||||
*mifosxHasPermission="'READ_DOCUMENT'"
|
||||
>
|
||||
{{ 'labels.inputs.Documents' | translate }}
|
||||
<a mat-tab-link [routerLink]="['./documents']" routerLinkActive #documents="routerLinkActive" [active]="documents.isActive" *mifosxHasPermission="'READ_DOCUMENT'">
|
||||
<i class="fa fa-folder-open cv-tab-icon"></i> {{ 'labels.inputs.Documents' | translate }}
|
||||
</a>
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./notes']"
|
||||
routerLinkActive
|
||||
#notes="routerLinkActive"
|
||||
[active]="notes.isActive"
|
||||
*mifosxHasPermission="'READ_CLIENTNOTE'"
|
||||
>
|
||||
{{ 'labels.inputs.Notes' | translate }}
|
||||
<a mat-tab-link [routerLink]="['./notes']" routerLinkActive #notes="routerLinkActive" [active]="notes.isActive" *mifosxHasPermission="'READ_CLIENTNOTE'">
|
||||
<i class="fa fa-sticky-note cv-tab-icon"></i> {{ 'labels.inputs.Notes' | translate }}
|
||||
</a>
|
||||
@for (clientDatatable of clientDatatables; track clientDatatable) {
|
||||
<a
|
||||
|
||||
@@ -6,24 +6,10 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
$dark-1: #0f1923;
|
||||
$dark-2: #162231;
|
||||
$dark-3: #1c2d3f;
|
||||
$accent: #38bdf8;
|
||||
$accent-dim: rgba(56, 189, 248, 0.15);
|
||||
$green: #34d399;
|
||||
$green-dim: rgba(52, 211, 153, 0.15);
|
||||
$red: #f87171;
|
||||
$red-dim: rgba(248, 113, 113, 0.15);
|
||||
$amber: #fbbf24;
|
||||
$white: #fff;
|
||||
$white-70: rgba(255, 255, 255, 0.7);
|
||||
$white-50: rgba(255, 255, 255, 0.5);
|
||||
$white-30: rgba(255, 255, 255, 0.3);
|
||||
$white-15: rgba(255, 255, 255, 0.15);
|
||||
$white-08: rgba(255, 255, 255, 0.08);
|
||||
// ── Design tokens (from home/toolbar) ──
|
||||
$brand-dark: #0d3d78;
|
||||
$accent: #0ea5e9;
|
||||
|
||||
// Main card
|
||||
:host {
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
@@ -31,25 +17,46 @@ $white-08: rgba(255, 255, 255, 0.08);
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
// ── Card wrapper ──
|
||||
.cv-card {
|
||||
background: linear-gradient(135deg, $dark-1 0%, $dark-2 50%, $dark-3 100%) !important;
|
||||
border-radius: 12px;
|
||||
background: #fff !important;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
// Top section
|
||||
// ── Blue banner (top section) ──
|
||||
.cv-top {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 20px;
|
||||
padding: 20px 24px 16px;
|
||||
padding: 20px 24px 18px;
|
||||
background: linear-gradient(135deg, $brand-dark 0%, #1a5276 100%);
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
// Stripe overlay — matches home/toolbar
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: repeating-linear-gradient(108deg, transparent 0, transparent 80px, rgb(255 255 255 / 2.5%) 80px, rgb(255 255 255 / 2.5%) 160px);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
> * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Avatar column
|
||||
// ── Avatar ──
|
||||
.cv-avatar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@@ -60,66 +67,59 @@ $white-08: rgba(255, 255, 255, 0.08);
|
||||
&__img {
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 12px;
|
||||
border-radius: 10px;
|
||||
object-fit: cover;
|
||||
border: 2px solid $white-15;
|
||||
border: 2px solid rgba(255, 255, 255, 0.25);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
min-width: 28px !important;
|
||||
width: 26px !important;
|
||||
height: 26px !important;
|
||||
min-width: 26px !important;
|
||||
padding: 0 !important;
|
||||
border-radius: 6px !important;
|
||||
background: $white-08 !important;
|
||||
color: $white-50 !important;
|
||||
border: 1px solid $white-15 !important;
|
||||
border-radius: 5px !important;
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
color: rgba(255, 255, 255, 0.6) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15) !important;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
transition: all 0.12s;
|
||||
|
||||
i {
|
||||
font-size: 11px;
|
||||
}
|
||||
i { font-size: 10px; }
|
||||
|
||||
&:hover {
|
||||
background: $white-15 !important;
|
||||
color: $white !important;
|
||||
background: rgba(255, 255, 255, 0.2) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
&--danger:hover {
|
||||
background: $red-dim !important;
|
||||
color: $red !important;
|
||||
border-color: rgba($red, 0.3) !important;
|
||||
background: rgba(239, 68, 68, 0.35) !important;
|
||||
border-color: rgba(239, 68, 68, 0.45) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__sig {
|
||||
font-size: 10px;
|
||||
color: $accent;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
&:hover { color: #fff; text-decoration: underline; }
|
||||
}
|
||||
}
|
||||
|
||||
// Info column
|
||||
.cv-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
// ── Info column ──
|
||||
.cv-info { flex: 1; min-width: 0; }
|
||||
|
||||
// Name row
|
||||
// ── Name row ──
|
||||
.cv-name-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -132,44 +132,49 @@ $white-08: rgba(255, 255, 255, 0.08);
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
&__dot {
|
||||
font-size: 10px;
|
||||
}
|
||||
&__dot { font-size: 10px; }
|
||||
|
||||
&__text {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: $white;
|
||||
color: #fff;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
&__status {
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
padding: 3px 10px;
|
||||
border-radius: 10px;
|
||||
padding: 2px 9px;
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
background: $green-dim;
|
||||
color: $green;
|
||||
border: 1px solid rgba($green, 0.2);
|
||||
letter-spacing: 0.4px;
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
color: #6ee7b7;
|
||||
border: 1px solid rgba(16, 185, 129, 0.25);
|
||||
}
|
||||
}
|
||||
|
||||
.cv-menu-btn {
|
||||
color: $white-50 !important;
|
||||
width: 36px !important;
|
||||
height: 36px !important;
|
||||
border: 1px solid $white-15;
|
||||
border-radius: 8px !important;
|
||||
color: rgba(255, 255, 255, 0.7) !important;
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
min-width: 28px !important;
|
||||
padding: 0 !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border-radius: 5px !important;
|
||||
line-height: 28px !important;
|
||||
|
||||
&:hover {
|
||||
background: $white-08;
|
||||
color: $white !important;
|
||||
&:hover { background: rgba(255, 255, 255, 0.1); color: #fff !important; }
|
||||
|
||||
.actions-menu {
|
||||
font-size: 14px !important;
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
line-height: 14px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// ID chips
|
||||
// ── ID chips ──
|
||||
.cv-ids {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
@@ -181,119 +186,183 @@ $white-08: rgba(255, 255, 255, 0.08);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 5px 12px;
|
||||
background: $white-08;
|
||||
border: 1px solid $white-15;
|
||||
border-radius: 6px;
|
||||
padding: 4px 11px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.13);
|
||||
border-radius: 5px;
|
||||
|
||||
&__label {
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
color: $white-30;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
|
||||
&__val {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: $white;
|
||||
color: #fff;
|
||||
font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
}
|
||||
|
||||
// Details grid
|
||||
// ── Details grid (inside banner, translucent cards) ──
|
||||
.cv-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0;
|
||||
background: $white-08;
|
||||
border-radius: 8px;
|
||||
border: 1px solid $white-15;
|
||||
overflow: hidden;
|
||||
gap: 6px;
|
||||
padding: 0 24px 18px;
|
||||
background: linear-gradient(135deg, $brand-dark 0%, #1a5276 100%);
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.cv-cell {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid $white-08;
|
||||
border-right: 1px solid $white-08;
|
||||
padding: 9px 13px;
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
transition: background 0.12s;
|
||||
|
||||
&:nth-child(4n) {
|
||||
border-right: none;
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
color: $white-30;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 12.5px;
|
||||
font-weight: 500;
|
||||
color: $white;
|
||||
line-height: 1.3;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
line-height: 1.35;
|
||||
}
|
||||
}
|
||||
|
||||
// Actions menu
|
||||
.actions-menu {
|
||||
color: $white !important;
|
||||
.cv-badge {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
letter-spacing: 0.3px;
|
||||
|
||||
&--green { background: rgba(16, 185, 129, 0.2); color: #6ee7b7; }
|
||||
&--yellow { background: rgba(245, 158, 11, 0.2); color: #fcd34d; }
|
||||
&--red { background: rgba(239, 68, 68, 0.2); color: #fca5a5; }
|
||||
}
|
||||
|
||||
// Responsive
|
||||
.actions-menu { color: #fff !important; }
|
||||
|
||||
// ── Tab bar ──
|
||||
.cv-tabs {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
|
||||
::ng-deep .mat-mdc-tab-link {
|
||||
font-size: 12px !important;
|
||||
font-weight: 500 !important;
|
||||
letter-spacing: 0.2px;
|
||||
min-width: 0 !important;
|
||||
padding: 0 14px !important;
|
||||
height: 42px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.cv-tab-icon {
|
||||
margin-right: 5px;
|
||||
font-size: 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
// Fix copy icons on dark background
|
||||
.cv-top, .cv-grid {
|
||||
::ng-deep {
|
||||
mifosx-external-identifier,
|
||||
mifosx-account-number {
|
||||
color: #fff !important;
|
||||
|
||||
.copy-icon,
|
||||
button,
|
||||
mat-icon,
|
||||
fa-icon {
|
||||
color: rgba(255, 255, 255, 0.4) !important;
|
||||
font-size: 11px !important;
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
min-width: 16px !important;
|
||||
padding: 0 !important;
|
||||
line-height: 16px !important;
|
||||
|
||||
&:hover {
|
||||
color: #fff !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Client menu styling ──
|
||||
::ng-deep .mat-mdc-menu-panel {
|
||||
border-radius: 10px !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06) !important;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12) !important;
|
||||
min-width: 200px !important;
|
||||
padding: 6px 0 !important;
|
||||
|
||||
.mat-mdc-menu-item {
|
||||
font-size: 13px !important;
|
||||
font-weight: 500 !important;
|
||||
height: 38px !important;
|
||||
min-height: 38px !important;
|
||||
padding: 0 16px !important;
|
||||
color: #333 !important;
|
||||
border-radius: 0 !important;
|
||||
margin: 0 !important;
|
||||
|
||||
&:hover {
|
||||
background: #f0f7ff !important;
|
||||
color: $brand-dark !important;
|
||||
}
|
||||
|
||||
.mat-icon,
|
||||
mat-icon {
|
||||
font-size: 15px !important;
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
min-width: 18px !important;
|
||||
margin-right: 10px !important;
|
||||
color: $accent !important;
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
|
||||
// Sub-menu trigger arrow
|
||||
.mat-mdc-menu-item-submenu-trigger::after {
|
||||
color: #999;
|
||||
}
|
||||
}
|
||||
|
||||
// ── Responsive ──
|
||||
@media (max-width: 1024px) {
|
||||
.cv-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.cv-cell:nth-child(4n) {
|
||||
border-right: 1px solid $white-08;
|
||||
}
|
||||
|
||||
.cv-cell:nth-child(3n) {
|
||||
border-right: none;
|
||||
}
|
||||
.cv-grid { grid-template-columns: repeat(3, 1fr); }
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.cv-top {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cv-name-row {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cv-ids {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cv-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
.cv-cell:nth-child(3n) {
|
||||
border-right: 1px solid $white-08;
|
||||
}
|
||||
|
||||
.cv-cell:nth-child(2n) {
|
||||
border-right: none;
|
||||
}
|
||||
.cv-top { flex-direction: column; align-items: center; text-align: center; }
|
||||
.cv-name-row { justify-content: center; }
|
||||
.cv-ids { justify-content: center; }
|
||||
.cv-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.cv-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.cv-cell {
|
||||
border-right: none !important;
|
||||
}
|
||||
.cv-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
<div class="ec-view">
|
||||
@if (loading) {
|
||||
<div class="ec-loading">
|
||||
<mat-progress-spinner mode="indeterminate" diameter="40"></mat-progress-spinner>
|
||||
<p>Loading classifications...</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!loading) {
|
||||
<div class="ec-header">
|
||||
<h3><fa-icon icon="building"></fa-icon> Scheme & Sector Classification</h3>
|
||||
<span class="ec-count">{{ enabledCategories.length }} active</span>
|
||||
</div>
|
||||
|
||||
@if (enabledCategories.length === 0) {
|
||||
<div class="ec-empty">
|
||||
<mat-icon>category</mat-icon>
|
||||
<span>No classifications enabled for this entity.</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@for (cat of enabledCategories; track cat.key) {
|
||||
<div class="ec-card">
|
||||
<div class="ec-card__header">
|
||||
<span class="ec-card__tag" [style.background]="cat.tagBg" [style.color]="cat.tagColor">{{ cat.tag }}</span>
|
||||
<span class="ec-card__title">{{ cat.label }}</span>
|
||||
<span class="ec-card__badge">Enabled</span>
|
||||
</div>
|
||||
<div class="ec-card__body">
|
||||
<div class="ec-card__grid">
|
||||
@for (field of getFieldPairs(getCategoryData(cat.key)); track field.label) {
|
||||
<div class="ec-card__field">
|
||||
<span class="ec-card__label">{{ field.label }}</span>
|
||||
<span class="ec-card__value">{{ field.value }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
.ec-view { padding: 16px 0; }
|
||||
|
||||
.ec-loading {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 12px;
|
||||
padding: 48px; color: #64748b;
|
||||
}
|
||||
|
||||
.ec-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
h3 { font-size: 16px; font-weight: 600; color: #1e293b; display: flex; align-items: center; gap: 8px;
|
||||
fa-icon { color: #1565c0; }
|
||||
}
|
||||
}
|
||||
|
||||
.ec-count {
|
||||
font-size: 12px; font-weight: 600; color: #059669;
|
||||
background: #ecfdf5; padding: 3px 10px; border-radius: 12px;
|
||||
}
|
||||
|
||||
.ec-empty {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 8px;
|
||||
padding: 40px; color: #94a3b8; font-size: 14px;
|
||||
background: #f8fafc; border: 2px dashed #e2e8f0; border-radius: 10px;
|
||||
mat-icon { font-size: 36px; width: 36px; height: 36px; }
|
||||
}
|
||||
|
||||
.ec-card {
|
||||
border: 1px solid #e2e8f0; border-radius: 10px; margin-bottom: 12px; overflow: hidden;
|
||||
}
|
||||
|
||||
.ec-card__header {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 12px 16px; background: #f8fafc; border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.ec-card__tag {
|
||||
font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 12px;
|
||||
}
|
||||
|
||||
.ec-card__title { font-size: 14px; font-weight: 600; color: #1e293b; flex: 1; }
|
||||
|
||||
.ec-card__badge {
|
||||
font-size: 10px; font-weight: 700; color: #059669;
|
||||
background: #ecfdf5; padding: 2px 8px; border-radius: 10px; border: 1px solid #a7f3d0;
|
||||
}
|
||||
|
||||
.ec-card__body { padding: 14px 16px; }
|
||||
|
||||
.ec-card__grid {
|
||||
display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px 16px;
|
||||
}
|
||||
|
||||
.ec-card__field { display: flex; flex-direction: column; gap: 2px; }
|
||||
.ec-card__label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: #94a3b8; font-weight: 500; }
|
||||
.ec-card__value { font-size: 13px; font-weight: 500; color: #1e293b; }
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ec-card__grid { grid-template-columns: repeat(2, 1fr); }
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||
import { ClientsService } from '../../clients.service';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-entity-classification-tab',
|
||||
templateUrl: './entity-classification-tab.component.html',
|
||||
styleUrls: ['./entity-classification-tab.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
FaIconComponent,
|
||||
MatIcon,
|
||||
MatProgressSpinner
|
||||
]
|
||||
})
|
||||
export class EntityClassificationTabComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
private clientsService = inject(ClientsService);
|
||||
|
||||
clientId: string = '';
|
||||
loading = true;
|
||||
data: any = null;
|
||||
|
||||
/** Category metadata for display */
|
||||
categories = [
|
||||
{ key: 'msme', label: 'MSME', tag: 'Udyam / MSME', tagColor: '#0A5C6B', tagBg: '#E6F4F6', icon: 'cogs' },
|
||||
{ key: 'startup', label: 'Startup India', tag: 'DPIIT', tagColor: '#1A5FAB', tagBg: '#EAF2FB', icon: 'rocket' },
|
||||
{ key: 'mudra', label: 'Mudra Borrower', tag: 'PMMY', tagColor: '#8A5A00', tagBg: '#FEF3DC', icon: 'hand-holding-usd' },
|
||||
{ key: 'eou', label: 'Export Oriented Unit', tag: 'EOU', tagColor: '#1B7A4A', tagBg: '#E8F5EE', icon: 'ship' },
|
||||
{ key: 'importexport', label: 'Import Export', tag: 'DGFT / IEC', tagColor: '#5B3A8A', tagBg: '#F0EBF8', icon: 'exchange-alt' },
|
||||
{ key: 'psl', label: 'Priority Sector', tag: 'RBI PSL', tagColor: '#7B1F1F', tagBg: '#FCEAEA', icon: 'bullseye' },
|
||||
{ key: 'nps', label: 'Non-Priority Sector', tag: 'General', tagColor: '#444441', tagBg: '#F1EFE8', icon: 'building' }
|
||||
];
|
||||
|
||||
ngOnInit() {
|
||||
this.clientId = this.route.parent?.snapshot.paramMap.get('clientId') || '';
|
||||
if (this.clientId) {
|
||||
this.clientsService.getEntityClassifications(this.clientId).subscribe({
|
||||
next: (res: any) => { this.data = res; this.loading = false; },
|
||||
error: () => { this.loading = false; }
|
||||
});
|
||||
} else {
|
||||
this.loading = false;
|
||||
}
|
||||
}
|
||||
|
||||
get enabledCategories(): any[] {
|
||||
if (!this.data) return [];
|
||||
return this.categories.filter(c => this.data[c.key]?.enabled);
|
||||
}
|
||||
|
||||
getCategoryData(key: string): any {
|
||||
return this.data?.[key] || {};
|
||||
}
|
||||
|
||||
getFieldPairs(catData: any): { label: string, value: any }[] {
|
||||
if (!catData) return [];
|
||||
const skip = ['id', 'clientId', 'enabled', 'locale', 'dateFormat'];
|
||||
return Object.keys(catData)
|
||||
.filter(k => !skip.includes(k) && catData[k] != null && catData[k] !== '' && catData[k] !== false)
|
||||
.map(k => ({
|
||||
label: this.formatLabel(k),
|
||||
value: this.formatValue(catData[k])
|
||||
}));
|
||||
}
|
||||
|
||||
private formatLabel(key: string): string {
|
||||
return key
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, s => s.toUpperCase())
|
||||
.replace(/Id$/, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
private formatValue(val: any): string {
|
||||
if (Array.isArray(val) && val.length === 3) {
|
||||
return `${val[2]}/${val[1]}/${val[0]}`;
|
||||
}
|
||||
if (val === true) return 'Yes';
|
||||
if (typeof val === 'number') return val.toLocaleString('en-IN');
|
||||
return String(val);
|
||||
}
|
||||
}
|
||||
@@ -7,118 +7,83 @@
|
||||
-->
|
||||
|
||||
<div class="nom-container mat-typography">
|
||||
<!-- Header -->
|
||||
|
||||
<!-- ═══ FAMILY MEMBERS SECTION ═══ -->
|
||||
<div class="nom-header">
|
||||
<div class="nom-header__left">
|
||||
<i class="fa fa-users nom-header__icon"></i>
|
||||
<h3 class="nom-header__title">{{ 'labels.heading.Family Members' | translate }}</h3>
|
||||
<span class="nom-header__badge">{{ clientFamilyMembers?.length || 0 }}</span>
|
||||
<mat-icon class="nom-header__mat-icon">people</mat-icon>
|
||||
<h3 class="nom-header__title">Family Members</h3>
|
||||
<span class="nom-header__badge">{{ familyMembers.length }}</span>
|
||||
</div>
|
||||
<button mat-flat-button color="primary" class="nom-header__btn" (click)="addNominee()">
|
||||
<fa-icon icon="plus" class="m-r-10"></fa-icon>{{ 'labels.buttons.Add' | translate }}
|
||||
<button mat-flat-button color="primary" class="nom-header__btn" (click)="addFamilyMember()">
|
||||
<mat-icon>add</mat-icon> Add Member
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Empty State -->
|
||||
@if (!clientFamilyMembers || clientFamilyMembers.length === 0) {
|
||||
@if (familyMembers.length === 0) {
|
||||
<div class="nom-empty">
|
||||
<i class="fa fa-user-o nom-empty__icon"></i>
|
||||
<p class="nom-empty__text">{{ 'labels.text.No nominees added yet' | translate }}</p>
|
||||
<mat-icon class="nom-empty__mat-icon">people_outline</mat-icon>
|
||||
<p class="nom-empty__text">No family members added yet.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Nominee Cards -->
|
||||
<mat-accordion multi>
|
||||
@for (member of clientFamilyMembers; track member.id; let i = $index) {
|
||||
@for (member of familyMembers; track member.id; let i = $index) {
|
||||
<mat-expansion-panel class="nom-panel">
|
||||
<mat-expansion-panel-header class="nom-panel__header">
|
||||
<mat-panel-title class="nom-panel__title">
|
||||
<i class="fa fa-user nom-panel__type-icon"></i>
|
||||
<mat-icon class="nom-panel__mat-icon">person</mat-icon>
|
||||
<span class="nom-panel__name">{{ displayName(member) }}</span>
|
||||
@if (member.relationship) {
|
||||
<span class="nom-panel__badge nom-panel__badge--relation">{{ member.relationship }}</span>
|
||||
}
|
||||
@if (member.nominationType) {
|
||||
<span class="nom-panel__badge nom-panel__badge--type">{{ member.nominationType }}</span>
|
||||
@if (getRelationship(member)) {
|
||||
<span class="nom-panel__badge nom-panel__badge--relation">{{ getRelationship(member) }}</span>
|
||||
}
|
||||
</mat-panel-title>
|
||||
<mat-panel-description class="nom-panel__desc">
|
||||
@if (member.percentageShare != null) {
|
||||
{{ member.percentageShare }}%
|
||||
@if (member.dateOfBirth) {
|
||||
{{ member.dateOfBirth | dateFormat }}
|
||||
}
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
<!-- Actions Bar -->
|
||||
<div class="nom-actions">
|
||||
<div class="nom-actions__buttons">
|
||||
<button mat-stroked-button color="primary" class="nom-actions__edit" (click)="editNominee(member, i)">
|
||||
<fa-icon icon="edit" class="m-r-10"></fa-icon>{{ 'labels.buttons.Edit' | translate }}
|
||||
<button mat-stroked-button color="primary" class="nom-actions__edit" (click)="editFamilyMember(member, i)">
|
||||
<fa-icon icon="edit" class="m-r-10"></fa-icon> Edit
|
||||
</button>
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="warn"
|
||||
class="nom-actions__delete"
|
||||
(click)="deleteFamilyMember(member.clientId, member.id, member.firstName, i)"
|
||||
>
|
||||
<fa-icon icon="trash" class="m-r-10"></fa-icon>{{ 'labels.buttons.Delete' | translate }}
|
||||
<button mat-stroked-button color="warn" class="nom-actions__delete" (click)="deleteMember(member, 'Family Member')">
|
||||
<fa-icon icon="trash" class="m-r-10"></fa-icon> Delete
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Details Grid -->
|
||||
<div class="nom-grid">
|
||||
@if (member.nominationType) {
|
||||
<div class="nom-field">
|
||||
<div class="nom-field__label">{{ 'labels.inputs.Nomination Type' | translate }}</div>
|
||||
<div class="nom-field__value">{{ member.nominationType }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (member.firstName) {
|
||||
<div class="nom-field">
|
||||
<div class="nom-field__label">{{ 'labels.inputs.Nominee Name' | translate }}</div>
|
||||
<div class="nom-field__label">Name</div>
|
||||
<div class="nom-field__value">{{ member.firstName }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (member.relationship) {
|
||||
@if (getRelationship(member)) {
|
||||
<div class="nom-field">
|
||||
<div class="nom-field__label">{{ 'labels.inputs.Relationship' | translate }}</div>
|
||||
<div class="nom-field__value">{{ member.relationship }}</div>
|
||||
<div class="nom-field__label">Relationship</div>
|
||||
<div class="nom-field__value">{{ getRelationship(member) }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (member.dateOfBirth) {
|
||||
<div class="nom-field">
|
||||
<div class="nom-field__label">{{ 'labels.inputs.Date Of Birth' | translate }}</div>
|
||||
<div class="nom-field__label">Date of Birth</div>
|
||||
<div class="nom-field__value">{{ member.dateOfBirth | dateFormat }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (member.percentageShare != null) {
|
||||
<div class="nom-field">
|
||||
<div class="nom-field__label">{{ 'labels.inputs.Percentage Share' | translate }}</div>
|
||||
<div class="nom-field__value nom-field__value--mono">{{ member.percentageShare }}%</div>
|
||||
</div>
|
||||
}
|
||||
@if (member.isDependent != null) {
|
||||
<div class="nom-field">
|
||||
<div class="nom-field__label">{{ 'labels.inputs.Nominee is Minor' | translate }}</div>
|
||||
<div class="nom-field__label">Dependent</div>
|
||||
<div class="nom-field__value">{{ member.isDependent | yesNo }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (member.guardianName) {
|
||||
<div class="nom-field">
|
||||
<div class="nom-field__label">{{ 'labels.inputs.Guardian Name' | translate }}</div>
|
||||
<div class="nom-field__value">{{ member.guardianName }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (member.guardianRelationship) {
|
||||
<div class="nom-field">
|
||||
<div class="nom-field__label">{{ 'labels.inputs.Relationship with Minor' | translate }}</div>
|
||||
<div class="nom-field__value">{{ member.guardianRelationship }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (member.remarks) {
|
||||
<div class="nom-field">
|
||||
<div class="nom-field__label">{{ 'labels.inputs.Remarks' | translate }}</div>
|
||||
<div class="nom-field__label">Remarks</div>
|
||||
<div class="nom-field__value">{{ member.remarks }}</div>
|
||||
</div>
|
||||
}
|
||||
@@ -126,4 +91,5 @@
|
||||
</mat-expansion-panel>
|
||||
}
|
||||
</mat-accordion>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -28,6 +28,10 @@ $orange-light: #fff3e0;
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.section-divider {
|
||||
margin: 28px 0 20px;
|
||||
}
|
||||
|
||||
// Header
|
||||
.nom-header {
|
||||
display: flex;
|
||||
@@ -46,6 +50,11 @@ $orange-light: #fff3e0;
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
&__mat-icon {
|
||||
color: $primary;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
@@ -84,6 +93,14 @@ $orange-light: #fff3e0;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&__mat-icon {
|
||||
font-size: 36px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
color: $gray-500;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&__text {
|
||||
color: $gray-500;
|
||||
font-size: 14px;
|
||||
@@ -127,6 +144,23 @@ $orange-light: #fff3e0;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&__mat-icon {
|
||||
font-size: 18px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: $primary-light;
|
||||
color: $primary;
|
||||
border-radius: 6px;
|
||||
|
||||
&--nominee {
|
||||
background: $green-light;
|
||||
color: $green;
|
||||
}
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
|
||||
@@ -22,6 +22,7 @@ import { SettingsService } from 'app/settings/settings.service';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import {
|
||||
MatAccordion,
|
||||
MatExpansionPanel,
|
||||
@@ -35,7 +36,7 @@ import { YesnoPipe } from '../../../pipes/yesno.pipe';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
/**
|
||||
* Client Family Members Tab
|
||||
* Client Family Members & Nominees Tab
|
||||
*/
|
||||
@Component({
|
||||
selector: 'mifosx-family-members-tab',
|
||||
@@ -44,6 +45,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
FaIconComponent,
|
||||
MatIcon,
|
||||
MatAccordion,
|
||||
MatExpansionPanel,
|
||||
MatExpansionPanelHeader,
|
||||
@@ -62,37 +64,56 @@ export class FamilyMembersTabComponent {
|
||||
private settingsService = inject(SettingsService);
|
||||
private dateUtils = inject(Dates);
|
||||
|
||||
/** Client Family Members */
|
||||
clientFamilyMembers: any;
|
||||
/** All family members from API */
|
||||
clientFamilyMembers: any[] = [];
|
||||
/** Client ID */
|
||||
clientId: string;
|
||||
/** Family member template options (loaded on first dialog open) */
|
||||
/** Family member template options */
|
||||
familyMemberOptions: any;
|
||||
|
||||
constructor() {
|
||||
this.route.data.subscribe((data: { clientFamilyMembers: any }) => {
|
||||
this.clientFamilyMembers = data.clientFamilyMembers;
|
||||
this.clientFamilyMembers = data.clientFamilyMembers || [];
|
||||
});
|
||||
this.clientId = this.route.parent.snapshot.params['clientId'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the nominee dialog in Add mode.
|
||||
*/
|
||||
addNominee() {
|
||||
/** Family members (non-nominee entries) */
|
||||
get familyMembers(): any[] {
|
||||
return this.clientFamilyMembers.filter(m => !m.nominationType || m.nominationType === '');
|
||||
}
|
||||
|
||||
/** Nominees (entries with nominationType set) */
|
||||
get nominees(): any[] {
|
||||
return this.clientFamilyMembers.filter(m => m.nominationType && m.nominationType !== '');
|
||||
}
|
||||
|
||||
/** Get relationship display name */
|
||||
getRelationship(member: any): string {
|
||||
if (member.relationship) return member.relationship;
|
||||
if (typeof member.relationshipId === 'object' && member.relationshipId?.name) return member.relationshipId.name;
|
||||
return '';
|
||||
}
|
||||
|
||||
displayName(member: any): string {
|
||||
return member.firstName || '';
|
||||
}
|
||||
|
||||
// ═══ Family Member Actions ═══
|
||||
|
||||
addFamilyMember() {
|
||||
this.ensureTemplateLoaded().then(() => {
|
||||
const dialogRef = this.dialog.open(NomineeDialogComponent, {
|
||||
width: '700px',
|
||||
data: {
|
||||
title: this.translateService.instant('labels.buttons.Add') + ' ' + this.translateService.instant('labels.heading.Nominee'),
|
||||
familyMemberOptions: this.familyMemberOptions
|
||||
title: 'Add Family Member',
|
||||
familyMemberOptions: this.familyMemberOptions,
|
||||
isFamilyMember: true
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: any) => {
|
||||
if (result) {
|
||||
this.clientsService.addFamilyMember(this.clientId, result).subscribe((res: any) => {
|
||||
// Refresh the list by fetching the member details
|
||||
this.clientsService.getClientFamilyMember(this.clientId, res.resourceId).subscribe((member: any) => {
|
||||
this.clientFamilyMembers.push(member);
|
||||
});
|
||||
@@ -102,25 +123,23 @@ export class FamilyMembersTabComponent {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the nominee dialog in Edit mode, pre-filled with member data.
|
||||
*/
|
||||
editNominee(member: any, index: number) {
|
||||
editFamilyMember(member: any, index: number) {
|
||||
const realIndex = this.clientFamilyMembers.indexOf(member);
|
||||
this.ensureTemplateLoaded().then(() => {
|
||||
const dialogRef = this.dialog.open(NomineeDialogComponent, {
|
||||
width: '700px',
|
||||
data: {
|
||||
title: this.translateService.instant('labels.buttons.Edit') + ' ' + this.translateService.instant('labels.heading.Nominee'),
|
||||
title: 'Edit Family Member',
|
||||
familyMemberOptions: this.familyMemberOptions,
|
||||
member
|
||||
member,
|
||||
isFamilyMember: true
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((result: any) => {
|
||||
if (result) {
|
||||
this.clientsService.editFamilyMember(member.clientId, member.id, result).subscribe(() => {
|
||||
this.clientsService.getClientFamilyMember(this.clientId, member.id).subscribe((updated: any) => {
|
||||
this.clientFamilyMembers[index] = updated;
|
||||
this.clientFamilyMembers[realIndex] = updated;
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -128,29 +147,68 @@ export class FamilyMembersTabComponent {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the family member.
|
||||
*/
|
||||
deleteFamilyMember(clientId: string, id: string, name: string, index: number) {
|
||||
// ═══ Nominee Actions ═══
|
||||
|
||||
addNominee() {
|
||||
this.ensureTemplateLoaded().then(() => {
|
||||
const dialogRef = this.dialog.open(NomineeDialogComponent, {
|
||||
width: '700px',
|
||||
data: {
|
||||
title: 'Add Nominee',
|
||||
familyMemberOptions: this.familyMemberOptions
|
||||
}
|
||||
});
|
||||
dialogRef.afterClosed().subscribe((result: any) => {
|
||||
if (result) {
|
||||
this.clientsService.addFamilyMember(this.clientId, result).subscribe((res: any) => {
|
||||
this.clientsService.getClientFamilyMember(this.clientId, res.resourceId).subscribe((member: any) => {
|
||||
this.clientFamilyMembers.push(member);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
editNominee(member: any, index: number) {
|
||||
const realIndex = this.clientFamilyMembers.indexOf(member);
|
||||
this.ensureTemplateLoaded().then(() => {
|
||||
const dialogRef = this.dialog.open(NomineeDialogComponent, {
|
||||
width: '700px',
|
||||
data: {
|
||||
title: 'Edit Nominee',
|
||||
familyMemberOptions: this.familyMemberOptions,
|
||||
member
|
||||
}
|
||||
});
|
||||
dialogRef.afterClosed().subscribe((result: any) => {
|
||||
if (result) {
|
||||
this.clientsService.editFamilyMember(member.clientId, member.id, result).subscribe(() => {
|
||||
this.clientsService.getClientFamilyMember(this.clientId, member.id).subscribe((updated: any) => {
|
||||
this.clientFamilyMembers[realIndex] = updated;
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// ═══ Common ═══
|
||||
|
||||
deleteMember(member: any, label: string) {
|
||||
const realIndex = this.clientFamilyMembers.indexOf(member);
|
||||
const deleteFamilyMemberDialogRef = this.dialog.open(DeleteDialogComponent, {
|
||||
data: { deleteContext: `Nominee id:${id} name : ${name}` }
|
||||
data: { deleteContext: `${label} ${member.firstName || ''}` }
|
||||
});
|
||||
deleteFamilyMemberDialogRef.afterClosed().subscribe((response: any) => {
|
||||
if (response.delete) {
|
||||
this.clientsService.deleteFamilyMember(clientId, id).subscribe(() => {
|
||||
this.clientFamilyMembers.splice(index, 1);
|
||||
if (response?.delete) {
|
||||
this.clientsService.deleteFamilyMember(member.clientId || this.clientId, member.id).subscribe(() => {
|
||||
this.clientFamilyMembers.splice(realIndex, 1);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
displayName(member: any): string {
|
||||
return member.firstName || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazily loads family member template options from the API.
|
||||
*/
|
||||
private ensureTemplateLoaded(): Promise<void> {
|
||||
if (this.familyMemberOptions) {
|
||||
return Promise.resolve();
|
||||
|
||||
@@ -6,180 +6,197 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="form60-container">
|
||||
<!-- Warning Notice -->
|
||||
<div class="form60-notice">
|
||||
<mat-icon>warning</mat-icon>
|
||||
<span>{{ 'labels.text.PAN not provided. Please fill Form 60 as per Income Tax Rule 114B.' | translate }}</span>
|
||||
</div>
|
||||
|
||||
@if (loading) {
|
||||
<div class="form60-loading">
|
||||
<mat-spinner diameter="40"></mat-spinner>
|
||||
<div class="f60-container">
|
||||
@if (loading || checkingPan) {
|
||||
<div class="f60-loading">
|
||||
<mat-spinner diameter="36"></mat-spinner>
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Read-only view when data exists and not editing -->
|
||||
}
|
||||
|
||||
<!-- ═══ PAN EXISTS — Form 60 Not Required ═══ -->
|
||||
@if (!loading && hasPan) {
|
||||
<div class="f60-pan-ok">
|
||||
<div class="f60-pan-ok__icon-wrap">
|
||||
<mat-icon class="f60-pan-ok__icon">verified</mat-icon>
|
||||
</div>
|
||||
<h3 class="f60-pan-ok__title">Form 60 Not Required</h3>
|
||||
<p class="f60-pan-ok__desc">
|
||||
PAN <strong>{{ panNumber }}</strong> is attached to this client.
|
||||
@if (panVerified) {
|
||||
<mat-icon class="f60-pan-ok__tick">check_circle</mat-icon> <span class="f60-pan-ok__verified">Verified</span>
|
||||
}
|
||||
</p>
|
||||
<p class="f60-pan-ok__rule">As per Income Tax Rule 114B, Form 60 is only required when PAN is not available.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ═══ NO PAN — Show Form 60 ═══ -->
|
||||
@if (!loading && !hasPan) {
|
||||
<!-- Warning Banner -->
|
||||
<div class="f60-warning">
|
||||
<mat-icon class="f60-warning__icon">warning_amber</mat-icon>
|
||||
<div class="f60-warning__text">
|
||||
<strong>PAN not provided</strong> — Form 60 declaration is mandatory as per Income Tax Rule 114B.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Read-only View -->
|
||||
@if (form60Data && !isEditMode) {
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ 'labels.inputs.Form 60 Declaration' | translate }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="form60-summary">
|
||||
<div class="f60-card">
|
||||
<div class="f60-card__header">
|
||||
<mat-icon class="f60-card__header-icon">description</mat-icon>
|
||||
<h3>Form 60 Declaration</h3>
|
||||
<span class="f60-card__status">Submitted</span>
|
||||
<button mat-stroked-button color="primary" (click)="toggleEditMode()" class="f60-card__edit">
|
||||
<mat-icon>edit</mat-icon> Edit
|
||||
</button>
|
||||
</div>
|
||||
<div class="f60-card__body">
|
||||
<div class="f60-detail-grid">
|
||||
@if (form60Data.fullName) {
|
||||
<div class="form60-field">
|
||||
<span class="label">{{ 'labels.inputs.Full Name' | translate }}</span>
|
||||
<span class="value">{{ form60Data.fullName }}</span>
|
||||
<div class="f60-detail">
|
||||
<span class="f60-detail__label"><mat-icon class="f60-detail__icon">person</mat-icon> Full Name</span>
|
||||
<span class="f60-detail__value">{{ form60Data.fullName }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (form60Data.dateOfBirth) {
|
||||
<div class="form60-field">
|
||||
<span class="label">{{ 'labels.inputs.Date Of Birth' | translate }}</span>
|
||||
<span class="value">{{ form60Data.dateOfBirth | dateFormat }}</span>
|
||||
<div class="f60-detail">
|
||||
<span class="f60-detail__label"><mat-icon class="f60-detail__icon">calendar_today</mat-icon> Date of Birth</span>
|
||||
<span class="f60-detail__value">{{ form60Data.dateOfBirth | dateFormat }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (form60Data.fatherName) {
|
||||
<div class="form60-field">
|
||||
<span class="label">{{ "labels.inputs.Father's Name" | translate }}</span>
|
||||
<span class="value">{{ form60Data.fatherName }}</span>
|
||||
<div class="f60-detail">
|
||||
<span class="f60-detail__label"><mat-icon class="f60-detail__icon">family_restroom</mat-icon> Father's Name</span>
|
||||
<span class="f60-detail__value">{{ form60Data.fatherName }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (form60Data.address) {
|
||||
<div class="form60-field">
|
||||
<span class="label">{{ 'labels.inputs.Address' | translate }}</span>
|
||||
<span class="value">{{ form60Data.address }}</span>
|
||||
<div class="f60-detail f60-detail--full">
|
||||
<span class="f60-detail__label"><mat-icon class="f60-detail__icon">location_on</mat-icon> Address</span>
|
||||
<span class="f60-detail__value">{{ form60Data.address }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (form60Data.transactionType) {
|
||||
<div class="form60-field">
|
||||
<span class="label">{{ 'labels.inputs.Transaction Type' | translate }}</span>
|
||||
<span class="value">{{ form60Data.transactionType }}</span>
|
||||
<div class="f60-detail">
|
||||
<span class="f60-detail__label"><mat-icon class="f60-detail__icon">receipt_long</mat-icon> Transaction Type</span>
|
||||
<span class="f60-detail__value">{{ form60Data.transactionType }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (form60Data.transactionAmount) {
|
||||
<div class="form60-field">
|
||||
<span class="label">{{ 'labels.inputs.Transaction Amount' | translate }}</span>
|
||||
<span class="value">{{ form60Data.transactionAmount }}</span>
|
||||
<div class="f60-detail">
|
||||
<span class="f60-detail__label"><mat-icon class="f60-detail__icon">currency_rupee</mat-icon> Transaction Amount</span>
|
||||
<span class="f60-detail__value">₹ {{ form60Data.transactionAmount }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (form60Data.estimatedTotalIncome) {
|
||||
<div class="form60-field">
|
||||
<span class="label">{{ 'labels.inputs.Estimated Total Income' | translate }}</span>
|
||||
<span class="value">{{ form60Data.estimatedTotalIncome }}</span>
|
||||
<div class="f60-detail">
|
||||
<span class="f60-detail__label"><mat-icon class="f60-detail__icon">account_balance_wallet</mat-icon> Estimated Total Income</span>
|
||||
<span class="f60-detail__value">₹ {{ form60Data.estimatedTotalIncome }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (form60Data.reasonForNoPan) {
|
||||
<div class="form60-field">
|
||||
<span class="label">{{ 'labels.inputs.Reason for Not Having PAN' | translate }}</span>
|
||||
<span class="value">{{ form60Data.reasonForNoPan }}</span>
|
||||
<div class="f60-detail">
|
||||
<span class="f60-detail__label"><mat-icon class="f60-detail__icon">help_outline</mat-icon> Reason for No PAN</span>
|
||||
<span class="f60-detail__value">{{ form60Data.reasonForNoPan }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</mat-card-content>
|
||||
<mat-card-actions>
|
||||
<button mat-raised-button color="primary" (click)="toggleEditMode()">
|
||||
<mat-icon>edit</mat-icon>
|
||||
{{ 'labels.buttons.Edit' | translate }}
|
||||
</button>
|
||||
</mat-card-actions>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Edit / Create Form -->
|
||||
@if (isEditMode) {
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ 'labels.inputs.Form 60 Declaration' | translate }}</mat-card-title>
|
||||
</mat-card-header>
|
||||
<mat-card-content>
|
||||
<div class="f60-card">
|
||||
<div class="f60-card__header">
|
||||
<mat-icon class="f60-card__header-icon">edit_note</mat-icon>
|
||||
<h3>Form 60 Declaration</h3>
|
||||
</div>
|
||||
<div class="f60-card__body">
|
||||
<form [formGroup]="form60Form" (ngSubmit)="submit()">
|
||||
<div class="layout-row-wrap responsive-column align-start-center">
|
||||
<!-- Full Name -->
|
||||
<mat-form-field class="flex-50-minus-25px">
|
||||
<mat-label>{{ 'labels.inputs.Full Name' | translate }}</mat-label>
|
||||
<div class="f60-form-grid">
|
||||
<mat-form-field appearance="outline" class="f60-ff">
|
||||
<mat-label>Full Name</mat-label>
|
||||
<span matPrefix class="f60-prefix"><mat-icon>person</mat-icon></span>
|
||||
<input formControlName="fullName" required matInput />
|
||||
@if (form60Form.controls.fullName.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Full Name' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
<mat-error>Full Name is <strong>required</strong></mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Date of Birth -->
|
||||
<mat-form-field class="flex-50-minus-25px" (click)="dobPicker.open()">
|
||||
<mat-label>{{ 'labels.inputs.Date Of Birth' | translate }}</mat-label>
|
||||
<mat-form-field appearance="outline" class="f60-ff" (click)="dobPicker.open()">
|
||||
<mat-label>Date of Birth</mat-label>
|
||||
<span matPrefix class="f60-prefix"><mat-icon>calendar_today</mat-icon></span>
|
||||
<input formControlName="dateOfBirth" matInput [max]="maxDate" [matDatepicker]="dobPicker" />
|
||||
<mat-datepicker-toggle matSuffix [for]="dobPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #dobPicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Father's Name -->
|
||||
<mat-form-field class="flex-50-minus-25px">
|
||||
<mat-label>{{ "labels.inputs.Father's Name" | translate }}</mat-label>
|
||||
<mat-form-field appearance="outline" class="f60-ff">
|
||||
<mat-label>Father's Name</mat-label>
|
||||
<span matPrefix class="f60-prefix"><mat-icon>family_restroom</mat-icon></span>
|
||||
<input formControlName="fatherName" matInput />
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Address -->
|
||||
<mat-form-field class="flex-50-minus-25px">
|
||||
<mat-label>{{ 'labels.inputs.Address' | translate }}</mat-label>
|
||||
<mat-form-field appearance="outline" class="f60-ff f60-ff--full">
|
||||
<mat-label>Address</mat-label>
|
||||
<span matPrefix class="f60-prefix"><mat-icon>location_on</mat-icon></span>
|
||||
<input formControlName="address" matInput />
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Transaction Type -->
|
||||
<mat-form-field class="flex-50-minus-25px">
|
||||
<mat-label>{{ 'labels.inputs.Transaction Type' | translate }}</mat-label>
|
||||
<mat-form-field appearance="outline" class="f60-ff">
|
||||
<mat-label>Transaction Type / Purpose</mat-label>
|
||||
<span matPrefix class="f60-prefix"><mat-icon>receipt_long</mat-icon></span>
|
||||
<mat-select formControlName="transactionType">
|
||||
@for (txnType of transactionTypes; track txnType.value) {
|
||||
<mat-option [value]="txnType.value">{{ txnType.key | translate }}</mat-option>
|
||||
<mat-option [value]="txnType.value">{{ txnType.label }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Transaction Amount -->
|
||||
<mat-form-field class="flex-50-minus-25px">
|
||||
<mat-label>{{ 'labels.inputs.Transaction Amount' | translate }}</mat-label>
|
||||
<mat-form-field appearance="outline" class="f60-ff">
|
||||
<mat-label>Transaction Amount</mat-label>
|
||||
<span matPrefix class="f60-prefix"><mat-icon>currency_rupee</mat-icon></span>
|
||||
<input type="number" formControlName="transactionAmount" matInput />
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Estimated Total Income -->
|
||||
<mat-form-field class="flex-50-minus-25px">
|
||||
<mat-label>{{ 'labels.inputs.Estimated Total Income' | translate }}</mat-label>
|
||||
<mat-form-field appearance="outline" class="f60-ff">
|
||||
<mat-label>Estimated Total Income</mat-label>
|
||||
<span matPrefix class="f60-prefix"><mat-icon>account_balance_wallet</mat-icon></span>
|
||||
<input type="number" formControlName="estimatedTotalIncome" matInput />
|
||||
</mat-form-field>
|
||||
|
||||
<!-- Reason for Not Having PAN -->
|
||||
<mat-form-field class="flex-50-minus-25px">
|
||||
<mat-label>{{ 'labels.inputs.Reason for Not Having PAN' | translate }}</mat-label>
|
||||
<mat-form-field appearance="outline" class="f60-ff">
|
||||
<mat-label>Reason for Not Having PAN</mat-label>
|
||||
<span matPrefix class="f60-prefix"><mat-icon>help_outline</mat-icon></span>
|
||||
<mat-select formControlName="reasonForNoPan">
|
||||
@for (reason of reasonOptions; track reason.value) {
|
||||
<mat-option [value]="reason.value">{{ reason.key | translate }}</mat-option>
|
||||
<mat-option [value]="reason.value">{{ reason.label }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Declaration Checkbox -->
|
||||
<div class="form60-declaration">
|
||||
<!-- Declaration -->
|
||||
<div class="f60-declaration">
|
||||
<mat-checkbox formControlName="declaration">
|
||||
{{ 'labels.text.I declare that I do not have PAN and information provided is correct.' | translate }}
|
||||
I hereby declare that I do not have a Permanent Account Number (PAN) and the information provided above is true and correct to the best of my knowledge and belief.
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="layout-row gap-32px layout-align-center">
|
||||
<!-- Actions -->
|
||||
<div class="f60-actions">
|
||||
@if (form60Data) {
|
||||
<button type="button" mat-raised-button (click)="toggleEditMode()">
|
||||
{{ 'labels.buttons.Cancel' | translate }}
|
||||
</button>
|
||||
<button type="button" mat-stroked-button (click)="toggleEditMode()">Cancel</button>
|
||||
}
|
||||
<button type="submit" mat-raised-button color="primary" [disabled]="!form60Form.valid">
|
||||
{{ 'labels.buttons.Submit' | translate }}
|
||||
<button type="submit" mat-flat-button color="primary" [disabled]="!form60Form.valid">
|
||||
<mat-icon>check</mat-icon> Submit Form 60
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -6,79 +6,119 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
.form60-container {
|
||||
padding: 16px;
|
||||
}
|
||||
$primary: #1E6BB8;
|
||||
$primary-light: #EBF3FF;
|
||||
$green: #16a34a;
|
||||
$green-light: #dcfce7;
|
||||
$amber: #f59e0b;
|
||||
$amber-light: #fef3c7;
|
||||
$border: #E2E8F0;
|
||||
$text: #1A2332;
|
||||
$muted: #64748B;
|
||||
$bg: #F0F4F8;
|
||||
|
||||
.form60-notice {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
background: #fff3cd;
|
||||
padding: 12px 16px;
|
||||
border-left: 5px solid #fc0;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 16px;
|
||||
font-size: 14px;
|
||||
color: #856404;
|
||||
.f60-container { padding: 20px 24px; }
|
||||
.f60-loading { display: flex; justify-content: center; padding: 40px; }
|
||||
|
||||
mat-icon {
|
||||
color: #856404;
|
||||
/* ── PAN OK — Not Required ── */
|
||||
.f60-pan-ok {
|
||||
text-align: center;
|
||||
padding: 48px 24px;
|
||||
background: $green-light;
|
||||
border: 1px solid #bbf7d0;
|
||||
border-radius: 12px;
|
||||
|
||||
&__icon-wrap {
|
||||
width: 72px; height: 72px; border-radius: 50%;
|
||||
background: #fff; display: inline-flex; align-items: center;
|
||||
justify-content: center; margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba($green, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.form60-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.form60-summary {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px;
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.form60-field {
|
||||
.label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #888;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
margin-top: 4px;
|
||||
display: block;
|
||||
&__icon { font-size: 40px; width: 40px; height: 40px; color: $green; }
|
||||
&__title { font-size: 20px; font-weight: 700; color: $text; margin: 0 0 8px; }
|
||||
&__desc {
|
||||
font-size: 15px; color: $text; margin: 0 0 6px;
|
||||
display: flex; align-items: center; justify-content: center; gap: 6px; flex-wrap: wrap;
|
||||
strong { font-family: 'JetBrains Mono', monospace; letter-spacing: 0.5px; }
|
||||
}
|
||||
&__tick { font-size: 18px !important; width: 18px !important; height: 18px !important; color: $green; }
|
||||
&__verified { font-size: 13px; font-weight: 600; color: $green; }
|
||||
&__rule { font-size: 12px; color: $muted; margin: 12px 0 0; }
|
||||
}
|
||||
|
||||
.form60-declaration {
|
||||
margin: 16px 0 24px;
|
||||
padding: 12px 16px;
|
||||
background: #f5f5f5;
|
||||
border-radius: 6px;
|
||||
/* ── Warning Banner ── */
|
||||
.f60-warning {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
background: $amber-light; padding: 14px 20px;
|
||||
border-left: 4px solid $amber; border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
&__icon { color: $amber; font-size: 24px; flex-shrink: 0; }
|
||||
&__text { font-size: 13px; color: #92400e; line-height: 1.5; strong { font-weight: 700; } }
|
||||
}
|
||||
|
||||
@media screen and (width >= 960px) {
|
||||
.layout-row-wrap > .flex-fill:nth-child(odd),
|
||||
.layout-row-wrap > .flex-50-minus-25px:nth-child(odd) {
|
||||
margin-right: 25px;
|
||||
}
|
||||
/* ── Card ── */
|
||||
.f60-card {
|
||||
background: #fff; border: 1px solid $border; border-radius: 12px; overflow: hidden;
|
||||
|
||||
.layout-row-wrap > .flex-fill:last-child,
|
||||
.layout-row-wrap > .flex-50-minus-25px:last-child {
|
||||
margin-right: 0;
|
||||
&__header {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 14px 20px; background: $bg; border-bottom: 1px solid $border;
|
||||
h3 { margin: 0; font-size: 15px; font-weight: 600; color: $text; flex: 1; }
|
||||
}
|
||||
&__header-icon { color: $primary; font-size: 22px; }
|
||||
&__status {
|
||||
font-size: 11px; font-weight: 600; color: $green; background: $green-light;
|
||||
padding: 3px 10px; border-radius: 12px;
|
||||
}
|
||||
&__edit {
|
||||
font-size: 12px !important; height: 30px !important; border-radius: 6px !important;
|
||||
mat-icon { font-size: 16px; width: 16px; height: 16px; margin-right: 4px; }
|
||||
}
|
||||
&__body { padding: 20px; }
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.form60-summary {
|
||||
grid-template-columns: 1fr;
|
||||
/* ── Detail Grid (read-only) ── */
|
||||
.f60-detail-grid {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 16px 24px;
|
||||
@media (max-width: 600px) { grid-template-columns: 1fr; }
|
||||
}
|
||||
.f60-detail {
|
||||
display: flex; flex-direction: column;
|
||||
&--full { grid-column: 1 / -1; }
|
||||
&__label {
|
||||
font-size: 11px; font-weight: 600; text-transform: uppercase;
|
||||
color: $muted; letter-spacing: 0.5px; margin-bottom: 4px;
|
||||
display: flex; align-items: center; gap: 4px;
|
||||
}
|
||||
&__icon { font-size: 15px !important; width: 15px !important; height: 15px !important; color: $primary; }
|
||||
&__value { font-size: 15px; font-weight: 600; color: $text; }
|
||||
}
|
||||
|
||||
/* ── Form Grid ── */
|
||||
.f60-form-grid {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 4px 16px;
|
||||
@media (max-width: 600px) { grid-template-columns: 1fr; }
|
||||
}
|
||||
.f60-ff { width: 100%; &--full { grid-column: 1 / -1; } }
|
||||
|
||||
.f60-prefix {
|
||||
display: inline-flex; align-items: center;
|
||||
padding-left: 4px; padding-right: 8px; margin-right: 8px;
|
||||
border-right: 1.5px solid #cbd5e1; line-height: 1;
|
||||
mat-icon { font-size: 20px; width: 20px; height: 20px; color: $primary; }
|
||||
}
|
||||
|
||||
/* ── Declaration ── */
|
||||
.f60-declaration {
|
||||
margin: 12px 0 20px; padding: 14px 16px;
|
||||
background: $bg; border-radius: 8px; border: 1px solid $border;
|
||||
font-size: 13px; line-height: 1.5; color: $muted;
|
||||
}
|
||||
|
||||
/* ── Actions ── */
|
||||
.f60-actions {
|
||||
display: flex; justify-content: flex-end; gap: 10px;
|
||||
button { border-radius: 8px !important; font-weight: 600 !important; }
|
||||
mat-icon { font-size: 18px; width: 18px; height: 18px; margin-right: 4px; vertical-align: middle; }
|
||||
}
|
||||
|
||||
@@ -54,32 +54,68 @@ export class Form60TabComponent implements OnInit, OnDestroy {
|
||||
form60Data: any = null;
|
||||
isEditMode = false;
|
||||
|
||||
/** PAN status — if PAN exists, Form 60 is not required */
|
||||
hasPan = false;
|
||||
panNumber: string | null = null;
|
||||
panVerified = false;
|
||||
checkingPan = true;
|
||||
|
||||
/** Maximum date allowed */
|
||||
maxDate = new Date();
|
||||
|
||||
/** Form */
|
||||
form60Form: UntypedFormGroup;
|
||||
|
||||
/** Dropdown options */
|
||||
transactionTypes = [
|
||||
{ value: 'Account Opening', key: 'labels.inputs.Account Opening' },
|
||||
{ value: 'Cash Deposit', key: 'labels.inputs.Cash Deposit' },
|
||||
{ value: 'Fixed Deposit', key: 'labels.inputs.Fixed Deposit' },
|
||||
{ value: 'Loan Processing', key: 'labels.inputs.Loan Processing' }
|
||||
];
|
||||
/** Dropdown options (loaded from API) */
|
||||
transactionTypes: any[] = [];
|
||||
loadingPurposes = true;
|
||||
|
||||
reasonOptions = [
|
||||
{ value: 'Applied for PAN', key: 'labels.inputs.Applied for PAN' },
|
||||
{ value: 'Income below taxable limit', key: 'labels.inputs.Income below taxable limit' },
|
||||
{ value: 'Other', key: 'labels.inputs.Other' }
|
||||
{ value: 'Applied for PAN', label: 'Applied for PAN' },
|
||||
{ value: 'Income below taxable limit', label: 'Income below taxable limit' },
|
||||
{ value: 'Agriculture income only', label: 'Agriculture income only' },
|
||||
{ value: 'NRI / Foreign National', label: 'NRI / Foreign National' },
|
||||
{ value: 'Other', label: 'Other' }
|
||||
];
|
||||
|
||||
ngOnInit(): void {
|
||||
this.maxDate = this.settingsService.businessDate;
|
||||
this.createForm();
|
||||
this.loadForm60Purposes();
|
||||
this.route.parent.params.subscribe((params: any) => {
|
||||
this.clientId = params['clientId'];
|
||||
this.loadForm60Data();
|
||||
this.checkPanStatus();
|
||||
});
|
||||
}
|
||||
|
||||
private loadForm60Purposes(): void {
|
||||
this.loadingPurposes = true;
|
||||
this.clientsService.getCodeValuesByName('FORM_60_PURPOSE').pipe(
|
||||
takeUntil(this.destroy$)
|
||||
).subscribe((values: any[]) => {
|
||||
this.transactionTypes = (values || [])
|
||||
.sort((a: any, b: any) => (a.position || 0) - (b.position || 0))
|
||||
.map((v: any) => ({ value: v.name, label: v.name }));
|
||||
this.loadingPurposes = false;
|
||||
});
|
||||
}
|
||||
|
||||
private checkPanStatus(): void {
|
||||
this.checkingPan = true;
|
||||
this.clientsService.getClientKyc(this.clientId).pipe(
|
||||
takeUntil(this.destroy$),
|
||||
catchError(() => of(null))
|
||||
).subscribe((kyc: any) => {
|
||||
this.checkingPan = false;
|
||||
if (kyc?.panNumber) {
|
||||
this.hasPan = true;
|
||||
this.panNumber = kyc.panNumber;
|
||||
this.panVerified = kyc.panVerified || false;
|
||||
this.loading = false;
|
||||
} else {
|
||||
this.hasPan = false;
|
||||
this.loadForm60Data();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -116,10 +152,15 @@ export class Form60TabComponent implements OnInit, OnDestroy {
|
||||
)
|
||||
.subscribe((data: any) => {
|
||||
this.loading = false;
|
||||
this.form60Data = data;
|
||||
if (data) {
|
||||
// API returns an array — use the latest (last) record
|
||||
if (Array.isArray(data) && data.length > 0) {
|
||||
this.form60Data = data[data.length - 1];
|
||||
this.populateForm(this.form60Data);
|
||||
} else if (data && !Array.isArray(data)) {
|
||||
this.form60Data = data;
|
||||
this.populateForm(data);
|
||||
} else {
|
||||
this.form60Data = null;
|
||||
this.isEditMode = true;
|
||||
}
|
||||
});
|
||||
@@ -155,6 +196,11 @@ export class Form60TabComponent implements OnInit, OnDestroy {
|
||||
if (formValue.dateOfBirth) {
|
||||
if (formValue.dateOfBirth instanceof Date) {
|
||||
formValue.dateOfBirth = this.dateUtils.formatDate(formValue.dateOfBirth, dateFormat);
|
||||
} else if (typeof formValue.dateOfBirth === 'string') {
|
||||
const parsed = new Date(formValue.dateOfBirth);
|
||||
if (!isNaN(parsed.getTime())) {
|
||||
formValue.dateOfBirth = this.dateUtils.formatDate(parsed, dateFormat);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
delete formValue.dateOfBirth;
|
||||
@@ -170,13 +216,13 @@ export class Form60TabComponent implements OnInit, OnDestroy {
|
||||
.submitClientForm60(this.clientId, data)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (response: any) => {
|
||||
this.form60Data = response || data;
|
||||
next: () => {
|
||||
this.isEditMode = false;
|
||||
this.alertService.alert({
|
||||
type: 'success',
|
||||
message: 'Form 60 submitted successfully.'
|
||||
});
|
||||
this.loadForm60Data();
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.alertService.alert({
|
||||
|
||||
@@ -6,134 +6,100 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="tab-container mat-typography">
|
||||
<div class="layout-row align-start">
|
||||
<div class="m-b-10">
|
||||
<h3>{{ 'labels.heading.Identities' | translate }}</h3>
|
||||
</div>
|
||||
<div class="action-button m-b-10 gap-25px">
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
(click)="addIdentifier()"
|
||||
*mifosxHasPermission="'CREATE_CLIENTIDENTIFIER'"
|
||||
>
|
||||
<fa-icon icon="plus" class="m-r-10"></fa-icon>
|
||||
{{ 'labels.buttons.Add' | translate }}
|
||||
</button>
|
||||
<div class="idt-container">
|
||||
<!-- Header -->
|
||||
<div class="idt-header">
|
||||
<div class="idt-header__left">
|
||||
<i class="fa fa-id-card idt-header__icon"></i>
|
||||
<h3 class="idt-header__title">{{ 'labels.heading.Identities' | translate }}</h3>
|
||||
<span class="idt-header__badge">{{ clientIdentities?.length || 0 }}</span>
|
||||
</div>
|
||||
<button mat-flat-button color="primary" class="idt-header__btn" (click)="addIdentifier()" *mifosxHasPermission="'CREATE_CLIENTIDENTIFIER'">
|
||||
<fa-icon icon="plus" class="m-r-10"></fa-icon>{{ 'labels.buttons.Add' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table mat-table #identifiersTable [dataSource]="clientIdentities" [ngStyle]="{ 'margin-top': '3%' }">
|
||||
<ng-container matColumnDef="id">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Id' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let identity">{{ identity.id }}</td>
|
||||
</ng-container>
|
||||
<!-- Empty State -->
|
||||
@if (!clientIdentities || clientIdentities.length === 0) {
|
||||
<div class="idt-empty">
|
||||
<i class="fa fa-id-badge idt-empty__icon"></i>
|
||||
<p class="idt-empty__text">{{ 'labels.messages.No Identities Available' | translate }}</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<ng-container matColumnDef="type">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Type' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let identity">{{ identity.documentType.name }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="documentKey">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Document Key' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let identity">{{ identity.documentKey }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="description">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Description' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let identity">{{ identity.description }}</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="documents">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Identity Documents' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let identity">
|
||||
@if (identity.documents?.length) {
|
||||
<div class="document-grid">
|
||||
@for (document of identity.documents; track trackByDocumentId($index, document)) {
|
||||
<div class="document-card">
|
||||
<div
|
||||
class="thumb"
|
||||
[class.clickable]="isPreviewable(document)"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
[attr.aria-label]="('labels.buttons.Preview' | translate) + ': ' + document.name"
|
||||
(keydown.enter)="isPreviewable(document) && openDocumentPreview(identity, document)"
|
||||
(keydown.space)="isPreviewable(document) && openDocumentPreview(identity, document)"
|
||||
(click)="isPreviewable(document) && openDocumentPreview(identity, document)"
|
||||
>
|
||||
@if (previewThumbnails[document.id]) {
|
||||
<img
|
||||
[src]="previewThumbnails[document.id]"
|
||||
[alt]="document.name"
|
||||
[title]="document.name"
|
||||
loading="lazy"
|
||||
/>
|
||||
} @else {
|
||||
<div class="placeholder">
|
||||
<fa-icon icon="file"></fa-icon>
|
||||
<span>{{ document.fileName || document.name }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (isPreviewable(document)) {
|
||||
<div class="preview-overlay">
|
||||
<fa-icon icon="eye"></fa-icon>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="title">{{ document.name }}</div>
|
||||
@if (document.fileName) {
|
||||
<div class="meta">{{ document.fileName }}</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<span class="muted">{{ 'labels.text.NoDocuments' | translate }}</span>
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="status">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Status' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let identity">
|
||||
{{ identity.status === 'clientIdentifierStatusType.active' ? 'active' : 'inactive' }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Actions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let identity; let i = index">
|
||||
<button
|
||||
class="identity-action-button"
|
||||
mat-raised-button
|
||||
color="warn"
|
||||
[title]="'labels.buttons.Delete' | translate"
|
||||
[attr.aria-label]="'labels.buttons.Delete' | translate"
|
||||
(click)="deleteIdentifier(identity.clientId, identity.id, i)"
|
||||
*mifosxHasPermission="'DELETE_CLIENTIDENTIFIER'"
|
||||
>
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="no-data">
|
||||
<td mat-footer-cell *matFooterCellDef [attr.colspan]="identitiesColumns.length">
|
||||
<div class="table-empty-state">
|
||||
<p>{{ 'labels.messages.No Identities Available' | translate }}</p>
|
||||
<!-- Identity Cards -->
|
||||
@for (identity of clientIdentities; track identity.id; let i = $index) {
|
||||
<div class="idt-card">
|
||||
<!-- Card Header -->
|
||||
<div class="idt-card__header">
|
||||
<div class="idt-card__title">
|
||||
<i class="fa fa-file-text idt-card__type-icon"></i>
|
||||
<span class="idt-card__type">{{ identity.documentType?.name }}</span>
|
||||
<span class="idt-card__status" [class.idt-card__status--active]="identity.status === 'clientIdentifierStatusType.active'" [class.idt-card__status--inactive]="identity.status !== 'clientIdentifierStatusType.active'">
|
||||
{{ identity.status === 'clientIdentifierStatusType.active' ? 'Active' : 'Inactive' }}
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
<button mat-stroked-button color="warn" class="idt-card__delete" (click)="deleteIdentifier(identity.clientId, identity.id, i)" *mifosxHasPermission="'DELETE_CLIENTIDENTIFIER'">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="identitiesColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: identitiesColumns"></tr>
|
||||
@if (clientIdentities?.length === 0) {
|
||||
<tr mat-footer-row *matFooterRowDef="['no-data']"></tr>
|
||||
}
|
||||
</table>
|
||||
<!-- Card Body -->
|
||||
<div class="idt-card__body">
|
||||
<div class="idt-grid">
|
||||
<div class="idt-field">
|
||||
<div class="idt-field__label">{{ 'labels.inputs.Id' | translate }}</div>
|
||||
<div class="idt-field__value idt-field__value--mono">{{ identity.id }}</div>
|
||||
</div>
|
||||
<div class="idt-field">
|
||||
<div class="idt-field__label">{{ 'labels.inputs.Document Key' | translate }}</div>
|
||||
<div class="idt-field__value idt-field__value--mono">{{ identity.documentKey }}</div>
|
||||
</div>
|
||||
@if (identity.description) {
|
||||
<div class="idt-field">
|
||||
<div class="idt-field__label">{{ 'labels.inputs.Description' | translate }}</div>
|
||||
<div class="idt-field__value">{{ identity.description }}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Documents -->
|
||||
@if (identity.documents?.length) {
|
||||
<div class="idt-docs">
|
||||
<div class="idt-docs__title">
|
||||
<i class="fa fa-paperclip"></i> {{ 'labels.inputs.Identity Documents' | translate }}
|
||||
</div>
|
||||
<div class="idt-docs__grid">
|
||||
@for (document of identity.documents; track trackByDocumentId($index, document)) {
|
||||
<div class="idt-doc" [class.idt-doc--clickable]="isPreviewable(document)"
|
||||
(click)="isPreviewable(document) && openDocumentPreview(identity, document)"
|
||||
(keydown.enter)="isPreviewable(document) && openDocumentPreview(identity, document)"
|
||||
tabindex="0" role="button"
|
||||
[attr.aria-label]="('labels.buttons.Preview' | translate) + ': ' + document.name">
|
||||
@if (previewThumbnails[document.id]) {
|
||||
<img [src]="previewThumbnails[document.id]" [alt]="document.name" class="idt-doc__thumb" loading="lazy" />
|
||||
} @else {
|
||||
<div class="idt-doc__placeholder">
|
||||
<i class="fa fa-file-o"></i>
|
||||
</div>
|
||||
}
|
||||
<div class="idt-doc__info">
|
||||
<span class="idt-doc__name">{{ document.name }}</span>
|
||||
@if (document.fileName) {
|
||||
<span class="idt-doc__file">{{ document.fileName }}</span>
|
||||
}
|
||||
</div>
|
||||
@if (isPreviewable(document)) {
|
||||
<i class="fa fa-eye idt-doc__preview"></i>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div #identityLightbox class="document-lightbox-host"></div>
|
||||
|
||||
@@ -6,142 +6,283 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@use '../../../../assets/styles/colours' as *;
|
||||
$primary: #1a73e8;
|
||||
$primary-light: #e8f0fe;
|
||||
$gray-50: #f8f9fa;
|
||||
$gray-100: #f1f3f4;
|
||||
$gray-200: #e8eaed;
|
||||
$gray-500: #9aa0a6;
|
||||
$gray-700: #5f6368;
|
||||
$gray-900: #202124;
|
||||
$white: #fff;
|
||||
$green: #34a853;
|
||||
$green-light: #e6f4ea;
|
||||
$red: #ea4335;
|
||||
$red-light: #fce8e6;
|
||||
|
||||
.tab-container {
|
||||
padding: 1rem;
|
||||
margin: 1rem 0;
|
||||
.idt-container {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
margin-left: auto;
|
||||
// Header
|
||||
.idt-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
&__icon { font-size: 16px; color: $primary; }
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: $gray-900;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
&__badge {
|
||||
background: $primary-light;
|
||||
color: $primary;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
.identity-action-button {
|
||||
min-width: 26px;
|
||||
padding: 0 6px;
|
||||
margin: 4px;
|
||||
line-height: 25px;
|
||||
&__btn {
|
||||
border-radius: 8px !important;
|
||||
font-size: 13px !important;
|
||||
font-weight: 600 !important;
|
||||
height: 36px !important;
|
||||
}
|
||||
}
|
||||
|
||||
// Empty
|
||||
.idt-empty {
|
||||
text-align: center;
|
||||
padding: 40px 20px;
|
||||
background: $gray-50;
|
||||
border-radius: 10px;
|
||||
border: 1px dashed $gray-200;
|
||||
|
||||
&__icon { font-size: 32px; color: $gray-500; margin-bottom: 8px; }
|
||||
&__text { color: $gray-500; font-size: 13px; margin: 0; }
|
||||
}
|
||||
|
||||
// Identity Card
|
||||
.idt-card {
|
||||
background: $white;
|
||||
border: 1px solid $gray-200;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 18px;
|
||||
background: $gray-50;
|
||||
border-bottom: 1px solid $gray-200;
|
||||
}
|
||||
|
||||
&__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&__type-icon {
|
||||
font-size: 14px;
|
||||
color: $primary;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: $primary-light;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&__type {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: $gray-900;
|
||||
}
|
||||
|
||||
&__status {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 2px 9px;
|
||||
border-radius: 10px;
|
||||
|
||||
&--active { background: $green-light; color: $green; }
|
||||
&--inactive { background: $red-light; color: $red; }
|
||||
}
|
||||
|
||||
&__delete {
|
||||
width: 30px !important;
|
||||
height: 30px !important;
|
||||
min-width: 30px !important;
|
||||
padding: 0 !important;
|
||||
border-radius: 6px !important;
|
||||
border-color: $gray-200 !important;
|
||||
color: $gray-500 !important;
|
||||
|
||||
&:hover { color: $red !important; border-color: $red !important; background: $red-light !important; }
|
||||
|
||||
i { font-size: 12px; }
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 14px 18px;
|
||||
}
|
||||
}
|
||||
|
||||
// Details grid
|
||||
.idt-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0;
|
||||
border: 1px solid $gray-100;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
|
||||
@media (max-width: 600px) { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
.idt-field {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid $gray-100;
|
||||
border-right: 1px solid $gray-100;
|
||||
|
||||
&:nth-child(3n) { border-right: none; }
|
||||
&:hover { background: $gray-50; }
|
||||
|
||||
&__label {
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: $gray-500;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: $gray-900;
|
||||
|
||||
&--mono {
|
||||
font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.document-grid {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
// Documents section
|
||||
.idt-docs {
|
||||
margin-top: 12px;
|
||||
|
||||
.document-card {
|
||||
width: 200px;
|
||||
background: $white;
|
||||
border: 1px solid $silver;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 4px rgba($dark-grey, 0.12);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
&__title {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: $gray-500;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba($dark-grey, 0.14);
|
||||
i { color: $primary; margin-right: 4px; }
|
||||
}
|
||||
|
||||
&__grid {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
.thumb {
|
||||
position: relative;
|
||||
background: $clouds;
|
||||
min-height: 7.5rem;
|
||||
.idt-doc {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: default;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
background: $gray-50;
|
||||
border: 1px solid $gray-100;
|
||||
border-radius: 6px;
|
||||
transition: all 0.12s;
|
||||
min-width: 180px;
|
||||
|
||||
&.clickable {
|
||||
&--clickable {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: $primary;
|
||||
background: $primary-light;
|
||||
}
|
||||
}
|
||||
|
||||
img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
&__thumb {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 4px;
|
||||
object-fit: cover;
|
||||
}
|
||||
}
|
||||
|
||||
.preview-overlay {
|
||||
position: absolute;
|
||||
bottom: 0.5rem;
|
||||
right: 0.5rem;
|
||||
background: rgba($white, 0.9);
|
||||
border-radius: 50%;
|
||||
padding: 0.4rem;
|
||||
color: $peter-river;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
|
||||
.thumb:hover &,
|
||||
.thumb:focus & {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: $light-grey;
|
||||
text-align: center;
|
||||
padding: 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4rem;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
fa-icon {
|
||||
font-size: 1.125rem;
|
||||
border: 1px solid $gray-200;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.2;
|
||||
&__placeholder {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: $gray-200;
|
||||
border-radius: 4px;
|
||||
color: $gray-500;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: $gray-900;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
&__file {
|
||||
font-size: 10px;
|
||||
color: $gray-500;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
&__preview {
|
||||
font-size: 12px;
|
||||
color: $primary;
|
||||
margin-left: auto;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
&:hover .idt-doc__preview { opacity: 1; }
|
||||
}
|
||||
|
||||
.preview-btn {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
bottom: 0.5rem;
|
||||
background: rgba($black, 0.65);
|
||||
color: $white;
|
||||
}
|
||||
|
||||
.card-body {
|
||||
padding: 0.625rem 0.75rem 0.75rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-weight: 700;
|
||||
color: $dark-grey;
|
||||
}
|
||||
|
||||
.meta {
|
||||
font-size: 0.75rem;
|
||||
color: $asbestos;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: $concrete;
|
||||
}
|
||||
|
||||
.document-lightbox-host {
|
||||
display: none;
|
||||
}
|
||||
.document-lightbox-host { display: none; }
|
||||
|
||||
@@ -97,8 +97,7 @@ export class IdentitiesTabComponent implements OnDestroy {
|
||||
'actions'
|
||||
];
|
||||
|
||||
/** Identifiers Table */
|
||||
@ViewChild('identifiersTable', { static: true }) identifiersTable: MatTable<Element>;
|
||||
/** (table removed — card layout now) */
|
||||
/** LightGallery host */
|
||||
@ViewChild('identityLightbox', { static: true }) identityLightbox: ElementRef<HTMLElement>;
|
||||
|
||||
@@ -215,20 +214,20 @@ export class IdentitiesTabComponent implements OnDestroy {
|
||||
};
|
||||
newIdentity.documents.push(newDoc);
|
||||
this.clientIdentities.push(newIdentity);
|
||||
this.identifiersTable.renderRows();
|
||||
// Array mutation triggers @for re-render
|
||||
this.setThumbnail(newDoc);
|
||||
},
|
||||
error: (err: any) => {
|
||||
console.error('Failed to upload document', err);
|
||||
// Still add the identifier even if document upload fails
|
||||
this.clientIdentities.push(newIdentity);
|
||||
this.identifiersTable.renderRows();
|
||||
// Array mutation triggers @for re-render
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// No file, just add the identifier
|
||||
this.clientIdentities.push(newIdentity);
|
||||
this.identifiersTable.renderRows();
|
||||
// Array mutation triggers @for re-render
|
||||
}
|
||||
},
|
||||
error: (err: any) => {
|
||||
@@ -253,7 +252,7 @@ export class IdentitiesTabComponent implements OnDestroy {
|
||||
if (response.delete) {
|
||||
this.clientService.deleteClientIdentifier(clientId, identifierId).subscribe((res) => {
|
||||
this.clientIdentities.splice(index, 1);
|
||||
this.identifiersTable.renderRows();
|
||||
// Array mutation triggers @for re-render
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -35,287 +35,226 @@
|
||||
|
||||
<!-- KYC Summary (read-only when not in edit mode) -->
|
||||
@if (!isEditMode && kycData) {
|
||||
<div class="kyc-summary-grid">
|
||||
<!-- PAN Card -->
|
||||
<mat-card class="kyc-card">
|
||||
<mat-card-content>
|
||||
<div class="kyc-card-header">
|
||||
<div class="kyc-card-label">
|
||||
<mat-icon class="kyc-card-icon">credit_card</mat-icon>
|
||||
<span>{{ 'labels.inputs.PAN Number' | translate }}</span>
|
||||
<span class="kyc-tag">KYC</span>
|
||||
</div>
|
||||
@if (kycData.panVerified) {
|
||||
<span class="status-chip verified">
|
||||
<mat-icon class="status-chip-icon">check_circle</mat-icon>
|
||||
{{ 'labels.inputs.Verified' | translate }}
|
||||
</span>
|
||||
} @else {
|
||||
<span class="status-chip not-verified">
|
||||
<mat-icon class="status-chip-icon">radio_button_unchecked</mat-icon>
|
||||
{{ 'labels.inputs.Not Verified' | translate }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<p class="kyc-value">{{ kycData.panNumber || '—' }}</p>
|
||||
@if (kycData.panVerifiedOn) {
|
||||
<p class="kyc-meta">
|
||||
{{ 'labels.inputs.Last Verified On' | translate }}: {{ formatDate(kycData.panVerifiedOn) }}
|
||||
</p>
|
||||
<!-- Verification Table -->
|
||||
<div class="kv-table">
|
||||
<div class="kv-table__header">
|
||||
<span class="kv-table__col kv-table__col--doc">Document</span>
|
||||
<span class="kv-table__col kv-table__col--num">Number</span>
|
||||
<span class="kv-table__col kv-table__col--status">Status</span>
|
||||
<span class="kv-table__col kv-table__col--date">Verified On</span>
|
||||
<span class="kv-table__col kv-table__col--action">Action</span>
|
||||
</div>
|
||||
|
||||
<!-- PAN Row -->
|
||||
<div class="kv-table__row">
|
||||
<span class="kv-table__col kv-table__col--doc">
|
||||
<mat-icon class="kv-doc-icon">credit_card</mat-icon> PAN
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--num kv-mono">{{ kycData.panNumber || '—' }}</span>
|
||||
<span class="kv-table__col kv-table__col--status">
|
||||
@if (kycData.panVerified) {
|
||||
<span class="kv-status kv-status--ok"><mat-icon>check_circle</mat-icon> Verified</span>
|
||||
} @else {
|
||||
<span class="kv-status kv-status--pending"><mat-icon>radio_button_unchecked</mat-icon> Pending</span>
|
||||
}
|
||||
@if (verificationError.pan) {
|
||||
<div class="kyc-error-msg">
|
||||
<mat-icon class="kyc-error-icon">error_outline</mat-icon>
|
||||
<span>{{ verificationError.pan }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (verificationStatus.pan === 'VERIFIED' && verificationDetails.pan?.name) {
|
||||
<p class="kyc-meta verification-detail-text">{{ verificationDetails.pan.name }}</p>
|
||||
}
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="primary"
|
||||
class="re-verify-btn"
|
||||
type="button"
|
||||
[disabled]="verifying.pan || !kycData.panNumber"
|
||||
(click)="verifyPAN()"
|
||||
>
|
||||
@if (verifying.pan) {
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
{{ 'labels.inputs.Re-Verify' | translate }}
|
||||
}
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--date">{{ kycData.panVerifiedOn ? formatDate(kycData.panVerifiedOn) : '—' }}</span>
|
||||
<span class="kv-table__col kv-table__col--action">
|
||||
<button mat-stroked-button class="kv-action-btn" type="button"
|
||||
[disabled]="verifying.pan || !kycData.panNumber" (click)="verifyPAN()">
|
||||
@if (verifying.pan) { <mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner> }
|
||||
@else { Re-Verify }
|
||||
</button>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Aadhaar Card -->
|
||||
<mat-card class="kyc-card">
|
||||
<mat-card-content>
|
||||
<div class="kyc-card-header">
|
||||
<div class="kyc-card-label">
|
||||
<mat-icon class="kyc-card-icon">fingerprint</mat-icon>
|
||||
<span>{{ 'labels.inputs.Aadhaar Number' | translate }}</span>
|
||||
<span class="kyc-tag">KYC</span>
|
||||
</div>
|
||||
@if (kycData.aadhaarVerified) {
|
||||
<span class="status-chip verified">
|
||||
<mat-icon class="status-chip-icon">check_circle</mat-icon>
|
||||
{{ 'labels.inputs.Verified' | translate }}
|
||||
</span>
|
||||
} @else {
|
||||
<span class="status-chip not-verified">
|
||||
<mat-icon class="status-chip-icon">radio_button_unchecked</mat-icon>
|
||||
{{ 'labels.inputs.Not Verified' | translate }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<p class="kyc-value">{{ kycData.aadhaarNumberMasked || '—' }}</p>
|
||||
@if (kycData.aadhaarVerifiedOn) {
|
||||
<p class="kyc-meta">
|
||||
{{ 'labels.inputs.Last Verified On' | translate }}: {{ formatDate(kycData.aadhaarVerifiedOn) }}
|
||||
</p>
|
||||
<!-- Aadhaar Row -->
|
||||
<div class="kv-table__row">
|
||||
<span class="kv-table__col kv-table__col--doc">
|
||||
<mat-icon class="kv-doc-icon">fingerprint</mat-icon> Aadhaar
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--num kv-mono">{{ kycData.aadhaarNumberMasked || '—' }}</span>
|
||||
<span class="kv-table__col kv-table__col--status">
|
||||
@if (kycData.aadhaarVerified) {
|
||||
<span class="kv-status kv-status--ok"><mat-icon>check_circle</mat-icon> Verified</span>
|
||||
} @else {
|
||||
<span class="kv-status kv-status--pending"><mat-icon>radio_button_unchecked</mat-icon> Pending</span>
|
||||
}
|
||||
@if (verificationError.aadhaar) {
|
||||
<div class="kyc-error-msg">
|
||||
<mat-icon class="kyc-error-icon">error_outline</mat-icon>
|
||||
<span>{{ verificationError.aadhaar }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (verificationStatus.aadhaar === 'VERIFIED' && verificationDetails.aadhaar?.name) {
|
||||
<p class="kyc-meta verification-detail-text">{{ verificationDetails.aadhaar.name }}</p>
|
||||
}
|
||||
|
||||
<!-- Step 1: Re-Verify button -->
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--date">{{ kycData.aadhaarVerifiedOn ? formatDate(kycData.aadhaarVerifiedOn) : '—' }}</span>
|
||||
<span class="kv-table__col kv-table__col--action">
|
||||
@if (!showAadhaarReVerify && !aadhaarOtpSent) {
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="primary"
|
||||
class="re-verify-btn"
|
||||
type="button"
|
||||
[disabled]="verifying.aadhaar || !kycData.aadhaarNumberMasked"
|
||||
(click)="startAadhaarReVerify()"
|
||||
>
|
||||
{{ 'labels.inputs.Re-Verify' | translate }}
|
||||
<button mat-stroked-button class="kv-action-btn" type="button"
|
||||
[disabled]="verifying.aadhaar || !kycData.aadhaarNumberMasked" (click)="startAadhaarReVerify()">
|
||||
Re-Verify
|
||||
</button>
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<!-- Step 2: Enter full Aadhaar + Send OTP -->
|
||||
@if (showAadhaarReVerify && !aadhaarOtpSent) {
|
||||
<div class="otp-section">
|
||||
<div class="otp-inline">
|
||||
<mat-form-field appearance="outline" class="otp-field">
|
||||
<mat-label>{{ 'labels.inputs.Enter 12-digit Aadhaar' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[(ngModel)]="aadhaarFullNumber"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
maxlength="12"
|
||||
type="password"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="accent"
|
||||
class="otp-verify-btn"
|
||||
type="button"
|
||||
[disabled]="!aadhaarFullNumber || aadhaarFullNumber.length !== 12 || verifying.aadhaar"
|
||||
(click)="sendAadhaarOtp()"
|
||||
>
|
||||
@if (verifying.aadhaar) {
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
Send OTP
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Step 3: Enter OTP + Verify -->
|
||||
@if (aadhaarOtpSent) {
|
||||
<div class="otp-section">
|
||||
<p class="otp-info">
|
||||
<mat-icon class="otp-info-icon">sms</mat-icon>
|
||||
OTP sent to Aadhaar-linked mobile
|
||||
</p>
|
||||
<div class="otp-inline">
|
||||
<mat-form-field appearance="outline" class="otp-field">
|
||||
<mat-label>Enter OTP</mat-label>
|
||||
<input
|
||||
matInput
|
||||
[(ngModel)]="aadhaarOtp"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
maxlength="6"
|
||||
autocomplete="one-time-code"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="accent"
|
||||
class="otp-verify-btn"
|
||||
type="button"
|
||||
[disabled]="!aadhaarOtp || aadhaarOtp.length < 4 || aadhaarOtpVerifying"
|
||||
(click)="verifyAadhaarOtp()"
|
||||
>
|
||||
@if (aadhaarOtpVerifying) {
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
Verify OTP
|
||||
}
|
||||
</button>
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
class="otp-resend-btn"
|
||||
[disabled]="verifying.aadhaar"
|
||||
(click)="sendAadhaarOtp()"
|
||||
>
|
||||
Resend
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<!-- GSTIN Card -->
|
||||
<mat-card class="kyc-card">
|
||||
<mat-card-content>
|
||||
<div class="kyc-card-header">
|
||||
<div class="kyc-card-label">
|
||||
<mat-icon class="kyc-card-icon">business</mat-icon>
|
||||
<span>{{ 'labels.inputs.GSTIN Number' | translate }}</span>
|
||||
<span class="kyc-tag">KYC</span>
|
||||
</div>
|
||||
<!-- GSTIN Row (Non-Individual only) -->
|
||||
@if (isNonIndividual) {
|
||||
<div class="kv-table__row">
|
||||
<span class="kv-table__col kv-table__col--doc">
|
||||
<mat-icon class="kv-doc-icon">business</mat-icon> GSTIN
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--num kv-mono">{{ kycData.gstinNumber || '—' }}</span>
|
||||
<span class="kv-table__col kv-table__col--status">
|
||||
@if (kycData.gstinVerified) {
|
||||
<span class="status-chip verified">
|
||||
<mat-icon class="status-chip-icon">check_circle</mat-icon>
|
||||
{{ 'labels.inputs.Verified' | translate }}
|
||||
</span>
|
||||
<span class="kv-status kv-status--ok"><mat-icon>check_circle</mat-icon> Verified</span>
|
||||
} @else {
|
||||
<span class="status-chip not-verified">
|
||||
<mat-icon class="status-chip-icon">radio_button_unchecked</mat-icon>
|
||||
{{ 'labels.inputs.Not Verified' | translate }}
|
||||
</span>
|
||||
<span class="kv-status kv-status--pending"><mat-icon>radio_button_unchecked</mat-icon> Pending</span>
|
||||
}
|
||||
</div>
|
||||
<p class="kyc-value">{{ kycData.gstinNumber || '—' }}</p>
|
||||
@if (kycData.gstinVerifiedOn) {
|
||||
<p class="kyc-meta">
|
||||
{{ 'labels.inputs.Last Verified On' | translate }}: {{ formatDate(kycData.gstinVerifiedOn) }}
|
||||
</p>
|
||||
}
|
||||
@if (verificationError.gstin) {
|
||||
<div class="kyc-error-msg">
|
||||
<mat-icon class="kyc-error-icon">error_outline</mat-icon>
|
||||
<span>{{ verificationError.gstin }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (verificationStatus.gstin === 'VERIFIED' && verificationDetails.gstin?.businessName) {
|
||||
<p class="kyc-meta verification-detail-text">{{ verificationDetails.gstin.businessName }}</p>
|
||||
}
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="primary"
|
||||
class="re-verify-btn"
|
||||
type="button"
|
||||
[disabled]="verifying.gstin || !kycData.gstinNumber"
|
||||
(click)="verifyGSTIN()"
|
||||
>
|
||||
@if (verifying.gstin) {
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--date">{{ kycData.gstinVerifiedOn ? formatDate(kycData.gstinVerifiedOn) : '—' }}</span>
|
||||
<span class="kv-table__col kv-table__col--action">
|
||||
<button mat-stroked-button class="kv-action-btn" type="button"
|
||||
[disabled]="verifying.gstin || !kycData.gstinNumber" (click)="verifyGSTIN()">
|
||||
@if (verifying.gstin) { <mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner> }
|
||||
@else { Re-Verify }
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Enterprise PAN Row (Non-Individual) -->
|
||||
@if (isNonIndividual && kycData?.enterprisePan) {
|
||||
<div class="kv-table__row">
|
||||
<span class="kv-table__col kv-table__col--doc">
|
||||
<mat-icon class="kv-doc-icon">credit_card</mat-icon> Enterprise PAN
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--num kv-mono">{{ kycData.enterprisePan }}</span>
|
||||
<span class="kv-table__col kv-table__col--status">
|
||||
@if (kycData.enterprisePanVerified) {
|
||||
<span class="kv-status kv-status--ok"><mat-icon>check_circle</mat-icon> Verified</span>
|
||||
} @else {
|
||||
{{ 'labels.inputs.Re-Verify' | translate }}
|
||||
<span class="kv-status kv-status--pending"><mat-icon>radio_button_unchecked</mat-icon> Pending</span>
|
||||
}
|
||||
</button>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--date">{{ kycData.enterprisePanVerifiedOn ? formatDate(kycData.enterprisePanVerifiedOn) : '—' }}</span>
|
||||
<span class="kv-table__col kv-table__col--action"></span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- CIN Row (Non-Individual / Company) -->
|
||||
@if (isNonIndividual && kycData?.cinNumber) {
|
||||
<div class="kv-table__row">
|
||||
<span class="kv-table__col kv-table__col--doc">
|
||||
<mat-icon class="kv-doc-icon">verified_user</mat-icon> CIN
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--num kv-mono">{{ kycData.cinNumber }}</span>
|
||||
<span class="kv-table__col kv-table__col--status">
|
||||
@if (kycData.cinVerified) {
|
||||
<span class="kv-status kv-status--ok"><mat-icon>check_circle</mat-icon> Verified</span>
|
||||
} @else {
|
||||
<span class="kv-status kv-status--pending"><mat-icon>radio_button_unchecked</mat-icon> Pending</span>
|
||||
}
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--date">—</span>
|
||||
<span class="kv-table__col kv-table__col--action"></span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Incorporation Date Row (Non-Individual) -->
|
||||
@if (isNonIndividual && kycData?.incorporationDate) {
|
||||
<div class="kv-table__row kv-table__row--link">
|
||||
<span class="kv-table__col kv-table__col--doc">
|
||||
<mat-icon class="kv-doc-icon">event</mat-icon> Incorporation Date
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--num">{{ formatDate(kycData.incorporationDate) }}</span>
|
||||
<span class="kv-table__col kv-table__col--status">—</span>
|
||||
<span class="kv-table__col kv-table__col--date">—</span>
|
||||
<span class="kv-table__col kv-table__col--action"></span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- PAN-Aadhaar Link Row -->
|
||||
<div class="kv-table__row kv-table__row--link">
|
||||
<span class="kv-table__col kv-table__col--doc">
|
||||
<mat-icon class="kv-doc-icon">link</mat-icon> PAN–Aadhaar Link
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--num">Sec 139AA</span>
|
||||
<span class="kv-table__col kv-table__col--status">
|
||||
@if (kycData?.panAadhaarSeedingStatus === 'Y') {
|
||||
<span class="kv-status kv-status--ok"><mat-icon>check_circle</mat-icon> Linked</span>
|
||||
} @else if (kycData?.panAadhaarSeedingStatus === 'N') {
|
||||
<span class="kv-status kv-status--fail"><mat-icon>cancel</mat-icon> Not Linked</span>
|
||||
} @else if (kycData?.panAadhaarSeedingStatus === 'E') {
|
||||
<span class="kv-status kv-status--info"><mat-icon>info</mat-icon> Exempt</span>
|
||||
} @else {
|
||||
<span class="kv-status kv-status--pending"><mat-icon>radio_button_unchecked</mat-icon> Pending</span>
|
||||
}
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--date">—</span>
|
||||
<span class="kv-table__col kv-table__col--action"></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Additional Info -->
|
||||
<mat-card class="kyc-info-card">
|
||||
<mat-card-content>
|
||||
<div class="kyc-info-grid">
|
||||
<div class="kyc-info-item">
|
||||
<span class="kyc-info-label">{{ 'labels.inputs.KYC Type' | translate }}</span>
|
||||
<span class="kyc-info-value">{{
|
||||
kycData.kycType === 1 ? 'Individual' : kycData.kycType === 2 ? 'Non-Individual' : '—'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="kyc-info-item">
|
||||
<span class="kyc-info-label">{{ 'labels.inputs.Risk Category' | translate }}</span>
|
||||
<span class="kyc-info-value">{{
|
||||
kycData.riskCategory === 1
|
||||
? 'Low'
|
||||
: kycData.riskCategory === 2
|
||||
? 'Medium'
|
||||
: kycData.riskCategory === 3
|
||||
? 'High'
|
||||
: '—'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="kyc-info-item">
|
||||
<span class="kyc-info-label">{{ 'labels.inputs.Initiated On' | translate }}</span>
|
||||
<span class="kyc-info-value">{{ kycData.initiatedOn || '—' }}</span>
|
||||
</div>
|
||||
<div class="kyc-info-item">
|
||||
<span class="kyc-info-label">{{ 'labels.inputs.Last Verified On' | translate }}</span>
|
||||
<span class="kyc-info-value">{{ formatDate(kycData.verifiedOn) }}</span>
|
||||
</div>
|
||||
<div class="kyc-info-item">
|
||||
<span class="kyc-info-label">{{ 'labels.inputs.Expiry Date' | translate }}</span>
|
||||
<span class="kyc-info-value">{{ kycData.expiryDate || '—' }}</span>
|
||||
</div>
|
||||
@if (kycData.remarks) {
|
||||
<div class="kyc-info-item full-span">
|
||||
<span class="kyc-info-label">{{ 'labels.inputs.Remarks' | translate }}</span>
|
||||
<span class="kyc-info-value">{{ kycData.remarks }}</span>
|
||||
</div>
|
||||
}
|
||||
<!-- Aadhaar OTP inline (shown below table when re-verifying) -->
|
||||
@if (showAadhaarReVerify && !aadhaarOtpSent) {
|
||||
<div class="kv-otp-bar">
|
||||
<mat-form-field appearance="outline" class="kv-otp-input">
|
||||
<mat-label>Enter 12-digit Aadhaar</mat-label>
|
||||
<input matInput [(ngModel)]="aadhaarFullNumber" [ngModelOptions]="{standalone: true}" maxlength="12" type="password" />
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" type="button" class="kv-otp-btn"
|
||||
[disabled]="!aadhaarFullNumber || aadhaarFullNumber.length !== 12 || verifying.aadhaar" (click)="sendAadhaarOtp()">
|
||||
@if (verifying.aadhaar) { <mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner> }
|
||||
@else { Send OTP }
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (aadhaarOtpSent) {
|
||||
<div class="kv-otp-bar">
|
||||
<span class="kv-otp-label"><mat-icon>sms</mat-icon> OTP sent</span>
|
||||
<mat-form-field appearance="outline" class="kv-otp-input">
|
||||
<mat-label>Enter OTP</mat-label>
|
||||
<input matInput [(ngModel)]="aadhaarOtp" [ngModelOptions]="{standalone: true}" maxlength="6" autocomplete="one-time-code" />
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" type="button" class="kv-otp-btn"
|
||||
[disabled]="!aadhaarOtp || aadhaarOtp.length < 4 || aadhaarOtpVerifying" (click)="verifyAadhaarOtp()">
|
||||
@if (aadhaarOtpVerifying) { <mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner> }
|
||||
@else { Verify }
|
||||
</button>
|
||||
<button mat-stroked-button type="button" class="kv-otp-btn" [disabled]="verifying.aadhaar" (click)="sendAadhaarOtp()">Resend</button>
|
||||
</div>
|
||||
}
|
||||
@if (verificationError.aadhaar) {
|
||||
<div class="kyc-error-msg"><mat-icon class="kyc-error-icon">error_outline</mat-icon> {{ verificationError.aadhaar }}</div>
|
||||
}
|
||||
@if (verificationError.pan) {
|
||||
<div class="kyc-error-msg"><mat-icon class="kyc-error-icon">error_outline</mat-icon> {{ verificationError.pan }}</div>
|
||||
}
|
||||
|
||||
<!-- KYC Meta Grid -->
|
||||
<div class="kv-meta-grid">
|
||||
<div class="kv-meta-item">
|
||||
<span class="kv-meta-label">KYC Type</span>
|
||||
<span class="kv-meta-value">{{ kycData.kycType === 1 ? 'Individual' : kycData.kycType === 2 ? 'Non-Individual' : '—' }}</span>
|
||||
</div>
|
||||
<div class="kv-meta-item">
|
||||
<span class="kv-meta-label">Risk Category</span>
|
||||
<span class="kv-meta-value">{{ kycData.riskCategory === 1 ? 'Low' : kycData.riskCategory === 2 ? 'Medium' : kycData.riskCategory === 3 ? 'High' : '—' }}</span>
|
||||
</div>
|
||||
<div class="kv-meta-item">
|
||||
<span class="kv-meta-label">Initiated On</span>
|
||||
<span class="kv-meta-value">{{ formatDate(kycData.initiatedOn) }}</span>
|
||||
</div>
|
||||
<div class="kv-meta-item">
|
||||
<span class="kv-meta-label">Last Verified</span>
|
||||
<span class="kv-meta-value">{{ formatDate(kycData.verifiedOn) }}</span>
|
||||
</div>
|
||||
<div class="kv-meta-item">
|
||||
<span class="kv-meta-label">Expiry Date</span>
|
||||
<span class="kv-meta-value">{{ formatDate(kycData.expiryDate) }}</span>
|
||||
</div>
|
||||
@if (kycData.remarks) {
|
||||
<div class="kv-meta-item kv-meta-item--full">
|
||||
<span class="kv-meta-label">Remarks</span>
|
||||
<span class="kv-meta-value">{{ kycData.remarks }}</span>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- No KYC Data State -->
|
||||
@@ -497,67 +436,69 @@
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- GSTIN -->
|
||||
<div class="kyc-edit-field">
|
||||
<div class="kyc-field-label-row">
|
||||
<span class="kyc-field-title">{{ 'labels.inputs.GSTIN Number' | translate }}</span>
|
||||
<span class="kyc-tag">KYC</span>
|
||||
</div>
|
||||
<div class="kyc-input-row">
|
||||
<mat-form-field appearance="outline" class="kyc-input">
|
||||
<mat-label>{{ 'labels.inputs.GSTIN Number' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="gstinNumber"
|
||||
maxlength="15"
|
||||
style="text-transform: uppercase"
|
||||
(input)="
|
||||
kycForm
|
||||
.get('gstinNumber')
|
||||
?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
|
||||
"
|
||||
/>
|
||||
@if (kycForm.get('gstinNumber')?.hasError('pattern') && kycForm.get('gstinNumber')?.touched) {
|
||||
<mat-error>{{ 'labels.inputs.Enter 15-character GSTIN' | translate }}</mat-error>
|
||||
}
|
||||
@if (verificationStatus.gstin === 'VERIFIED') {
|
||||
<mat-icon matSuffix class="status-icon verified">check_circle</mat-icon>
|
||||
}
|
||||
@if (verificationStatus.gstin === 'FAILED') {
|
||||
<mat-icon matSuffix class="status-icon failed">cancel</mat-icon>
|
||||
}
|
||||
</mat-form-field>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="accent"
|
||||
type="button"
|
||||
class="verify-btn"
|
||||
[disabled]="
|
||||
!kycId ||
|
||||
!kycForm.get('gstinNumber')?.value ||
|
||||
kycForm.get('gstinNumber')?.invalid ||
|
||||
verifying.gstin
|
||||
"
|
||||
(click)="verifyGSTIN()"
|
||||
[matTooltip]="!kycId ? 'Save KYC details first' : ''"
|
||||
>
|
||||
@if (verifying.gstin) {
|
||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
{{ 'labels.inputs.Verify' | translate }}
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
@if (verificationError.gstin) {
|
||||
<div class="kyc-error-msg">
|
||||
<mat-icon class="kyc-error-icon">error_outline</mat-icon>
|
||||
<span>{{ verificationError.gstin }}</span>
|
||||
<!-- GSTIN (Non-Individual only) -->
|
||||
@if (isNonIndividual) {
|
||||
<div class="kyc-edit-field">
|
||||
<div class="kyc-field-label-row">
|
||||
<span class="kyc-field-title">{{ 'labels.inputs.GSTIN Number' | translate }}</span>
|
||||
<span class="kyc-tag">KYC</span>
|
||||
</div>
|
||||
}
|
||||
@if (verificationStatus.gstin === 'VERIFIED' && verificationDetails.gstin?.businessName) {
|
||||
<span class="verification-detail-text">{{ verificationDetails.gstin.businessName }}</span>
|
||||
}
|
||||
</div>
|
||||
<div class="kyc-input-row">
|
||||
<mat-form-field appearance="outline" class="kyc-input">
|
||||
<mat-label>{{ 'labels.inputs.GSTIN Number' | translate }}</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="gstinNumber"
|
||||
maxlength="15"
|
||||
style="text-transform: uppercase"
|
||||
(input)="
|
||||
kycForm
|
||||
.get('gstinNumber')
|
||||
?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
|
||||
"
|
||||
/>
|
||||
@if (kycForm.get('gstinNumber')?.hasError('pattern') && kycForm.get('gstinNumber')?.touched) {
|
||||
<mat-error>{{ 'labels.inputs.Enter 15-character GSTIN' | translate }}</mat-error>
|
||||
}
|
||||
@if (verificationStatus.gstin === 'VERIFIED') {
|
||||
<mat-icon matSuffix class="status-icon verified">check_circle</mat-icon>
|
||||
}
|
||||
@if (verificationStatus.gstin === 'FAILED') {
|
||||
<mat-icon matSuffix class="status-icon failed">cancel</mat-icon>
|
||||
}
|
||||
</mat-form-field>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="accent"
|
||||
type="button"
|
||||
class="verify-btn"
|
||||
[disabled]="
|
||||
!kycId ||
|
||||
!kycForm.get('gstinNumber')?.value ||
|
||||
kycForm.get('gstinNumber')?.invalid ||
|
||||
verifying.gstin
|
||||
"
|
||||
(click)="verifyGSTIN()"
|
||||
[matTooltip]="!kycId ? 'Save KYC details first' : ''"
|
||||
>
|
||||
@if (verifying.gstin) {
|
||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
{{ 'labels.inputs.Verify' | translate }}
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
@if (verificationError.gstin) {
|
||||
<div class="kyc-error-msg">
|
||||
<mat-icon class="kyc-error-icon">error_outline</mat-icon>
|
||||
<span>{{ verificationError.gstin }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (verificationStatus.gstin === 'VERIFIED' && verificationDetails.gstin?.businessName) {
|
||||
<span class="verification-detail-text">{{ verificationDetails.gstin.businessName }}</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- KYC Type -->
|
||||
@if (!kycId) {
|
||||
|
||||
@@ -369,3 +369,263 @@
|
||||
color: #c62828;
|
||||
}
|
||||
}
|
||||
|
||||
/* PAN-Aadhaar Link Card */
|
||||
.kyc-link-card {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.status-chip {
|
||||
&.linked {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
border: 1px solid #a7f3d0;
|
||||
}
|
||||
|
||||
&.not-linked {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
&.exempt {
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
border: 1px solid #bfdbfe;
|
||||
}
|
||||
}
|
||||
|
||||
.pan-aadhaar-detail {
|
||||
margin: 12px 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.pan-aadhaar-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
font-size: 13px;
|
||||
padding: 4px 0;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.pan-aadhaar-label {
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
min-width: 120px;
|
||||
}
|
||||
|
||||
.pan-aadhaar-value {
|
||||
color: #0f172a;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.text-success {
|
||||
color: #059669 !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
.text-danger {
|
||||
color: #dc2626 !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
|
||||
/* ═══ KYC Verification Table ═══ */
|
||||
.kv-table {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.kv-table__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #e2e8f0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.kv-table__header .kv-table__col {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: #64748b;
|
||||
padding: 10px 14px;
|
||||
}
|
||||
|
||||
.kv-table__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
padding: 0;
|
||||
transition: background 0.1s;
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
&:hover { background: #f8fafc; }
|
||||
|
||||
&--link { background: #fafbfc; }
|
||||
}
|
||||
|
||||
.kv-table__col {
|
||||
padding: 12px 14px;
|
||||
font-size: 13px;
|
||||
color: #1e293b;
|
||||
|
||||
&--doc {
|
||||
flex: 0 0 160px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
&--num { flex: 1; }
|
||||
&--status { flex: 0 0 150px; }
|
||||
&--date { flex: 0 0 150px; color: #64748b; font-size: 12px; }
|
||||
&--action { flex: 0 0 110px; text-align: right; }
|
||||
}
|
||||
|
||||
.kv-doc-icon {
|
||||
font-size: 18px !important;
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
color: #1074b9;
|
||||
}
|
||||
|
||||
.kv-mono {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
letter-spacing: 0.5px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.kv-status {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 14px !important;
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
}
|
||||
|
||||
&--ok { background: #ecfdf5; color: #059669; }
|
||||
&--pending { background: #f8fafc; color: #94a3b8; }
|
||||
&--fail { background: #fef2f2; color: #dc2626; }
|
||||
&--info { background: #eff6ff; color: #2563eb; }
|
||||
}
|
||||
|
||||
.kv-action-btn {
|
||||
font-size: 11px !important;
|
||||
height: 30px !important;
|
||||
line-height: 30px !important;
|
||||
padding: 0 12px !important;
|
||||
border-radius: 6px !important;
|
||||
font-weight: 600 !important;
|
||||
color: #1074b9 !important;
|
||||
border-color: #1074b9 !important;
|
||||
}
|
||||
|
||||
/* OTP Bar (below table) */
|
||||
.kv-otp-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.kv-otp-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
color: #1074b9;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
|
||||
mat-icon { font-size: 16px !important; width: 16px !important; height: 16px !important; }
|
||||
}
|
||||
|
||||
.kv-otp-input {
|
||||
width: 200px;
|
||||
flex-shrink: 0;
|
||||
|
||||
::ng-deep .mat-mdc-form-field-infix { padding: 6px 0 !important; min-height: 36px !important; }
|
||||
}
|
||||
|
||||
.kv-otp-btn {
|
||||
height: 36px !important;
|
||||
font-size: 12px !important;
|
||||
font-weight: 600 !important;
|
||||
border-radius: 6px !important;
|
||||
padding: 0 16px !important;
|
||||
}
|
||||
|
||||
/* Meta Grid */
|
||||
.kv-meta-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.kv-meta-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
padding: 12px 14px;
|
||||
border-right: 1px solid #f1f5f9;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
|
||||
&:nth-child(5n) { border-right: none; }
|
||||
&--full { grid-column: 1 / -1; border-right: none; }
|
||||
}
|
||||
|
||||
.kv-meta-label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.kv-meta-value {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.kv-table__col {
|
||||
&--doc { flex: 0 0 120px; }
|
||||
&--date { display: none; }
|
||||
&--action { flex: 0 0 90px; }
|
||||
}
|
||||
.kv-meta-grid { grid-template-columns: repeat(3, 1fr); }
|
||||
.kv-meta-item:nth-child(5n) { border-right: 1px solid #f1f5f9; }
|
||||
.kv-meta-item:nth-child(3n) { border-right: none; }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.kv-table__row { flex-wrap: wrap; }
|
||||
.kv-table__col { &--status, &--action { flex: 0 0 50%; } }
|
||||
.kv-meta-grid { grid-template-columns: 1fr 1fr; }
|
||||
.kv-otp-bar { flex-wrap: wrap; }
|
||||
}
|
||||
|
||||
@@ -52,6 +52,7 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
kycId: string | null = null;
|
||||
kycData: any = null;
|
||||
clientDateOfBirth: string | null = null;
|
||||
isNonIndividual = false;
|
||||
loading = true;
|
||||
saving = false;
|
||||
isEditMode = false;
|
||||
@@ -150,7 +151,7 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
/** Load client's date of birth from parent route resolved data */
|
||||
/** Load client's date of birth and legal form from parent route resolved data */
|
||||
loadClientDateOfBirth(): void {
|
||||
this.route.parent.data.subscribe((data: any) => {
|
||||
if (data?.clientViewData?.dateOfBirth) {
|
||||
@@ -161,6 +162,9 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
this.clientDateOfBirth = dob;
|
||||
}
|
||||
}
|
||||
if (data?.clientViewData?.legalForm?.id === 2) {
|
||||
this.isNonIndividual = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -431,6 +435,15 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
return part.toLowerCase();
|
||||
}
|
||||
|
||||
getPanAadhaarStatusLabel(status: string): string {
|
||||
switch (status) {
|
||||
case 'Y': return 'Linked (Seeded)';
|
||||
case 'N': return 'Not Linked';
|
||||
case 'E': return 'Exempt';
|
||||
default: return 'Pending Verification';
|
||||
}
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
<div class="nom-container mat-typography">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="nom-page-header">
|
||||
<div class="nom-page-header__left">
|
||||
<mat-icon class="nom-page-header__icon">shield</mat-icon>
|
||||
<div>
|
||||
<h3 class="nom-page-header__title">Account Nominees</h3>
|
||||
<p class="nom-page-header__sub">Manage nominees for each account. Nominees are account-specific.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (loading) {
|
||||
<div class="nom-loading">
|
||||
<mat-progress-spinner diameter="24" mode="indeterminate"></mat-progress-spinner>
|
||||
<span>Loading accounts...</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!loading && !hasAccounts) {
|
||||
<div class="nom-no-accounts">
|
||||
<mat-icon class="nom-no-accounts__icon">account_balance_wallet</mat-icon>
|
||||
<p>No active accounts found.</p>
|
||||
<p class="nom-no-accounts__sub">Nominees can be added once an account is opened.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!loading && hasAccounts) {
|
||||
<!-- Compact: Type chips + Account dropdown + Add button in one row -->
|
||||
<div class="nom-toolbar">
|
||||
<div class="nom-type-chips">
|
||||
@for (group of accountGroups; track group.type) {
|
||||
@if (group.accounts.length > 0) {
|
||||
<button type="button" class="nom-type-chip"
|
||||
[class.active]="selectedType === group.type"
|
||||
(click)="selectedType = group.type; onTypeChange()">
|
||||
<mat-icon>{{ group.icon }}</mat-icon>
|
||||
{{ group.label }}
|
||||
<span class="nom-type-chip__count">{{ group.accounts.length }}</span>
|
||||
</button>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@if (selectedGroup && selectedGroup.accounts.length > 0) {
|
||||
<div class="nom-toolbar__right">
|
||||
<mat-form-field appearance="outline" class="nom-account-select">
|
||||
<mat-label>Account</mat-label>
|
||||
<mat-select [(ngModel)]="selectedAccountId" (ngModelChange)="onAccountChange()">
|
||||
@for (acc of selectedGroup.accounts; track acc.id) {
|
||||
<mat-option [value]="acc.id">{{ acc.productName }} — {{ acc.accountNo }}@if (acc.accountBalance != null) { ({{ acc.accountBalance | number:'1.2-2' }}) }</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
@if (selectedAccountId && !showAddForm) {
|
||||
<button mat-flat-button color="primary" class="nom-add-btn"
|
||||
[disabled]="totalShare >= 100" (click)="showAddForm = true">
|
||||
<mat-icon>add</mat-icon> Add Nominee
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Nominees for selected account -->
|
||||
@if (selectedAccountId) {
|
||||
@if (nomineesLoading) {
|
||||
<div class="nom-loading nom-loading--sm">
|
||||
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
|
||||
<span>Loading...</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Share bar + Nominee list -->
|
||||
@if (nominees.length > 0) {
|
||||
<div class="nom-share-bar">
|
||||
<span class="nom-share-bar__label">Share</span>
|
||||
<div class="nom-share-bar__track">
|
||||
<div class="nom-share-bar__fill"
|
||||
[style.width.%]="totalShare > 100 ? 100 : totalShare"
|
||||
[class.over]="totalShare > 100"
|
||||
[class.complete]="totalShare === 100"></div>
|
||||
</div>
|
||||
<span class="nom-share-bar__value" [class.over]="totalShare > 100">{{ totalShare }}%</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@for (nominee of nominees; track nominee.id; let i = $index) {
|
||||
<div class="nom-card">
|
||||
<div class="nom-card__main">
|
||||
<div class="nom-card__avatar">
|
||||
@if (nominee.profileImageUrl) {
|
||||
<img [src]="nominee.profileImageUrl" alt="" class="nom-card__photo zoomable"
|
||||
(click)="zoomImage(nominee.profileImageUrl, 'Nominee Photo')" />
|
||||
} @else {
|
||||
<mat-icon>person</mat-icon>
|
||||
}
|
||||
</div>
|
||||
<div class="nom-card__info">
|
||||
<span class="nom-card__name">{{ nominee.nomineeDisplayName || nominee.firstName || '—' }}</span>
|
||||
<span class="nom-card__meta">
|
||||
{{ getRelationship(nominee) }}
|
||||
@if (nominee.nomineeDateOfBirth || nominee.dateOfBirth) { · DOB: {{ (nominee.nomineeDateOfBirth || nominee.dateOfBirth) | dateFormat }} }
|
||||
@if (nominee.nomineeMobileNo || nominee.mobileNo) { · {{ nominee.nomineeMobileNo || nominee.mobileNo }} }
|
||||
</span>
|
||||
</div>
|
||||
@if (nominee.signatureUrl) {
|
||||
<div class="nom-card__sig">
|
||||
<img [src]="nominee.signatureUrl" alt="Signature" class="zoomable"
|
||||
(click)="zoomImage(nominee.signatureUrl, 'Nominee Signature')" />
|
||||
</div>
|
||||
}
|
||||
@if (nominee.nomineeIdentifier) {
|
||||
<span class="nom-card__id">{{ nominee.nomineeIdentifier }}</span>
|
||||
}
|
||||
<div class="nom-card__share">
|
||||
<span class="nom-card__share-value">{{ nominee.percentageShare || 0 }}%</span>
|
||||
</div>
|
||||
<div class="nom-card__actions">
|
||||
<button mat-icon-button type="button" matTooltip="View" (click)="viewNominee(nominee)"><mat-icon>visibility</mat-icon></button>
|
||||
<button mat-icon-button type="button" matTooltip="Edit" (click)="editNominee(nominee, i)"><mat-icon>edit</mat-icon></button>
|
||||
<button mat-icon-button type="button" matTooltip="Delete" class="nom-card__delete" (click)="deleteNominee(nominee, i)"><mat-icon>delete</mat-icon></button>
|
||||
</div>
|
||||
</div>
|
||||
@if (nominee.nomineeIsMinor && (nominee.guardianDisplayName || nominee.guardianName)) {
|
||||
<div class="nom-card__guardian">
|
||||
<mat-icon>shield</mat-icon>
|
||||
Guardian: <strong>{{ nominee.guardianDisplayName || nominee.guardianName }}</strong>
|
||||
@if (nominee.guardianRelationshipName) { ({{ nominee.guardianRelationshipName }}) }
|
||||
</div>
|
||||
}
|
||||
<!-- Inline Edit -->
|
||||
@if (editingIndex === i) {
|
||||
<div class="nom-card__edit">
|
||||
<div class="nom-edit-row">
|
||||
<mat-form-field appearance="outline" class="nom-edit-field">
|
||||
<mat-label>Type</mat-label>
|
||||
<mat-select [(ngModel)]="editNomType">
|
||||
@for (opt of nominationTypeOptions; track opt.value) { <mat-option [value]="opt.value">{{ opt.label }}</mat-option> }
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="nom-edit-field">
|
||||
<mat-label>Relationship</mat-label>
|
||||
<mat-select [(ngModel)]="editRelId">
|
||||
@for (rel of relationshipOptions; track rel) { <mat-option [value]="rel.id">{{ rel.name }}</mat-option> }
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="nom-edit-field nom-edit-field--sm">
|
||||
<mat-label>Share %</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="editShare" min="1" [max]="maxShareForEdit(i)" />
|
||||
@if (editShare && editShare > maxShareForEdit(i)) { <mat-error>Max {{ maxShareForEdit(i) }}%</mat-error> }
|
||||
</mat-form-field>
|
||||
<button mat-flat-button color="primary" type="button" class="nom-edit-save"
|
||||
[disabled]="!editRelId || !editShare || editShare > maxShareForEdit(i)"
|
||||
(click)="saveEdit(nominee, i)"><mat-icon>check</mat-icon></button>
|
||||
<button mat-stroked-button type="button" class="nom-edit-cancel" (click)="cancelEdit()"><mat-icon>close</mat-icon></button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Empty -->
|
||||
@if (!nomineesLoading && nominees.length === 0 && !showAddForm) {
|
||||
<div class="nom-empty">
|
||||
<mat-icon>person_add</mat-icon>
|
||||
<span>No nominees for this account.</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- ═══ ADD NOMINEE FORM ═══ -->
|
||||
@if (showAddForm) {
|
||||
<div class="nom-add-form">
|
||||
<div class="nom-add-form__title"><fa-icon icon="search"></fa-icon> Search & Link Nominee</div>
|
||||
|
||||
@if (!selectedClient) {
|
||||
<div class="nom-search-wrapper">
|
||||
<mat-form-field appearance="outline" class="nom-search-field">
|
||||
<mat-label>Search by Name, Client ID or Mobile</mat-label>
|
||||
<span matPrefix class="nom-search-prefix"><mat-icon>search</mat-icon></span>
|
||||
<input matInput [value]="searchText" (input)="onSearch($any($event.target).value)" (blur)="dismissSearch()" placeholder="Type at least 2 characters..." autocomplete="off" />
|
||||
</mat-form-field>
|
||||
@if (searchResults.length > 0 || searching) {
|
||||
<div class="nom-search-results">
|
||||
@if (searching) { <div class="nom-search-loading"><mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner> Searching...</div> }
|
||||
@for (client of searchResults; track client.id) {
|
||||
<div class="nom-search-item" (mousedown)="selectClient(client); $event.preventDefault()">
|
||||
<mat-icon class="nom-search-item__icon">person</mat-icon>
|
||||
<div class="nom-search-item__info">
|
||||
<span class="nom-search-item__name">{{ client.displayName }}</span>
|
||||
<span class="nom-search-item__meta">ID: {{ client.id }} · {{ client.officeName }}@if (client.dateOfBirth) { · DOB: {{ client.dateOfBirth | dateFormat }} }@if (client.fatherSpouseName) { · {{ client.fatherSpouseName }} }@if (client.mobileNo) { · {{ client.mobileNo }} }</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@if (!searching && searchText.length >= 2 && searchResults.length === 0) {
|
||||
<div class="search-no-results-hint">No clients found</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (selectedClient) {
|
||||
<!-- Linked Card -->
|
||||
<div class="nom-linked-card">
|
||||
<div class="nom-linked-card__left">
|
||||
@if (linkedProfileImage) {
|
||||
<img [src]="linkedProfileImage" class="nom-linked-card__photo zoomable" (click)="zoomImage(linkedProfileImage, 'Photo')" />
|
||||
} @else { <mat-icon class="nom-linked-card__icon">person</mat-icon> }
|
||||
</div>
|
||||
<div class="nom-linked-card__info">
|
||||
<span class="nom-linked-card__name">{{ selectedClient.displayName }}</span>
|
||||
<span class="nom-linked-card__meta">ID: {{ selectedClient.id }}@if (selectedClient.mobileNo) { · {{ selectedClient.mobileNo }} }</span>
|
||||
</div>
|
||||
@if (linkedSignatureImage) {
|
||||
<div class="nom-linked-card__sig">
|
||||
<img [src]="linkedSignatureImage" alt="Signature" class="zoomable" (click)="zoomImage(linkedSignatureImage, 'Signature')" />
|
||||
</div>
|
||||
}
|
||||
<button mat-icon-button type="button" (click)="removeSelectedClient()"><mat-icon>close</mat-icon></button>
|
||||
</div>
|
||||
|
||||
@if (fetchingDetails) {
|
||||
<div class="nom-fetching-hint"><mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner> Fetching details...</div>
|
||||
}
|
||||
|
||||
@if (!fetchingDetails) {
|
||||
<!-- KYC details -->
|
||||
@if (selectedClient.dateOfBirth || selectedClient.panNumber || selectedClient.aadhaarMasked) {
|
||||
<div class="nom-readonly-row">
|
||||
@if (selectedClient.dateOfBirth) { <span class="nom-ro-item"><span class="nom-ro-label">DOB</span> {{ selectedClient.dateOfBirth | dateFormat }}</span> }
|
||||
@if (selectedClient.panNumber) { <span class="nom-ro-item"><span class="nom-ro-label">PAN</span> {{ selectedClient.panNumber }} @if (linkedKycStatus?.panVerified) { <mat-icon class="nom-ro-ok">check_circle</mat-icon> }</span> }
|
||||
@if (selectedClient.aadhaarMasked) { <span class="nom-ro-item"><span class="nom-ro-label">Aadhaar</span> {{ selectedClient.aadhaarMasked }} @if (linkedKycStatus?.aadhaarVerified) { <mat-icon class="nom-ro-ok">check_circle</mat-icon> }</span> }
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Nominee fields inline -->
|
||||
<div class="nom-fields-row">
|
||||
<mat-form-field appearance="outline" class="nom-field-item">
|
||||
<mat-label>Nomination Type</mat-label>
|
||||
<mat-select [(ngModel)]="nomineeNomType">
|
||||
@for (opt of nominationTypeOptions; track opt.value) { <mat-option [value]="opt.value">{{ opt.label }}</mat-option> }
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="nom-field-item">
|
||||
<mat-label>Relationship</mat-label>
|
||||
<mat-select [(ngModel)]="nomineeRelationshipId">
|
||||
@for (rel of relationshipOptions; track rel) { <mat-option [value]="rel.id">{{ rel.name }}</mat-option> }
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="nom-field-item nom-field-item--sm">
|
||||
<mat-label>Share %</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="nomineeShare" min="1" [max]="maxAllowedShare" />
|
||||
@if (nomineeShare && nomineeShare > maxAllowedShare) { <mat-error>Max {{ maxAllowedShare }}%</mat-error> }
|
||||
</mat-form-field>
|
||||
<div class="nom-field-item nom-field-item--toggle">
|
||||
<mat-slide-toggle [(ngModel)]="nomineeIsMinor">Minor</mat-slide-toggle>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (nomineeIsMinor) {
|
||||
<div class="nom-guardian-section">
|
||||
<span class="nom-guardian-label"><mat-icon>shield</mat-icon> Guardian</span>
|
||||
@if (!selectedGuardian) {
|
||||
<mat-form-field appearance="outline" class="nom-search-field">
|
||||
<input matInput [ngModel]="guardianSearchText" (ngModelChange)="onGuardianSearch($event)" placeholder="Search guardian..." autocomplete="off" />
|
||||
</mat-form-field>
|
||||
@if (guardianSearchResults.length > 0 || guardianSearching) {
|
||||
<div class="nom-search-results nom-search-results--inline">
|
||||
@if (guardianSearching) { <div class="nom-search-loading"><mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner></div> }
|
||||
@for (client of guardianSearchResults; track client.id) {
|
||||
<div class="nom-search-item" (click)="selectGuardian(client)">
|
||||
<mat-icon class="nom-search-item__icon">person</mat-icon>
|
||||
<span class="nom-search-item__name">{{ client.displayName }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<span class="nom-guardian-selected">{{ selectedGuardian.displayName }} <button mat-icon-button (click)="selectedGuardian = null"><mat-icon>close</mat-icon></button></span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<mat-form-field appearance="outline" class="nom-remarks-field">
|
||||
<mat-label>Remarks</mat-label>
|
||||
<input matInput [(ngModel)]="nomineeRemarks" placeholder="Optional" />
|
||||
</mat-form-field>
|
||||
|
||||
<div class="nom-form-actions">
|
||||
<button mat-stroked-button type="button" (click)="resetAddForm()">Cancel</button>
|
||||
<button mat-raised-button color="primary" type="button"
|
||||
[disabled]="!nomineeRelationshipId || !nomineeShare || nomineeSaving || (nomineeShare > maxAllowedShare) || (nomineeIsMinor && !selectedGuardian)"
|
||||
(click)="saveNominee()">
|
||||
@if (nomineeSaving) { <mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner> }
|
||||
@else { <mat-icon>check</mat-icon> Save }
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
</div>
|
||||
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
$primary: #1074b9;
|
||||
$primary-light: #e8f0fe;
|
||||
|
||||
.nom-container { padding: 16px 24px; }
|
||||
|
||||
/* Header */
|
||||
.nom-page-header {
|
||||
background: #0f172a; padding: 14px 20px; border-radius: 10px; margin-bottom: 16px;
|
||||
&__left { display: flex; align-items: center; gap: 10px; }
|
||||
&__icon { color: #60a5fa; }
|
||||
&__title { font-size: 15px; font-weight: 600; color: #fff; margin: 0; }
|
||||
&__sub { font-size: 10px; color: rgba(255,255,255,0.45); margin: 1px 0 0; }
|
||||
}
|
||||
|
||||
.nom-loading { display: flex; align-items: center; gap: 8px; padding: 24px 0; justify-content: center; font-size: 13px; color: #64748b;
|
||||
&--sm { padding: 12px 0; }
|
||||
}
|
||||
|
||||
.nom-no-accounts {
|
||||
text-align: center; padding: 36px 20px; background: #f8fafc; border-radius: 8px; border: 1px dashed #e2e8f0;
|
||||
&__icon { font-size: 40px; width: 40px; height: 40px; color: #94a3b8; margin-bottom: 8px; }
|
||||
p { color: #64748b; font-size: 13px; margin: 2px 0; }
|
||||
&__sub { font-size: 11px; color: #94a3b8; }
|
||||
}
|
||||
|
||||
/* Toolbar: chips on top, account + add below */
|
||||
.nom-toolbar {
|
||||
display: flex; flex-direction: column; gap: 10px; margin-bottom: 12px;
|
||||
&__right { display: flex; align-items: center; gap: 10px; }
|
||||
}
|
||||
.nom-type-chips { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.nom-type-chip {
|
||||
display: inline-flex; align-items: center; gap: 5px; padding: 6px 12px;
|
||||
border: 1.5px solid #e2e8f0; border-radius: 6px; background: #fff;
|
||||
font-size: 12px; font-weight: 500; color: #64748b; cursor: pointer; transition: all 0.15s;
|
||||
mat-icon { font-size: 16px; width: 16px; height: 16px; }
|
||||
&__count { font-size: 9px; font-weight: 700; background: #f1f5f9; color: #64748b; padding: 0 5px; border-radius: 6px; }
|
||||
&.active { border-color: $primary; background: $primary-light; color: $primary;
|
||||
.nom-type-chip__count { background: $primary; color: #fff; }
|
||||
}
|
||||
&:hover:not(.active) { border-color: #94a3b8; }
|
||||
}
|
||||
.nom-account-select { flex: 1; min-width: 280px; }
|
||||
.nom-add-btn { border-radius: 6px !important; font-size: 12px !important; height: 36px !important; white-space: nowrap; }
|
||||
|
||||
/* Share bar */
|
||||
.nom-share-bar {
|
||||
display: flex; align-items: center; gap: 8px; padding: 6px 12px;
|
||||
background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; margin-bottom: 8px;
|
||||
&__label { font-size: 11px; font-weight: 600; color: #64748b; white-space: nowrap; }
|
||||
&__track { flex: 1; height: 6px; background: #e2e8f0; border-radius: 3px; overflow: hidden; }
|
||||
&__fill {
|
||||
height: 100%; border-radius: 3px; transition: width 0.3s;
|
||||
background: linear-gradient(90deg, #3b82f6, #10b981);
|
||||
&.over { background: #ef4444; } &.complete { background: linear-gradient(90deg, #10b981, #059669); }
|
||||
}
|
||||
&__value { font-size: 13px; font-weight: 700; color: #10b981; min-width: 35px; text-align: right; &.over { color: #ef4444; } }
|
||||
}
|
||||
|
||||
/* Nominee card — compact */
|
||||
.nom-card { border: 1px solid #e2e8f0; border-radius: 6px; margin-bottom: 6px; overflow: hidden; }
|
||||
.nom-card__main { display: flex; align-items: center; gap: 10px; padding: 8px 12px; }
|
||||
.nom-card__avatar {
|
||||
width: 32px; height: 32px; border-radius: 50%; background: #dbeafe;
|
||||
display: flex; align-items: center; justify-content: center; color: #1565c0; flex-shrink: 0;
|
||||
mat-icon { font-size: 18px; }
|
||||
}
|
||||
.nom-card__photo { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
|
||||
.nom-card__sig {
|
||||
flex-shrink: 0;
|
||||
img { height: 24px; max-width: 60px; object-fit: contain; border: 1px solid #e2e8f0; border-radius: 3px; background: #fff; padding: 1px 3px; }
|
||||
}
|
||||
.nom-card__info { flex: 1; min-width: 0; }
|
||||
.nom-card__name { font-weight: 600; font-size: 13px; color: #1e293b; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.nom-card__meta { font-size: 11px; color: #64748b; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.nom-card__id {
|
||||
font-size: 10px; font-weight: 600; color: #1d4ed8; background: #eff6ff;
|
||||
padding: 2px 8px; border-radius: 4px; white-space: nowrap;
|
||||
font-family: 'Roboto Mono', monospace; letter-spacing: 0.3px; flex-shrink: 0;
|
||||
}
|
||||
.nom-card__share { text-align: center; min-width: 40px; flex-shrink: 0; }
|
||||
.nom-card__share-value { font-size: 15px; font-weight: 700; color: #1565c0; }
|
||||
.nom-card__actions { display: flex; gap: 0; flex-shrink: 0;
|
||||
button { width: 32px !important; height: 32px !important; line-height: 32px !important;
|
||||
mat-icon { font-size: 18px; }
|
||||
}
|
||||
}
|
||||
.nom-card__delete { color: #94a3b8 !important; &:hover { color: #ef4444 !important; } }
|
||||
.nom-card__guardian {
|
||||
display: flex; align-items: center; gap: 5px; padding: 5px 12px; background: #fff7ed;
|
||||
font-size: 11px; color: #92400e; border-top: 1px solid #fde68a;
|
||||
mat-icon { font-size: 14px; width: 14px; height: 14px; }
|
||||
}
|
||||
|
||||
/* Inline edit */
|
||||
.nom-card__edit { padding: 8px 12px; border-top: 1px solid #e2e8f0; background: #f8fafc; }
|
||||
.nom-edit-row { display: flex; align-items: center; gap: 8px; }
|
||||
.nom-edit-field { flex: 1; &--sm { max-width: 100px; } }
|
||||
.nom-edit-save, .nom-edit-cancel { min-width: 36px !important; height: 36px !important; padding: 0 !important; }
|
||||
|
||||
/* Empty */
|
||||
.nom-empty {
|
||||
text-align: center; padding: 24px; background: #f8fafc; border-radius: 6px; border: 1px dashed #e2e8f0;
|
||||
mat-icon { font-size: 28px; width: 28px; height: 28px; color: #94a3b8; margin-bottom: 4px; display: block; margin: 0 auto 4px; }
|
||||
span { font-size: 12px; color: #94a3b8; }
|
||||
}
|
||||
|
||||
/* ═══ Add Form ═══ */
|
||||
.nom-add-form {
|
||||
border: 1px solid #e2e8f0; border-radius: 8px; padding: 14px; margin-top: 8px; background: #fafbfc;
|
||||
&__title { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; color: #1e293b; margin-bottom: 12px; }
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.nom-search-wrapper { position: relative; margin-bottom: 8px; }
|
||||
.nom-search-field { width: 100%; }
|
||||
.nom-search-prefix { color: #94a3b8; }
|
||||
.nom-search-results {
|
||||
position: absolute; top: 56px; left: 0; right: 0; z-index: 100;
|
||||
background: #fff; border: 1px solid #e2e8f0; border-radius: 6px;
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.1); max-height: 220px; overflow-y: auto;
|
||||
&--inline { position: relative; top: 0; margin-top: 4px; }
|
||||
}
|
||||
.nom-search-loading { display: flex; align-items: center; gap: 6px; padding: 10px 12px; font-size: 12px; color: #64748b; }
|
||||
.nom-search-item {
|
||||
display: flex; align-items: center; gap: 8px; padding: 8px 12px; cursor: pointer; border-bottom: 1px solid #f1f5f9;
|
||||
&:last-child { border-bottom: none; } &:hover { background: #f0f7ff; }
|
||||
&__icon { color: $primary; font-size: 18px; }
|
||||
&__info { display: flex; flex-direction: column; flex: 1; }
|
||||
&__name { font-size: 13px; font-weight: 600; color: #1e293b; }
|
||||
&__meta { font-size: 10px; color: #64748b; }
|
||||
}
|
||||
.search-no-results-hint { font-size: 11px; color: #94a3b8; text-align: center; padding: 4px 0; }
|
||||
|
||||
/* Linked card */
|
||||
.nom-linked-card {
|
||||
display: flex; align-items: center; gap: 8px; padding: 8px 12px;
|
||||
background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; margin-bottom: 8px;
|
||||
&__left { flex-shrink: 0; }
|
||||
&__icon { color: $primary; }
|
||||
&__photo { width: 36px; height: 36px; border-radius: 50%; object-fit: cover; border: 2px solid #e2e8f0; }
|
||||
&__info { flex: 1; display: flex; flex-direction: column; }
|
||||
&__name { font-size: 13px; font-weight: 600; color: #1e293b; }
|
||||
&__meta { font-size: 11px; color: #64748b; font-family: 'Roboto Mono', monospace; }
|
||||
&__sig {
|
||||
flex-shrink: 0;
|
||||
img { height: 24px; max-width: 60px; object-fit: contain; border: 1px solid #e2e8f0; border-radius: 3px; background: #fff; padding: 1px 3px; }
|
||||
}
|
||||
}
|
||||
.nom-fetching-hint { display: flex; align-items: center; gap: 6px; font-size: 11px; color: #64748b; margin: -4px 0 8px; }
|
||||
|
||||
/* Read-only KYC row */
|
||||
.nom-readonly-row {
|
||||
display: flex; flex-wrap: wrap; gap: 16px; padding: 8px 12px;
|
||||
background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; margin-bottom: 10px;
|
||||
}
|
||||
.nom-ro-item { font-size: 12px; color: #1e293b; display: inline-flex; align-items: center; gap: 4px; }
|
||||
.nom-ro-label { font-size: 10px; font-weight: 600; color: #94a3b8; text-transform: uppercase; margin-right: 2px; }
|
||||
.nom-ro-ok { font-size: 14px !important; width: 14px !important; height: 14px !important; color: #16a34a; }
|
||||
|
||||
/* Nominee fields */
|
||||
.nom-fields-row { display: flex; flex-wrap: wrap; gap: 0 12px; align-items: center; margin-bottom: 4px; }
|
||||
.nom-field-item { flex: 1; min-width: 160px; &--sm { max-width: 100px; min-width: 80px; } &--toggle { padding-top: 8px; } }
|
||||
.nom-remarks-field { width: 100%; }
|
||||
.nom-form-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }
|
||||
|
||||
/* Guardian */
|
||||
.nom-guardian-section { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
|
||||
.nom-guardian-label { display: flex; align-items: center; gap: 4px; font-size: 12px; font-weight: 600; color: #92400e; mat-icon { font-size: 16px; } }
|
||||
.nom-guardian-selected { display: flex; align-items: center; gap: 4px; font-size: 13px; font-weight: 500; color: #1e293b;
|
||||
button { width: 24px !important; height: 24px !important; line-height: 24px !important; mat-icon { font-size: 14px; } }
|
||||
}
|
||||
|
||||
/* Zoom */
|
||||
.zoomable { cursor: zoom-in; transition: transform 0.15s; &:hover { transform: scale(1.08); box-shadow: 0 4px 12px rgba(0,0,0,0.15); } }
|
||||
::ng-deep .image-zoom-panel { .mat-mdc-dialog-container { padding: 0 !important; border-radius: 8px !important; overflow: hidden; } }
|
||||
@@ -0,0 +1,498 @@
|
||||
/**
|
||||
* Copyright since 2025 Mifos Initiative
|
||||
*
|
||||
* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
|
||||
import { ClientsService } from '../../clients.service';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { DeleteDialogComponent } from '../../../shared/delete-dialog/delete-dialog.component';
|
||||
import { ImageZoomDialogComponent } from '../../client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { MatSlideToggle } from '@angular/material/slide-toggle';
|
||||
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
|
||||
interface AccountGroup {
|
||||
type: string;
|
||||
apiType: string; // SB, CB, FD, RD, LA
|
||||
label: string;
|
||||
icon: string;
|
||||
accounts: any[];
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-nominee-tab',
|
||||
templateUrl: './nominee-tab.component.html',
|
||||
styleUrls: ['./nominee-tab.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
FormsModule,
|
||||
FaIconComponent,
|
||||
MatIcon,
|
||||
MatProgressSpinner,
|
||||
MatTooltip,
|
||||
MatSlideToggle,
|
||||
DateFormatPipe
|
||||
]
|
||||
})
|
||||
export class NomineeTabComponent implements OnInit {
|
||||
private route = inject(ActivatedRoute);
|
||||
private clientsService = inject(ClientsService);
|
||||
private settingsService = inject(SettingsService);
|
||||
private dialog = inject(MatDialog);
|
||||
private sanitizer = inject(DomSanitizer);
|
||||
|
||||
clientId: string;
|
||||
loading = true;
|
||||
accountGroups: AccountGroup[] = [];
|
||||
selectedType: string = '';
|
||||
selectedAccountId: number | null = null;
|
||||
|
||||
/** Nominees for selected account */
|
||||
nominees: any[] = [];
|
||||
nomineesLoading = false;
|
||||
|
||||
/** Add nominee form */
|
||||
showAddForm = false;
|
||||
searchText = '';
|
||||
searchResults: any[] = [];
|
||||
searching = false;
|
||||
private searchTimer: any = null;
|
||||
selectedClient: any = null;
|
||||
fetchingDetails = false;
|
||||
|
||||
/** Nominee form fields */
|
||||
nomineeRelationshipId: number | null = null;
|
||||
nomineeShare: number | null = null;
|
||||
nomineeIsMinor = false;
|
||||
nomineeSaving = false;
|
||||
nomineeRemarks = '';
|
||||
|
||||
/** Guardian */
|
||||
guardianSearchText = '';
|
||||
guardianSearchResults: any[] = [];
|
||||
guardianSearching = false;
|
||||
selectedGuardian: any = null;
|
||||
private guardianSearchTimer: any = null;
|
||||
|
||||
/** Linked client details */
|
||||
linkedProfileImage: SafeUrl | null = null;
|
||||
linkedSignatureImage: SafeUrl | null = null;
|
||||
linkedKycStatus: any = null;
|
||||
|
||||
/** Template data from API */
|
||||
relationshipOptions: any[] = [];
|
||||
|
||||
/** Nomination type options */
|
||||
nominationTypeOptions = [
|
||||
{ value: 'DA1', label: 'DA1 - Add Nominee' },
|
||||
{ value: 'DA5', label: 'DA5 - Modify / Cancel Nominee' }
|
||||
];
|
||||
|
||||
/** Add form nomination type */
|
||||
nomineeNomType = 'DA1';
|
||||
|
||||
/** Editing state */
|
||||
editingIndex: number | null = null;
|
||||
editRelId: number | null = null;
|
||||
editShare: number | null = null;
|
||||
editNomType: string = 'DA1';
|
||||
|
||||
get hasAccounts(): boolean {
|
||||
return this.accountGroups.some(g => g.accounts.length > 0);
|
||||
}
|
||||
|
||||
get selectedGroup(): AccountGroup | undefined {
|
||||
return this.accountGroups.find(g => g.type === this.selectedType);
|
||||
}
|
||||
|
||||
get selectedAccount(): any {
|
||||
return this.selectedGroup?.accounts.find((a: any) => a.id === this.selectedAccountId);
|
||||
}
|
||||
|
||||
get totalShare(): number {
|
||||
return this.nominees.reduce((sum, n) => sum + (n.percentageShare || 0), 0);
|
||||
}
|
||||
|
||||
get maxAllowedShare(): number {
|
||||
return 100 - this.totalShare;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.clientId = this.route.parent.snapshot.params['clientId'];
|
||||
this.loadAccounts();
|
||||
this.loadTemplate();
|
||||
}
|
||||
|
||||
private loadTemplate(): void {
|
||||
this.clientsService.getClientNomineeTemplate(this.clientId).subscribe({
|
||||
next: (template: any) => {
|
||||
this.relationshipOptions = template?.relationshipOptions || [];
|
||||
},
|
||||
error: () => {
|
||||
// Fallback to family member template
|
||||
this.clientsService.getClientTemplate().subscribe((t: any) => {
|
||||
this.relationshipOptions = t?.familyMemberOptions?.relationshipIdOptions || [];
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private loadAccounts(): void {
|
||||
this.loading = true;
|
||||
this.clientsService.getClientAccountData(this.clientId).subscribe({
|
||||
next: (data: any) => {
|
||||
this.loading = false;
|
||||
this.accountGroups = [
|
||||
{ type: 'savings', apiType: 'SB', label: 'Savings Account', icon: 'savings', accounts: this.filterActive(data.savingsAccounts) },
|
||||
{ type: 'current', apiType: 'CB', label: 'Current Account', icon: 'account_balance', accounts: this.filterActive(data.currentAccounts) },
|
||||
{ type: 'fd', apiType: 'FD', label: 'Fixed Deposit', icon: 'lock', accounts: this.filterActive(data.fixedDepositAccounts) },
|
||||
{ type: 'rd', apiType: 'RD', label: 'Recurring Deposit', icon: 'autorenew', accounts: this.filterActive(data.recurringDepositAccounts) },
|
||||
{ type: 'loan', apiType: 'LA', label: 'Loan Account', icon: 'credit_score', accounts: this.filterActive(data.loanAccounts) }
|
||||
];
|
||||
const firstWithAccounts = this.accountGroups.find(g => g.accounts.length > 0);
|
||||
if (firstWithAccounts) {
|
||||
this.selectedType = firstWithAccounts.type;
|
||||
// Auto-select if only 1 account in the group
|
||||
if (firstWithAccounts.accounts.length === 1) {
|
||||
this.selectedAccountId = firstWithAccounts.accounts[0].id;
|
||||
this.loadNominees();
|
||||
}
|
||||
}
|
||||
},
|
||||
error: () => { this.loading = false; }
|
||||
});
|
||||
}
|
||||
|
||||
private filterActive(accounts: any[]): any[] {
|
||||
if (!accounts) return [];
|
||||
// Include all non-closed accounts (active, disbursed, pending, etc.)
|
||||
return accounts.filter((a: any) =>
|
||||
a.status?.active ||
|
||||
a.status?.value === 'Active' ||
|
||||
!a.status?.closed
|
||||
);
|
||||
}
|
||||
|
||||
onTypeChange(): void {
|
||||
this.selectedAccountId = null;
|
||||
this.nominees = [];
|
||||
this.resetAddForm();
|
||||
// Auto-select if only 1 account
|
||||
if (this.selectedGroup?.accounts.length === 1) {
|
||||
this.selectedAccountId = this.selectedGroup.accounts[0].id;
|
||||
this.loadNominees();
|
||||
}
|
||||
}
|
||||
|
||||
onAccountChange(): void {
|
||||
this.nominees = [];
|
||||
this.resetAddForm();
|
||||
if (this.selectedAccountId) { this.loadNominees(); }
|
||||
}
|
||||
|
||||
private loadNominees(): void {
|
||||
this.nomineesLoading = true;
|
||||
const account = this.selectedAccount;
|
||||
const accountType = this.selectedGroup?.apiType || '';
|
||||
this.clientsService.getClientNominees(this.clientId, account?.accountNo, accountType).subscribe({
|
||||
next: (data: any) => {
|
||||
this.nomineesLoading = false;
|
||||
this.nominees = Array.isArray(data) ? data : [];
|
||||
// Enrich each nominee with profile image & signature
|
||||
this.nominees.forEach(n => this.enrichNomineeImages(n));
|
||||
},
|
||||
error: () => { this.nomineesLoading = false; this.nominees = []; }
|
||||
});
|
||||
}
|
||||
|
||||
/** Fetch profile image & signature for a nominee card */
|
||||
private enrichNomineeImages(nominee: any): void {
|
||||
const cid = nominee.nomineeClientId?.toString();
|
||||
if (!cid) return;
|
||||
|
||||
// Profile image
|
||||
this.clientsService.getClientProfileImage(cid).subscribe({
|
||||
next: (img: any) => {
|
||||
if (img) { nominee.profileImageUrl = this.sanitizer.bypassSecurityTrustResourceUrl(img); }
|
||||
},
|
||||
error: () => {}
|
||||
});
|
||||
|
||||
// Signature
|
||||
this.clientsService.getClientDocuments(cid).subscribe({
|
||||
next: (docs: any[]) => {
|
||||
const sigDoc = docs?.find((d: any) =>
|
||||
d.name?.toLowerCase().includes('signature') || d.description?.toLowerCase().includes('signature')
|
||||
);
|
||||
if (sigDoc) {
|
||||
this.clientsService.getClientSignatureImage(cid, sigDoc.id.toString()).subscribe({
|
||||
next: (blob: Blob) => {
|
||||
nominee.signatureUrl = this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(blob));
|
||||
},
|
||||
error: () => {}
|
||||
});
|
||||
}
|
||||
},
|
||||
error: () => {}
|
||||
});
|
||||
}
|
||||
|
||||
// ═══ Search & Link ═══
|
||||
|
||||
onSearch(text: string): void {
|
||||
this.searchText = text;
|
||||
clearTimeout(this.searchTimer);
|
||||
if (!text || text.length < 2) { this.searchResults = []; return; }
|
||||
this.searching = true;
|
||||
this.searchTimer = setTimeout(() => {
|
||||
this.clientsService.searchByText(text, 0, 8).subscribe({
|
||||
next: (data: any) => {
|
||||
this.searchResults = data?.content || [];
|
||||
this.searching = false;
|
||||
this.clientsService.enrichSearchResults(this.searchResults);
|
||||
},
|
||||
error: () => { this.searchResults = []; this.searching = false; }
|
||||
});
|
||||
}, 400);
|
||||
}
|
||||
|
||||
dismissSearch(): void {
|
||||
setTimeout(() => { this.searchResults = []; this.searching = false; }, 200);
|
||||
}
|
||||
|
||||
selectClient(client: any): void {
|
||||
this.selectedClient = client;
|
||||
this.searchResults = [];
|
||||
this.searchText = '';
|
||||
this.fetchClientDetails(client.id);
|
||||
}
|
||||
|
||||
removeSelectedClient(): void {
|
||||
this.selectedClient = null;
|
||||
this.linkedProfileImage = null;
|
||||
this.linkedSignatureImage = null;
|
||||
this.linkedKycStatus = null;
|
||||
}
|
||||
|
||||
private fetchClientDetails(clientId: number): void {
|
||||
this.fetchingDetails = true;
|
||||
this.linkedProfileImage = null;
|
||||
this.linkedSignatureImage = null;
|
||||
this.linkedKycStatus = null;
|
||||
let pending = 4;
|
||||
const done = () => { pending--; if (pending <= 0) { this.fetchingDetails = false; } };
|
||||
|
||||
this.clientsService.getClientData(clientId.toString()).subscribe({
|
||||
next: (data: any) => {
|
||||
if (data?.dateOfBirth) { this.selectedClient = { ...this.selectedClient, dateOfBirth: data.dateOfBirth }; }
|
||||
done();
|
||||
},
|
||||
error: () => done()
|
||||
});
|
||||
|
||||
this.clientsService.getClientKyc(clientId.toString()).subscribe({
|
||||
next: (kyc: any) => {
|
||||
if (kyc) {
|
||||
this.selectedClient = { ...this.selectedClient, panNumber: kyc.panNumber, aadhaarMasked: kyc.aadhaarNumberMasked };
|
||||
this.linkedKycStatus = { panVerified: kyc.panVerified || false, aadhaarVerified: kyc.aadhaarVerified || false };
|
||||
}
|
||||
done();
|
||||
},
|
||||
error: () => done()
|
||||
});
|
||||
|
||||
this.clientsService.getClientProfileImage(clientId.toString()).subscribe({
|
||||
next: (img: any) => { if (img) { this.linkedProfileImage = this.sanitizer.bypassSecurityTrustResourceUrl(img); } done(); },
|
||||
error: () => done()
|
||||
});
|
||||
|
||||
this.clientsService.getClientDocuments(clientId.toString()).subscribe({
|
||||
next: (docs: any[]) => {
|
||||
const sigDoc = docs?.find((d: any) => d.name?.toLowerCase().includes('signature') || d.description?.toLowerCase().includes('signature'));
|
||||
if (sigDoc) {
|
||||
this.clientsService.getClientSignatureImage(clientId.toString(), sigDoc.id.toString()).subscribe({
|
||||
next: (blob: Blob) => { this.linkedSignatureImage = this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(blob)); done(); },
|
||||
error: () => done()
|
||||
});
|
||||
} else { done(); }
|
||||
},
|
||||
error: () => done()
|
||||
});
|
||||
}
|
||||
|
||||
// ═══ Guardian ═══
|
||||
|
||||
onGuardianSearch(text: string): void {
|
||||
this.guardianSearchText = text;
|
||||
clearTimeout(this.guardianSearchTimer);
|
||||
if (!text || text.length < 2) { this.guardianSearchResults = []; return; }
|
||||
this.guardianSearching = true;
|
||||
this.guardianSearchTimer = setTimeout(() => {
|
||||
this.clientsService.searchByText(text, 0, 8).subscribe({
|
||||
next: (data: any) => { this.guardianSearchResults = data?.content || []; this.guardianSearching = false; this.clientsService.enrichSearchResults(this.guardianSearchResults); },
|
||||
error: () => { this.guardianSearchResults = []; this.guardianSearching = false; }
|
||||
});
|
||||
}, 400);
|
||||
}
|
||||
|
||||
selectGuardian(client: any): void {
|
||||
this.selectedGuardian = client;
|
||||
this.guardianSearchResults = [];
|
||||
this.guardianSearchText = '';
|
||||
}
|
||||
|
||||
// ═══ Save Nominee ═══
|
||||
|
||||
saveNominee(): void {
|
||||
if (!this.selectedClient || !this.nomineeRelationshipId || !this.nomineeShare) return;
|
||||
if (this.totalShare + this.nomineeShare > 100) return;
|
||||
|
||||
this.nomineeSaving = true;
|
||||
const account = this.selectedAccount;
|
||||
const accountType = this.selectedGroup?.apiType || '';
|
||||
|
||||
const payload: any = {
|
||||
nomineeClientId: this.selectedClient.id,
|
||||
accountNo: account?.accountNo,
|
||||
accountType,
|
||||
nominationType: this.nomineeNomType,
|
||||
relationshipId: this.nomineeRelationshipId,
|
||||
percentageShare: this.nomineeShare,
|
||||
nomineeIsMinor: this.nomineeIsMinor,
|
||||
remarks: this.nomineeRemarks || null,
|
||||
locale: 'en'
|
||||
};
|
||||
|
||||
if (this.nomineeIsMinor && this.selectedGuardian) {
|
||||
payload.guardianClientId = this.selectedGuardian.id;
|
||||
}
|
||||
|
||||
this.clientsService.addNominee(this.clientId, payload).subscribe({
|
||||
next: (res: any) => {
|
||||
this.nomineeSaving = false;
|
||||
// Reload nominees to get the full response including nomineeIdentifier
|
||||
this.loadNominees();
|
||||
this.resetAddForm();
|
||||
},
|
||||
error: () => { this.nomineeSaving = false; }
|
||||
});
|
||||
}
|
||||
|
||||
// ═══ View & Edit ═══
|
||||
|
||||
viewNominee(nominee: any): void {
|
||||
// Resolve the linked client ID — could be nomineeClientId or nested
|
||||
const clientId = nominee.nomineeClientId || nominee.linkedClientId || nominee.clientId;
|
||||
const displayName = nominee.nomineeDisplayName || nominee.firstName || '';
|
||||
import('../../client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component').then(m => {
|
||||
this.dialog.open(m.ClientFamilyMemberDialogComponent, {
|
||||
data: {
|
||||
context: 'View',
|
||||
member: {
|
||||
firstName: displayName,
|
||||
relationshipId: nominee.relationshipId,
|
||||
dateOfBirth: nominee.nomineeDateOfBirth || nominee.dateOfBirth,
|
||||
linkedClientId: clientId,
|
||||
linkedClientName: displayName,
|
||||
isAlive: true,
|
||||
isDependent: nominee.nomineeIsMinor,
|
||||
panNumber: nominee.panNumber || '',
|
||||
aadhaarNumber: nominee.aadhaarMasked || ''
|
||||
},
|
||||
options: { relationshipIdOptions: this.relationshipOptions }
|
||||
},
|
||||
width: '640px', maxWidth: '95vw', panelClass: 'fm-dialog-panel'
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
editNominee(nominee: any, index: number): void {
|
||||
this.editingIndex = index;
|
||||
this.editRelId = nominee.relationshipId || null;
|
||||
this.editShare = nominee.percentageShare;
|
||||
this.editNomType = nominee.nominationType || 'DA1';
|
||||
}
|
||||
|
||||
cancelEdit(): void { this.editingIndex = null; }
|
||||
|
||||
maxShareForEdit(index: number): number {
|
||||
return 100 - this.nominees.reduce((sum, n, i) => i === index ? sum : sum + (n.percentageShare || 0), 0);
|
||||
}
|
||||
|
||||
saveEdit(nominee: any, index: number): void {
|
||||
if (!this.editRelId || !this.editShare || this.editShare > this.maxShareForEdit(index)) return;
|
||||
|
||||
const payload: any = {
|
||||
relationshipId: this.editRelId,
|
||||
percentageShare: this.editShare,
|
||||
nominationType: this.editNomType,
|
||||
locale: 'en'
|
||||
};
|
||||
|
||||
this.clientsService.updateNominee(this.clientId, nominee.id.toString(), payload).subscribe({
|
||||
next: () => {
|
||||
// Reload to get updated data
|
||||
this.loadNominees();
|
||||
this.editingIndex = null;
|
||||
},
|
||||
error: () => {}
|
||||
});
|
||||
}
|
||||
|
||||
deleteNominee(nominee: any, index: number): void {
|
||||
const ref = this.dialog.open(DeleteDialogComponent, {
|
||||
data: { deleteContext: `nominee ${nominee.nomineeDisplayName || nominee.firstName || ''}` }
|
||||
});
|
||||
ref.afterClosed().subscribe((response: any) => {
|
||||
if (response?.delete) {
|
||||
this.clientsService.deleteNominee(this.clientId, nominee.id.toString()).subscribe({
|
||||
next: () => this.nominees.splice(index, 1),
|
||||
error: () => this.nominees.splice(index, 1)
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
zoomImage(imageSrc: any, title: string): void {
|
||||
this.dialog.open(ImageZoomDialogComponent, {
|
||||
data: { imageSrc, title },
|
||||
panelClass: 'image-zoom-panel',
|
||||
maxWidth: '90vw', maxHeight: '90vh'
|
||||
});
|
||||
}
|
||||
|
||||
getRelationship(member: any): string {
|
||||
if (member.relationshipName) return member.relationshipName;
|
||||
if (member.relationship) return member.relationship;
|
||||
if (typeof member.relationshipId === 'object' && member.relationshipId?.name) return member.relationshipId.name;
|
||||
const opt = this.relationshipOptions.find((r: any) => r.id === member.relationshipId);
|
||||
return opt?.name || '';
|
||||
}
|
||||
|
||||
resetAddForm(): void {
|
||||
this.showAddForm = false;
|
||||
this.selectedClient = null;
|
||||
this.linkedProfileImage = null;
|
||||
this.linkedSignatureImage = null;
|
||||
this.linkedKycStatus = null;
|
||||
this.nomineeNomType = 'DA1';
|
||||
this.nomineeRelationshipId = null;
|
||||
this.nomineeShare = null;
|
||||
this.nomineeIsMinor = false;
|
||||
this.nomineeRemarks = '';
|
||||
this.selectedGuardian = null;
|
||||
this.searchText = '';
|
||||
this.guardianSearchText = '';
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,12 @@
|
||||
</div>
|
||||
}
|
||||
@if (isPerson()) {
|
||||
@if (clientViewData.salutation) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Salutation</div>
|
||||
<div class="pd-field__value">{{ clientViewData.salutation.name }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.firstname) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.First Name' | translate }}</div>
|
||||
@@ -44,7 +50,13 @@
|
||||
@if (clientViewData.dateOfBirth) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Date of Birth' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.dateOfBirth | dateFormat }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.dateOfBirth | dateFormat:'DD-MMMM-YYYY' }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.ageYears != null) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Age</div>
|
||||
<div class="pd-field__value">{{ clientViewData.ageYears }}y {{ clientViewData.ageMonths || 0 }}m {{ clientViewData.ageDays || 0 }}d</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.gender) {
|
||||
@@ -53,11 +65,29 @@
|
||||
<div class="pd-field__value">{{ clientViewData.gender.name }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.maritalStatus) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Marital Status</div>
|
||||
<div class="pd-field__value">{{ clientViewData.maritalStatus.name }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.isMinor != null) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Minor</div>
|
||||
<div class="pd-field__value">
|
||||
<span class="pd-verify-badge" [class.pd-verify-badge--verified]="!clientViewData.isMinor" [class.pd-verify-badge--pending]="clientViewData.isMinor">
|
||||
{{ clientViewData.isMinor ? 'Yes (Age < 10)' : 'No' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Is staff' | translate }}</div>
|
||||
<div class="pd-field__value">{{
|
||||
clientViewData.isStaff ? ('labels.commons.Yes' | translate) : ('labels.commons.No' | translate)
|
||||
}}</div>
|
||||
<div class="pd-field__value">
|
||||
<span class="pd-verify-badge" [class.pd-verify-badge--verified]="clientViewData.isStaff" [class.pd-verify-badge--pending]="!clientViewData.isStaff">
|
||||
{{ clientViewData.isStaff ? 'Yes' : 'No' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (isLegalEntity()) {
|
||||
@@ -70,16 +100,20 @@
|
||||
@if (clientViewData.dateOfBirth) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Incorporation Date' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.dateOfBirth | dateFormat }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.dateOfBirth | dateFormat:'DD-MM-YYYY' }}</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (clientViewData.legalForm) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Legal Form' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.legalForm.value }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.legalForm.id === 1 ? 'Individual' : 'Non-Individual' }}</div>
|
||||
</div>
|
||||
}
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Nationality</div>
|
||||
<div class="pd-field__value">{{ clientViewData.nationalityId || clientViewData.nationality || 'Indian' }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -100,10 +134,6 @@
|
||||
<div class="pd-field__label">{{ 'labels.inputs.CIF Number' | translate }}</div>
|
||||
<div class="pd-field__value monospace">{{ clientViewData.cifNumber || '-' }}</div>
|
||||
</div>
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.External Id' | translate }}</div>
|
||||
<div class="pd-field__value monospace">{{ clientViewData.externalId || '-' }}</div>
|
||||
</div>
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Office' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.officeName || '-' }}</div>
|
||||
@@ -112,10 +142,10 @@
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Staff' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.staffName || ('labels.inputs.Unassigned' | translate) }}</div>
|
||||
</div>
|
||||
@if (clientViewData.savingsAccountId) {
|
||||
@if (clientViewData.savingsAccountNo) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Default Savings Account' | translate }}</div>
|
||||
<div class="pd-field__value monospace">{{ clientViewData.savingsAccountId }}</div>
|
||||
<div class="pd-field__value monospace">{{ clientViewData.savingsAccountNo }}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -146,7 +176,7 @@
|
||||
@if (clientViewData.clientNonPersonDetails.incorpValidityTillDate) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Incorporation Validity Till Date' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.clientNonPersonDetails.incorpValidityTillDate | dateFormat }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.clientNonPersonDetails.incorpValidityTillDate | dateFormat:'DD-MM-YYYY' }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.clientNonPersonDetails.incorpNumber) {
|
||||
@@ -166,11 +196,11 @@
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Contact Information -->
|
||||
<!-- Contact Details -->
|
||||
<div class="pd-section">
|
||||
<div class="pd-section__header">
|
||||
<i class="fa fa-phone"></i>
|
||||
<h3>{{ 'labels.heading.Contact Information' | translate }}</h3>
|
||||
<h3>Contact Details</h3>
|
||||
</div>
|
||||
<div class="pd-section__body">
|
||||
<div class="pd-grid">
|
||||
@@ -180,6 +210,14 @@
|
||||
<span class="pd-contact-icon"><i class="fa fa-mobile"></i> {{ clientViewData.mobileNo || '-' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@if (clientViewData.alternateMobileNo) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Alternate Mobile No</div>
|
||||
<div class="pd-field__value">
|
||||
<span class="pd-contact-icon"><i class="fa fa-mobile"></i> {{ clientViewData.alternateMobileNo }}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Email' | translate }}</div>
|
||||
<div class="pd-field__value">
|
||||
@@ -190,14 +228,125 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Classification -->
|
||||
<!-- DIP & Sanctions Verification -->
|
||||
<div class="pd-section">
|
||||
<div class="pd-section__header">
|
||||
<i class="fa fa-tag"></i>
|
||||
<h3>{{ 'labels.heading.Classification' | translate }}</h3>
|
||||
<i class="fa fa-shield"></i>
|
||||
<h3>DIP & Sanctions Verification</h3>
|
||||
</div>
|
||||
<div class="pd-section__body">
|
||||
<div class="pd-grid">
|
||||
<!-- Primary Mobile DIP -->
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Mobile DIP Status</div>
|
||||
<div class="pd-field__value">
|
||||
@if (clientViewData.mobileVerified === true || clientViewData.mobileVerified === 'true' || clientViewData.mobileVerified === 1
|
||||
|| clientViewData.dipVerified === true || clientViewData.dipVerified === 'true' || clientViewData.dipVerified === 1) {
|
||||
<span class="pd-verify-badge pd-verify-badge--verified">
|
||||
<i class="fa fa-check-circle"></i> Verified
|
||||
</span>
|
||||
} @else {
|
||||
<span class="pd-verify-badge pd-verify-badge--pending">
|
||||
<i class="fa fa-clock-o"></i> Pending
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (clientViewData.dipTimestamp) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">DIP Verified On</div>
|
||||
<div class="pd-field__value monospace">{{ formatDateTime(clientViewData.dipTimestamp) }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.dipLookupLogId) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">DIP Lookup Log ID</div>
|
||||
<div class="pd-field__value monospace">{{ clientViewData.dipLookupLogId }}</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Alternate Mobile DIP -->
|
||||
@if (clientViewData.alternateMobileNo) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Alternate Mobile DIP Status</div>
|
||||
<div class="pd-field__value">
|
||||
@if (clientViewData.alternateDipVerified === true || clientViewData.alternateDipVerified === 'true' || clientViewData.alternateDipVerified === 1) {
|
||||
<span class="pd-verify-badge pd-verify-badge--verified">
|
||||
<i class="fa fa-check-circle"></i> Verified
|
||||
</span>
|
||||
} @else {
|
||||
<span class="pd-verify-badge pd-verify-badge--pending">
|
||||
<i class="fa fa-clock-o"></i> Pending
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (clientViewData.alternateDipTimestamp) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Alternate DIP Verified On</div>
|
||||
<div class="pd-field__value monospace">{{ formatDateTime(clientViewData.alternateDipTimestamp) }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.alternateDipLookupLogId) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Alternate DIP Log ID</div>
|
||||
<div class="pd-field__value monospace">{{ clientViewData.alternateDipLookupLogId }}</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Sanctions / Name Screening -->
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Name Screening (Sanctions)</div>
|
||||
<div class="pd-field__value">
|
||||
@if (clientViewData.nameVerified === true || clientViewData.nameVerified === 'true' || clientViewData.nameVerified === 1) {
|
||||
<span class="pd-verify-badge pd-verify-badge--verified"><i class="fa fa-check-circle"></i> Clear</span>
|
||||
} @else if (clientViewData.nameVerified === false || clientViewData.nameVerified === 'false' || clientViewData.nameVerified === 0) {
|
||||
<span class="pd-verify-badge pd-verify-badge--danger"><i class="fa fa-exclamation-triangle"></i> Match Found</span>
|
||||
} @else {
|
||||
<span class="pd-verify-badge pd-verify-badge--pending"><i class="fa fa-clock-o"></i> Pending</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (clientViewData.screeningLogId) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Screening Log ID</div>
|
||||
<div class="pd-field__value monospace">{{ clientViewData.screeningLogId }}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Financial & Classification (Individual only) -->
|
||||
@if (clientViewData?.legalForm?.id !== 2) {
|
||||
<div class="pd-section">
|
||||
<div class="pd-section__header">
|
||||
<i class="fa fa-chart-line"></i>
|
||||
<h3>Financial & Classification</h3>
|
||||
</div>
|
||||
<div class="pd-section__body">
|
||||
<div class="pd-grid">
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Monthly Income</div>
|
||||
<div class="pd-field__value">
|
||||
@if (clientViewData.monthlyIncome != null && clientViewData.monthlyIncome !== '') {
|
||||
<span class="monospace">₹ {{ clientViewData.monthlyIncome | number }}</span>
|
||||
} @else {
|
||||
-
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Annual Turnover (Last FY)</div>
|
||||
<div class="pd-field__value">
|
||||
@if (clientViewData.annualTurnover != null && clientViewData.annualTurnover !== '') {
|
||||
<span class="monospace">₹ {{ clientViewData.annualTurnover | number }}</span>
|
||||
} @else {
|
||||
-
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (clientViewData.clientType) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Client Type' | translate }}</div>
|
||||
@@ -222,14 +371,39 @@
|
||||
<div class="pd-field__value">{{ clientViewData.occupation.name }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (!clientViewData.clientType && !clientViewData.clientClassification) {
|
||||
<div class="pd-field full-width">
|
||||
<div class="pd-field__value no-data">{{ 'labels.commons.No classification data available' | translate }}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Guardian Details (Minor) -->
|
||||
@if (clientViewData.isMinor && clientViewData.guardianClientId) {
|
||||
<div class="pd-section">
|
||||
<div class="pd-section__header">
|
||||
<i class="fa fa-user-shield"></i>
|
||||
<h3>Guardian Details</h3>
|
||||
<span class="pd-section__badge pd-section__badge--minor">Minor</span>
|
||||
</div>
|
||||
<div class="pd-section__body">
|
||||
<div class="pd-grid">
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Guardian Name</div>
|
||||
<div class="pd-field__value">{{ clientViewData.guardianClientDisplayName || '-' }}</div>
|
||||
</div>
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Guardian Client ID</div>
|
||||
<div class="pd-field__value monospace">{{ clientViewData.guardianClientId }}</div>
|
||||
</div>
|
||||
@if (clientViewData.guardianRelation) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Relationship</div>
|
||||
<div class="pd-field__value">{{ clientViewData.guardianRelation }}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Group Membership -->
|
||||
@if (clientViewData.groups && clientViewData.groups.length > 0) {
|
||||
@@ -262,19 +436,19 @@
|
||||
@if (clientViewData.timeline?.submittedOnDate) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Submitted On' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.timeline.submittedOnDate | dateFormat }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.timeline.submittedOnDate | dateFormat:'DD-MM-YYYY' }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.activationDate) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Activation Date' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.activationDate | dateFormat }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.activationDate | dateFormat:'DD-MM-YYYY' }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.timeline?.closedOnDate) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Closed Date' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.timeline.closedOnDate | dateFormat }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.timeline.closedOnDate | dateFormat:'DD-MM-YYYY' }}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -59,6 +59,20 @@ $teal: #00897b;
|
||||
}
|
||||
}
|
||||
|
||||
.pd-section__badge {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 2px 10px;
|
||||
border-radius: 10px;
|
||||
margin-left: auto;
|
||||
letter-spacing: 0.3px;
|
||||
|
||||
&--minor {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
.pd-section__body {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
@@ -205,3 +219,34 @@ $teal: #00897b;
|
||||
color: $gray-500;
|
||||
}
|
||||
}
|
||||
|
||||
// DIP Verification badges
|
||||
.pd-verify-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
padding: 3px 12px;
|
||||
border-radius: 12px;
|
||||
|
||||
i { font-size: 12px; }
|
||||
|
||||
&--verified {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
border: 1px solid #a7f3d0;
|
||||
}
|
||||
|
||||
&--pending {
|
||||
background: #fffbeb;
|
||||
color: #d97706;
|
||||
border: 1px solid #fde68a;
|
||||
}
|
||||
|
||||
&--danger {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
/** Custom Imports */
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
|
||||
import { DecimalPipe } from '@angular/common';
|
||||
import { LegalFormId } from 'app/clients/models/legal-form.enum';
|
||||
|
||||
/** Interfaces */
|
||||
@@ -56,7 +57,8 @@ interface ClientViewData {
|
||||
standalone: true,
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
DateFormatPipe
|
||||
DateFormatPipe,
|
||||
DecimalPipe
|
||||
]
|
||||
})
|
||||
export class PersonalDataTabComponent {
|
||||
@@ -84,4 +86,24 @@ export class PersonalDataTabComponent {
|
||||
isLegalEntity(): boolean {
|
||||
return this.clientViewData?.legalForm?.id === LegalFormId.ENTITY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format ISO timestamp to DD-MM-YYYY HH:MM:SS
|
||||
*/
|
||||
formatDateTime(value: string): string {
|
||||
if (!value) return '-';
|
||||
try {
|
||||
const d = new Date(value);
|
||||
if (isNaN(d.getTime())) return value;
|
||||
const dd = String(d.getDate()).padStart(2, '0');
|
||||
const mm = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const yyyy = d.getFullYear();
|
||||
const hh = String(d.getHours()).padStart(2, '0');
|
||||
const min = String(d.getMinutes()).padStart(2, '0');
|
||||
const ss = String(d.getSeconds()).padStart(2, '0');
|
||||
return `${dd}-${mm}-${yyyy} ${hh}:${min}:${ss}`;
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user