merging code

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Nitesh Yadav
2026-04-20 14:26:25 +05:30
co-authored by Claude Sonnet 4.6
322 changed files with 28779 additions and 4976 deletions
@@ -42,7 +42,9 @@
<mat-select-trigger>
@if (createClientForm.get('legalFormId')?.value) {
<span class="legal-form-option">
<mat-icon class="legal-form-option-icon">{{ createClientForm.get('legalFormId')?.value === 1 ? 'person' : 'business' }}</mat-icon>
<mat-icon class="legal-form-option-icon">{{
createClientForm.get('legalFormId')?.value === 1 ? 'person' : 'business'
}}</mat-icon>
{{ getSelectedLegalFormLabel() }}
</span>
}
@@ -67,12 +69,12 @@
@if (getSelectedNationality().flagType === 'india') {
<span class="nat-flag">
<svg viewBox="0 0 36 24" class="nat-flag-svg">
<rect width="36" height="8" fill="#FF9933"/>
<rect y="8" width="36" height="8" fill="#FFFFFF"/>
<rect y="16" width="36" height="8" fill="#138808"/>
<circle cx="18" cy="12" r="3" fill="#000080"/>
<circle cx="18" cy="12" r="2.5" fill="#FFFFFF"/>
<circle cx="18" cy="12" r="0.7" fill="#000080"/>
<rect width="36" height="8" fill="#FF9933" />
<rect y="8" width="36" height="8" fill="#FFFFFF" />
<rect y="16" width="36" height="8" fill="#138808" />
<circle cx="18" cy="12" r="3" fill="#000080" />
<circle cx="18" cy="12" r="2.5" fill="#FFFFFF" />
<circle cx="18" cy="12" r="0.7" fill="#000080" />
</svg>
</span>
} @else {
@@ -88,12 +90,12 @@
@if (nat.flagType === 'india') {
<span class="nat-flag">
<svg viewBox="0 0 36 24" class="nat-flag-svg">
<rect width="36" height="8" fill="#FF9933"/>
<rect y="8" width="36" height="8" fill="#FFFFFF"/>
<rect y="16" width="36" height="8" fill="#138808"/>
<circle cx="18" cy="12" r="3" fill="#000080"/>
<circle cx="18" cy="12" r="2.5" fill="#FFFFFF"/>
<circle cx="18" cy="12" r="0.7" fill="#000080"/>
<rect width="36" height="8" fill="#FF9933" />
<rect y="8" width="36" height="8" fill="#FFFFFF" />
<rect y="16" width="36" height="8" fill="#138808" />
<circle cx="18" cy="12" r="3" fill="#000080" />
<circle cx="18" cy="12" r="2.5" fill="#FFFFFF" />
<circle cx="18" cy="12" r="0.7" fill="#000080" />
</svg>
</span>
} @else {
@@ -129,7 +131,10 @@
@if (createClientForm.get('fullname')) {
<mat-form-field appearance="outline" class="form-field full-width">
<mat-label>
{{ 'labels.inputs.' + getDateLabel(createClientForm.get('legalFormId')?.value, ['Name', 'Entity Name']) | translate }}
{{
'labels.inputs.' + getDateLabel(createClientForm.get('legalFormId')?.value, ['Name', 'Entity Name'])
| translate
}}
</mat-label>
<input matInput required formControlName="fullname" />
@if (createClientForm.controls.fullname.hasError('required')) {
@@ -150,30 +155,39 @@
</div>
<div class="verify-inline">
@if (!sanctionsVerified && !sanctionsBlocked) {
<button mat-stroked-button color="primary" type="button" class="verify-name-btn"
<button
mat-stroked-button
color="primary"
type="button"
class="verify-name-btn"
[disabled]="readOnly"
(click)="verifySanctions()">
(click)="verifySanctions()"
>
<fa-icon icon="shield-alt" class="verify-name-icon"></fa-icon>
Verify Entity
</button>
}
@if (sanctionsVerified) {
<span class="sanctions-citation">RBI KYC Master Direction (2016) — Section 51 &amp; PMLA 2002</span>
<span class="verify-status verified">
<fa-icon icon="check-circle"></fa-icon> Verified
</span>
<mat-icon class="sanctions-info-icon"
<span class="verify-status verified"> <fa-icon icon="check-circle"></fa-icon> Verified </span>
<mat-icon
class="sanctions-info-icon"
matTooltip="All customer names must be verified against the UN Security Council Consolidated List before account opening and periodically thereafter. Positive matches must be escalated to the Principal Officer immediately."
matTooltipClass="sanctions-tooltip">info</mat-icon>
matTooltipClass="sanctions-tooltip"
>info</mat-icon
>
}
@if (sanctionsBlocked) {
<span class="sanctions-citation">RBI KYC Master Direction (2016) — Section 51 &amp; PMLA 2002</span>
<span class="verify-status blocked">
<fa-icon icon="exclamation-triangle"></fa-icon> Sanctions Match
</span>
<mat-icon class="sanctions-info-icon blocked-info"
<mat-icon
class="sanctions-info-icon blocked-info"
matTooltip="All customer names must be verified against the UN Security Council Consolidated List before account opening and periodically thereafter. Positive matches must be escalated to the Principal Officer immediately."
matTooltipClass="sanctions-tooltip">info</mat-icon>
matTooltipClass="sanctions-tooltip"
>info</mat-icon
>
}
</div>
</div>
@@ -181,7 +195,9 @@
}
<!-- Person: Salutation + First / Middle / Last Name -->
@if (createClientForm.get('firstname') || createClientForm.get('middlename') || createClientForm.get('lastname')) {
@if (
createClientForm.get('firstname') || createClientForm.get('middlename') || createClientForm.get('lastname')
) {
<div class="name-fields-row">
@if (createClientForm.get('salutationId') && salutationOptions?.length > 0) {
<mat-form-field appearance="outline" class="name-field salutation-field-wide">
@@ -213,7 +229,10 @@
<mat-label>{{ 'labels.inputs.Middle Name' | translate }}</mat-label>
<span matPrefix class="field-prefix-icon"><mat-icon>person_outline</mat-icon></span>
<input matInput formControlName="middlename" />
@if (createClientForm.controls.middlename?.hasError('pattern') && createClientForm.controls.middlename?.touched) {
@if (
createClientForm.controls.middlename?.hasError('pattern') &&
createClientForm.controls.middlename?.touched
) {
<mat-error>Only letters, spaces, dots and hyphens allowed</mat-error>
}
</mat-form-field>
@@ -243,30 +262,39 @@
</div>
<div class="verify-inline">
@if (!sanctionsVerified && !sanctionsBlocked) {
<button mat-stroked-button color="primary" type="button" class="verify-name-btn"
<button
mat-stroked-button
color="primary"
type="button"
class="verify-name-btn"
[disabled]="readOnly"
(click)="verifySanctions()">
(click)="verifySanctions()"
>
<fa-icon icon="shield-alt" class="verify-name-icon"></fa-icon>
Verify Client
</button>
}
@if (sanctionsVerified) {
<span class="sanctions-citation">RBI KYC Master Direction (2016) — Section 51 &amp; PMLA 2002</span>
<span class="verify-status verified">
<fa-icon icon="check-circle"></fa-icon> Verified
</span>
<mat-icon class="sanctions-info-icon"
<span class="verify-status verified"> <fa-icon icon="check-circle"></fa-icon> Verified </span>
<mat-icon
class="sanctions-info-icon"
matTooltip="All customer names must be verified against the UN Security Council Consolidated List before account opening and periodically thereafter. Positive matches must be escalated to the Principal Officer immediately."
matTooltipClass="sanctions-tooltip">info</mat-icon>
matTooltipClass="sanctions-tooltip"
>info</mat-icon
>
}
@if (sanctionsBlocked) {
<span class="sanctions-citation">RBI KYC Master Direction (2016) — Section 51 &amp; PMLA 2002</span>
<span class="verify-status blocked">
<fa-icon icon="exclamation-triangle"></fa-icon> Sanctions Match
</span>
<mat-icon class="sanctions-info-icon blocked-info"
<mat-icon
class="sanctions-info-icon blocked-info"
matTooltip="All customer names must be verified against the UN Security Council Consolidated List before account opening and periodically thereafter. Positive matches must be escalated to the Principal Officer immediately."
matTooltipClass="sanctions-tooltip">info</mat-icon>
matTooltipClass="sanctions-tooltip"
>info</mat-icon
>
}
</div>
</div>
@@ -278,10 +306,18 @@
<mat-form-field appearance="outline" class="form-field" (click)="dateOfBirthDatePicker.open()">
<mat-label>Date of Birth</mat-label>
<span matPrefix class="field-prefix-icon"><mat-icon>calendar_today</mat-icon></span>
<input matInput required [max]="maxDate" [matDatepicker]="dateOfBirthDatePicker" formControlName="dateOfBirth" />
<input
matInput
required
[max]="maxDate"
[matDatepicker]="dateOfBirthDatePicker"
formControlName="dateOfBirth"
/>
<mat-datepicker-toggle matSuffix [for]="dateOfBirthDatePicker"></mat-datepicker-toggle>
<mat-datepicker #dateOfBirthDatePicker></mat-datepicker>
@if (createClientForm.get('dateOfBirth')?.hasError('required') && createClientForm.get('dateOfBirth')?.touched) {
@if (
createClientForm.get('dateOfBirth')?.hasError('required') && createClientForm.get('dateOfBirth')?.touched
) {
<mat-error>Date of Birth is <strong>required</strong></mat-error>
}
</mat-form-field>
@@ -324,7 +360,6 @@
}
</div>
}
</div>
<!-- Minor / Guardian Mapping (mandatory if age < 10) -->
@@ -341,9 +376,15 @@
</div>
<div class="guardian-section__header-right">
<div class="guardian-section__age-chips">
<span class="guardian-age-chip"><strong>{{ ageBreakdown.years }}</strong> yrs</span>
<span class="guardian-age-chip"><strong>{{ ageBreakdown.months }}</strong> mos</span>
<span class="guardian-age-chip"><strong>{{ ageBreakdown.days }}</strong> days</span>
<span class="guardian-age-chip"
><strong>{{ ageBreakdown.years }}</strong> yrs</span
>
<span class="guardian-age-chip"
><strong>{{ ageBreakdown.months }}</strong> mos</span
>
<span class="guardian-age-chip"
><strong>{{ ageBreakdown.days }}</strong> days</span
>
<span class="guardian-minor-badge">MINOR</span>
</div>
@if (!selectedGuardian && !guardianLinking) {
@@ -357,13 +398,14 @@
<!-- Body -->
<div class="guardian-section__body" formGroupName="guardianDetails">
<!-- Empty state: no guardian linked, not linking -->
@if (!selectedGuardian && !guardianLinking) {
<div class="guardian-empty guardian-empty--required">
<mat-icon class="guardian-empty__icon">person_off</mat-icon>
<div class="guardian-empty__content">
<span class="guardian-empty__text">No guardian linked yet. Click <strong>"Link Guardian"</strong> to search and add.</span>
<span class="guardian-empty__text"
>No guardian linked yet. Click <strong>"Link Guardian"</strong> to search and add.</span
>
<span class="guardian-empty__required">Guardian is mandatory for minor accounts</span>
</div>
</div>
@@ -377,22 +419,33 @@
<mat-select required formControlName="guardianRelation">
<mat-option value="FATHER"><mat-icon class="icon-option-icon">man</mat-icon> Father</mat-option>
<mat-option value="MOTHER"><mat-icon class="icon-option-icon">woman</mat-icon> Mother</mat-option>
<mat-option value="LEGAL_GUARDIAN"><mat-icon class="icon-option-icon">gavel</mat-icon> Legal Guardian</mat-option>
<mat-option value="GRANDPARENT"><mat-icon class="icon-option-icon">elderly</mat-icon> Grandparent</mat-option>
<mat-option value="LEGAL_GUARDIAN"
><mat-icon class="icon-option-icon">gavel</mat-icon> Legal Guardian</mat-option
>
<mat-option value="GRANDPARENT"
><mat-icon class="icon-option-icon">elderly</mat-icon> Grandparent</mat-option
>
<mat-option value="OTHER"><mat-icon class="icon-option-icon">group</mat-icon> Other</mat-option>
</mat-select>
</mat-form-field>
<div class="guardian-search-wrapper guardian-search-col">
<mat-form-field appearance="outline" class="guardian-search-field"
[class.guardian-search-disabled]="!createClientForm.get('guardianDetails.guardianRelation')?.value">
<mat-form-field
appearance="outline"
class="guardian-search-field"
[class.guardian-search-disabled]="!createClientForm.get('guardianDetails.guardianRelation')?.value"
>
<mat-label>Search Guardian by Name or Client ID</mat-label>
<span matPrefix class="field-prefix-icon"><mat-icon>person_search</mat-icon></span>
<input matInput [value]="guardianSearchText"
<input
matInput
[value]="guardianSearchText"
(input)="onGuardianSearch($any($event.target).value)"
(blur)="dismissGuardianSearch()"
[disabled]="!createClientForm.get('guardianDetails.guardianRelation')?.value"
placeholder="Select relationship first, then search..." autocomplete="off" />
placeholder="Select relationship first, then search..."
autocomplete="off"
/>
</mat-form-field>
@if (guardianSearching) {
@@ -413,11 +466,17 @@
<div class="guardian-result-item__info">
<span class="guardian-result-item__name">{{ result.displayName }}</span>
<span class="guardian-result-item__meta">
ID: {{ result.id }} &middot; A/C: {{ result.accountNumber }} &middot; {{ result.officeName }}@if (result.dateOfBirth) { &middot; DOB: {{ result.dateOfBirth | dateFormat }} }@if (result.fatherSpouseName) { &middot; {{ result.fatherSpouseName }} }
ID: {{ result.id }} &middot; A/C: {{ result.accountNumber }} &middot;
{{ result.officeName }}
@if (result.dateOfBirth) {
&middot; DOB: {{ result.dateOfBirth | dateFormat }}
}
@if (result.fatherSpouseName) {
&middot; {{ result.fatherSpouseName }}
}
</span>
</div>
<span class="guardian-result-item__status"
[class.active]="result.status?.value === 'Active'">
<span class="guardian-result-item__status" [class.active]="result.status?.value === 'Active'">
{{ result.status?.value }}
</span>
</div>
@@ -429,7 +488,13 @@
}
</div>
<button mat-icon-button type="button" class="guardian-cancel-link" (click)="guardianLinking = false" matTooltip="Cancel">
<button
mat-icon-button
type="button"
class="guardian-cancel-link"
(click)="guardianLinking = false"
matTooltip="Cancel"
>
<mat-icon>close</mat-icon>
</button>
</div>
@@ -453,8 +518,12 @@
<span class="guardian-row__col guardian-row__col--photo">
<div class="guardian-row__avatar" [class.has-photo]="selectedGuardian.profileImageUrl">
@if (selectedGuardian.profileImageUrl) {
<img [src]="selectedGuardian.profileImageUrl" alt="Photo" class="avatar-photo guardian-row__zoomable"
(click)="zoomGuardianImage(selectedGuardian.profileImageUrl, 'Guardian Photo')" />
<img
[src]="selectedGuardian.profileImageUrl"
alt="Photo"
class="avatar-photo guardian-row__zoomable"
(click)="zoomGuardianImage(selectedGuardian.profileImageUrl, 'Guardian Photo')"
/>
} @else {
<fa-icon icon="user-shield"></fa-icon>
}
@@ -470,26 +539,40 @@
</div>
</span>
<span class="guardian-row__col guardian-row__col--rel">
<span class="guardian-row__rel-badge">{{ createClientForm.get('guardianDetails.guardianRelation')?.value || '-' }}</span>
<span class="guardian-row__rel-badge">{{
createClientForm.get('guardianDetails.guardianRelation')?.value || '-'
}}</span>
</span>
<span class="guardian-row__col guardian-row__col--cif">
<span class="guardian-row__mono">{{ selectedGuardian.accountNo || selectedGuardian.accountNumber || '-' }}</span>
<span class="guardian-row__mono">{{
selectedGuardian.accountNo || selectedGuardian.accountNumber || '-'
}}</span>
</span>
<span class="guardian-row__col guardian-row__col--status">
<span class="guardian-row__status-badge"
[class.guardian-row__status-badge--active]="selectedGuardian.status?.value === 'Active'">
<span
class="guardian-row__status-badge"
[class.guardian-row__status-badge--active]="selectedGuardian.status?.value === 'Active'"
>
{{ selectedGuardian.status?.value || '-' }}
</span>
</span>
<span class="guardian-row__col guardian-row__col--actions">
<button mat-icon-button type="button" class="guardian-action-btn"
<button
mat-icon-button
type="button"
class="guardian-action-btn"
(click)="guardianExpanded = !guardianExpanded"
[matTooltip]="guardianExpanded ? 'Collapse' : 'View Details'">
[matTooltip]="guardianExpanded ? 'Collapse' : 'View Details'"
>
<mat-icon>{{ guardianExpanded ? 'expand_less' : 'visibility' }}</mat-icon>
</button>
<button mat-icon-button type="button" class="guardian-action-btn guardian-action-btn--delete"
<button
mat-icon-button
type="button"
class="guardian-action-btn guardian-action-btn--delete"
(click)="removeGuardian()"
matTooltip="Delete">
matTooltip="Delete"
>
<mat-icon>delete</mat-icon>
</button>
</span>
@@ -506,16 +589,22 @@
@if (selectedGuardian.panNumber) {
<div class="guardian-expand__item">
<span class="guardian-expand__label">PAN</span>
<span class="guardian-expand__value">{{ selectedGuardian.panNumber }}
@if (selectedGuardian.panVerified) { <mat-icon class="verify-tick-sm">check_circle</mat-icon> }
<span class="guardian-expand__value"
>{{ selectedGuardian.panNumber }}
@if (selectedGuardian.panVerified) {
<mat-icon class="verify-tick-sm">check_circle</mat-icon>
}
</span>
</div>
}
@if (selectedGuardian.aadhaarMasked) {
<div class="guardian-expand__item">
<span class="guardian-expand__label">Aadhaar</span>
<span class="guardian-expand__value">{{ selectedGuardian.aadhaarMasked }}
@if (selectedGuardian.aadhaarVerified) { <mat-icon class="verify-tick-sm">check_circle</mat-icon> }
<span class="guardian-expand__value"
>{{ selectedGuardian.aadhaarMasked }}
@if (selectedGuardian.aadhaarVerified) {
<mat-icon class="verify-tick-sm">check_circle</mat-icon>
}
</span>
</div>
}
@@ -535,8 +624,12 @@
@if (selectedGuardian.signatureUrl) {
<div class="guardian-expand__sig">
<span class="guardian-expand__label">Specimen Signature</span>
<img [src]="selectedGuardian.signatureUrl" alt="Signature" class="guardian-expand__sig-img guardian-row__zoomable"
(click)="zoomGuardianImage(selectedGuardian.signatureUrl, 'Specimen Signature')" />
<img
[src]="selectedGuardian.signatureUrl"
alt="Signature"
class="guardian-expand__sig-img guardian-row__zoomable"
(click)="zoomGuardianImage(selectedGuardian.signatureUrl, 'Specimen Signature')"
/>
</div>
}
</div>
@@ -549,7 +642,7 @@
<!-- Non-Individual Details -->
@if (createClientForm.get('clientNonPersonDetails')) {
<div class="form-grid" formGroupName="clientNonPersonDetails" style="margin-top: 16px;">
<div class="form-grid" formGroupName="clientNonPersonDetails" style="margin-top: 16px">
<mat-form-field appearance="outline" class="form-field">
<mat-label>Constitution Type</mat-label>
<mat-select formControlName="constitutionId" required>
@@ -607,7 +700,10 @@
</h4>
<div class="minor-info-bar">
<fa-icon icon="info-circle" class="minor-info-icon"></fa-icon>
<span>Select registered clients as authorized signatories — as per Board Resolution / Partnership Deed. Max 10.</span>
<span
>Select registered clients as authorized signatories — as per Board Resolution / Partnership Deed. Max
10.</span
>
</div>
<!-- Signatory List Table -->
@@ -619,6 +715,7 @@
<span class="sig-table__col sig-table__col--name">Name</span>
<span class="sig-table__col sig-table__col--id">Client ID</span>
<span class="sig-table__col sig-table__col--pan">PAN</span>
<span class="sig-table__col sig-table__col--din">DIN</span>
<span class="sig-table__col sig-table__col--status">Status</span>
<span class="sig-table__col sig-table__col--actions">Actions</span>
</div>
@@ -637,16 +734,45 @@
<span class="sig-table__col sig-table__col--name">
<strong>{{ sig.displayName }}</strong>
@if (sig._loading) {
<mat-progress-spinner diameter="14" mode="indeterminate" class="sig-inline-spinner"></mat-progress-spinner>
<mat-progress-spinner
diameter="14"
mode="indeterminate"
class="sig-inline-spinner"
></mat-progress-spinner>
}
</span>
<span class="sig-table__col sig-table__col--id">{{ sig.cifNumber || sig.id }}</span>
<span class="sig-table__col sig-table__col--pan">
{{ sig.panNumber || '-' }}
@if (sig.panVerified) { <mat-icon class="verify-tick-sm">check_circle</mat-icon> }
@if (sig.panVerified) {
<mat-icon class="verify-tick-sm">check_circle</mat-icon>
}
</span>
<span class="sig-table__col sig-table__col--din">
@if (sig.dinNumber) {
{{ sig.dinNumber }}
@if (sig.dinVerified) {
<mat-icon class="verify-tick-sm">check_circle</mat-icon>
}
@if (!sig.dinVerified) {
<mat-icon class="verify-cross-sm">cancel</mat-icon>
}
} @else {
<button
mat-stroked-button
class="din-verify-btn"
type="button"
matTooltip="Verify DIN"
(click)="openDinVerifyDialog(sig)"
>
<mat-icon class="din-verify-icon">badge</mat-icon> Verify DIN
</button>
}
</span>
<span class="sig-table__col sig-table__col--status">
<span class="sig-status-chip" [class.active]="sig.status?.value === 'Active'">{{ sig.status?.value || '-' }}</span>
<span class="sig-status-chip" [class.active]="sig.status?.value === 'Active'">{{
sig.status?.value || '-'
}}</span>
</span>
<span class="sig-table__col sig-table__col--actions">
<button mat-icon-button type="button" matTooltip="View Details" (click)="viewSignatoryDetails(sig)">
@@ -668,10 +794,14 @@
<mat-form-field appearance="outline" class="guardian-search-field">
<mat-label>Add Signatory — Search by Name or Client ID</mat-label>
<span matPrefix class="field-prefix-icon"><mat-icon>person_add</mat-icon></span>
<input matInput [value]="signatorySearchText"
<input
matInput
[value]="signatorySearchText"
(input)="onSignatorySearch($any($event.target).value)"
(blur)="dismissSignatorySearch()"
placeholder="Type name or client ID..." autocomplete="off" />
placeholder="Type name or client ID..."
autocomplete="off"
/>
</mat-form-field>
@if (signatorySearching) {
<div class="guardian-results">
@@ -688,9 +818,20 @@
<div class="guardian-result-item__avatar"><fa-icon icon="user"></fa-icon></div>
<div class="guardian-result-item__info">
<span class="guardian-result-item__name">{{ result.displayName }}</span>
<span class="guardian-result-item__meta">ID: {{ result.id }} &middot; A/C: {{ result.accountNumber }} &middot; {{ result.officeName }}@if (result.dateOfBirth) { &middot; DOB: {{ result.dateOfBirth | dateFormat }} }@if (result.fatherSpouseName) { &middot; {{ result.fatherSpouseName }} }</span>
<span class="guardian-result-item__meta"
>ID: {{ result.id }} &middot; A/C: {{ result.accountNumber }} &middot;
{{ result.officeName }}
@if (result.dateOfBirth) {
&middot; DOB: {{ result.dateOfBirth | dateFormat }}
}
@if (result.fatherSpouseName) {
&middot; {{ result.fatherSpouseName }}
}
</span>
</div>
<span class="guardian-result-item__status" [class.active]="result.status?.value === 'Active'">{{ result.status?.value }}</span>
<span class="guardian-result-item__status" [class.active]="result.status?.value === 'Active'">{{
result.status?.value
}}</span>
</div>
}
</div>
@@ -716,7 +857,6 @@
<textarea matInput formControlName="remarks" cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
</mat-form-field>
</div>
}
</div>
@@ -725,9 +865,12 @@
<h4 class="section-title">
<fa-icon icon="mobile-alt" class="section-icon"></fa-icon>
Contact Details
<mat-icon class="dip-info-icon"
<mat-icon
class="dip-info-icon"
matTooltip="Digital Intelligence Platform (DIP) refers to the fraud-monitoring and intelligence ecosystem created by the Department of Telecommunications (DoT)"
matTooltipPosition="above">info</mat-icon>
matTooltipPosition="above"
>info</mat-icon
>
</h4>
<div class="form-grid">
@@ -735,21 +878,35 @@
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'labels.inputs.Mobile No' | translate }}</mat-label>
<span matPrefix class="field-prefix-icon"><mat-icon>phone_android</mat-icon></span>
<input matInput required type="text" formControlName="mobileNo" placeholder="e.g. 9876543210" maxlength="10"
<input
matInput
required
type="text"
formControlName="mobileNo"
placeholder="e.g. 9876543210"
maxlength="10"
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()"
(paste)="$event.preventDefault()" />
(paste)="$event.preventDefault()"
/>
@if (createClientForm.get('mobileNo')?.hasError('required') && createClientForm.get('mobileNo')?.touched) {
<mat-error>Mobile number is <strong>required</strong></mat-error>
} @else if (createClientForm.get('mobileNo')?.hasError('pattern') && createClientForm.get('mobileNo')?.touched) {
} @else if (
createClientForm.get('mobileNo')?.hasError('pattern') && createClientForm.get('mobileNo')?.touched
) {
<mat-error>Enter valid 10-digit number (starts with 6-9)</mat-error>
}
</mat-form-field>
<div class="form-field mobile-verify-row">
@if (!mobileVerified) {
<button mat-stroked-button color="primary" type="button" class="verify-btn"
<button
mat-stroked-button
color="primary"
type="button"
class="verify-btn"
[disabled]="readOnly || createClientForm.get('mobileNo')?.value?.length !== 10"
(click)="verifyWithDIP()">
(click)="verifyWithDIP()"
>
<fa-icon icon="shield-alt" class="m-r-10"></fa-icon>
Verify with DIP
</button>
@@ -762,19 +919,33 @@
<mat-form-field appearance="outline" class="form-field">
<mat-label>Alternate Mobile No</mat-label>
<span matPrefix class="field-prefix-icon"><mat-icon>phone_android</mat-icon></span>
<input matInput type="text" formControlName="alternateMobileNo" placeholder="e.g. 9876543210" maxlength="10"
<input
matInput
type="text"
formControlName="alternateMobileNo"
placeholder="e.g. 9876543210"
maxlength="10"
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()"
(paste)="$event.preventDefault()" />
@if (createClientForm.get('alternateMobileNo')?.hasError('pattern') && createClientForm.get('alternateMobileNo')?.touched) {
(paste)="$event.preventDefault()"
/>
@if (
createClientForm.get('alternateMobileNo')?.hasError('pattern') &&
createClientForm.get('alternateMobileNo')?.touched
) {
<mat-error>Enter valid 10-digit number (starts with 6-9)</mat-error>
}
</mat-form-field>
<div class="form-field mobile-verify-row">
@if (!alternateMobileVerified) {
<button mat-stroked-button color="primary" type="button" class="verify-btn"
<button
mat-stroked-button
color="primary"
type="button"
class="verify-btn"
[disabled]="readOnly || createClientForm.get('alternateMobileNo')?.value?.length !== 10"
(click)="verifyAlternateDIP()">
(click)="verifyAlternateDIP()"
>
<fa-icon icon="shield-alt" class="m-r-10"></fa-icon>
Verify with DIP
</button>
@@ -788,21 +959,27 @@
<mat-label>{{ 'labels.inputs.Email Address' | translate }}</mat-label>
<span matPrefix class="field-prefix-icon"><mat-icon>email</mat-icon></span>
<input matInput required formControlName="emailAddress" placeholder="e.g. name@example.com" />
@if (createClientForm.get('emailAddress')?.hasError('required') && createClientForm.get('emailAddress')?.touched) {
@if (
createClientForm.get('emailAddress')?.hasError('required') && createClientForm.get('emailAddress')?.touched
) {
<mat-error>Email is <strong>required</strong></mat-error>
} @else if (createClientForm.get('emailAddress')?.invalid && createClientForm.get('emailAddress')?.touched) {
<mat-error>Enter a valid email (e.g. name&#64;example.com)</mat-error>
}
</mat-form-field>
@if (createClientForm.get('legalFormId')?.value === LegalFormId.PERSON && maritalStatusOptions.length > 0 && !isMinor) {
@if (
createClientForm.get('legalFormId')?.value === LegalFormId.PERSON && maritalStatusOptions.length > 0 && !isMinor
) {
<mat-form-field appearance="outline" class="form-field">
<mat-label>Marital Status</mat-label>
<mat-select formControlName="maritalStatusId">
<mat-select-trigger>
@if (getSelectedMaritalStatus()) {
<span class="icon-option">
<mat-icon class="icon-option-icon">{{ getMaritalStatusIcon(getSelectedMaritalStatus().name) }}</mat-icon>
<mat-icon class="icon-option-icon">{{
getMaritalStatusIcon(getSelectedMaritalStatus().name)
}}</mat-icon>
{{ getSelectedMaritalStatus().name }}
</span>
}
@@ -823,81 +1000,90 @@
<!-- Section: Financial & Classification (Individual only) -->
@if (createClientForm.get('legalFormId')?.value === LegalFormId.PERSON) {
<div class="section-block">
<h4 class="section-title">
<fa-icon icon="chart-line" class="section-icon"></fa-icon>
Financial &amp; Classification
</h4>
<div class="form-grid">
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'labels.inputs.Occupation Category' | translate }}</mat-label>
<mat-select formControlName="occupationCategoryId">
<mat-select-trigger>
@if (getSelectedOccupationCategory()) {
<span class="occ-option">
<mat-icon class="occ-option-icon">{{ getOccupationCategoryIcon(getSelectedOccupationCategory().name) }}</mat-icon>
{{ getSelectedOccupationCategory().name }}
</span>
<div class="section-block">
<h4 class="section-title">
<fa-icon icon="chart-line" class="section-icon"></fa-icon>
Financial &amp; Classification
</h4>
<div class="form-grid">
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'labels.inputs.Occupation Category' | translate }}</mat-label>
<mat-select formControlName="occupationCategoryId">
<mat-select-trigger>
@if (getSelectedOccupationCategory()) {
<span class="occ-option">
<mat-icon class="occ-option-icon">{{
getOccupationCategoryIcon(getSelectedOccupationCategory().name)
}}</mat-icon>
{{ getSelectedOccupationCategory().name }}
</span>
}
</mat-select-trigger>
@for (cat of occupationCategoryOptions; track cat) {
<mat-option [value]="cat.id">
<span class="occ-option">
<mat-icon class="occ-option-icon">{{ getOccupationCategoryIcon(cat.name) }}</mat-icon>
{{ cat.name }}
</span>
</mat-option>
}
</mat-select-trigger>
@for (cat of occupationCategoryOptions; track cat) {
<mat-option [value]="cat.id">
<span class="occ-option">
<mat-icon class="occ-option-icon">{{ getOccupationCategoryIcon(cat.name) }}</mat-icon>
{{ cat.name }}
</span>
</mat-option>
}
</mat-select>
</mat-form-field>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'labels.inputs.Occupation' | translate }}</mat-label>
<mat-select formControlName="occupationId">
<mat-select-trigger>
@if (getSelectedOccupation()) {
<span class="occ-option">
<mat-icon class="occ-option-icon">{{ getOccupationIcon(getSelectedOccupation().name) }}</mat-icon>
{{ getSelectedOccupation().name }}
</span>
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'labels.inputs.Occupation' | translate }}</mat-label>
<mat-select formControlName="occupationId">
<mat-select-trigger>
@if (getSelectedOccupation()) {
<span class="occ-option">
<mat-icon class="occ-option-icon">{{ getOccupationIcon(getSelectedOccupation().name) }}</mat-icon>
{{ getSelectedOccupation().name }}
</span>
}
</mat-select-trigger>
@for (occ of occupationOptions; track occ) {
<mat-option [value]="occ.id">
<span class="occ-option">
<mat-icon class="occ-option-icon">{{ getOccupationIcon(occ.name) }}</mat-icon>
{{ occ.name }}
</span>
</mat-option>
}
</mat-select-trigger>
@for (occ of occupationOptions; track occ) {
<mat-option [value]="occ.id">
<span class="occ-option">
<mat-icon class="occ-option-icon">{{ getOccupationIcon(occ.name) }}</mat-icon>
{{ occ.name }}
</span>
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="form-field">
<mat-label>Monthly Income</mat-label>
<span matPrefix class="rupee-prefix">&#8377;</span>
<input matInput type="number" formControlName="monthlyIncome" min="0" />
@if (
createClientForm.get('monthlyIncome')?.hasError('pattern') && createClientForm.get('monthlyIncome')?.touched
) {
<mat-error>Enter a valid amount</mat-error>
} @else if (
createClientForm.get('monthlyIncome')?.hasError('min') && createClientForm.get('monthlyIncome')?.touched
) {
<mat-error>Amount cannot be negative</mat-error>
}
</mat-select>
</mat-form-field>
</mat-form-field>
<mat-form-field appearance="outline" class="form-field">
<mat-label>Monthly Income</mat-label>
<span matPrefix class="rupee-prefix">&#8377;</span>
<input matInput type="number" formControlName="monthlyIncome" min="0" />
@if (createClientForm.get('monthlyIncome')?.hasError('pattern') && createClientForm.get('monthlyIncome')?.touched) {
<mat-error>Enter a valid amount</mat-error>
} @else if (createClientForm.get('monthlyIncome')?.hasError('min') && createClientForm.get('monthlyIncome')?.touched) {
<mat-error>Amount cannot be negative</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="form-field">
<mat-label>Annual Turnover (Last FY)</mat-label>
<span matPrefix class="rupee-prefix">&#8377;</span>
<input matInput type="number" formControlName="annualTurnover" min="0" />
@if (createClientForm.get('annualTurnover')?.hasError('pattern') && createClientForm.get('annualTurnover')?.touched) {
<mat-error>Enter a valid amount</mat-error>
} @else if (createClientForm.get('annualTurnover')?.hasError('min') && createClientForm.get('annualTurnover')?.touched) {
<mat-error>Amount cannot be negative</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="form-field">
<mat-label>Annual Turnover (Last FY)</mat-label>
<span matPrefix class="rupee-prefix">&#8377;</span>
<input matInput type="number" formControlName="annualTurnover" min="0" />
@if (
createClientForm.get('annualTurnover')?.hasError('pattern') &&
createClientForm.get('annualTurnover')?.touched
) {
<mat-error>Enter a valid amount</mat-error>
} @else if (
createClientForm.get('annualTurnover')?.hasError('min') && createClientForm.get('annualTurnover')?.touched
) {
<mat-error>Amount cannot be negative</mat-error>
}
</mat-form-field>
</div>
</div>
</div>
}
<!-- submittedOnDate is set automatically via form default (business date) -->
@@ -13,7 +13,7 @@ form {
.section-block {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
border-bottom: 1px solid rgb(0 0 0 / 6%);
&:last-child {
border-bottom: none;
@@ -28,7 +28,7 @@ form {
font-size: 15px;
font-weight: 600;
color: #333;
margin: 0 0 16px 0;
margin: 0 0 16px;
.section-icon {
color: var(--md-sys-color-primary, #1074b9);
@@ -235,7 +235,7 @@ form {
border-radius: 12px;
overflow: hidden;
background: #fff;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.04);
box-shadow: 0 2px 8px rgb(0 0 0 / 4%);
}
.guardian-section__header {
@@ -268,7 +268,7 @@ form {
.guardian-section__sub {
font-size: 11px;
color: rgba(255, 255, 255, 0.55);
color: rgb(255 255 255 / 55%);
}
.guardian-section__age-chips {
@@ -280,9 +280,9 @@ form {
.guardian-age-chip {
font-size: 11px;
font-weight: 500;
color: rgba(255, 255, 255, 0.8);
color: rgb(255 255 255 / 80%);
padding: 3px 10px;
background: rgba(255, 255, 255, 0.1);
background: rgb(255 255 255 / 10%);
border-radius: 10px;
strong {
@@ -408,7 +408,10 @@ form {
.guardian-cancel-link {
color: #94a3b8 !important;
margin-top: 8px;
&:hover { color: #ef4444 !important; }
&:hover {
color: #ef4444 !important;
}
}
/* ═══ Guardian Row Table ═══ */
@@ -437,7 +440,10 @@ form {
align-items: center;
padding: 10px 12px;
transition: background 0.1s;
&:hover { background: #f8fafc; }
&:hover {
background: #f8fafc;
}
}
.guardian-row__col {
@@ -445,12 +451,33 @@ form {
font-size: 13px;
color: #1e293b;
&--photo { flex: 0 0 50px; }
&--name { flex: 1; min-width: 0; }
&--rel { flex: 0 0 120px; }
&--cif { flex: 0 0 130px; }
&--status { flex: 0 0 80px; }
&--actions { flex: 0 0 110px; display: flex; gap: 2px; justify-content: flex-end; }
&--photo {
flex: 0 0 50px;
}
&--name {
flex: 1;
min-width: 0;
}
&--rel {
flex: 0 0 120px;
}
&--cif {
flex: 0 0 130px;
}
&--status {
flex: 0 0 80px;
}
&--actions {
flex: 0 0 110px;
display: flex;
gap: 2px;
justify-content: flex-end;
}
}
.guardian-row__avatar {
@@ -543,14 +570,22 @@ form {
color: #64748b;
}
&:hover mat-icon { color: #1074b9; }
&--delete:hover mat-icon { color: #ef4444; }
&:hover mat-icon {
color: #1074b9;
}
&--delete:hover mat-icon {
color: #ef4444;
}
}
.guardian-row__zoomable {
cursor: zoom-in;
transition: transform 0.15s;
&:hover { transform: scale(1.05); }
&:hover {
transform: scale(1.05);
}
}
/* Expandable detail panel */
@@ -558,12 +593,19 @@ form {
border-top: 1px solid #e2e8f0;
background: #f8fafc;
padding: 16px 20px;
animation: guardianSlideDown 0.2s ease-out;
animation: guardian-slide-down 0.2s ease-out;
}
@keyframes guardianSlideDown {
from { opacity: 0; max-height: 0; }
to { opacity: 1; max-height: 500px; }
@keyframes guardian-slide-down {
from {
opacity: 0;
max-height: 0;
}
to {
opacity: 1;
max-height: 500px;
}
}
.guardian-expand__grid {
@@ -638,12 +680,13 @@ form {
padding: 0 !important;
border-radius: 12px !important;
}
.mdc-dialog__surface {
border-radius: 12px !important;
}
}
@media (max-width: 768px) {
@media (width <= 768px) {
.guardian-section__header {
flex-direction: column;
align-items: flex-start;
@@ -665,7 +708,9 @@ form {
width: 100%;
}
.guardian-row__header { display: none; }
.guardian-row__header {
display: none;
}
.guardian-row__data {
flex-wrap: wrap;
@@ -673,10 +718,23 @@ form {
}
.guardian-row__col {
&--photo { flex: 0 0 40px; }
&--name { flex: 1 1 200px; }
&--rel, &--cif, &--status { flex: 0 0 auto; }
&--actions { flex: 0 0 auto; }
&--photo {
flex: 0 0 40px;
}
&--name {
flex: 1 1 200px;
}
&--rel,
&--cif,
&--status {
flex: 0 0 auto;
}
&--actions {
flex: 0 0 auto;
}
}
.guardian-expand__grid {
@@ -738,7 +796,7 @@ form {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 10px;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
max-height: 280px;
overflow-y: auto;
}
@@ -776,8 +834,13 @@ form {
transition: background 0.1s;
border-bottom: 1px solid #f1f5f9;
&:last-child { border-bottom: none; }
&:hover { background: #f0f7ff; }
&:last-child {
border-bottom: none;
}
&:hover {
background: #f0f7ff;
}
}
.guardian-result-item__avatar {
@@ -834,7 +897,7 @@ form {
border-radius: 12px;
overflow: hidden;
background: #fff;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
box-shadow: 0 1px 4px rgb(0 0 0 / 6%);
}
.guardian-card__header {
@@ -881,7 +944,10 @@ form {
.guardian-card__remove {
color: #94a3b8 !important;
&:hover { color: #ef4444 !important; }
&:hover {
color: #ef4444 !important;
}
}
.guardian-card__body {
@@ -898,8 +964,13 @@ form {
border-bottom: 1px solid #f1f5f9;
border-right: 1px solid #f1f5f9;
&:nth-child(2n) { border-right: none; }
&:nth-last-child(-n+2) { border-bottom: none; }
&:nth-child(2n) {
border-right: none;
}
&:nth-last-child(-n + 2) {
border-bottom: none;
}
}
.guardian-card__label {
@@ -945,7 +1016,7 @@ form {
height: 16px;
border-radius: 3px;
overflow: hidden;
box-shadow: 0 0 0 1px rgba(0, 0, 0, 0.1);
box-shadow: 0 0 0 1px rgb(0 0 0 / 10%);
flex-shrink: 0;
}
@@ -1030,7 +1101,7 @@ form {
width: 24px;
height: 24px;
border: none;
background: rgba(255, 255, 255, 0.15);
background: rgb(255 255 255 / 15%);
border-radius: 4px;
color: #fff;
cursor: pointer;
@@ -1040,7 +1111,7 @@ form {
font-size: 12px;
&:hover {
background: rgba(255, 255, 255, 0.25);
background: rgb(255 255 255 / 25%);
}
}
@@ -1068,10 +1139,22 @@ form {
color: #0f172a;
}
.row-micro td { background: #ecfdf5; }
.row-small td { background: #e0f2fe; }
.row-medium td { background: #fffbeb; }
.row-large td { background: #f8fafc; color: #94a3b8; }
.row-micro td {
background: #ecfdf5;
}
.row-small td {
background: #e0f2fe;
}
.row-medium td {
background: #fffbeb;
}
.row-large td {
background: #f8fafc;
color: #94a3b8;
}
}
.msme-dot {
@@ -1081,10 +1164,21 @@ form {
border-radius: 50%;
margin-right: 6px;
&.green { background: #10b981; }
&.blue { background: #0ea5e9; }
&.amber { background: #f59e0b; }
&.gray { background: #cbd5e1; }
&.green {
background: #10b981;
}
&.blue {
background: #0ea5e9;
}
&.amber {
background: #f59e0b;
}
&.gray {
background: #cbd5e1;
}
}
.msme-class-table-footer {
@@ -1201,6 +1295,7 @@ form {
.mdc-notched-outline__trailing {
border-color: #10b981 !important;
}
input:disabled {
color: #059669 !important;
-webkit-text-fill-color: #059669 !important;
@@ -1213,6 +1308,7 @@ form {
.mdc-notched-outline__trailing {
border-color: #0ea5e9 !important;
}
input:disabled {
color: #0284c7 !important;
-webkit-text-fill-color: #0284c7 !important;
@@ -1225,6 +1321,7 @@ form {
.mdc-notched-outline__trailing {
border-color: #f59e0b !important;
}
input:disabled {
color: #d97706 !important;
-webkit-text-fill-color: #d97706 !important;
@@ -1390,7 +1487,7 @@ form {
}
}
@media (max-width: 768px) {
@media (width <= 768px) {
.form-grid {
grid-template-columns: 1fr;
}
@@ -1495,18 +1592,68 @@ form {
border-bottom: 1px solid #f1f5f9;
transition: background 0.15s;
&:last-child { border-bottom: none; }
&:hover { background: #f8fafc; }
&:last-child {
border-bottom: none;
}
&:hover {
background: #f8fafc;
}
}
.sig-table__col {
&--sno { flex: 0 0 32px; text-align: center; color: #94a3b8; font-size: 12px; }
&--photo { flex: 0 0 40px; }
&--name { flex: 1; font-size: 13px; display: flex; align-items: center; gap: 6px; }
&--id { flex: 0 0 100px; font-size: 12px; color: #64748b; }
&--pan { flex: 0 0 120px; font-size: 12px; color: #1e293b; display: flex; align-items: center; gap: 3px; }
&--status { flex: 0 0 80px; }
&--actions { flex: 0 0 80px; display: flex; gap: 2px; }
&--sno {
flex: 0 0 32px;
text-align: center;
color: #94a3b8;
font-size: 12px;
}
&--photo {
flex: 0 0 40px;
}
&--name {
flex: 1;
font-size: 13px;
display: flex;
align-items: center;
gap: 6px;
}
&--id {
flex: 0 0 100px;
font-size: 12px;
color: #64748b;
}
&--pan {
flex: 0 0 120px;
font-size: 12px;
color: #1e293b;
display: flex;
align-items: center;
gap: 3px;
}
&--din {
flex: 0 0 140px;
font-size: 12px;
color: #1e293b;
display: flex;
align-items: center;
gap: 3px;
}
&--status {
flex: 0 0 80px;
}
&--actions {
flex: 0 0 80px;
display: flex;
gap: 2px;
}
}
.sig-table__avatar {
@@ -1521,11 +1668,21 @@ form {
font-size: 12px;
overflow: hidden;
&.has-photo { padding: 0; }
.avatar-photo { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
&.has-photo {
padding: 0;
}
.avatar-photo {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
}
.sig-inline-spinner { display: inline-block; }
.sig-inline-spinner {
display: inline-block;
}
.verify-tick-sm {
font-size: 14px !important;
@@ -1534,11 +1691,40 @@ form {
color: #22c55e;
}
.verify-cross-sm {
font-size: 14px !important;
width: 14px !important;
height: 14px !important;
color: #f59e0b;
}
.din-verify-btn {
font-size: 11px !important;
padding: 2px 8px !important;
min-height: 24px !important;
line-height: 20px !important;
border-radius: 6px !important;
display: flex;
align-items: center;
gap: 4px;
color: #1074b9 !important;
border-color: #93c5fd !important;
.din-verify-icon {
font-size: 14px !important;
width: 14px !important;
height: 14px !important;
}
}
.sig-status-chip {
font-size: 11px;
font-weight: 600;
color: #94a3b8;
&.active { color: #22c55e; }
&.active {
color: #22c55e;
}
}
.sig-action-icon {
@@ -1546,7 +1732,10 @@ form {
width: 18px !important;
height: 18px !important;
color: #64748b;
&.remove { color: #ef4444; }
&.remove {
color: #ef4444;
}
}
.sig-add-wrapper {
@@ -1561,11 +1750,13 @@ form {
overflow: visible !important;
max-height: none !important;
}
.mdc-dialog__surface {
border-radius: 12px !important;
overflow: visible !important;
max-height: none !important;
}
.mat-mdc-dialog-content {
max-height: none !important;
overflow: visible !important;
@@ -1625,7 +1816,7 @@ form {
&__sub {
font-size: 11px;
color: rgba(255, 255, 255, 0.5);
color: rgb(255 255 255 / 50%);
margin: 2px 0 0;
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,922 @@
/**
* 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, OnDestroy, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef, MatDialogModule } from '@angular/material/dialog';
import { MatProgressBarModule } from '@angular/material/progress-bar';
import { MatButtonModule } from '@angular/material/button';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatIconModule } from '@angular/material/icon';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { ClientsService } from 'app/clients/clients.service';
@Component({
selector: 'mifosx-din-verify-dialog',
template: `
<h2 mat-dialog-title>
<fa-icon icon="id-badge" class="dialog-title-icon"></fa-icon>
DIN Verification
</h2>
<mat-dialog-content>
<!-- Step 1: Enter DIN -->
@if (!verifying && !verified && !failed && !errorOccurred) {
<div class="din-entry">
<div class="signatory-info">
<fa-icon icon="user-shield" class="sig-icon"></fa-icon>
<div class="sig-detail">
<span class="sig-name">{{ signatoryName }}</span>
<span class="sig-meta">CIF: {{ cifNumber }} &middot; PAN: {{ panNumber }}</span>
</div>
</div>
@if (entityName) {
<div class="entity-info">
<fa-icon icon="building" class="entity-icon"></fa-icon>
<div class="entity-detail">
<span class="entity-label">Entity</span>
<span class="entity-name">{{ entityName }}</span>
</div>
</div>
}
<div class="din-info-bar">
<fa-icon icon="info-circle" class="info-icon"></fa-icon>
<span
>Enter the <strong>Director Identification Number (DIN)</strong> issued by the Ministry of Corporate
Affairs (MCA) for this authorized signatory. The system will verify the DIN and match the entity name
against the director's associated companies.</span
>
</div>
<mat-form-field appearance="outline" class="din-input-field">
<mat-label>DIN Number</mat-label>
<mat-icon matPrefix class="din-prefix-icon">badge</mat-icon>
<input
matInput
[(ngModel)]="dinNumber"
placeholder="e.g. 01234567"
maxlength="8"
(keyup.enter)="onVerify()"
autocomplete="off"
/>
<mat-hint>8-digit DIN number from MCA</mat-hint>
</mat-form-field>
</div>
}
<!-- Step 2: Verifying -->
@if (verifying) {
<div class="progress-section">
<mat-progress-bar mode="determinate" [value]="progress" color="primary"></mat-progress-bar>
<div class="progress-percent">{{ progress }}%</div>
<div class="progress-steps">
@for (step of progressSteps; track step.label; let i = $index) {
<div class="progress-step" [class.active]="i === currentStep" [class.done]="i < currentStep">
<fa-icon
[icon]="i < currentStep ? 'check-circle' : step.icon"
[class.done-icon]="i < currentStep"
></fa-icon>
<span>{{ step.label }}</span>
</div>
}
</div>
<p class="verify-msg">
Verifying DIN <strong>{{ dinNumber }}</strong> for <strong>{{ signatoryName }}</strong> against
<strong>MCA (Ministry of Corporate Affairs)</strong> records.
</p>
</div>
}
<!-- Step 3a: Verified -->
@if (!verifying && verified) {
<div class="result-card success-card">
<div class="result-icon-ring success-ring">
<fa-icon icon="check-circle" class="result-shield success-shield"></fa-icon>
</div>
<div class="result-badge success-badge">VERIFIED</div>
<h3 class="result-title">DIN Verified Successfully</h3>
<p class="result-detail">
DIN <strong class="din-highlight">{{ dinNumber }}</strong> for <strong>{{ signatoryName }}</strong> has been
verified against <strong>MCA</strong> records.
</p>
<div class="result-meta">
<div class="meta-row">
<span class="meta-label">DIN</span>
<span class="meta-value meta-mono">{{ dinNumber }}</span>
</div>
@if (directorName) {
<div class="meta-row">
<span class="meta-label">Director Name</span>
<span class="meta-value">{{ directorName }}</span>
</div>
}
<div class="meta-row">
<span class="meta-label">Status</span>
<span class="meta-value meta-success">{{ dinStatus || 'Active' }}</span>
</div>
<div class="meta-row">
<span class="meta-label">Verified On</span>
<span class="meta-value">{{ verifiedDate }}</span>
</div>
@if (dinDesignation) {
<div class="meta-row">
<span class="meta-label">Designation</span>
<span class="meta-value">{{ dinDesignation }}</span>
</div>
}
</div>
<!-- Company Match Section -->
@if (companies.length > 0) {
<div class="company-match-section">
<h4 class="company-match-title">
<fa-icon icon="building" class="company-title-icon"></fa-icon>
Associated Companies ({{ companies.length }})
</h4>
@if (companyMatch) {
<div class="match-banner match-success">
<fa-icon icon="check-circle" class="match-banner-icon"></fa-icon>
<div>
<strong>Entity Match Found</strong>
<p>
<strong>{{ entityName }}</strong> matches <strong>{{ matchedCompany }}</strong>
</p>
</div>
</div>
} @else {
<div class="match-banner match-warning">
<fa-icon icon="exclamation-triangle" class="match-banner-icon"></fa-icon>
<div>
<strong>No Entity Match</strong>
<p>
<strong>{{ entityName }}</strong> does not match any of the director's associated companies.
</p>
</div>
</div>
}
<div class="company-list">
@for (company of companies; track company.companyName || $index) {
<div class="company-item" [class.company-matched]="company._matched">
@if (company._matched) {
<mat-icon class="company-match-icon match">check_circle</mat-icon>
} @else {
<mat-icon class="company-match-icon no-match">radio_button_unchecked</mat-icon>
}
<div class="company-info">
<span class="company-name">{{ company.companyName }}</span>
<span class="company-meta">
@if (company.cin) {
CIN: {{ company.cin }}
}
@if (company.cin && company.designation) {
&middot;
}
@if (company.designation) {
{{ company.designation }}
}
@if ((company.cin || company.designation) && company.appointmentDate) {
&middot;
}
@if (company.appointmentDate) {
Since: {{ company.appointmentDate }}
}
</span>
</div>
</div>
}
</div>
</div>
}
</div>
}
<!-- Step 3b: Verification Failed -->
@if (!verifying && failed) {
<div class="result-card fail-card">
<div class="result-icon-ring fail-ring">
<fa-icon icon="times-circle" class="result-shield fail-shield"></fa-icon>
</div>
<div class="result-badge fail-badge">FAILED</div>
<h3 class="result-title">DIN Verification Failed</h3>
<p class="result-detail">
DIN <strong class="din-highlight-fail">{{ dinNumber }}</strong> could not be verified.
</p>
@if (failReason) {
<div class="fail-reason">
<fa-icon icon="exclamation-triangle" class="fail-reason-icon"></fa-icon>
<span>{{ failReason }}</span>
</div>
}
</div>
}
<!-- Step 3c: Error -->
@if (!verifying && errorOccurred) {
<div class="result-card error-card">
<fa-icon icon="exclamation-circle" class="error-icon-large"></fa-icon>
<p class="error-msg">Verification failed. Please try again.</p>
<p class="error-detail">{{ errorMessage }}</p>
</div>
}
</mat-dialog-content>
<mat-dialog-actions align="end">
@if (!verifying && !verified && !failed && !errorOccurred) {
<button mat-stroked-button (click)="onSkip()">Skip</button>
<button mat-raised-button color="primary" [disabled]="!dinNumber || dinNumber.length < 8" (click)="onVerify()">
<fa-icon icon="search"></fa-icon> Verify DIN
</button>
}
@if (!verifying && verified) {
<button mat-raised-button color="primary" (click)="onClose(true)"><fa-icon icon="check"></fa-icon> Done</button>
}
@if (!verifying && failed) {
<button mat-stroked-button (click)="onRetry()">Retry</button>
<button mat-raised-button color="warn" (click)="onClose(false)"><fa-icon icon="times"></fa-icon> Close</button>
}
@if (!verifying && errorOccurred) {
<button mat-stroked-button (click)="onRetry()">Retry</button>
<button mat-raised-button (click)="onClose(false)">Close</button>
}
</mat-dialog-actions>
`,
styles: [
`
:host {
display: block;
min-width: 520px;
}
h2[mat-dialog-title] {
display: flex;
align-items: center;
gap: 8px;
margin: 0;
padding: 16px 24px;
font-size: 18px;
font-weight: 600;
color: #1074b9;
}
.dialog-title-icon {
font-size: 20px;
}
mat-dialog-content {
padding: 16px 24px;
max-height: 70vh;
overflow: auto;
}
/* Signatory info */
.signatory-info {
display: flex;
align-items: center;
gap: 12px;
padding: 14px 16px;
background: #f0f4f8;
border-radius: 10px;
margin-bottom: 10px;
border: 1px solid #e2e8f0;
}
.sig-icon {
font-size: 22px;
color: #1074b9;
}
.sig-detail {
display: flex;
flex-direction: column;
}
.sig-name {
font-size: 15px;
font-weight: 700;
color: #1a2332;
}
.sig-meta {
font-size: 12px;
color: #64748b;
margin-top: 2px;
}
/* Entity info */
.entity-info {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 16px;
background: #fffbeb;
border-radius: 8px;
margin-bottom: 14px;
border: 1px solid #fde68a;
}
.entity-icon {
font-size: 18px;
color: #b45309;
}
.entity-detail {
display: flex;
flex-direction: column;
}
.entity-label {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #92400e;
}
.entity-name {
font-size: 14px;
font-weight: 700;
color: #78350f;
}
/* DIN info bar */
.din-info-bar {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 10px 14px;
background: #e3f2fd;
border-radius: 8px;
margin-bottom: 16px;
font-size: 13px;
color: #333;
line-height: 1.5;
}
.info-icon {
font-size: 16px;
color: #1074b9;
flex-shrink: 0;
margin-top: 2px;
}
/* DIN input */
.din-input-field {
width: 100%;
}
.din-prefix-icon {
color: #64748b;
margin-right: 4px;
}
/* Progress */
.progress-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 12px;
}
.progress-percent {
font-size: 22px;
font-weight: 700;
color: #1074b9;
margin-top: 4px;
}
.progress-steps {
display: flex;
flex-direction: column;
gap: 10px;
width: 100%;
padding: 8px 16px;
}
.progress-step {
display: flex;
align-items: center;
gap: 8px;
font-size: 13px;
color: #9ca3af;
transition: color 0.3s ease;
}
.progress-step.active {
color: #1074b9;
font-weight: 600;
}
.progress-step.done {
color: #388e3c;
}
.done-icon {
color: #388e3c;
}
.verify-msg {
font-size: 14px;
color: #555;
line-height: 1.6;
text-align: center;
margin: 4px 0 0;
}
/* Result cards */
.result-card {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px 16px;
gap: 12px;
}
.result-icon-ring {
width: 64px;
height: 64px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
.success-ring {
background: linear-gradient(135deg, #ecfdf5, #d1fae5);
border: 3px solid #a7f3d0;
}
.fail-ring {
background: linear-gradient(135deg, #fef2f2, #fecaca);
border: 3px solid #fca5a5;
}
.result-shield {
font-size: 28px;
}
.success-shield {
color: #059669;
}
.fail-shield {
color: #dc2626;
}
.result-badge {
display: inline-block;
font-size: 11px;
font-weight: 800;
letter-spacing: 1.5px;
padding: 3px 14px;
border-radius: 20px;
}
.success-badge {
color: #059669;
background: #ecfdf5;
border: 1px solid #a7f3d0;
}
.fail-badge {
color: #dc2626;
background: #fef2f2;
border: 1px solid #fecaca;
}
.result-title {
font-size: 17px;
font-weight: 700;
color: #111827;
margin: 0;
}
.result-detail {
font-size: 13px;
color: #6b7280;
text-align: center;
line-height: 1.6;
margin: 0;
max-width: 400px;
}
.din-highlight {
color: #059669;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
}
.din-highlight-fail {
color: #dc2626;
font-family: 'JetBrains Mono', 'Fira Code', monospace;
}
.result-meta {
width: 100%;
background: #f9fafb;
border: 1px solid #e5e7eb;
border-radius: 8px;
padding: 12px 16px;
margin-top: 4px;
}
.meta-row {
display: flex;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid #f3f4f6;
}
.meta-row:last-child {
border-bottom: none;
}
.meta-label {
font-size: 12px;
color: #9ca3af;
font-weight: 500;
}
.meta-value {
font-size: 12px;
color: #374151;
font-weight: 600;
}
.meta-mono {
font-family: 'JetBrains Mono', 'Fira Code', monospace;
letter-spacing: 0.5px;
}
.meta-success {
color: #059669;
}
/* ═══ Company Match Section ═══ */
.company-match-section {
width: 100%;
margin-top: 8px;
}
.company-match-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 700;
color: #1e293b;
margin: 0 0 10px;
}
.company-title-icon {
font-size: 14px;
color: #64748b;
}
/* Match banner */
.match-banner {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px 14px;
border-radius: 8px;
margin-bottom: 10px;
font-size: 13px;
line-height: 1.5;
}
.match-banner p {
margin: 2px 0 0;
font-size: 12px;
}
.match-success {
background: #ecfdf5;
border: 1px solid #a7f3d0;
color: #065f46;
}
.match-success .match-banner-icon {
color: #059669;
font-size: 18px;
margin-top: 1px;
flex-shrink: 0;
}
.match-warning {
background: #fffbeb;
border: 1px solid #fde68a;
color: #92400e;
}
.match-warning .match-banner-icon {
color: #d97706;
font-size: 18px;
margin-top: 1px;
flex-shrink: 0;
}
/* Company list */
.company-list {
border: 1px solid #e5e7eb;
border-radius: 8px;
overflow: hidden;
}
.company-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
border-bottom: 1px solid #f3f4f6;
transition: background 0.15s;
}
.company-item:last-child {
border-bottom: none;
}
.company-item:hover {
background: #f8fafc;
}
.company-matched {
background: #f0fdf4 !important;
border-left: 3px solid #22c55e;
}
.company-match-icon {
font-size: 18px !important;
width: 18px !important;
height: 18px !important;
flex-shrink: 0;
}
.company-match-icon.match {
color: #22c55e;
}
.company-match-icon.no-match {
color: #d1d5db;
}
.company-info {
display: flex;
flex-direction: column;
min-width: 0;
}
.company-name {
font-size: 13px;
font-weight: 600;
color: #1e293b;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.company-meta {
font-size: 11px;
color: #94a3b8;
margin-top: 2px;
}
/* Fail reason */
.fail-reason {
display: flex;
align-items: center;
gap: 10px;
width: 100%;
background: #fef2f2;
border: 1px solid #fecaca;
padding: 10px 14px;
border-radius: 6px;
font-size: 13px;
color: #991b1b;
line-height: 1.5;
}
.fail-reason-icon {
font-size: 16px;
color: #dc2626;
flex-shrink: 0;
}
/* Error */
.error-icon-large {
font-size: 48px;
color: #f57c00;
}
.error-msg {
font-size: 16px;
font-weight: 600;
color: #f57c00;
margin: 0;
text-align: center;
}
.error-detail {
font-size: 14px;
color: #555;
text-align: center;
}
mat-dialog-actions {
padding: 8px 24px 16px;
}
mat-dialog-actions button {
display: flex;
align-items: center;
gap: 6px;
}
`
],
imports: [
CommonModule,
FormsModule,
MatDialogModule,
MatProgressBarModule,
MatButtonModule,
MatFormFieldModule,
MatInputModule,
MatIconModule,
FaIconComponent
]
})
export class DinVerifyDialogComponent implements OnInit, OnDestroy {
private dialogRef = inject(MatDialogRef<DinVerifyDialogComponent>);
private data: {
clientId: string;
kycId: string;
signatoryName: string;
cifNumber: string;
panNumber: string;
entityName: string;
} = inject(MAT_DIALOG_DATA);
private clientsService = inject(ClientsService);
signatoryName = '';
cifNumber = '';
panNumber = '';
entityName = '';
dinNumber = '';
verifying = false;
verified = false;
failed = false;
errorOccurred = false;
errorMessage = '';
failReason = '';
directorName = '';
dinStatus = '';
dinDesignation = '';
verifiedDate = new Date().toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
/** Companies list from DIN verification response */
companies: any[] = [];
/** Whether entity name matched a company */
companyMatch = false;
/** The matched company name */
matchedCompany = '';
progress = 0;
currentStep = 0;
progressSteps = [
{ label: 'Connecting to MCA portal...', icon: 'globe' as any },
{ label: 'Looking up DIN records...', icon: 'search' as any },
{ label: 'Matching entity with companies...', icon: 'building' as any },
{ label: 'Saving verification...', icon: 'database' as any }
];
private intervalId: any;
private apiResponse: any = null;
private apiError: any = null;
private apiDone = false;
ngOnInit() {
this.signatoryName = this.data?.signatoryName || '';
this.cifNumber = this.data?.cifNumber || '';
this.panNumber = this.data?.panNumber || '-';
this.entityName = this.data?.entityName || '';
}
onVerify() {
if (!this.dinNumber || this.dinNumber.length < 8) return;
this.verifying = true;
this.verified = false;
this.failed = false;
this.errorOccurred = false;
this.apiDone = false;
this.apiResponse = null;
this.apiError = null;
this.progress = 0;
this.currentStep = 0;
this.companies = [];
this.companyMatch = false;
this.matchedCompany = '';
this.dialogRef.disableClose = true;
// Fire API call — pass clientId, kycId, dinNumber and entityName
this.clientsService.verifyDIN(this.data.clientId, this.data.kycId, this.dinNumber, this.entityName).subscribe({
next: (res: any) => {
this.apiResponse = res;
this.apiDone = true;
this.tryShowResult();
},
error: (err: any) => {
this.apiError = err;
this.apiDone = true;
this.tryShowResult();
}
});
// Animate progress
this.intervalId = setInterval(() => {
this.progress += 2;
this.currentStep = Math.min(Math.floor(this.progress / 25), this.progressSteps.length - 1);
if (this.progress >= 100) {
clearInterval(this.intervalId);
this.intervalId = null;
this.tryShowResult();
}
}, 60);
}
private tryShowResult() {
if (!this.apiDone || this.progress < 100) return;
this.verifying = false;
this.dialogRef.disableClose = false;
if (this.apiError) {
this.errorOccurred = true;
this.errorMessage =
this.apiError?.error?.defaultUserMessage ||
this.apiError?.error?.message ||
this.apiError?.message ||
'Unable to verify DIN. Please try again.';
return;
}
const res = this.apiResponse;
const changes = res?.changes || res;
if (changes?.dinVerified || res?.dinVerified) {
this.verified = true;
this.directorName = changes?.directorName || res?.directorName || '';
this.dinStatus = changes?.dinStatus || res?.dinStatus || 'Active';
this.dinDesignation = changes?.dinDesignation || res?.dinDesignation || '';
this.verifiedDate = new Date().toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' });
// Process company list from response
const rawCompanies = changes?.companies || res?.companies || [];
this.companies = rawCompanies.map((c: any) => ({
...c,
companyName: c.companyName || c.name || '',
cin: c.cin || c.cinNumber || '',
designation: c.designation || '',
appointmentDate: c.appointmentDate || c.dateOfAppointment || '',
_matched: false
}));
// Match entity name against company list
this.matchCompanyName();
} else {
this.failed = true;
this.failReason =
changes?.dinFailReason ||
res?.failReason ||
res?.defaultUserMessage ||
'DIN could not be verified against MCA records.';
}
}
/** Match the entity name against the list of companies from the DIN response */
private matchCompanyName() {
if (!this.entityName || this.companies.length === 0) return;
const normalize = (s: string) => s.toLowerCase().replace(/[^a-z0-9]/g, '');
const entityNorm = normalize(this.entityName);
for (const company of this.companies) {
const compNorm = normalize(company.companyName);
// Exact match or substring containment
if (compNorm === entityNorm || compNorm.includes(entityNorm) || entityNorm.includes(compNorm)) {
company._matched = true;
this.companyMatch = true;
this.matchedCompany = company.companyName;
break;
}
}
}
onSkip() {
this.dialogRef.close({ skipped: true });
}
onRetry() {
this.verified = false;
this.failed = false;
this.errorOccurred = false;
this.progress = 0;
this.currentStep = 0;
this.companies = [];
this.companyMatch = false;
this.matchedCompany = '';
}
onClose(verified: boolean) {
this.dialogRef.close({
verified,
dinNumber: this.dinNumber,
directorName: this.directorName,
dinStatus: this.dinStatus,
dinDesignation: this.dinDesignation,
dinVerified: verified,
companyMatch: this.companyMatch,
matchedCompany: this.matchedCompany,
companies: this.companies,
response: this.apiResponse
});
}
ngOnDestroy() {
if (this.intervalId) {
clearInterval(this.intervalId);
}
}
}
@@ -1,3 +1,11 @@
/**
* 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, OnDestroy, inject } from '@angular/core';
import { CommonModule } from '@angular/common';
import { MatDialogRef, MatDialogModule, MAT_DIALOG_DATA } from '@angular/material/dialog';
@@ -21,7 +29,10 @@ import { ClientsService } from 'app/clients/clients.service';
<div class="progress-steps">
@for (step of progressSteps; track step.label; let i = $index) {
<div class="progress-step" [class.active]="i === currentStep" [class.done]="i < currentStep">
<fa-icon [icon]="i < currentStep ? 'check-circle' : step.icon" [class.done-icon]="i < currentStep"></fa-icon>
<fa-icon
[icon]="i < currentStep ? 'check-circle' : step.icon"
[class.done-icon]="i < currentStep"
></fa-icon>
<span>{{ step.label }}</span>
</div>
}
@@ -42,8 +53,8 @@ import { ClientsService } from 'app/clients/clients.service';
<h3 class="approve-title">No Sanctions Match Found</h3>
<p class="approve-detail">
<strong>{{ clientName }}</strong> has been screened against the
<strong>UN Security Council Consolidated List</strong> and
<strong>FIU-IND</strong> records. No confirmed match was found.
<strong>UN Security Council Consolidated List</strong> and <strong>FIU-IND</strong> records. No confirmed
match was found.
</p>
<div class="approve-meta">
<div class="meta-row">
@@ -550,7 +561,12 @@ export class SanctionsVerifyDialogComponent implements OnInit, OnDestroy {
}
private runProgress() {
const stepDurations = [1200, 1500, 1800, 1000]; // ms per step
const stepDurations = [
1200,
1500,
1800,
1000
]; // ms per step
const totalDuration = stepDurations.reduce((a, b) => a + b, 0);
let elapsed = 0;
const interval = 50;
@@ -602,7 +618,10 @@ export class SanctionsVerifyDialogComponent implements OnInit, OnDestroy {
if (this.apiError) {
this.errorOccurred = true;
this.errorMessage = this.apiError?.error?.message || this.apiError?.message || 'Unable to connect to sanctions screening service.';
this.errorMessage =
this.apiError?.error?.message ||
this.apiError?.message ||
'Unable to connect to sanctions screening service.';
} else {
this.screeningLogId = this.apiResult?.screeningLogId || null;
if (this.apiResult?.matchFound) {