Merge branch 'feature/merge-updates' from cbsnexora into feature/ui-reports-product

Conflict resolutions:
- src/main.scss: kept both new dialog backdrops (CIC/PSL/ALM/CIMS/CKYC/Acct)
  and incoming dialog panel styles (.abd-dialog-panel, .arc-dialog-panel)
- client-general-step.component.html/.ts: kept HEAD's agriculture/non-agri
  income fields, education/degree/religion/social-category dropdowns,
  and inject() refactor matching the incoming pattern
- kyc-management.component.html/.scss/.ts: kept HEAD's Home Visit
  terminology, takeAction() method, hv-* and vkyc-* dialog styles;
  accepted incoming .km-warn-d style addition

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-04-29 19:22:00 +05:30
co-authored by Claude Opus 4.7
470 changed files with 46834 additions and 11014 deletions
@@ -91,7 +91,9 @@
@if (isFieldEnabled('stateProvinceId')) {
<div class="detail-row">
<span class="detail-label">{{ 'labels.inputs.State / Province' | translate }}</span>
<span class="detail-value">{{ getSelectedValue('stateProvinceIdOptions', address.stateProvinceId)?.name }}</span>
<span class="detail-value">{{
getSelectedValue('stateProvinceIdOptions', address.stateProvinceId)?.name
}}</span>
</div>
}
@if (isFieldEnabled('countryId')) {
@@ -1,18 +1,48 @@
/**
* 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/.
*/
/**
* Entity Classification — Scheme & Sector Classification
*/
.ec-page { padding: 0; }
.ec-page {
padding: 0;
}
.step-header {
background: #0f172a;
padding: 16px 24px;
border-radius: 10px 10px 0 0;
margin-bottom: 20px;
&__left { display: flex; align-items: center; gap: 12px; }
&__icon { font-size: 18px; color: #60a5fa; }
&__title { font-size: 16px; font-weight: 600; color: #fff; margin: 0; }
&__sub { font-size: 11px; color: rgb(255 255 255 / 50%); margin: 2px 0 0; }
&__left {
display: flex;
align-items: center;
gap: 12px;
}
&__icon {
font-size: 18px;
color: #60a5fa;
}
&__title {
font-size: 16px;
font-weight: 600;
color: #fff;
margin: 0;
}
&__sub {
font-size: 11px;
color: rgb(255 255 255 / 50%);
margin: 2px 0 0;
}
}
.ec-chips-label {
@@ -32,7 +62,11 @@
min-height: 28px;
}
.ec-no-chip { font-size: 12px; color: #94a3b8; padding: 4px 0; }
.ec-no-chip {
font-size: 12px;
color: #94a3b8;
padding: 4px 0;
}
.ec-chip {
padding: 4px 10px;
@@ -50,7 +84,10 @@
height: 14px !important;
cursor: pointer;
opacity: 0.6;
&:hover { opacity: 1; }
&:hover {
opacity: 1;
}
}
.ec-card {
@@ -60,7 +97,10 @@
margin-bottom: 12px;
overflow: hidden;
transition: border-color 0.2s;
&--open { border-color: #93c5fd; }
&--open {
border-color: #93c5fd;
}
}
.ec-card-head {
@@ -71,10 +111,17 @@
cursor: pointer;
user-select: none;
transition: background 0.15s;
&:hover { background: #f8fafc; }
&:hover {
background: #f8fafc;
}
}
.ec-card-head-left { display: flex; align-items: center; gap: 12px; }
.ec-card-head-left {
display: flex;
align-items: center;
gap: 12px;
}
.ec-tag {
padding: 3px 10px;
@@ -85,14 +132,31 @@
white-space: nowrap;
}
.ec-card-title { font-size: 14px; font-weight: 600; color: #1e293b; }
.ec-card-desc { font-size: 12px; color: #64748b; margin-top: 2px; }
.ec-card-toggle { display: flex; align-items: center; gap: 10px; }
.ec-card-title {
font-size: 14px;
font-weight: 600;
color: #1e293b;
}
.ec-card-desc {
font-size: 12px;
color: #64748b;
margin-top: 2px;
}
.ec-card-toggle {
display: flex;
align-items: center;
gap: 10px;
}
.ec-chevron {
transition: transform 0.25s;
color: #94a3b8;
&.open { transform: rotate(180deg); }
&.open {
transform: rotate(180deg);
}
}
.ec-card-body {
@@ -105,7 +169,10 @@
grid-template-columns: repeat(3, 1fr);
gap: 4px 16px;
margin-bottom: 8px;
mat-form-field { width: 100%; }
mat-form-field {
width: 100%;
}
}
.ec-grid-2 {
@@ -113,7 +180,10 @@
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
margin-bottom: 8px;
mat-form-field { width: 100%; }
mat-form-field {
width: 100%;
}
}
.ec-sub-title {
@@ -125,7 +195,12 @@
margin: 16px 0 12px;
padding-top: 12px;
border-top: 1px dashed #e2e8f0;
&:first-child { margin-top: 0; padding-top: 0; border-top: none; }
&:first-child {
margin-top: 0;
padding-top: 0;
border-top: none;
}
}
.ec-check-grid {
@@ -139,8 +214,14 @@
border: 1px solid #e2e8f0;
border-radius: 6px;
font-size: 12px;
transition: background 0.12s, border-color 0.12s;
&:hover { background: #f8fafc; border-color: #93c5fd; }
transition:
background 0.12s,
border-color 0.12s;
&:hover {
background: #f8fafc;
border-color: #93c5fd;
}
}
}
@@ -161,8 +242,19 @@
border-radius: 6px;
}
.ec-flag-label { font-size: 13px; font-weight: 500; color: #1e293b; display: block; }
.ec-flag-sub { font-size: 11px; color: #94a3b8; display: block; margin-top: 1px; }
.ec-flag-label {
font-size: 13px;
font-weight: 500;
color: #1e293b;
display: block;
}
.ec-flag-sub {
font-size: 11px;
color: #94a3b8;
display: block;
margin-top: 1px;
}
.ec-reg-note {
margin-top: 16px;
@@ -173,7 +265,10 @@
font-size: 12px;
color: #0c4a6e;
line-height: 1.6;
strong { font-weight: 600; }
strong {
font-weight: 600;
}
}
.ec-card-actions {
@@ -182,7 +277,12 @@
margin-top: 16px;
padding-top: 12px;
border-top: 1px solid #f1f5f9;
button { display: flex; align-items: center; gap: 6px; }
button {
display: flex;
align-items: center;
gap: 6px;
}
}
.ec-info-banner {
@@ -198,11 +298,27 @@
margin-top: 12px;
}
@media (width <= 900px) { .ec-grid-3 { grid-template-columns: repeat(2, 1fr); } }
@media (width <= 900px) {
.ec-grid-3 {
grid-template-columns: repeat(2, 1fr);
}
}
@media (width <= 600px) {
.ec-grid-3, .ec-grid-2 { grid-template-columns: 1fr; }
.ec-check-grid { grid-template-columns: 1fr; }
.ec-card-head { padding: 12px 14px; }
.ec-card-body { padding: 14px; }
.ec-grid-3,
.ec-grid-2 {
grid-template-columns: 1fr;
}
.ec-check-grid {
grid-template-columns: 1fr;
}
.ec-card-head {
padding: 12px 14px;
}
.ec-card-body {
padding: 14px;
}
}
@@ -1,5 +1,9 @@
/**
* 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, Input, OnInit, OnChanges, SimpleChanges, inject } from '@angular/core';
@@ -54,13 +58,69 @@ export class ClientEntityKycStepComponent implements OnInit, OnChanges {
/** Category metadata */
categories: CategoryMeta[] = [
{ id: 'msme', label: 'MSME', tag: 'Udyam / MSME', tagColor: '#0A5C6B', tagBg: '#E6F4F6', desc: 'Micro, Small & Medium Enterprise — Ministry of MSME / Udyam Registration', icon: 'cogs' },
{ id: 'startup', label: 'Startup India Registered', tag: 'DPIIT', tagColor: '#1A5FAB', tagBg: '#EAF2FB', desc: 'DPIIT recognition under Startup India initiative', icon: 'rocket' },
{ id: 'mudra', label: 'Mudra Borrower', tag: 'PMMY', tagColor: '#8A5A00', tagBg: '#FEF3DC', desc: 'Pradhan Mantri Mudra Yojana — SIDBI / Mudra scheme', icon: 'hand-holding-usd' },
{ id: 'eou', label: 'Export Oriented Unit', tag: 'EOU', tagColor: '#1B7A4A', tagBg: '#E8F5EE', desc: 'Customs-registered EOU under DGFT / Ministry of Commerce', icon: 'ship' },
{ id: 'importexport', label: 'Import Export Business', tag: 'DGFT / IEC', tagColor: '#5B3A8A', tagBg: '#F0EBF8', desc: 'Entity with valid DGFT Importer Exporter Code', icon: 'exchange-alt' },
{ id: 'psl', label: 'Priority Sector Borrower', tag: 'RBI PSL', tagColor: '#7B1F1F', tagBg: '#FCEAEA', desc: 'Eligible under RBI Priority Sector Lending guidelines', icon: 'bullseye' },
{ id: 'nps', label: 'Non-Priority Sector', tag: 'General', tagColor: '#444441', tagBg: '#F1EFE8', desc: 'Standard corporate — no special priority sector classification', icon: 'building' }
{
id: 'msme',
label: 'MSME',
tag: 'Udyam / MSME',
tagColor: '#0A5C6B',
tagBg: '#E6F4F6',
desc: 'Micro, Small & Medium Enterprise — Ministry of MSME / Udyam Registration',
icon: 'cogs'
},
{
id: 'startup',
label: 'Startup India Registered',
tag: 'DPIIT',
tagColor: '#1A5FAB',
tagBg: '#EAF2FB',
desc: 'DPIIT recognition under Startup India initiative',
icon: 'rocket'
},
{
id: 'mudra',
label: 'Mudra Borrower',
tag: 'PMMY',
tagColor: '#8A5A00',
tagBg: '#FEF3DC',
desc: 'Pradhan Mantri Mudra Yojana — SIDBI / Mudra scheme',
icon: 'hand-holding-usd'
},
{
id: 'eou',
label: 'Export Oriented Unit',
tag: 'EOU',
tagColor: '#1B7A4A',
tagBg: '#E8F5EE',
desc: 'Customs-registered EOU under DGFT / Ministry of Commerce',
icon: 'ship'
},
{
id: 'importexport',
label: 'Import Export Business',
tag: 'DGFT / IEC',
tagColor: '#5B3A8A',
tagBg: '#F0EBF8',
desc: 'Entity with valid DGFT Importer Exporter Code',
icon: 'exchange-alt'
},
{
id: 'psl',
label: 'Priority Sector Borrower',
tag: 'RBI PSL',
tagColor: '#7B1F1F',
tagBg: '#FCEAEA',
desc: 'Eligible under RBI Priority Sector Lending guidelines',
icon: 'bullseye'
},
{
id: 'nps',
label: 'Non-Priority Sector',
tag: 'General',
tagColor: '#444441',
tagBg: '#F1EFE8',
desc: 'Standard corporate — no special priority sector classification',
icon: 'building'
}
];
/** Toggle & expand state per category */
@@ -89,7 +149,15 @@ export class ClientEntityKycStepComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void {
if (changes['readOnly']) {
const forms = [this.msmeForm, this.startupForm, this.mudraForm, this.eouForm, this.importexportForm, this.pslForm, this.npsForm];
const forms = [
this.msmeForm,
this.startupForm,
this.mudraForm,
this.eouForm,
this.importexportForm,
this.pslForm,
this.npsForm
];
for (const form of forms) {
if (!form) continue;
if (this.readOnly) {
@@ -213,14 +281,22 @@ export class ClientEntityKycStepComponent implements OnInit, OnChanges {
getForm(categoryId: string): UntypedFormGroup {
switch (categoryId) {
case 'msme': return this.msmeForm;
case 'startup': return this.startupForm;
case 'mudra': return this.mudraForm;
case 'eou': return this.eouForm;
case 'importexport': return this.importexportForm;
case 'psl': return this.pslForm;
case 'nps': return this.npsForm;
default: return this.fb.group({});
case 'msme':
return this.msmeForm;
case 'startup':
return this.startupForm;
case 'mudra':
return this.mudraForm;
case 'eou':
return this.eouForm;
case 'importexport':
return this.importexportForm;
case 'psl':
return this.pslForm;
case 'nps':
return this.npsForm;
default:
return this.fb.group({});
}
}
@@ -236,7 +312,7 @@ export class ClientEntityKycStepComponent implements OnInit, OnChanges {
}
get enabledCategories(): CategoryMeta[] {
return this.categories.filter(c => this.enabled[c.id]);
return this.categories.filter((c) => this.enabled[c.id]);
}
get enabledCount(): number {
@@ -266,7 +342,7 @@ export class ClientEntityKycStepComponent implements OnInit, OnChanges {
next: () => {
this.saving[categoryId] = false;
this.saved[categoryId] = true;
setTimeout(() => this.saved[categoryId] = false, 3000);
setTimeout(() => (this.saved[categoryId] = false), 3000);
},
error: () => {
this.saving[categoryId] = false;
@@ -306,6 +382,10 @@ export class ClientEntityKycStepComponent implements OnInit, OnChanges {
}
// Keep old getter for backward compatibility
get entityKycData(): any { return this.classificationData; }
get hasEntityKycData(): boolean { return this.hasClassificationData; }
get entityKycData(): any {
return this.classificationData;
}
get hasEntityKycData(): boolean {
return this.hasClassificationData;
}
}
@@ -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/.
-->
<!--
Copyright since 2025 Mifos Initiative
-->
@@ -10,7 +18,6 @@
<mat-dialog-content>
<form [formGroup]="familyMemberForm">
<!-- ═══ VIEW MODE ═══ -->
@if (isViewMode) {
<!-- Linked client card -->
@@ -18,20 +25,31 @@
<div class="fm-linked-card">
<div class="fm-linked-card__left">
@if (linkedProfileImage) {
<img [src]="linkedProfileImage" class="fm-linked-card__photo zoomable" alt="Profile"
(click)="zoomImage(linkedProfileImage, 'Client Photograph')" />
<img
[src]="linkedProfileImage"
class="fm-linked-card__photo zoomable"
alt="Profile"
(click)="zoomImage(linkedProfileImage, 'Client Photograph')"
/>
} @else {
<mat-icon class="fm-linked-card__icon">person</mat-icon>
}
</div>
<div class="fm-linked-card__info">
<span class="fm-linked-card__name">{{ selectedClient.displayName }}</span>
<span class="fm-linked-card__meta">Client ID: {{ selectedClient.id }}{{ selectedClient.accountNumber ? ' · A/C: ' + selectedClient.accountNumber : '' }}</span>
<span class="fm-linked-card__meta"
>Client ID: {{ selectedClient.id
}}{{ selectedClient.accountNumber ? ' · A/C: ' + selectedClient.accountNumber : '' }}</span
>
</div>
@if (linkedSignatureImage) {
<div class="fm-linked-card__sig">
<img [src]="linkedSignatureImage" alt="Signature" class="zoomable"
(click)="zoomImage(linkedSignatureImage, 'Specimen Signature')" />
<img
[src]="linkedSignatureImage"
alt="Signature"
class="zoomable"
(click)="zoomImage(linkedSignatureImage, 'Specimen Signature')"
/>
<span>Signature</span>
</div>
}
@@ -57,7 +75,11 @@
</div>
<div class="fm-readonly-item">
<span class="fm-readonly-label">Date of Birth</span>
<span class="fm-readonly-value">{{ familyMemberForm.get('dateOfBirth')?.value ? (familyMemberForm.get('dateOfBirth')?.value | date:'dd MMM yyyy') : '—' }}</span>
<span class="fm-readonly-value">{{
familyMemberForm.get('dateOfBirth')?.value
? (familyMemberForm.get('dateOfBirth')?.value | date: 'dd MMM yyyy')
: '—'
}}</span>
</div>
<div class="fm-readonly-item">
<span class="fm-readonly-label">Nationality</span>
@@ -89,19 +111,25 @@
<span class="fm-readonly-label">Status</span>
<span class="fm-readonly-value">
{{ familyMemberForm.get('isAlive')?.value !== false ? 'Alive' : 'Deceased' }}
@if (familyMemberForm.get('isDependent')?.value) { · Dependent }
@if (familyMemberForm.get('isDependent')?.value) {
· Dependent
}
</span>
</div>
@if (isSpouse && familyMemberForm.get('marriageDate')?.value) {
<div class="fm-readonly-item">
<span class="fm-readonly-label">Marriage Date</span>
<span class="fm-readonly-value">{{ familyMemberForm.get('marriageDate')?.value | date:'dd MMM yyyy' }}</span>
<span class="fm-readonly-value">{{
familyMemberForm.get('marriageDate')?.value | date: 'dd MMM yyyy'
}}</span>
</div>
}
@if (isSpouse && familyMemberForm.get('marriageAuthority')?.value) {
<div class="fm-readonly-item">
<span class="fm-readonly-label">Issuing Authority</span>
<span class="fm-readonly-value">{{ getMarriageAuthorityLabel(familyMemberForm.get('marriageAuthority')?.value) }}</span>
<span class="fm-readonly-value">{{
getMarriageAuthorityLabel(familyMemberForm.get('marriageAuthority')?.value)
}}</span>
</div>
}
</div>
@@ -136,7 +164,9 @@
</mat-form-field>
@if (searching) {
<div class="fm-search-results">
<div class="fm-search-loading"><mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner> Searching...</div>
<div class="fm-search-loading">
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner> Searching...
</div>
</div>
}
@if (!searching && searchResults.length > 0) {
@@ -146,7 +176,18 @@
<mat-icon class="fm-search-item__icon">person</mat-icon>
<div class="fm-search-item__info">
<span class="fm-search-item__name">{{ r.displayName }}</span>
<span class="fm-search-item__meta">ID: {{ r.id }} · {{ r.officeName }}@if (r.dateOfBirth) { · DOB: {{ r.dateOfBirth | dateFormat }} }@if (r.fatherSpouseName) { · {{ r.fatherSpouseName }} }@if (r.mobileNo) { · {{ r.mobileNo }} }</span>
<span class="fm-search-item__meta"
>ID: {{ r.id }} · {{ r.officeName }}
@if (r.dateOfBirth) {
· DOB: {{ r.dateOfBirth | dateFormat }}
}
@if (r.fatherSpouseName) {
· {{ r.fatherSpouseName }}
}
@if (r.mobileNo) {
· {{ r.mobileNo }}
}
</span>
</div>
</div>
}
@@ -162,24 +203,37 @@
<div class="fm-linked-card">
<div class="fm-linked-card__left">
@if (linkedProfileImage) {
<img [src]="linkedProfileImage" class="fm-linked-card__photo zoomable" alt="Profile"
(click)="zoomImage(linkedProfileImage, 'Client Photograph')" />
<img
[src]="linkedProfileImage"
class="fm-linked-card__photo zoomable"
alt="Profile"
(click)="zoomImage(linkedProfileImage, 'Client Photograph')"
/>
} @else {
<mat-icon class="fm-linked-card__icon">person</mat-icon>
}
</div>
<div class="fm-linked-card__info">
<span class="fm-linked-card__name">{{ selectedClient.displayName }}</span>
<span class="fm-linked-card__meta">Client ID: {{ selectedClient.id }}{{ selectedClient.accountNumber ? ' · A/C: ' + selectedClient.accountNumber : '' }}</span>
<span class="fm-linked-card__meta"
>Client ID: {{ selectedClient.id
}}{{ selectedClient.accountNumber ? ' · A/C: ' + selectedClient.accountNumber : '' }}</span
>
</div>
@if (linkedSignatureImage) {
<div class="fm-linked-card__sig">
<img [src]="linkedSignatureImage" alt="Signature" class="zoomable"
(click)="zoomImage(linkedSignatureImage, 'Specimen Signature')" />
<img
[src]="linkedSignatureImage"
alt="Signature"
class="zoomable"
(click)="zoomImage(linkedSignatureImage, 'Specimen Signature')"
/>
<span>Signature</span>
</div>
}
<button mat-icon-button type="button" (click)="removeLinkedClient()" class="fm-linked-card__remove"><mat-icon>close</mat-icon></button>
<button mat-icon-button type="button" (click)="removeLinkedClient()" class="fm-linked-card__remove">
<mat-icon>close</mat-icon>
</button>
</div>
@if (fetchingLinkedData) {
@@ -198,7 +252,11 @@
</div>
<div class="fm-readonly-item">
<span class="fm-readonly-label">Date of Birth</span>
<span class="fm-readonly-value">{{ familyMemberForm.get('dateOfBirth')?.value ? (familyMemberForm.get('dateOfBirth')?.value | date:'dd MMM yyyy') : '—' }}</span>
<span class="fm-readonly-value">{{
familyMemberForm.get('dateOfBirth')?.value
? (familyMemberForm.get('dateOfBirth')?.value | date: 'dd MMM yyyy')
: '—'
}}</span>
</div>
<div class="fm-readonly-item">
<span class="fm-readonly-label">Aadhaar</span>
@@ -292,8 +350,13 @@
<button type="button" mat-stroked-button class="fm-file-btn" (click)="certInput.click()">
<mat-icon>upload_file</mat-icon> Upload Marriage Certificate
</button>
<input #certInput type="file" hidden accept=".pdf,.jpg,.jpeg,.png"
(change)="onMarriageCertSelected($event)" />
<input
#certInput
type="file"
hidden
accept=".pdf,.jpg,.jpeg,.png"
(change)="onMarriageCertSelected($event)"
/>
<span class="fm-file-hint">PDF, JPG or PNG (max 5 MB)</span>
} @else {
<div class="fm-file-attached">
@@ -313,15 +376,32 @@
<div class="fm-grid">
<mat-form-field appearance="outline">
<mat-label>Aadhaar Number</mat-label>
<input matInput formControlName="aadhaarNumber" maxlength="12" placeholder="12-digit Aadhaar"
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()" />
<input
matInput
formControlName="aadhaarNumber"
maxlength="12"
placeholder="12-digit Aadhaar"
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()"
/>
</mat-form-field>
<mat-form-field appearance="outline">
<mat-label>PAN Number</mat-label>
<input matInput formControlName="panNumber" maxlength="10" placeholder="ABCDE1234F" style="text-transform:uppercase"
(input)="familyMemberForm.get('panNumber')?.setValue($any($event.target).value.toUpperCase(), {emitEvent:false})" />
@if (familyMemberForm.get('panNumber')?.hasError('pattern') && familyMemberForm.get('panNumber')?.touched) {
<input
matInput
formControlName="panNumber"
maxlength="10"
placeholder="ABCDE1234F"
style="text-transform: uppercase"
(input)="
familyMemberForm
.get('panNumber')
?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
"
/>
@if (
familyMemberForm.get('panNumber')?.hasError('pattern') && familyMemberForm.get('panNumber')?.touched
) {
<mat-error>Invalid PAN format</mat-error>
}
</mat-form-field>
@@ -348,9 +428,13 @@
<button mat-flat-button color="primary" mat-dialog-close type="button">Close</button>
} @else {
<button mat-stroked-button mat-dialog-close type="button">Cancel</button>
<button mat-flat-button color="primary" type="button"
<button
mat-flat-button
color="primary"
type="button"
[disabled]="familyMemberForm.invalid"
[mat-dialog-close]="{ member: familyMember }">
[mat-dialog-close]="{ member: familyMember }"
>
{{ data.context === 'Edit' ? 'Update' : 'Add Member' }}
</button>
}
@@ -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/.
*/
.fm-dialog__header {
display: flex;
align-items: center;
@@ -9,7 +17,9 @@
font-weight: 600;
color: #1e293b;
.fm-dialog__icon { color: #1074b9; }
.fm-dialog__icon {
color: #1074b9;
}
}
.fm-mode-toggle {
@@ -32,7 +42,11 @@
cursor: pointer;
transition: all 0.15s;
mat-icon { font-size: 18px; width: 18px; height: 18px; }
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
&.active {
border-color: #1074b9;
@@ -40,11 +54,19 @@
color: #1074b9;
}
&:hover:not(.active) { border-color: #94a3b8; }
&:hover:not(.active) {
border-color: #94a3b8;
}
}
.fm-search-wrapper { position: relative; margin-bottom: 12px; }
.fm-search-field { width: 100%; }
.fm-search-wrapper {
position: relative;
margin-bottom: 12px;
}
.fm-search-field {
width: 100%;
}
.fm-search-results {
position: absolute;
@@ -92,13 +114,34 @@
cursor: pointer;
border-bottom: 1px solid #f1f5f9;
&:last-child { border-bottom: none; }
&:hover { background: #f0f7ff; }
&:last-child {
border-bottom: none;
}
&__icon { color: #1074b9; font-size: 20px; }
&__info { display: flex; flex-direction: column; }
&__name { font-size: 13px; font-weight: 600; color: #1e293b; }
&__meta { font-size: 11px; color: #64748b; }
&:hover {
background: #f0f7ff;
}
&__icon {
color: #1074b9;
font-size: 20px;
}
&__info {
display: flex;
flex-direction: column;
}
&__name {
font-size: 13px;
font-weight: 600;
color: #1e293b;
}
&__meta {
font-size: 11px;
color: #64748b;
}
}
.fm-linked-card {
@@ -111,8 +154,13 @@
border-radius: 8px;
margin-bottom: 16px;
&__left { flex-shrink: 0; }
&__icon { color: #1074b9; }
&__left {
flex-shrink: 0;
}
&__icon {
color: #1074b9;
}
&__photo {
width: 40px;
@@ -121,9 +169,24 @@
object-fit: cover;
border: 2px solid #e2e8f0;
}
&__info { flex: 1; display: flex; flex-direction: column; }
&__name { font-size: 14px; font-weight: 600; color: #1e293b; }
&__meta { font-size: 12px; color: #64748b; font-family: 'Roboto Mono', monospace; }
&__info {
flex: 1;
display: flex;
flex-direction: column;
}
&__name {
font-size: 14px;
font-weight: 600;
color: #1e293b;
}
&__meta {
font-size: 12px;
color: #64748b;
font-family: 'Roboto Mono', monospace;
}
&__sig {
display: flex;
@@ -149,7 +212,14 @@
letter-spacing: 0.04em;
}
}
&__remove { color: #94a3b8 !important; &:hover { color: #ef4444 !important; } }
&__remove {
color: #94a3b8 !important;
&:hover {
color: #ef4444 !important;
}
}
}
.fm-fetching-hint {
@@ -204,8 +274,16 @@
padding: 10px 16px;
margin-bottom: 8px;
.fm-readonly-label { display: block; margin-bottom: 4px; }
.fm-readonly-value { font-size: 13px; color: #475569; line-height: 1.5; }
.fm-readonly-label {
display: block;
margin-bottom: 4px;
}
.fm-readonly-value {
font-size: 13px;
color: #475569;
line-height: 1.5;
}
}
.fm-kyc-badge {
@@ -266,7 +344,9 @@
.zoomable {
cursor: zoom-in;
transition: transform 0.15s ease, box-shadow 0.15s ease;
transition:
transform 0.15s ease,
box-shadow 0.15s ease;
&:hover {
transform: scale(1.08);
@@ -275,7 +355,11 @@
}
::ng-deep .image-zoom-panel {
.mat-mdc-dialog-container { padding: 0 !important; border-radius: 10px !important; overflow: hidden; }
.mat-mdc-dialog-container {
padding: 0 !important;
border-radius: 10px !important;
overflow: hidden;
}
}
.fm-file-upload {
@@ -294,7 +378,11 @@
color: #1074b9;
border-color: #cbd5e1;
mat-icon { font-size: 18px; width: 18px; height: 18px; }
mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
}
.fm-file-hint {
@@ -312,7 +400,10 @@
border-radius: 6px;
width: 100%;
&__icon { color: #1074b9; font-size: 20px; }
&__icon {
color: #1074b9;
font-size: 20px;
}
&__name {
flex: 1;
@@ -330,7 +421,14 @@
height: 28px !important;
line-height: 28px !important;
mat-icon { font-size: 16px; width: 16px; height: 16px; }
&:hover { color: #ef4444 !important; }
mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
}
&:hover {
color: #ef4444 !important;
}
}
}
@@ -87,7 +87,9 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
}
/** Whether dialog is in View-only mode */
get isViewMode(): boolean { return this.data.context === 'View'; }
get isViewMode(): boolean {
return this.data.context === 'View';
}
/** Get relationship name from ID */
get relationshipName(): string {
@@ -109,7 +111,7 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
/** Get marriage authority display label */
getMarriageAuthorityLabel(value: string): string {
return this.marriageAuthorityOptions.find(a => a.value === value)?.label || value || '—';
return this.marriageAuthorityOptions.find((a) => a.value === value)?.label || value || '—';
}
/** Marriage certificate file */
@@ -150,7 +152,10 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
});
if (m.linkedClientId) {
this.entryMode = 'linked';
this.selectedClient = { id: m.linkedClientId, displayName: m.linkedClientName || m.firstName || `Client #${m.linkedClientId}` };
this.selectedClient = {
id: m.linkedClientId,
displayName: m.linkedClientName || m.firstName || `Client #${m.linkedClientId}`
};
// Re-fetch linked client details (profile, signature, KYC status)
this.fetchLinkedClientData(m.linkedClientId.toString());
}
@@ -162,13 +167,22 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
createFamilyMemberForm() {
this.familyMemberForm = this.formBuilder.group({
firstName: ['', Validators.required],
relationshipId: ['', Validators.required],
firstName: [
'',
Validators.required
],
relationshipId: [
'',
Validators.required
],
dateOfBirth: [''],
isDependent: [false],
remarks: [''],
aadhaarNumber: [''],
panNumber: ['', Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)],
panNumber: [
'',
Validators.pattern(/^[A-Z]{5}[0-9]{4}[A-Z]{1}$/)
],
nationality: ['INDIAN'],
isAlive: [true],
marriageDate: [''],
@@ -208,7 +222,10 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
this.searching = false;
this.clientsService.enrichSearchResults(this.searchResults);
},
error: () => { this.searchResults = []; this.searching = false; }
error: () => {
this.searchResults = [];
this.searching = false;
}
});
}, 400);
}
@@ -242,7 +259,9 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
this.setLinkedFieldsReadonly(true);
this.fetchingLinkedData = false;
},
error: () => { this.fetchingLinkedData = false; }
error: () => {
this.fetchingLinkedData = false;
}
});
// Fetch KYC (PAN, Aadhaar, verification status)
@@ -250,8 +269,12 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
next: (kyc: any) => {
if (kyc) {
const patch: any = {};
if (kyc.panNumber) { patch.panNumber = kyc.panNumber; }
if (kyc.aadhaarNumberMasked) { patch.aadhaarNumber = kyc.aadhaarNumberMasked; }
if (kyc.panNumber) {
patch.panNumber = kyc.panNumber;
}
if (kyc.aadhaarNumberMasked) {
patch.aadhaarNumber = kyc.aadhaarNumberMasked;
}
this.familyMemberForm.patchValue(patch);
this.linkedKycStatus = {
panVerified: kyc.panVerified || false,
@@ -278,8 +301,8 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
// Fetch signature image
this.clientsService.getClientDocuments(clientId).subscribe({
next: (docs: any[]) => {
const sigDoc = docs?.find((d: any) =>
d.name?.toLowerCase().includes('signature') || d.description?.toLowerCase().includes('signature')
const sigDoc = docs?.find(
(d: any) => d.name?.toLowerCase().includes('signature') || d.description?.toLowerCase().includes('signature')
);
if (sigDoc) {
this.clientsService.getClientSignatureImage(clientId, sigDoc.id.toString()).subscribe({
@@ -296,8 +319,13 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
/** Disable/enable fields that are auto-populated from linked client */
private setLinkedFieldsReadonly(readonly: boolean): void {
const fields = ['firstName', 'dateOfBirth', 'aadhaarNumber', 'panNumber'];
fields.forEach(f => {
const fields = [
'firstName',
'dateOfBirth',
'aadhaarNumber',
'panNumber'
];
fields.forEach((f) => {
const ctrl = this.familyMemberForm.get(f);
if (ctrl) {
readonly ? ctrl.disable() : ctrl.enable();
@@ -313,7 +341,11 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
this.linkedKycStatus = null;
this.setLinkedFieldsReadonly(false);
this.familyMemberForm.patchValue({
firstName: '', linkedClientId: '', dateOfBirth: '', aadhaarNumber: '', panNumber: ''
firstName: '',
linkedClientId: '',
dateOfBirth: '',
aadhaarNumber: '',
panNumber: ''
});
}
@@ -385,26 +417,49 @@ export class ClientFamilyMemberDialogComponent implements OnInit {
</div>
</div>
`,
styles: [`
.img-zoom-dialog { display: flex; flex-direction: column; }
.img-zoom-header {
display: flex; align-items: center; justify-content: space-between;
padding: 10px 16px; background: #f8fafc; border-bottom: 1px solid #e2e8f0;
font-size: 14px; font-weight: 600; color: #1e293b;
}
.img-zoom-body {
display: flex; align-items: center; justify-content: center;
padding: 20px; background: #fff; min-height: 200px;
}
.img-zoom-body img {
max-width: 100%; max-height: 70vh; object-fit: contain;
border-radius: 6px; box-shadow: 0 2px 12px rgba(0,0,0,0.1);
}
`],
imports: [MatIcon, STANDALONE_SHARED_IMPORTS]
styles: [
`
.img-zoom-dialog {
display: flex;
flex-direction: column;
}
.img-zoom-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 16px;
background: #f8fafc;
border-bottom: 1px solid #e2e8f0;
font-size: 14px;
font-weight: 600;
color: #1e293b;
}
.img-zoom-body {
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
background: #fff;
min-height: 200px;
}
.img-zoom-body img {
max-width: 100%;
max-height: 70vh;
object-fit: contain;
border-radius: 6px;
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
}
`
],
imports: [
MatIcon,
STANDALONE_SHARED_IMPORTS
]
})
export class ImageZoomDialogComponent {
dialogRef = inject<MatDialogRef<ImageZoomDialogComponent>>(MatDialogRef);
data = inject(MAT_DIALOG_DATA);
close() { this.dialogRef.close(); }
close() {
this.dialogRef.close();
}
}
@@ -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/.
-->
<!--
Copyright since 2025 Mifos Initiative
-->
@@ -75,9 +83,13 @@
{{ member.dateOfBirth ? (member.dateOfBirth | dateFormat) : '—' }}
</span>
<span class="fm-table__col fm-table__col--id fm-mono">
@if (member.panNumber) { {{ member.panNumber }} }
@else if (member.aadhaarNumber) { {{ member.aadhaarNumber }} }
@else { — }
@if (member.panNumber) {
{{ member.panNumber }}
} @else if (member.aadhaarNumber) {
{{ member.aadhaarNumber }}
} @else {
—
}
</span>
<span class="fm-table__col fm-table__col--status">
@if (member.isAlive !== false) {
@@ -87,14 +99,32 @@
}
</span>
<span class="fm-table__col fm-table__col--actions">
<button mat-icon-button type="button" (click)="viewFamilyMember(member)" class="fm-action-btn" matTooltip="View">
<button
mat-icon-button
type="button"
(click)="viewFamilyMember(member)"
class="fm-action-btn"
matTooltip="View"
>
<mat-icon>visibility</mat-icon>
</button>
@if (!readOnly) {
<button mat-icon-button type="button" (click)="editFamilyMember(member, i)" class="fm-action-btn" matTooltip="Edit">
<button
mat-icon-button
type="button"
(click)="editFamilyMember(member, i)"
class="fm-action-btn"
matTooltip="Edit"
>
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button type="button" (click)="deleteFamilyMember(member.firstName, i)" class="fm-action-btn fm-action-btn--delete" matTooltip="Delete">
<button
mat-icon-button
type="button"
(click)="deleteFamilyMember(member.firstName, i)"
class="fm-action-btn fm-action-btn--delete"
matTooltip="Delete"
>
<mat-icon>delete</mat-icon>
</button>
}
@@ -103,5 +133,4 @@
}
</div>
}
</div>
@@ -1,11 +1,29 @@
/**
* 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/.
*/
.zoomable {
cursor: zoom-in;
transition: transform 0.15s ease, box-shadow 0.15s ease;
&:hover { transform: scale(1.08); box-shadow: 0 4px 16px rgb(0 0 0 / 18%); }
transition:
transform 0.15s ease,
box-shadow 0.15s ease;
&:hover {
transform: scale(1.08);
box-shadow: 0 4px 16px rgb(0 0 0 / 18%);
}
}
::ng-deep .image-zoom-panel {
.mat-mdc-dialog-container { padding: 0 !important; border-radius: 10px !important; overflow: hidden; }
.mat-mdc-dialog-container {
padding: 0 !important;
border-radius: 10px !important;
overflow: hidden;
}
}
.search-no-results-hint {
@@ -63,7 +81,10 @@
color: #1e293b;
}
.fm-step__icon { color: #1074b9; font-size: 14px; }
.fm-step__icon {
color: #1074b9;
font-size: 14px;
}
.fm-step__count {
background: #1074b9;
@@ -170,8 +191,13 @@
border-bottom: 1px solid #f1f5f9;
transition: background 0.1s;
&:last-child { border-bottom: none; }
&:hover { background: #f8fafc; }
&:last-child {
border-bottom: none;
}
&:hover {
background: #f8fafc;
}
}
.fm-table__col {
@@ -186,11 +212,31 @@
gap: 8px;
min-width: 0;
}
&--rel { flex: 0 0 130px; }
&--dob { flex: 0 0 120px; color: #64748b; font-size: 12px; }
&--id { flex: 0 0 140px; }
&--status { flex: 0 0 60px; text-align: center; }
&--actions { flex: 0 0 80px; display: flex; gap: 2px; }
&--rel {
flex: 0 0 130px;
}
&--dob {
flex: 0 0 120px;
color: #64748b;
font-size: 12px;
}
&--id {
flex: 0 0 140px;
}
&--status {
flex: 0 0 60px;
text-align: center;
}
&--actions {
flex: 0 0 80px;
display: flex;
gap: 2px;
}
}
.fm-table__avatar {
@@ -232,8 +278,15 @@
padding: 2px 8px;
border-radius: 10px;
&--yes { background: #ecfdf5; color: #059669; }
&--no { background: #fef2f2; color: #dc2626; }
&--yes {
background: #ecfdf5;
color: #059669;
}
&--no {
background: #fef2f2;
color: #dc2626;
}
}
.fm-action-btn {
@@ -248,8 +301,13 @@
color: #64748b;
}
&:hover mat-icon { color: #1074b9; }
&--delete:hover mat-icon { color: #ef4444; }
&:hover mat-icon {
color: #1074b9;
}
&--delete:hover mat-icon {
color: #ef4444;
}
}
@media (width <= 768px) {
@@ -260,7 +318,10 @@
}
.fm-table__col {
&--type, &--id { display: none; }
&--type,
&--id {
display: none;
}
}
}
@@ -284,11 +345,27 @@
gap: 10px;
}
.nom-header__icon { font-size: 16px; color: #1565c0; }
.nom-header__title { font-size: 16px; font-weight: 600; color: #1e293b; display: block; }
.nom-header__sub { font-size: 11px; color: #94a3b8; display: block; }
.nom-header__icon {
font-size: 16px;
color: #1565c0;
}
.nom-add-btn { font-size: 13px; }
.nom-header__title {
font-size: 16px;
font-weight: 600;
color: #1e293b;
display: block;
}
.nom-header__sub {
font-size: 11px;
color: #94a3b8;
display: block;
}
.nom-add-btn {
font-size: 13px;
}
/* Share bar */
.nom-share-bar {
@@ -302,7 +379,11 @@
border-radius: 6px;
}
.nom-share-bar__label { font-size: 12px; color: #64748b; white-space: nowrap; }
.nom-share-bar__label {
font-size: 12px;
color: #64748b;
white-space: nowrap;
}
.nom-share-bar__track {
flex: 1;
@@ -317,8 +398,14 @@
background: #1565c0;
border-radius: 4px;
transition: width 0.3s;
&.complete { background: #059669; }
&.over { background: #dc2626; }
&.complete {
background: #059669;
}
&.over {
background: #dc2626;
}
}
.nom-share-bar__value {
@@ -327,7 +414,10 @@
color: #1565c0;
min-width: 36px;
text-align: right;
&.over { color: #dc2626; }
&.over {
color: #dc2626;
}
}
/* Nominee card */
@@ -346,24 +436,62 @@
}
.nom-card__avatar {
width: 36px; height: 36px;
width: 36px;
height: 36px;
border-radius: 50%;
background: #dbeafe;
display: flex; align-items: center; justify-content: center;
display: flex;
align-items: center;
justify-content: center;
color: #1565c0;
mat-icon { font-size: 20px; }
mat-icon {
font-size: 20px;
}
}
.nom-card__photo { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
.nom-card__info { flex: 1; }
.nom-card__name { font-weight: 600; font-size: 14px; color: #1e293b; display: block; }
.nom-card__meta { font-size: 12px; color: #64748b; display: block; margin-top: 2px; }
.nom-card__photo {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 50%;
}
.nom-card__info {
flex: 1;
}
.nom-card__name {
font-weight: 600;
font-size: 14px;
color: #1e293b;
display: block;
}
.nom-card__meta {
font-size: 12px;
color: #64748b;
display: block;
margin-top: 2px;
}
.nom-card__sig {
flex-shrink: 0;
img { height: 24px; max-width: 60px; object-fit: contain; border: 1px solid #e2e8f0; border-radius: 4px; padding: 2px; }
img {
height: 24px;
max-width: 60px;
object-fit: contain;
border: 1px solid #e2e8f0;
border-radius: 4px;
padding: 2px;
}
}
.nom-card__actions {
display: flex;
gap: 2px;
}
.nom-card__actions { display: flex; gap: 2px; }
.nom-card__edit {
padding: 12px 16px;
@@ -392,8 +520,18 @@
min-width: 50px;
}
.nom-card__share-value { font-size: 18px; font-weight: 700; color: #1565c0; display: block; }
.nom-card__share-label { font-size: 10px; color: #94a3b8; text-transform: uppercase; }
.nom-card__share-value {
font-size: 18px;
font-weight: 700;
color: #1565c0;
display: block;
}
.nom-card__share-label {
font-size: 10px;
color: #94a3b8;
text-transform: uppercase;
}
.nom-card__guardian {
display: flex;
@@ -404,7 +542,13 @@
border-top: 1px solid #e0f2fe;
font-size: 12px;
color: #0c4a6e;
mat-icon { font-size: 16px; width: 16px; height: 16px; color: #0ea5e9; }
mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
color: #0ea5e9;
}
}
/* Empty */
@@ -421,8 +565,16 @@
font-size: 14px;
}
.nom-empty__icon { font-size: 36px; width: 36px; height: 36px; color: #cbd5e1; }
.nom-empty__sub { font-size: 12px; }
.nom-empty__icon {
font-size: 36px;
width: 36px;
height: 36px;
color: #cbd5e1;
}
.nom-empty__sub {
font-size: 12px;
}
/* Nominee form */
.nom-form {
@@ -444,20 +596,30 @@
}
/* Search */
.nom-search-wrapper { position: relative; }
.nom-search-field { width: 100%; }
.nom-search-wrapper {
position: relative;
}
.nom-search-field {
width: 100%;
}
.nom-search-prefix {
display: inline-flex;
align-items: center;
padding-right: 8px;
mat-icon { font-size: 20px; color: #94a3b8; }
mat-icon {
font-size: 20px;
color: #94a3b8;
}
}
.nom-search-results {
position: absolute;
top: 100%;
left: 0; right: 0;
left: 0;
right: 0;
z-index: 10;
background: #fff;
border: 1px solid #e2e8f0;
@@ -469,9 +631,12 @@
}
.nom-search-loading {
display: flex; align-items: center; gap: 8px;
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
font-size: 13px; color: #64748b;
font-size: 13px;
color: #64748b;
}
.nom-search-item {
@@ -480,14 +645,43 @@
gap: 10px;
padding: 10px 16px;
cursor: pointer;
&:hover { background: #f1f5f9; }
&:hover {
background: #f1f5f9;
}
}
.nom-search-item__icon { color: #94a3b8; font-size: 20px; }
.nom-search-item__info { flex: 1; }
.nom-search-item__name { font-weight: 500; font-size: 13px; color: #1e293b; display: block; }
.nom-search-item__meta { font-size: 11px; color: #94a3b8; display: block; }
.nom-search-item__status { font-size: 11px; font-weight: 600; color: #94a3b8; &.active { color: #22c55e; } }
.nom-search-item__icon {
color: #94a3b8;
font-size: 20px;
}
.nom-search-item__info {
flex: 1;
}
.nom-search-item__name {
font-weight: 500;
font-size: 13px;
color: #1e293b;
display: block;
}
.nom-search-item__meta {
font-size: 11px;
color: #94a3b8;
display: block;
}
.nom-search-item__status {
font-size: 11px;
font-weight: 600;
color: #94a3b8;
&.active {
color: #22c55e;
}
}
/* Selected nominee */
.nom-selected {
@@ -507,31 +701,92 @@
}
.nom-selected__avatar {
width: 40px; height: 40px;
width: 40px;
height: 40px;
border-radius: 50%;
background: #dbeafe;
display: flex; align-items: center; justify-content: center;
display: flex;
align-items: center;
justify-content: center;
color: #1565c0;
overflow: hidden;
}
.nom-selected__photo { width: 100%; height: 100%; object-fit: cover; }
.nom-selected__info { flex: 1; }
.nom-selected__name { font-weight: 600; font-size: 14px; color: #1e293b; display: block; }
.nom-selected__meta { font-size: 12px; color: #64748b; display: block; }
.nom-selected__photo {
width: 100%;
height: 100%;
object-fit: cover;
}
.nom-selected__info {
flex: 1;
}
.nom-selected__name {
font-weight: 600;
font-size: 14px;
color: #1e293b;
display: block;
}
.nom-selected__meta {
font-size: 12px;
color: #64748b;
display: block;
}
.nom-selected__sig {
display: flex; flex-direction: column; align-items: center; gap: 2px; flex-shrink: 0;
img { height: 28px; max-width: 70px; object-fit: contain; border: 1px solid #e2e8f0; border-radius: 4px; background: #fff; padding: 2px 4px; }
span { font-size: 9px; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.04em; }
display: flex;
flex-direction: column;
align-items: center;
gap: 2px;
flex-shrink: 0;
img {
height: 28px;
max-width: 70px;
object-fit: contain;
border: 1px solid #e2e8f0;
border-radius: 4px;
background: #fff;
padding: 2px 4px;
}
span {
font-size: 9px;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.04em;
}
}
.nom-selected__remove {
color: #94a3b8;
}
.nom-selected__loading {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
font-size: 12px;
color: #64748b;
}
.nom-selected__remove { color: #94a3b8; }
.nom-selected__loading { display: flex; align-items: center; gap: 8px; padding: 12px 16px; font-size: 12px; color: #64748b; }
.nom-kyc-badge {
display: inline-flex; align-items: center; margin-left: 4px;
mat-icon { font-size: 14px; width: 14px; height: 14px; }
&.verified { color: #16a34a; }
display: inline-flex;
align-items: center;
margin-left: 4px;
mat-icon {
font-size: 14px;
width: 14px;
height: 14px;
}
&.verified {
color: #16a34a;
}
}
.nom-selected__details {
@@ -542,10 +797,29 @@
flex-wrap: wrap;
}
.nom-detail { display: flex; flex-direction: column; gap: 2px; }
.nom-detail__label { font-size: 10px; text-transform: uppercase; color: #94a3b8; letter-spacing: 0.5px; }
.nom-detail__value { font-size: 13px; font-weight: 500; color: #1e293b; }
.nom-detail__value.mono { font-family: 'Roboto Mono', monospace; font-size: 12px; }
.nom-detail {
display: flex;
flex-direction: column;
gap: 2px;
}
.nom-detail__label {
font-size: 10px;
text-transform: uppercase;
color: #94a3b8;
letter-spacing: 0.5px;
}
.nom-detail__value {
font-size: 13px;
font-weight: 500;
color: #1e293b;
}
.nom-detail__value.mono {
font-family: 'Roboto Mono', monospace;
font-size: 12px;
}
/* Form fields */
.nom-form-fields {
@@ -556,8 +830,13 @@
align-items: center;
}
.nom-form-field { width: 100%; }
.nom-minor-toggle { padding-top: 8px; }
.nom-form-field {
width: 100%;
}
.nom-minor-toggle {
padding-top: 8px;
}
/* Guardian */
.nom-guardian-section {
@@ -573,7 +852,11 @@
font-weight: 600;
color: #0c4a6e;
margin-bottom: 10px;
mat-icon { font-size: 18px; color: #0ea5e9; }
mat-icon {
font-size: 18px;
color: #0ea5e9;
}
}
.nom-guardian-card {
@@ -584,13 +867,32 @@
background: #f0f9ff;
border: 1px solid #bae6fd;
border-radius: 8px;
mat-icon { color: #0ea5e9; }
mat-icon {
color: #0ea5e9;
}
}
.nom-guardian-card__info { flex: 1; }
.nom-guardian-card__name { font-weight: 600; font-size: 13px; color: #1e293b; display: block; }
.nom-guardian-card__meta { font-size: 11px; color: #64748b; display: block; }
.nom-guardian-rel { width: 160px; }
.nom-guardian-card__info {
flex: 1;
}
.nom-guardian-card__name {
font-weight: 600;
font-size: 13px;
color: #1e293b;
display: block;
}
.nom-guardian-card__meta {
font-size: 11px;
color: #64748b;
display: block;
}
.nom-guardian-rel {
width: 160px;
}
/* Actions */
.nom-form-actions {
@@ -599,11 +901,24 @@
gap: 10px;
padding: 12px 16px;
border-top: 1px solid #f1f5f9;
button { display: flex; align-items: center; gap: 6px; }
button {
display: flex;
align-items: center;
gap: 6px;
}
}
@media (width <= 768px) {
.nom-form-fields { grid-template-columns: 1fr; }
.nom-guardian-card { flex-wrap: wrap; }
.nom-guardian-rel { width: 100%; }
.nom-form-fields {
grid-template-columns: 1fr;
}
.nom-guardian-card {
flex-wrap: wrap;
}
.nom-guardian-rel {
width: 100%;
}
}
@@ -1,5 +1,9 @@
/**
* 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, Input, OnInit, inject } from '@angular/core';
@@ -88,9 +92,20 @@ export class ClientFamilyMembersStepComponent implements OnInit {
/** Relationship options */
relationshipOptions = [
'Father', 'Mother', 'Spouse', 'Son', 'Daughter',
'Brother', 'Sister', 'Uncle', 'Aunt', 'Grandfather',
'Grandmother', 'Friend', 'Business Partner', 'Other'
'Father',
'Mother',
'Spouse',
'Son',
'Daughter',
'Brother',
'Sister',
'Uncle',
'Aunt',
'Grandfather',
'Grandmother',
'Friend',
'Business Partner',
'Other'
];
ngOnInit(): void {
@@ -109,14 +124,18 @@ export class ClientFamilyMembersStepComponent implements OnInit {
this.loaded = true;
this.clientFamilyMembers = Array.isArray(data) ? data : [];
},
error: () => { this.loaded = true; }
error: () => {
this.loaded = true;
}
});
}
addFamilyMember() {
const ref = this.dialog.open(ClientFamilyMemberDialogComponent, {
data: { context: 'Add', options: this.clientTemplate.familyMemberOptions },
width: '640px', maxWidth: '95vw', panelClass: 'fm-dialog-panel'
width: '640px',
maxWidth: '95vw',
panelClass: 'fm-dialog-panel'
});
ref.afterClosed().subscribe((response: any) => {
if (response?.member) {
@@ -128,14 +147,18 @@ export class ClientFamilyMembersStepComponent implements OnInit {
viewFamilyMember(member: any) {
this.dialog.open(ClientFamilyMemberDialogComponent, {
data: { context: 'View', member, options: this.clientTemplate.familyMemberOptions },
width: '640px', maxWidth: '95vw', panelClass: 'fm-dialog-panel'
width: '640px',
maxWidth: '95vw',
panelClass: 'fm-dialog-panel'
});
}
editFamilyMember(member: any, index: number) {
const ref = this.dialog.open(ClientFamilyMemberDialogComponent, {
data: { context: 'Edit', member, options: this.clientTemplate.familyMemberOptions },
width: '640px', maxWidth: '95vw', panelClass: 'fm-dialog-panel'
width: '640px',
maxWidth: '95vw',
panelClass: 'fm-dialog-panel'
});
ref.afterClosed().subscribe((response: any) => {
if (response?.member) {
@@ -175,8 +198,7 @@ export class ClientFamilyMembersStepComponent implements OnInit {
return this.clientFamilyMembers.some((m: any) => {
const rel = m.relationship || m.relationshipId;
const relName = typeof rel === 'string' ? rel : '';
return relName.toLowerCase() === 'spouse' ||
m.firstName?.toLowerCase().includes('spouse');
return relName.toLowerCase() === 'spouse' || m.firstName?.toLowerCase().includes('spouse');
});
}
@@ -191,14 +213,19 @@ export class ClientFamilyMembersStepComponent implements OnInit {
const allMembers = Array.isArray(data) ? data : [];
this.nominees = allMembers.filter((m: any) => m.nominationType && m.nominationType !== '');
},
error: () => { this.nomineesLoaded = true; }
error: () => {
this.nomineesLoaded = true;
}
});
}
onNomineeSearch(text: string): void {
this.nomineeSearchText = text;
clearTimeout(this.nomineeSearchTimer);
if (!text || text.length < 2) { this.nomineeSearchResults = []; return; }
if (!text || text.length < 2) {
this.nomineeSearchResults = [];
return;
}
this.nomineeSearching = true;
this.nomineeSearchTimer = setTimeout(() => {
this.clientsService.searchByText(text, 0, 10).subscribe({
@@ -207,7 +234,10 @@ export class ClientFamilyMembersStepComponent implements OnInit {
this.nomineeSearching = false;
this.clientsService.enrichSearchResults(this.nomineeSearchResults);
},
error: () => { this.nomineeSearchResults = []; this.nomineeSearching = false; }
error: () => {
this.nomineeSearchResults = [];
this.nomineeSearching = false;
}
});
}, 400);
}
@@ -232,7 +262,9 @@ export class ClientFamilyMembersStepComponent implements OnInit {
// Fetch client data for DOB
this.clientsService.getClientData(clientId.toString()).subscribe({
next: (data: any) => {
if (data?.dateOfBirth) { details.dateOfBirth = data.dateOfBirth; }
if (data?.dateOfBirth) {
details.dateOfBirth = data.dateOfBirth;
}
done();
},
error: () => done()
@@ -253,7 +285,9 @@ export class ClientFamilyMembersStepComponent implements OnInit {
// Fetch profile image
this.clientsService.getClientProfileImage(clientId.toString()).subscribe({
next: (img: any) => {
if (img) { details.profileImageUrl = this.sanitizer.bypassSecurityTrustResourceUrl(img); }
if (img) {
details.profileImageUrl = this.sanitizer.bypassSecurityTrustResourceUrl(img);
}
done();
},
error: () => done()
@@ -262,8 +296,8 @@ export class ClientFamilyMembersStepComponent implements OnInit {
// Fetch signature image
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')
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({
@@ -273,7 +307,9 @@ export class ClientFamilyMembersStepComponent implements OnInit {
},
error: () => done()
});
} else { done(); }
} else {
done();
}
},
error: () => done()
});
@@ -282,7 +318,10 @@ export class ClientFamilyMembersStepComponent implements OnInit {
onGuardianSearch(text: string): void {
this.guardianSearchText = text;
clearTimeout(this.guardianSearchTimer);
if (!text || text.length < 2) { this.guardianSearchResults = []; return; }
if (!text || text.length < 2) {
this.guardianSearchResults = [];
return;
}
this.guardianSearching = true;
this.guardianSearchTimer = setTimeout(() => {
this.clientsService.searchByText(text, 0, 10).subscribe({
@@ -291,7 +330,10 @@ export class ClientFamilyMembersStepComponent implements OnInit {
this.guardianSearching = false;
this.clientsService.enrichSearchResults(this.guardianSearchResults);
},
error: () => { this.guardianSearchResults = []; this.guardianSearching = false; }
error: () => {
this.guardianSearchResults = [];
this.guardianSearching = false;
}
});
}, 400);
}
@@ -315,7 +357,8 @@ export class ClientFamilyMembersStepComponent implements OnInit {
}
saveNominee(): void {
if (!this.clientId || !this.selectedNomineeClient || !this.nomineeRelationshipId || !this.nomineePercentageShare) return;
if (!this.clientId || !this.selectedNomineeClient || !this.nomineeRelationshipId || !this.nomineePercentageShare)
return;
if (this.totalSharePercentage > 100) return;
this.nomineeSaving = true;
@@ -360,7 +403,9 @@ export class ClientFamilyMembersStepComponent implements OnInit {
});
this.resetNomineeForm();
},
error: () => { this.nomineeSaving = false; }
error: () => {
this.nomineeSaving = false;
}
});
}
@@ -383,13 +428,13 @@ export class ClientFamilyMembersStepComponent implements OnInit {
/** Max share allowed for the nominee being edited (excludes its own share) */
maxShareForEdit(index: number): number {
const otherTotal = this.nominees.reduce((sum, n, i) => i === index ? sum : sum + (n.percentageShare || 0), 0);
const otherTotal = this.nominees.reduce((sum, n, i) => (i === index ? sum : sum + (n.percentageShare || 0)), 0);
return 100 - otherTotal;
}
saveEditNominee(nominee: any, index: number): void {
if (!this.editNomineeRelId || !this.editNomineeShare) return;
const otherTotal = this.nominees.reduce((sum, n, i) => i === index ? sum : sum + (n.percentageShare || 0), 0);
const otherTotal = this.nominees.reduce((sum, n, i) => (i === index ? sum : sum + (n.percentageShare || 0)), 0);
if (otherTotal + this.editNomineeShare > 100) return;
const relOption = this.clientTemplate?.familyMemberOptions?.relationshipIdOptions?.find(
@@ -413,8 +458,12 @@ export class ClientFamilyMembersStepComponent implements OnInit {
dateFormat: 'dd MMMM yyyy',
locale: 'en'
};
if (nominee.guardianName) { payload.guardianName = nominee.guardianName; }
if (nominee.guardianRelationship) { payload.guardianRelationship = nominee.guardianRelationship; }
if (nominee.guardianName) {
payload.guardianName = nominee.guardianName;
}
if (nominee.guardianRelationship) {
payload.guardianRelationship = nominee.guardianRelationship;
}
this.clientsService.editFamilyMember(this.clientId, nominee.id, payload).subscribe({ error: () => {} });
}
@@ -2020,11 +2020,12 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
export class SignatoryDetailDialogComponent {
private clientService = inject(ClientsService);
private sanitizer = inject(DomSanitizer);
private inputData = inject<any>(MAT_DIALOG_DATA);
data: any;
loading = false;
constructor() {
const inputData = inject<any>(MAT_DIALOG_DATA);
const inputData = this.inputData;
this.data = { ...inputData };
const cid = inputData.id?.toString();
if (!cid) return;
@@ -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 { FormsModule } from '@angular/forms';
@@ -39,13 +47,23 @@ import { ClientsService } from 'app/clients/clients.service';
}
<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>
<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" />
<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>
@@ -59,14 +77,17 @@ 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>
}
</div>
<p class="verify-msg">
Verifying DIN <strong>{{ dinNumber }}</strong> for <strong>{{ signatoryName }}</strong>
against <strong>MCA (Ministry of Corporate Affairs)</strong> records.
Verifying DIN <strong>{{ dinNumber }}</strong> for <strong>{{ signatoryName }}</strong> against
<strong>MCA (Ministry of Corporate Affairs)</strong> records.
</p>
</div>
}
@@ -80,9 +101,8 @@ import { ClientsService } from 'app/clients/clients.service';
<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.
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">
@@ -124,7 +144,9 @@ import { ClientsService } from 'app/clients/clients.service';
<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>
<p>
<strong>{{ entityName }}</strong> matches <strong>{{ matchedCompany }}</strong>
</p>
</div>
</div>
} @else {
@@ -132,7 +154,9 @@ import { ClientsService } from 'app/clients/clients.service';
<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>
<p>
<strong>{{ entityName }}</strong> does not match any of the director's associated companies.
</p>
</div>
</div>
}
@@ -148,11 +172,21 @@ import { ClientsService } from 'app/clients/clients.service';
<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 }} }
@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>
@@ -201,15 +235,11 @@ import { ClientsService } from 'app/clients/clients.service';
</button>
}
@if (!verifying && verified) {
<button mat-raised-button color="primary" (click)="onClose(true)">
<fa-icon icon="check"></fa-icon> Done
</button>
<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>
<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>
@@ -235,7 +265,9 @@ import { ClientsService } from 'app/clients/clients.service';
color: #1074b9;
}
.dialog-title-icon { font-size: 20px; }
.dialog-title-icon {
font-size: 20px;
}
mat-dialog-content {
padding: 16px 24px;
@@ -255,10 +287,24 @@ import { ClientsService } from 'app/clients/clients.service';
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; }
.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 {
@@ -272,10 +318,26 @@ import { ClientsService } from 'app/clients/clients.service';
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; }
.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 {
@@ -291,49 +353,180 @@ import { ClientsService } from 'app/clients/clients.service';
line-height: 1.5;
}
.info-icon { font-size: 16px; color: #1074b9; flex-shrink: 0; margin-top: 2px; }
.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; }
.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-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; }
.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; }
.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-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-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-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; }
.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 {
@@ -351,7 +544,10 @@ import { ClientsService } from 'app/clients/clients.service';
margin: 0 0 10px;
}
.company-title-icon { font-size: 14px; color: #64748b; }
.company-title-icon {
font-size: 14px;
color: #64748b;
}
/* Match banner */
.match-banner {
@@ -365,7 +561,10 @@ import { ClientsService } from 'app/clients/clients.service';
line-height: 1.5;
}
.match-banner p { margin: 2px 0 0; font-size: 12px; }
.match-banner p {
margin: 2px 0 0;
font-size: 12px;
}
.match-success {
background: #ecfdf5;
@@ -373,7 +572,12 @@ import { ClientsService } from 'app/clients/clients.service';
color: #065f46;
}
.match-success .match-banner-icon { color: #059669; font-size: 18px; margin-top: 1px; flex-shrink: 0; }
.match-success .match-banner-icon {
color: #059669;
font-size: 18px;
margin-top: 1px;
flex-shrink: 0;
}
.match-warning {
background: #fffbeb;
@@ -381,7 +585,12 @@ import { ClientsService } from 'app/clients/clients.service';
color: #92400e;
}
.match-warning .match-banner-icon { color: #d97706; font-size: 18px; margin-top: 1px; flex-shrink: 0; }
.match-warning .match-banner-icon {
color: #d97706;
font-size: 18px;
margin-top: 1px;
flex-shrink: 0;
}
/* Company list */
.company-list {
@@ -399,9 +608,13 @@ import { ClientsService } from 'app/clients/clients.service';
transition: background 0.15s;
}
.company-item:last-child { border-bottom: none; }
.company-item:last-child {
border-bottom: none;
}
.company-item:hover { background: #f8fafc; }
.company-item:hover {
background: #f8fafc;
}
.company-matched {
background: #f0fdf4 !important;
@@ -415,28 +628,78 @@ import { ClientsService } from 'app/clients/clients.service';
flex-shrink: 0;
}
.company-match-icon.match { color: #22c55e; }
.company-match-icon.no-match { color: #d1d5db; }
.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; }
.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;
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;
}
.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; }
.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; }
mat-dialog-actions {
padding: 8px 24px 16px;
}
mat-dialog-actions button {
display: flex;
align-items: center;
gap: 6px;
}
`
],
imports: [
@@ -561,7 +824,11 @@ export class DinVerifyDialogComponent implements OnInit, OnDestroy {
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.';
this.errorMessage =
this.apiError?.error?.defaultUserMessage ||
this.apiError?.error?.message ||
this.apiError?.message ||
'Unable to verify DIN. Please try again.';
return;
}
@@ -590,7 +857,11 @@ export class DinVerifyDialogComponent implements OnInit, OnDestroy {
this.matchCompanyName();
} else {
this.failed = true;
this.failReason = changes?.dinFailReason || res?.failReason || res?.defaultUserMessage || 'DIN could not be verified against MCA records.';
this.failReason =
changes?.dinFailReason ||
res?.failReason ||
res?.defaultUserMessage ||
'DIN could not be verified against MCA records.';
}
}
@@ -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) {
@@ -20,15 +20,18 @@
<mat-tab-group class="addr-tabs" [(selectedIndex)]="activeTab" animationDuration="150ms">
<!-- ═══ TAB 1: PERMANENT ═══ -->
<mat-tab>
<ng-template mat-tab-label>
<span class="addr-tab-num">1</span> Permanent Address
</ng-template>
<ng-template mat-tab-label> <span class="addr-tab-num">1</span> Permanent Address </ng-template>
<div class="addr-panel" [formGroup]="permanentForm">
<div class="addr-grid addr-grid--1">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Address Line 1</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>home</mat-icon></span>
<input matInput required formControlName="addressLine1" placeholder="House / Flat No., Floor, Building Name" />
<input
matInput
required
formControlName="addressLine1"
placeholder="House / Flat No., Floor, Building Name"
/>
</mat-form-field>
</div>
<div class="addr-grid addr-grid--1">
@@ -45,9 +48,15 @@
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>PIN Code</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>pin_drop</mat-icon></span>
<input matInput required formControlName="pinCode" maxlength="6" placeholder="6-digit PIN"
<input
matInput
required
formControlName="pinCode"
maxlength="6"
placeholder="6-digit PIN"
(input)="onPinInput('permanent')"
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()" />
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()"
/>
@if (permanentForm.get('pinCode')?.hasError('pattern') && permanentForm.get('pinCode')?.touched) {
<mat-error>Enter a valid 6-digit PIN code</mat-error>
}
@@ -126,9 +135,7 @@
<!-- ═══ TAB 2: CURRENT / RESIDENTIAL ═══ -->
<mat-tab>
<ng-template mat-tab-label>
<span class="addr-tab-num">2</span> Current / Residential
</ng-template>
<ng-template mat-tab-label> <span class="addr-tab-num">2</span> Current / Residential </ng-template>
<div class="addr-panel" [formGroup]="currentForm">
<div class="addr-same-row">
<mat-checkbox [checked]="sameAsPermanent" (change)="toggleSameAsPermanent($event.checked)">
@@ -140,7 +147,12 @@
<div class="addr-grid addr-grid--1">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Address Line 1</mat-label>
<input matInput required formControlName="addressLine1" placeholder="House / Flat No., Floor, Building Name" />
<input
matInput
required
formControlName="addressLine1"
placeholder="House / Flat No., Floor, Building Name"
/>
</mat-form-field>
</div>
<div class="addr-grid addr-grid--1">
@@ -155,9 +167,15 @@
<div class="addr-grid addr-grid--2">
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>PIN Code</mat-label>
<input matInput required formControlName="pinCode" maxlength="6" placeholder="6-digit PIN"
<input
matInput
required
formControlName="pinCode"
maxlength="6"
placeholder="6-digit PIN"
(input)="onPinInput('current')"
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()" />
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()"
/>
@if (currentForm.get('pinCode')?.hasError('pattern') && currentForm.get('pinCode')?.touched) {
<mat-error>Enter a valid 6-digit PIN code</mat-error>
}
@@ -230,9 +248,7 @@
<!-- ═══ TAB 3: OFFICE / WORK ═══ -->
<mat-tab>
<ng-template mat-tab-label>
<span class="addr-tab-num">3</span> Office / Work
</ng-template>
<ng-template mat-tab-label> <span class="addr-tab-num">3</span> Office / Work </ng-template>
<div class="addr-panel" [formGroup]="officeForm">
<div class="addr-grid addr-grid--2">
<mat-form-field appearance="outline" class="addr-ff">
@@ -268,9 +284,14 @@
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>PIN Code</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>pin_drop</mat-icon></span>
<input matInput formControlName="pinCode" maxlength="6" placeholder="6-digit PIN"
<input
matInput
formControlName="pinCode"
maxlength="6"
placeholder="6-digit PIN"
(input)="onPinInput('office')"
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()" />
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()"
/>
@if (officeForm.get('pinCode')?.hasError('pattern') && officeForm.get('pinCode')?.touched) {
<mat-error>Enter a valid 6-digit PIN code</mat-error>
}
@@ -348,13 +369,13 @@
<!-- ═══ TAB 4: CORRESPONDENCE ═══ -->
<mat-tab>
<ng-template mat-tab-label>
<span class="addr-tab-num">4</span> Correspondence
</ng-template>
<ng-template mat-tab-label> <span class="addr-tab-num">4</span> Correspondence </ng-template>
<div class="addr-panel" [formGroup]="correspondenceForm">
<div class="addr-copy-row">
<span class="addr-copy-label">Copy address from:</span>
<button mat-stroked-button type="button" class="addr-copy-btn" (click)="copyFrom('permanent')">Permanent</button>
<button mat-stroked-button type="button" class="addr-copy-btn" (click)="copyFrom('permanent')">
Permanent
</button>
<button mat-stroked-button type="button" class="addr-copy-btn" (click)="copyFrom('current')">Current</button>
<button mat-stroked-button type="button" class="addr-copy-btn" (click)="copyFrom('office')">Office</button>
</div>
@@ -363,7 +384,12 @@
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>Address Line 1</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>home</mat-icon></span>
<input matInput required formControlName="addressLine1" placeholder="House / Flat No., Floor, Building Name" />
<input
matInput
required
formControlName="addressLine1"
placeholder="House / Flat No., Floor, Building Name"
/>
</mat-form-field>
</div>
<div class="addr-grid addr-grid--1">
@@ -380,9 +406,15 @@
<mat-form-field appearance="outline" class="addr-ff">
<mat-label>PIN Code</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>pin_drop</mat-icon></span>
<input matInput required formControlName="pinCode" maxlength="6" placeholder="6-digit PIN"
<input
matInput
required
formControlName="pinCode"
maxlength="6"
placeholder="6-digit PIN"
(input)="onPinInput('correspondence')"
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()" />
(keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()"
/>
@if (correspondenceForm.get('pinCode')?.hasError('pattern') && correspondenceForm.get('pinCode')?.touched) {
<mat-error>Enter a valid 6-digit PIN code</mat-error>
}
@@ -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/.
*/
/**
* Copyright since 2025 Mifos Initiative
*/
@@ -76,9 +84,17 @@ $muted: #64748b;
gap: 4px 16px;
margin-bottom: 4px;
&--1 { grid-template-columns: 1fr; }
&--2 { grid-template-columns: 1fr 1fr; }
&--3 { grid-template-columns: 1fr 1fr 1fr; }
&--1 {
grid-template-columns: 1fr;
}
&--2 {
grid-template-columns: 1fr 1fr;
}
&--3 {
grid-template-columns: 1fr 1fr 1fr;
}
}
.addr-ff {
@@ -101,7 +117,8 @@ $muted: #64748b;
white-space: nowrap;
}
&::before, &::after {
&::before,
&::after {
content: '';
flex: 1;
height: 1px;
@@ -174,7 +191,8 @@ $muted: #64748b;
// Responsive
@media (width <= 768px) {
.addr-grid--2, .addr-grid--3 {
.addr-grid--2,
.addr-grid--3 {
grid-template-columns: 1fr;
}
@@ -42,27 +42,60 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
sameAsPermanent = false;
permanentForm: UntypedFormGroup = this.fb.group({
addressLine1: ['', Validators.required],
addressLine1: [
'',
Validators.required
],
addressLine2: [''],
pinCode: ['', [Validators.required, Validators.pattern(/^\d{6}$/)]],
locality: ['', Validators.required],
pinCode: [
'',
[
Validators.required,
Validators.pattern(/^\d{6}$/)
]
],
locality: [
'',
Validators.required
],
city: [''],
district: [''],
state: ['', Validators.required],
state: [
'',
Validators.required
],
country: [{ value: 'India', disabled: true }],
landmark: [''],
proofType: ['', Validators.required],
proofType: [
'',
Validators.required
],
proofDocNumber: ['']
});
currentForm: UntypedFormGroup = this.fb.group({
addressLine1: ['', Validators.required],
addressLine1: [
'',
Validators.required
],
addressLine2: [''],
pinCode: ['', [Validators.required, Validators.pattern(/^\d{6}$/)]],
locality: ['', Validators.required],
pinCode: [
'',
[
Validators.required,
Validators.pattern(/^\d{6}$/)
]
],
locality: [
'',
Validators.required
],
city: [''],
district: [''],
state: ['', Validators.required],
state: [
'',
Validators.required
],
country: [{ value: 'India', disabled: true }],
landmark: [''],
residenceType: [''],
@@ -74,7 +107,10 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
department: [''],
addressLine1: [''],
addressLine2: [''],
pinCode: ['', Validators.pattern(/^\d{6}$/)],
pinCode: [
'',
Validators.pattern(/^\d{6}$/)
],
locality: [''],
city: [''],
district: [''],
@@ -87,13 +123,28 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
});
correspondenceForm: UntypedFormGroup = this.fb.group({
addressLine1: ['', Validators.required],
addressLine1: [
'',
Validators.required
],
addressLine2: [''],
pinCode: ['', [Validators.required, Validators.pattern(/^\d{6}$/)]],
locality: ['', Validators.required],
pinCode: [
'',
[
Validators.required,
Validators.pattern(/^\d{6}$/)
]
],
locality: [
'',
Validators.required
],
city: [''],
district: [''],
state: ['', Validators.required],
state: [
'',
Validators.required
],
country: [{ value: 'India', disabled: true }],
landmark: [''],
commMode: [''],
@@ -116,18 +167,36 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
};
proofTypes = [
'Aadhaar Card', 'Passport', 'Voter ID', 'Driving Licence',
'Utility Bill (not older than 3 months)', 'Bank Statement'
'Aadhaar Card',
'Passport',
'Voter ID',
'Driving Licence',
'Utility Bill (not older than 3 months)',
'Bank Statement'
];
residenceTypes = ['Owned', 'Rented', 'Leased', 'Company Provided', 'Parental / Family Owned'];
residenceTypes = [
'Owned',
'Rented',
'Leased',
'Company Provided',
'Parental / Family Owned'
];
employmentTypes = [
'Salaried (Private)', 'Salaried (Government / PSU)',
'Self-Employed Professional', 'Business Owner', 'Retired'
'Salaried (Private)',
'Salaried (Government / PSU)',
'Self-Employed Professional',
'Business Owner',
'Retired'
];
commModes = ['Post / Courier', 'Email', 'SMS', 'WhatsApp'];
commModes = [
'Post / Courier',
'Email',
'SMS',
'WhatsApp'
];
private pinTimers: Record<string, any> = {};
private pinCache: Record<string, any[]> = {};
@@ -144,7 +213,12 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
ngOnChanges(changes: SimpleChanges): void {
if (changes['readOnly']) {
const forms = [this.permanentForm, this.currentForm, this.officeForm, this.correspondenceForm];
const forms = [
this.permanentForm,
this.currentForm,
this.officeForm,
this.correspondenceForm
];
for (const form of forms) {
if (this.readOnly) {
form.disable({ emitEvent: false });
@@ -168,10 +242,19 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
// Build type-to-tab mapping from address type names
const tabMap: Record<string, string> = {
permanent: 'permanent', 'permanent address': 'permanent',
residential: 'current', current: 'current', 'current / residential': 'current', communication: 'current',
office: 'office', 'office / work': 'office', business: 'office', work: 'office',
correspondence: 'correspondence', mailing: 'correspondence', postal: 'correspondence'
permanent: 'permanent',
'permanent address': 'permanent',
residential: 'current',
current: 'current',
'current / residential': 'current',
communication: 'current',
office: 'office',
'office / work': 'office',
business: 'office',
work: 'office',
correspondence: 'correspondence',
mailing: 'correspondence',
postal: 'correspondence'
};
for (const addr of addresses) {
@@ -192,7 +275,9 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
});
}
},
error: () => { this.addressLoaded = true; }
error: () => {
this.addressLoaded = true;
}
});
}
@@ -235,11 +320,13 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
private applyPinData(tab: string, offices: any[]) {
const seen = new Set<string>();
this.localityOptions[tab] = offices.filter(o => {
if (seen.has(o.Name)) return false;
seen.add(o.Name);
return true;
}).map(o => o.Name);
this.localityOptions[tab] = offices
.filter((o) => {
if (seen.has(o.Name)) return false;
seen.add(o.Name);
return true;
})
.map((o) => o.Name);
const form = this.getForm(tab);
const f = offices[0];
@@ -259,11 +346,16 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
getForm(tab: string): UntypedFormGroup {
switch (tab) {
case 'permanent': return this.permanentForm;
case 'current': return this.currentForm;
case 'office': return this.officeForm;
case 'correspondence': return this.correspondenceForm;
default: return this.permanentForm;
case 'permanent':
return this.permanentForm;
case 'current':
return this.currentForm;
case 'office':
return this.officeForm;
case 'correspondence':
return this.correspondenceForm;
default:
return this.permanentForm;
}
}
@@ -307,7 +399,9 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
get addressData(): any {
return {
permanent: this.permanentForm.getRawValue(),
current: this.sameAsPermanent ? { ...this.permanentForm.getRawValue(), sameAsPermanent: true } : this.currentForm.getRawValue(),
current: this.sameAsPermanent
? { ...this.permanentForm.getRawValue(), sameAsPermanent: true }
: this.currentForm.getRawValue(),
office: this.officeForm.getRawValue(),
correspondence: this.correspondenceForm.getRawValue()
};
@@ -832,7 +832,8 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
} else {
this.gstinVerifyStatus = 'FAILED';
this.showPanErrorAlert(
changes?.gstinVerificationMessage || 'GSTIN verification failed. Please check the GSTIN number and try again.'
changes?.gstinVerificationMessage ||
'GSTIN verification failed. Please check the GSTIN number and try again.'
);
}
},
@@ -840,7 +841,9 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
this.gstinVerifying = false;
this.gstinVerifyStatus = 'FAILED';
this.showPanErrorAlert(
err?.error?.errors?.[0]?.defaultUserMessage || err?.error?.defaultUserMessage || 'GSTIN verification failed.'
err?.error?.errors?.[0]?.defaultUserMessage ||
err?.error?.defaultUserMessage ||
'GSTIN verification failed.'
);
}
});
@@ -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/.
-->
<!--
Copyright since 2025 Mifos Initiative
-->
@@ -20,11 +28,13 @@
<div class="crop-workspace" [class.hidden]="!imageLoaded">
<div class="crop-source">
<canvas #sourceCanvas
<canvas
#sourceCanvas
(mousedown)="onMouseDown($event)"
(mousemove)="onMouseMove($event)"
(mouseup)="onMouseUp()"
(mouseleave)="onMouseUp()">
(mouseleave)="onMouseUp()"
>
</canvas>
<div class="crop-hint">Drag to move. Drag corner to resize.</div>
</div>
@@ -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/.
*/
/**
* Copyright since 2025 Mifos Initiative
*/
@@ -1,8 +1,12 @@
/**
* 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, Inject, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
import { Component, inject, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef, MatDialogContent, MatDialogActions } from '@angular/material/dialog';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
@@ -59,10 +63,8 @@ export class ImageCropDialogComponent implements AfterViewInit {
zoom = 1;
constructor(
public dialogRef: MatDialogRef<ImageCropDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: ImageCropData
) {}
dialogRef = inject(MatDialogRef<ImageCropDialogComponent>);
data = inject<ImageCropData>(MAT_DIALOG_DATA);
ngAfterViewInit() {
const reader = new FileReader();
@@ -127,9 +129,14 @@ export class ImageCropDialogComponent implements AfterViewInit {
ctx.clearRect(this.cropX, this.cropY, this.cropW, this.cropH);
ctx.drawImage(
this.img,
this.cropX * this.scaleX, this.cropY * this.scaleY,
this.cropW * this.scaleX, this.cropH * this.scaleY,
this.cropX, this.cropY, this.cropW, this.cropH
this.cropX * this.scaleX,
this.cropY * this.scaleY,
this.cropW * this.scaleX,
this.cropH * this.scaleY,
this.cropX,
this.cropY,
this.cropW,
this.cropH
);
// Crop border
@@ -141,14 +148,31 @@ export class ImageCropDialogComponent implements AfterViewInit {
const hs = 8;
ctx.fillStyle = '#1353a0';
const corners = [
[this.cropX, this.cropY],
[this.cropX + this.cropW, this.cropY],
[this.cropX, this.cropY + this.cropH],
[this.cropX + this.cropW, this.cropY + this.cropH]
[
this.cropX,
this.cropY
],
[
this.cropX + this.cropW,
this.cropY
],
[
this.cropX,
this.cropY + this.cropH
],
[
this.cropX + this.cropW,
this.cropY + this.cropH
]
];
corners.forEach(([x, y]) => {
ctx.fillRect(x - hs / 2, y - hs / 2, hs, hs);
});
corners.forEach(
([
x,
y
]) => {
ctx.fillRect(x - hs / 2, y - hs / 2, hs, hs);
}
);
}
private updatePreview() {
@@ -163,9 +187,14 @@ export class ImageCropDialogComponent implements AfterViewInit {
pCtx.clearRect(0, 0, pw, ph);
pCtx.drawImage(
this.img,
this.cropX * this.scaleX, this.cropY * this.scaleY,
this.cropW * this.scaleX, this.cropH * this.scaleY,
0, 0, pw, ph
this.cropX * this.scaleX,
this.cropY * this.scaleY,
this.cropW * this.scaleX,
this.cropH * this.scaleY,
0,
0,
pw,
ph
);
}
@@ -229,9 +258,14 @@ export class ImageCropDialogComponent implements AfterViewInit {
const ctx = offscreen.getContext('2d')!;
ctx.drawImage(
this.img,
this.cropX * this.scaleX, this.cropY * this.scaleY,
this.cropW * this.scaleX, this.cropH * this.scaleY,
0, 0, offscreen.width, offscreen.height
this.cropX * this.scaleX,
this.cropY * this.scaleY,
this.cropW * this.scaleX,
this.cropH * this.scaleY,
0,
0,
offscreen.width,
offscreen.height
);
offscreen.toBlob((blob) => {
@@ -405,8 +405,13 @@
}
.preview-field {
&:nth-child(3n) { border-right: 1px solid rgb(0 0 0 / 4%); }
&:nth-child(2n) { border-right: none; }
&:nth-child(3n) {
border-right: 1px solid rgb(0 0 0 / 4%);
}
&:nth-child(2n) {
border-right: none;
}
}
}
@@ -486,8 +491,13 @@
border-bottom: 1px solid #f1f5f9;
transition: background 0.1s;
&:last-child { border-bottom: none; }
&:hover { background: #f8fafc; }
&:last-child {
border-bottom: none;
}
&:hover {
background: #f8fafc;
}
}
.fm-preview-row__sno {