+36
-28
@@ -2,24 +2,26 @@
|
||||
|
||||
## Files in this directory
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `V1__client_schema_extensions.sql` | DB migration — run on Fineract MySQL/MariaDB |
|
||||
| `ClientEntityKyc.java` | JPA Entity for `m_client_entity_kyc` table |
|
||||
| `ClientAddressIndia.java` | JPA Entity for `m_client_address_india` table |
|
||||
| `ClientEntityKycRepository.java` | Spring Data repository |
|
||||
| `ClientAddressIndiaRepository.java` | Spring Data repository |
|
||||
| `ClientEntityKycApiResource.java` | REST controller — `/clients/{id}/entity-kyc` |
|
||||
| File | Purpose |
|
||||
| ------------------------------------ | ----------------------------------------------- |
|
||||
| `V1__client_schema_extensions.sql` | DB migration — run on Fineract MySQL/MariaDB |
|
||||
| `ClientEntityKyc.java` | JPA Entity for `m_client_entity_kyc` table |
|
||||
| `ClientAddressIndia.java` | JPA Entity for `m_client_address_india` table |
|
||||
| `ClientEntityKycRepository.java` | Spring Data repository |
|
||||
| `ClientAddressIndiaRepository.java` | Spring Data repository |
|
||||
| `ClientEntityKycApiResource.java` | REST controller — `/clients/{id}/entity-kyc` |
|
||||
| `ClientAddressIndiaApiResource.java` | REST controller — `/clients/{id}/india-address` |
|
||||
|
||||
## How to apply
|
||||
|
||||
### Step 1: Run SQL Migration
|
||||
|
||||
```bash
|
||||
mysql -u root -p fineract_tenantdb < V1__client_schema_extensions.sql
|
||||
```
|
||||
|
||||
### Step 2: Copy Java files to Fineract source
|
||||
|
||||
```
|
||||
ClientEntityKyc.java → fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/
|
||||
ClientAddressIndia.java → fineract-provider/src/main/java/org/apache/fineract/portfolio/client/domain/
|
||||
@@ -30,7 +32,9 @@ ClientAddressIndiaApiResource.java → fineract-provider/src/main/java/org/apach
|
||||
```
|
||||
|
||||
### Step 3: Update existing Fineract Client API
|
||||
|
||||
In `ClientApiResource.java`, the `POST /clients` endpoint needs to accept these additional fields in the request body:
|
||||
|
||||
- `nationality`, `annualTurnover`, `creditLimit`
|
||||
- `isMinor`, `ageYears`, `ageMonths`, `ageDays`
|
||||
- `guardianName`, `guardianRelation`, `guardianMobile`, `guardianIdNumber`, `guardianAddress`
|
||||
@@ -38,9 +42,11 @@ In `ClientApiResource.java`, the `POST /clients` endpoint needs to accept these
|
||||
In `ClientWritePlatformServiceJpaRepositoryImpl.java`, map these fields to the `m_client` entity during create/update.
|
||||
|
||||
### Step 4: Register new API resources
|
||||
|
||||
Add to `fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/config/FineractWebMvcConfigurer.java` or equivalent Spring config.
|
||||
|
||||
### Step 5: Rebuild Fineract
|
||||
|
||||
```bash
|
||||
./gradlew bootRun
|
||||
```
|
||||
@@ -48,28 +54,30 @@ Add to `fineract-provider/src/main/java/org/apache/fineract/infrastructure/core/
|
||||
## API Reference
|
||||
|
||||
### Entity KYC
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| POST | `/clients/{clientId}/entity-kyc` | Create/update entity KYC + MSME |
|
||||
| GET | `/clients/{clientId}/entity-kyc` | Retrieve entity KYC |
|
||||
| DELETE | `/clients/{clientId}/entity-kyc` | Delete entity KYC |
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
| ------ | -------------------------------- | ------------------------------- |
|
||||
| POST | `/clients/{clientId}/entity-kyc` | Create/update entity KYC + MSME |
|
||||
| GET | `/clients/{clientId}/entity-kyc` | Retrieve entity KYC |
|
||||
| DELETE | `/clients/{clientId}/entity-kyc` | Delete entity KYC |
|
||||
|
||||
### India Address
|
||||
| Method | Endpoint | Description |
|
||||
|--------|----------|-------------|
|
||||
| POST | `/clients/{clientId}/india-address` | Save all 4 address types |
|
||||
| GET | `/clients/{clientId}/india-address` | Get all addresses |
|
||||
| GET | `/clients/{clientId}/india-address/{type}` | Get specific type (PERMANENT/CURRENT/OFFICE/CORRESPONDENCE) |
|
||||
| PUT | `/clients/{clientId}/india-address/{type}` | Update specific type |
|
||||
| DELETE | `/clients/{clientId}/india-address` | Delete all addresses |
|
||||
|
||||
| Method | Endpoint | Description |
|
||||
| ------ | ------------------------------------------ | ----------------------------------------------------------- |
|
||||
| POST | `/clients/{clientId}/india-address` | Save all 4 address types |
|
||||
| GET | `/clients/{clientId}/india-address` | Get all addresses |
|
||||
| GET | `/clients/{clientId}/india-address/{type}` | Get specific type (PERMANENT/CURRENT/OFFICE/CORRESPONDENCE) |
|
||||
| PUT | `/clients/{clientId}/india-address/{type}` | Update specific type |
|
||||
| DELETE | `/clients/{clientId}/india-address` | Delete all addresses |
|
||||
|
||||
## Tables Summary
|
||||
|
||||
| Table | New/Modified | Records |
|
||||
|-------|-------------|---------|
|
||||
| `m_client` | Modified — 12 new columns | Guardian, nationality, minor, turnover |
|
||||
| `m_client_non_person` | Modified — 13 new columns | MSME, CIN, enterprise PAN, signatory |
|
||||
| `m_client_kyc` | Modified — 19 new columns | AML, freeze, form60/61, form15G/H, KYC doc |
|
||||
| `m_client_entity_kyc` | **New** | Entity-specific KYC, MSME, signatory, auth doc |
|
||||
| `m_client_address_india` | **New** | 4-type India address with PIN lookup fields |
|
||||
| `m_code_value` | Modified | Added Gender: Transgender, Other |
|
||||
| Table | New/Modified | Records |
|
||||
| ------------------------ | ------------------------- | ---------------------------------------------- |
|
||||
| `m_client` | Modified — 12 new columns | Guardian, nationality, minor, turnover |
|
||||
| `m_client_non_person` | Modified — 13 new columns | MSME, CIN, enterprise PAN, signatory |
|
||||
| `m_client_kyc` | Modified — 19 new columns | AML, freeze, form60/61, form15G/H, KYC doc |
|
||||
| `m_client_entity_kyc` | **New** | Entity-specific KYC, MSME, signatory, auth doc |
|
||||
| `m_client_address_india` | **New** | 4-type India address with PIN lookup fields |
|
||||
| `m_code_value` | Modified | Added Gender: Transgender, Other |
|
||||
|
||||
Generated
+14
@@ -42,6 +42,7 @@
|
||||
"lightgallery": "^2.9.0",
|
||||
"lodash": "4.17.23",
|
||||
"moment": "^2.29.4",
|
||||
"ngx-image-cropper": "^9.1.6",
|
||||
"ngx-mat-select-search": "^8.0.2",
|
||||
"rxjs": "^7.8.2",
|
||||
"signature_pad": "^5.1.3",
|
||||
@@ -18822,6 +18823,19 @@
|
||||
"node": ">= 0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ngx-image-cropper": {
|
||||
"version": "9.1.6",
|
||||
"resolved": "https://registry.npmjs.org/ngx-image-cropper/-/ngx-image-cropper-9.1.6.tgz",
|
||||
"integrity": "sha512-b250YJ+jZovfqIj8vdEOrpEFay34be5f1Hpvg6Db68VMlvdyyuzboJdR0gCupbXtVcG6qQ86L7YG+SYxXJwApw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=17.3.0",
|
||||
"@angular/core": ">=17.3.0"
|
||||
}
|
||||
},
|
||||
"node_modules/ngx-mat-select-search": {
|
||||
"version": "8.0.4",
|
||||
"resolved": "https://registry.npmjs.org/ngx-mat-select-search/-/ngx-mat-select-search-8.0.4.tgz",
|
||||
|
||||
@@ -70,6 +70,7 @@
|
||||
"lightgallery": "^2.9.0",
|
||||
"lodash": "4.17.23",
|
||||
"moment": "^2.29.4",
|
||||
"ngx-image-cropper": "^9.1.6",
|
||||
"ngx-mat-select-search": "^8.0.2",
|
||||
"rxjs": "^7.8.2",
|
||||
"signature_pad": "^5.1.3",
|
||||
|
||||
@@ -145,7 +145,11 @@
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/clients/{{clientId}}/kyc",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["clients", "{{clientId}}", "kyc"]
|
||||
"path": [
|
||||
"clients",
|
||||
"{{clientId}}",
|
||||
"kyc"
|
||||
]
|
||||
},
|
||||
"description": "Saves KYC details including PAN, Aadhaar, AML compliance, document info, risk category, and Form 60/15 data."
|
||||
}
|
||||
@@ -163,7 +167,11 @@
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/clients/{{clientId}}/kyc",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["clients", "{{clientId}}", "kyc"]
|
||||
"path": [
|
||||
"clients",
|
||||
"{{clientId}}",
|
||||
"kyc"
|
||||
]
|
||||
},
|
||||
"description": "Fetches existing KYC data for a client."
|
||||
}
|
||||
@@ -197,7 +205,11 @@
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/clients/{{clientId}}/images",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["clients", "{{clientId}}", "images"]
|
||||
"path": [
|
||||
"clients",
|
||||
"{{clientId}}",
|
||||
"images"
|
||||
]
|
||||
},
|
||||
"description": "Uploads client profile photograph."
|
||||
}
|
||||
@@ -236,7 +248,11 @@
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/clients/{{clientId}}/documents",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["clients", "{{clientId}}", "documents"]
|
||||
"path": [
|
||||
"clients",
|
||||
"{{clientId}}",
|
||||
"documents"
|
||||
]
|
||||
},
|
||||
"description": "Uploads client signature as a document."
|
||||
}
|
||||
@@ -262,7 +278,11 @@
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/client/{{clientId}}/addresses?type=101",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["client", "{{clientId}}", "addresses"],
|
||||
"path": [
|
||||
"client",
|
||||
"{{clientId}}",
|
||||
"addresses"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "type",
|
||||
@@ -295,7 +315,11 @@
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/client/{{clientId}}/addresses?type=102",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["client", "{{clientId}}", "addresses"],
|
||||
"path": [
|
||||
"client",
|
||||
"{{clientId}}",
|
||||
"addresses"
|
||||
],
|
||||
"query": [
|
||||
{
|
||||
"key": "type",
|
||||
@@ -320,7 +344,11 @@
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/client/addresses/template",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["client", "addresses", "template"]
|
||||
"path": [
|
||||
"client",
|
||||
"addresses",
|
||||
"template"
|
||||
]
|
||||
},
|
||||
"description": "Fetches address template with `addressTypeIdOptions` and `stateProvinceIdOptions`. Use these IDs in address save requests."
|
||||
}
|
||||
@@ -346,7 +374,11 @@
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/clients/{{clientId}}/familymembers",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["clients", "{{clientId}}", "familymembers"]
|
||||
"path": [
|
||||
"clients",
|
||||
"{{clientId}}",
|
||||
"familymembers"
|
||||
]
|
||||
},
|
||||
"description": "Adds a family member to the client. Call once per member."
|
||||
}
|
||||
@@ -364,7 +396,11 @@
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/clients/{{clientId}}/familymembers",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["clients", "{{clientId}}", "familymembers"]
|
||||
"path": [
|
||||
"clients",
|
||||
"{{clientId}}",
|
||||
"familymembers"
|
||||
]
|
||||
},
|
||||
"description": "Fetches all family members for a client."
|
||||
}
|
||||
@@ -403,7 +439,13 @@
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/clients/{{clientId}}/familymembers/{{familyMemberId}}/documents",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["clients", "{{clientId}}", "familymembers", "{{familyMemberId}}", "documents"]
|
||||
"path": [
|
||||
"clients",
|
||||
"{{clientId}}",
|
||||
"familymembers",
|
||||
"{{familyMemberId}}",
|
||||
"documents"
|
||||
]
|
||||
},
|
||||
"description": "Uploads a marriage certificate document for a family member. Set `familyMemberId` from the Add Family Member response."
|
||||
}
|
||||
@@ -421,7 +463,10 @@
|
||||
"url": {
|
||||
"raw": "{{baseUrl}}/clients/{{clientId}}",
|
||||
"host": ["{{baseUrl}}"],
|
||||
"path": ["clients", "{{clientId}}"]
|
||||
"path": [
|
||||
"clients",
|
||||
"{{clientId}}"
|
||||
]
|
||||
},
|
||||
"description": "Fetches full client details to verify saved data."
|
||||
}
|
||||
|
||||
@@ -18,7 +18,12 @@
|
||||
<!-- Cards Grid -->
|
||||
<div class="acc-grid">
|
||||
<!-- Frequent Postings -->
|
||||
<a class="acc-card" [routerLink]="['journal-entries/frequent-postings']" *mifosxHasPermission="'CREATE_JOURNALENTRY'" #frequentPostings>
|
||||
<a
|
||||
class="acc-card"
|
||||
[routerLink]="['journal-entries/frequent-postings']"
|
||||
*mifosxHasPermission="'CREATE_JOURNALENTRY'"
|
||||
#frequentPostings
|
||||
>
|
||||
<div class="acc-card__icon blue"><i class="fas fa-sync-alt"></i></div>
|
||||
<div class="acc-card__info">
|
||||
<span class="acc-card__title">{{ 'labels.heading.Frequent Postings' | translate }}</span>
|
||||
@@ -27,11 +32,18 @@
|
||||
</a>
|
||||
|
||||
<!-- Create Journal Entries -->
|
||||
<a class="acc-card" [routerLink]="['journal-entries/create']" *mifosxHasPermission="'CREATE_JOURNALENTRY'" #createJournalEntries>
|
||||
<a
|
||||
class="acc-card"
|
||||
[routerLink]="['journal-entries/create']"
|
||||
*mifosxHasPermission="'CREATE_JOURNALENTRY'"
|
||||
#createJournalEntries
|
||||
>
|
||||
<div class="acc-card__icon green"><i class="fas fa-plus-circle"></i></div>
|
||||
<div class="acc-card__info">
|
||||
<span class="acc-card__title">{{ 'labels.heading.Create Journal Entries' | translate }}</span>
|
||||
<span class="acc-card__desc">{{ 'labels.text.Manual journal entry transactions recorded in a journal' | translate }}</span>
|
||||
<span class="acc-card__desc">{{
|
||||
'labels.text.Manual journal entry transactions recorded in a journal' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -45,20 +57,34 @@
|
||||
</a>
|
||||
|
||||
<!-- Accounts Linked to Financial Activities -->
|
||||
<a class="acc-card" [routerLink]="['financial-activity-mappings']" *mifosxHasPermission="'READ_FINANCIALACTIVITYACCOUNT'" #accountsLinked>
|
||||
<a
|
||||
class="acc-card"
|
||||
[routerLink]="['financial-activity-mappings']"
|
||||
*mifosxHasPermission="'READ_FINANCIALACTIVITYACCOUNT'"
|
||||
#accountsLinked
|
||||
>
|
||||
<div class="acc-card__icon teal"><i class="fas fa-link"></i></div>
|
||||
<div class="acc-card__info">
|
||||
<span class="acc-card__title">{{ 'labels.heading.Accounts Linked to Financial Activities' | translate }}</span>
|
||||
<span class="acc-card__desc">{{ 'labels.text.List of Financial Activities and GL Account Mappings' | translate }}</span>
|
||||
<span class="acc-card__desc">{{
|
||||
'labels.text.List of Financial Activities and GL Account Mappings' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
<!-- Migrate Opening Balances -->
|
||||
<a class="acc-card" [routerLink]="['migrate-opening-balances']" *mifosxHasPermission="'READ_JOURNALENTRY'" #migrateOpeningBalances>
|
||||
<a
|
||||
class="acc-card"
|
||||
[routerLink]="['migrate-opening-balances']"
|
||||
*mifosxHasPermission="'READ_JOURNALENTRY'"
|
||||
#migrateOpeningBalances
|
||||
>
|
||||
<div class="acc-card__icon amber"><i class="fas fa-hand-holding-usd"></i></div>
|
||||
<div class="acc-card__info">
|
||||
<span class="acc-card__title">{{ 'labels.heading.Migrate Opening Balances (Office-wise)' | translate }}</span>
|
||||
<span class="acc-card__desc">{{ 'labels.text.Set or update office-level opening balances for GL accounts' | translate }}</span>
|
||||
<span class="acc-card__desc">{{
|
||||
'labels.text.Set or update office-level opening balances for GL accounts' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -76,7 +102,9 @@
|
||||
<div class="acc-card__icon red"><i class="fas fa-archive"></i></div>
|
||||
<div class="acc-card__info">
|
||||
<span class="acc-card__title">{{ 'labels.heading.Closing Entries' | translate }}</span>
|
||||
<span class="acc-card__desc">{{ 'labels.text.Journal entries made at the end of accounting period' | translate }}</span>
|
||||
<span class="acc-card__desc">{{
|
||||
'labels.text.Journal entries made at the end of accounting period' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
</a>
|
||||
|
||||
@@ -114,9 +142,15 @@
|
||||
<h2>{{ 'labels.heading.Chart of Accounts' | translate }}</h2>
|
||||
<p class="mw300">{{ 'labels.text.The Chart of Accounts - General Ledger Setup' | translate }}</p>
|
||||
<div class="layout-row align-end gap-2px responsive-column">
|
||||
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">{{ 'labels.buttons.Close' | translate }}</button>
|
||||
<button mat-raised-button color="primary" (click)="popover.close(); previousStepChartofAccounts()">{{ 'labels.buttons.Back' | translate }}</button>
|
||||
<button mat-raised-button color="primary" (click)="popover.close(); nextStepChartofAccounts()">{{ 'labels.buttons.Next' | translate }}</button>
|
||||
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">
|
||||
{{ 'labels.buttons.Close' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" (click)="popover.close(); previousStepChartofAccounts()">
|
||||
{{ 'labels.buttons.Back' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" (click)="popover.close(); nextStepChartofAccounts()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -124,9 +158,15 @@
|
||||
<h2>{{ 'labels.heading.Accounts Linked to Financial Activities' | translate }}</h2>
|
||||
<p class="mw400">{{ 'labels.text.Accounts linked to Financial Activities' | translate }}</p>
|
||||
<div class="layout-row align-end gap-2px responsive-column">
|
||||
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">{{ 'labels.buttons.Close' | translate }}</button>
|
||||
<button mat-raised-button color="primary" (click)="popover.close(); previousStepAccountsLinked()">{{ 'labels.buttons.Back' | translate }}</button>
|
||||
<button mat-raised-button color="primary" (click)="popover.close(); nextStepAccountsLinked()">{{ 'labels.buttons.Next' | translate }}</button>
|
||||
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">
|
||||
{{ 'labels.buttons.Close' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" (click)="popover.close(); previousStepAccountsLinked()">
|
||||
{{ 'labels.buttons.Back' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" (click)="popover.close(); nextStepAccountsLinked()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -134,9 +174,15 @@
|
||||
<h2>{{ 'labels.heading.Migrate Opening Balances (Office-wise)' | translate }}</h2>
|
||||
<p class="mw400">{{ 'labels.text.GLAccount Balances Application' | translate }}</p>
|
||||
<div class="layout-row align-end gap-2px responsive-column">
|
||||
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">{{ 'labels.buttons.Close' | translate }}</button>
|
||||
<button mat-raised-button color="primary" (click)="popover.close(); previousStepMigrateOpeningBalances()">{{ 'labels.buttons.Back' | translate }}</button>
|
||||
<button mat-raised-button color="primary" (click)="popover.close(); nextStepMigrateOpeningBalances()">{{ 'labels.buttons.Next' | translate }}</button>
|
||||
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">
|
||||
{{ 'labels.buttons.Close' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" (click)="popover.close(); previousStepMigrateOpeningBalances()">
|
||||
{{ 'labels.buttons.Back' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" (click)="popover.close(); nextStepMigrateOpeningBalances()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -144,9 +190,15 @@
|
||||
<h2>{{ 'labels.heading.Closing Entries' | translate }}</h2>
|
||||
<p class="mw300">{{ 'labels.text.Accounting to journal entries' | translate }}</p>
|
||||
<div class="layout-row align-end gap-2px responsive-column">
|
||||
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">{{ 'labels.buttons.Close' | translate }}</button>
|
||||
<button mat-raised-button color="primary" (click)="popover.close(); previousStepClosingEntries()">{{ 'labels.buttons.Back' | translate }}</button>
|
||||
<button mat-raised-button color="primary" (click)="popover.close(); nextStepClosingEntries()">{{ 'labels.buttons.Next' | translate }}</button>
|
||||
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">
|
||||
{{ 'labels.buttons.Close' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" (click)="popover.close(); previousStepClosingEntries()">
|
||||
{{ 'labels.buttons.Back' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" (click)="popover.close(); nextStepClosingEntries()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -154,8 +206,14 @@
|
||||
<h2>{{ 'labels.heading.Create Journal Entries' | translate }}</h2>
|
||||
<p class="mw300">{{ 'labels.text.Journal Entries Transactions' | translate }}</p>
|
||||
<div class="layout-row align-end gap-2px responsive-column">
|
||||
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">{{ 'labels.buttons.Close' | translate }}</button>
|
||||
<button mat-raised-button color="primary" (click)="popover.close(); previousStepCreateJournalEntries()">{{ 'labels.buttons.Back' | translate }}</button>
|
||||
<button mat-raised-button color="primary" (click)="popover.close(); nextStepCreateJournalEntries()">{{ 'labels.buttons.Next' | translate }}</button>
|
||||
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">
|
||||
{{ 'labels.buttons.Close' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" (click)="popover.close(); previousStepCreateJournalEntries()">
|
||||
{{ 'labels.buttons.Back' | translate }}
|
||||
</button>
|
||||
<button mat-raised-button color="primary" (click)="popover.close(); nextStepCreateJournalEntries()">
|
||||
{{ 'labels.buttons.Next' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
@@ -46,10 +46,21 @@ $accent: #0ea5e9;
|
||||
span {
|
||||
border-radius: 2px;
|
||||
|
||||
&:nth-child(1) { background: $brand-dark; }
|
||||
&:nth-child(2) { background: $accent; }
|
||||
&:nth-child(3) { background: #10b981; }
|
||||
&:nth-child(4) { background: #cbd5e1; }
|
||||
&:nth-child(1) {
|
||||
background: $brand-dark;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
background: $accent;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
background: #cbd5e1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +70,9 @@ $accent: #0ea5e9;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
|
||||
@media (max-width: 768px) { grid-template-columns: 1fr; }
|
||||
@media (width <= 768px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.acc-card {
|
||||
@@ -68,14 +81,16 @@ $accent: #0ea5e9;
|
||||
gap: 14px;
|
||||
padding: 16px 18px;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border: 1px solid rgb(0 0 0 / 6%);
|
||||
border-radius: 8px;
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: box-shadow 0.15s, transform 0.15s;
|
||||
transition:
|
||||
box-shadow 0.15s,
|
||||
transform 0.15s;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@@ -89,12 +104,35 @@ $accent: #0ea5e9;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
|
||||
&.blue { background: #e0f2fe; color: #0284c7; }
|
||||
&.green { background: #d1fae5; color: #059669; }
|
||||
&.purple { background: #ede9fe; color: #7c3aed; }
|
||||
&.amber { background: #fef3c7; color: #d97706; }
|
||||
&.teal { background: #ccfbf1; color: #0d9488; }
|
||||
&.red { background: #fee2e2; color: #dc2626; }
|
||||
&.blue {
|
||||
background: #e0f2fe;
|
||||
color: #0284c7;
|
||||
}
|
||||
|
||||
&.green {
|
||||
background: #d1fae5;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
&.purple {
|
||||
background: #ede9fe;
|
||||
color: #7c3aed;
|
||||
}
|
||||
|
||||
&.amber {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
}
|
||||
|
||||
&.teal {
|
||||
background: #ccfbf1;
|
||||
color: #0d9488;
|
||||
}
|
||||
|
||||
&.red {
|
||||
background: #fee2e2;
|
||||
color: #dc2626;
|
||||
}
|
||||
}
|
||||
|
||||
&__info {
|
||||
@@ -125,10 +163,18 @@ $accent: #0ea5e9;
|
||||
:host-context(.dark-theme) {
|
||||
.acc-card {
|
||||
background: #1e1e2e;
|
||||
border-color: rgba(255, 255, 255, 0.06);
|
||||
border-color: rgb(255 255 255 / 6%);
|
||||
}
|
||||
|
||||
.acc-card__title { color: #e0e0e0; }
|
||||
.acc-card__desc { color: #666; }
|
||||
.acc-header__title { color: #e0e0e0; }
|
||||
.acc-card__title {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
|
||||
.acc-card__desc {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.acc-header__title {
|
||||
color: #e0e0e0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,6 +51,7 @@ import { ConfigurationWizardModule } from './configuration-wizard/configuration-
|
||||
import { RemittancesModule } from './remittances/remittances.module';
|
||||
import { SupersetAnalyticsModule } from './superset-analytics/superset-analytics.module';
|
||||
import { CkycModule } from './ckyc/ckyc.module';
|
||||
import { KycManagementModule } from './kyc-management/kyc-management.module';
|
||||
import { RbiCircularsModule } from './rbi-circulars/rbi-circulars.module';
|
||||
import { CfcfrmsModule } from './cfcfrms/cfcfrms.module';
|
||||
import { BankCalendarModule } from './bank-calendar/bank-calendar.module';
|
||||
@@ -141,6 +142,7 @@ export function HttpLoaderFactory(http: HttpClient) {
|
||||
RemittancesModule,
|
||||
SupersetAnalyticsModule,
|
||||
CkycModule,
|
||||
KycManagementModule,
|
||||
RbiCircularsModule,
|
||||
CfcfrmsModule,
|
||||
BankCalendarModule,
|
||||
|
||||
@@ -41,7 +41,14 @@
|
||||
<div class="hol-row hol-row--two">
|
||||
<mat-form-field appearance="outline" class="hol-field" (click)="fromPicker.open()">
|
||||
<mat-label>From Date</mat-label>
|
||||
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="fromPicker" required formControlName="fromDate" />
|
||||
<input
|
||||
matInput
|
||||
[min]="minDate"
|
||||
[max]="maxDate"
|
||||
[matDatepicker]="fromPicker"
|
||||
required
|
||||
formControlName="fromDate"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="fromPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #fromPicker></mat-datepicker>
|
||||
@if (holidayForm.controls.fromDate.hasError('required')) {
|
||||
@@ -51,7 +58,14 @@
|
||||
|
||||
<mat-form-field appearance="outline" class="hol-field" (click)="toPicker.open()">
|
||||
<mat-label>To Date</mat-label>
|
||||
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="toPicker" required formControlName="toDate" />
|
||||
<input
|
||||
matInput
|
||||
[min]="minDate"
|
||||
[max]="maxDate"
|
||||
[matDatepicker]="toPicker"
|
||||
required
|
||||
formControlName="toDate"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="toPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #toPicker></mat-datepicker>
|
||||
@if (holidayForm.controls.toDate.hasError('required')) {
|
||||
@@ -90,7 +104,14 @@
|
||||
<div class="hol-row">
|
||||
<mat-form-field appearance="outline" class="hol-field" (click)="reschPicker.open()">
|
||||
<mat-label>Repayments Rescheduled To</mat-label>
|
||||
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="reschPicker" required formControlName="repaymentsRescheduledTo" />
|
||||
<input
|
||||
matInput
|
||||
[min]="minDate"
|
||||
[max]="maxDate"
|
||||
[matDatepicker]="reschPicker"
|
||||
required
|
||||
formControlName="repaymentsRescheduledTo"
|
||||
/>
|
||||
<mat-datepicker-toggle matSuffix [for]="reschPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #reschPicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
@@ -42,12 +42,27 @@ export class AddHolidayDialogComponent implements OnInit {
|
||||
|
||||
ngOnInit() {
|
||||
this.holidayForm = this.fb.group({
|
||||
name: ['', Validators.required],
|
||||
name: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
description: [''],
|
||||
fromDate: ['', Validators.required],
|
||||
toDate: ['', Validators.required],
|
||||
reschedulingType: ['', Validators.required],
|
||||
officeId: ['', Validators.required]
|
||||
fromDate: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
toDate: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
reschedulingType: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
officeId: [
|
||||
'',
|
||||
Validators.required
|
||||
]
|
||||
});
|
||||
|
||||
this.loadData();
|
||||
|
||||
@@ -111,7 +111,16 @@
|
||||
[class.cal-grid__day--sat2nd4th]="(day.is2ndSaturday || day.is4thSaturday) && day.isCurrentMonth"
|
||||
[class.cal-grid__day--holiday]="day.isHoliday && day.isCurrentMonth"
|
||||
[class.cal-grid__day--working]="day.isWorking && day.isCurrentMonth"
|
||||
[matTooltip]="day.holidayName || (day.is2ndSaturday ? '2nd Saturday Off' : day.is4thSaturday ? '4th Saturday Off' : day.isSunday && day.isCurrentMonth ? 'Sunday' : '')"
|
||||
[matTooltip]="
|
||||
day.holidayName ||
|
||||
(day.is2ndSaturday
|
||||
? '2nd Saturday Off'
|
||||
: day.is4thSaturday
|
||||
? '4th Saturday Off'
|
||||
: day.isSunday && day.isCurrentMonth
|
||||
? 'Sunday'
|
||||
: '')
|
||||
"
|
||||
>
|
||||
{{ day.isCurrentMonth ? day.date : '' }}
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@use 'sass:color';
|
||||
|
||||
$navy: #0d1b2a;
|
||||
$blue: #1e88e5;
|
||||
$green: #10b981;
|
||||
@@ -239,7 +241,7 @@ $purple: #7c3aed;
|
||||
flex-shrink: 0;
|
||||
|
||||
&:hover {
|
||||
background: darken($blue, 8%);
|
||||
background: color.adjust($blue, $lightness: -8%);
|
||||
box-shadow: 0 2px 8px rgb(30 136 229 / 35%);
|
||||
}
|
||||
}
|
||||
@@ -434,7 +436,7 @@ $purple: #7c3aed;
|
||||
}
|
||||
|
||||
// ─── Responsive ───
|
||||
@media screen and (max-width: 1200px) {
|
||||
@media screen and (width <= 1200px) {
|
||||
.cal-months {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
@@ -444,7 +446,7 @@ $purple: #7c3aed;
|
||||
}
|
||||
}
|
||||
|
||||
@media screen and (max-width: 768px) {
|
||||
@media screen and (width <= 768px) {
|
||||
.cal-months {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
@@ -56,7 +56,15 @@ export class BankCalendarComponent implements OnInit {
|
||||
|
||||
currentYear: number;
|
||||
months: MonthData[] = [];
|
||||
weekDays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
||||
weekDays = [
|
||||
'Sun',
|
||||
'Mon',
|
||||
'Tue',
|
||||
'Wed',
|
||||
'Thu',
|
||||
'Fri',
|
||||
'Sat'
|
||||
];
|
||||
workingDaysConfig: string[] = [];
|
||||
holidays: any[] = [];
|
||||
overallStats = { working: 0, nonWorking: 0, holidays: 0, sundays: 0, sat2nd4th: 0, totalDays: 0 };
|
||||
@@ -135,7 +143,14 @@ export class BankCalendarComponent implements OnInit {
|
||||
|
||||
private loadWorkingDays() {
|
||||
// Default Indian banking working days: Mon-Sat (Sun off, 2nd & 4th Sat off)
|
||||
this.workingDaysConfig = ['MO', 'TU', 'WE', 'TH', 'FR', 'SA'];
|
||||
this.workingDaysConfig = [
|
||||
'MO',
|
||||
'TU',
|
||||
'WE',
|
||||
'TH',
|
||||
'FR',
|
||||
'SA'
|
||||
];
|
||||
this.mergeGazettedHolidays();
|
||||
this.buildCalendar();
|
||||
|
||||
@@ -157,7 +172,11 @@ export class BankCalendarComponent implements OnInit {
|
||||
for (const gh of gazetted) {
|
||||
const exists = this.holidays.some((h: any) => {
|
||||
if (h.fromDate) {
|
||||
const [y, m, d] = h.fromDate;
|
||||
const [
|
||||
y,
|
||||
m,
|
||||
d
|
||||
] = h.fromDate;
|
||||
return y === this.currentYear && m === gh.month && d === gh.day;
|
||||
}
|
||||
return false;
|
||||
@@ -165,8 +184,16 @@ export class BankCalendarComponent implements OnInit {
|
||||
if (!exists) {
|
||||
this.holidays.push({
|
||||
name: gh.name,
|
||||
fromDate: [this.currentYear, gh.month, gh.day],
|
||||
toDate: [this.currentYear, gh.month, gh.day],
|
||||
fromDate: [
|
||||
this.currentYear,
|
||||
gh.month,
|
||||
gh.day
|
||||
],
|
||||
toDate: [
|
||||
this.currentYear,
|
||||
gh.month,
|
||||
gh.day
|
||||
],
|
||||
description: gh.name,
|
||||
status: { value: 'Active' }
|
||||
});
|
||||
@@ -197,8 +224,18 @@ export class BankCalendarComponent implements OnInit {
|
||||
const today = new Date();
|
||||
|
||||
const monthNames = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December'
|
||||
];
|
||||
|
||||
const days: CalendarDay[] = [];
|
||||
@@ -287,7 +324,7 @@ export class BankCalendarComponent implements OnInit {
|
||||
isSunday: false,
|
||||
is2ndSaturday: false,
|
||||
is4thSaturday: false,
|
||||
dayOfWeek: (days.length) % 7
|
||||
dayOfWeek: days.length % 7
|
||||
});
|
||||
}
|
||||
|
||||
@@ -307,7 +344,11 @@ export class BankCalendarComponent implements OnInit {
|
||||
private getHolidayForDate(year: number, month: number, day: number): any {
|
||||
return this.holidays.find((h: any) => {
|
||||
if (h.fromDate) {
|
||||
const [y, m, d] = h.fromDate;
|
||||
const [
|
||||
y,
|
||||
m,
|
||||
d
|
||||
] = h.fromDate;
|
||||
return y === year && m === month + 1 && d === day;
|
||||
}
|
||||
return false;
|
||||
@@ -337,10 +378,28 @@ export class BankCalendarComponent implements OnInit {
|
||||
|
||||
printHolidayList() {
|
||||
const monthNames = [
|
||||
'January', 'February', 'March', 'April', 'May', 'June',
|
||||
'July', 'August', 'September', 'October', 'November', 'December'
|
||||
'January',
|
||||
'February',
|
||||
'March',
|
||||
'April',
|
||||
'May',
|
||||
'June',
|
||||
'July',
|
||||
'August',
|
||||
'September',
|
||||
'October',
|
||||
'November',
|
||||
'December'
|
||||
];
|
||||
const dayNames = [
|
||||
'Sunday',
|
||||
'Monday',
|
||||
'Tuesday',
|
||||
'Wednesday',
|
||||
'Thursday',
|
||||
'Friday',
|
||||
'Saturday'
|
||||
];
|
||||
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
||||
|
||||
// --- Section 1: Gazetted / API Holidays ---
|
||||
const gazettedList: { sno: number; date: string; day: string; occasion: string }[] = [];
|
||||
@@ -352,7 +411,11 @@ export class BankCalendarComponent implements OnInit {
|
||||
|
||||
sortedHolidays.forEach((h: any, i: number) => {
|
||||
if (h.fromDate) {
|
||||
const [y, m, d] = h.fromDate;
|
||||
const [
|
||||
y,
|
||||
m,
|
||||
d
|
||||
] = h.fromDate;
|
||||
const date = new Date(y, m - 1, d);
|
||||
gazettedList.push({
|
||||
sno: i + 1,
|
||||
@@ -406,7 +469,7 @@ export class BankCalendarComponent implements OnInit {
|
||||
}
|
||||
|
||||
const buildRows = (list: typeof gazettedList) =>
|
||||
list.map(h => `<tr><td>${h.sno}</td><td>${h.date}</td><td>${h.day}</td><td>${h.occasion}</td></tr>`).join('');
|
||||
list.map((h) => `<tr><td>${h.sno}</td><td>${h.date}</td><td>${h.day}</td><td>${h.occasion}</td></tr>`).join('');
|
||||
|
||||
const html = `
|
||||
<!DOCTYPE html>
|
||||
|
||||
@@ -25,16 +25,32 @@
|
||||
<input matInput placeholder="{{ 'labels.text.SearchByExternalID' | translate }}" [formControl]="externalId" />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-checkbox #showClosedCenters labelPosition="after" (change)="changeShowClosedCenters()" class="table-checkbox">
|
||||
<mat-checkbox
|
||||
#showClosedCenters
|
||||
labelPosition="after"
|
||||
(change)="changeShowClosedCenters()"
|
||||
class="table-checkbox"
|
||||
>
|
||||
{{ 'labels.inputs.Show Closed Centers' | translate }}
|
||||
</mat-checkbox>
|
||||
|
||||
<div class="table-buttons">
|
||||
<button mat-stroked-button [routerLink]="['../', 'organization', 'bulk-import', 'Centers']" *mifosxHasPermission="'CREATE_CENTER'" class="btn-import">
|
||||
<button
|
||||
mat-stroked-button
|
||||
[routerLink]="['../', 'organization', 'bulk-import', 'Centers']"
|
||||
*mifosxHasPermission="'CREATE_CENTER'"
|
||||
class="btn-import"
|
||||
>
|
||||
<fa-icon icon="download"></fa-icon>
|
||||
{{ 'labels.buttons.Import Center' | translate }}
|
||||
</button>
|
||||
<button mat-flat-button color="primary" [routerLink]="['create']" *mifosxHasPermission="'CREATE_CENTER'" class="btn-create">
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
[routerLink]="['create']"
|
||||
*mifosxHasPermission="'CREATE_CENTER'"
|
||||
class="btn-create"
|
||||
>
|
||||
<fa-icon icon="plus"></fa-icon>
|
||||
{{ 'labels.buttons.Create Center' | translate }}
|
||||
</button>
|
||||
@@ -75,7 +91,12 @@
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns" [routerLink]="[row.id, 'general']" class="table-row"></tr>
|
||||
<tr
|
||||
mat-row
|
||||
*matRowDef="let row; columns: displayedColumns"
|
||||
[routerLink]="[row.id, 'general']"
|
||||
class="table-row"
|
||||
></tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator
|
||||
|
||||
@@ -14,11 +14,13 @@
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 4px 0 0;
|
||||
color: #666;
|
||||
@@ -73,9 +75,11 @@
|
||||
.result-card {
|
||||
border-left: 4px solid #ccc;
|
||||
}
|
||||
|
||||
.result-flagged {
|
||||
border-left-color: #ef4444;
|
||||
}
|
||||
|
||||
.result-clear {
|
||||
border-left-color: #10b981;
|
||||
}
|
||||
@@ -83,6 +87,7 @@
|
||||
.warn-icon {
|
||||
color: #ef4444;
|
||||
}
|
||||
|
||||
.clear-icon {
|
||||
color: #10b981;
|
||||
}
|
||||
@@ -100,9 +105,11 @@
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ml-24 {
|
||||
margin-left: 24px;
|
||||
}
|
||||
@@ -110,9 +117,11 @@
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mr-05 {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
@@ -152,6 +161,7 @@
|
||||
font-size: 13px;
|
||||
color: #7f1d1d;
|
||||
border-bottom: 1px solid #fee2e2;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
@@ -162,6 +172,7 @@
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
color: #065f46;
|
||||
|
||||
p {
|
||||
margin-top: 12px;
|
||||
font-size: 15px;
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
@media (width <= 1280px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,9 +58,11 @@
|
||||
&.pending {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
&.active {
|
||||
background: #1e88e5;
|
||||
}
|
||||
|
||||
&.resolved {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
@media (width <= 1280px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
@@ -80,36 +80,47 @@
|
||||
|
||||
&.stat-total {
|
||||
border-color: #1e88e5;
|
||||
|
||||
.stat-icon {
|
||||
background: #1e88e5;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #1e88e5;
|
||||
}
|
||||
}
|
||||
|
||||
&.stat-pending {
|
||||
border-color: #f59e0b;
|
||||
|
||||
.stat-icon {
|
||||
background: #f59e0b;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #f59e0b;
|
||||
}
|
||||
}
|
||||
|
||||
&.stat-lien {
|
||||
border-color: #eab308;
|
||||
|
||||
.stat-icon {
|
||||
background: #eab308;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #92400e;
|
||||
}
|
||||
}
|
||||
|
||||
&.stat-recovered {
|
||||
border-color: #10b981;
|
||||
|
||||
.stat-icon {
|
||||
background: #10b981;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
color: #10b981;
|
||||
}
|
||||
@@ -122,7 +133,7 @@
|
||||
gap: 20px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
@media (width <= 1280px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
gap: 24px;
|
||||
align-items: start;
|
||||
|
||||
@media (max-width: 1280px) {
|
||||
@media (width <= 1280px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,70 +11,88 @@
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 4px 0 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.report-layout {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.report-card {
|
||||
flex: 1;
|
||||
max-width: 700px;
|
||||
}
|
||||
|
||||
.wide-card {
|
||||
max-width: 900px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.half-width {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.mr-05 {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-16 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
margin-top: 8px;
|
||||
height: 48px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.inline-spinner {
|
||||
display: inline-block;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.checkbox-row {
|
||||
margin: 8px 0 16px;
|
||||
}
|
||||
|
||||
.success-result {
|
||||
text-align: center;
|
||||
padding: 32px 16px;
|
||||
|
||||
.success-icon {
|
||||
color: #10b981;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #10b981;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
.result-details {
|
||||
text-align: left;
|
||||
max-width: 500px;
|
||||
@@ -83,25 +101,31 @@
|
||||
border-radius: 8px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.result-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
.result-label {
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.result-value {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
|
||||
.status-submitted {
|
||||
color: #1e88e5;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -14,11 +14,13 @@
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 4px 0 0;
|
||||
color: #666;
|
||||
@@ -43,6 +45,7 @@
|
||||
.mr-05 {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-16 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
@@ -61,10 +64,12 @@
|
||||
.success-result {
|
||||
text-align: center;
|
||||
padding: 32px 16px;
|
||||
|
||||
.success-icon {
|
||||
color: #10b981;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #10b981;
|
||||
margin-bottom: 24px;
|
||||
@@ -85,6 +90,7 @@
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
@@ -94,9 +100,11 @@
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.result-value {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
|
||||
@@ -14,11 +14,13 @@
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 4px 0 0;
|
||||
color: #666;
|
||||
@@ -43,6 +45,7 @@
|
||||
.mr-05 {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-16 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
@@ -61,10 +64,12 @@
|
||||
.success-result {
|
||||
text-align: center;
|
||||
padding: 32px 16px;
|
||||
|
||||
.success-icon {
|
||||
color: #10b981;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #10b981;
|
||||
margin-bottom: 24px;
|
||||
@@ -85,6 +90,7 @@
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
@@ -94,12 +100,15 @@
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.result-value {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
|
||||
.status-flagged {
|
||||
color: #ef4444;
|
||||
font-weight: 700;
|
||||
|
||||
@@ -14,11 +14,13 @@
|
||||
|
||||
.page-header {
|
||||
margin-bottom: 24px;
|
||||
|
||||
h2 {
|
||||
margin: 0;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 4px 0 0;
|
||||
color: #666;
|
||||
@@ -43,6 +45,7 @@
|
||||
.mr-05 {
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.mt-16 {
|
||||
margin-top: 16px;
|
||||
}
|
||||
@@ -61,10 +64,12 @@
|
||||
.success-result {
|
||||
text-align: center;
|
||||
padding: 32px 16px;
|
||||
|
||||
.success-icon {
|
||||
color: #10b981;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: #10b981;
|
||||
margin-bottom: 24px;
|
||||
@@ -85,6 +90,7 @@
|
||||
justify-content: space-between;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
@@ -94,9 +100,11 @@
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.result-value {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
|
||||
@@ -6,14 +6,565 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="container">
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<div class="placeholder-content">
|
||||
<fa-icon icon="shield-alt" size="3x" class="placeholder-icon"></fa-icon>
|
||||
<h3>{{ 'labels.heading.CKYC' | translate }}</h3>
|
||||
<p>{{ 'labels.text.This section is under development.' | translate }}</p>
|
||||
<div class="cv-container">
|
||||
<!-- Header -->
|
||||
<div class="cv-header">
|
||||
<div class="cv-header__left">
|
||||
<div class="cv-header__icon"><fa-icon icon="shield-alt"></fa-icon></div>
|
||||
<div>
|
||||
<h2 class="cv-header__title">CKYC Verification</h2>
|
||||
<p class="cv-header__sub">Batch · Upload · Match — CERSAI compliance pipeline</p>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
</div>
|
||||
<button class="cv-btn cv-btn--ghost" (click)="fetch()">
|
||||
<fa-icon icon="sync"></fa-icon>
|
||||
<span>Refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Summary KPIs -->
|
||||
<div class="cv-kpis">
|
||||
<div class="cv-kpi">
|
||||
<div class="cv-kpi__icon cv-kpi__icon--blue"><fa-icon icon="file-archive"></fa-icon></div>
|
||||
<div>
|
||||
<div class="cv-kpi__label">Total batches</div>
|
||||
<div class="cv-kpi__value">{{ totalBatches }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cv-kpi">
|
||||
<div class="cv-kpi__icon cv-kpi__icon--teal"><fa-icon icon="users"></fa-icon></div>
|
||||
<div>
|
||||
<div class="cv-kpi__label">Total records</div>
|
||||
<div class="cv-kpi__value">{{ totalRecords }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cv-kpi">
|
||||
<div class="cv-kpi__icon cv-kpi__icon--amber"><fa-icon icon="hourglass-half"></fa-icon></div>
|
||||
<div>
|
||||
<div class="cv-kpi__label">Pending upload</div>
|
||||
<div class="cv-kpi__value">{{ totalPending }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cv-kpi">
|
||||
<div class="cv-kpi__icon cv-kpi__icon--green"><fa-icon icon="check-double"></fa-icon></div>
|
||||
<div>
|
||||
<div class="cv-kpi__label">Uploaded</div>
|
||||
<div class="cv-kpi__value">{{ totalUploaded }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cv-kpi">
|
||||
<div class="cv-kpi__icon cv-kpi__icon--purple"><fa-icon icon="database"></fa-icon></div>
|
||||
<div>
|
||||
<div class="cv-kpi__label">Total size</div>
|
||||
<div class="cv-kpi__value">{{ formatBytes(totalSize) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="cv-filters">
|
||||
<div class="cv-filters__grid">
|
||||
<div class="cv-field">
|
||||
<label class="cv-label"><fa-icon icon="calendar"></fa-icon> From date</label>
|
||||
<input type="date" class="cv-input" [(ngModel)]="filters.fromDate" />
|
||||
</div>
|
||||
<div class="cv-field">
|
||||
<label class="cv-label"><fa-icon icon="calendar"></fa-icon> To date</label>
|
||||
<input type="date" class="cv-input" [(ngModel)]="filters.toDate" />
|
||||
</div>
|
||||
<div class="cv-field">
|
||||
<label class="cv-label"><fa-icon icon="filter"></fa-icon> Upload status</label>
|
||||
<select class="cv-input" [(ngModel)]="filters.uploadedToCersai">
|
||||
<option value="">All batches</option>
|
||||
<option value="false">Pending upload</option>
|
||||
<option value="true">Uploaded</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cv-filters__actions">
|
||||
<button class="cv-btn cv-btn--ghost" (click)="resetFilters()">
|
||||
<fa-icon icon="times"></fa-icon> <span>Reset</span>
|
||||
</button>
|
||||
<button class="cv-btn cv-btn--primary" (click)="fetch()">
|
||||
<fa-icon icon="search"></fa-icon> <span>Apply filters</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Section tabs -->
|
||||
<div class="cv-tabs">
|
||||
<button
|
||||
type="button"
|
||||
class="cv-tab"
|
||||
[class.cv-tab--active]="activeSection === 'zip'"
|
||||
(click)="activeSection = 'zip'"
|
||||
>
|
||||
<fa-icon icon="file-archive"></fa-icon>
|
||||
<span>ZIP Batch Details</span>
|
||||
<span class="cv-tab__badge cv-tab__badge--blue">Phase 2</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="cv-tab"
|
||||
[class.cv-tab--active]="activeSection === 'sftp'"
|
||||
(click)="activeSection = 'sftp'"
|
||||
>
|
||||
<fa-icon icon="cloud-upload-alt"></fa-icon>
|
||||
<span>SFTP Upload & ACK · API Match</span>
|
||||
@if (totalPending > 0) {
|
||||
<span class="cv-tab__count">{{ totalPending }}</span>
|
||||
}
|
||||
<span class="cv-tab__badge cv-tab__badge--amber">Phase 3-4</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- SECTION 1: ZIP Details -->
|
||||
@if (activeSection === 'zip') {
|
||||
<div class="cv-section">
|
||||
<div class="cv-section__head">
|
||||
<div class="cv-section__title">
|
||||
<fa-icon icon="file-archive"></fa-icon>
|
||||
<div>
|
||||
<h3>ZIP Batch Details</h3>
|
||||
<p>CERSAI ZIP archives grouped by creation date</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="cv-badge cv-badge--blue">Phase 2</span>
|
||||
</div>
|
||||
|
||||
@if (loading) {
|
||||
<div class="cv-state cv-state--loading">
|
||||
<fa-icon icon="spinner" animation="spin"></fa-icon>
|
||||
<span>Loading batches…</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (error && !loading) {
|
||||
<div class="cv-state cv-state--error">
|
||||
<fa-icon icon="exclamation-triangle"></fa-icon>
|
||||
<span>{{ error }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!loading && !error && groups.length === 0) {
|
||||
<div class="cv-state cv-state--empty">
|
||||
<fa-icon icon="file-archive" size="2x"></fa-icon>
|
||||
<h4>No batches found</h4>
|
||||
<p>Create a batch from the Record Master to see it here.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!loading && groups.length > 0) {
|
||||
<div class="cv-groups">
|
||||
@for (group of groups; track group.dateLabel) {
|
||||
<div class="cv-group" [class.cv-group--open]="group.expanded">
|
||||
<button class="cv-group__head" (click)="toggleGroup(group)">
|
||||
<div class="cv-group__left">
|
||||
<fa-icon [icon]="group.expanded ? 'chevron-down' : 'chevron-right'" class="cv-group__chev"></fa-icon>
|
||||
<div class="cv-group__date-pill">
|
||||
<fa-icon icon="calendar-check"></fa-icon>
|
||||
<span>{{ group.dateLabel }}</span>
|
||||
</div>
|
||||
<div class="cv-group__summary">
|
||||
<span
|
||||
><strong>{{ group.batchCount }}</strong> batches</span
|
||||
>
|
||||
<span class="cv-group__sep">·</span>
|
||||
<span
|
||||
><strong>{{ group.totalRecordCount }}</strong> records</span
|
||||
>
|
||||
<span class="cv-group__sep">·</span>
|
||||
<span>{{ formatBytes(group.totalSizeBytes) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cv-group__right">
|
||||
@if (group.pendingCount > 0) {
|
||||
<span class="cv-pill cv-pill--amber">
|
||||
<fa-icon icon="hourglass-half"></fa-icon> {{ group.pendingCount }} pending
|
||||
</span>
|
||||
}
|
||||
@if (group.uploadedCount > 0) {
|
||||
<span class="cv-pill cv-pill--green">
|
||||
<fa-icon icon="check-double"></fa-icon> {{ group.uploadedCount }} uploaded
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</button>
|
||||
|
||||
@if (group.expanded) {
|
||||
<div class="cv-group__body">
|
||||
<table class="cv-batches">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Batch ID</th>
|
||||
<th>Filename</th>
|
||||
<th class="cv-batches__num">Records</th>
|
||||
<th class="cv-batches__num">Size</th>
|
||||
<th>Status</th>
|
||||
<th class="cv-batches__actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (batch of group.batches; track trackBatch($index, batch)) {
|
||||
<tr>
|
||||
<td class="cv-batches__bid">
|
||||
<fa-icon icon="tag"></fa-icon>
|
||||
<span>{{ batch.batchId }}</span>
|
||||
</td>
|
||||
<td class="cv-batches__file">
|
||||
<fa-icon icon="file-archive"></fa-icon>
|
||||
<span>{{ batch.filename }}</span>
|
||||
</td>
|
||||
<td class="cv-batches__num">{{ batch.recordCount }}</td>
|
||||
<td class="cv-batches__num">{{ formatBytes(batch.sizeBytes) }}</td>
|
||||
<td>
|
||||
@if (batch.uploadedToCersai) {
|
||||
<span class="cv-pill cv-pill--green">
|
||||
<fa-icon icon="check-circle"></fa-icon> Uploaded
|
||||
</span>
|
||||
} @else {
|
||||
<span class="cv-pill cv-pill--amber">
|
||||
<fa-icon icon="hourglass-half"></fa-icon> Pending
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td class="cv-batches__actions">
|
||||
<button class="cv-action-btn" (click)="openManifest(batch, $event)" title="View manifest">
|
||||
<fa-icon icon="eye"></fa-icon>
|
||||
</button>
|
||||
@if (batch.documentId) {
|
||||
<button
|
||||
class="cv-action-btn cv-action-btn--primary"
|
||||
(click)="downloadZip(batch, $event)"
|
||||
title="Download ZIP"
|
||||
>
|
||||
<fa-icon icon="download"></fa-icon>
|
||||
</button>
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- SECTION 2: SFTP Upload + API Match -->
|
||||
@if (activeSection === 'sftp') {
|
||||
<div class="cv-section">
|
||||
<div class="cv-section__head">
|
||||
<div class="cv-section__title">
|
||||
<fa-icon icon="cloud-upload-alt"></fa-icon>
|
||||
<div>
|
||||
<h3>SFTP Upload & ACK · API Match</h3>
|
||||
<p>CERSAI SFTP transfer history with ACK receipts and per-batch logs</p>
|
||||
</div>
|
||||
</div>
|
||||
<span class="cv-badge cv-badge--amber">Phase 3-4</span>
|
||||
</div>
|
||||
|
||||
<div class="cv-sftp-kpis">
|
||||
<div class="cv-sftp-kpi cv-sftp-kpi--green">
|
||||
<div class="cv-sftp-kpi__icon"><fa-icon icon="check-circle"></fa-icon></div>
|
||||
<div>
|
||||
<div class="cv-sftp-kpi__value">{{ sftpSuccessCount }}</div>
|
||||
<div class="cv-sftp-kpi__label">Uploaded successfully</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cv-sftp-kpi cv-sftp-kpi--red">
|
||||
<div class="cv-sftp-kpi__icon"><fa-icon icon="times-circle"></fa-icon></div>
|
||||
<div>
|
||||
<div class="cv-sftp-kpi__value">{{ sftpFailedCount }}</div>
|
||||
<div class="cv-sftp-kpi__label">Failed</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="cv-sftp-kpi cv-sftp-kpi--amber">
|
||||
<div class="cv-sftp-kpi__icon"><fa-icon icon="hourglass-half"></fa-icon></div>
|
||||
<div>
|
||||
<div class="cv-sftp-kpi__value">{{ sftpPendingCount }}</div>
|
||||
<div class="cv-sftp-kpi__label">Pending upload</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (loading) {
|
||||
<div class="cv-state cv-state--loading">
|
||||
<fa-icon icon="spinner" animation="spin"></fa-icon>
|
||||
<span>Loading logs…</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!loading && sftpLogs.length === 0) {
|
||||
<div class="cv-state cv-state--empty">
|
||||
<fa-icon icon="cloud-upload-alt" size="2x"></fa-icon>
|
||||
<h4>No SFTP upload history yet</h4>
|
||||
<p>Upload a batch from the Record Master to see CERSAI transfer logs here.</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!loading && sftpLogs.length > 0) {
|
||||
<div class="cv-sftp-logs">
|
||||
@for (log of sftpLogs; track log.id) {
|
||||
<div
|
||||
class="cv-sftp-log"
|
||||
[class.cv-sftp-log--success]="isSftpSuccess(log)"
|
||||
[class.cv-sftp-log--failed]="isSftpFailed(log)"
|
||||
>
|
||||
<div class="cv-sftp-log__icon">
|
||||
@if (isSftpSuccess(log)) {
|
||||
<fa-icon icon="check-circle"></fa-icon>
|
||||
} @else if (isSftpFailed(log)) {
|
||||
<fa-icon icon="times-circle"></fa-icon>
|
||||
} @else {
|
||||
<fa-icon icon="hourglass-half"></fa-icon>
|
||||
}
|
||||
</div>
|
||||
<div class="cv-sftp-log__body">
|
||||
<div class="cv-sftp-log__head">
|
||||
<span class="cv-sftp-log__bid"> <fa-icon icon="tag"></fa-icon> {{ log.batchId }} </span>
|
||||
@if (isSftpSuccess(log)) {
|
||||
<span class="cv-pill cv-pill--green">
|
||||
<fa-icon icon="check-circle"></fa-icon> Upload successful
|
||||
</span>
|
||||
} @else if (isSftpFailed(log)) {
|
||||
<span class="cv-pill cv-pill--red"> <fa-icon icon="times-circle"></fa-icon> Upload failed </span>
|
||||
} @else {
|
||||
<span class="cv-pill cv-pill--amber"> <fa-icon icon="hourglass-half"></fa-icon> Pending </span>
|
||||
}
|
||||
<span class="cv-sftp-log__time">
|
||||
<fa-icon icon="clock"></fa-icon>
|
||||
{{ log.uploadedOnLabel || log.createdOnLabel || '—' }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="cv-sftp-log__meta">
|
||||
<div>
|
||||
<span class="cv-sftp-log__label">Filename</span>
|
||||
<span class="cv-sftp-log__value">{{ log.filename }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="cv-sftp-log__label">Records</span>
|
||||
<span class="cv-sftp-log__value">{{ log.recordCount }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="cv-sftp-log__label">Size</span>
|
||||
<span class="cv-sftp-log__value">{{ formatBytes(log.sizeBytes) }}</span>
|
||||
</div>
|
||||
@if (log.cersaiReference) {
|
||||
<div>
|
||||
<span class="cv-sftp-log__label">CERSAI Ref</span>
|
||||
<span class="cv-sftp-log__value cv-sftp-log__value--mono">{{ log.cersaiReference }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (log.uploadRemarks) {
|
||||
<div class="cv-sftp-log__remarks" [class.cv-sftp-log__remarks--err]="isSftpFailed(log)">
|
||||
<fa-icon [icon]="isSftpFailed(log) ? 'exclamation-triangle' : 'info-circle'"></fa-icon>
|
||||
<span>{{ log.uploadRemarks }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="cv-sftp-log__actions">
|
||||
<button class="cv-action-btn" (click)="openManifest(log, $event)" title="View manifest">
|
||||
<fa-icon icon="eye"></fa-icon>
|
||||
</button>
|
||||
@if (log.documentId) {
|
||||
<button
|
||||
class="cv-action-btn cv-action-btn--primary"
|
||||
(click)="downloadZip(log, $event)"
|
||||
title="Download ZIP"
|
||||
>
|
||||
<fa-icon icon="download"></fa-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Manifest modal -->
|
||||
@if (showManifest) {
|
||||
<div class="cv-modal" (click)="closeManifest()">
|
||||
<div class="cv-modal__card" (click)="$event.stopPropagation()">
|
||||
<div class="cv-modal__header">
|
||||
<div class="cv-modal__title">
|
||||
<fa-icon icon="file-archive"></fa-icon>
|
||||
Batch Manifest
|
||||
</div>
|
||||
<button class="cv-modal__close" (click)="closeManifest()">
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="cv-modal__body">
|
||||
@if (manifestLoading) {
|
||||
<div class="cv-state cv-state--loading">
|
||||
<fa-icon icon="spinner" animation="spin"></fa-icon>
|
||||
<span>Loading manifest…</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (manifestError) {
|
||||
<div class="cv-state cv-state--error">
|
||||
<fa-icon icon="exclamation-triangle"></fa-icon>
|
||||
<span>{{ manifestError }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (manifestBatch && !manifestLoading) {
|
||||
<div class="cv-manifest-head">
|
||||
<div class="cv-manifest-head__grid">
|
||||
<div>
|
||||
<span class="cv-kpi__label">Batch ID</span>
|
||||
<span class="cv-kpi__value cv-kpi__value--mono">{{ manifestBatch.batchId }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="cv-kpi__label">Created on</span>
|
||||
<span class="cv-kpi__value">{{ manifestBatch.createdOnLabel || '—' }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="cv-kpi__label">Records</span>
|
||||
<span class="cv-kpi__value">{{ manifestBatch.recordCount }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="cv-kpi__label">Size</span>
|
||||
<span class="cv-kpi__value">{{ formatBytes(manifestBatch.sizeBytes) }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="cv-kpi__label">Status</span>
|
||||
@if (manifestBatch.uploadedToCersai) {
|
||||
<span class="cv-pill cv-pill--green"> <fa-icon icon="check-circle"></fa-icon> Uploaded </span>
|
||||
} @else {
|
||||
<span class="cv-pill cv-pill--amber"> <fa-icon icon="hourglass-half"></fa-icon> Pending </span>
|
||||
}
|
||||
</div>
|
||||
<div class="cv-manifest-head__grid-span">
|
||||
<span class="cv-kpi__label">Filename</span>
|
||||
<span class="cv-kpi__value">{{ manifestBatch.filename }}</span>
|
||||
</div>
|
||||
<div class="cv-manifest-head__grid-span">
|
||||
<span class="cv-kpi__label">Checksum</span>
|
||||
<span class="cv-kpi__value cv-kpi__value--mono">{{ manifestBatch.checksum }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (manifestBatch.manifest?.length > 0) {
|
||||
<div class="cv-manifest-title">
|
||||
<fa-icon icon="list-alt"></fa-icon>
|
||||
Packaged clients ({{ manifestBatch.manifest.length }})
|
||||
</div>
|
||||
<div class="cv-manifest-table-wrap">
|
||||
<table class="cv-manifest-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Client</th>
|
||||
<th class="cv-th-c">P</th>
|
||||
<th class="cv-th-c">S</th>
|
||||
<th class="cv-th-c">ID</th>
|
||||
<th class="cv-th-c">Ad</th>
|
||||
<th class="cv-th-r">Total size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (entry of manifestBatch.manifest; track entry.clientId) {
|
||||
<tr [class.cv-manifest-row--complete]="isEntryComplete(entry)">
|
||||
<td>
|
||||
<div class="cv-manifest-client">
|
||||
<strong>{{ entry.displayName || 'Client ' + entry.clientId }}</strong>
|
||||
@if (entry.cifNumber) {
|
||||
<span class="cv-manifest-cif">
|
||||
<fa-icon icon="hashtag"></fa-icon>{{ entry.cifNumber }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td class="cv-td-c">
|
||||
<span
|
||||
class="cv-dot"
|
||||
[class.cv-dot--ok]="entry.photo?.packaged"
|
||||
[class.cv-dot--miss]="!entry.photo?.packaged"
|
||||
[title]="entry.photo?.zipEntry || 'Missing'"
|
||||
>
|
||||
<fa-icon [icon]="entry.photo?.packaged ? 'check' : 'times'"></fa-icon>
|
||||
</span>
|
||||
</td>
|
||||
<td class="cv-td-c">
|
||||
<span
|
||||
class="cv-dot"
|
||||
[class.cv-dot--ok]="entry.signature?.packaged"
|
||||
[class.cv-dot--miss]="!entry.signature?.packaged"
|
||||
[title]="entry.signature?.zipEntry || 'Missing'"
|
||||
>
|
||||
<fa-icon [icon]="entry.signature?.packaged ? 'check' : 'times'"></fa-icon>
|
||||
</span>
|
||||
</td>
|
||||
<td class="cv-td-c">
|
||||
<span
|
||||
class="cv-dot"
|
||||
[class.cv-dot--ok]="entry.idProof?.packaged"
|
||||
[class.cv-dot--miss]="!entry.idProof?.packaged"
|
||||
[title]="
|
||||
(entry.idProof?.type || 'ID') +
|
||||
(entry.idProof?.zipEntry ? ' — ' + entry.idProof.zipEntry : '')
|
||||
"
|
||||
>
|
||||
<fa-icon [icon]="entry.idProof?.packaged ? 'check' : 'times'"></fa-icon>
|
||||
</span>
|
||||
</td>
|
||||
<td class="cv-td-c">
|
||||
<span
|
||||
class="cv-dot"
|
||||
[class.cv-dot--ok]="entry.addressProof?.packaged"
|
||||
[class.cv-dot--miss]="!entry.addressProof?.packaged"
|
||||
[title]="
|
||||
(entry.addressProof?.type || 'Address') +
|
||||
(entry.addressProof?.zipEntry ? ' — ' + entry.addressProof.zipEntry : '')
|
||||
"
|
||||
>
|
||||
<fa-icon [icon]="entry.addressProof?.packaged ? 'check' : 'times'"></fa-icon>
|
||||
</span>
|
||||
</td>
|
||||
<td class="cv-td-r cv-manifest-size">
|
||||
{{
|
||||
formatBytes(
|
||||
(entry.photo?.bytes || 0) +
|
||||
(entry.signature?.bytes || 0) +
|
||||
(entry.idProof?.bytes || 0) +
|
||||
(entry.addressProof?.bytes || 0)
|
||||
)
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="cv-modal__footer">
|
||||
<button class="cv-btn cv-btn--ghost" (click)="closeManifest()"><fa-icon icon="times"></fa-icon> Close</button>
|
||||
@if (manifestBatch?.documentId) {
|
||||
<button class="cv-btn cv-btn--primary" (click)="downloadZip(manifestBatch, $event)">
|
||||
<fa-icon icon="download"></fa-icon>
|
||||
<span>Download ZIP</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,16 +6,312 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit, DestroyRef, inject } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { ClientsService } from '../../clients/clients.service';
|
||||
|
||||
interface CkycBatch {
|
||||
id: number;
|
||||
batchId: string;
|
||||
filename: string;
|
||||
checksum: string;
|
||||
recordCount: number;
|
||||
sizeBytes: number;
|
||||
documentId?: number;
|
||||
createdOn: any;
|
||||
uploadedToCersai: boolean;
|
||||
uploadedOn?: any;
|
||||
uploadStatus?: string;
|
||||
uploadRemarks?: string;
|
||||
cersaiReference?: string;
|
||||
createdOnLabel?: string;
|
||||
uploadedOnLabel?: string;
|
||||
}
|
||||
|
||||
interface CkycBatchGroup {
|
||||
date: any;
|
||||
dateLabel: string;
|
||||
batchCount: number;
|
||||
totalRecordCount: number;
|
||||
totalSizeBytes: number;
|
||||
uploadedCount: number;
|
||||
pendingCount: number;
|
||||
batches: CkycBatch[];
|
||||
expanded: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-ckyc-verification',
|
||||
templateUrl: './ckyc-verification.component.html',
|
||||
styleUrls: ['./ckyc-verification.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
FaIconComponent
|
||||
FaIconComponent,
|
||||
FormsModule
|
||||
]
|
||||
})
|
||||
export class CkycVerificationComponent {}
|
||||
export class CkycVerificationComponent implements OnInit {
|
||||
private clientsService = inject(ClientsService);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
filters: { fromDate: string; toDate: string; uploadedToCersai: '' | 'true' | 'false' } = {
|
||||
fromDate: '',
|
||||
toDate: '',
|
||||
uploadedToCersai: ''
|
||||
};
|
||||
|
||||
loading = false;
|
||||
error = '';
|
||||
activeSection: 'zip' | 'sftp' = 'zip';
|
||||
groups: CkycBatchGroup[] = [];
|
||||
totalBatches = 0;
|
||||
totalRecords = 0;
|
||||
totalSize = 0;
|
||||
totalPending = 0;
|
||||
totalUploaded = 0;
|
||||
|
||||
/** SFTP log view */
|
||||
sftpLogs: CkycBatch[] = [];
|
||||
sftpSuccessCount = 0;
|
||||
sftpFailedCount = 0;
|
||||
sftpPendingCount = 0;
|
||||
|
||||
/** Manifest dialog */
|
||||
showManifest = false;
|
||||
manifestLoading = false;
|
||||
manifestError = '';
|
||||
manifestBatch: any = null;
|
||||
|
||||
ngOnInit(): void {
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
fetch(): void {
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
const query: any = { groupByDate: true };
|
||||
if (this.filters.fromDate) query.fromDate = this.filters.fromDate;
|
||||
if (this.filters.toDate) query.toDate = this.filters.toDate;
|
||||
if (this.filters.uploadedToCersai !== '') query.uploadedToCersai = this.filters.uploadedToCersai;
|
||||
|
||||
this.clientsService
|
||||
.getCkycBatches(query)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: (res: any) => {
|
||||
const rawGroups = res?.groups || [];
|
||||
this.groups = rawGroups.map((g: any, idx: number) => ({
|
||||
date: g.date,
|
||||
dateLabel: this.formatJavaDate(g.date),
|
||||
batchCount: g.batchCount || 0,
|
||||
totalRecordCount: g.totalRecordCount || 0,
|
||||
totalSizeBytes: g.totalSizeBytes || 0,
|
||||
uploadedCount: g.uploadedCount || 0,
|
||||
pendingCount: g.pendingCount || 0,
|
||||
batches: (g.batches || []).map((b: any) => ({
|
||||
...b,
|
||||
createdOnLabel: this.formatJavaDate(b.createdOn),
|
||||
uploadedOnLabel: this.formatJavaDateTime(b.uploadedOn)
|
||||
})),
|
||||
expanded: idx === 0
|
||||
}));
|
||||
this.computeSummary();
|
||||
this.computeSftpLogs();
|
||||
this.loading = false;
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.loading = false;
|
||||
this.error = err?.error?.defaultUserMessage || err?.message || 'Failed to load CKYC batches.';
|
||||
this.groups = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private computeSummary(): void {
|
||||
this.totalBatches = 0;
|
||||
this.totalRecords = 0;
|
||||
this.totalSize = 0;
|
||||
this.totalPending = 0;
|
||||
this.totalUploaded = 0;
|
||||
for (const g of this.groups) {
|
||||
this.totalBatches += g.batchCount;
|
||||
this.totalRecords += g.totalRecordCount;
|
||||
this.totalSize += g.totalSizeBytes;
|
||||
this.totalPending += g.pendingCount;
|
||||
this.totalUploaded += g.uploadedCount;
|
||||
}
|
||||
}
|
||||
|
||||
resetFilters(): void {
|
||||
this.filters = { fromDate: '', toDate: '', uploadedToCersai: '' };
|
||||
this.fetch();
|
||||
}
|
||||
|
||||
toggleGroup(group: CkycBatchGroup): void {
|
||||
group.expanded = !group.expanded;
|
||||
}
|
||||
|
||||
openManifest(batch: CkycBatch, event: Event): void {
|
||||
event.stopPropagation();
|
||||
this.showManifest = true;
|
||||
this.manifestLoading = true;
|
||||
this.manifestError = '';
|
||||
this.manifestBatch = { ...batch, createdOnLabel: this.formatJavaDate(batch.createdOn) };
|
||||
|
||||
this.clientsService
|
||||
.getCkycBatchById(batch.id)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: (res: any) => {
|
||||
this.manifestBatch = {
|
||||
...res,
|
||||
createdOnLabel: this.formatJavaDate(res.createdOn),
|
||||
uploadedOnLabel: this.formatJavaDateTime(res.uploadedOn)
|
||||
};
|
||||
this.manifestLoading = false;
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.manifestLoading = false;
|
||||
this.manifestError = err?.error?.defaultUserMessage || err?.message || 'Failed to load batch details.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
closeManifest(): void {
|
||||
this.showManifest = false;
|
||||
this.manifestBatch = null;
|
||||
this.manifestError = '';
|
||||
}
|
||||
|
||||
downloadZip(batch: CkycBatch, event: Event): void {
|
||||
event.stopPropagation();
|
||||
if (!batch.documentId) return;
|
||||
this.clientsService
|
||||
.downloadCkycBatchZip(batch.id, batch.documentId)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: (blob: Blob) => {
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = batch.filename || `batch-${batch.batchId}.zip`;
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
},
|
||||
error: () => undefined
|
||||
});
|
||||
}
|
||||
|
||||
formatJavaDate(value: any): string {
|
||||
if (!value) return '';
|
||||
if (Array.isArray(value) && value.length >= 3) {
|
||||
const [
|
||||
y,
|
||||
m,
|
||||
d
|
||||
] = value;
|
||||
return `${String(d).padStart(2, '0')}-${String(m).padStart(2, '0')}-${y}`;
|
||||
}
|
||||
if (typeof value === 'string') return value;
|
||||
return '';
|
||||
}
|
||||
|
||||
formatJavaDateTime(value: any): string {
|
||||
if (value == null || value === '') return '';
|
||||
const pad = (n: any) => String(n ?? 0).padStart(2, '0');
|
||||
if (Array.isArray(value) && value.length >= 3) {
|
||||
const [
|
||||
y,
|
||||
mo,
|
||||
d,
|
||||
h = 0,
|
||||
mi = 0,
|
||||
s = 0
|
||||
] = value;
|
||||
const date = `${pad(d)}-${pad(mo)}-${y}`;
|
||||
if (value.length >= 4) {
|
||||
return `${date} ${pad(h)}:${pad(mi)}:${pad(s)}`;
|
||||
}
|
||||
return date;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const dt = new Date(value);
|
||||
if (!isNaN(dt.getTime())) {
|
||||
return `${pad(dt.getDate())}-${pad(dt.getMonth() + 1)}-${dt.getFullYear()} ${pad(dt.getHours())}:${pad(dt.getMinutes())}:${pad(dt.getSeconds())}`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private computeSftpLogs(): void {
|
||||
const flat: CkycBatch[] = [];
|
||||
for (const g of this.groups) {
|
||||
for (const b of g.batches) {
|
||||
if (b.uploadedToCersai || b.uploadStatus || b.uploadedOn) {
|
||||
flat.push(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
flat.sort((a, b) => {
|
||||
const at = this.dateValue(a.uploadedOn);
|
||||
const bt = this.dateValue(b.uploadedOn);
|
||||
return bt - at;
|
||||
});
|
||||
this.sftpLogs = flat;
|
||||
this.sftpSuccessCount = flat.filter((b) => this.isSftpSuccess(b)).length;
|
||||
this.sftpFailedCount = flat.filter((b) => this.isSftpFailed(b)).length;
|
||||
this.sftpPendingCount = this.totalPending;
|
||||
}
|
||||
|
||||
isSftpSuccess(b: CkycBatch): boolean {
|
||||
if (b.uploadStatus) return b.uploadStatus.toLowerCase() === 'success';
|
||||
return !!b.uploadedToCersai;
|
||||
}
|
||||
|
||||
isSftpFailed(b: CkycBatch): boolean {
|
||||
return !!b.uploadStatus && b.uploadStatus.toLowerCase() === 'failed';
|
||||
}
|
||||
|
||||
private dateValue(value: any): number {
|
||||
if (Array.isArray(value) && value.length >= 3) {
|
||||
const [
|
||||
y,
|
||||
mo,
|
||||
d,
|
||||
h = 0,
|
||||
mi = 0,
|
||||
s = 0
|
||||
] = value;
|
||||
return new Date(y, (mo || 1) - 1, d || 1, h, mi, s).getTime();
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const t = Date.parse(value);
|
||||
return isNaN(t) ? 0 : t;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
formatBytes(bytes: number | undefined): string {
|
||||
if (!bytes) return '—';
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
|
||||
}
|
||||
|
||||
trackBatch(_: number, b: CkycBatch): number {
|
||||
return b.id;
|
||||
}
|
||||
|
||||
/** Completion check per manifest entry */
|
||||
isEntryComplete(entry: any): boolean {
|
||||
return (
|
||||
entry?.photo?.packaged && entry?.signature?.packaged && entry?.idProof?.packaged && entry?.addressProof?.packaged
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
+128
-113
@@ -6,120 +6,135 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="container">
|
||||
<mat-card>
|
||||
<div class="layout-row responsive-column gap-20px">
|
||||
<div class="flex-50">
|
||||
<mat-nav-list>
|
||||
<mat-list-item>
|
||||
<div class="menu-list-item-content">
|
||||
<div class="menu-left-section" [routerLink]="['record-master']">
|
||||
<mat-icon matListIcon>
|
||||
<fa-icon icon="database" size="sm"></fa-icon>
|
||||
</mat-icon>
|
||||
<div matLine>
|
||||
{{ 'labels.heading.Record Master' | translate }}
|
||||
@if (arrowBooleans[0]) {
|
||||
<p matLine [routerLink]="['record-master']" class="menu-explanation">
|
||||
{{ 'labels.text.Manage master records for CKYC processing' | translate }}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-right-section">
|
||||
@if (!arrowBooleans[0]) {
|
||||
<fa-icon (click)="arrowBooleansToggle(0)" icon="arrow-down" size="md"></fa-icon>
|
||||
}
|
||||
@if (arrowBooleans[0]) {
|
||||
<fa-icon (click)="arrowBooleansToggle(0)" icon="arrow-up" size="md"></fa-icon>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</mat-list-item>
|
||||
|
||||
<mat-list-item>
|
||||
<div class="menu-list-item-content">
|
||||
<div class="menu-left-section" [routerLink]="['ckyc-verification']">
|
||||
<mat-icon matListIcon>
|
||||
<fa-icon icon="shield-alt" size="sm"></fa-icon>
|
||||
</mat-icon>
|
||||
<div matLine>
|
||||
{{ 'labels.heading.CKYC' | translate }}
|
||||
@if (arrowBooleans[1]) {
|
||||
<p matLine [routerLink]="['ckyc-verification']" class="menu-explanation">
|
||||
{{ 'labels.text.Central KYC verification and document management' | translate }}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-right-section">
|
||||
@if (!arrowBooleans[1]) {
|
||||
<fa-icon (click)="arrowBooleansToggle(1)" icon="arrow-down" size="md"></fa-icon>
|
||||
}
|
||||
@if (arrowBooleans[1]) {
|
||||
<fa-icon (click)="arrowBooleansToggle(1)" icon="arrow-up" size="md"></fa-icon>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</mat-list-item>
|
||||
</mat-nav-list>
|
||||
<div class="ckyc-dash">
|
||||
<!-- Hero -->
|
||||
<div class="ckyc-dash__hero">
|
||||
<div class="ckyc-dash__hero-left">
|
||||
<div class="ckyc-dash__hero-icon">
|
||||
<fa-icon icon="shield-alt"></fa-icon>
|
||||
</div>
|
||||
|
||||
<div class="flex-50">
|
||||
<mat-nav-list>
|
||||
<mat-list-item>
|
||||
<div class="menu-list-item-content">
|
||||
<div class="menu-left-section" [routerLink]="['ckyc-reports']">
|
||||
<mat-icon matListIcon>
|
||||
<fa-icon icon="chart-pie" size="sm"></fa-icon>
|
||||
</mat-icon>
|
||||
<div matLine>
|
||||
{{ 'labels.heading.CKYC Reports' | translate }}
|
||||
@if (arrowBooleans[2]) {
|
||||
<p matLine [routerLink]="['ckyc-reports']" class="menu-explanation">
|
||||
{{ 'labels.text.View and generate CKYC compliance reports' | translate }}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-right-section">
|
||||
@if (!arrowBooleans[2]) {
|
||||
<fa-icon (click)="arrowBooleansToggle(2)" icon="arrow-down" size="md"></fa-icon>
|
||||
}
|
||||
@if (arrowBooleans[2]) {
|
||||
<fa-icon (click)="arrowBooleansToggle(2)" icon="arrow-up" size="md"></fa-icon>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</mat-list-item>
|
||||
|
||||
<mat-list-item>
|
||||
<div class="menu-list-item-content">
|
||||
<div class="menu-left-section" [routerLink]="['ekyc']">
|
||||
<mat-icon matListIcon>
|
||||
<fa-icon icon="fingerprint" size="sm"></fa-icon>
|
||||
</mat-icon>
|
||||
<div matLine>
|
||||
{{ 'labels.heading.E-KYC' | translate }}
|
||||
@if (arrowBooleans[3]) {
|
||||
<p matLine [routerLink]="['ekyc']" class="menu-explanation">
|
||||
{{ 'labels.text.Electronic KYC verification using Aadhaar and biometrics' | translate }}
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="menu-right-section">
|
||||
@if (!arrowBooleans[3]) {
|
||||
<fa-icon (click)="arrowBooleansToggle(3)" icon="arrow-down" size="md"></fa-icon>
|
||||
}
|
||||
@if (arrowBooleans[3]) {
|
||||
<fa-icon (click)="arrowBooleansToggle(3)" icon="arrow-up" size="md"></fa-icon>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</mat-list-item>
|
||||
</mat-nav-list>
|
||||
<div>
|
||||
<h1 class="ckyc-dash__title">CKYC Compliance Centre</h1>
|
||||
<p class="ckyc-dash__sub">
|
||||
Central KYC registry management — collect, upload and verify client artefacts against CERSAI
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</mat-card>
|
||||
<div class="ckyc-dash__hero-badge">
|
||||
<fa-icon icon="check-circle"></fa-icon>
|
||||
<span>CERSAI ready</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 5-Phase pipeline banner -->
|
||||
<div class="ckyc-pipeline">
|
||||
<div class="ckyc-pipeline__head">
|
||||
<span class="ckyc-pipeline__heading"> <fa-icon icon="stream"></fa-icon> 5-phase compliance pipeline </span>
|
||||
<span class="ckyc-pipeline__hint">CBS → CKYC automation flow</span>
|
||||
</div>
|
||||
|
||||
<div class="ckyc-pipeline__track">
|
||||
<div class="ckyc-phase">
|
||||
<div class="ckyc-phase__num">1</div>
|
||||
<div class="ckyc-phase__icon"><fa-icon icon="database"></fa-icon></div>
|
||||
<div class="ckyc-phase__title">Mark Non-CKYC</div>
|
||||
<div class="ckyc-phase__desc">Identify missing CKYC registrations</div>
|
||||
</div>
|
||||
<div class="ckyc-phase__arrow"><fa-icon icon="long-arrow-alt-right"></fa-icon></div>
|
||||
|
||||
<div class="ckyc-phase">
|
||||
<div class="ckyc-phase__num">2</div>
|
||||
<div class="ckyc-phase__icon"><fa-icon icon="file-archive"></fa-icon></div>
|
||||
<div class="ckyc-phase__title">Auto ZIP</div>
|
||||
<div class="ckyc-phase__desc">Bundle into CERSAI format</div>
|
||||
</div>
|
||||
<div class="ckyc-phase__arrow"><fa-icon icon="long-arrow-alt-right"></fa-icon></div>
|
||||
|
||||
<div class="ckyc-phase ckyc-phase--accent ckyc-phase--wide">
|
||||
<div class="ckyc-phase__num ckyc-phase__num--dual">3 · 4</div>
|
||||
<div class="ckyc-phase__icon"><fa-icon icon="cloud-upload-alt"></fa-icon></div>
|
||||
<div class="ckyc-phase__title">SFTP Upload & API Match</div>
|
||||
<div class="ckyc-phase__desc">Transfer · ACK · Resolve CKYC numbers</div>
|
||||
</div>
|
||||
<div class="ckyc-phase__arrow"><fa-icon icon="long-arrow-alt-right"></fa-icon></div>
|
||||
|
||||
<div class="ckyc-phase">
|
||||
<div class="ckyc-phase__num">5</div>
|
||||
<div class="ckyc-phase__icon"><fa-icon icon="check-double"></fa-icon></div>
|
||||
<div class="ckyc-phase__title">Status Update</div>
|
||||
<div class="ckyc-phase__desc">Update master & MIS</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Menu cards -->
|
||||
<h2 class="ckyc-dash__section-title"><fa-icon icon="th-large"></fa-icon> Quick actions</h2>
|
||||
|
||||
<div class="ckyc-dash__grid">
|
||||
<a class="ckyc-card ckyc-card--blue" [routerLink]="['record-master']">
|
||||
<div class="ckyc-card__glow"></div>
|
||||
<div class="ckyc-card__icon">
|
||||
<fa-icon icon="database"></fa-icon>
|
||||
</div>
|
||||
<div class="ckyc-card__body">
|
||||
<div class="ckyc-card__title">Record Master</div>
|
||||
<p class="ckyc-card__desc">
|
||||
Manage master records, identify clients without CKYC and collect missing artefacts.
|
||||
</p>
|
||||
<div class="ckyc-card__tags">
|
||||
<span class="ckyc-tag">Phase 1</span>
|
||||
<span class="ckyc-tag">Clients</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ckyc-card__cta">Open <fa-icon icon="arrow-right"></fa-icon></div>
|
||||
</a>
|
||||
|
||||
<a class="ckyc-card ckyc-card--teal" [routerLink]="['ckyc-verification']">
|
||||
<div class="ckyc-card__glow"></div>
|
||||
<div class="ckyc-card__icon">
|
||||
<fa-icon icon="shield-alt"></fa-icon>
|
||||
</div>
|
||||
<div class="ckyc-card__body">
|
||||
<div class="ckyc-card__title">CKYC Verification</div>
|
||||
<p class="ckyc-card__desc">Batch, upload and match records against the CERSAI registry.</p>
|
||||
<div class="ckyc-card__tags">
|
||||
<span class="ckyc-tag">Phase 2-4</span>
|
||||
<span class="ckyc-tag">Batches</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ckyc-card__cta">Open <fa-icon icon="arrow-right"></fa-icon></div>
|
||||
</a>
|
||||
|
||||
<a class="ckyc-card ckyc-card--amber" [routerLink]="['ckyc-reports']">
|
||||
<div class="ckyc-card__glow"></div>
|
||||
<div class="ckyc-card__icon">
|
||||
<fa-icon icon="chart-pie"></fa-icon>
|
||||
</div>
|
||||
<div class="ckyc-card__body">
|
||||
<div class="ckyc-card__title">CKYC Reports</div>
|
||||
<p class="ckyc-card__desc">Generate compliance MIS, track status counts and export for regulators.</p>
|
||||
<div class="ckyc-card__tags">
|
||||
<span class="ckyc-tag">Phase 5</span>
|
||||
<span class="ckyc-tag">MIS</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ckyc-card__cta">Open <fa-icon icon="arrow-right"></fa-icon></div>
|
||||
</a>
|
||||
|
||||
<a class="ckyc-card ckyc-card--purple" [routerLink]="['ekyc']">
|
||||
<div class="ckyc-card__glow"></div>
|
||||
<div class="ckyc-card__icon">
|
||||
<fa-icon icon="fingerprint"></fa-icon>
|
||||
</div>
|
||||
<div class="ckyc-card__body">
|
||||
<div class="ckyc-card__title">E-KYC</div>
|
||||
<p class="ckyc-card__desc">Electronic KYC verification using Aadhaar OTP and biometric authentication.</p>
|
||||
<div class="ckyc-card__tags">
|
||||
<span class="ckyc-tag">Digital</span>
|
||||
<span class="ckyc-tag">Aadhaar</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ckyc-card__cta">Open <fa-icon icon="arrow-right"></fa-icon></div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,3 +5,511 @@
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
$primary: #1a73e8;
|
||||
$primary-dark: #0c447c;
|
||||
$primary-light: #e8f0fe;
|
||||
$gray-50: #f8f9fa;
|
||||
$gray-100: #f1f3f4;
|
||||
$gray-200: #e8eaed;
|
||||
$gray-500: #9aa0a6;
|
||||
$gray-700: #5f6368;
|
||||
$gray-900: #202124;
|
||||
$white: #fff;
|
||||
$teal: #0f6e56;
|
||||
$teal-light: #e1f5ee;
|
||||
$teal-bg: #f0fbf6;
|
||||
$amber: #ba7517;
|
||||
$amber-light: #faeeda;
|
||||
$amber-bg: #fff8eb;
|
||||
$purple: #6a4cb3;
|
||||
$purple-light: #efeafb;
|
||||
$purple-bg: #f8f5ff;
|
||||
|
||||
.ckyc-dash {
|
||||
padding: 24px 28px 40px;
|
||||
max-width: 1400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
/* ---------- HERO ---------- */
|
||||
.ckyc-dash__hero {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 16px;
|
||||
padding: 24px 28px;
|
||||
background: linear-gradient(135deg, #0c447c 0%, #1a73e8 55%, #4285f4 100%);
|
||||
border-radius: 16px;
|
||||
color: $white;
|
||||
margin-bottom: 22px;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 6px 24px rgb(26 115 232 / 20%);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -40%;
|
||||
right: -10%;
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgb(255 255 255 / 15%), transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
&-icon {
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
border-radius: 14px;
|
||||
background: rgb(255 255 255 / 18%);
|
||||
border: 1px solid rgb(255 255 255 / 30%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
backdrop-filter: blur(10px);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&-badge {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 16px;
|
||||
background: rgb(255 255 255 / 15%);
|
||||
border: 1px solid rgb(255 255 255 / 30%);
|
||||
border-radius: 30px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
backdrop-filter: blur(10px);
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
|
||||
fa-icon {
|
||||
color: #9fe1cb;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ckyc-dash__title {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
.ckyc-dash__sub {
|
||||
margin: 4px 0 0;
|
||||
font-size: 13px;
|
||||
opacity: 0.85;
|
||||
max-width: 560px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
/* ---------- Pipeline banner ---------- */
|
||||
.ckyc-pipeline {
|
||||
background: $white;
|
||||
border: 1px solid $gray-200;
|
||||
border-radius: 14px;
|
||||
padding: 18px 20px;
|
||||
margin-bottom: 22px;
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px dashed $gray-200;
|
||||
}
|
||||
|
||||
&__heading {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: $gray-900;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
fa-icon {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
&__hint {
|
||||
font-size: 11px;
|
||||
color: $gray-500;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__track {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
gap: 4px;
|
||||
overflow-x: auto;
|
||||
padding: 4px 2px;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background: $gray-200;
|
||||
border-radius: 4px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ckyc-phase {
|
||||
flex: 1;
|
||||
min-width: 140px;
|
||||
background: $primary-light;
|
||||
border: 1px solid rgba($primary, 0.15);
|
||||
border-radius: 10px;
|
||||
padding: 12px 12px 14px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
transition: transform 0.15s;
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
&__num {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 10px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border-radius: 50%;
|
||||
background: $primary;
|
||||
color: $white;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&--dual {
|
||||
width: auto;
|
||||
min-width: 20px;
|
||||
height: 20px;
|
||||
padding: 0 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.5px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
border-radius: 10px;
|
||||
background: $primary;
|
||||
color: $white;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 15px;
|
||||
margin: 0 auto 8px;
|
||||
box-shadow: 0 3px 10px rgba($primary, 0.25);
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: $primary-dark;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
font-size: 10px;
|
||||
color: $gray-700;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
&__arrow {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: $gray-200;
|
||||
font-size: 16px;
|
||||
padding: 0 2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&--accent {
|
||||
background: $amber-light;
|
||||
border-color: rgba($amber, 0.25);
|
||||
|
||||
.ckyc-phase__icon {
|
||||
background: $amber;
|
||||
box-shadow: 0 3px 10px rgba($amber, 0.3);
|
||||
}
|
||||
|
||||
.ckyc-phase__num {
|
||||
background: $amber;
|
||||
}
|
||||
|
||||
.ckyc-phase__title {
|
||||
color: #633806;
|
||||
}
|
||||
}
|
||||
|
||||
&--wide {
|
||||
flex: 1.8;
|
||||
min-width: 210px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Section title ---------- */
|
||||
.ckyc-dash__section-title {
|
||||
font-size: 14px;
|
||||
font-weight: 700;
|
||||
color: $gray-900;
|
||||
margin: 10px 0 14px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
|
||||
fa-icon {
|
||||
color: $primary;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* ---------- Menu grid ---------- */
|
||||
.ckyc-dash__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.ckyc-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 22px 22px 18px;
|
||||
background: $white;
|
||||
border: 1px solid $gray-200;
|
||||
border-radius: 16px;
|
||||
text-decoration: none;
|
||||
color: $gray-900;
|
||||
transition:
|
||||
transform 0.2s,
|
||||
box-shadow 0.2s,
|
||||
border-color 0.2s;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 220px;
|
||||
|
||||
&__glow {
|
||||
position: absolute;
|
||||
top: -50px;
|
||||
right: -50px;
|
||||
width: 160px;
|
||||
height: 160px;
|
||||
border-radius: 50%;
|
||||
opacity: 0.12;
|
||||
pointer-events: none;
|
||||
transition:
|
||||
transform 0.4s,
|
||||
opacity 0.3s;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
transform: translateY(-3px);
|
||||
box-shadow: 0 10px 30px rgb(0 0 0 / 8%);
|
||||
|
||||
.ckyc-card__glow {
|
||||
transform: scale(1.25);
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.ckyc-card__cta {
|
||||
background: $primary;
|
||||
color: $white;
|
||||
|
||||
fa-icon {
|
||||
transform: translateX(3px);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 14px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 20px;
|
||||
color: $white;
|
||||
margin-bottom: 16px;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
box-shadow: 0 6px 18px rgb(0 0 0 / 10%);
|
||||
}
|
||||
|
||||
&__body {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: $gray-900;
|
||||
margin-bottom: 6px;
|
||||
letter-spacing: -0.2px;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
font-size: 12px;
|
||||
color: $gray-700;
|
||||
line-height: 1.55;
|
||||
margin: 0 0 12px;
|
||||
}
|
||||
|
||||
&__tags {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__cta {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
align-self: flex-start;
|
||||
padding: 8px 14px;
|
||||
border-radius: 10px;
|
||||
background: $gray-50;
|
||||
color: $gray-700;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
margin-top: 14px;
|
||||
border: 1px solid $gray-200;
|
||||
transition: all 0.2s;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
fa-icon {
|
||||
font-size: 11px;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
}
|
||||
|
||||
/* ------ Colour variants ------ */
|
||||
&--blue {
|
||||
.ckyc-card__icon {
|
||||
background: linear-gradient(135deg, $primary, #4285f4);
|
||||
}
|
||||
|
||||
.ckyc-card__glow {
|
||||
background: radial-gradient(circle, $primary, transparent 60%);
|
||||
}
|
||||
|
||||
&:hover .ckyc-card__cta {
|
||||
background: $primary;
|
||||
border-color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
&--teal {
|
||||
.ckyc-card__icon {
|
||||
background: linear-gradient(135deg, $teal, #1d9e75);
|
||||
}
|
||||
|
||||
.ckyc-card__glow {
|
||||
background: radial-gradient(circle, $teal, transparent 60%);
|
||||
}
|
||||
|
||||
&:hover .ckyc-card__cta {
|
||||
background: $teal;
|
||||
border-color: $teal;
|
||||
}
|
||||
}
|
||||
|
||||
&--amber {
|
||||
.ckyc-card__icon {
|
||||
background: linear-gradient(135deg, $amber, #d4892f);
|
||||
}
|
||||
|
||||
.ckyc-card__glow {
|
||||
background: radial-gradient(circle, $amber, transparent 60%);
|
||||
}
|
||||
|
||||
&:hover .ckyc-card__cta {
|
||||
background: $amber;
|
||||
border-color: $amber;
|
||||
}
|
||||
}
|
||||
|
||||
&--purple {
|
||||
.ckyc-card__icon {
|
||||
background: linear-gradient(135deg, $purple, #8a6fd4);
|
||||
}
|
||||
|
||||
.ckyc-card__glow {
|
||||
background: radial-gradient(circle, $purple, transparent 60%);
|
||||
}
|
||||
|
||||
&:hover .ckyc-card__cta {
|
||||
background: $purple;
|
||||
border-color: $purple;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ckyc-tag {
|
||||
display: inline-block;
|
||||
padding: 3px 9px;
|
||||
background: $gray-100;
|
||||
color: $gray-700;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
border-radius: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
/* ---------- Responsive ---------- */
|
||||
@media (width <= 900px) {
|
||||
.ckyc-dash {
|
||||
padding: 16px 18px 30px;
|
||||
}
|
||||
|
||||
.ckyc-dash__hero {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.ckyc-pipeline__track {
|
||||
flex-flow: row nowrap;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.ckyc-dash__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.ckyc-phase {
|
||||
min-width: 120px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,7 @@
|
||||
*/
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { RouterLink } from '@angular/router';
|
||||
import { MatNavList, MatListItem } from '@angular/material/list';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { MatLine } from '@angular/material/grid-list';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
@Component({
|
||||
@@ -20,17 +16,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
styleUrls: ['./ckyc.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatNavList,
|
||||
MatListItem,
|
||||
MatIcon,
|
||||
FaIconComponent,
|
||||
MatLine
|
||||
FaIconComponent
|
||||
]
|
||||
})
|
||||
export class CkycComponent {
|
||||
arrowBooleans: boolean[] = new Array(4).fill(false);
|
||||
|
||||
arrowBooleansToggle(arrowNumber: number) {
|
||||
this.arrowBooleans[arrowNumber] = !this.arrowBooleans[arrowNumber];
|
||||
}
|
||||
}
|
||||
export class CkycComponent {}
|
||||
|
||||
@@ -6,14 +6,978 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="container">
|
||||
<mat-card>
|
||||
<mat-card-content>
|
||||
<div class="placeholder-content">
|
||||
<fa-icon icon="database" size="3x" class="placeholder-icon"></fa-icon>
|
||||
<h3>{{ 'labels.heading.Record Master' | translate }}</h3>
|
||||
<p>{{ 'labels.text.This section is under development.' | translate }}</p>
|
||||
<div class="rm-container">
|
||||
<!-- Header -->
|
||||
<div class="rm-header">
|
||||
<div class="rm-header__left">
|
||||
<div class="rm-header__icon">
|
||||
<fa-icon icon="database"></fa-icon>
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<div>
|
||||
<h2 class="rm-header__title">{{ 'labels.heading.Record Master' | translate }}</h2>
|
||||
<p class="rm-header__sub">Clients missing CKYC registration or required artefacts</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="rm-header__actions">
|
||||
<button mat-stroked-button class="rm-btn rm-btn--ghost" (click)="fetchRecords()">
|
||||
<fa-icon icon="sync-alt"></fa-icon>
|
||||
<span>Refresh</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 3-tab filter -->
|
||||
<div class="rm-tabs">
|
||||
<button type="button" class="rm-tab" [class.rm-tab--active]="activeTab === 'all'" (click)="setTab('all')">
|
||||
<fa-icon icon="list"></fa-icon>
|
||||
<span>All clients</span>
|
||||
<span class="rm-tab__count">{{ tabCounts.all }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rm-tab rm-tab--amber"
|
||||
[class.rm-tab--active]="activeTab === 'pending-collection'"
|
||||
(click)="setTab('pending-collection')"
|
||||
>
|
||||
<fa-icon icon="hourglass-half"></fa-icon>
|
||||
<span>Pending collection</span>
|
||||
<span class="rm-tab__count">{{ tabCounts.pendingCollection }}</span>
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="rm-tab rm-tab--green"
|
||||
[class.rm-tab--active]="activeTab === 'ready'"
|
||||
(click)="setTab('ready')"
|
||||
>
|
||||
<fa-icon icon="check-double"></fa-icon>
|
||||
<span>Ready for CERSAI</span>
|
||||
<span class="rm-tab__count">{{ tabCounts.ready }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<form class="rm-filters" [formGroup]="filterForm" (ngSubmit)="applyFilters()">
|
||||
<div class="rm-filters__grid">
|
||||
<div class="rm-field">
|
||||
<label class="rm-label"><fa-icon icon="building"></fa-icon> Office</label>
|
||||
<select class="rm-input" formControlName="officeId">
|
||||
<option value="">All offices</option>
|
||||
@for (o of offices; track o.id) {
|
||||
<option [value]="o.id">{{ o.name }}</option>
|
||||
}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="rm-field">
|
||||
<label class="rm-label"><fa-icon icon="cube"></fa-icon> Loan product ID</label>
|
||||
<input type="number" class="rm-input" formControlName="productId" placeholder="Optional" />
|
||||
</div>
|
||||
|
||||
<div class="rm-field">
|
||||
<label class="rm-label"><fa-icon icon="calendar"></fa-icon> From date</label>
|
||||
<input type="date" class="rm-input" formControlName="fromDate" />
|
||||
</div>
|
||||
|
||||
<div class="rm-field">
|
||||
<label class="rm-label"><fa-icon icon="calendar"></fa-icon> To date</label>
|
||||
<input type="date" class="rm-input" formControlName="toDate" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rm-filters__actions">
|
||||
<button type="button" mat-stroked-button class="rm-btn rm-btn--ghost" (click)="resetFilters()">
|
||||
<fa-icon icon="times"></fa-icon> <span>Reset</span>
|
||||
</button>
|
||||
<button type="submit" mat-flat-button color="primary" class="rm-btn rm-btn--primary">
|
||||
<fa-icon icon="search"></fa-icon> <span>Apply filters</span>
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Loading / error / empty -->
|
||||
@if (loading) {
|
||||
<div class="rm-state rm-state--loading">
|
||||
<fa-icon icon="spinner" animation="spin"></fa-icon>
|
||||
<span>Loading CKYC records…</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (error && !loading) {
|
||||
<div class="rm-state rm-state--error">
|
||||
<fa-icon icon="exclamation-triangle"></fa-icon>
|
||||
<span>{{ error }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!loading && !error && rows.length === 0) {
|
||||
<div class="rm-state rm-state--empty">
|
||||
<fa-icon icon="check-circle" size="3x"></fa-icon>
|
||||
<h3>No records found</h3>
|
||||
<p>All clients matching your filters have complete CKYC artefacts. 🎉</p>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Results table -->
|
||||
@if (!loading && rows.length > 0) {
|
||||
<div class="rm-results">
|
||||
<div class="rm-results__head">
|
||||
<span class="rm-results__count">
|
||||
<fa-icon icon="list"></fa-icon>
|
||||
Showing <strong>{{ pageStart }}–{{ pageEnd }}</strong> of
|
||||
<strong>{{ totalFilteredRecords }}</strong>
|
||||
</span>
|
||||
@if (selected.size > 0) {
|
||||
<span class="rm-results__selected">
|
||||
<fa-icon icon="check-square"></fa-icon>
|
||||
<strong>{{ selected.size }}</strong> selected
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="rm-table-wrap">
|
||||
<table class="rm-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="rm-table__check">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="allSelected"
|
||||
[indeterminate]="someSelected"
|
||||
(change)="toggleAll()"
|
||||
aria-label="Select all"
|
||||
/>
|
||||
</th>
|
||||
<th>Client</th>
|
||||
<th>Contact</th>
|
||||
<th>Address</th>
|
||||
<th class="rm-table__artefacts">Artefacts</th>
|
||||
<th class="rm-table__completion">Completion</th>
|
||||
<th class="rm-table__actions">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (row of rows; track row.clientId) {
|
||||
<tr
|
||||
class="rm-row"
|
||||
[class.rm-row--selected]="isSelected(row.clientId)"
|
||||
(click)="toggleRow(row.clientId, $event)"
|
||||
>
|
||||
<td class="rm-table__check">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="isSelected(row.clientId)"
|
||||
(click)="$event.stopPropagation()"
|
||||
(change)="toggleRow(row.clientId)"
|
||||
[attr.aria-label]="'Select ' + row.fullName"
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div class="rm-row__client">
|
||||
<div class="rm-row__avatar">{{ row.fullName?.charAt(0) || '?' }}</div>
|
||||
<div class="rm-row__ident">
|
||||
<div class="rm-row__name">{{ row.fullName }}</div>
|
||||
<div class="rm-row__account"><fa-icon icon="hashtag"></fa-icon> {{ row.accountNo }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="rm-row__line">
|
||||
<fa-icon icon="envelope"></fa-icon>
|
||||
<span [class.rm-row__missing]="!row.email">{{ row.email || 'No email' }}</span>
|
||||
</div>
|
||||
<div class="rm-row__line">
|
||||
<fa-icon icon="phone"></fa-icon>
|
||||
<span [class.rm-row__missing]="!row.mobile">{{ row.mobile || 'No mobile' }}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="rm-row__address">
|
||||
<fa-icon icon="map-marker-alt"></fa-icon>
|
||||
<span [class.rm-row__missing]="!row.address">{{ row.address || 'No address on file' }}</span>
|
||||
</td>
|
||||
<td class="rm-table__artefacts">
|
||||
<div class="rm-row__chips">
|
||||
<span
|
||||
class="rm-chip"
|
||||
[class.rm-chip--ok]="row.hasPhoto"
|
||||
[class.rm-chip--miss]="!row.hasPhoto"
|
||||
title="Photo"
|
||||
>
|
||||
<fa-icon [icon]="row.hasPhoto ? 'check' : 'times'"></fa-icon> P
|
||||
</span>
|
||||
<span
|
||||
class="rm-chip"
|
||||
[class.rm-chip--ok]="row.hasSignature"
|
||||
[class.rm-chip--miss]="!row.hasSignature"
|
||||
title="Signature"
|
||||
>
|
||||
<fa-icon [icon]="row.hasSignature ? 'check' : 'times'"></fa-icon> S
|
||||
</span>
|
||||
<span
|
||||
class="rm-chip"
|
||||
[class.rm-chip--ok]="row.hasIdProof"
|
||||
[class.rm-chip--miss]="!row.hasIdProof"
|
||||
title="ID Proof"
|
||||
>
|
||||
<fa-icon [icon]="row.hasIdProof ? 'check' : 'times'"></fa-icon> ID
|
||||
</span>
|
||||
<span
|
||||
class="rm-chip"
|
||||
[class.rm-chip--ok]="row.hasAddressProof"
|
||||
[class.rm-chip--miss]="!row.hasAddressProof"
|
||||
title="Address Proof"
|
||||
>
|
||||
<fa-icon [icon]="row.hasAddressProof ? 'check' : 'times'"></fa-icon> Ad
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="rm-table__completion">
|
||||
<div
|
||||
class="rm-completion"
|
||||
[class.rm-completion--full]="artefactCompletion(row) === 4"
|
||||
[class.rm-completion--half]="artefactCompletion(row) >= 2 && artefactCompletion(row) < 4"
|
||||
[class.rm-completion--low]="artefactCompletion(row) < 2"
|
||||
>
|
||||
{{ artefactCompletion(row) }}/4
|
||||
</div>
|
||||
</td>
|
||||
<td class="rm-table__actions">
|
||||
<button class="rm-action-btn" (click)="openView(row, $event)" title="View details">
|
||||
<fa-icon icon="eye"></fa-icon>
|
||||
</button>
|
||||
<button
|
||||
class="rm-action-btn rm-action-btn--primary"
|
||||
(click)="collectCkyc(row, $event)"
|
||||
title="Collect CKYC"
|
||||
>
|
||||
<fa-icon icon="cloud-upload-alt"></fa-icon>
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<!-- Pagination -->
|
||||
<div class="rm-pager">
|
||||
<div class="rm-pager__size">
|
||||
<span>Rows per page:</span>
|
||||
<select class="rm-input rm-input--sm" [value]="limit" (change)="changeLimit($any($event.target).value)">
|
||||
<option [value]="10">10</option>
|
||||
<option [value]="25">25</option>
|
||||
<option [value]="50">50</option>
|
||||
<option [value]="100">100</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="rm-pager__nav">
|
||||
<button
|
||||
mat-icon-button
|
||||
class="rm-pager__btn"
|
||||
[disabled]="!canGoPrev"
|
||||
(click)="changePage(-1)"
|
||||
aria-label="Previous page"
|
||||
>
|
||||
<fa-icon icon="chevron-left"></fa-icon>
|
||||
</button>
|
||||
<span class="rm-pager__label">{{ pageStart }}–{{ pageEnd }}</span>
|
||||
<button
|
||||
mat-icon-button
|
||||
class="rm-pager__btn"
|
||||
[disabled]="!canGoNext"
|
||||
(click)="changePage(1)"
|
||||
aria-label="Next page"
|
||||
>
|
||||
<fa-icon icon="chevron-right"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Floating action bar (when rows are selected) -->
|
||||
@if (selected.size > 0) {
|
||||
<div class="rm-action-bar">
|
||||
<div class="rm-action-bar__info">
|
||||
<div class="rm-action-bar__count">
|
||||
<fa-icon icon="check-square"></fa-icon>
|
||||
<strong>{{ selected.size }}</strong> selected
|
||||
</div>
|
||||
@if (selectedIncompleteCount > 0) {
|
||||
<span class="rm-action-bar__warn">
|
||||
<fa-icon icon="exclamation-triangle"></fa-icon>
|
||||
{{ selectedIncompleteCount }} incomplete
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<div class="rm-action-bar__btns">
|
||||
<button class="rm-btn rm-btn--ghost-light" (click)="clearSelection()">
|
||||
<fa-icon icon="times"></fa-icon> <span>Clear</span>
|
||||
</button>
|
||||
<button class="rm-btn rm-btn--amber" (click)="openZipDialog()">
|
||||
<fa-icon icon="file-archive"></fa-icon>
|
||||
<span>Create Auto ZIP ({{ selected.size }})</span>
|
||||
<fa-icon icon="arrow-right"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Client details view dialog -->
|
||||
@if (showViewDialog && viewDetails) {
|
||||
<div class="rm-modal" (click)="closeView()">
|
||||
<div class="rm-modal__card rm-modal__card--wide" (click)="$event.stopPropagation()">
|
||||
<div class="rm-modal__header">
|
||||
<div class="rm-modal__title rm-modal__title--blue">
|
||||
<fa-icon icon="id-card-alt"></fa-icon>
|
||||
Client KYC details
|
||||
</div>
|
||||
<button class="rm-modal__close" (click)="closeView()">
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="rm-modal__body">
|
||||
<!-- Client header -->
|
||||
<div class="rm-view__client">
|
||||
<div class="rm-view__avatar">
|
||||
{{ viewDetails.row.fullName?.charAt(0) || '?' }}
|
||||
</div>
|
||||
<div class="rm-view__client-info">
|
||||
<div class="rm-view__client-name">{{ viewDetails.row.fullName }}</div>
|
||||
<div class="rm-view__client-meta">
|
||||
<span class="rm-view__cif">
|
||||
<fa-icon icon="hashtag"></fa-icon> CIF <strong>{{ viewDetails.row.accountNo }}</strong>
|
||||
</span>
|
||||
<span class="rm-view__sep">•</span>
|
||||
<span>
|
||||
<fa-icon icon="envelope"></fa-icon>
|
||||
{{ viewDetails.row.email || 'No email' }}
|
||||
</span>
|
||||
<span class="rm-view__sep">•</span>
|
||||
<span>
|
||||
<fa-icon icon="phone"></fa-icon>
|
||||
{{ viewDetails.row.mobile || 'No mobile' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (viewLoading) {
|
||||
<div class="rm-view__loading">
|
||||
<fa-icon icon="spinner" animation="spin"></fa-icon>
|
||||
Loading KYC details…
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- KYC snapshot -->
|
||||
<div class="rm-view__section-title"><fa-icon icon="shield-alt"></fa-icon> KYC Snapshot</div>
|
||||
<div class="rm-view__kpis">
|
||||
<div class="rm-kpi">
|
||||
<div class="rm-kpi__label"><fa-icon icon="check-double"></fa-icon> KYC Status</div>
|
||||
@if (viewDetails.kycStatus) {
|
||||
<div
|
||||
class="rm-kpi__value rm-kpi__pill"
|
||||
[class.rm-kpi__pill--ok]="viewDetails.kycStatusCode === 'clientKycStatus.verified'"
|
||||
[class.rm-kpi__pill--warn]="
|
||||
viewDetails.kycStatusCode === 'clientKycStatus.pending' ||
|
||||
viewDetails.kycStatusCode === 'clientKycStatus.initiated'
|
||||
"
|
||||
[class.rm-kpi__pill--err]="
|
||||
viewDetails.kycStatusCode === 'clientKycStatus.rejected' ||
|
||||
viewDetails.kycStatusCode === 'clientKycStatus.expired'
|
||||
"
|
||||
>
|
||||
{{ viewDetails.kycStatus }}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="rm-kpi__value rm-kpi__missing">—</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="rm-kpi">
|
||||
<div class="rm-kpi__label"><fa-icon icon="calendar-check"></fa-icon> KYC Date</div>
|
||||
<div class="rm-kpi__value">
|
||||
{{ viewDetails.kycDate || '—' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rm-kpi">
|
||||
<div class="rm-kpi__label"><fa-icon icon="hourglass-half"></fa-icon> Expiry Date</div>
|
||||
<div class="rm-kpi__value">
|
||||
{{ viewDetails.expiryDate || '—' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rm-kpi">
|
||||
<div class="rm-kpi__label"><fa-icon icon="exclamation-triangle"></fa-icon> Risk Category</div>
|
||||
<div class="rm-kpi__value">
|
||||
{{ viewDetails.riskCategory || '—' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 4 documents -->
|
||||
<div class="rm-view__section-title"><fa-icon icon="folder-open"></fa-icon> CKYC Documents</div>
|
||||
<div class="rm-view__docs">
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
docTile;
|
||||
context: { slot: viewDetails.docs.photo, label: 'Photo / Image of Client', icon: 'user-circle' }
|
||||
"
|
||||
></ng-container>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
docTile;
|
||||
context: { slot: viewDetails.docs.signature, label: 'Signature of Client', icon: 'file-signature' }
|
||||
"
|
||||
></ng-container>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
docTile;
|
||||
context: { slot: viewDetails.docs.idProof, label: 'Identity Proof', icon: 'id-card' }
|
||||
"
|
||||
></ng-container>
|
||||
<ng-container
|
||||
*ngTemplateOutlet="
|
||||
docTile;
|
||||
context: { slot: viewDetails.docs.addressProof, label: 'Address Proof', icon: 'home' }
|
||||
"
|
||||
></ng-container>
|
||||
</div>
|
||||
|
||||
<ng-template #docTile let-slot="slot" let-label="label" let-icon="icon">
|
||||
<div class="rm-doc" [class.rm-doc--ok]="slot.present" [class.rm-doc--miss]="!slot.present">
|
||||
<div class="rm-doc__head">
|
||||
<div class="rm-doc__icon"><fa-icon [icon]="icon"></fa-icon></div>
|
||||
<div class="rm-doc__body">
|
||||
<div class="rm-doc__label">{{ label }}</div>
|
||||
<div class="rm-doc__value">
|
||||
{{ slot.name }}
|
||||
@if (slot.key) {
|
||||
<span class="rm-doc__key">{{ slot.key }}</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<fa-icon class="rm-doc__state" [icon]="slot.present ? 'check-circle' : 'times-circle'"></fa-icon>
|
||||
</div>
|
||||
|
||||
@if (slot.files?.length) {
|
||||
<div class="rm-doc__files">
|
||||
@for (file of slot.files; track file.id) {
|
||||
<div class="rm-file" (click)="openFilePreview(file, $event)" title="Open">
|
||||
@if (file.previewUrl && file.isImage !== false && !file.isPdf) {
|
||||
<img [src]="file.previewUrl" class="rm-file__thumb" [alt]="file.name" />
|
||||
} @else if (file.isPdf) {
|
||||
<div class="rm-file__thumb rm-file__thumb--icon rm-file__thumb--pdf">
|
||||
<fa-icon icon="file-pdf"></fa-icon>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="rm-file__thumb rm-file__thumb--icon">
|
||||
<fa-icon icon="file-alt"></fa-icon>
|
||||
</div>
|
||||
}
|
||||
<div class="rm-file__info">
|
||||
@if (file.fileName) {
|
||||
<span class="rm-file__name">{{ file.fileName }}</span>
|
||||
} @else {
|
||||
<span class="rm-file__name">{{ file.name }}</span>
|
||||
}
|
||||
<fa-icon icon="external-link-alt" class="rm-file__open"></fa-icon>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
} @else if (slot.present) {
|
||||
<div class="rm-doc__placeholder">
|
||||
<fa-icon icon="spinner" animation="spin"></fa-icon>
|
||||
Loading preview…
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
|
||||
<div class="rm-modal__footer">
|
||||
<button class="rm-btn rm-btn--ghost" (click)="closeView()"><fa-icon icon="times"></fa-icon> Close</button>
|
||||
<button class="rm-btn rm-btn--primary" (click)="collectCkycFromView()">
|
||||
<fa-icon icon="cloud-upload-alt"></fa-icon>
|
||||
<span>Collect CKYC</span>
|
||||
<fa-icon icon="arrow-right"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Auto ZIP Pipeline Dialog -->
|
||||
@if (showZipDialog) {
|
||||
<div class="rm-modal">
|
||||
<div class="rm-modal__card">
|
||||
<div class="rm-modal__header">
|
||||
<div class="rm-modal__title">
|
||||
<fa-icon icon="file-archive"></fa-icon>
|
||||
Auto ZIP Creation
|
||||
</div>
|
||||
<button class="rm-modal__close" [disabled]="zipRunning" (click)="closeZipDialog()">
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="rm-modal__body">
|
||||
<p class="rm-modal__intro">
|
||||
Bundling <strong>{{ selected.size }}</strong> client record{{ selected.size === 1 ? '' : 's' }}
|
||||
into CERSAI-format ZIP. Files will be packaged with an SHA-256 checksum and registered for SFTP upload.
|
||||
</p>
|
||||
|
||||
@if (selectedIncompleteCount > 0 && !zipRunning && !zipResult) {
|
||||
<div class="rm-modal__warn">
|
||||
<fa-icon icon="exclamation-triangle"></fa-icon>
|
||||
<div>
|
||||
<strong>{{ selectedIncompleteCount }} record(s) are missing artefacts.</strong>
|
||||
They will still be packaged but flagged as incomplete in the manifest.
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!zipRunning && !zipResult) {
|
||||
<div class="rm-field rm-field--remarks">
|
||||
<label class="rm-label">
|
||||
<fa-icon icon="sticky-note"></fa-icon> Remarks <span class="rm-label__opt">(optional)</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="rm-input rm-textarea"
|
||||
rows="2"
|
||||
placeholder="e.g. Daily CERSAI push — 2026-04-14 15:30"
|
||||
[(ngModel)]="zipRemarks"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
></textarea>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Stepper -->
|
||||
<ol class="rm-steps">
|
||||
@for (phase of zipPhases; track phase.key; let i = $index) {
|
||||
<li
|
||||
class="rm-step"
|
||||
[class.rm-step--active]="phase.status === 'active'"
|
||||
[class.rm-step--done]="phase.status === 'done'"
|
||||
[class.rm-step--failed]="phase.status === 'failed'"
|
||||
>
|
||||
<div class="rm-step__indicator">
|
||||
@if (phase.status === 'done') {
|
||||
<fa-icon icon="check"></fa-icon>
|
||||
} @else if (phase.status === 'active') {
|
||||
<fa-icon icon="spinner" animation="spin"></fa-icon>
|
||||
} @else if (phase.status === 'failed') {
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
} @else {
|
||||
<span>{{ i + 1 }}</span>
|
||||
}
|
||||
</div>
|
||||
<div class="rm-step__content">
|
||||
<div class="rm-step__label"><fa-icon [icon]="phase.icon"></fa-icon> {{ phase.label }}</div>
|
||||
@if (phase.message) {
|
||||
<div class="rm-step__msg">{{ phase.message }}</div>
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ol>
|
||||
|
||||
@if (zipResult && !zipResult.error) {
|
||||
<div class="rm-modal__result">
|
||||
<div class="rm-modal__result-head">
|
||||
<fa-icon icon="check-circle"></fa-icon>
|
||||
<span>Batch created successfully</span>
|
||||
</div>
|
||||
<div class="rm-modal__result-grid">
|
||||
<div>
|
||||
<span class="rm-modal__result-label">Batch ID</span>
|
||||
<span class="rm-modal__result-value rm-modal__result-value--mono">{{ zipResult.batchId }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="rm-modal__result-label">Filename</span>
|
||||
<span class="rm-modal__result-value">{{ zipResult.filename }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="rm-modal__result-label">Records</span>
|
||||
<span class="rm-modal__result-value">{{ zipResult.recordCount }}</span>
|
||||
</div>
|
||||
@if (zipResult.sizeBytes) {
|
||||
<div>
|
||||
<span class="rm-modal__result-label">Size</span>
|
||||
<span class="rm-modal__result-value">{{ (zipResult.sizeBytes / 1024 / 1024).toFixed(2) }} MB</span>
|
||||
</div>
|
||||
}
|
||||
@if (zipResult.createdOn) {
|
||||
<div>
|
||||
<span class="rm-modal__result-label">Created on</span>
|
||||
<span class="rm-modal__result-value">{{ zipResult.createdOn }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (zipResult.documentId) {
|
||||
<div>
|
||||
<span class="rm-modal__result-label">Document ID</span>
|
||||
<span class="rm-modal__result-value">#{{ zipResult.documentId }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="rm-modal__result-grid-span">
|
||||
<span class="rm-modal__result-label">Checksum</span>
|
||||
<span class="rm-modal__result-value rm-modal__result-value--mono">{{ zipResult.checksum }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (zipResult.manifest && zipResult.manifest.length > 0) {
|
||||
<div class="rm-manifest">
|
||||
<button class="rm-manifest__toggle" (click)="toggleManifest()">
|
||||
<fa-icon [icon]="manifestExpanded ? 'chevron-down' : 'chevron-right'"></fa-icon>
|
||||
<span><strong>View manifest</strong> — {{ zipResult.manifest.length }} record(s) packaged</span>
|
||||
<span class="rm-manifest__stats">
|
||||
<span class="rm-manifest__stat rm-manifest__stat--full">
|
||||
<fa-icon icon="check-circle"></fa-icon> {{ manifestStats.full }} complete
|
||||
</span>
|
||||
@if (manifestStats.partial > 0) {
|
||||
<span class="rm-manifest__stat rm-manifest__stat--partial">
|
||||
<fa-icon icon="exclamation-triangle"></fa-icon> {{ manifestStats.partial }} partial
|
||||
</span>
|
||||
}
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@if (manifestExpanded) {
|
||||
<div class="rm-manifest__body">
|
||||
<table class="rm-manifest__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Client</th>
|
||||
<th class="rm-manifest__th-center">P</th>
|
||||
<th class="rm-manifest__th-center">S</th>
|
||||
<th class="rm-manifest__th-center">ID</th>
|
||||
<th class="rm-manifest__th-center">Ad</th>
|
||||
<th class="rm-manifest__th-right">Total size</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (entry of zipResult.manifest; track entry.clientId) {
|
||||
<tr>
|
||||
<td>
|
||||
<div class="rm-manifest__client">
|
||||
<strong>{{ entry.displayName || 'Client ' + entry.clientId }}</strong>
|
||||
@if (entry.cifNumber) {
|
||||
<span class="rm-manifest__cif">
|
||||
<fa-icon icon="hashtag"></fa-icon>{{ entry.cifNumber }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
</td>
|
||||
<td class="rm-manifest__td-center">
|
||||
<span
|
||||
class="rm-manifest__chip"
|
||||
[class.rm-manifest__chip--ok]="entry.photo?.packaged"
|
||||
[class.rm-manifest__chip--miss]="!entry.photo?.packaged"
|
||||
[title]="entry.photo?.zipEntry || 'Missing'"
|
||||
>
|
||||
<fa-icon [icon]="entry.photo?.packaged ? 'check' : 'times'"></fa-icon>
|
||||
</span>
|
||||
</td>
|
||||
<td class="rm-manifest__td-center">
|
||||
<span
|
||||
class="rm-manifest__chip"
|
||||
[class.rm-manifest__chip--ok]="entry.signature?.packaged"
|
||||
[class.rm-manifest__chip--miss]="!entry.signature?.packaged"
|
||||
[title]="entry.signature?.zipEntry || 'Missing'"
|
||||
>
|
||||
<fa-icon [icon]="entry.signature?.packaged ? 'check' : 'times'"></fa-icon>
|
||||
</span>
|
||||
</td>
|
||||
<td class="rm-manifest__td-center">
|
||||
<span
|
||||
class="rm-manifest__chip"
|
||||
[class.rm-manifest__chip--ok]="entry.idProof?.packaged"
|
||||
[class.rm-manifest__chip--miss]="!entry.idProof?.packaged"
|
||||
[title]="
|
||||
(entry.idProof?.type || 'Identity Proof') +
|
||||
(entry.idProof?.zipEntry ? ' — ' + entry.idProof.zipEntry : '')
|
||||
"
|
||||
>
|
||||
<fa-icon [icon]="entry.idProof?.packaged ? 'check' : 'times'"></fa-icon>
|
||||
</span>
|
||||
</td>
|
||||
<td class="rm-manifest__td-center">
|
||||
<span
|
||||
class="rm-manifest__chip"
|
||||
[class.rm-manifest__chip--ok]="entry.addressProof?.packaged"
|
||||
[class.rm-manifest__chip--miss]="!entry.addressProof?.packaged"
|
||||
[title]="
|
||||
(entry.addressProof?.type || 'Address Proof') +
|
||||
(entry.addressProof?.zipEntry ? ' — ' + entry.addressProof.zipEntry : '')
|
||||
"
|
||||
>
|
||||
<fa-icon [icon]="entry.addressProof?.packaged ? 'check' : 'times'"></fa-icon>
|
||||
</span>
|
||||
</td>
|
||||
<td class="rm-manifest__td-right rm-manifest__size">
|
||||
{{
|
||||
formatBytes(
|
||||
(entry.photo?.bytes || 0) +
|
||||
(entry.signature?.bytes || 0) +
|
||||
(entry.idProof?.bytes || 0) +
|
||||
(entry.addressProof?.bytes || 0)
|
||||
)
|
||||
}}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (zipResult?.error) {
|
||||
<div class="rm-modal__error">
|
||||
<fa-icon icon="exclamation-triangle"></fa-icon>
|
||||
<div>
|
||||
<strong>Batch creation failed</strong>
|
||||
<span>{{ zipResult!.error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="rm-modal__footer">
|
||||
<button class="rm-btn rm-btn--ghost" [disabled]="zipRunning" (click)="closeZipDialog()">
|
||||
@if (zipResult && !zipResult.error) {
|
||||
Close
|
||||
} @else {
|
||||
Cancel
|
||||
}
|
||||
</button>
|
||||
@if (!zipResult || zipResult.error) {
|
||||
<button class="rm-btn rm-btn--amber" [disabled]="zipRunning" (click)="startZipCreation()">
|
||||
@if (zipRunning) {
|
||||
<fa-icon icon="spinner" animation="spin"></fa-icon>
|
||||
<span>Creating…</span>
|
||||
} @else {
|
||||
<fa-icon icon="cogs"></fa-icon>
|
||||
<span>{{ zipResult?.error ? 'Retry' : 'Start ZIP creation' }}</span>
|
||||
}
|
||||
</button>
|
||||
} @else {
|
||||
<button class="rm-btn rm-btn--primary" (click)="proceedToUpload()">
|
||||
<fa-icon icon="cloud-upload-alt"></fa-icon>
|
||||
<span>Proceed to SFTP upload</span>
|
||||
<fa-icon icon="arrow-right"></fa-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- SFTP Upload Progress Dialog -->
|
||||
@if (showUploadDialog) {
|
||||
<div class="rm-modal">
|
||||
<div class="rm-modal__card">
|
||||
<div class="rm-modal__header">
|
||||
<div class="rm-modal__title rm-modal__title--teal">
|
||||
<fa-icon icon="cloud-upload-alt"></fa-icon>
|
||||
SFTP Upload to CERSAI
|
||||
</div>
|
||||
<button class="rm-modal__close" [disabled]="uploadRunning" (click)="closeUploadDialog()">
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="rm-modal__body">
|
||||
<p class="rm-modal__intro">
|
||||
Transferring batch to the <strong>CERSAI SFTP</strong> drop server and waiting for acknowledgement. On
|
||||
success, the returned <strong>CKYC numbers</strong> will be saved against each client's master record.
|
||||
</p>
|
||||
|
||||
@if (uploadResult?.status === 'ALREADY_UPLOADED') {
|
||||
<div class="rm-modal__warn rm-modal__warn--blue">
|
||||
<fa-icon icon="info-circle"></fa-icon>
|
||||
<div>
|
||||
<strong>Batch already uploaded.</strong>
|
||||
{{ uploadResult?.remarks || 'No action was taken.' }}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Stepper -->
|
||||
<ol class="rm-steps">
|
||||
@for (phase of uploadPhases; track phase.key; let i = $index) {
|
||||
<li
|
||||
class="rm-step"
|
||||
[class.rm-step--active]="phase.status === 'active'"
|
||||
[class.rm-step--done]="phase.status === 'done'"
|
||||
[class.rm-step--failed]="phase.status === 'failed'"
|
||||
>
|
||||
<div class="rm-step__indicator">
|
||||
@if (phase.status === 'done') {
|
||||
<fa-icon icon="check"></fa-icon>
|
||||
} @else if (phase.status === 'active') {
|
||||
<fa-icon icon="spinner" animation="spin"></fa-icon>
|
||||
} @else if (phase.status === 'failed') {
|
||||
<fa-icon icon="times"></fa-icon>
|
||||
} @else {
|
||||
<span>{{ i + 1 }}</span>
|
||||
}
|
||||
</div>
|
||||
<div class="rm-step__content">
|
||||
<div class="rm-step__label"><fa-icon [icon]="phase.icon"></fa-icon> {{ phase.label }}</div>
|
||||
@if (phase.message) {
|
||||
<div class="rm-step__msg">{{ phase.message }}</div>
|
||||
}
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ol>
|
||||
|
||||
@if (uploadResult && !uploadResult.error) {
|
||||
<div class="rm-modal__result">
|
||||
<div class="rm-modal__result-head">
|
||||
<fa-icon [icon]="uploadResult.status === 'ALREADY_UPLOADED' ? 'info-circle' : 'check-circle'"></fa-icon>
|
||||
<span>
|
||||
@if (uploadResult.status === 'ALREADY_UPLOADED') {
|
||||
Batch already uploaded to CERSAI
|
||||
} @else {
|
||||
Upload successful — ACK received
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<div class="rm-modal__result-grid">
|
||||
<div>
|
||||
<span class="rm-modal__result-label">CERSAI Reference</span>
|
||||
<span class="rm-modal__result-value rm-modal__result-value--mono">{{
|
||||
uploadResult.cersaiReference
|
||||
}}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="rm-modal__result-label">Batch ID</span>
|
||||
<span class="rm-modal__result-value rm-modal__result-value--mono">{{ uploadResult.batchId }}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="rm-modal__result-label">Uploaded on</span>
|
||||
<span class="rm-modal__result-value">{{ uploadResult.uploadedOn }}</span>
|
||||
</div>
|
||||
@if (uploadResult.receivedAt) {
|
||||
<div>
|
||||
<span class="rm-modal__result-label">Received at</span>
|
||||
<span class="rm-modal__result-value">{{ uploadResult.receivedAt }}</span>
|
||||
</div>
|
||||
}
|
||||
<div>
|
||||
<span class="rm-modal__result-label">Records</span>
|
||||
<span class="rm-modal__result-value">
|
||||
{{ uploadResult.processedRecords }} / {{ uploadResult.totalRecords }}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<span class="rm-modal__result-label">Status</span>
|
||||
<span class="rm-modal__result-value">
|
||||
<span
|
||||
class="rm-kpi__pill"
|
||||
[class.rm-kpi__pill--ok]="uploadResult.status === 'SUCCESS'"
|
||||
[class.rm-kpi__pill--warn]="uploadResult.status === 'ALREADY_UPLOADED'"
|
||||
>
|
||||
{{ uploadResult.status }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div class="rm-modal__result-grid-span">
|
||||
<span class="rm-modal__result-label">Checksum sent</span>
|
||||
<span class="rm-modal__result-value rm-modal__result-value--mono">{{
|
||||
uploadResult.checksumSent
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="rm-modal__result-grid-span">
|
||||
<span class="rm-modal__result-label">Checksum verified</span>
|
||||
<span class="rm-modal__result-value rm-modal__result-value--mono">{{
|
||||
uploadResult.checksumVerified
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (uploadResult.perClientCkycNumbers && uploadResult.perClientCkycNumbers.length > 0) {
|
||||
<div class="rm-ckyc-numbers">
|
||||
<div class="rm-ckyc-numbers__title">
|
||||
<fa-icon icon="id-card-alt"></fa-icon>
|
||||
Issued CKYC numbers ({{ uploadResult.perClientCkycNumbers.length }})
|
||||
</div>
|
||||
<div class="rm-ckyc-numbers__table-wrap">
|
||||
<table class="rm-ckyc-numbers__table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Client</th>
|
||||
<th>CIF</th>
|
||||
<th>CKYC Number</th>
|
||||
<th class="rm-ckyc-numbers__status">Status</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@for (c of uploadResult.perClientCkycNumbers; track c.clientId) {
|
||||
<tr>
|
||||
<td>#{{ c.clientId }}</td>
|
||||
<td class="rm-ckyc-numbers__mono">{{ c.cifNumber }}</td>
|
||||
<td class="rm-ckyc-numbers__mono rm-ckyc-numbers__ckyc">{{ c.ckycNumber }}</td>
|
||||
<td class="rm-ckyc-numbers__status">
|
||||
<span class="rm-kpi__pill rm-kpi__pill--ok">
|
||||
<fa-icon icon="check-circle"></fa-icon> {{ c.status }}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
@if (uploadResult?.error) {
|
||||
<div class="rm-modal__error">
|
||||
<fa-icon icon="exclamation-triangle"></fa-icon>
|
||||
<div>
|
||||
<strong>Upload failed</strong>
|
||||
<span>{{ uploadResult!.error }}</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="rm-modal__footer">
|
||||
<button class="rm-btn rm-btn--ghost" [disabled]="uploadRunning" (click)="closeUploadDialog()">
|
||||
@if (uploadResult && !uploadResult.error) {
|
||||
Close
|
||||
} @else {
|
||||
Cancel
|
||||
}
|
||||
</button>
|
||||
@if (!uploadResult || uploadResult.error) {
|
||||
<button class="rm-btn rm-btn--amber" [disabled]="uploadRunning" (click)="startSftpUpload()">
|
||||
@if (uploadRunning) {
|
||||
<fa-icon icon="spinner" animation="spin"></fa-icon>
|
||||
<span>Uploading…</span>
|
||||
} @else {
|
||||
<fa-icon icon="cloud-upload-alt"></fa-icon>
|
||||
<span>{{ uploadResult?.error ? 'Retry upload' : 'Start SFTP upload' }}</span>
|
||||
}
|
||||
</button>
|
||||
} @else {
|
||||
<button class="rm-btn rm-btn--primary" (click)="goToVerification()">
|
||||
<fa-icon icon="shield-alt"></fa-icon>
|
||||
<span>Go to Verification</span>
|
||||
<fa-icon icon="arrow-right"></fa-icon>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,16 +6,930 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, OnInit, inject, DestroyRef } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { Router } from '@angular/router';
|
||||
import { FormBuilder, FormGroup, ReactiveFormsModule, FormsModule } from '@angular/forms';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { ClientsService } from '../../clients/clients.service';
|
||||
import { forkJoin, of } from 'rxjs';
|
||||
import { catchError } from 'rxjs/operators';
|
||||
|
||||
interface CkycRecordRow {
|
||||
clientId: number;
|
||||
accountNo: string;
|
||||
fullName: string;
|
||||
email: string | null;
|
||||
mobile: string | null;
|
||||
address: string | null;
|
||||
status: number;
|
||||
statusCode: string;
|
||||
hasPhoto: boolean;
|
||||
hasSignature: boolean;
|
||||
hasIdProof: boolean;
|
||||
hasAddressProof: boolean;
|
||||
}
|
||||
|
||||
interface ZipPhase {
|
||||
key: string;
|
||||
label: string;
|
||||
icon: string;
|
||||
status: 'pending' | 'active' | 'done' | 'failed';
|
||||
message?: string;
|
||||
}
|
||||
|
||||
interface DocFile {
|
||||
id: number | string;
|
||||
name: string;
|
||||
fileName?: string;
|
||||
type?: string;
|
||||
previewUrl?: string;
|
||||
isImage?: boolean;
|
||||
isPdf?: boolean;
|
||||
parentEntityId?: number | string;
|
||||
source: 'client' | 'identifier' | 'photo';
|
||||
}
|
||||
|
||||
interface DocSlot {
|
||||
present: boolean;
|
||||
name: string;
|
||||
key?: string;
|
||||
files: DocFile[];
|
||||
}
|
||||
|
||||
interface ClientViewDetails {
|
||||
row: CkycRecordRow;
|
||||
kycStatus?: string;
|
||||
kycStatusCode?: string;
|
||||
kycDate?: string;
|
||||
expiryDate?: string;
|
||||
riskCategory?: string;
|
||||
docs: {
|
||||
photo: DocSlot;
|
||||
signature: DocSlot;
|
||||
idProof: DocSlot;
|
||||
addressProof: DocSlot;
|
||||
};
|
||||
}
|
||||
|
||||
type TabKey = 'all' | 'pending-collection' | 'ready';
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-record-master',
|
||||
templateUrl: './record-master.component.html',
|
||||
styleUrls: ['./record-master.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
FaIconComponent
|
||||
FaIconComponent,
|
||||
ReactiveFormsModule,
|
||||
FormsModule
|
||||
]
|
||||
})
|
||||
export class RecordMasterComponent {}
|
||||
export class RecordMasterComponent implements OnInit {
|
||||
private clientsService = inject(ClientsService);
|
||||
private router = inject(Router);
|
||||
private fb = inject(FormBuilder);
|
||||
private destroyRef = inject(DestroyRef);
|
||||
|
||||
filterForm!: FormGroup;
|
||||
offices: any[] = [];
|
||||
rows: CkycRecordRow[] = [];
|
||||
allRows: CkycRecordRow[] = []; // raw response before client-side tab filter
|
||||
totalFilteredRecords = 0;
|
||||
limit = 25;
|
||||
offset = 0;
|
||||
loading = false;
|
||||
error = '';
|
||||
|
||||
/** 3-tab filter: drives which `missing` value is sent & client-side filter on "ready" */
|
||||
activeTab: TabKey = 'all';
|
||||
tabCounts = { all: 0, pendingCollection: 0, ready: 0 };
|
||||
|
||||
/** Selection state — Set of clientIds */
|
||||
selected = new Set<number>();
|
||||
|
||||
/** Cached proof-type id → kind ('id' | 'address') map */
|
||||
private proofKindMap = new Map<string, 'id' | 'address'>();
|
||||
private proofKindLoaded = false;
|
||||
|
||||
/** View details dialog state */
|
||||
showViewDialog = false;
|
||||
viewLoading = false;
|
||||
viewDetails: ClientViewDetails | null = null;
|
||||
|
||||
/** Auto-ZIP pipeline dialog state */
|
||||
showZipDialog = false;
|
||||
zipRunning = false;
|
||||
zipPhases: ZipPhase[] = [
|
||||
{ key: 'validate', label: 'Validate records', icon: 'clipboard-check', status: 'pending' },
|
||||
{ key: 'collect', label: 'Collect artefacts', icon: 'folder-open', status: 'pending' },
|
||||
{ key: 'package', label: 'Package CERSAI ZIP', icon: 'file-archive', status: 'pending' },
|
||||
{ key: 'checksum', label: 'Generate checksum', icon: 'fingerprint', status: 'pending' },
|
||||
{ key: 'register', label: 'Register batch', icon: 'database', status: 'pending' }
|
||||
];
|
||||
zipResult: {
|
||||
id?: number;
|
||||
batchId?: string;
|
||||
filename?: string;
|
||||
checksum?: string;
|
||||
recordCount?: number;
|
||||
sizeBytes?: number;
|
||||
createdOn?: string;
|
||||
documentId?: number;
|
||||
uploadedToCersai?: boolean;
|
||||
manifest?: any[];
|
||||
error?: string;
|
||||
} | null = null;
|
||||
zipRemarks = '';
|
||||
manifestExpanded = false;
|
||||
|
||||
/** SFTP upload dialog state */
|
||||
showUploadDialog = false;
|
||||
uploadRunning = false;
|
||||
uploadPhases: ZipPhase[] = [
|
||||
{ key: 'connect', label: 'Connect to CERSAI SFTP', icon: 'link', status: 'pending' },
|
||||
{ key: 'transfer', label: 'Transfer ZIP file', icon: 'cloud-upload-alt', status: 'pending' },
|
||||
{ key: 'verify', label: 'Verify checksum', icon: 'fingerprint', status: 'pending' },
|
||||
{ key: 'ack', label: 'Await ACK receipt', icon: 'inbox', status: 'pending' },
|
||||
{ key: 'register', label: 'Update CKYC numbers', icon: 'id-card-alt', status: 'pending' }
|
||||
];
|
||||
uploadResult: {
|
||||
status?: string;
|
||||
batchRegistryId?: number;
|
||||
batchId?: string;
|
||||
filename?: string;
|
||||
cersaiReference?: string;
|
||||
receivedAt?: string;
|
||||
uploadedOn?: string;
|
||||
checksumSent?: string;
|
||||
checksumVerified?: string;
|
||||
totalRecords?: number;
|
||||
processedRecords?: number;
|
||||
perClientCkycNumbers?: any[];
|
||||
remarks?: string;
|
||||
error?: string;
|
||||
} | null = null;
|
||||
|
||||
toggleManifest(): void {
|
||||
this.manifestExpanded = !this.manifestExpanded;
|
||||
}
|
||||
|
||||
formatBytes(bytes: number | undefined): string {
|
||||
if (!bytes) return '—';
|
||||
if (bytes < 1024) return `${bytes} B`;
|
||||
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
||||
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
|
||||
}
|
||||
|
||||
get manifestStats() {
|
||||
if (!this.zipResult?.manifest) return { full: 0, partial: 0, total: 0 };
|
||||
const total = this.zipResult.manifest.length;
|
||||
let full = 0;
|
||||
this.zipResult.manifest.forEach((m: any) => {
|
||||
if (m.photo?.packaged && m.signature?.packaged && m.idProof?.packaged && m.addressProof?.packaged) {
|
||||
full++;
|
||||
}
|
||||
});
|
||||
return { full, partial: total - full, total };
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.filterForm = this.fb.group({
|
||||
officeId: [''],
|
||||
productId: [''],
|
||||
fromDate: [''],
|
||||
toDate: ['']
|
||||
});
|
||||
|
||||
this.loadOffices();
|
||||
this.loadProofKinds();
|
||||
this.fetchRecords();
|
||||
}
|
||||
|
||||
/**
|
||||
* Workaround for backend bug where /ckyc/record-master returns
|
||||
* hasIdProof=false even when an active PAN/Aadhaar/etc identifier
|
||||
* exists. We load the proof-type map once and then per-row override
|
||||
* hasIdProof / hasAddressProof using /clients/{id}/identifiers.
|
||||
*
|
||||
* TODO: remove once backend SQL is fixed.
|
||||
*/
|
||||
private loadProofKinds(): void {
|
||||
this.clientsService
|
||||
.getKycProofTypes()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: (res: any) => {
|
||||
(res?.idProofs || []).forEach((p: any) => this.proofKindMap.set(String(p.id), 'id'));
|
||||
(res?.addressProofs || []).forEach((p: any) => this.proofKindMap.set(String(p.id), 'address'));
|
||||
this.proofKindLoaded = true;
|
||||
// If records are already loaded, re-verify them now
|
||||
if (this.rows.length > 0) this.reverifyArtefacts();
|
||||
},
|
||||
error: () => undefined
|
||||
});
|
||||
}
|
||||
|
||||
private reverifyArtefacts(): void {
|
||||
if (!this.proofKindLoaded || this.allRows.length === 0) return;
|
||||
const requests: { [clientId: number]: any } = {};
|
||||
this.allRows.forEach((row) => {
|
||||
requests[row.clientId] = this.clientsService
|
||||
.getClientIdentifiers(String(row.clientId))
|
||||
.pipe(catchError(() => of([])));
|
||||
});
|
||||
forkJoin(requests)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe((map: any) => {
|
||||
this.allRows.forEach((row) => {
|
||||
const list = Array.isArray(map[row.clientId])
|
||||
? map[row.clientId]
|
||||
: (map[row.clientId] as any)?.pageItems || [];
|
||||
let hasId = false;
|
||||
let hasAddr = false;
|
||||
for (const idf of list) {
|
||||
const docTypeId = String(idf?.documentType?.id ?? '');
|
||||
const kind = this.proofKindMap.get(docTypeId);
|
||||
const isActive =
|
||||
idf?.status === 'clientIdentifierStatusType.active' || idf?.status === 'Active' || !idf?.status;
|
||||
if (!isActive || !kind) continue;
|
||||
if (kind === 'id') hasId = true;
|
||||
else if (kind === 'address') hasAddr = true;
|
||||
}
|
||||
// Override backend values only when we find something it missed
|
||||
if (hasId) row.hasIdProof = true;
|
||||
if (hasAddr) row.hasAddressProof = true;
|
||||
});
|
||||
// Re-apply tab filter + counts now that overrides are in place
|
||||
this.applyTabFilter();
|
||||
});
|
||||
}
|
||||
|
||||
private loadOffices(): void {
|
||||
this.clientsService
|
||||
.getOffices()
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: (res: any) => {
|
||||
this.offices = Array.isArray(res) ? res : res?.pageItems || [];
|
||||
},
|
||||
error: () => undefined
|
||||
});
|
||||
}
|
||||
|
||||
fetchRecords(newOffset: number | null = null): void {
|
||||
if (newOffset !== null) this.offset = newOffset;
|
||||
const v = this.filterForm.value;
|
||||
this.loading = true;
|
||||
this.error = '';
|
||||
// Only the "Pending Collection" tab passes missing=any; All and Ready tabs
|
||||
// omit the param so complete clients (like client 89) show up too.
|
||||
const missing = this.activeTab === 'pending-collection' ? 'any' : undefined;
|
||||
|
||||
this.clientsService
|
||||
.getCkycRecordMaster({
|
||||
officeId: v.officeId || undefined,
|
||||
productId: v.productId || undefined,
|
||||
fromDate: this.formatDate(v.fromDate),
|
||||
toDate: this.formatDate(v.toDate),
|
||||
missing,
|
||||
limit: this.limit,
|
||||
offset: this.offset
|
||||
})
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: (res: any) => {
|
||||
this.allRows = res?.pageItems || [];
|
||||
this.totalFilteredRecords = res?.totalFilteredRecords || 0;
|
||||
this.applyTabFilter();
|
||||
this.reverifyArtefacts();
|
||||
this.loading = false;
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.loading = false;
|
||||
this.error = err?.error?.defaultUserMessage || 'Failed to load CKYC records.';
|
||||
this.allRows = [];
|
||||
this.rows = [];
|
||||
this.totalFilteredRecords = 0;
|
||||
this.tabCounts = { all: 0, pendingCollection: 0, ready: 0 };
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Apply the tab's client-side filter and recompute counts */
|
||||
private applyTabFilter(): void {
|
||||
if (this.activeTab === 'ready') {
|
||||
this.rows = this.allRows.filter((r) => r.hasPhoto && r.hasSignature && r.hasIdProof && r.hasAddressProof);
|
||||
} else {
|
||||
this.rows = this.allRows;
|
||||
}
|
||||
this.tabCounts = this.computeCounts(this.allRows);
|
||||
}
|
||||
|
||||
private computeCounts(all: CkycRecordRow[]) {
|
||||
return {
|
||||
all: all.length,
|
||||
pendingCollection: all.filter((r) => !r.hasPhoto || !r.hasSignature || !r.hasIdProof || !r.hasAddressProof)
|
||||
.length,
|
||||
ready: all.filter((r) => r.hasPhoto && r.hasSignature && r.hasIdProof && r.hasAddressProof).length
|
||||
};
|
||||
}
|
||||
|
||||
setTab(tab: TabKey): void {
|
||||
if (this.activeTab === tab) return;
|
||||
this.activeTab = tab;
|
||||
this.offset = 0;
|
||||
this.selected.clear();
|
||||
this.fetchRecords();
|
||||
}
|
||||
|
||||
private formatDate(value: any): string | undefined {
|
||||
if (!value) return undefined;
|
||||
const d = value instanceof Date ? value : new Date(value);
|
||||
if (Number.isNaN(d.getTime())) return undefined;
|
||||
const y = d.getFullYear();
|
||||
const m = String(d.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(d.getDate()).padStart(2, '0');
|
||||
return `${y}-${m}-${day}`;
|
||||
}
|
||||
|
||||
applyFilters(): void {
|
||||
this.offset = 0;
|
||||
this.fetchRecords();
|
||||
}
|
||||
|
||||
resetFilters(): void {
|
||||
this.filterForm.reset({ officeId: '', productId: '', fromDate: '', toDate: '' });
|
||||
this.offset = 0;
|
||||
this.fetchRecords();
|
||||
}
|
||||
|
||||
changePage(direction: -1 | 1): void {
|
||||
const next = this.offset + direction * this.limit;
|
||||
if (next < 0) return;
|
||||
this.offset = next;
|
||||
this.fetchRecords();
|
||||
}
|
||||
|
||||
changeLimit(newLimit: number): void {
|
||||
this.limit = Number(newLimit) || 25;
|
||||
this.offset = 0;
|
||||
this.fetchRecords();
|
||||
}
|
||||
|
||||
openClient(row: CkycRecordRow): void {
|
||||
this.router.navigate([
|
||||
'/clients/',
|
||||
row.clientId,
|
||||
'general'
|
||||
]);
|
||||
}
|
||||
|
||||
openView(row: CkycRecordRow, event?: Event): void {
|
||||
event?.stopPropagation();
|
||||
this.releaseViewBlobUrls();
|
||||
this.showViewDialog = true;
|
||||
this.viewLoading = true;
|
||||
this.viewDetails = {
|
||||
row,
|
||||
docs: {
|
||||
photo: { present: row.hasPhoto, name: row.hasPhoto ? 'Profile photo' : 'Not uploaded', files: [] },
|
||||
signature: {
|
||||
present: row.hasSignature,
|
||||
name: row.hasSignature ? 'Client signature' : 'Not uploaded',
|
||||
files: []
|
||||
},
|
||||
idProof: { present: row.hasIdProof, name: row.hasIdProof ? 'Identifier' : 'Not uploaded', files: [] },
|
||||
addressProof: {
|
||||
present: row.hasAddressProof,
|
||||
name: row.hasAddressProof ? 'Identifier' : 'Not uploaded',
|
||||
files: []
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
forkJoin({
|
||||
kyc: this.clientsService.getClientKyc(String(row.clientId)).pipe(catchError(() => of(null))),
|
||||
identifiers: this.clientsService.getClientIdentifiers(String(row.clientId)).pipe(catchError(() => of([]))),
|
||||
documents: this.clientsService.getClientDocuments(String(row.clientId)).pipe(catchError(() => of([]))),
|
||||
photo: this.clientsService.getClientProfileImage(String(row.clientId)).pipe(catchError(() => of(null)))
|
||||
})
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe(({ kyc, identifiers, documents, photo }) => {
|
||||
if (!this.viewDetails) return;
|
||||
|
||||
// KYC status + dates
|
||||
if (kyc) {
|
||||
const code = kyc.kycStatusCode || '';
|
||||
this.viewDetails.kycStatus = this.formatKycStatus(code);
|
||||
this.viewDetails.kycStatusCode = code;
|
||||
this.viewDetails.kycDate = this.formatJavaDate(kyc.verifiedOn || kyc.initiatedOn);
|
||||
this.viewDetails.expiryDate = this.formatJavaDate(kyc.expiryDate);
|
||||
this.viewDetails.riskCategory = (kyc.riskCategoryCode || '').replace('kycRiskCategory.', '').toUpperCase();
|
||||
}
|
||||
|
||||
// 1. Photo — from /clients/{id}/images (base64 data URL)
|
||||
if (photo && typeof photo === 'string' && photo.length > 0) {
|
||||
this.viewDetails.docs.photo = {
|
||||
present: true,
|
||||
name: 'Profile photo',
|
||||
files: [
|
||||
{
|
||||
id: 'client-photo',
|
||||
name: 'Profile photo',
|
||||
previewUrl: photo,
|
||||
isImage: true,
|
||||
source: 'photo'
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Signature — client document named 'clientSignature'
|
||||
const signatureDoc = (documents as any[])?.find((d: any) => d?.name === 'clientSignature');
|
||||
if (signatureDoc) {
|
||||
this.viewDetails.docs.signature = {
|
||||
present: true,
|
||||
name: signatureDoc.fileName || 'Client signature',
|
||||
files: [
|
||||
{
|
||||
id: signatureDoc.id,
|
||||
name: signatureDoc.name,
|
||||
fileName: signatureDoc.fileName,
|
||||
type: signatureDoc.type,
|
||||
parentEntityId: row.clientId,
|
||||
source: 'client',
|
||||
isImage: /image/i.test(signatureDoc.type || ''),
|
||||
isPdf: /pdf/i.test(signatureDoc.type || '')
|
||||
}
|
||||
]
|
||||
};
|
||||
// Fetch blob for thumbnail
|
||||
this.clientsService
|
||||
.getClientSignatureImage(String(row.clientId), String(signatureDoc.id))
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: (blob: any) => {
|
||||
if (blob && this.viewDetails?.docs.signature.files[0]) {
|
||||
this.viewDetails.docs.signature.files[0].previewUrl = URL.createObjectURL(blob);
|
||||
}
|
||||
},
|
||||
error: () => undefined
|
||||
});
|
||||
}
|
||||
|
||||
// 3 & 4. Identifier ID/Address proof — classify and fetch attached documents
|
||||
const idList = Array.isArray(identifiers) ? identifiers : (identifiers as any)?.pageItems || [];
|
||||
let updatedHasId = false;
|
||||
let updatedHasAddr = false;
|
||||
for (const idf of idList) {
|
||||
const docTypeId = String(idf?.documentType?.id ?? '');
|
||||
const kind = this.proofKindMap.get(docTypeId);
|
||||
const isActive =
|
||||
idf?.status === 'clientIdentifierStatusType.active' || idf?.status === 'Active' || !idf?.status;
|
||||
if (!isActive || !kind) continue;
|
||||
const proofName = idf?.documentType?.name || 'Identifier';
|
||||
const rawKey = idf?.documentKey || '';
|
||||
const synthetic = rawKey.startsWith(`${proofName} - `);
|
||||
const slot: DocSlot = {
|
||||
present: true,
|
||||
name: proofName,
|
||||
key: synthetic ? '' : rawKey,
|
||||
files: []
|
||||
};
|
||||
|
||||
if (kind === 'id' && !updatedHasId) {
|
||||
this.viewDetails.docs.idProof = slot;
|
||||
updatedHasId = true;
|
||||
this.loadIdentifierFiles(idf.id, 'idProof');
|
||||
} else if (kind === 'address' && !updatedHasAddr) {
|
||||
this.viewDetails.docs.addressProof = slot;
|
||||
updatedHasAddr = true;
|
||||
this.loadIdentifierFiles(idf.id, 'addressProof');
|
||||
}
|
||||
}
|
||||
|
||||
this.viewLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
/** Fetch documents attached to a client_identifier and load previews */
|
||||
private loadIdentifierFiles(identifierId: number | string, slot: 'idProof' | 'addressProof'): void {
|
||||
this.clientsService
|
||||
.getClientIdentificationDocuments(String(identifierId))
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: (docs: any) => {
|
||||
const list = Array.isArray(docs) ? docs : (docs as any)?.pageItems || [];
|
||||
if (!this.viewDetails) return;
|
||||
const files: DocFile[] = list.map((d: any) => ({
|
||||
id: d.id,
|
||||
name: d.name,
|
||||
fileName: d.fileName,
|
||||
type: d.type,
|
||||
parentEntityId: identifierId,
|
||||
source: 'identifier' as const,
|
||||
isImage: /image/i.test(d.type || ''),
|
||||
isPdf: /pdf/i.test(d.type || '')
|
||||
}));
|
||||
this.viewDetails.docs[slot].files = files;
|
||||
// Fetch blob previews
|
||||
files.forEach((f) => {
|
||||
this.clientsService
|
||||
.downloadClientIdentificationDocument(String(identifierId), String(f.id))
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: (blob: any) => {
|
||||
if (blob) f.previewUrl = URL.createObjectURL(blob);
|
||||
},
|
||||
error: () => undefined
|
||||
});
|
||||
});
|
||||
},
|
||||
error: () => undefined
|
||||
});
|
||||
}
|
||||
|
||||
private releaseViewBlobUrls(): void {
|
||||
if (!this.viewDetails) return;
|
||||
const all = [
|
||||
...this.viewDetails.docs.photo.files,
|
||||
...this.viewDetails.docs.signature.files,
|
||||
...this.viewDetails.docs.idProof.files,
|
||||
...this.viewDetails.docs.addressProof.files
|
||||
];
|
||||
all.forEach((f) => {
|
||||
if (f.previewUrl?.startsWith('blob:')) URL.revokeObjectURL(f.previewUrl);
|
||||
});
|
||||
}
|
||||
|
||||
openFilePreview(file: DocFile, event: Event): void {
|
||||
event.stopPropagation();
|
||||
if (!file.previewUrl) return;
|
||||
window.open(file.previewUrl, '_blank', 'noopener,noreferrer');
|
||||
}
|
||||
|
||||
closeView(): void {
|
||||
this.releaseViewBlobUrls();
|
||||
this.showViewDialog = false;
|
||||
this.viewDetails = null;
|
||||
}
|
||||
|
||||
private formatJavaDate(value: any): string {
|
||||
if (!value) return '';
|
||||
if (Array.isArray(value) && value.length >= 3) {
|
||||
const [
|
||||
y,
|
||||
m,
|
||||
d
|
||||
] = value;
|
||||
return `${String(d).padStart(2, '0')}-${String(m).padStart(2, '0')}-${y}`;
|
||||
}
|
||||
if (typeof value === 'string') return value;
|
||||
return '';
|
||||
}
|
||||
|
||||
private formatKycStatus(code: string): string {
|
||||
if (!code) return 'Unknown';
|
||||
const map: Record<string, string> = {
|
||||
'clientKycStatus.verified': 'Verified',
|
||||
'clientKycStatus.initiated': 'Initiated',
|
||||
'clientKycStatus.pending': 'Pending',
|
||||
'clientKycStatus.rejected': 'Rejected',
|
||||
'clientKycStatus.expired': 'Expired'
|
||||
};
|
||||
return map[code] || code.replace('clientKycStatus.', '');
|
||||
}
|
||||
|
||||
collectCkycFromView(): void {
|
||||
if (!this.viewDetails) return;
|
||||
const row = this.viewDetails.row;
|
||||
this.closeView();
|
||||
this.collectCkyc(row, new Event('click'));
|
||||
}
|
||||
|
||||
collectCkyc(row: CkycRecordRow, event: Event): void {
|
||||
event.stopPropagation();
|
||||
this.router.navigate([
|
||||
'/clients/',
|
||||
row.clientId,
|
||||
'ckyc'
|
||||
]);
|
||||
}
|
||||
|
||||
createClient(): void {
|
||||
this.router.navigate(['/clients/create']);
|
||||
}
|
||||
|
||||
artefactCompletion(row: CkycRecordRow): number {
|
||||
const items = [
|
||||
row.hasPhoto,
|
||||
row.hasSignature,
|
||||
row.hasIdProof,
|
||||
row.hasAddressProof
|
||||
];
|
||||
return items.filter(Boolean).length;
|
||||
}
|
||||
|
||||
/** Selection helpers */
|
||||
toggleRow(clientId: number, event?: Event): void {
|
||||
event?.stopPropagation();
|
||||
if (this.selected.has(clientId)) this.selected.delete(clientId);
|
||||
else this.selected.add(clientId);
|
||||
}
|
||||
|
||||
isSelected(clientId: number): boolean {
|
||||
return this.selected.has(clientId);
|
||||
}
|
||||
|
||||
toggleAll(): void {
|
||||
if (this.allSelected) {
|
||||
this.rows.forEach((r) => this.selected.delete(r.clientId));
|
||||
} else {
|
||||
this.rows.forEach((r) => this.selected.add(r.clientId));
|
||||
}
|
||||
}
|
||||
|
||||
get allSelected(): boolean {
|
||||
return this.rows.length > 0 && this.rows.every((r) => this.selected.has(r.clientId));
|
||||
}
|
||||
|
||||
get someSelected(): boolean {
|
||||
return this.selected.size > 0 && !this.allSelected;
|
||||
}
|
||||
|
||||
clearSelection(): void {
|
||||
this.selected.clear();
|
||||
}
|
||||
|
||||
get selectedRows(): CkycRecordRow[] {
|
||||
return this.rows.filter((r) => this.selected.has(r.clientId));
|
||||
}
|
||||
|
||||
get selectedCompleteCount(): number {
|
||||
return this.selectedRows.filter((r) => this.artefactCompletion(r) === 4).length;
|
||||
}
|
||||
|
||||
get selectedIncompleteCount(): number {
|
||||
return this.selectedRows.length - this.selectedCompleteCount;
|
||||
}
|
||||
|
||||
/** Auto-ZIP pipeline */
|
||||
openZipDialog(): void {
|
||||
if (this.selected.size === 0) return;
|
||||
this.showZipDialog = true;
|
||||
this.zipRunning = false;
|
||||
this.zipResult = null;
|
||||
this.manifestExpanded = false;
|
||||
this.zipPhases = this.zipPhases.map((p) => ({
|
||||
...p,
|
||||
status: 'pending' as const,
|
||||
message: undefined as string | undefined
|
||||
}));
|
||||
}
|
||||
|
||||
closeZipDialog(): void {
|
||||
if (this.zipRunning) return;
|
||||
this.showZipDialog = false;
|
||||
}
|
||||
|
||||
startZipCreation(): void {
|
||||
if (this.zipRunning || this.selected.size === 0) return;
|
||||
this.zipRunning = true;
|
||||
this.zipResult = null;
|
||||
// Reset phases
|
||||
this.zipPhases = this.zipPhases.map((p) => ({
|
||||
...p,
|
||||
status: 'pending' as const,
|
||||
message: undefined as string | undefined
|
||||
}));
|
||||
|
||||
// Kick off visual phase animation for steps 1-4 while the POST is in flight.
|
||||
// Step 5 (Register batch) is held until the API responds.
|
||||
this.animatePhases();
|
||||
|
||||
// Fire the real backend call
|
||||
const clientIds = Array.from(this.selected);
|
||||
const remarks = this.zipRemarks?.trim()
|
||||
? this.zipRemarks.trim()
|
||||
: `CERSAI push — ${new Date().toISOString().slice(0, 19).replace('T', ' ')}`;
|
||||
|
||||
this.clientsService
|
||||
.createCkycBatch({ clientIds, remarks })
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: (res: any) => {
|
||||
// Complete all remaining phases and populate result
|
||||
this.zipPhases = this.zipPhases.map((p) => ({ ...p, status: 'done' as const }));
|
||||
this.zipResult = {
|
||||
id: res?.id,
|
||||
batchId: res?.batchId,
|
||||
filename: res?.filename,
|
||||
checksum: res?.checksum,
|
||||
recordCount: res?.recordCount,
|
||||
sizeBytes: res?.sizeBytes,
|
||||
createdOn: res?.createdOn,
|
||||
documentId: res?.documentId,
|
||||
uploadedToCersai: res?.uploadedToCersai,
|
||||
manifest: res?.manifest || []
|
||||
};
|
||||
this.zipRunning = false;
|
||||
},
|
||||
error: (err: any) => {
|
||||
// Mark current active phase (or last pending) as failed
|
||||
const activeIdx = this.zipPhases.findIndex((p) => p.status === 'active');
|
||||
const idx = activeIdx >= 0 ? activeIdx : this.zipPhases.findIndex((p) => p.status === 'pending');
|
||||
if (idx >= 0) {
|
||||
this.zipPhases[idx].status = 'failed';
|
||||
this.zipPhases[idx].message =
|
||||
err?.error?.defaultUserMessage || err?.error?.developerMessage || err?.message || 'Request failed';
|
||||
}
|
||||
this.zipResult = {
|
||||
error: err?.error?.defaultUserMessage || err?.message || 'Failed to create CKYC batch.'
|
||||
};
|
||||
this.zipRunning = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Run phases 1-4 on a short timer as a visual progress indicator
|
||||
* while the single POST is in flight. Phase 5 is held — it only
|
||||
* completes when the API response arrives (success or failure).
|
||||
*/
|
||||
private animatePhases(): void {
|
||||
const step = 500; // ms per phase
|
||||
for (let i = 0; i < this.zipPhases.length - 1; i++) {
|
||||
setTimeout(() => {
|
||||
// Don't override a phase that's already done/failed by the API response
|
||||
if (this.zipPhases[i].status === 'pending') this.zipPhases[i].status = 'active';
|
||||
}, i * step);
|
||||
setTimeout(
|
||||
() => {
|
||||
if (this.zipPhases[i].status === 'active') this.zipPhases[i].status = 'done';
|
||||
},
|
||||
(i + 1) * step
|
||||
);
|
||||
}
|
||||
// Activate phase 5 after phases 1-4 finish, leave it active until API returns
|
||||
setTimeout(
|
||||
() => {
|
||||
const last = this.zipPhases.length - 1;
|
||||
if (this.zipPhases[last].status === 'pending') this.zipPhases[last].status = 'active';
|
||||
},
|
||||
(this.zipPhases.length - 1) * step
|
||||
);
|
||||
}
|
||||
|
||||
proceedToUpload(): void {
|
||||
if (!this.zipResult?.id) return;
|
||||
this.showZipDialog = false;
|
||||
this.openUploadDialog(this.zipResult.id);
|
||||
}
|
||||
|
||||
/** Open the SFTP upload progress dialog for a given batch registry id */
|
||||
openUploadDialog(batchRegistryId: number): void {
|
||||
this.showUploadDialog = true;
|
||||
this.uploadRunning = false;
|
||||
this.uploadResult = null;
|
||||
this.uploadPhases = this.uploadPhases.map((p) => ({
|
||||
...p,
|
||||
status: 'pending' as const,
|
||||
message: undefined as string | undefined
|
||||
}));
|
||||
// Stash the id for retry
|
||||
this.pendingUploadBatchId = batchRegistryId;
|
||||
}
|
||||
|
||||
private pendingUploadBatchId: number | null = null;
|
||||
|
||||
closeUploadDialog(): void {
|
||||
if (this.uploadRunning) return;
|
||||
this.showUploadDialog = false;
|
||||
this.pendingUploadBatchId = null;
|
||||
}
|
||||
|
||||
startSftpUpload(): void {
|
||||
if (this.uploadRunning || !this.pendingUploadBatchId) return;
|
||||
this.uploadRunning = true;
|
||||
this.uploadResult = null;
|
||||
this.uploadPhases = this.uploadPhases.map((p) => ({
|
||||
...p,
|
||||
status: 'pending' as const,
|
||||
message: undefined as string | undefined
|
||||
}));
|
||||
|
||||
// Animate phases 1-4 while POST is in flight; phase 5 held until response.
|
||||
this.animateUploadPhases();
|
||||
|
||||
this.clientsService
|
||||
.uploadCkycBatchToSftp(this.pendingUploadBatchId)
|
||||
.pipe(takeUntilDestroyed(this.destroyRef))
|
||||
.subscribe({
|
||||
next: (res: any) => {
|
||||
this.uploadPhases = this.uploadPhases.map((p) => ({ ...p, status: 'done' as const }));
|
||||
this.uploadResult = {
|
||||
status: res?.status,
|
||||
batchRegistryId: res?.batchRegistryId,
|
||||
batchId: res?.batchId,
|
||||
filename: res?.filename,
|
||||
cersaiReference: res?.cersaiReference,
|
||||
receivedAt: this.formatJavaDateTime(res?.receivedAt),
|
||||
uploadedOn: this.formatJavaDateTime(res?.uploadedOn),
|
||||
checksumSent: res?.checksumSent,
|
||||
checksumVerified: res?.checksumVerified,
|
||||
totalRecords: res?.totalRecords,
|
||||
processedRecords: res?.processedRecords,
|
||||
perClientCkycNumbers: res?.perClientCkycNumbers || [],
|
||||
remarks: res?.remarks
|
||||
};
|
||||
this.uploadRunning = false;
|
||||
},
|
||||
error: (err: any) => {
|
||||
const activeIdx = this.uploadPhases.findIndex((p) => p.status === 'active');
|
||||
const idx = activeIdx >= 0 ? activeIdx : this.uploadPhases.findIndex((p) => p.status === 'pending');
|
||||
if (idx >= 0) {
|
||||
this.uploadPhases[idx].status = 'failed';
|
||||
this.uploadPhases[idx].message =
|
||||
err?.error?.defaultUserMessage || err?.error?.developerMessage || err?.message || 'Upload failed';
|
||||
}
|
||||
this.uploadResult = {
|
||||
error: err?.error?.defaultUserMessage || err?.message || 'SFTP upload failed.'
|
||||
};
|
||||
this.uploadRunning = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private formatJavaDateTime(value: any): string {
|
||||
if (value == null || value === '') return '';
|
||||
const pad = (n: any) => String(n ?? 0).padStart(2, '0');
|
||||
if (Array.isArray(value) && value.length >= 3) {
|
||||
const [
|
||||
y,
|
||||
mo,
|
||||
d,
|
||||
h = 0,
|
||||
mi = 0,
|
||||
s = 0
|
||||
] = value;
|
||||
const date = `${pad(d)}-${pad(mo)}-${y}`;
|
||||
if (value.length >= 4) {
|
||||
return `${date} ${pad(h)}:${pad(mi)}:${pad(s)}`;
|
||||
}
|
||||
return date;
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
const dt = new Date(value);
|
||||
if (!isNaN(dt.getTime())) {
|
||||
return `${pad(dt.getDate())}-${pad(dt.getMonth() + 1)}-${dt.getFullYear()} ${pad(dt.getHours())}:${pad(dt.getMinutes())}:${pad(dt.getSeconds())}`;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private animateUploadPhases(): void {
|
||||
const step = 550;
|
||||
for (let i = 0; i < this.uploadPhases.length - 1; i++) {
|
||||
setTimeout(() => {
|
||||
if (this.uploadPhases[i].status === 'pending') this.uploadPhases[i].status = 'active';
|
||||
}, i * step);
|
||||
setTimeout(
|
||||
() => {
|
||||
if (this.uploadPhases[i].status === 'active') this.uploadPhases[i].status = 'done';
|
||||
},
|
||||
(i + 1) * step
|
||||
);
|
||||
}
|
||||
setTimeout(
|
||||
() => {
|
||||
const last = this.uploadPhases.length - 1;
|
||||
if (this.uploadPhases[last].status === 'pending') this.uploadPhases[last].status = 'active';
|
||||
},
|
||||
(this.uploadPhases.length - 1) * step
|
||||
);
|
||||
}
|
||||
|
||||
goToVerification(): void {
|
||||
this.closeUploadDialog();
|
||||
this.router.navigate(['/ckyc/ckyc-verification']);
|
||||
}
|
||||
|
||||
get pageStart(): number {
|
||||
return this.totalFilteredRecords === 0 ? 0 : this.offset + 1;
|
||||
}
|
||||
|
||||
get pageEnd(): number {
|
||||
return this.offset + this.rows.length;
|
||||
}
|
||||
|
||||
get canGoPrev(): boolean {
|
||||
return this.offset > 0;
|
||||
}
|
||||
|
||||
get canGoNext(): boolean {
|
||||
return this.rows.length === this.limit;
|
||||
}
|
||||
}
|
||||
|
||||
+3
-1
@@ -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')) {
|
||||
|
||||
+6
-6
@@ -41,11 +41,11 @@
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 24px 16px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
color: rgb(0 0 0 / 45%);
|
||||
font-size: 14px;
|
||||
background: rgba(0, 0, 0, 0.02);
|
||||
background: rgb(0 0 0 / 2%);
|
||||
border-radius: 6px;
|
||||
border: 1px dashed rgba(0, 0, 0, 0.12);
|
||||
border: 1px dashed rgb(0 0 0 / 12%);
|
||||
|
||||
.empty-icon {
|
||||
font-size: 18px;
|
||||
@@ -58,7 +58,7 @@
|
||||
.panel-icon {
|
||||
margin-right: 8px;
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
color: rgb(0 0 0 / 45%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@
|
||||
|
||||
.detail-label {
|
||||
font-size: 12px;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
color: rgb(0 0 0 / 50%);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@@ -94,7 +94,7 @@
|
||||
color: #333;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (width <= 768px) {
|
||||
.section-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
+528
-204
File diff suppressed because it is too large
Load Diff
+147
-29
@@ -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: rgba(255,255,255,0.5); 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 {
|
||||
@@ -133,13 +208,20 @@
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
mat-checkbox {
|
||||
padding: 8px 12px;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -160,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;
|
||||
@@ -172,7 +265,10 @@
|
||||
font-size: 12px;
|
||||
color: #0c4a6e;
|
||||
line-height: 1.6;
|
||||
strong { font-weight: 600; }
|
||||
|
||||
strong {
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.ec-card-actions {
|
||||
@@ -181,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 {
|
||||
@@ -197,10 +298,27 @@
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) { .ec-grid-3 { grid-template-columns: repeat(2, 1fr); } }
|
||||
@media (max-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; }
|
||||
@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;
|
||||
}
|
||||
}
|
||||
|
||||
+100
-20
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+112
-28
@@ -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>
|
||||
}
|
||||
|
||||
+131
-27
@@ -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;
|
||||
@@ -55,7 +77,7 @@
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.12);
|
||||
box-shadow: 0 8px 24px rgb(0 0 0 / 12%);
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
@@ -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,14 @@
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&__left { flex-shrink: 0; }
|
||||
&__icon { color: #1074b9; }
|
||||
&__left {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
color: #1074b9;
|
||||
}
|
||||
|
||||
&__photo {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
@@ -120,9 +169,25 @@
|
||||
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;
|
||||
flex-direction: column;
|
||||
@@ -139,6 +204,7 @@
|
||||
background: #fff;
|
||||
padding: 2px 4px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 9px;
|
||||
color: #94a3b8;
|
||||
@@ -146,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 {
|
||||
@@ -201,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 {
|
||||
@@ -225,6 +306,7 @@
|
||||
background: #dcfce7;
|
||||
color: #16a34a;
|
||||
}
|
||||
|
||||
&.not-verified {
|
||||
background: #fef3c7;
|
||||
color: #d97706;
|
||||
@@ -262,16 +344,22 @@
|
||||
|
||||
.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);
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
.fm-file-upload {
|
||||
@@ -290,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 {
|
||||
@@ -308,7 +400,11 @@
|
||||
border-radius: 6px;
|
||||
width: 100%;
|
||||
|
||||
&__icon { color: #1074b9; font-size: 20px; }
|
||||
&__icon {
|
||||
color: #1074b9;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
&__name {
|
||||
flex: 1;
|
||||
font-size: 13px;
|
||||
@@ -318,13 +414,21 @@
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__remove {
|
||||
color: #94a3b8 !important;
|
||||
width: 28px !important;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+88
-33
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
+36
-7
@@ -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>
|
||||
|
||||
+402
-84
@@ -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 rgba(0,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 {
|
||||
@@ -42,7 +60,7 @@
|
||||
|
||||
&__sub {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
color: rgb(255 255 255 / 50%);
|
||||
margin: 2px 0 0;
|
||||
}
|
||||
}
|
||||
@@ -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,11 +301,16 @@
|
||||
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 (max-width: 768px) {
|
||||
@media (width <= 768px) {
|
||||
.fm-step__header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
@@ -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,23 +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;
|
||||
@@ -391,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;
|
||||
@@ -403,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 */
|
||||
@@ -420,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 {
|
||||
@@ -443,33 +596,47 @@
|
||||
}
|
||||
|
||||
/* 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;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 10%);
|
||||
max-height: 240px;
|
||||
overflow-y: auto;
|
||||
margin-top: -12px;
|
||||
}
|
||||
|
||||
.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 {
|
||||
@@ -478,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 {
|
||||
@@ -505,30 +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 {
|
||||
@@ -539,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 {
|
||||
@@ -553,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 {
|
||||
@@ -570,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 {
|
||||
@@ -581,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 {
|
||||
@@ -596,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 (max-width: 768px) {
|
||||
.nom-form-fields { grid-template-columns: 1fr; }
|
||||
.nom-guardian-card { flex-wrap: wrap; }
|
||||
.nom-guardian-rel { width: 100%; }
|
||||
@media (width <= 768px) {
|
||||
.nom-form-fields {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.nom-guardian-card {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.nom-guardian-rel {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
+74
-25
@@ -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: () => {} });
|
||||
}
|
||||
|
||||
|
||||
+348
-162
@@ -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 & 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 & 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 & 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 & 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 }} · A/C: {{ result.accountNumber }} · {{ result.officeName }}@if (result.dateOfBirth) { · DOB: {{ result.dateOfBirth | dateFormat }} }@if (result.fatherSpouseName) { · {{ result.fatherSpouseName }} }
|
||||
ID: {{ result.id }} · A/C: {{ result.accountNumber }} ·
|
||||
{{ result.officeName }}
|
||||
@if (result.dateOfBirth) {
|
||||
· DOB: {{ result.dateOfBirth | dateFormat }}
|
||||
}
|
||||
@if (result.fatherSpouseName) {
|
||||
· {{ result.fatherSpouseName }}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<span class="guardian-result-item__status"
|
||||
[class.active]="result.status?.value === 'Active'">
|
||||
<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 }} · A/C: {{ result.accountNumber }} · {{ result.officeName }}@if (result.dateOfBirth) { · DOB: {{ result.dateOfBirth | dateFormat }} }@if (result.fatherSpouseName) { · {{ result.fatherSpouseName }} }</span>
|
||||
<span class="guardian-result-item__meta"
|
||||
>ID: {{ result.id }} · A/C: {{ result.accountNumber }} ·
|
||||
{{ result.officeName }}
|
||||
@if (result.dateOfBirth) {
|
||||
· DOB: {{ result.dateOfBirth | dateFormat }}
|
||||
}
|
||||
@if (result.fatherSpouseName) {
|
||||
· {{ result.fatherSpouseName }}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
<span class="guardian-result-item__status" [class.active]="result.status?.value === 'Active'">{{ result.status?.value }}</span>
|
||||
<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@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 & 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 & 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">₹</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">₹</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">₹</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">₹</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) -->
|
||||
|
||||
+252
-61
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
+639
-163
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 }} · 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) {
|
||||
·
|
||||
}
|
||||
@if (company.designation) {
|
||||
{{ company.designation }}
|
||||
}
|
||||
@if ((company.cin || company.designation) && company.appointmentDate) {
|
||||
·
|
||||
}
|
||||
@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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+24
-5
@@ -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) {
|
||||
|
||||
+56
-24
@@ -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>
|
||||
}
|
||||
|
||||
+25
-7
@@ -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
|
||||
*/
|
||||
@@ -38,7 +46,7 @@ $muted: #64748b;
|
||||
|
||||
&__sub {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
color: rgb(255 255 255 / 50%);
|
||||
margin: 2px 0 0;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -173,8 +190,9 @@ $muted: #64748b;
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@media (max-width: 768px) {
|
||||
.addr-grid--2, .addr-grid--3 {
|
||||
@media (width <= 768px) {
|
||||
.addr-grid--2,
|
||||
.addr-grid--3 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
|
||||
+131
-37
@@ -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()
|
||||
};
|
||||
|
||||
@@ -305,23 +305,85 @@
|
||||
<mat-datepicker #incorpDatePicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<!-- GSTIN (all non-individual) -->
|
||||
<mat-form-field appearance="outline" class="form-field">
|
||||
<mat-label>GSTIN Number</mat-label>
|
||||
<input
|
||||
matInput
|
||||
formControlName="gstinNumber"
|
||||
placeholder="15-character GSTIN"
|
||||
maxlength="15"
|
||||
style="text-transform: uppercase"
|
||||
(input)="
|
||||
kycForm.get('gstinNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
|
||||
"
|
||||
/>
|
||||
@if (kycForm.get('gstinNumber')?.hasError('pattern') && kycForm.get('gstinNumber')?.touched) {
|
||||
<mat-error>Invalid GSTIN format (e.g. 22AAAAA0000A1Z5)</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<!-- GSTIN (all non-individual) with Verify -->
|
||||
<div class="form-field verify-field">
|
||||
<mat-form-field appearance="outline" class="verify-input">
|
||||
<mat-label>GSTIN Number</mat-label>
|
||||
<span matPrefix class="field-prefix-icon"><mat-icon>receipt_long</mat-icon></span>
|
||||
<input
|
||||
matInput
|
||||
formControlName="gstinNumber"
|
||||
placeholder="15-character GSTIN"
|
||||
maxlength="15"
|
||||
style="text-transform: uppercase"
|
||||
(input)="
|
||||
kycForm.get('gstinNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })
|
||||
"
|
||||
/>
|
||||
@if (kycForm.get('gstinNumber')?.hasError('pattern') && kycForm.get('gstinNumber')?.touched) {
|
||||
<mat-error>Invalid GSTIN format (e.g. 22AAAAA0000A1Z5)</mat-error>
|
||||
}
|
||||
@if (gstinVerifyStatus === 'VERIFIED') {
|
||||
<mat-icon matSuffix class="verify-icon-ok">check_circle</mat-icon>
|
||||
}
|
||||
@if (gstinVerifyStatus === 'FAILED') {
|
||||
<mat-icon matSuffix class="verify-icon-fail">cancel</mat-icon>
|
||||
}
|
||||
</mat-form-field>
|
||||
<div class="verify-action">
|
||||
@if (gstinVerifyStatus === 'NONE' || gstinVerifyStatus === 'FAILED') {
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
class="verify-btn-inline"
|
||||
[disabled]="
|
||||
readOnly ||
|
||||
!kycForm.get('gstinNumber')?.value ||
|
||||
kycForm.get('gstinNumber')?.value.length < 15 ||
|
||||
gstinVerifying ||
|
||||
!clientId
|
||||
"
|
||||
(click)="verifyGstin()"
|
||||
>
|
||||
@if (gstinVerifying) {
|
||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
<fa-icon icon="shield-alt"></fa-icon> Verify GSTIN
|
||||
}
|
||||
</button>
|
||||
}
|
||||
@if (gstinVerifyStatus === 'VERIFIED') {
|
||||
<span class="verify-chip verified"><fa-icon icon="check-circle"></fa-icon> Verified</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (gstinVerifyStatus === 'VERIFIED') {
|
||||
<div class="pan-result-card full-width">
|
||||
<div class="pan-result-header">
|
||||
<fa-icon icon="check-circle" class="pan-result-ok-icon"></fa-icon>
|
||||
<span>GSTIN Verified Successfully</span>
|
||||
</div>
|
||||
<div class="pan-result-body">
|
||||
@if (gstinVerifyName) {
|
||||
<div class="pan-result-row">
|
||||
<span class="pan-result-label">Trade Name / Legal Name</span>
|
||||
<span class="pan-result-value name">{{ gstinVerifyName }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (gstinVerifyState) {
|
||||
<div class="pan-result-row">
|
||||
<span class="pan-result-label">State</span>
|
||||
<span class="pan-result-value">{{ gstinVerifyState }}</span>
|
||||
</div>
|
||||
}
|
||||
<div class="pan-result-row">
|
||||
<span class="pan-result-label">Verified On</span>
|
||||
<span class="pan-result-value date">{{ gstinVerifiedOn }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- PAN — Proprietorship uses Individual PAN, others use Enterprise PAN -->
|
||||
@if (constitutionMeta?.value === 'PROPRIETORSHIP') {
|
||||
|
||||
@@ -263,6 +263,13 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
entPanVerifyName = '';
|
||||
entPanVerifiedOn = '';
|
||||
|
||||
/** GSTIN verification */
|
||||
gstinVerifyStatus: 'NONE' | 'VERIFIED' | 'FAILED' = 'NONE';
|
||||
gstinVerifying = false;
|
||||
gstinVerifyName = '';
|
||||
gstinVerifiedOn = '';
|
||||
gstinVerifyState = '';
|
||||
|
||||
/** CIN verification (dummy) */
|
||||
cinVerifyStatus: 'NONE' | 'VERIFIED' | 'FAILED' = 'NONE';
|
||||
cinVerifying = false;
|
||||
@@ -480,6 +487,17 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
if (data.panAadhaarSeedingStatus) {
|
||||
this.panAadhaarSeedingStatus = data.panAadhaarSeedingStatus;
|
||||
}
|
||||
if (data.gstinVerified) {
|
||||
this.gstinVerifyStatus = 'VERIFIED';
|
||||
this.gstinVerifyName = data.gstinTradeName || data.gstinLegalName || '';
|
||||
this.gstinVerifyState = data.gstinState || '';
|
||||
const rawGstinDate = data.gstinVerifiedOn;
|
||||
if (Array.isArray(rawGstinDate) && rawGstinDate.length >= 3) {
|
||||
this.gstinVerifiedOn = `${rawGstinDate[2]?.toString().padStart(2, '0')}/${rawGstinDate[1]?.toString().padStart(2, '0')}/${rawGstinDate[0]}`;
|
||||
} else if (rawGstinDate) {
|
||||
this.gstinVerifiedOn = rawGstinDate;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Load existing photo & signature
|
||||
this.loadExistingPhoto();
|
||||
@@ -772,6 +790,71 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
|
||||
);
|
||||
}
|
||||
|
||||
/** Verify GSTIN via API */
|
||||
verifyGstin() {
|
||||
if (!this.clientId) return;
|
||||
const gstin = this.kycForm.value.gstinNumber;
|
||||
if (!gstin || this.kycForm.get('gstinNumber')?.invalid) return;
|
||||
|
||||
this.gstinVerifying = true;
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
|
||||
const payload: any = {
|
||||
locale: 'en',
|
||||
kycType: 1,
|
||||
riskCategory: this.kycForm.value.riskCategory || 1,
|
||||
gstinNumber: gstin,
|
||||
dateFormat,
|
||||
panNumber: this.kycForm.value.enterprisePan || this.kycForm.value.panNumber || null,
|
||||
aadhaarNumberMasked: 'XXXXXXXX0000'
|
||||
};
|
||||
|
||||
this.saveKycAndDo(
|
||||
payload,
|
||||
() => {
|
||||
this.clientsService.verifyGSTIN(this.clientId, this.kycId).subscribe({
|
||||
next: (res: any) => {
|
||||
this.gstinVerifying = false;
|
||||
const changes = res?.changes || res;
|
||||
if (changes?.gstinVerified === true) {
|
||||
this.gstinVerifyStatus = 'VERIFIED';
|
||||
this.gstinVerifyName = changes?.gstinTradeName || changes?.gstinLegalName || '';
|
||||
this.gstinVerifyState = changes?.gstinState || '';
|
||||
const rawDate = changes?.gstinVerifiedOn;
|
||||
if (Array.isArray(rawDate)) {
|
||||
this.gstinVerifiedOn = `${rawDate[2]?.toString().padStart(2, '0')}/${rawDate[1]?.toString().padStart(2, '0')}/${rawDate[0]}`;
|
||||
} else {
|
||||
const now = new Date();
|
||||
this.gstinVerifiedOn =
|
||||
rawDate ||
|
||||
`${now.getDate().toString().padStart(2, '0')}/${(now.getMonth() + 1).toString().padStart(2, '0')}/${now.getFullYear()}`;
|
||||
}
|
||||
} else {
|
||||
this.gstinVerifyStatus = 'FAILED';
|
||||
this.showPanErrorAlert(
|
||||
changes?.gstinVerificationMessage ||
|
||||
'GSTIN verification failed. Please check the GSTIN number and try again.'
|
||||
);
|
||||
}
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.gstinVerifying = false;
|
||||
this.gstinVerifyStatus = 'FAILED';
|
||||
this.showPanErrorAlert(
|
||||
err?.error?.errors?.[0]?.defaultUserMessage ||
|
||||
err?.error?.defaultUserMessage ||
|
||||
'GSTIN verification failed.'
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
(msg) => {
|
||||
this.gstinVerifying = false;
|
||||
this.showPanErrorAlert(msg);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/** Verify CIN (dummy — no real API) */
|
||||
verifyCin() {
|
||||
const cin = this.kycForm.value.cinNumber;
|
||||
|
||||
+12
-2
@@ -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>
|
||||
|
||||
+9
-1
@@ -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
|
||||
*/
|
||||
@@ -158,7 +166,7 @@ $navy: #0f172a;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@media (width <= 600px) {
|
||||
.crop-workspace {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
+55
-21
@@ -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) => {
|
||||
|
||||
+387
-162
@@ -88,14 +88,18 @@
|
||||
}
|
||||
@if (client?.dateOfBirth) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">{{ client.legalFormId === LegalFormId.PERSON ? 'Date of Birth' : 'Incorporation Date' }}</span>
|
||||
<span class="preview-label">{{
|
||||
client.legalFormId === LegalFormId.PERSON ? 'Date of Birth' : 'Incorporation Date'
|
||||
}}</span>
|
||||
<span class="preview-value">{{ client.dateOfBirth | dateFormat }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (client?.legalFormId === LegalFormId.PERSON && client?.genderId) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Gender</span>
|
||||
<span class="preview-value">{{ client.genderId | find: clientTemplate.genderOptions : 'id' : 'name' }}</span>
|
||||
<span class="preview-value">{{
|
||||
client.genderId | find: clientTemplate.genderOptions : 'id' : 'name'
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
@if (client?.legalFormId) {
|
||||
@@ -107,13 +111,17 @@
|
||||
@if (client?.officeId) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Office</span>
|
||||
<span class="preview-value">{{ client.officeId | find: clientTemplate.officeOptions : 'id' : 'name' }}</span>
|
||||
<span class="preview-value">{{
|
||||
client.officeId | find: clientTemplate.officeOptions : 'id' : 'name'
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
@if (client?.staffId) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Staff</span>
|
||||
<span class="preview-value">{{ client.staffId | find: clientTemplate.staffOptions : 'id' : 'displayName' }}</span>
|
||||
<span class="preview-value">{{
|
||||
client.staffId | find: clientTemplate.staffOptions : 'id' : 'displayName'
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
@if (client?.legalFormId === LegalFormId.PERSON) {
|
||||
@@ -137,7 +145,9 @@
|
||||
@if (client?.salutationId) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Salutation</span>
|
||||
<span class="preview-value">{{ client.salutationId | find: clientTemplate.salutationOptions : 'id' : 'name' }}</span>
|
||||
<span class="preview-value">{{
|
||||
client.salutationId | find: clientTemplate.salutationOptions : 'id' : 'name'
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
@if (client?.ageYears != null) {
|
||||
@@ -150,7 +160,11 @@
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Minor</span>
|
||||
<span class="preview-value">
|
||||
<span [class]="client.isMinor ? 'preview-badge preview-badge--danger' : 'preview-badge preview-badge--verified'">
|
||||
<span
|
||||
[class]="
|
||||
client.isMinor ? 'preview-badge preview-badge--danger' : 'preview-badge preview-badge--verified'
|
||||
"
|
||||
>
|
||||
{{ client.isMinor ? 'Yes (Age < 10)' : 'No' }}
|
||||
</span>
|
||||
</span>
|
||||
@@ -172,13 +186,19 @@
|
||||
@if (client.clientNonPersonDetails.constitutionId) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Constitution</span>
|
||||
<span class="preview-value">{{ client.clientNonPersonDetails.constitutionId | find: clientTemplate.clientNonPersonConstitutionOptions : 'id' : 'name' }}</span>
|
||||
<span class="preview-value">{{
|
||||
client.clientNonPersonDetails.constitutionId
|
||||
| find: clientTemplate.clientNonPersonConstitutionOptions : 'id' : 'name'
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
@if (client.clientNonPersonDetails.mainBusinessLineId) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Main Business Line</span>
|
||||
<span class="preview-value">{{ client.clientNonPersonDetails.mainBusinessLineId | find: clientTemplate.clientNonPersonMainBusinessLineOptions : 'id' : 'name' }}</span>
|
||||
<span class="preview-value">{{
|
||||
client.clientNonPersonDetails.mainBusinessLineId
|
||||
| find: clientTemplate.clientNonPersonMainBusinessLineOptions : 'id' : 'name'
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
@if (client.clientNonPersonDetails.incorpNumber) {
|
||||
@@ -215,7 +235,11 @@
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">DIP Verification</span>
|
||||
<span class="preview-value">
|
||||
<span [class]="client?.dipVerified ? 'preview-badge preview-badge--verified' : 'preview-badge preview-badge--pending'">
|
||||
<span
|
||||
[class]="
|
||||
client?.dipVerified ? 'preview-badge preview-badge--verified' : 'preview-badge preview-badge--pending'
|
||||
"
|
||||
>
|
||||
{{ client?.dipVerified ? 'Verified' : 'Pending' }}
|
||||
</span>
|
||||
</span>
|
||||
@@ -236,7 +260,13 @@
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Alternate DIP Verification</span>
|
||||
<span class="preview-value">
|
||||
<span [class]="client?.alternateDipVerified ? 'preview-badge preview-badge--verified' : 'preview-badge preview-badge--pending'">
|
||||
<span
|
||||
[class]="
|
||||
client?.alternateDipVerified
|
||||
? 'preview-badge preview-badge--verified'
|
||||
: 'preview-badge preview-badge--pending'
|
||||
"
|
||||
>
|
||||
{{ client?.alternateDipVerified ? 'Verified' : 'Pending' }}
|
||||
</span>
|
||||
</span>
|
||||
@@ -257,8 +287,18 @@
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Name Verification (Sanctions)</span>
|
||||
<span class="preview-value">
|
||||
<span [class]="client?.nameVerified ? 'preview-badge preview-badge--verified' : 'preview-badge preview-badge--danger'">
|
||||
{{ client?.nameVerified ? 'Verified — Clear' : (client?.nameVerified === false ? 'Sanctions Match' : 'Pending') }}
|
||||
<span
|
||||
[class]="
|
||||
client?.nameVerified ? 'preview-badge preview-badge--verified' : 'preview-badge preview-badge--danger'
|
||||
"
|
||||
>
|
||||
{{
|
||||
client?.nameVerified
|
||||
? 'Verified — Clear'
|
||||
: client?.nameVerified === false
|
||||
? 'Sanctions Match'
|
||||
: 'Pending'
|
||||
}}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
@@ -274,61 +314,82 @@
|
||||
|
||||
<!-- Financial & Classification (Individual only) -->
|
||||
@if (client?.legalFormId !== 2) {
|
||||
<div class="info-card">
|
||||
<div class="card-header">
|
||||
<fa-icon icon="chart-line" class="card-icon"></fa-icon>
|
||||
<span>Financial & Classification</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="preview-grid">
|
||||
@if (client?.occupationCategoryId) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Occupation Category</span>
|
||||
<span class="preview-value">{{ client.occupationCategoryId | find: clientTemplate.occupationCategoryOptions : 'id' : 'name' }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (client?.occupationId) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Occupation</span>
|
||||
<span class="preview-value">{{ client.occupationId | find: clientTemplate.occupationOptions : 'id' : 'name' }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (client?.clientTypeId) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Client Type</span>
|
||||
<span class="preview-value">{{ client.clientTypeId | find: clientTemplate.clientTypeOptions : 'id' : 'name' }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (client?.clientClassificationId) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Client Classification</span>
|
||||
<span class="preview-value">{{ client.clientClassificationId | find: clientTemplate.clientClassificationOptions : 'id' : 'name' }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (client?.monthlyIncome) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Monthly Income</span>
|
||||
<span class="preview-value">₹ {{ client.monthlyIncome | number }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (client?.annualTurnover) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Annual Turnover</span>
|
||||
<span class="preview-value">₹ {{ client.annualTurnover | number }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (!client?.occupationCategoryId && !client?.occupationId && !client?.clientTypeId && !client?.clientClassificationId && !client?.monthlyIncome && !client?.annualTurnover) {
|
||||
<div class="preview-field full-width">
|
||||
<span class="preview-value no-data">No classification data provided</span>
|
||||
</div>
|
||||
}
|
||||
<div class="info-card">
|
||||
<div class="card-header">
|
||||
<fa-icon icon="chart-line" class="card-icon"></fa-icon>
|
||||
<span>Financial & Classification</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="preview-grid">
|
||||
@if (client?.occupationCategoryId) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Occupation Category</span>
|
||||
<span class="preview-value">{{
|
||||
client.occupationCategoryId | find: clientTemplate.occupationCategoryOptions : 'id' : 'name'
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
@if (client?.occupationId) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Occupation</span>
|
||||
<span class="preview-value">{{
|
||||
client.occupationId | find: clientTemplate.occupationOptions : 'id' : 'name'
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
@if (client?.clientTypeId) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Client Type</span>
|
||||
<span class="preview-value">{{
|
||||
client.clientTypeId | find: clientTemplate.clientTypeOptions : 'id' : 'name'
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
@if (client?.clientClassificationId) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Client Classification</span>
|
||||
<span class="preview-value">{{
|
||||
client.clientClassificationId | find: clientTemplate.clientClassificationOptions : 'id' : 'name'
|
||||
}}</span>
|
||||
</div>
|
||||
}
|
||||
@if (client?.monthlyIncome) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Monthly Income</span>
|
||||
<span class="preview-value">₹ {{ client.monthlyIncome | number }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (client?.annualTurnover) {
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Annual Turnover</span>
|
||||
<span class="preview-value">₹ {{ client.annualTurnover | number }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (
|
||||
!client?.occupationCategoryId &&
|
||||
!client?.occupationId &&
|
||||
!client?.clientTypeId &&
|
||||
!client?.clientClassificationId &&
|
||||
!client?.monthlyIncome &&
|
||||
!client?.annualTurnover
|
||||
) {
|
||||
<div class="preview-field full-width">
|
||||
<span class="preview-value no-data">No classification data provided</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- KYC Details -->
|
||||
@if (kycData?.panNumber || kycData?.aadhaarNumberMasked || kycData?.gstinNumber || kycData?.enterprisePan || kycData?.cinNumber) {
|
||||
@if (
|
||||
kycData?.panNumber ||
|
||||
kycData?.aadhaarNumberMasked ||
|
||||
kycData?.gstinNumber ||
|
||||
kycData?.enterprisePan ||
|
||||
kycData?.cinNumber
|
||||
) {
|
||||
<div class="info-card">
|
||||
<div class="card-header">
|
||||
<fa-icon icon="fingerprint" class="card-icon"></fa-icon>
|
||||
@@ -391,9 +452,13 @@
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Risk Category</span>
|
||||
<span class="preview-value">
|
||||
@if (kycData.riskCategory === 1) { Low Risk }
|
||||
@else if (kycData.riskCategory === 2) { Medium Risk }
|
||||
@else if (kycData.riskCategory === 3) { High Risk }
|
||||
@if (kycData.riskCategory === 1) {
|
||||
Low Risk
|
||||
} @else if (kycData.riskCategory === 2) {
|
||||
Medium Risk
|
||||
} @else if (kycData.riskCategory === 3) {
|
||||
High Risk
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@@ -414,7 +479,7 @@
|
||||
<div class="card-header">
|
||||
<fa-icon icon="user-shield" class="card-icon"></fa-icon>
|
||||
<span>Guardian Details</span>
|
||||
<span class="preview-badge preview-badge--info" style="margin-left:auto">Minor</span>
|
||||
<span class="preview-badge preview-badge--info" style="margin-left: auto">Minor</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="preview-grid">
|
||||
@@ -450,7 +515,13 @@
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Status</span>
|
||||
<span class="preview-value">
|
||||
<span [class]="guardian.status?.value === 'Active' ? 'preview-badge preview-badge--verified' : 'preview-badge preview-badge--pending'">
|
||||
<span
|
||||
[class]="
|
||||
guardian.status?.value === 'Active'
|
||||
? 'preview-badge preview-badge--verified'
|
||||
: 'preview-badge preview-badge--pending'
|
||||
"
|
||||
>
|
||||
{{ guardian.status.value }}
|
||||
</span>
|
||||
</span>
|
||||
@@ -524,7 +595,12 @@
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@if (!addressData.permanent?.addressLine1 && !addressData.current?.addressLine1 && !addressData.office?.addressLine1 && !addressData.correspondence?.addressLine1) {
|
||||
@if (
|
||||
!addressData.permanent?.addressLine1 &&
|
||||
!addressData.current?.addressLine1 &&
|
||||
!addressData.office?.addressLine1 &&
|
||||
!addressData.correspondence?.addressLine1
|
||||
) {
|
||||
<div class="preview-field full-width">
|
||||
<span class="preview-value no-data">No address data provided</span>
|
||||
</div>
|
||||
@@ -563,40 +639,76 @@
|
||||
<div class="card-body">
|
||||
<div class="preview-grid">
|
||||
@if (entityClassificationData.msme.udyamRegistrationNumber) {
|
||||
<div class="preview-field"><span class="preview-label">Udyam Registration No.</span><span class="preview-value mono">{{ entityClassificationData.msme.udyamRegistrationNumber }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Udyam Registration No.</span
|
||||
><span class="preview-value mono">{{ entityClassificationData.msme.udyamRegistrationNumber }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.msme.msmeClassification) {
|
||||
<div class="preview-field"><span class="preview-label">MSME Classification</span><span class="preview-value">{{ entityClassificationData.msme.msmeClassification }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">MSME Classification</span
|
||||
><span class="preview-value">{{ entityClassificationData.msme.msmeClassification }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.msme.enterpriseType) {
|
||||
<div class="preview-field"><span class="preview-label">Enterprise Type</span><span class="preview-value">{{ entityClassificationData.msme.enterpriseType }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Enterprise Type</span
|
||||
><span class="preview-value">{{ entityClassificationData.msme.enterpriseType }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.msme.nicActivityCode) {
|
||||
<div class="preview-field"><span class="preview-label">NIC Activity Code</span><span class="preview-value mono">{{ entityClassificationData.msme.nicActivityCode }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">NIC Activity Code</span
|
||||
><span class="preview-value mono">{{ entityClassificationData.msme.nicActivityCode }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.msme.annualTurnover) {
|
||||
<div class="preview-field"><span class="preview-label">Annual Turnover</span><span class="preview-value">₹ {{ entityClassificationData.msme.annualTurnover }} Cr</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Annual Turnover</span
|
||||
><span class="preview-value">₹ {{ entityClassificationData.msme.annualTurnover }} Cr</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.msme.investmentPlantMachinery) {
|
||||
<div class="preview-field"><span class="preview-label">Investment (Plant & Machinery)</span><span class="preview-value">₹ {{ entityClassificationData.msme.investmentPlantMachinery }} Cr</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Investment (Plant & Machinery)</span
|
||||
><span class="preview-value">₹ {{ entityClassificationData.msme.investmentPlantMachinery }} Cr</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.msme.financialYearAssessment) {
|
||||
<div class="preview-field"><span class="preview-label">FY of Assessment</span><span class="preview-value">{{ entityClassificationData.msme.financialYearAssessment }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">FY of Assessment</span
|
||||
><span class="preview-value">{{ entityClassificationData.msme.financialYearAssessment }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.msme.socialCategoryOwner) {
|
||||
<div class="preview-field"><span class="preview-label">Social Category</span><span class="preview-value">{{ entityClassificationData.msme.socialCategoryOwner }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Social Category</span
|
||||
><span class="preview-value">{{ entityClassificationData.msme.socialCategoryOwner }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.msme.genderPrimaryOwner) {
|
||||
<div class="preview-field"><span class="preview-label">Gender of Owner</span><span class="preview-value">{{ entityClassificationData.msme.genderPrimaryOwner }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Gender of Owner</span
|
||||
><span class="preview-value">{{ entityClassificationData.msme.genderPrimaryOwner }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.msme.noOfEmployees) {
|
||||
<div class="preview-field"><span class="preview-label">No. of Employees</span><span class="preview-value">{{ entityClassificationData.msme.noOfEmployees }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">No. of Employees</span
|
||||
><span class="preview-value">{{ entityClassificationData.msme.noOfEmployees }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.msme.womenLedEnterprise) {
|
||||
<div class="preview-field"><span class="preview-label">Women-led Enterprise</span><span class="preview-value"><span class="preview-badge preview-badge--verified">Yes</span></span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Women-led Enterprise</span
|
||||
><span class="preview-value"><span class="preview-badge preview-badge--verified">Yes</span></span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.msme.divyangPwdOwned) {
|
||||
<div class="preview-field"><span class="preview-label">Divyang (PWD) Owned</span><span class="preview-value"><span class="preview-badge preview-badge--verified">Yes</span></span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Divyang (PWD) Owned</span
|
||||
><span class="preview-value"><span class="preview-badge preview-badge--verified">Yes</span></span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -613,19 +725,34 @@
|
||||
<div class="card-body">
|
||||
<div class="preview-grid">
|
||||
@if (entityClassificationData.startup.dpiitRecognitionNumber) {
|
||||
<div class="preview-field"><span class="preview-label">DPIIT Recognition No.</span><span class="preview-value mono">{{ entityClassificationData.startup.dpiitRecognitionNumber }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">DPIIT Recognition No.</span
|
||||
><span class="preview-value mono">{{ entityClassificationData.startup.dpiitRecognitionNumber }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.startup.entityStage) {
|
||||
<div class="preview-field"><span class="preview-label">Entity Stage</span><span class="preview-value">{{ entityClassificationData.startup.entityStage }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Entity Stage</span
|
||||
><span class="preview-value">{{ entityClassificationData.startup.entityStage }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.startup.sectorAsDpiit) {
|
||||
<div class="preview-field"><span class="preview-label">Sector (DPIIT)</span><span class="preview-value">{{ entityClassificationData.startup.sectorAsDpiit }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Sector (DPIIT)</span
|
||||
><span class="preview-value">{{ entityClassificationData.startup.sectorAsDpiit }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.startup.taxExemption80iac) {
|
||||
<div class="preview-field"><span class="preview-label">80IAC Tax Exemption</span><span class="preview-value"><span class="preview-badge preview-badge--verified">Yes</span></span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">80IAC Tax Exemption</span
|
||||
><span class="preview-value"><span class="preview-badge preview-badge--verified">Yes</span></span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.startup.pslLoanLimit) {
|
||||
<div class="preview-field"><span class="preview-label">PSL Loan Limit</span><span class="preview-value">₹ {{ entityClassificationData.startup.pslLoanLimit | number }} Cr</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">PSL Loan Limit</span
|
||||
><span class="preview-value">₹ {{ entityClassificationData.startup.pslLoanLimit | number }} Cr</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -642,22 +769,40 @@
|
||||
<div class="card-body">
|
||||
<div class="preview-grid">
|
||||
@if (entityClassificationData.mudra.mudraCategory) {
|
||||
<div class="preview-field"><span class="preview-label">Mudra Category</span><span class="preview-value">{{ entityClassificationData.mudra.mudraCategory }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Mudra Category</span
|
||||
><span class="preview-value">{{ entityClassificationData.mudra.mudraCategory }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.mudra.mudraLoanAccountNo) {
|
||||
<div class="preview-field"><span class="preview-label">Loan Account No.</span><span class="preview-value mono">{{ entityClassificationData.mudra.mudraLoanAccountNo }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Loan Account No.</span
|
||||
><span class="preview-value mono">{{ entityClassificationData.mudra.mudraLoanAccountNo }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.mudra.sanctionedAmount) {
|
||||
<div class="preview-field"><span class="preview-label">Sanctioned Amount</span><span class="preview-value">₹ {{ entityClassificationData.mudra.sanctionedAmount | number }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Sanctioned Amount</span
|
||||
><span class="preview-value">₹ {{ entityClassificationData.mudra.sanctionedAmount | number }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.mudra.lendingInstitutionName) {
|
||||
<div class="preview-field"><span class="preview-label">Lending Institution</span><span class="preview-value">{{ entityClassificationData.mudra.lendingInstitutionName }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Lending Institution</span
|
||||
><span class="preview-value">{{ entityClassificationData.mudra.lendingInstitutionName }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.mudra.repaymentStatus) {
|
||||
<div class="preview-field"><span class="preview-label">Repayment Status</span><span class="preview-value">{{ entityClassificationData.mudra.repaymentStatus }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Repayment Status</span
|
||||
><span class="preview-value">{{ entityClassificationData.mudra.repaymentStatus }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.mudra.npaFlag) {
|
||||
<div class="preview-field"><span class="preview-label">NPA Flag</span><span class="preview-value"><span class="preview-badge preview-badge--danger">NPA</span></span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">NPA Flag</span
|
||||
><span class="preview-value"><span class="preview-badge preview-badge--danger">NPA</span></span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -674,19 +819,36 @@
|
||||
<div class="card-body">
|
||||
<div class="preview-grid">
|
||||
@if (entityClassificationData.eou.eouRegistrationNumber) {
|
||||
<div class="preview-field"><span class="preview-label">EOU Registration No.</span><span class="preview-value mono">{{ entityClassificationData.eou.eouRegistrationNumber }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">EOU Registration No.</span
|
||||
><span class="preview-value mono">{{ entityClassificationData.eou.eouRegistrationNumber }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.eou.registeredDcZone) {
|
||||
<div class="preview-field"><span class="preview-label">Registered DC Zone</span><span class="preview-value">{{ entityClassificationData.eou.registeredDcZone }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Registered DC Zone</span
|
||||
><span class="preview-value">{{ entityClassificationData.eou.registeredDcZone }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.eou.gstinEou) {
|
||||
<div class="preview-field"><span class="preview-label">GSTIN (EOU)</span><span class="preview-value mono">{{ entityClassificationData.eou.gstinEou }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">GSTIN (EOU)</span
|
||||
><span class="preview-value mono">{{ entityClassificationData.eou.gstinEou }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.eou.annualExportObligation) {
|
||||
<div class="preview-field"><span class="preview-label">Export Obligation</span><span class="preview-value">₹ {{ entityClassificationData.eou.annualExportObligation | number }} Cr</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Export Obligation</span
|
||||
><span class="preview-value"
|
||||
>₹ {{ entityClassificationData.eou.annualExportObligation | number }} Cr</span
|
||||
>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.eou.adCategoryBankCode) {
|
||||
<div class="preview-field"><span class="preview-label">AD Bank Code</span><span class="preview-value mono">{{ entityClassificationData.eou.adCategoryBankCode }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">AD Bank Code</span
|
||||
><span class="preview-value mono">{{ entityClassificationData.eou.adCategoryBankCode }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -703,22 +865,40 @@
|
||||
<div class="card-body">
|
||||
<div class="preview-grid">
|
||||
@if (entityClassificationData.importexport.iecCode) {
|
||||
<div class="preview-field"><span class="preview-label">IEC Code</span><span class="preview-value mono">{{ entityClassificationData.importexport.iecCode }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">IEC Code</span
|
||||
><span class="preview-value mono">{{ entityClassificationData.importexport.iecCode }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.importexport.iecStatus) {
|
||||
<div class="preview-field"><span class="preview-label">IEC Status</span><span class="preview-value">{{ entityClassificationData.importexport.iecStatus }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">IEC Status</span
|
||||
><span class="preview-value">{{ entityClassificationData.importexport.iecStatus }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.importexport.tradeType) {
|
||||
<div class="preview-field"><span class="preview-label">Trade Type</span><span class="preview-value">{{ entityClassificationData.importexport.tradeType }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Trade Type</span
|
||||
><span class="preview-value">{{ entityClassificationData.importexport.tradeType }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.importexport.primaryHsCode) {
|
||||
<div class="preview-field"><span class="preview-label">Primary HS Code</span><span class="preview-value mono">{{ entityClassificationData.importexport.primaryHsCode }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Primary HS Code</span
|
||||
><span class="preview-value mono">{{ entityClassificationData.importexport.primaryHsCode }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.importexport.eefcAccountRequired) {
|
||||
<div class="preview-field"><span class="preview-label">EEFC Account</span><span class="preview-value"><span class="preview-badge preview-badge--verified">Required</span></span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">EEFC Account</span
|
||||
><span class="preview-value"><span class="preview-badge preview-badge--verified">Required</span></span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.importexport.femaDeclarationObtained) {
|
||||
<div class="preview-field"><span class="preview-label">FEMA Declaration</span><span class="preview-value"><span class="preview-badge preview-badge--verified">Obtained</span></span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">FEMA Declaration</span
|
||||
><span class="preview-value"><span class="preview-badge preview-badge--verified">Obtained</span></span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -735,22 +915,40 @@
|
||||
<div class="card-body">
|
||||
<div class="preview-grid">
|
||||
@if (entityClassificationData.psl.pslCategory) {
|
||||
<div class="preview-field"><span class="preview-label">PSL Category</span><span class="preview-value">{{ entityClassificationData.psl.pslCategory }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">PSL Category</span
|
||||
><span class="preview-value">{{ entityClassificationData.psl.pslCategory }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.psl.pslSubCategory) {
|
||||
<div class="preview-field"><span class="preview-label">PSL Sub-category</span><span class="preview-value">{{ entityClassificationData.psl.pslSubCategory }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">PSL Sub-category</span
|
||||
><span class="preview-value">{{ entityClassificationData.psl.pslSubCategory }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.psl.pslSanctionedLimit) {
|
||||
<div class="preview-field"><span class="preview-label">Sanctioned Limit</span><span class="preview-value">₹ {{ entityClassificationData.psl.pslSanctionedLimit | number }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Sanctioned Limit</span
|
||||
><span class="preview-value">₹ {{ entityClassificationData.psl.pslSanctionedLimit | number }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.psl.fiddReportingCode) {
|
||||
<div class="preview-field"><span class="preview-label">FIDD Reporting Code</span><span class="preview-value mono">{{ entityClassificationData.psl.fiddReportingCode }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">FIDD Reporting Code</span
|
||||
><span class="preview-value mono">{{ entityClassificationData.psl.fiddReportingCode }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.psl.weakerSectionFlag) {
|
||||
<div class="preview-field"><span class="preview-label">Weaker Section</span><span class="preview-value"><span class="preview-badge preview-badge--verified">Yes</span></span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Weaker Section</span
|
||||
><span class="preview-value"><span class="preview-badge preview-badge--verified">Yes</span></span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.psl.womenEntrepreneurFlag) {
|
||||
<div class="preview-field"><span class="preview-label">Women Entrepreneur</span><span class="preview-value"><span class="preview-badge preview-badge--verified">Yes</span></span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Women Entrepreneur</span
|
||||
><span class="preview-value"><span class="preview-badge preview-badge--verified">Yes</span></span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -767,25 +965,46 @@
|
||||
<div class="card-body">
|
||||
<div class="preview-grid">
|
||||
@if (entityClassificationData.nps.industrySector) {
|
||||
<div class="preview-field"><span class="preview-label">Industry Sector</span><span class="preview-value">{{ entityClassificationData.nps.industrySector }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Industry Sector</span
|
||||
><span class="preview-value">{{ entityClassificationData.nps.industrySector }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.nps.nicCode) {
|
||||
<div class="preview-field"><span class="preview-label">NIC Code</span><span class="preview-value mono">{{ entityClassificationData.nps.nicCode }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">NIC Code</span
|
||||
><span class="preview-value mono">{{ entityClassificationData.nps.nicCode }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.nps.annualRevenueBand) {
|
||||
<div class="preview-field"><span class="preview-label">Annual Revenue Band</span><span class="preview-value">{{ entityClassificationData.nps.annualRevenueBand }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Annual Revenue Band</span
|
||||
><span class="preview-value">{{ entityClassificationData.nps.annualRevenueBand }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.nps.riskCategory) {
|
||||
<div class="preview-field"><span class="preview-label">Risk Category</span><span class="preview-value">{{ entityClassificationData.nps.riskCategory }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">Risk Category</span
|
||||
><span class="preview-value">{{ entityClassificationData.nps.riskCategory }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.nps.amlRiskClassification) {
|
||||
<div class="preview-field"><span class="preview-label">AML Risk</span><span class="preview-value">{{ entityClassificationData.nps.amlRiskClassification }}</span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">AML Risk</span
|
||||
><span class="preview-value">{{ entityClassificationData.nps.amlRiskClassification }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.nps.pepFlag) {
|
||||
<div class="preview-field"><span class="preview-label">PEP Flag</span><span class="preview-value"><span class="preview-badge preview-badge--danger">PEP</span></span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">PEP Flag</span
|
||||
><span class="preview-value"><span class="preview-badge preview-badge--danger">PEP</span></span>
|
||||
</div>
|
||||
}
|
||||
@if (entityClassificationData.nps.amlWatchlistMatch) {
|
||||
<div class="preview-field"><span class="preview-label">AML Watchlist</span><span class="preview-value"><span class="preview-badge preview-badge--danger">Match Found</span></span></div>
|
||||
<div class="preview-field">
|
||||
<span class="preview-label">AML Watchlist</span
|
||||
><span class="preview-value"><span class="preview-badge preview-badge--danger">Match Found</span></span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -793,58 +1012,64 @@
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
<!-- Family Members (Individual only) -->
|
||||
@if (client?.legalFormId !== 2) {
|
||||
<div class="info-card">
|
||||
<div class="card-header">
|
||||
<fa-icon icon="users" class="card-icon"></fa-icon>
|
||||
<span>Family Members</span>
|
||||
@if (familyMembers.length > 0) {
|
||||
<span class="count-badge">{{ familyMembers.length }}</span>
|
||||
}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (familyMembers.length === 0) {
|
||||
<div class="preview-empty preview-empty--warn">
|
||||
<mat-icon class="preview-empty__icon">warning</mat-icon>
|
||||
<span>No family members added. At least one is required.</span>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="fm-preview-list">
|
||||
@for (member of familyMembers; track member; let i = $index) {
|
||||
<div class="fm-preview-row">
|
||||
<div class="fm-preview-row__sno">{{ i + 1 }}</div>
|
||||
<div class="fm-preview-row__avatar">
|
||||
<mat-icon>person</mat-icon>
|
||||
</div>
|
||||
<div class="fm-preview-row__info">
|
||||
<span class="fm-preview-row__name">{{ member.firstName || member.displayName }}</span>
|
||||
<span class="fm-preview-row__meta">
|
||||
@if (member.relationship) { {{ member.relationship }} }
|
||||
@if (member.dateOfBirth) { · DOB: {{ member.dateOfBirth }} }
|
||||
</span>
|
||||
</div>
|
||||
@if (member.aadhaarNumber || member.panNumber) {
|
||||
<div class="fm-preview-row__ids">
|
||||
@if (member.panNumber) { <span class="fm-preview-id">PAN: {{ member.panNumber }}</span> }
|
||||
@if (member.aadhaarNumber) { <span class="fm-preview-id">Aadhaar: {{ member.aadhaarNumber }}</span> }
|
||||
<div class="info-card">
|
||||
<div class="card-header">
|
||||
<fa-icon icon="users" class="card-icon"></fa-icon>
|
||||
<span>Family Members</span>
|
||||
@if (familyMembers.length > 0) {
|
||||
<span class="count-badge">{{ familyMembers.length }}</span>
|
||||
}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
@if (familyMembers.length === 0) {
|
||||
<div class="preview-empty preview-empty--warn">
|
||||
<mat-icon class="preview-empty__icon">warning</mat-icon>
|
||||
<span>No family members added. At least one is required.</span>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="fm-preview-list">
|
||||
@for (member of familyMembers; track member; let i = $index) {
|
||||
<div class="fm-preview-row">
|
||||
<div class="fm-preview-row__sno">{{ i + 1 }}</div>
|
||||
<div class="fm-preview-row__avatar">
|
||||
<mat-icon>person</mat-icon>
|
||||
</div>
|
||||
}
|
||||
<div class="fm-preview-row__status">
|
||||
@if (member.isDependent) {
|
||||
<span class="preview-badge preview-badge--info">Dependent</span>
|
||||
}
|
||||
@if (member.linkedClientId) {
|
||||
<span class="preview-badge preview-badge--verified">Linked</span>
|
||||
<div class="fm-preview-row__info">
|
||||
<span class="fm-preview-row__name">{{ member.firstName || member.displayName }}</span>
|
||||
<span class="fm-preview-row__meta">
|
||||
@if (member.relationship) {
|
||||
{{ member.relationship }}
|
||||
}
|
||||
@if (member.dateOfBirth) {
|
||||
· DOB: {{ member.dateOfBirth }}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
@if (member.aadhaarNumber || member.panNumber) {
|
||||
<div class="fm-preview-row__ids">
|
||||
@if (member.panNumber) {
|
||||
<span class="fm-preview-id">PAN: {{ member.panNumber }}</span>
|
||||
}
|
||||
@if (member.aadhaarNumber) {
|
||||
<span class="fm-preview-id">Aadhaar: {{ member.aadhaarNumber }}</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<div class="fm-preview-row__status">
|
||||
@if (member.isDependent) {
|
||||
<span class="preview-badge preview-badge--info">Dependent</span>
|
||||
}
|
||||
@if (member.linkedClientId) {
|
||||
<span class="preview-badge preview-badge--verified">Linked</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
</div>
|
||||
|
||||
+34
-24
@@ -32,7 +32,7 @@
|
||||
|
||||
&__sub {
|
||||
font-size: 11px;
|
||||
color: rgba(255, 255, 255, 0.5);
|
||||
color: rgb(255 255 255 / 50%);
|
||||
margin: 2px 0 0;
|
||||
}
|
||||
}
|
||||
@@ -51,7 +51,7 @@
|
||||
padding: 20px 24px;
|
||||
background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%);
|
||||
border-radius: 10px;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border: 1px solid rgb(0 0 0 / 6%);
|
||||
}
|
||||
|
||||
.profile-avatar {
|
||||
@@ -96,9 +96,9 @@
|
||||
font-weight: 500;
|
||||
padding: 3px 10px;
|
||||
border-radius: 20px;
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
background: rgb(255 255 255 / 70%);
|
||||
color: #555;
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid rgb(0 0 0 / 8%);
|
||||
|
||||
.chip-icon {
|
||||
font-size: 10px;
|
||||
@@ -120,7 +120,7 @@
|
||||
|
||||
/* ── Info Cards ── */
|
||||
.info-card {
|
||||
border: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border: 1px solid rgb(0 0 0 / 8%);
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
background: #fff;
|
||||
@@ -132,7 +132,7 @@
|
||||
gap: 8px;
|
||||
padding: 12px 20px;
|
||||
background: #fafafa;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border-bottom: 1px solid rgb(0 0 0 / 6%);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
@@ -177,8 +177,8 @@
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-right: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-bottom: 1px solid rgb(0 0 0 / 4%);
|
||||
border-right: 1px solid rgb(0 0 0 / 4%);
|
||||
|
||||
&:nth-child(3n) {
|
||||
border-right: none;
|
||||
@@ -195,7 +195,7 @@
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
color: rgba(0, 0, 0, 0.45);
|
||||
color: rgb(0 0 0 / 45%);
|
||||
}
|
||||
|
||||
.preview-value {
|
||||
@@ -204,7 +204,7 @@
|
||||
color: #1a1a1a;
|
||||
|
||||
&.no-data {
|
||||
color: rgba(0, 0, 0, 0.3);
|
||||
color: rgb(0 0 0 / 30%);
|
||||
font-style: italic;
|
||||
font-weight: 400;
|
||||
}
|
||||
@@ -239,7 +239,7 @@
|
||||
border-collapse: collapse;
|
||||
|
||||
tr {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-bottom: 1px solid rgb(0 0 0 / 4%);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
@@ -254,7 +254,7 @@
|
||||
.info-label {
|
||||
width: 40%;
|
||||
font-size: 13px;
|
||||
color: rgba(0, 0, 0, 0.5);
|
||||
color: rgb(0 0 0 / 50%);
|
||||
font-weight: 500;
|
||||
padding-right: 16px;
|
||||
}
|
||||
@@ -278,17 +278,17 @@
|
||||
padding: 10px 16px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: rgba(0, 0, 0, 0.55);
|
||||
color: rgb(0 0 0 / 55%);
|
||||
text-align: left;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-bottom: 1px solid rgb(0 0 0 / 8%);
|
||||
}
|
||||
}
|
||||
|
||||
tbody {
|
||||
tr {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
|
||||
border-bottom: 1px solid rgb(0 0 0 / 4%);
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
@@ -349,7 +349,7 @@
|
||||
.address-block {
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 16px;
|
||||
border-bottom: 1px dashed rgba(0, 0, 0, 0.08);
|
||||
border-bottom: 1px dashed rgb(0 0 0 / 8%);
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
@@ -381,7 +381,7 @@
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
padding: 20px 0 4px;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-top: 1px solid rgb(0 0 0 / 8%);
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
@@ -394,23 +394,28 @@
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
color: rgb(0 0 0 / 60%);
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
/* ── Responsive ── */
|
||||
@media (max-width: 960px) {
|
||||
@media (width <= 960px) {
|
||||
.preview-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.preview-field {
|
||||
&:nth-child(3n) { border-right: 1px solid rgba(0, 0, 0, 0.04); }
|
||||
&:nth-child(2n) { border-right: none; }
|
||||
&:nth-child(3n) {
|
||||
border-right: 1px solid rgb(0 0 0 / 4%);
|
||||
}
|
||||
|
||||
&:nth-child(2n) {
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (width <= 768px) {
|
||||
.profile-card {
|
||||
flex-direction: column;
|
||||
text-align: center;
|
||||
@@ -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 {
|
||||
|
||||
@@ -75,7 +75,12 @@ export class ClientPreviewStepComponent {
|
||||
@Input() maritalStatusOptions: any[] = [];
|
||||
|
||||
/** Address tab labels */
|
||||
addressTabs = ['permanent', 'current', 'office', 'correspondence'];
|
||||
addressTabs = [
|
||||
'permanent',
|
||||
'current',
|
||||
'office',
|
||||
'correspondence'
|
||||
];
|
||||
addressTabLabels: Record<string, string> = {
|
||||
permanent: 'Permanent Address',
|
||||
current: 'Current / Residential',
|
||||
|
||||
@@ -34,12 +34,18 @@
|
||||
<mat-panel-title class="addr-panel__title">
|
||||
<i class="fa fa-home addr-panel__type-icon"></i>
|
||||
<span class="addr-panel__type">{{ address.addressType }}</span>
|
||||
<span class="addr-panel__status" [class.addr-panel__status--active]="address.isActive" [class.addr-panel__status--inactive]="!address.isActive">
|
||||
<span
|
||||
class="addr-panel__status"
|
||||
[class.addr-panel__status--active]="address.isActive"
|
||||
[class.addr-panel__status--inactive]="!address.isActive"
|
||||
>
|
||||
{{ address.isActive ? 'Active' : 'Inactive' }}
|
||||
</span>
|
||||
</mat-panel-title>
|
||||
<mat-panel-description class="addr-panel__desc">
|
||||
{{ address.city || '' }}{{ address.city && (address.countyDistrict || address.addressLine3) ? ', ' : '' }}{{ address.countyDistrict || address.addressLine3 || '' }}{{ address.postalCode ? ' - ' + address.postalCode : '' }}
|
||||
{{ address.city || '' }}{{ address.city && (address.countyDistrict || address.addressLine3) ? ', ' : ''
|
||||
}}{{ address.countyDistrict || address.addressLine3 || ''
|
||||
}}{{ address.postalCode ? ' - ' + address.postalCode : '' }}
|
||||
</mat-panel-description>
|
||||
</mat-expansion-panel-header>
|
||||
|
||||
@@ -101,12 +107,16 @@
|
||||
@if (address.stateProvinceId) {
|
||||
<div class="addr-field">
|
||||
<div class="addr-field__label">State</div>
|
||||
<div class="addr-field__value">{{ getSelectedValue('stateProvinceIdOptions', address.stateProvinceId)?.name || '-' }}</div>
|
||||
<div class="addr-field__value">
|
||||
{{ getSelectedValue('stateProvinceIdOptions', address.stateProvinceId)?.name || '-' }}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div class="addr-field">
|
||||
<div class="addr-field__label">Country</div>
|
||||
<div class="addr-field__value">{{ getSelectedValue('countryIdOptions', address.countryId)?.name || 'India' }}</div>
|
||||
<div class="addr-field__value">
|
||||
{{ getSelectedValue('countryIdOptions', address.countryId)?.name || 'India' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
|
||||
@@ -92,7 +92,7 @@ $red-light: #fce8e6;
|
||||
margin-bottom: 12px !important;
|
||||
border-radius: 10px !important;
|
||||
border: 1px solid $gray-200 !important;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06) !important;
|
||||
box-shadow: 0 1px 3px rgb(0 0 0 / 6%) !important;
|
||||
|
||||
&::ng-deep .mat-expansion-panel-body {
|
||||
padding: 0 20px 20px !important;
|
||||
@@ -194,11 +194,11 @@ $red-light: #fce8e6;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (width <= 768px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
@media (width <= 480px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -233,7 +233,7 @@ $red-light: #fce8e6;
|
||||
word-break: break-word;
|
||||
|
||||
&--mono {
|
||||
font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
|
||||
font-family: 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
@@ -24,52 +24,100 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Top Section: Read-Only Client Info -->
|
||||
<mat-card class="ckyc-info-card">
|
||||
<mat-card-content>
|
||||
<div class="ckyc-info-grid">
|
||||
<!-- Account Number Dropdown -->
|
||||
<div class="ckyc-info-item">
|
||||
<span class="ckyc-info-label">{{ 'labels.inputs.Account Number' | translate }}</span>
|
||||
<mat-form-field appearance="outline">
|
||||
<mat-select
|
||||
[value]="selectedAccount?.accountNo || ''"
|
||||
(selectionChange)="onAccountSelect($event.value)"
|
||||
[placeholder]="'labels.inputs.Select Account' | translate"
|
||||
>
|
||||
@for (account of allAccounts; track account.accountNo) {
|
||||
<mat-option [value]="account.accountNo">
|
||||
{{ account.accountNo }} ({{ account.productName }})
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Client ID -->
|
||||
<div class="ckyc-info-item">
|
||||
<span class="ckyc-info-label">{{ 'labels.inputs.Client ID' | translate }}</span>
|
||||
<span class="ckyc-info-value">{{ clientId }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Client Name -->
|
||||
<div class="ckyc-info-item">
|
||||
<span class="ckyc-info-label">{{ 'labels.inputs.Client Name' | translate }}</span>
|
||||
<span class="ckyc-info-value">{{ clientName }}</span>
|
||||
</div>
|
||||
|
||||
<!-- Account Type -->
|
||||
<div class="ckyc-info-item">
|
||||
<span class="ckyc-info-label">{{ 'labels.inputs.Account Type' | translate }}</span>
|
||||
<span class="ckyc-info-value">{{ selectedAccountType || '—' }}</span>
|
||||
</div>
|
||||
<!-- CERSAI Sync Status hero -->
|
||||
<div class="cersai-hero" [class.cersai-hero--synced]="ckycNumber" [class.cersai-hero--pending]="!ckycNumber">
|
||||
<div class="cersai-hero__left">
|
||||
<div class="cersai-hero__icon">
|
||||
@if (ckycNumber) {
|
||||
<fa-icon icon="check-double"></fa-icon>
|
||||
} @else {
|
||||
<fa-icon icon="hourglass-half"></fa-icon>
|
||||
}
|
||||
</div>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
<div class="cersai-hero__text">
|
||||
<div class="cersai-hero__status">
|
||||
@if (ckycNumber) {
|
||||
<span class="cersai-hero__tag">SYNCED WITH CERSAI</span>
|
||||
} @else {
|
||||
<span class="cersai-hero__tag cersai-hero__tag--pending">PENDING CERSAI SYNC</span>
|
||||
}
|
||||
</div>
|
||||
@if (ckycNumber) {
|
||||
<div class="cersai-hero__ckyc-label">CKYC Registry Number</div>
|
||||
<div class="cersai-hero__ckyc-number">{{ ckycNumber }}</div>
|
||||
<div class="cersai-hero__meta">
|
||||
@if (ckycUploadedOn) {
|
||||
<span>
|
||||
<fa-icon icon="calendar-check"></fa-icon>
|
||||
Uploaded on <strong>{{ ckycUploadedOn }}</strong>
|
||||
</span>
|
||||
}
|
||||
@if (ckycBatchId) {
|
||||
<span class="cersai-hero__sep">·</span>
|
||||
<span>
|
||||
<fa-icon icon="file-archive"></fa-icon>
|
||||
Batch <strong>{{ ckycBatchId }}</strong>
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<div class="cersai-hero__ckyc-label">This client has not yet been synced to the CERSAI registry</div>
|
||||
<div class="cersai-hero__hint">
|
||||
Collect all 4 artefacts below, then create a batch from the Record Master to upload.
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (ckycNumber) {
|
||||
<div class="cersai-hero__pulse">
|
||||
<span class="cersai-hero__pulse-dot"></span>
|
||||
Live
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Client basic info -->
|
||||
<div class="ckyc-info-grid">
|
||||
<div class="ckyc-info-item">
|
||||
<span class="ckyc-info-label"> <fa-icon icon="hashtag"></fa-icon> CIF Number </span>
|
||||
<span class="ckyc-info-value ckyc-info-value--mono">{{ cifNumber || clientId }}</span>
|
||||
</div>
|
||||
|
||||
<div class="ckyc-info-item">
|
||||
<span class="ckyc-info-label"> <fa-icon icon="user"></fa-icon> Full Name </span>
|
||||
<span class="ckyc-info-value">{{ clientName || '—' }}</span>
|
||||
</div>
|
||||
|
||||
<div class="ckyc-info-item">
|
||||
<span class="ckyc-info-label"> <fa-icon icon="envelope"></fa-icon> Email </span>
|
||||
@if (clientEmail) {
|
||||
<span class="ckyc-info-value">{{ clientEmail }}</span>
|
||||
} @else {
|
||||
<span class="ckyc-info-value ckyc-info-value--missing">No email on file</span>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div class="ckyc-info-item">
|
||||
<span class="ckyc-info-label"> <fa-icon icon="phone"></fa-icon> Mobile </span>
|
||||
@if (clientMobile) {
|
||||
<span class="ckyc-info-value">{{ clientMobile }}</span>
|
||||
} @else {
|
||||
<span class="ckyc-info-value ckyc-info-value--missing">No mobile on file</span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Document Upload Section -->
|
||||
<h4 class="ckyc-section-title">{{ 'labels.heading.CKYC Documents' | translate }}</h4>
|
||||
|
||||
@if (checkingExisting) {
|
||||
<div class="ckyc-existing-loading">
|
||||
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
|
||||
<span>Checking for already-uploaded artefacts…</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="ckyc-upload-grid">
|
||||
<!-- Address Proof Document -->
|
||||
<mat-card class="ckyc-upload-card">
|
||||
@@ -78,64 +126,101 @@
|
||||
<mat-icon class="ckyc-upload-icon">home</mat-icon>
|
||||
<span>{{ 'labels.inputs.Address Proof Document' | translate }}</span>
|
||||
</div>
|
||||
<p class="ckyc-upload-hint">{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG, PDF</p>
|
||||
<div class="ckyc-file-row">
|
||||
<input
|
||||
type="file"
|
||||
#addressProofInput
|
||||
accept=".png,.jpeg,.jpg,.pdf"
|
||||
(change)="onFileSelect($event, 'ADDRESS_PROOF')"
|
||||
[style.display]="'none'"
|
||||
/>
|
||||
<mat-form-field appearance="outline" class="ckyc-file-name-field">
|
||||
<input
|
||||
matInput
|
||||
[readonly]="true"
|
||||
[value]="addressProofFile?.name || ''"
|
||||
[placeholder]="'labels.text.NoFileSelected' | translate"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button mat-stroked-button type="button" (click)="addressProofInput.click()">
|
||||
<fa-icon icon="folder-open" size="lg"></fa-icon>
|
||||
{{ 'labels.buttons.Browse' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
[disabled]="!addressProofFile || uploading['ADDRESS_PROOF']"
|
||||
(click)="uploadDocument('ADDRESS_PROOF')"
|
||||
>
|
||||
@if (uploading['ADDRESS_PROOF']) {
|
||||
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
{{ 'labels.buttons.Upload' | translate }}
|
||||
}
|
||||
</button>
|
||||
@if (uploadErrors['ADDRESS_PROOF']) {
|
||||
<div class="ckyc-upload-error">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
<span>{{ uploadErrors['ADDRESS_PROOF'] }}</span>
|
||||
|
||||
@if (existing.ADDRESS_PROOF.present) {
|
||||
<div class="ckyc-existing">
|
||||
<div class="ckyc-existing__head">
|
||||
<fa-icon icon="check-circle" class="ckyc-existing__ok"></fa-icon>
|
||||
<div>
|
||||
<strong>Already available</strong>
|
||||
<span class="ckyc-existing__source">from {{ existing.ADDRESS_PROOF.source }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ckyc-existing__meta">
|
||||
<fa-icon icon="file-alt"></fa-icon>
|
||||
<span>{{ existing.ADDRESS_PROOF.name }}</span>
|
||||
@if (existing.ADDRESS_PROOF.documentKey) {
|
||||
<span class="ckyc-existing__key">{{ existing.ADDRESS_PROOF.documentKey }}</span>
|
||||
}
|
||||
@if (existing.ADDRESS_PROOF.fileCount && existing.ADDRESS_PROOF.fileCount > 0) {
|
||||
<span class="ckyc-existing__count">
|
||||
{{ existing.ADDRESS_PROOF.fileCount }}
|
||||
{{ existing.ADDRESS_PROOF.fileCount === 1 ? 'file' : 'files' }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
class="ckyc-existing__replace"
|
||||
(click)="toggleReplace('ADDRESS_PROOF')"
|
||||
>
|
||||
<fa-icon [icon]="existing.ADDRESS_PROOF.expanded ? 'times' : 'upload'"></fa-icon>
|
||||
{{ existing.ADDRESS_PROOF.expanded ? 'Cancel replace' : 'Upload new version' }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (uploadResponses['ADDRESS_PROOF']) {
|
||||
<div class="ckyc-upload-response">
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Status' | translate }}:</b> {{ uploadResponses['ADDRESS_PROOF'].status }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Request ID' | translate }}:</b> {{ uploadResponses['ADDRESS_PROOF'].requestId }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Acknowledgement Number' | translate }}:</b>
|
||||
{{ uploadResponses['ADDRESS_PROOF'].acknowledgementNumber }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Processing Status' | translate }}:</b>
|
||||
{{ uploadResponses['ADDRESS_PROOF'].processingStatus }}
|
||||
</p>
|
||||
|
||||
@if (!existing.ADDRESS_PROOF.present || existing.ADDRESS_PROOF.expanded) {
|
||||
<p class="ckyc-upload-hint">{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG, PDF</p>
|
||||
<div class="ckyc-file-row">
|
||||
<input
|
||||
type="file"
|
||||
#addressProofInput
|
||||
accept=".png,.jpeg,.jpg,.pdf"
|
||||
(change)="onFileSelect($event, 'ADDRESS_PROOF')"
|
||||
[style.display]="'none'"
|
||||
/>
|
||||
<mat-form-field appearance="outline" class="ckyc-file-name-field">
|
||||
<input
|
||||
matInput
|
||||
[readonly]="true"
|
||||
[value]="addressProofFile?.name || ''"
|
||||
[placeholder]="'labels.text.NoFileSelected' | translate"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button mat-stroked-button type="button" (click)="addressProofInput.click()">
|
||||
<fa-icon icon="folder-open" size="lg"></fa-icon>
|
||||
{{ 'labels.buttons.Browse' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
[disabled]="!addressProofFile || uploading['ADDRESS_PROOF']"
|
||||
(click)="uploadDocument('ADDRESS_PROOF')"
|
||||
>
|
||||
@if (uploading['ADDRESS_PROOF']) {
|
||||
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
{{ 'labels.buttons.Upload' | translate }}
|
||||
}
|
||||
</button>
|
||||
@if (uploadErrors['ADDRESS_PROOF']) {
|
||||
<div class="ckyc-upload-error">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
<span>{{ uploadErrors['ADDRESS_PROOF'] }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (uploadResponses['ADDRESS_PROOF']) {
|
||||
<div class="ckyc-upload-response">
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Status' | translate }}:</b> {{ uploadResponses['ADDRESS_PROOF'].status }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Request ID' | translate }}:</b> {{ uploadResponses['ADDRESS_PROOF'].requestId }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Acknowledgement Number' | translate }}:</b>
|
||||
{{ uploadResponses['ADDRESS_PROOF'].acknowledgementNumber }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Processing Status' | translate }}:</b>
|
||||
{{ uploadResponses['ADDRESS_PROOF'].processingStatus }}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -147,64 +232,100 @@
|
||||
<mat-icon class="ckyc-upload-icon">photo_camera</mat-icon>
|
||||
<span>{{ 'labels.inputs.Photo Proof Document' | translate }}</span>
|
||||
</div>
|
||||
<p class="ckyc-upload-hint">{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG, PDF</p>
|
||||
<div class="ckyc-file-row">
|
||||
<input
|
||||
type="file"
|
||||
#photoProofInput
|
||||
accept=".png,.jpeg,.jpg,.pdf"
|
||||
(change)="onFileSelect($event, 'PHOTO_PROOF')"
|
||||
[style.display]="'none'"
|
||||
/>
|
||||
<mat-form-field appearance="outline" class="ckyc-file-name-field">
|
||||
<input
|
||||
matInput
|
||||
[readonly]="true"
|
||||
[value]="photoProofFile?.name || ''"
|
||||
[placeholder]="'labels.text.NoFileSelected' | translate"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button mat-stroked-button type="button" (click)="photoProofInput.click()">
|
||||
<fa-icon icon="folder-open" size="lg"></fa-icon>
|
||||
{{ 'labels.buttons.Browse' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
[disabled]="!photoProofFile || uploading['PHOTO_PROOF']"
|
||||
(click)="uploadDocument('PHOTO_PROOF')"
|
||||
>
|
||||
@if (uploading['PHOTO_PROOF']) {
|
||||
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
{{ 'labels.buttons.Upload' | translate }}
|
||||
}
|
||||
</button>
|
||||
@if (uploadErrors['PHOTO_PROOF']) {
|
||||
<div class="ckyc-upload-error">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
<span>{{ uploadErrors['PHOTO_PROOF'] }}</span>
|
||||
|
||||
@if (existing.PHOTO_PROOF.present) {
|
||||
<div class="ckyc-existing">
|
||||
<div class="ckyc-existing__head">
|
||||
<fa-icon icon="check-circle" class="ckyc-existing__ok"></fa-icon>
|
||||
<div>
|
||||
<strong>Already available</strong>
|
||||
<span class="ckyc-existing__source">from {{ existing.PHOTO_PROOF.source }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="ckyc-existing__meta">
|
||||
<fa-icon icon="id-card"></fa-icon>
|
||||
<span>{{ existing.PHOTO_PROOF.name }}</span>
|
||||
@if (existing.PHOTO_PROOF.documentKey) {
|
||||
<span class="ckyc-existing__key">{{ existing.PHOTO_PROOF.documentKey }}</span>
|
||||
}
|
||||
@if (existing.PHOTO_PROOF.fileCount && existing.PHOTO_PROOF.fileCount > 0) {
|
||||
<span class="ckyc-existing__count">
|
||||
{{ existing.PHOTO_PROOF.fileCount }} {{ existing.PHOTO_PROOF.fileCount === 1 ? 'file' : 'files' }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
class="ckyc-existing__replace"
|
||||
(click)="toggleReplace('PHOTO_PROOF')"
|
||||
>
|
||||
<fa-icon [icon]="existing.PHOTO_PROOF.expanded ? 'times' : 'upload'"></fa-icon>
|
||||
{{ existing.PHOTO_PROOF.expanded ? 'Cancel replace' : 'Upload new version' }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (uploadResponses['PHOTO_PROOF']) {
|
||||
<div class="ckyc-upload-response">
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Status' | translate }}:</b> {{ uploadResponses['PHOTO_PROOF'].status }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Request ID' | translate }}:</b> {{ uploadResponses['PHOTO_PROOF'].requestId }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Acknowledgement Number' | translate }}:</b>
|
||||
{{ uploadResponses['PHOTO_PROOF'].acknowledgementNumber }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Processing Status' | translate }}:</b>
|
||||
{{ uploadResponses['PHOTO_PROOF'].processingStatus }}
|
||||
</p>
|
||||
|
||||
@if (!existing.PHOTO_PROOF.present || existing.PHOTO_PROOF.expanded) {
|
||||
<p class="ckyc-upload-hint">{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG, PDF</p>
|
||||
<div class="ckyc-file-row">
|
||||
<input
|
||||
type="file"
|
||||
#photoProofInput
|
||||
accept=".png,.jpeg,.jpg,.pdf"
|
||||
(change)="onFileSelect($event, 'PHOTO_PROOF')"
|
||||
[style.display]="'none'"
|
||||
/>
|
||||
<mat-form-field appearance="outline" class="ckyc-file-name-field">
|
||||
<input
|
||||
matInput
|
||||
[readonly]="true"
|
||||
[value]="photoProofFile?.name || ''"
|
||||
[placeholder]="'labels.text.NoFileSelected' | translate"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button mat-stroked-button type="button" (click)="photoProofInput.click()">
|
||||
<fa-icon icon="folder-open" size="lg"></fa-icon>
|
||||
{{ 'labels.buttons.Browse' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
[disabled]="!photoProofFile || uploading['PHOTO_PROOF']"
|
||||
(click)="uploadDocument('PHOTO_PROOF')"
|
||||
>
|
||||
@if (uploading['PHOTO_PROOF']) {
|
||||
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
{{ 'labels.buttons.Upload' | translate }}
|
||||
}
|
||||
</button>
|
||||
@if (uploadErrors['PHOTO_PROOF']) {
|
||||
<div class="ckyc-upload-error">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
<span>{{ uploadErrors['PHOTO_PROOF'] }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (uploadResponses['PHOTO_PROOF']) {
|
||||
<div class="ckyc-upload-response">
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Status' | translate }}:</b> {{ uploadResponses['PHOTO_PROOF'].status }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Request ID' | translate }}:</b> {{ uploadResponses['PHOTO_PROOF'].requestId }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Acknowledgement Number' | translate }}:</b>
|
||||
{{ uploadResponses['PHOTO_PROOF'].acknowledgementNumber }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Processing Status' | translate }}:</b>
|
||||
{{ uploadResponses['PHOTO_PROOF'].processingStatus }}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -216,64 +337,91 @@
|
||||
<mat-icon class="ckyc-upload-icon">person</mat-icon>
|
||||
<span>{{ 'labels.inputs.Image of Client' | translate }}</span>
|
||||
</div>
|
||||
<p class="ckyc-upload-hint">{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG</p>
|
||||
<div class="ckyc-file-row">
|
||||
<input
|
||||
type="file"
|
||||
#clientImageInput
|
||||
accept=".png,.jpeg,.jpg"
|
||||
(change)="onFileSelect($event, 'CLIENT_IMAGE')"
|
||||
[style.display]="'none'"
|
||||
/>
|
||||
<mat-form-field appearance="outline" class="ckyc-file-name-field">
|
||||
<input
|
||||
matInput
|
||||
[readonly]="true"
|
||||
[value]="clientImageFile?.name || ''"
|
||||
[placeholder]="'labels.text.NoFileSelected' | translate"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button mat-stroked-button type="button" (click)="clientImageInput.click()">
|
||||
<fa-icon icon="folder-open" size="lg"></fa-icon>
|
||||
{{ 'labels.buttons.Browse' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
[disabled]="!clientImageFile || uploading['CLIENT_IMAGE']"
|
||||
(click)="uploadDocument('CLIENT_IMAGE')"
|
||||
>
|
||||
@if (uploading['CLIENT_IMAGE']) {
|
||||
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
{{ 'labels.buttons.Upload' | translate }}
|
||||
}
|
||||
</button>
|
||||
@if (uploadErrors['CLIENT_IMAGE']) {
|
||||
<div class="ckyc-upload-error">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
<span>{{ uploadErrors['CLIENT_IMAGE'] }}</span>
|
||||
|
||||
@if (existing.CLIENT_IMAGE.present) {
|
||||
<div class="ckyc-existing">
|
||||
<div class="ckyc-existing__head">
|
||||
<fa-icon icon="check-circle" class="ckyc-existing__ok"></fa-icon>
|
||||
<div>
|
||||
<strong>Already available</strong>
|
||||
<span class="ckyc-existing__source">from {{ existing.CLIENT_IMAGE.source }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@if (existing.CLIENT_IMAGE.previewUrl) {
|
||||
<img [src]="existing.CLIENT_IMAGE.previewUrl" alt="Client photo" class="ckyc-existing__thumb" />
|
||||
}
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
class="ckyc-existing__replace"
|
||||
(click)="toggleReplace('CLIENT_IMAGE')"
|
||||
>
|
||||
<fa-icon [icon]="existing.CLIENT_IMAGE.expanded ? 'times' : 'upload'"></fa-icon>
|
||||
{{ existing.CLIENT_IMAGE.expanded ? 'Cancel replace' : 'Upload new version' }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (uploadResponses['CLIENT_IMAGE']) {
|
||||
<div class="ckyc-upload-response">
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Status' | translate }}:</b> {{ uploadResponses['CLIENT_IMAGE'].status }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Request ID' | translate }}:</b> {{ uploadResponses['CLIENT_IMAGE'].requestId }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Acknowledgement Number' | translate }}:</b>
|
||||
{{ uploadResponses['CLIENT_IMAGE'].acknowledgementNumber }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Processing Status' | translate }}:</b>
|
||||
{{ uploadResponses['CLIENT_IMAGE'].processingStatus }}
|
||||
</p>
|
||||
|
||||
@if (!existing.CLIENT_IMAGE.present || existing.CLIENT_IMAGE.expanded) {
|
||||
<p class="ckyc-upload-hint">{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG</p>
|
||||
<div class="ckyc-file-row">
|
||||
<input
|
||||
type="file"
|
||||
#clientImageInput
|
||||
accept=".png,.jpeg,.jpg"
|
||||
(change)="onFileSelect($event, 'CLIENT_IMAGE')"
|
||||
[style.display]="'none'"
|
||||
/>
|
||||
<mat-form-field appearance="outline" class="ckyc-file-name-field">
|
||||
<input
|
||||
matInput
|
||||
[readonly]="true"
|
||||
[value]="clientImageFile?.name || ''"
|
||||
[placeholder]="'labels.text.NoFileSelected' | translate"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button mat-stroked-button type="button" (click)="clientImageInput.click()">
|
||||
<fa-icon icon="folder-open" size="lg"></fa-icon>
|
||||
{{ 'labels.buttons.Browse' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
[disabled]="!clientImageFile || uploading['CLIENT_IMAGE']"
|
||||
(click)="uploadDocument('CLIENT_IMAGE')"
|
||||
>
|
||||
@if (uploading['CLIENT_IMAGE']) {
|
||||
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
{{ 'labels.buttons.Upload' | translate }}
|
||||
}
|
||||
</button>
|
||||
@if (uploadErrors['CLIENT_IMAGE']) {
|
||||
<div class="ckyc-upload-error">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
<span>{{ uploadErrors['CLIENT_IMAGE'] }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (uploadResponses['CLIENT_IMAGE']) {
|
||||
<div class="ckyc-upload-response">
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Status' | translate }}:</b> {{ uploadResponses['CLIENT_IMAGE'].status }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Request ID' | translate }}:</b> {{ uploadResponses['CLIENT_IMAGE'].requestId }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Acknowledgement Number' | translate }}:</b>
|
||||
{{ uploadResponses['CLIENT_IMAGE'].acknowledgementNumber }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Processing Status' | translate }}:</b>
|
||||
{{ uploadResponses['CLIENT_IMAGE'].processingStatus }}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
@@ -285,64 +433,100 @@
|
||||
<mat-icon class="ckyc-upload-icon">draw</mat-icon>
|
||||
<span>{{ 'labels.inputs.Signature of Client' | translate }}</span>
|
||||
</div>
|
||||
<p class="ckyc-upload-hint">{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG</p>
|
||||
<div class="ckyc-file-row">
|
||||
<input
|
||||
type="file"
|
||||
#clientSignatureInput
|
||||
accept=".png,.jpeg,.jpg"
|
||||
(change)="onFileSelect($event, 'CLIENT_SIGNATURE')"
|
||||
[style.display]="'none'"
|
||||
/>
|
||||
<mat-form-field appearance="outline" class="ckyc-file-name-field">
|
||||
<input
|
||||
matInput
|
||||
[readonly]="true"
|
||||
[value]="clientSignatureFile?.name || ''"
|
||||
[placeholder]="'labels.text.NoFileSelected' | translate"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button mat-stroked-button type="button" (click)="clientSignatureInput.click()">
|
||||
<fa-icon icon="folder-open" size="lg"></fa-icon>
|
||||
{{ 'labels.buttons.Browse' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
[disabled]="!clientSignatureFile || uploading['CLIENT_SIGNATURE']"
|
||||
(click)="uploadDocument('CLIENT_SIGNATURE')"
|
||||
>
|
||||
@if (uploading['CLIENT_SIGNATURE']) {
|
||||
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
{{ 'labels.buttons.Upload' | translate }}
|
||||
}
|
||||
</button>
|
||||
@if (uploadErrors['CLIENT_SIGNATURE']) {
|
||||
<div class="ckyc-upload-error">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
<span>{{ uploadErrors['CLIENT_SIGNATURE'] }}</span>
|
||||
|
||||
@if (existing.CLIENT_SIGNATURE.present) {
|
||||
<div class="ckyc-existing">
|
||||
<div class="ckyc-existing__head">
|
||||
<fa-icon icon="check-circle" class="ckyc-existing__ok"></fa-icon>
|
||||
<div>
|
||||
<strong>Already available</strong>
|
||||
<span class="ckyc-existing__source">from {{ existing.CLIENT_SIGNATURE.source }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@if (existing.CLIENT_SIGNATURE.previewUrl) {
|
||||
<img
|
||||
[src]="existing.CLIENT_SIGNATURE.previewUrl"
|
||||
alt="Client signature"
|
||||
class="ckyc-existing__thumb ckyc-existing__thumb--wide"
|
||||
/>
|
||||
}
|
||||
<div class="ckyc-existing__meta">
|
||||
<fa-icon icon="file-signature"></fa-icon>
|
||||
<span>{{ existing.CLIENT_SIGNATURE.name }}</span>
|
||||
</div>
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
class="ckyc-existing__replace"
|
||||
(click)="toggleReplace('CLIENT_SIGNATURE')"
|
||||
>
|
||||
<fa-icon [icon]="existing.CLIENT_SIGNATURE.expanded ? 'times' : 'upload'"></fa-icon>
|
||||
{{ existing.CLIENT_SIGNATURE.expanded ? 'Cancel replace' : 'Upload new version' }}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@if (uploadResponses['CLIENT_SIGNATURE']) {
|
||||
<div class="ckyc-upload-response">
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Status' | translate }}:</b> {{ uploadResponses['CLIENT_SIGNATURE'].status }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Request ID' | translate }}:</b> {{ uploadResponses['CLIENT_SIGNATURE'].requestId }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Acknowledgement Number' | translate }}:</b>
|
||||
{{ uploadResponses['CLIENT_SIGNATURE'].acknowledgementNumber }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Processing Status' | translate }}:</b>
|
||||
{{ uploadResponses['CLIENT_SIGNATURE'].processingStatus }}
|
||||
</p>
|
||||
|
||||
@if (!existing.CLIENT_SIGNATURE.present || existing.CLIENT_SIGNATURE.expanded) {
|
||||
<p class="ckyc-upload-hint">{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG</p>
|
||||
<div class="ckyc-file-row">
|
||||
<input
|
||||
type="file"
|
||||
#clientSignatureInput
|
||||
accept=".png,.jpeg,.jpg"
|
||||
(change)="onFileSelect($event, 'CLIENT_SIGNATURE')"
|
||||
[style.display]="'none'"
|
||||
/>
|
||||
<mat-form-field appearance="outline" class="ckyc-file-name-field">
|
||||
<input
|
||||
matInput
|
||||
[readonly]="true"
|
||||
[value]="clientSignatureFile?.name || ''"
|
||||
[placeholder]="'labels.text.NoFileSelected' | translate"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button mat-stroked-button type="button" (click)="clientSignatureInput.click()">
|
||||
<fa-icon icon="folder-open" size="lg"></fa-icon>
|
||||
{{ 'labels.buttons.Browse' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
[disabled]="!clientSignatureFile || uploading['CLIENT_SIGNATURE']"
|
||||
(click)="uploadDocument('CLIENT_SIGNATURE')"
|
||||
>
|
||||
@if (uploading['CLIENT_SIGNATURE']) {
|
||||
<mat-progress-spinner diameter="18" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
{{ 'labels.buttons.Upload' | translate }}
|
||||
}
|
||||
</button>
|
||||
@if (uploadErrors['CLIENT_SIGNATURE']) {
|
||||
<div class="ckyc-upload-error">
|
||||
<mat-icon>error_outline</mat-icon>
|
||||
<span>{{ uploadErrors['CLIENT_SIGNATURE'] }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (uploadResponses['CLIENT_SIGNATURE']) {
|
||||
<div class="ckyc-upload-response">
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Status' | translate }}:</b> {{ uploadResponses['CLIENT_SIGNATURE'].status }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Request ID' | translate }}:</b>
|
||||
{{ uploadResponses['CLIENT_SIGNATURE'].requestId }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Acknowledgement Number' | translate }}:</b>
|
||||
{{ uploadResponses['CLIENT_SIGNATURE'].acknowledgementNumber }}
|
||||
</p>
|
||||
<p>
|
||||
<b>{{ 'labels.inputs.Processing Status' | translate }}:</b>
|
||||
{{ uploadResponses['CLIENT_SIGNATURE'].processingStatus }}
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
@@ -49,35 +49,266 @@
|
||||
line-height: 18px;
|
||||
}
|
||||
|
||||
.ckyc-info-card {
|
||||
margin-bottom: 24px;
|
||||
/* ===================== CERSAI Sync Hero ===================== */
|
||||
.cersai-hero {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 20px;
|
||||
padding: 22px 26px;
|
||||
border-radius: 16px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 18px;
|
||||
color: #fff;
|
||||
box-shadow: 0 6px 22px rgb(0 0 0 / 10%);
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -60%;
|
||||
right: -10%;
|
||||
width: 320px;
|
||||
height: 320px;
|
||||
border-radius: 50%;
|
||||
background: radial-gradient(circle, rgb(255 255 255 / 18%), transparent 70%);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
&--synced {
|
||||
background: linear-gradient(135deg, #0f6e56 0%, #1d9e75 55%, #2fb88c 100%);
|
||||
}
|
||||
|
||||
&--pending {
|
||||
background: linear-gradient(135deg, #8a5a14 0%, #ba7517 55%, #d4892f 100%);
|
||||
}
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
min-width: 0;
|
||||
position: relative;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 60px;
|
||||
height: 60px;
|
||||
border-radius: 16px;
|
||||
background: rgb(255 255 255 / 20%);
|
||||
border: 1px solid rgb(255 255 255 / 35%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 26px;
|
||||
flex-shrink: 0;
|
||||
backdrop-filter: blur(10px);
|
||||
}
|
||||
|
||||
&__text {
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__tag {
|
||||
display: inline-block;
|
||||
padding: 4px 12px;
|
||||
border-radius: 20px;
|
||||
background: rgb(255 255 255 / 22%);
|
||||
border: 1px solid rgb(255 255 255 / 40%);
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.8px;
|
||||
text-transform: uppercase;
|
||||
backdrop-filter: blur(8px);
|
||||
}
|
||||
|
||||
&__ckyc-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
opacity: 0.9;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin: 10px 0 4px;
|
||||
}
|
||||
|
||||
&__ckyc-number {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
font-family: 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
letter-spacing: 0.5px;
|
||||
line-height: 1.1;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
opacity: 0.95;
|
||||
flex-wrap: wrap;
|
||||
|
||||
fa-icon {
|
||||
font-size: 11px;
|
||||
opacity: 0.9;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
|
||||
&__sep {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
&__hint {
|
||||
font-size: 12px;
|
||||
opacity: 0.9;
|
||||
line-height: 1.5;
|
||||
max-width: 480px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
&__pulse {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 14px;
|
||||
background: rgb(255 255 255 / 20%);
|
||||
border: 1px solid rgb(255 255 255 / 35%);
|
||||
border-radius: 20px;
|
||||
font-size: 11px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.5px;
|
||||
backdrop-filter: blur(8px);
|
||||
flex-shrink: 0;
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
&__pulse-dot {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
border-radius: 50%;
|
||||
background: #9fe1cb;
|
||||
box-shadow: 0 0 0 0 rgb(159 225 203 / 70%);
|
||||
animation: cersai-pulse 2s infinite;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes cersai-pulse {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgb(159 225 203 / 70%);
|
||||
}
|
||||
|
||||
70% {
|
||||
box-shadow: 0 0 0 10px rgb(159 225 203 / 0%);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 0 rgb(159 225 203 / 0%);
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== Client info grid ===================== */
|
||||
.ckyc-info-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 16px;
|
||||
align-items: start;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.ckyc-info-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
padding: 14px 16px;
|
||||
background: #fff;
|
||||
border: 1px solid #e8eaed;
|
||||
border-radius: 12px;
|
||||
min-width: 0;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: #c6dafc;
|
||||
box-shadow: 0 2px 8px rgb(26 115 232 / 6%);
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 900px) {
|
||||
.ckyc-info-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.cersai-hero {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
|
||||
&__left {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
&__ckyc-number {
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 500px) {
|
||||
.ckyc-info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.ckyc-info-label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #888;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: #5f6368;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
letter-spacing: 0.06em;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
fa-icon {
|
||||
color: #1a73e8;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.ckyc-info-value {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
min-height: 20px;
|
||||
font-size: 13px;
|
||||
color: #202124;
|
||||
font-weight: 600;
|
||||
min-height: 18px;
|
||||
word-break: break-word;
|
||||
|
||||
&--mono {
|
||||
font-family: 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 12px;
|
||||
color: #1a73e8;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
&--missing {
|
||||
color: #9aa0a6;
|
||||
font-weight: 500;
|
||||
font-style: italic;
|
||||
}
|
||||
}
|
||||
|
||||
.ckyc-info-hint {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.ckyc-section-title {
|
||||
@@ -87,31 +318,60 @@
|
||||
|
||||
.ckyc-upload-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 16px;
|
||||
margin-bottom: 16px;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.ckyc-upload-card {
|
||||
border-radius: 12px !important;
|
||||
border: 1px solid #e8eaed;
|
||||
box-shadow: none !important;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
box-shadow 0.15s;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 240px;
|
||||
|
||||
&:hover {
|
||||
border-color: #c6dafc;
|
||||
box-shadow: 0 2px 10px rgb(26 115 232 / 6%) !important;
|
||||
}
|
||||
|
||||
.mat-mdc-card-content {
|
||||
padding: 16px;
|
||||
padding: 18px 20px !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.ckyc-upload-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
margin-bottom: 8px;
|
||||
color: #202124;
|
||||
padding-bottom: 12px;
|
||||
margin-bottom: 0;
|
||||
border-bottom: 1px solid #f1f3f4;
|
||||
}
|
||||
|
||||
.ckyc-upload-icon {
|
||||
font-size: 20px;
|
||||
height: 20px;
|
||||
width: 20px;
|
||||
color: #1976d2;
|
||||
font-size: 18px;
|
||||
height: 32px;
|
||||
width: 32px;
|
||||
color: #fff;
|
||||
background: linear-gradient(135deg, #1a73e8, #4285f4);
|
||||
border-radius: 8px;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
box-shadow: 0 2px 6px rgb(26 115 232 / 25%);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ckyc-upload-hint {
|
||||
@@ -163,6 +423,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 900px) {
|
||||
.ckyc-upload-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
.ckyc-info-grid {
|
||||
grid-template-columns: 1fr;
|
||||
@@ -180,3 +446,157 @@
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== Already-available banners ===================== */
|
||||
|
||||
.ckyc-existing-loading {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 14px;
|
||||
background: #e8f0fe;
|
||||
color: #0c447c;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
margin-bottom: 14px;
|
||||
|
||||
mat-progress-spinner {
|
||||
display: inline-block;
|
||||
}
|
||||
}
|
||||
|
||||
.ckyc-existing {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding: 14px 14px 12px;
|
||||
background: #f0fbf6;
|
||||
border: 1px solid #cfecdd;
|
||||
border-radius: 10px;
|
||||
margin-top: 14px;
|
||||
flex: 1;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0 auto 0 0;
|
||||
width: 3px;
|
||||
background: #1d9e75;
|
||||
border-radius: 10px 0 0 10px;
|
||||
}
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
strong {
|
||||
display: block;
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #085041;
|
||||
line-height: 1.2;
|
||||
}
|
||||
}
|
||||
|
||||
&__ok {
|
||||
font-size: 20px;
|
||||
color: #1d9e75;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__source {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
color: #0f6e56;
|
||||
margin-top: 2px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 12px;
|
||||
color: #085041;
|
||||
flex-wrap: wrap;
|
||||
padding: 8px 10px;
|
||||
background: #fff;
|
||||
border: 1px solid #cfecdd;
|
||||
border-radius: 8px;
|
||||
|
||||
> span:first-of-type {
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
fa-icon {
|
||||
color: #1d9e75;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
&__key {
|
||||
font-family: 'SF Mono', Consolas, monospace;
|
||||
font-size: 11px;
|
||||
background: #e1f5ee;
|
||||
color: #0f6e56;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #cfecdd;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
&__count {
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
background: #1d9e75;
|
||||
color: #fff;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
|
||||
&__thumb {
|
||||
width: 100px;
|
||||
height: 100px;
|
||||
border-radius: 10px;
|
||||
border: 2px solid #fff;
|
||||
outline: 1px solid #cfecdd;
|
||||
background: #fff;
|
||||
object-fit: cover;
|
||||
align-self: center;
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 6%);
|
||||
|
||||
&--wide {
|
||||
width: 200px;
|
||||
height: 80px;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
&__replace {
|
||||
align-self: stretch;
|
||||
margin-top: auto;
|
||||
height: 34px !important;
|
||||
font-size: 11px !important;
|
||||
font-weight: 600 !important;
|
||||
border-radius: 8px !important;
|
||||
border: 1px solid #cfecdd !important;
|
||||
color: #0f6e56 !important;
|
||||
background: #fff !important;
|
||||
padding: 0 12px !important;
|
||||
letter-spacing: 0.2px;
|
||||
|
||||
fa-icon {
|
||||
margin-right: 6px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #e1f5ee !important;
|
||||
border-color: #1d9e75 !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
/** Angular Imports */
|
||||
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { Subject, takeUntil, catchError, of } from 'rxjs';
|
||||
import { Subject, takeUntil, catchError, of, forkJoin } from 'rxjs';
|
||||
|
||||
/** Custom Services */
|
||||
import { ClientsService } from '../../clients.service';
|
||||
@@ -19,6 +19,17 @@ import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
|
||||
interface ExistingArtefact {
|
||||
present: boolean;
|
||||
name?: string;
|
||||
source?: string;
|
||||
previewUrl?: string;
|
||||
documentId?: string;
|
||||
documentKey?: string;
|
||||
fileCount?: number;
|
||||
expanded?: boolean;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-ckyc-tab',
|
||||
templateUrl: './ckyc-tab.component.html',
|
||||
@@ -41,6 +52,10 @@ export class CkycTabComponent implements OnInit, OnDestroy {
|
||||
/** Top section - read-only client info */
|
||||
clientId: string;
|
||||
clientName: string = '';
|
||||
clientEmail: string = '';
|
||||
clientMobile: string = '';
|
||||
cifNumber: string = '';
|
||||
ckycNumber: string = '';
|
||||
allAccounts: any[] = [];
|
||||
selectedAccount: any = null;
|
||||
selectedAccountType: string = '';
|
||||
@@ -48,6 +63,17 @@ export class CkycTabComponent implements OnInit, OnDestroy {
|
||||
/** State */
|
||||
loading = true;
|
||||
ckycData: any = null;
|
||||
ckycUploadedOn = '';
|
||||
ckycBatchId = '';
|
||||
checkingExisting = false;
|
||||
|
||||
/** Pre-existing artefacts detected from client data */
|
||||
existing: Record<'ADDRESS_PROOF' | 'PHOTO_PROOF' | 'CLIENT_IMAGE' | 'CLIENT_SIGNATURE', ExistingArtefact> = {
|
||||
ADDRESS_PROOF: { present: false },
|
||||
PHOTO_PROOF: { present: false },
|
||||
CLIENT_IMAGE: { present: false },
|
||||
CLIENT_SIGNATURE: { present: false }
|
||||
};
|
||||
|
||||
/** File references */
|
||||
addressProofFile: File | null = null;
|
||||
@@ -80,13 +106,18 @@ export class CkycTabComponent implements OnInit, OnDestroy {
|
||||
this.loadClientInfo();
|
||||
this.loadAccounts();
|
||||
this.loadCkycData();
|
||||
this.loadExistingArtefacts();
|
||||
});
|
||||
}
|
||||
|
||||
private loadClientInfo(): void {
|
||||
this.route.parent.data.subscribe((data: any) => {
|
||||
if (data?.clientViewData) {
|
||||
this.clientName = data.clientViewData.displayName || '';
|
||||
const cv = data?.clientViewData;
|
||||
if (cv) {
|
||||
this.clientName = cv.displayName || '';
|
||||
this.clientEmail = cv.emailAddress || '';
|
||||
this.clientMobile = cv.mobileNo || '';
|
||||
this.cifNumber = cv.accountNo || cv.externalId || '';
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -122,8 +153,10 @@ export class CkycTabComponent implements OnInit, OnDestroy {
|
||||
|
||||
private loadCkycData(): void {
|
||||
this.loading = true;
|
||||
// Read CKYC registry data from /clients/{id}/kyc — that's where the
|
||||
// SFTP upload flow persists ckycNumber / ckycUploadedOn / ckycBatchId.
|
||||
this.clientsService
|
||||
.getClientCkyc(this.clientId)
|
||||
.getClientKyc(this.clientId)
|
||||
.pipe(
|
||||
takeUntil(this.destroy$),
|
||||
catchError(() => {
|
||||
@@ -134,9 +167,120 @@ export class CkycTabComponent implements OnInit, OnDestroy {
|
||||
.subscribe((data: any) => {
|
||||
this.loading = false;
|
||||
this.ckycData = data;
|
||||
this.ckycNumber = data?.ckycNumber || '';
|
||||
this.ckycUploadedOn = this.formatJavaDate(data?.ckycUploadedOn);
|
||||
this.ckycBatchId = data?.ckycBatchId || '';
|
||||
});
|
||||
}
|
||||
|
||||
private formatJavaDate(value: any): string {
|
||||
if (!value) return '';
|
||||
if (Array.isArray(value) && value.length >= 3) {
|
||||
const [
|
||||
y,
|
||||
m,
|
||||
d
|
||||
] = value;
|
||||
return `${String(d).padStart(2, '0')}-${String(m).padStart(2, '0')}-${y}`;
|
||||
}
|
||||
if (typeof value === 'string') return value;
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch photo (profile image), signature (client document),
|
||||
* ID proof and address proof (client identifiers) so already-uploaded
|
||||
* artefacts are shown here instead of forcing re-upload.
|
||||
*/
|
||||
private loadExistingArtefacts(): void {
|
||||
this.checkingExisting = true;
|
||||
|
||||
forkJoin({
|
||||
photo: this.clientsService.getClientProfileImage(this.clientId).pipe(catchError(() => of(null))),
|
||||
documents: this.clientsService.getClientDocuments(this.clientId).pipe(catchError(() => of([]))),
|
||||
identifiers: this.clientsService.getClientIdentifiers(this.clientId).pipe(catchError(() => of([]))),
|
||||
proofTypes: this.clientsService.getKycProofTypes().pipe(catchError(() => of({ idProofs: [], addressProofs: [] })))
|
||||
})
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe(({ photo, documents, identifiers, proofTypes }) => {
|
||||
this.checkingExisting = false;
|
||||
|
||||
// 1. Photo / image of client — from m_client.image_id
|
||||
if (photo && typeof photo === 'string' && photo.length > 0) {
|
||||
this.existing.CLIENT_IMAGE = {
|
||||
present: true,
|
||||
source: 'Client registration',
|
||||
previewUrl: photo,
|
||||
name: 'Profile photo'
|
||||
};
|
||||
}
|
||||
|
||||
// 2. Signature — client document with name='clientSignature'
|
||||
const signatureDoc = (documents as any[])?.find((d: any) => d?.name === 'clientSignature');
|
||||
if (signatureDoc) {
|
||||
this.existing.CLIENT_SIGNATURE = {
|
||||
present: true,
|
||||
source: 'Client registration',
|
||||
name: signatureDoc.fileName || 'Client signature',
|
||||
documentId: signatureDoc.id
|
||||
};
|
||||
// Fetch blob for preview
|
||||
this.clientsService
|
||||
.getClientSignatureImage(this.clientId, signatureDoc.id)
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe({
|
||||
next: (blob: any) => {
|
||||
if (blob) {
|
||||
this.existing.CLIENT_SIGNATURE.previewUrl = URL.createObjectURL(blob);
|
||||
}
|
||||
},
|
||||
error: () => undefined
|
||||
});
|
||||
}
|
||||
|
||||
// 3 & 4. ID proof + Address proof — client identifiers classified via proof-types
|
||||
const idProofIds = new Set<string>((proofTypes?.idProofs || []).map((p: any) => String(p.id)));
|
||||
const addressProofIds = new Set<string>((proofTypes?.addressProofs || []).map((p: any) => String(p.id)));
|
||||
|
||||
const idList = Array.isArray(identifiers) ? identifiers : (identifiers as any)?.pageItems || [];
|
||||
|
||||
for (const identity of idList) {
|
||||
const docTypeId = String(identity?.documentType?.id ?? '');
|
||||
const isActive =
|
||||
!identity?.status ||
|
||||
identity?.status?.code === 'clientIdentifierStatusType.active' ||
|
||||
identity?.status === 'clientIdentifierStatusType.active' ||
|
||||
identity?.status === 'Active';
|
||||
if (!isActive) continue;
|
||||
|
||||
const proofName = identity?.documentType?.name || 'Identifier';
|
||||
// Address proof documentKey is auto-generated as "<ProofName> - <clientId>"
|
||||
// in identities-tab; hide it when it's just that synthetic key.
|
||||
const rawKey = identity?.documentKey || '';
|
||||
const isSyntheticKey = rawKey.startsWith(`${proofName} - `);
|
||||
const common = {
|
||||
present: true,
|
||||
source: 'Identities section',
|
||||
name: proofName,
|
||||
documentKey: isSyntheticKey ? '' : rawKey,
|
||||
documentId: identity?.id,
|
||||
fileCount: (identity?.documents || []).length
|
||||
};
|
||||
|
||||
if (idProofIds.has(docTypeId) && !this.existing.PHOTO_PROOF.present) {
|
||||
this.existing.PHOTO_PROOF = common;
|
||||
} else if (addressProofIds.has(docTypeId) && !this.existing.ADDRESS_PROOF.present) {
|
||||
this.existing.ADDRESS_PROOF = common;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleReplace(documentType: string): void {
|
||||
const entry = (this.existing as any)[documentType];
|
||||
if (entry) entry.expanded = !entry.expanded;
|
||||
}
|
||||
|
||||
onAccountSelect(accountNo: string): void {
|
||||
this.selectedAccount = this.allAccounts.find((a) => a.accountNo === accountNo) || null;
|
||||
this.selectedAccountType = this.selectedAccount?.accountType || '';
|
||||
@@ -215,5 +359,9 @@ export class CkycTabComponent implements OnInit, OnDestroy {
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
// Release object URLs
|
||||
Object.values(this.existing).forEach((e) => {
|
||||
if (e.previewUrl?.startsWith('blob:')) URL.revokeObjectURL(e.previewUrl);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,29 +10,36 @@
|
||||
<div class="cv-top">
|
||||
<!-- Avatar -->
|
||||
<div class="cv-avatar">
|
||||
<img class="cv-avatar__img"
|
||||
[src]="clientImage ? clientImage : 'assets/images/user_placeholder.png'" />
|
||||
<img class="cv-avatar__img" [src]="clientImage ? clientImage : 'assets/images/user_placeholder.png'" />
|
||||
<div class="cv-avatar__actions">
|
||||
<button class="cv-avatar__btn"
|
||||
<button
|
||||
class="cv-avatar__btn"
|
||||
(click)="doAction('Upload Image')"
|
||||
matTooltip="{{ 'tooltips.Upload Image' | translate }}"
|
||||
*mifosxHasPermission="'CREATE_CLIENTIMAGE'">
|
||||
*mifosxHasPermission="'CREATE_CLIENTIMAGE'"
|
||||
>
|
||||
<i class="fa fa-upload"></i>
|
||||
</button>
|
||||
<button class="cv-avatar__btn"
|
||||
<button
|
||||
class="cv-avatar__btn"
|
||||
(click)="doAction('Capture Image')"
|
||||
matTooltip="{{ 'tooltips.Capture Image' | translate }}"
|
||||
*mifosxHasPermission="'CREATE_CLIENTIMAGE'">
|
||||
*mifosxHasPermission="'CREATE_CLIENTIMAGE'"
|
||||
>
|
||||
<i class="fa fa-camera"></i>
|
||||
</button>
|
||||
<button class="cv-avatar__btn cv-avatar__btn--danger"
|
||||
<button
|
||||
class="cv-avatar__btn cv-avatar__btn--danger"
|
||||
(click)="doAction('Delete Image')"
|
||||
matTooltip="{{ 'tooltips.Delete Image' | translate }}"
|
||||
*mifosxHasPermission="'DELETE_CLIENTIMAGE'">
|
||||
*mifosxHasPermission="'DELETE_CLIENTIMAGE'"
|
||||
>
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
<span class="cv-avatar__sig" (click)="doAction('View Signature')">{{ 'labels.inputs.View Signature' | translate }}</span>
|
||||
<span class="cv-avatar__sig" (click)="doAction('View Signature')">{{
|
||||
'labels.inputs.View Signature' | translate
|
||||
}}</span>
|
||||
</div>
|
||||
|
||||
<!-- Info -->
|
||||
@@ -40,12 +47,19 @@
|
||||
<!-- Name + Status + Menu -->
|
||||
<div class="cv-name-row">
|
||||
<div class="cv-name">
|
||||
<i class="fa fa-stop cv-name__dot" [ngClass]="clientViewData.status.code | statusLookup"
|
||||
matTooltip="{{ 'labels.status.' + clientViewData.status.value | translate }}"></i>
|
||||
<i
|
||||
class="fa fa-stop cv-name__dot"
|
||||
[ngClass]="clientViewData.status.code | statusLookup"
|
||||
matTooltip="{{ 'labels.status.' + clientViewData.status.value | translate }}"
|
||||
></i>
|
||||
<span class="cv-name__text">
|
||||
<mifosx-entity-name
|
||||
[entityName]="complianceHideClientData ? maskName(clientViewData.displayName) : clientViewData.displayName"
|
||||
[display]="'right'" [hideCopy]="complianceHideClientData"></mifosx-entity-name>
|
||||
[entityName]="
|
||||
complianceHideClientData ? maskName(clientViewData.displayName) : clientViewData.displayName
|
||||
"
|
||||
[display]="'right'"
|
||||
[hideCopy]="complianceHideClientData"
|
||||
></mifosx-entity-name>
|
||||
</span>
|
||||
<span class="cv-name__status">
|
||||
{{ 'labels.status.' + clientViewData.status.value | translate }}
|
||||
@@ -77,8 +91,12 @@
|
||||
<span class="cv-id__label">{{ 'labels.inputs.External Id' | translate }}</span>
|
||||
<span class="cv-id__val">
|
||||
<mifosx-external-identifier
|
||||
[externalId]="complianceHideClientData ? maskValue(clientViewData.externalId) : clientViewData.externalId"
|
||||
[completed]="true" [hideCopy]="complianceHideClientData"></mifosx-external-identifier>
|
||||
[externalId]="
|
||||
complianceHideClientData ? maskValue(clientViewData.externalId) : clientViewData.externalId
|
||||
"
|
||||
[completed]="true"
|
||||
[hideCopy]="complianceHideClientData"
|
||||
></mifosx-external-identifier>
|
||||
</span>
|
||||
</div>
|
||||
}
|
||||
@@ -118,7 +136,9 @@
|
||||
<div class="cv-cell__value">
|
||||
<mifosx-external-identifier
|
||||
[externalId]="complianceHideClientData ? maskValue(clientViewData.mobileNo) : clientViewData.mobileNo"
|
||||
[completed]="true" [hideCopy]="complianceHideClientData"></mifosx-external-identifier>
|
||||
[completed]="true"
|
||||
[hideCopy]="complianceHideClientData"
|
||||
></mifosx-external-identifier>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -127,8 +147,12 @@
|
||||
<div class="cv-cell__label">{{ 'labels.inputs.Email' | translate }}</div>
|
||||
<div class="cv-cell__value">
|
||||
<mifosx-external-identifier
|
||||
[externalId]="complianceHideClientData ? maskEmail(clientViewData.emailAddress) : clientViewData.emailAddress"
|
||||
[completed]="true" [hideCopy]="complianceHideClientData"></mifosx-external-identifier>
|
||||
[externalId]="
|
||||
complianceHideClientData ? maskEmail(clientViewData.emailAddress) : clientViewData.emailAddress
|
||||
"
|
||||
[completed]="true"
|
||||
[hideCopy]="complianceHideClientData"
|
||||
></mifosx-external-identifier>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -148,10 +172,26 @@
|
||||
<div class="cv-cell">
|
||||
<div class="cv-cell__label">DIP Status</div>
|
||||
<div class="cv-cell__value">
|
||||
<span class="cv-badge"
|
||||
[class.cv-badge--green]="clientViewData.dipVerified === true || clientViewData.dipVerified === 'true' || clientViewData.dipVerified === 1"
|
||||
[class.cv-badge--yellow]="clientViewData.dipVerified !== true && clientViewData.dipVerified !== 'true' && clientViewData.dipVerified !== 1">
|
||||
{{ (clientViewData.dipVerified === true || clientViewData.dipVerified === 'true' || clientViewData.dipVerified === 1) ? 'Verified' : 'Pending' }}
|
||||
<span
|
||||
class="cv-badge"
|
||||
[class.cv-badge--green]="
|
||||
clientViewData.dipVerified === true ||
|
||||
clientViewData.dipVerified === 'true' ||
|
||||
clientViewData.dipVerified === 1
|
||||
"
|
||||
[class.cv-badge--yellow]="
|
||||
clientViewData.dipVerified !== true &&
|
||||
clientViewData.dipVerified !== 'true' &&
|
||||
clientViewData.dipVerified !== 1
|
||||
"
|
||||
>
|
||||
{{
|
||||
clientViewData.dipVerified === true ||
|
||||
clientViewData.dipVerified === 'true' ||
|
||||
clientViewData.dipVerified === 1
|
||||
? 'Verified'
|
||||
: 'Pending'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,10 +200,26 @@
|
||||
<div class="cv-cell">
|
||||
<div class="cv-cell__label">Sanctions Screening</div>
|
||||
<div class="cv-cell__value">
|
||||
<span class="cv-badge"
|
||||
[class.cv-badge--green]="clientViewData.nameVerified === true || clientViewData.nameVerified === 'true' || clientViewData.nameVerified === 1"
|
||||
[class.cv-badge--red]="clientViewData.nameVerified === false || clientViewData.nameVerified === 'false' || clientViewData.nameVerified === 0">
|
||||
{{ (clientViewData.nameVerified === true || clientViewData.nameVerified === 'true' || clientViewData.nameVerified === 1) ? 'Clear' : 'Pending' }}
|
||||
<span
|
||||
class="cv-badge"
|
||||
[class.cv-badge--green]="
|
||||
clientViewData.nameVerified === true ||
|
||||
clientViewData.nameVerified === 'true' ||
|
||||
clientViewData.nameVerified === 1
|
||||
"
|
||||
[class.cv-badge--red]="
|
||||
clientViewData.nameVerified === false ||
|
||||
clientViewData.nameVerified === 'false' ||
|
||||
clientViewData.nameVerified === 0
|
||||
"
|
||||
>
|
||||
{{
|
||||
clientViewData.nameVerified === true ||
|
||||
clientViewData.nameVerified === 'true' ||
|
||||
clientViewData.nameVerified === 1
|
||||
? 'Clear'
|
||||
: 'Pending'
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -206,244 +262,299 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-menu #clientMenu="matMenu">
|
||||
<button mat-menu-item [routerLink]="['edit']" *mifosxHasPermission="'UPDATE_CLIENT'">
|
||||
<mat-menu #clientMenu="matMenu">
|
||||
<button mat-menu-item [routerLink]="['edit']" *mifosxHasPermission="'UPDATE_CLIENT'">
|
||||
<mat-icon matListIcon>
|
||||
<fa-icon icon="edit" size="sm"></fa-icon>
|
||||
</mat-icon>
|
||||
<span>{{ 'labels.buttons.Edit' | translate }}</span>
|
||||
</button>
|
||||
|
||||
@if (isActive()) {
|
||||
<button mat-menu-item [matMenuTriggerFor]="accountApplications">
|
||||
<mat-icon matListIcon>
|
||||
<fa-icon icon="money-bill-alt" size="sm"></fa-icon>
|
||||
</mat-icon>
|
||||
<span>{{ 'labels.buttons.Applications' | translate }}</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
<button mat-menu-item [matMenuTriggerFor]="Actions">
|
||||
<mat-icon matListIcon>
|
||||
<fa-icon icon="tasks" size="sm"></fa-icon>
|
||||
</mat-icon>
|
||||
<span>{{ 'labels.buttons.Actions' | translate }}</span>
|
||||
</button>
|
||||
|
||||
@if (!clientViewData.staffId) {
|
||||
<button mat-menu-item (click)="doAction('Assign Staff')">
|
||||
<mat-icon matListIcon>
|
||||
<fa-icon icon="user-tie" size="sm"></fa-icon>
|
||||
</mat-icon>
|
||||
<span>{{ 'labels.buttons.Assign Staff' | translate }}</span>
|
||||
</button>
|
||||
}
|
||||
|
||||
@if (clientViewData.staffId) {
|
||||
<span>
|
||||
<button mat-menu-item *mifosxHasPermission="'UNASSIGNSTAFF_CLIENT'" (click)="doAction('Unassign Staff')">
|
||||
<mat-icon matListIcon>
|
||||
<fa-icon icon="edit" size="sm"></fa-icon>
|
||||
<fa-icon icon="user-tie" size="sm"></fa-icon>
|
||||
</mat-icon>
|
||||
<span>{{ 'labels.buttons.Edit' | translate }}</span>
|
||||
<span>{{ 'labels.buttons.Unassign Staff' | translate }}</span>
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
|
||||
@if (isActive()) {
|
||||
<button mat-menu-item [matMenuTriggerFor]="accountApplications">
|
||||
<mat-icon matListIcon>
|
||||
<fa-icon icon="money-bill-alt" size="sm"></fa-icon>
|
||||
</mat-icon>
|
||||
<span>{{ 'labels.buttons.Applications' | translate }}</span>
|
||||
</button>
|
||||
}
|
||||
<button mat-menu-item [matMenuTriggerFor]="More">{{ 'labels.buttons.More' | translate }}</button>
|
||||
</mat-menu>
|
||||
|
||||
<button mat-menu-item [matMenuTriggerFor]="Actions">
|
||||
<mat-icon matListIcon>
|
||||
<fa-icon icon="tasks" size="sm"></fa-icon>
|
||||
</mat-icon>
|
||||
<span>{{ 'labels.buttons.Actions' | translate }}</span>
|
||||
</button>
|
||||
<mat-menu #accountApplications="matMenu">
|
||||
<button mat-menu-item *mifosxHasPermission="'CREATE_LOAN'" [routerLink]="['loans-accounts', 'create']">
|
||||
{{ 'labels.buttons.New Loan Account' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item *mifosxHasPermission="'CREATE_SAVINGSACCOUNT'" [routerLink]="['savings-accounts', 'create']">
|
||||
{{ 'labels.buttons.New Savings Account' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item *mifosxHasPermission="'CREATE_SHAREACCOUNT'" [routerLink]="['shares-accounts', 'create']">
|
||||
{{ 'labels.buttons.New Share Account' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
*mifosxHasPermission="'CREATE_RECURRINGDEPOSITACCOUNT'"
|
||||
[routerLink]="['recurring-deposits-accounts', 'create-recurring-deposits-account']"
|
||||
>
|
||||
{{ 'labels.buttons.New Recurring Deposit Account' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
*mifosxHasPermission="'CREATE_FIXEDDEPOSITACCOUNT'"
|
||||
[routerLink]="['fixed-deposits-accounts', 'create']"
|
||||
>
|
||||
{{ 'labels.buttons.New Fixed Deposits Account' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item *mifosxHasPermission="'CREATE_CURRENTACCOUNT'" [routerLink]="['current-accounts', 'create']">
|
||||
{{ 'labels.buttons.New Current Account' | translate }}
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
||||
@if (!clientViewData.staffId) {
|
||||
<button mat-menu-item (click)="doAction('Assign Staff')">
|
||||
<mat-icon matListIcon>
|
||||
<fa-icon icon="user-tie" size="sm"></fa-icon>
|
||||
</mat-icon>
|
||||
<span>{{ 'labels.buttons.Assign Staff' | translate }}</span>
|
||||
</button>
|
||||
}
|
||||
<mat-menu #Actions="matMenu">
|
||||
<button mat-menu-item (click)="doAction('Close')">{{ 'labels.buttons.Close' | translate }}</button>
|
||||
<button mat-menu-item (click)="doAction('Transfer Client')">
|
||||
{{ 'labels.buttons.Transfer Client' | translate }}
|
||||
</button>
|
||||
@if (clientViewData.status.value === 'Pending') {
|
||||
<span
|
||||
><button mat-menu-item (click)="doAction('Activate')">
|
||||
{{ 'labels.buttons.Activate' | translate }}
|
||||
</button></span
|
||||
>
|
||||
}
|
||||
@if (clientViewData.status.value === 'Pending') {
|
||||
<span
|
||||
><button mat-menu-item (click)="doAction('Withdraw')">
|
||||
{{ 'labels.buttons.Withdraw' | translate }}
|
||||
</button></span
|
||||
>
|
||||
}
|
||||
@if (clientViewData.status.value === 'Pending') {
|
||||
<span
|
||||
><button mat-menu-item (click)="doAction('Reject')">{{ 'labels.buttons.Reject' | translate }}</button></span
|
||||
>
|
||||
}
|
||||
@if (clientViewData.status.value === 'Pending') {
|
||||
<span
|
||||
><button mat-menu-item *mifosxHasPermission="'DELETE_CLIENT'" (click)="doAction('Delete')">
|
||||
{{ 'labels.buttons.Delete' | translate }}
|
||||
</button></span
|
||||
>
|
||||
}
|
||||
@if (clientViewData.status.value === 'Closed') {
|
||||
<span
|
||||
><button mat-menu-item (click)="doAction('Reactivate')">
|
||||
{{ 'labels.buttons.Reactivate' | translate }}
|
||||
</button></span
|
||||
>
|
||||
}
|
||||
@if (clientViewData.status.value === 'Rejected') {
|
||||
<span
|
||||
><button mat-menu-item (click)="doAction('Undo Rejection')">
|
||||
{{ 'labels.buttons.Undo Rejection' | translate }}
|
||||
</button></span
|
||||
>
|
||||
}
|
||||
@if (clientViewData.status.value === 'Transfer in progress') {
|
||||
<span
|
||||
><button mat-menu-item (click)="doAction('Undo Transfer')">
|
||||
{{ 'labels.buttons.Undo Transfer' | translate }}
|
||||
</button></span
|
||||
>
|
||||
}
|
||||
@if (clientViewData.status.value === 'Transfer in progress') {
|
||||
<span
|
||||
><button mat-menu-item (click)="doAction('Accept Transfer')">
|
||||
{{ 'labels.buttons.Accept Transfer' | translate }}
|
||||
</button></span
|
||||
>
|
||||
}
|
||||
@if (clientViewData.status.value === 'Transfer in progress') {
|
||||
<span
|
||||
><button mat-menu-item (click)="doAction('Reject Transfer')">
|
||||
{{ 'labels.buttons.Reject transfer' | translate }}
|
||||
</button></span
|
||||
>
|
||||
}
|
||||
</mat-menu>
|
||||
|
||||
@if (clientViewData.staffId) {
|
||||
<span>
|
||||
<button mat-menu-item *mifosxHasPermission="'UNASSIGNSTAFF_CLIENT'" (click)="doAction('Unassign Staff')">
|
||||
<mat-icon matListIcon>
|
||||
<fa-icon icon="user-tie" size="sm"></fa-icon>
|
||||
</mat-icon>
|
||||
<span>{{ 'labels.buttons.Unassign Staff' | translate }}</span>
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
|
||||
<button mat-menu-item [matMenuTriggerFor]="More">{{ 'labels.buttons.More' | translate }}</button>
|
||||
</mat-menu>
|
||||
|
||||
<mat-menu #accountApplications="matMenu">
|
||||
<button mat-menu-item *mifosxHasPermission="'CREATE_LOAN'" [routerLink]="['loans-accounts', 'create']">
|
||||
{{ 'labels.buttons.New Loan Account' | translate }}
|
||||
</button>
|
||||
<mat-menu #More="matMenu">
|
||||
<button mat-menu-item (click)="doAction('Add Charge')" *mifosxHasPermission="'CREATE_CLIENTCHARGE'">
|
||||
{{ 'labels.buttons.Add Charge' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item (click)="doAction('Create Collateral')">
|
||||
{{ 'labels.buttons.Create Collateral' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item (click)="doAction('Survey')" [disabled]="true">
|
||||
{{ 'labels.buttons.Survey' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="doAction('Update Default Savings')"
|
||||
*mifosxHasPermission="'UPDATESAVINGSACCOUNT_CLIENT'"
|
||||
>
|
||||
{{ 'labels.buttons.Update Default Savings' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item (click)="doAction('Upload Signature')" *mifosxHasPermission="'CREATE_CLIENTIMAGE'">
|
||||
{{ 'labels.buttons.Upload Signature' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item (click)="doAction('Delete Signature')" *mifosxHasPermission="'DELETE_CLIENTIMAGE'">
|
||||
{{ 'labels.buttons.Delete Signature' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item (click)="doAction('Client Screen Reports')">
|
||||
{{ 'labels.buttons.Client Screen Reports' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="doAction('Create Standing Instructions')"
|
||||
*mifosxHasPermission="'CREATE_STANDINGINSTRUCTION'"
|
||||
>
|
||||
{{ 'labels.buttons.Create Standing Instructions' | translate }}
|
||||
</button>
|
||||
@if (clientViewData.status.value !== 'Transfer on hold') {
|
||||
<span>
|
||||
<button
|
||||
mat-menu-item
|
||||
*mifosxHasPermission="'CREATE_SAVINGSACCOUNT'"
|
||||
[routerLink]="['savings-accounts', 'create']"
|
||||
(click)="doAction('View Standing Instructions')"
|
||||
*mifosxHasPermission="'READ_STANDINGINSTRUCTION'"
|
||||
>
|
||||
{{ 'labels.buttons.New Savings Account' | translate }}
|
||||
{{ 'labels.buttons.View Standing Instructions' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item *mifosxHasPermission="'CREATE_SHAREACCOUNT'" [routerLink]="['shares-accounts', 'create']">
|
||||
{{ 'labels.buttons.New Share Account' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
*mifosxHasPermission="'CREATE_RECURRINGDEPOSITACCOUNT'"
|
||||
[routerLink]="['recurring-deposits-accounts', 'create-recurring-deposits-account']"
|
||||
>
|
||||
{{ 'labels.buttons.New Recurring Deposit Account' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
*mifosxHasPermission="'CREATE_FIXEDDEPOSITACCOUNT'"
|
||||
[routerLink]="['fixed-deposits-accounts', 'create']"
|
||||
>
|
||||
{{ 'labels.buttons.New Fixed Deposits Account' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
*mifosxHasPermission="'CREATE_CURRENTACCOUNT'"
|
||||
[routerLink]="['current-accounts', 'create']"
|
||||
>
|
||||
{{ 'labels.buttons.New Current Account' | translate }}
|
||||
</button>
|
||||
</mat-menu>
|
||||
|
||||
<mat-menu #Actions="matMenu">
|
||||
<button mat-menu-item (click)="doAction('Close')">{{ 'labels.buttons.Close' | translate }}</button>
|
||||
<button mat-menu-item (click)="doAction('Transfer Client')">
|
||||
{{ 'labels.buttons.Transfer Client' | translate }}
|
||||
</button>
|
||||
@if (clientViewData.status.value === 'Pending') {
|
||||
<span
|
||||
><button mat-menu-item (click)="doAction('Activate')">
|
||||
{{ 'labels.buttons.Activate' | translate }}
|
||||
</button></span
|
||||
>
|
||||
}
|
||||
@if (clientViewData.status.value === 'Pending') {
|
||||
<span
|
||||
><button mat-menu-item (click)="doAction('Withdraw')">
|
||||
{{ 'labels.buttons.Withdraw' | translate }}
|
||||
</button></span
|
||||
>
|
||||
}
|
||||
@if (clientViewData.status.value === 'Pending') {
|
||||
<span
|
||||
><button mat-menu-item (click)="doAction('Reject')">{{ 'labels.buttons.Reject' | translate }}</button></span
|
||||
>
|
||||
}
|
||||
@if (clientViewData.status.value === 'Pending') {
|
||||
<span
|
||||
><button mat-menu-item *mifosxHasPermission="'DELETE_CLIENT'" (click)="doAction('Delete')">
|
||||
{{ 'labels.buttons.Delete' | translate }}
|
||||
</button></span
|
||||
>
|
||||
}
|
||||
@if (clientViewData.status.value === 'Closed') {
|
||||
<span
|
||||
><button mat-menu-item (click)="doAction('Reactivate')">
|
||||
{{ 'labels.buttons.Reactivate' | translate }}
|
||||
</button></span
|
||||
>
|
||||
}
|
||||
@if (clientViewData.status.value === 'Rejected') {
|
||||
<span
|
||||
><button mat-menu-item (click)="doAction('Undo Rejection')">
|
||||
{{ 'labels.buttons.Undo Rejection' | translate }}
|
||||
</button></span
|
||||
>
|
||||
}
|
||||
@if (clientViewData.status.value === 'Transfer in progress') {
|
||||
<span
|
||||
><button mat-menu-item (click)="doAction('Undo Transfer')">
|
||||
{{ 'labels.buttons.Undo Transfer' | translate }}
|
||||
</button></span
|
||||
>
|
||||
}
|
||||
@if (clientViewData.status.value === 'Transfer in progress') {
|
||||
<span
|
||||
><button mat-menu-item (click)="doAction('Accept Transfer')">
|
||||
{{ 'labels.buttons.Accept Transfer' | translate }}
|
||||
</button></span
|
||||
>
|
||||
}
|
||||
@if (clientViewData.status.value === 'Transfer in progress') {
|
||||
<span
|
||||
><button mat-menu-item (click)="doAction('Reject Transfer')">
|
||||
{{ 'labels.buttons.Reject transfer' | translate }}
|
||||
</button></span
|
||||
>
|
||||
}
|
||||
</mat-menu>
|
||||
|
||||
<mat-menu #More="matMenu">
|
||||
<button mat-menu-item (click)="doAction('Add Charge')" *mifosxHasPermission="'CREATE_CLIENTCHARGE'">
|
||||
{{ 'labels.buttons.Add Charge' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item (click)="doAction('Create Collateral')">
|
||||
{{ 'labels.buttons.Create Collateral' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item (click)="doAction('Survey')" [disabled]="true">
|
||||
{{ 'labels.buttons.Survey' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="doAction('Update Default Savings')"
|
||||
*mifosxHasPermission="'UPDATESAVINGSACCOUNT_CLIENT'"
|
||||
>
|
||||
{{ 'labels.buttons.Update Default Savings' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item (click)="doAction('Upload Signature')" *mifosxHasPermission="'CREATE_CLIENTIMAGE'">
|
||||
{{ 'labels.buttons.Upload Signature' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item (click)="doAction('Delete Signature')" *mifosxHasPermission="'DELETE_CLIENTIMAGE'">
|
||||
{{ 'labels.buttons.Delete Signature' | translate }}
|
||||
</button>
|
||||
<button mat-menu-item (click)="doAction('Client Screen Reports')">
|
||||
{{ 'labels.buttons.Client Screen Reports' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="doAction('Create Standing Instructions')"
|
||||
*mifosxHasPermission="'CREATE_STANDINGINSTRUCTION'"
|
||||
>
|
||||
{{ 'labels.buttons.Create Standing Instructions' | translate }}
|
||||
</button>
|
||||
@if (clientViewData.status.value !== 'Transfer on hold') {
|
||||
<span>
|
||||
<button
|
||||
mat-menu-item
|
||||
(click)="doAction('View Standing Instructions')"
|
||||
*mifosxHasPermission="'READ_STANDINGINSTRUCTION'"
|
||||
>
|
||||
{{ 'labels.buttons.View Standing Instructions' | translate }}
|
||||
</button>
|
||||
</span>
|
||||
}
|
||||
</mat-menu>
|
||||
</span>
|
||||
}
|
||||
</mat-menu>
|
||||
|
||||
<div>
|
||||
<nav mat-tab-nav-bar class="cv-tabs" [tabPanel]="tabPanel">
|
||||
<a mat-tab-link [routerLink]="['./general']" routerLinkActive #general="routerLinkActive" [active]="general.isActive">
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./general']"
|
||||
routerLinkActive
|
||||
#general="routerLinkActive"
|
||||
[active]="general.isActive"
|
||||
>
|
||||
<i class="fa fa-tachometer cv-tab-icon"></i> {{ 'labels.inputs.General' | translate }}
|
||||
</a>
|
||||
<a mat-tab-link [routerLink]="['./personal-data']" routerLinkActive #personalData="routerLinkActive" [active]="personalData.isActive">
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./personal-data']"
|
||||
routerLinkActive
|
||||
#personalData="routerLinkActive"
|
||||
[active]="personalData.isActive"
|
||||
>
|
||||
<i class="fa fa-user cv-tab-icon"></i> {{ 'labels.inputs.Personal Data' | translate }}
|
||||
</a>
|
||||
<a mat-tab-link [routerLink]="['./address']" routerLinkActive #address="routerLinkActive" [active]="address.isActive">
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./address']"
|
||||
routerLinkActive
|
||||
#address="routerLinkActive"
|
||||
[active]="address.isActive"
|
||||
>
|
||||
<i class="fa fa-map-marker cv-tab-icon"></i> {{ 'labels.inputs.Address' | translate }}
|
||||
</a>
|
||||
@if (clientViewData?.legalForm?.id !== 2) {
|
||||
<a mat-tab-link [routerLink]="['./family-members']" routerLinkActive #familyMembers="routerLinkActive" [active]="familyMembers.isActive">
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./family-members']"
|
||||
routerLinkActive
|
||||
#familyMembers="routerLinkActive"
|
||||
[active]="familyMembers.isActive"
|
||||
>
|
||||
<i class="fa fa-users cv-tab-icon"></i> Family Members
|
||||
</a>
|
||||
}
|
||||
<a mat-tab-link [routerLink]="['./nominees']" routerLinkActive #nomineesTab="routerLinkActive" [active]="nomineesTab.isActive">
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./nominees']"
|
||||
routerLinkActive
|
||||
#nomineesTab="routerLinkActive"
|
||||
[active]="nomineesTab.isActive"
|
||||
>
|
||||
<i class="fa fa-shield cv-tab-icon"></i> Nominee
|
||||
</a>
|
||||
<a mat-tab-link [routerLink]="['./identities']" routerLinkActive #identities="routerLinkActive" [active]="identities.isActive" *mifosxHasPermission="'READ_CLIENTIDENTIFIER'">
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./identities']"
|
||||
routerLinkActive
|
||||
#identities="routerLinkActive"
|
||||
[active]="identities.isActive"
|
||||
*mifosxHasPermission="'READ_CLIENTIDENTIFIER'"
|
||||
>
|
||||
<i class="fa fa-id-card cv-tab-icon"></i> {{ 'labels.inputs.Identities' | translate }}
|
||||
</a>
|
||||
<a mat-tab-link [routerLink]="['./kyc']" routerLinkActive #kyc="routerLinkActive" [active]="kyc.isActive">
|
||||
<i class="fa fa-check-circle cv-tab-icon"></i> {{ 'labels.inputs.KYC' | translate }}
|
||||
</a>
|
||||
@if (clientViewData.legalForm?.id !== 1) {
|
||||
<a mat-tab-link [routerLink]="['./entity-classification']" routerLinkActive #ecTab="routerLinkActive" [active]="ecTab.isActive">
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./entity-classification']"
|
||||
routerLinkActive
|
||||
#ecTab="routerLinkActive"
|
||||
[active]="ecTab.isActive"
|
||||
>
|
||||
<i class="fa fa-building cv-tab-icon"></i> Scheme & Sector
|
||||
</a>
|
||||
}
|
||||
<a mat-tab-link [routerLink]="['./ckyc']" routerLinkActive #ckyc="routerLinkActive" [active]="ckyc.isActive">
|
||||
<i class="fa fa-shield cv-tab-icon"></i> {{ 'labels.inputs.CKYC' | translate }}
|
||||
</a>
|
||||
<a mat-tab-link [routerLink]="['./form60']" routerLinkActive #form60="routerLinkActive" [active]="form60.isActive">
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./form60']"
|
||||
routerLinkActive
|
||||
#form60="routerLinkActive"
|
||||
[active]="form60.isActive"
|
||||
>
|
||||
<i class="fa fa-file-text cv-tab-icon"></i> {{ 'labels.inputs.Form 60' | translate }}
|
||||
</a>
|
||||
<a mat-tab-link [routerLink]="['./documents']" routerLinkActive #documents="routerLinkActive" [active]="documents.isActive" *mifosxHasPermission="'READ_DOCUMENT'">
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./documents']"
|
||||
routerLinkActive
|
||||
#documents="routerLinkActive"
|
||||
[active]="documents.isActive"
|
||||
*mifosxHasPermission="'READ_DOCUMENT'"
|
||||
>
|
||||
<i class="fa fa-folder-open cv-tab-icon"></i> {{ 'labels.inputs.Documents' | translate }}
|
||||
</a>
|
||||
<a mat-tab-link [routerLink]="['./notes']" routerLinkActive #notes="routerLinkActive" [active]="notes.isActive" *mifosxHasPermission="'READ_CLIENTNOTE'">
|
||||
<a
|
||||
mat-tab-link
|
||||
[routerLink]="['./notes']"
|
||||
routerLinkActive
|
||||
#notes="routerLinkActive"
|
||||
[active]="notes.isActive"
|
||||
*mifosxHasPermission="'READ_CLIENTNOTE'"
|
||||
>
|
||||
<i class="fa fa-sticky-note cv-tab-icon"></i> {{ 'labels.inputs.Notes' | translate }}
|
||||
</a>
|
||||
@for (clientDatatable of clientDatatables; track clientDatatable) {
|
||||
|
||||
@@ -22,8 +22,8 @@ $accent: #0ea5e9;
|
||||
background: #fff !important;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06);
|
||||
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.04);
|
||||
border: 1px solid rgb(0 0 0 / 6%);
|
||||
box-shadow: 0 1px 4px rgb(0 0 0 / 4%);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
@@ -42,14 +42,18 @@ $accent: #0ea5e9;
|
||||
&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background: repeating-linear-gradient(108deg, transparent 0, transparent 80px, rgb(255 255 255 / 2.5%) 80px, rgb(255 255 255 / 2.5%) 160px);
|
||||
inset: 0;
|
||||
background: repeating-linear-gradient(
|
||||
108deg,
|
||||
transparent 0,
|
||||
transparent 80px,
|
||||
rgb(255 255 255 / 2.5%) 80px,
|
||||
rgb(255 255 255 / 2.5%) 160px
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* stylelint-disable-next-line selector-max-universal */
|
||||
> * {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
@@ -69,7 +73,7 @@ $accent: #0ea5e9;
|
||||
height: 72px;
|
||||
border-radius: 10px;
|
||||
object-fit: cover;
|
||||
border: 2px solid rgba(255, 255, 255, 0.25);
|
||||
border: 2px solid rgb(255 255 255 / 25%);
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
@@ -84,40 +88,48 @@ $accent: #0ea5e9;
|
||||
min-width: 26px !important;
|
||||
padding: 0 !important;
|
||||
border-radius: 5px !important;
|
||||
background: rgba(255, 255, 255, 0.1) !important;
|
||||
color: rgba(255, 255, 255, 0.6) !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15) !important;
|
||||
background: rgb(255 255 255 / 10%) !important;
|
||||
color: rgb(255 255 255 / 60%) !important;
|
||||
border: 1px solid rgb(255 255 255 / 15%) !important;
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
cursor: pointer;
|
||||
transition: all 0.12s;
|
||||
|
||||
i { font-size: 10px; }
|
||||
i {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.2) !important;
|
||||
background: rgb(255 255 255 / 20%) !important;
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
&--danger:hover {
|
||||
background: rgba(239, 68, 68, 0.35) !important;
|
||||
border-color: rgba(239, 68, 68, 0.45) !important;
|
||||
background: rgb(239 68 68 / 35%) !important;
|
||||
border-color: rgb(239 68 68 / 45%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
&__sig {
|
||||
font-size: 10px;
|
||||
color: rgba(255, 255, 255, 0.6);
|
||||
color: rgb(255 255 255 / 60%);
|
||||
cursor: pointer;
|
||||
font-weight: 500;
|
||||
|
||||
&:hover { color: #fff; text-decoration: underline; }
|
||||
&:hover {
|
||||
color: #fff;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Info column ──
|
||||
.cv-info { flex: 1; min-width: 0; }
|
||||
.cv-info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
// ── Name row ──
|
||||
.cv-name-row {
|
||||
@@ -132,7 +144,9 @@ $accent: #0ea5e9;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
|
||||
&__dot { font-size: 10px; }
|
||||
&__dot {
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 20px;
|
||||
@@ -148,23 +162,26 @@ $accent: #0ea5e9;
|
||||
border-radius: 4px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
background: rgba(16, 185, 129, 0.2);
|
||||
background: rgb(16 185 129 / 20%);
|
||||
color: #6ee7b7;
|
||||
border: 1px solid rgba(16, 185, 129, 0.25);
|
||||
border: 1px solid rgb(16 185 129 / 25%);
|
||||
}
|
||||
}
|
||||
|
||||
.cv-menu-btn {
|
||||
color: rgba(255, 255, 255, 0.7) !important;
|
||||
color: rgb(255 255 255 / 70%) !important;
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
min-width: 28px !important;
|
||||
padding: 0 !important;
|
||||
border: 1px solid rgba(255, 255, 255, 0.15);
|
||||
border: 1px solid rgb(255 255 255 / 15%);
|
||||
border-radius: 5px !important;
|
||||
line-height: 28px !important;
|
||||
|
||||
&:hover { background: rgba(255, 255, 255, 0.1); color: #fff !important; }
|
||||
&:hover {
|
||||
background: rgb(255 255 255 / 10%);
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
.actions-menu {
|
||||
font-size: 14px !important;
|
||||
@@ -187,14 +204,14 @@ $accent: #0ea5e9;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 11px;
|
||||
background: rgba(255, 255, 255, 0.1);
|
||||
border: 1px solid rgba(255, 255, 255, 0.13);
|
||||
background: rgb(255 255 255 / 10%);
|
||||
border: 1px solid rgb(255 255 255 / 13%);
|
||||
border-radius: 5px;
|
||||
|
||||
&__label {
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
color: rgba(255, 255, 255, 0.4);
|
||||
color: rgb(255 255 255 / 40%);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
}
|
||||
@@ -203,7 +220,7 @@ $accent: #0ea5e9;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
|
||||
font-family: 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
}
|
||||
@@ -220,19 +237,19 @@ $accent: #0ea5e9;
|
||||
|
||||
.cv-cell {
|
||||
padding: 9px 13px;
|
||||
background: rgba(255, 255, 255, 0.07);
|
||||
background: rgb(255 255 255 / 7%);
|
||||
border-radius: 6px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
border: 1px solid rgb(255 255 255 / 8%);
|
||||
transition: background 0.12s;
|
||||
|
||||
&:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
background: rgb(255 255 255 / 12%);
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
color: rgba(255, 255, 255, 0.45);
|
||||
color: rgb(255 255 255 / 45%);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 2px;
|
||||
@@ -253,17 +270,30 @@ $accent: #0ea5e9;
|
||||
border-radius: 10px;
|
||||
letter-spacing: 0.3px;
|
||||
|
||||
&--green { background: rgba(16, 185, 129, 0.2); color: #6ee7b7; }
|
||||
&--yellow { background: rgba(245, 158, 11, 0.2); color: #fcd34d; }
|
||||
&--red { background: rgba(239, 68, 68, 0.2); color: #fca5a5; }
|
||||
&--green {
|
||||
background: rgb(16 185 129 / 20%);
|
||||
color: #6ee7b7;
|
||||
}
|
||||
|
||||
&--yellow {
|
||||
background: rgb(245 158 11 / 20%);
|
||||
color: #fcd34d;
|
||||
}
|
||||
|
||||
&--red {
|
||||
background: rgb(239 68 68 / 20%);
|
||||
color: #fca5a5;
|
||||
}
|
||||
}
|
||||
|
||||
.actions-menu { color: #fff !important; }
|
||||
.actions-menu {
|
||||
color: #fff !important;
|
||||
}
|
||||
|
||||
// ── Tab bar ──
|
||||
.cv-tabs {
|
||||
background: #fff;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
|
||||
border-bottom: 1px solid rgb(0 0 0 / 6%);
|
||||
|
||||
::ng-deep .mat-mdc-tab-link {
|
||||
font-size: 12px !important;
|
||||
@@ -282,7 +312,8 @@ $accent: #0ea5e9;
|
||||
}
|
||||
|
||||
// Fix copy icons on dark background
|
||||
.cv-top, .cv-grid {
|
||||
.cv-top,
|
||||
.cv-grid {
|
||||
::ng-deep {
|
||||
mifosx-external-identifier,
|
||||
mifosx-account-number {
|
||||
@@ -292,7 +323,7 @@ $accent: #0ea5e9;
|
||||
button,
|
||||
mat-icon,
|
||||
fa-icon {
|
||||
color: rgba(255, 255, 255, 0.4) !important;
|
||||
color: rgb(255 255 255 / 40%) !important;
|
||||
font-size: 11px !important;
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
@@ -311,8 +342,8 @@ $accent: #0ea5e9;
|
||||
// ── Client menu styling ──
|
||||
::ng-deep .mat-mdc-menu-panel {
|
||||
border-radius: 10px !important;
|
||||
border: 1px solid rgba(0, 0, 0, 0.06) !important;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.12) !important;
|
||||
border: 1px solid rgb(0 0 0 / 6%) !important;
|
||||
box-shadow: 0 8px 30px rgb(0 0 0 / 12%) !important;
|
||||
min-width: 200px !important;
|
||||
padding: 6px 0 !important;
|
||||
|
||||
@@ -352,17 +383,34 @@ $accent: #0ea5e9;
|
||||
}
|
||||
|
||||
// ── Responsive ──
|
||||
@media (max-width: 1024px) {
|
||||
.cv-grid { grid-template-columns: repeat(3, 1fr); }
|
||||
@media (width <= 1024px) {
|
||||
.cv-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.cv-top { flex-direction: column; align-items: center; text-align: center; }
|
||||
.cv-name-row { justify-content: center; }
|
||||
.cv-ids { justify-content: center; }
|
||||
.cv-grid { grid-template-columns: repeat(2, 1fr); }
|
||||
@media (width <= 768px) {
|
||||
.cv-top {
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.cv-name-row {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cv-ids {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.cv-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.cv-grid { grid-template-columns: 1fr; }
|
||||
@media (width <= 480px) {
|
||||
.cv-grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<!--
|
||||
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/.
|
||||
-->
|
||||
|
||||
<h2 mat-dialog-title class="icd-title">{{ title }}</h2>
|
||||
<mat-dialog-content class="icd-body">
|
||||
<p class="icd-hint">Drag the corners to crop. The result will be saved as a PNG.</p>
|
||||
<div class="icd-cropper">
|
||||
<image-cropper
|
||||
[imageFile]="data.file"
|
||||
[maintainAspectRatio]="false"
|
||||
[containWithinAspectRatio]="false"
|
||||
format="png"
|
||||
output="blob"
|
||||
(imageCropped)="imageCropped($event)"
|
||||
(imageLoaded)="imageLoaded($event)"
|
||||
(loadImageFailed)="loadImageFailed()"
|
||||
></image-cropper>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions class="icd-actions">
|
||||
<button mat-button type="button" (click)="cancel()">Cancel</button>
|
||||
<button mat-flat-button color="primary" type="button" (click)="save()" [disabled]="!croppedBlob">
|
||||
Use cropped image
|
||||
</button>
|
||||
</mat-dialog-actions>
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
min-width: 480px;
|
||||
max-width: 720px;
|
||||
}
|
||||
|
||||
.icd-title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
padding: 16px 20px 8px;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.icd-body {
|
||||
padding: 0 20px 12px;
|
||||
}
|
||||
|
||||
.icd-hint {
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
margin: 0 0 10px;
|
||||
}
|
||||
|
||||
.icd-cropper {
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
padding: 8px;
|
||||
max-height: 60vh;
|
||||
overflow: hidden;
|
||||
|
||||
/* stylelint-disable-next-line selector-type-no-unknown */
|
||||
::ng-deep image-cropper {
|
||||
display: block;
|
||||
max-height: 56vh;
|
||||
}
|
||||
}
|
||||
|
||||
.icd-actions {
|
||||
padding: 8px 20px 16px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
}
|
||||
+93
@@ -0,0 +1,93 @@
|
||||
/**
|
||||
* 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 } from '@angular/core';
|
||||
import {
|
||||
MatDialogRef,
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogTitle,
|
||||
MatDialogActions,
|
||||
MatDialogContent,
|
||||
MatDialogClose
|
||||
} from '@angular/material/dialog';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ImageCropperComponent, ImageCroppedEvent, LoadedImage } from 'ngx-image-cropper';
|
||||
import { MatButton } from '@angular/material/button';
|
||||
|
||||
interface CropperDialogData {
|
||||
file: File;
|
||||
title?: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-image-cropper-dialog',
|
||||
templateUrl: './image-cropper-dialog.component.html',
|
||||
styleUrls: ['./image-cropper-dialog.component.scss'],
|
||||
imports: [
|
||||
CommonModule,
|
||||
ImageCropperComponent,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
MatButton
|
||||
]
|
||||
})
|
||||
export class ImageCropperDialogComponent {
|
||||
dialogRef = inject<MatDialogRef<ImageCropperDialogComponent>>(MatDialogRef);
|
||||
data = inject<CropperDialogData>(MAT_DIALOG_DATA);
|
||||
|
||||
croppedBlob: Blob | null = null;
|
||||
loading = true;
|
||||
title = '';
|
||||
|
||||
constructor() {
|
||||
this.title = this.data?.title || 'Crop image';
|
||||
}
|
||||
|
||||
imageCropped(event: ImageCroppedEvent): void {
|
||||
this.croppedBlob = event.blob || null;
|
||||
}
|
||||
|
||||
imageLoaded(_image: LoadedImage): void {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
loadImageFailed(): void {
|
||||
this.loading = false;
|
||||
this.dialogRef.close(null);
|
||||
}
|
||||
|
||||
save(): void {
|
||||
if (!this.croppedBlob) {
|
||||
this.dialogRef.close(null);
|
||||
return;
|
||||
}
|
||||
const ext = this.guessExt(this.data.file.name);
|
||||
const cropped = new File([this.croppedBlob], `${this.stripExt(this.data.file.name)}-cropped.${ext}`, {
|
||||
type: this.croppedBlob.type || 'image/png'
|
||||
});
|
||||
this.dialogRef.close(cropped);
|
||||
}
|
||||
|
||||
cancel(): void {
|
||||
this.dialogRef.close(null);
|
||||
}
|
||||
|
||||
private stripExt(name: string): string {
|
||||
const i = name.lastIndexOf('.');
|
||||
return i > 0 ? name.slice(0, i) : name;
|
||||
}
|
||||
|
||||
private guessExt(name: string): string {
|
||||
const i = name.lastIndexOf('.');
|
||||
if (i < 0) return 'png';
|
||||
const ext = name.slice(i + 1).toLowerCase();
|
||||
return ext === 'jpg' || ext === 'jpeg' ? 'png' : ext;
|
||||
}
|
||||
}
|
||||
+391
-83
@@ -6,7 +6,7 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
|
||||
<div class="fd-dialog">
|
||||
<div class="fd-dialog" [class.fd-dialog--kyc]="documentIdentifier">
|
||||
<div class="fd-header">
|
||||
<h1 mat-dialog-title class="fd-title">
|
||||
@if (documentIdentifier) {
|
||||
@@ -23,85 +23,398 @@
|
||||
</div>
|
||||
|
||||
<div class="fd-content">
|
||||
<form [formGroup]="uploadDocumentForm" class="fd-form">
|
||||
@if (documentIdentifier) {
|
||||
<!-- Identifier fields in 2-column grid -->
|
||||
<div class="fd-grid fd-grid--cols-2">
|
||||
<div class="fd-field">
|
||||
<mat-form-field class="flex-fill" appearance="outline">
|
||||
<mat-label>{{ 'labels.inputs.Document Type' | translate }}</mat-label>
|
||||
<mat-select formControlName="documentTypeId" required>
|
||||
@for (docType of allowedDocumentTypes; track docType) {
|
||||
<mat-option [value]="docType.id">
|
||||
{{ docType.name }}
|
||||
</mat-option>
|
||||
@if (documentIdentifier) {
|
||||
<div class="kyc-intro">
|
||||
<i class="fa fa-shield kyc-intro__icon"></i>
|
||||
<div>
|
||||
<strong>KYC Document Upload</strong>
|
||||
<p>
|
||||
Upload KYC documents as per RBI guidelines. Accepted:
|
||||
<i class="fa fa-file-image-o"></i> JPG, <i class="fa fa-file-image-o"></i> PNG,
|
||||
<i class="fa fa-file-pdf-o"></i> PDF — max 5MB each.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (loadingProofs) {
|
||||
<div class="kyc-loading"><i class="fa fa-spinner fa-spin"></i> Loading document types…</div>
|
||||
}
|
||||
|
||||
<form [formGroup]="uploadDocumentForm" class="kyc-form">
|
||||
<!-- SECTION 1: IDENTITY PROOF -->
|
||||
<div class="kyc-section" formGroupName="id">
|
||||
<div class="kyc-section__header">
|
||||
<div class="kyc-badge kyc-badge--blue">
|
||||
<i class="fa fa-user-circle-o"></i>
|
||||
</div>
|
||||
<div class="kyc-section__titles">
|
||||
<p class="kyc-section__title"><i class="fa fa-id-badge"></i> Identity proof</p>
|
||||
<p class="kyc-section__sub">Select document type — upload requirements will update automatically</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prefilled name (from client registration) -->
|
||||
@if (clientName) {
|
||||
<div class="kyc-prefill">
|
||||
<i class="fa fa-user"></i>
|
||||
<div>
|
||||
<span class="kyc-prefill__label">Full name (from registration)</span>
|
||||
<span class="kyc-prefill__value">{{ clientName }}</span>
|
||||
</div>
|
||||
<i class="fa fa-lock kyc-prefill__lock" title="Prefilled from client registration"></i>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="kyc-field kyc-mb-sm">
|
||||
<label class="kyc-label"> <i class="fa fa-file-text-o"></i> Document type </label>
|
||||
<div class="kyc-select-wrap">
|
||||
<i class="fa fa-id-card kyc-select-wrap__icon"></i>
|
||||
<select
|
||||
class="kyc-input kyc-select"
|
||||
formControlName="proofId"
|
||||
(change)="onIdProofChange($any($event.target).value)"
|
||||
>
|
||||
<option value="">— Select document —</option>
|
||||
@for (p of idProofs; track p.id) {
|
||||
<option [value]="p.id">{{ p.name }}</option>
|
||||
}
|
||||
</mat-select>
|
||||
@if (uploadDocumentForm.controls.documentTypeId?.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Document Type' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
</select>
|
||||
<i class="fa fa-chevron-down kyc-select-wrap__chevron"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (selectedIdProof) {
|
||||
<div class="kyc-field kyc-mb-sm">
|
||||
<label class="kyc-label"> <i class="fa fa-hashtag"></i> {{ selectedIdProof.documentKeyLabel }} </label>
|
||||
<div class="kyc-input-wrap">
|
||||
<i class="fa fa-pencil kyc-input-wrap__icon"></i>
|
||||
<input
|
||||
type="text"
|
||||
class="kyc-input kyc-input--padded"
|
||||
formControlName="documentKey"
|
||||
[placeholder]="selectedIdProof.documentKeyPlaceholder || 'Enter document number'"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (selectedIdProof?.info) {
|
||||
<div class="kyc-banner kyc-banner--blue">
|
||||
<i class="fa fa-info-circle"></i>
|
||||
<span>{{ selectedIdProof.info }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (selectedIdProof) {
|
||||
<div class="kyc-upload-grid" [class.kyc-upload-grid--two]="selectedIdProof.sides === 2">
|
||||
<div class="kyc-field">
|
||||
<div class="kyc-upload-label">
|
||||
<span class="kyc-label">
|
||||
<i class="fa fa-file-image-o"></i>
|
||||
{{ selectedIdProof.sides === 2 ? 'Front side' : selectedIdProof.name + ' (single copy)' }}
|
||||
</span>
|
||||
<span class="kyc-tag kyc-tag--req"> <i class="fa fa-asterisk"></i> Required </span>
|
||||
</div>
|
||||
<label
|
||||
class="kyc-dropzone"
|
||||
[class.kyc-dropzone--has]="idFiles.front"
|
||||
[class.kyc-dropzone--req]="!idFiles.front"
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
accept=".jpg,.jpeg,.png,.pdf"
|
||||
hidden
|
||||
(change)="onFilePick('id', 'front', $event)"
|
||||
/>
|
||||
@if (idFiles.front) {
|
||||
<i class="fa fa-check-circle kyc-dropzone__icon"></i>
|
||||
<p class="kyc-dropzone__text">File selected</p>
|
||||
<p class="kyc-dropzone__name"><i class="fa fa-paperclip"></i> {{ getFileName('id', 'front') }}</p>
|
||||
} @else {
|
||||
<i class="fa fa-cloud-upload kyc-dropzone__icon"></i>
|
||||
<p class="kyc-dropzone__text">Click to upload</p>
|
||||
<p class="kyc-dropzone__hint">
|
||||
<i class="fa fa-file-image-o"></i> JPG · <i class="fa fa-file-image-o"></i> PNG ·
|
||||
<i class="fa fa-file-pdf-o"></i> PDF · max 5MB
|
||||
</p>
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@if (selectedIdProof.sides === 2) {
|
||||
<div class="kyc-field">
|
||||
<div class="kyc-upload-label">
|
||||
<span class="kyc-label"> <i class="fa fa-file-image-o"></i> Back side </span>
|
||||
<span class="kyc-tag kyc-tag--req"> <i class="fa fa-asterisk"></i> Required </span>
|
||||
</div>
|
||||
<label
|
||||
class="kyc-dropzone"
|
||||
[class.kyc-dropzone--has]="idFiles.back"
|
||||
[class.kyc-dropzone--req]="!idFiles.back"
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
accept=".jpg,.jpeg,.png,.pdf"
|
||||
hidden
|
||||
(change)="onFilePick('id', 'back', $event)"
|
||||
/>
|
||||
@if (idFiles.back) {
|
||||
<i class="fa fa-check-circle kyc-dropzone__icon"></i>
|
||||
<p class="kyc-dropzone__text">File selected</p>
|
||||
<p class="kyc-dropzone__name"><i class="fa fa-paperclip"></i> {{ getFileName('id', 'back') }}</p>
|
||||
} @else {
|
||||
<i class="fa fa-cloud-upload kyc-dropzone__icon"></i>
|
||||
<p class="kyc-dropzone__text">Click to upload</p>
|
||||
<p class="kyc-dropzone__hint">
|
||||
<i class="fa fa-file-image-o"></i> JPG · <i class="fa fa-file-image-o"></i> PNG ·
|
||||
<i class="fa fa-file-pdf-o"></i> PDF · max 5MB
|
||||
</p>
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="fd-field">
|
||||
<mat-form-field class="flex-fill" appearance="outline">
|
||||
<mat-label>{{ 'labels.inputs.Status' | translate }}</mat-label>
|
||||
<mat-select formControlName="status" required>
|
||||
@for (status of statusOptions; track status) {
|
||||
<mat-option [value]="status.value">
|
||||
{{ status.label }}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="fd-field">
|
||||
<mat-form-field class="flex-fill" appearance="outline">
|
||||
<mat-label>{{ 'labels.inputs.Document Key' | translate }}</mat-label>
|
||||
<input formControlName="documentKey" required matInput />
|
||||
@if (uploadDocumentForm.controls.documentKey?.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.Document Key' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="fd-field">
|
||||
<mat-form-field class="flex-fill" appearance="outline">
|
||||
<mat-label>{{ 'labels.inputs.Description' | translate }}</mat-label>
|
||||
<input formControlName="description" matInput />
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (idMergedPreview && selectedIdProof?.sides === 2) {
|
||||
<div class="kyc-merged">
|
||||
<div class="kyc-merged__head">
|
||||
<span class="kyc-merged__title">
|
||||
<i class="fa fa-object-group"></i> Combined preview (front + back)
|
||||
</span>
|
||||
<span class="kyc-merged__hint">
|
||||
Saved as a single PNG. Re-crop a side to update the merged file.
|
||||
</span>
|
||||
</div>
|
||||
<div class="kyc-merged__preview">
|
||||
<img [src]="idMergedPreview" alt="Merged ID document" />
|
||||
</div>
|
||||
<div class="kyc-merged__actions">
|
||||
<button type="button" class="kyc-merged__btn" (click)="reCrop('id', 'front')">
|
||||
<i class="fa fa-crop"></i> Re-crop front
|
||||
</button>
|
||||
<button type="button" class="kyc-merged__btn" (click)="reCrop('id', 'back')">
|
||||
<i class="fa fa-crop"></i> Re-crop back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- File section -->
|
||||
<div class="fd-section-divider">
|
||||
<span class="fd-section-divider__text">
|
||||
<i class="fa fa-paperclip"></i> {{ 'labels.heading.Documents' | translate }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="fd-grid fd-grid--cols-2">
|
||||
<div class="fd-field">
|
||||
<mat-form-field class="flex-fill" appearance="outline">
|
||||
<mat-label>{{ 'labels.inputs.File Name' | translate }}</mat-label>
|
||||
<input formControlName="fileName" required matInput />
|
||||
@if (uploadDocumentForm.controls.fileName.hasError('required')) {
|
||||
<mat-error>
|
||||
{{ 'labels.inputs.File Name' | translate }} {{ 'labels.commons.is' | translate }}
|
||||
<strong>{{ 'labels.commons.required' | translate }}</strong>
|
||||
</mat-error>
|
||||
<!-- SECTION 2: ADDRESS PROOF -->
|
||||
<div class="kyc-section" formGroupName="address">
|
||||
<div class="kyc-section__header">
|
||||
<div class="kyc-badge kyc-badge--teal">
|
||||
<i class="fa fa-map-marker"></i>
|
||||
</div>
|
||||
<div class="kyc-section__titles">
|
||||
<p class="kyc-section__title"><i class="fa fa-home"></i> Address proof</p>
|
||||
<p class="kyc-section__sub">Select document type — upload requirements will update automatically</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Prefilled permanent address -->
|
||||
@if (clientAddress) {
|
||||
<div class="kyc-prefill kyc-prefill--teal">
|
||||
<i class="fa fa-map-marker"></i>
|
||||
<div>
|
||||
<span class="kyc-prefill__label">Permanent address (from registration)</span>
|
||||
<span class="kyc-prefill__value">{{ clientAddress }}</span>
|
||||
</div>
|
||||
<i class="fa fa-lock kyc-prefill__lock" title="Prefilled from client registration"></i>
|
||||
</div>
|
||||
} @else {
|
||||
<div class="kyc-banner kyc-banner--amber">
|
||||
<i class="fa fa-exclamation-triangle"></i>
|
||||
<span>No permanent address found on file. Add one in the Address tab first.</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div class="kyc-field">
|
||||
<label class="kyc-label"> <i class="fa fa-file-text-o"></i> Document type </label>
|
||||
<div class="kyc-chip-wrap">
|
||||
@for (group of addressGroups; track group.label; let last = $last) {
|
||||
<div class="kyc-chip-group-label"><i class="fa fa-folder-open-o"></i> {{ group.label }}</div>
|
||||
<div class="kyc-chip-grid">
|
||||
@for (p of group.items; track p.id) {
|
||||
<span
|
||||
class="kyc-chip"
|
||||
[class.kyc-chip--sel]="isAddrProofSelected(p)"
|
||||
(click)="selectAddrProof(p)"
|
||||
(keydown.enter)="selectAddrProof(p)"
|
||||
tabindex="0"
|
||||
role="button"
|
||||
>
|
||||
@if (isAddrProofSelected(p)) {
|
||||
<i class="fa fa-check-circle"></i>
|
||||
} @else {
|
||||
<i class="fa fa-circle-o"></i>
|
||||
}
|
||||
{{ p.name }}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
@if (!last) {
|
||||
<div class="kyc-chip-divider"></div>
|
||||
}
|
||||
}
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<div class="fd-field fd-field--file">
|
||||
<mifosx-file-upload flex="100%" (change)="onFileSelect($event)"></mifosx-file-upload>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (selectedAddrProof?.info) {
|
||||
<div class="kyc-banner kyc-banner--teal">
|
||||
<i class="fa fa-check-circle"></i>
|
||||
<span>{{ selectedAddrProof.info }}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (selectedAddrProof) {
|
||||
<div class="kyc-upload-grid" [class.kyc-upload-grid--two]="selectedAddrProof.sides === 2">
|
||||
<div class="kyc-field">
|
||||
<div class="kyc-upload-label">
|
||||
<span class="kyc-label">
|
||||
<i class="fa fa-file-image-o"></i>
|
||||
{{ selectedAddrProof.sides === 2 ? 'Front side' : selectedAddrProof.name + ' (single copy)' }}
|
||||
</span>
|
||||
<span class="kyc-tag kyc-tag--req"> <i class="fa fa-asterisk"></i> Required </span>
|
||||
</div>
|
||||
<label
|
||||
class="kyc-dropzone"
|
||||
[class.kyc-dropzone--has]="addrFiles.front"
|
||||
[class.kyc-dropzone--req]="!addrFiles.front"
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
accept=".jpg,.jpeg,.png,.pdf"
|
||||
hidden
|
||||
(change)="onFilePick('address', 'front', $event)"
|
||||
/>
|
||||
@if (addrFiles.front) {
|
||||
<i class="fa fa-check-circle kyc-dropzone__icon"></i>
|
||||
<p class="kyc-dropzone__text">File selected</p>
|
||||
<p class="kyc-dropzone__name">
|
||||
<i class="fa fa-paperclip"></i> {{ getFileName('address', 'front') }}
|
||||
</p>
|
||||
} @else {
|
||||
<i class="fa fa-cloud-upload kyc-dropzone__icon"></i>
|
||||
<p class="kyc-dropzone__text">Click to upload</p>
|
||||
<p class="kyc-dropzone__hint">
|
||||
<i class="fa fa-file-image-o"></i> JPG · <i class="fa fa-file-image-o"></i> PNG ·
|
||||
<i class="fa fa-file-pdf-o"></i> PDF · max 5MB
|
||||
</p>
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@if (selectedAddrProof.sides === 2) {
|
||||
<div class="kyc-field">
|
||||
<div class="kyc-upload-label">
|
||||
<span class="kyc-label"> <i class="fa fa-file-image-o"></i> Back side </span>
|
||||
<span class="kyc-tag kyc-tag--req"> <i class="fa fa-asterisk"></i> Required </span>
|
||||
</div>
|
||||
<label
|
||||
class="kyc-dropzone"
|
||||
[class.kyc-dropzone--has]="addrFiles.back"
|
||||
[class.kyc-dropzone--req]="!addrFiles.back"
|
||||
>
|
||||
<input
|
||||
type="file"
|
||||
accept=".jpg,.jpeg,.png,.pdf"
|
||||
hidden
|
||||
(change)="onFilePick('address', 'back', $event)"
|
||||
/>
|
||||
@if (addrFiles.back) {
|
||||
<i class="fa fa-check-circle kyc-dropzone__icon"></i>
|
||||
<p class="kyc-dropzone__text">File selected</p>
|
||||
<p class="kyc-dropzone__name">
|
||||
<i class="fa fa-paperclip"></i> {{ getFileName('address', 'back') }}
|
||||
</p>
|
||||
} @else {
|
||||
<i class="fa fa-cloud-upload kyc-dropzone__icon"></i>
|
||||
<p class="kyc-dropzone__text">Click to upload</p>
|
||||
<p class="kyc-dropzone__hint">
|
||||
<i class="fa fa-file-image-o"></i> JPG · <i class="fa fa-file-image-o"></i> PNG ·
|
||||
<i class="fa fa-file-pdf-o"></i> PDF · max 5MB
|
||||
</p>
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (selectedAddrProof.requiresDate) {
|
||||
<div class="kyc-field kyc-field--date">
|
||||
<label class="kyc-label">
|
||||
<i class="fa fa-calendar"></i> Issue / statement date
|
||||
<span class="kyc-opt">(not older than 3 months)</span>
|
||||
</label>
|
||||
<div class="kyc-input-wrap kyc-input-wrap--date">
|
||||
<i class="fa fa-calendar-o kyc-input-wrap__icon"></i>
|
||||
<input type="date" class="kyc-input kyc-input--date" formControlName="issueDate" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (addrMergedPreview && selectedAddrProof?.sides === 2) {
|
||||
<div class="kyc-merged">
|
||||
<div class="kyc-merged__head">
|
||||
<span class="kyc-merged__title">
|
||||
<i class="fa fa-object-group"></i> Combined preview (front + back)
|
||||
</span>
|
||||
<span class="kyc-merged__hint">
|
||||
Saved as a single PNG. Re-crop a side to update the merged file.
|
||||
</span>
|
||||
</div>
|
||||
<div class="kyc-merged__preview">
|
||||
<img [src]="addrMergedPreview" alt="Merged address document" />
|
||||
</div>
|
||||
<div class="kyc-merged__actions">
|
||||
<button type="button" class="kyc-merged__btn" (click)="reCrop('address', 'front')">
|
||||
<i class="fa fa-crop"></i> Re-crop front
|
||||
</button>
|
||||
<button type="button" class="kyc-merged__btn" (click)="reCrop('address', 'back')">
|
||||
<i class="fa fa-crop"></i> Re-crop back
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
</div>
|
||||
} @else {
|
||||
<!-- Standard upload form -->
|
||||
|
||||
<div class="kyc-section kyc-section--notes">
|
||||
<label class="kyc-label">
|
||||
<i class="fa fa-sticky-note-o"></i> Description <span class="kyc-opt">(optional)</span>
|
||||
</label>
|
||||
<textarea
|
||||
class="kyc-textarea"
|
||||
formControlName="description"
|
||||
placeholder="Add any additional notes about these documents…"
|
||||
rows="2"
|
||||
></textarea>
|
||||
</div>
|
||||
|
||||
<div class="kyc-status-bar">
|
||||
<span>
|
||||
<span class="kyc-dot" [class.kyc-dot--done]="isIdComplete()">
|
||||
@if (isIdComplete()) {
|
||||
<i class="fa fa-check"></i>
|
||||
}
|
||||
</span>
|
||||
<i class="fa fa-id-badge"></i> Identity proof
|
||||
</span>
|
||||
<span>
|
||||
<span class="kyc-dot" [class.kyc-dot--done]="isAddressComplete()">
|
||||
@if (isAddressComplete()) {
|
||||
<i class="fa fa-check"></i>
|
||||
}
|
||||
</span>
|
||||
<i class="fa fa-home"></i> Address proof
|
||||
</span>
|
||||
<span class="kyc-status-hint"> <i class="fa fa-info-circle"></i> Fill in at least one section </span>
|
||||
</div>
|
||||
</form>
|
||||
} @else {
|
||||
<form [formGroup]="uploadDocumentForm" class="fd-form">
|
||||
<div class="fd-grid fd-grid--cols-2">
|
||||
<div class="fd-field">
|
||||
<mat-form-field class="flex-fill" appearance="outline">
|
||||
@@ -125,24 +438,19 @@
|
||||
<div class="fd-field fd-field--file">
|
||||
<mifosx-file-upload flex="100%" (change)="onFileSelect($event)"></mifosx-file-upload>
|
||||
</div>
|
||||
}
|
||||
</form>
|
||||
</form>
|
||||
}
|
||||
</div>
|
||||
|
||||
<mat-dialog-actions class="fd-actions" align="end">
|
||||
<button mat-stroked-button mat-dialog-close class="fd-btn fd-btn--cancel">
|
||||
<i class="fa fa-times fd-btn__icon"></i>
|
||||
{{ 'labels.buttons.Cancel' | translate }}
|
||||
</button>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
[disabled]="!uploadDocumentForm.valid"
|
||||
[mat-dialog-close]="uploadDocumentForm.value"
|
||||
class="fd-btn fd-btn--submit"
|
||||
>
|
||||
<button mat-flat-button color="primary" [disabled]="!canSubmit" (click)="submit()" class="fd-btn fd-btn--submit">
|
||||
<i class="fa fa-check fd-btn__icon"></i>
|
||||
@if (documentIdentifier) {
|
||||
{{ 'labels.buttons.Add' | translate }}
|
||||
{{ 'labels.buttons.Submit' | translate }}
|
||||
} @else {
|
||||
{{ 'labels.buttons.Upload' | translate }}
|
||||
}
|
||||
|
||||
+746
-1
@@ -184,8 +184,753 @@ $white: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@media (width <= 600px) {
|
||||
.fd-grid--cols-2 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
/* ===================== KYC Identifier Mode ===================== */
|
||||
|
||||
$blue-50: #e6f1fb;
|
||||
$blue-100: #d4e5f7;
|
||||
$blue-700: #185fa5;
|
||||
$blue-900: #0c447c;
|
||||
$teal-50: #e1f5ee;
|
||||
$teal-700: #0f6e56;
|
||||
$teal-900: #085041;
|
||||
$teal-accent: #1d9e75;
|
||||
$amber-50: #faeeda;
|
||||
$amber-700: #ba7517;
|
||||
$amber-900: #633806;
|
||||
$red-50: #faece7;
|
||||
$red-100: #f5c4b3;
|
||||
$red-700: #d85a30;
|
||||
$red-900: #993c1d;
|
||||
|
||||
.fd-dialog--kyc {
|
||||
.fd-content {
|
||||
padding: 18px 24px 8px !important;
|
||||
max-height: 75vh !important;
|
||||
overflow-y: auto !important;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-intro {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
background: linear-gradient(135deg, $blue-50, $teal-50);
|
||||
border: 1px solid $gray-200;
|
||||
border-radius: 10px;
|
||||
padding: 14px 16px;
|
||||
margin: 0 0 16px;
|
||||
|
||||
&__icon {
|
||||
font-size: 22px;
|
||||
color: $primary;
|
||||
flex-shrink: 0;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
strong {
|
||||
font-size: 13px;
|
||||
color: $gray-900;
|
||||
display: block;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
p {
|
||||
font-size: 12px;
|
||||
color: $gray-700;
|
||||
margin: 0;
|
||||
line-height: 1.5;
|
||||
|
||||
i {
|
||||
color: $primary;
|
||||
margin: 0 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-prefill {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: $blue-50;
|
||||
border: 1px solid $blue-100;
|
||||
border-left: 3px solid $blue-700;
|
||||
border-radius: 8px;
|
||||
padding: 10px 14px;
|
||||
margin-bottom: 14px;
|
||||
|
||||
> i:first-child {
|
||||
font-size: 16px;
|
||||
color: $blue-700;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
> div {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: $gray-500;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 13px;
|
||||
color: $blue-900;
|
||||
font-weight: 500;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
&__lock {
|
||||
font-size: 12px;
|
||||
color: $gray-500;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&--teal {
|
||||
background: $teal-50;
|
||||
border-color: $teal-50;
|
||||
border-left-color: $teal-700;
|
||||
|
||||
> i:first-child {
|
||||
color: $teal-700;
|
||||
}
|
||||
|
||||
.kyc-prefill__value {
|
||||
color: $teal-900;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-input-wrap,
|
||||
.kyc-select-wrap {
|
||||
position: relative;
|
||||
display: block;
|
||||
|
||||
&__icon {
|
||||
position: absolute;
|
||||
left: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: $gray-500;
|
||||
font-size: 13px;
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-select-wrap {
|
||||
&__chevron {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
color: $gray-500;
|
||||
font-size: 11px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.kyc-select {
|
||||
padding-left: 36px;
|
||||
padding-right: 32px;
|
||||
background-image: none;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-input-wrap .kyc-input,
|
||||
.kyc-input.kyc-input--padded {
|
||||
padding-left: 38px;
|
||||
}
|
||||
|
||||
/* Date input — match styling of other kyc inputs */
|
||||
.kyc-input--date {
|
||||
font-family: inherit;
|
||||
color: $gray-900;
|
||||
cursor: pointer;
|
||||
min-height: 40px;
|
||||
padding-right: 14px !important;
|
||||
|
||||
&::-webkit-calendar-picker-indicator {
|
||||
opacity: 0.55;
|
||||
cursor: pointer;
|
||||
filter: grayscale(1);
|
||||
transition: opacity 0.15s;
|
||||
}
|
||||
|
||||
&:hover::-webkit-calendar-picker-indicator {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
&::-webkit-datetime-edit {
|
||||
color: $gray-900;
|
||||
padding-top: 1px;
|
||||
}
|
||||
|
||||
&::-webkit-datetime-edit-fields-wrapper {
|
||||
color: $gray-900;
|
||||
}
|
||||
|
||||
&::-webkit-datetime-edit-text {
|
||||
color: $gray-500;
|
||||
padding: 0 2px;
|
||||
}
|
||||
|
||||
&:invalid::-webkit-datetime-edit,
|
||||
&:not(:focus):placeholder-shown::-webkit-datetime-edit {
|
||||
color: $gray-500;
|
||||
}
|
||||
}
|
||||
|
||||
/* Notes section — textarea */
|
||||
.kyc-section--notes {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.kyc-textarea {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid $gray-200;
|
||||
border-radius: 8px;
|
||||
background: $gray-50;
|
||||
color: $gray-900;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
resize: vertical;
|
||||
min-height: 60px;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
background 0.15s;
|
||||
|
||||
&::placeholder {
|
||||
color: $gray-500;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $primary;
|
||||
background: $white;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-loading {
|
||||
font-size: 13px;
|
||||
color: $gray-500;
|
||||
padding: 8px 0 14px;
|
||||
|
||||
i {
|
||||
margin-right: 6px;
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.kyc-section {
|
||||
background: $white;
|
||||
border: 1px solid $gray-200;
|
||||
border-radius: 12px;
|
||||
padding: 18px;
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid $gray-200;
|
||||
}
|
||||
|
||||
&__titles {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: $gray-900;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
i {
|
||||
color: $primary;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
&__sub {
|
||||
font-size: 12px;
|
||||
color: $gray-500;
|
||||
margin: 2px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-badge {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
flex-shrink: 0;
|
||||
|
||||
&--blue {
|
||||
background: $blue-50;
|
||||
color: $blue-700;
|
||||
border: 1px solid $blue-100;
|
||||
}
|
||||
|
||||
&--teal {
|
||||
background: $teal-50;
|
||||
color: $teal-700;
|
||||
border: 1px solid $teal-50;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-row {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&--2 {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-mb-sm {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.kyc-field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.kyc-label {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: $gray-700;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
i {
|
||||
color: $gray-500;
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-opt {
|
||||
font-size: 11px;
|
||||
color: $gray-500;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.kyc-input {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border: 1px solid $gray-200;
|
||||
border-radius: 8px;
|
||||
background: $gray-50;
|
||||
color: $gray-900;
|
||||
font-size: 13px;
|
||||
font-family: inherit;
|
||||
transition:
|
||||
border-color 0.15s,
|
||||
background 0.15s;
|
||||
|
||||
&::placeholder {
|
||||
color: $gray-500;
|
||||
}
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
border-color: $primary;
|
||||
background: $white;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
background: $gray-100;
|
||||
color: $gray-500;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-select {
|
||||
appearance: none;
|
||||
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='10' height='6' viewBox='0 0 10 6'><path fill='%235f6368' d='M0 0l5 6 5-6z'/></svg>");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 12px center;
|
||||
padding-right: 32px;
|
||||
}
|
||||
|
||||
.kyc-banner {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 10px 12px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
margin-bottom: 12px;
|
||||
|
||||
i {
|
||||
font-size: 14px;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
|
||||
&--blue {
|
||||
background: $blue-50;
|
||||
color: $blue-900;
|
||||
border-left: 3px solid $blue-700;
|
||||
}
|
||||
|
||||
&--amber {
|
||||
background: $amber-50;
|
||||
color: $amber-900;
|
||||
border-left: 3px solid $amber-700;
|
||||
}
|
||||
|
||||
&--teal {
|
||||
background: $teal-50;
|
||||
color: $teal-900;
|
||||
border-left: 3px solid $teal-700;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-upload-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 12px;
|
||||
margin-top: 4px;
|
||||
|
||||
&--two {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-upload-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.kyc-tag {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.3px;
|
||||
|
||||
&--req {
|
||||
background: $red-50;
|
||||
color: $red-900;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-dropzone {
|
||||
display: block;
|
||||
border: 1.5px dashed $gray-200;
|
||||
border-radius: 8px;
|
||||
padding: 16px 12px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
background: $gray-50;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
border-color: $primary;
|
||||
background: $gray-100;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 20px;
|
||||
color: $gray-500;
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 13px;
|
||||
color: $gray-700;
|
||||
margin: 0 0 2px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__hint {
|
||||
font-size: 11px;
|
||||
color: $gray-500;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 11px;
|
||||
color: $teal-900;
|
||||
font-weight: 600;
|
||||
margin: 6px 0 0;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
&--has {
|
||||
border-color: $teal-accent;
|
||||
border-style: solid;
|
||||
background: $teal-50;
|
||||
|
||||
.kyc-dropzone__icon {
|
||||
color: $teal-accent;
|
||||
}
|
||||
|
||||
.kyc-dropzone__text {
|
||||
color: $teal-900;
|
||||
}
|
||||
}
|
||||
|
||||
&--req {
|
||||
border-color: $red-700;
|
||||
background: $red-50;
|
||||
|
||||
&:hover {
|
||||
background: $red-100;
|
||||
}
|
||||
|
||||
.kyc-dropzone__icon {
|
||||
color: $red-700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-chip-wrap {
|
||||
background: $gray-50;
|
||||
border: 1px solid $gray-200;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
}
|
||||
|
||||
.kyc-chip-group-label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: $gray-500;
|
||||
letter-spacing: 0.06em;
|
||||
text-transform: uppercase;
|
||||
margin-bottom: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
i {
|
||||
color: $gray-500;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-chip-grid {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.kyc-chip-divider {
|
||||
height: 1px;
|
||||
background: $gray-200;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.kyc-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 6px 12px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
border: 1px solid $gray-200;
|
||||
background: $white;
|
||||
color: $gray-700;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
white-space: nowrap;
|
||||
user-select: none;
|
||||
|
||||
i {
|
||||
font-size: 11px;
|
||||
color: $gray-500;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: $teal-700;
|
||||
color: $teal-900;
|
||||
|
||||
i {
|
||||
color: $teal-700;
|
||||
}
|
||||
}
|
||||
|
||||
&--sel {
|
||||
background: $teal-50;
|
||||
border-color: $teal-700;
|
||||
color: $teal-900;
|
||||
font-weight: 600;
|
||||
|
||||
i {
|
||||
color: $teal-accent;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-status-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
font-size: 12px;
|
||||
color: $gray-500;
|
||||
margin-top: 4px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid $gray-200;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
|
||||
span i {
|
||||
color: $gray-500;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.kyc-status-hint i {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-status-hint {
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.kyc-dot {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border-radius: 50%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-right: 6px;
|
||||
background: $gray-200;
|
||||
color: $white;
|
||||
transition: background 0.3s;
|
||||
font-size: 9px;
|
||||
|
||||
&--done {
|
||||
background: $teal-accent;
|
||||
}
|
||||
}
|
||||
|
||||
.kyc-merged {
|
||||
margin-top: 14px;
|
||||
padding: 12px 14px;
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 10px;
|
||||
|
||||
&__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
color: #0c4a6e;
|
||||
|
||||
i {
|
||||
margin-right: 6px;
|
||||
}
|
||||
}
|
||||
|
||||
&__hint {
|
||||
font-size: 11px;
|
||||
color: #075985;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
&__preview {
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 8px;
|
||||
text-align: center;
|
||||
max-height: 320px;
|
||||
overflow: auto;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
max-height: 300px;
|
||||
display: inline-block;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
|
||||
&__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-top: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__btn {
|
||||
background: #fff;
|
||||
border: 1px solid #0284c7;
|
||||
color: #0284c7;
|
||||
border-radius: 6px;
|
||||
padding: 6px 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-family: inherit;
|
||||
transition: all 0.15s;
|
||||
|
||||
i {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #0284c7;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 680px) {
|
||||
.kyc-row--2,
|
||||
.kyc-upload-grid--two {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
+478
-44
@@ -8,16 +8,150 @@
|
||||
|
||||
import { Component, OnInit, inject } from '@angular/core';
|
||||
import {
|
||||
MatDialog,
|
||||
MatDialogRef,
|
||||
MAT_DIALOG_DATA,
|
||||
MatDialogTitle,
|
||||
MatDialogActions,
|
||||
MatDialogClose
|
||||
} from '@angular/material/dialog';
|
||||
import { UntypedFormGroup, UntypedFormBuilder, Validators, FormControl, ReactiveFormsModule } from '@angular/forms';
|
||||
import { ImageCropperDialogComponent } from '../image-cropper-dialog/image-cropper-dialog.component';
|
||||
import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms';
|
||||
import { FileUploadComponent } from '../../../../shared/file-upload/file-upload.component';
|
||||
import { MatIconButton } from '@angular/material/button';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { ClientsService } from '../../../clients.service';
|
||||
|
||||
interface ProofType {
|
||||
id: string | number;
|
||||
name: string;
|
||||
code?: string;
|
||||
sides?: number;
|
||||
category?: string;
|
||||
documentKeyLabel?: string;
|
||||
documentKeyPlaceholder?: string;
|
||||
requiresDate?: boolean;
|
||||
info?: string;
|
||||
}
|
||||
|
||||
type SectionKey = 'id' | 'address';
|
||||
|
||||
/**
|
||||
* Client-side enrichment for KYC proof types. Backend currently returns
|
||||
* only { id, name, description (code), position, active, mandatory } — we
|
||||
* derive sides / category / banner text / input labels from the code.
|
||||
*/
|
||||
const PROOF_META: Record<string, Partial<ProofType>> = {
|
||||
PAN: {
|
||||
sides: 1,
|
||||
documentKeyLabel: 'PAN number',
|
||||
documentKeyPlaceholder: 'e.g. ABCDE1234F',
|
||||
info: 'PAN card — upload front side only (single copy required).'
|
||||
},
|
||||
AADHAAR: {
|
||||
sides: 2,
|
||||
documentKeyLabel: '12-digit Aadhaar number',
|
||||
documentKeyPlaceholder: 'XXXX XXXX XXXX',
|
||||
category: 'Government-issued ID',
|
||||
info: 'Aadhaar — both front and back copies mandatory as per UIDAI guidelines.'
|
||||
},
|
||||
PASSPORT: {
|
||||
sides: 2,
|
||||
documentKeyLabel: 'Passport number',
|
||||
documentKeyPlaceholder: 'e.g. A1234567',
|
||||
category: 'Government-issued ID',
|
||||
info: 'Passport — upload front (photo page) and back (last page with address).'
|
||||
},
|
||||
VOTER_ID: {
|
||||
sides: 2,
|
||||
documentKeyLabel: 'EPIC number',
|
||||
documentKeyPlaceholder: 'Voter ID number',
|
||||
category: 'Government-issued ID',
|
||||
info: 'Voter ID (EPIC) — both sides required as address appears on the back.'
|
||||
},
|
||||
DRIVING_LICENSE: {
|
||||
sides: 2,
|
||||
documentKeyLabel: 'Driving licence number',
|
||||
documentKeyPlaceholder: 'DL number',
|
||||
category: 'Government-issued ID',
|
||||
info: 'Driving licence — both sides required; vehicle class and validity are on the back.'
|
||||
},
|
||||
NREGA_JOB_CARD: {
|
||||
sides: 1,
|
||||
documentKeyLabel: 'NREGA job card number',
|
||||
documentKeyPlaceholder: 'Job card number',
|
||||
info: 'NREGA job card — ensure government authority signature is visible.'
|
||||
},
|
||||
NPR_LETTER: {
|
||||
sides: 1,
|
||||
documentKeyLabel: 'Reference / letter number',
|
||||
documentKeyPlaceholder: 'NPR reference number',
|
||||
info: 'NPR letter — upload full document.'
|
||||
},
|
||||
UTILITY_BILL: {
|
||||
sides: 1,
|
||||
category: 'Utility bills',
|
||||
requiresDate: true,
|
||||
info: 'Utility bill — must not be older than 3 months.'
|
||||
},
|
||||
ELECTRICITY_BILL: {
|
||||
sides: 1,
|
||||
category: 'Utility bills',
|
||||
requiresDate: true,
|
||||
info: 'Electricity bill — must not be older than 3 months.'
|
||||
},
|
||||
WATER_BILL: {
|
||||
sides: 1,
|
||||
category: 'Utility bills',
|
||||
requiresDate: true,
|
||||
info: 'Water bill — must not be older than 3 months.'
|
||||
},
|
||||
GAS_BILL: {
|
||||
sides: 1,
|
||||
category: 'Utility bills',
|
||||
requiresDate: true,
|
||||
info: 'Gas bill — must not be older than 3 months.'
|
||||
},
|
||||
INTERNET_BILL: {
|
||||
sides: 1,
|
||||
category: 'Utility bills',
|
||||
requiresDate: true,
|
||||
info: 'Internet bill — must not be older than 3 months.'
|
||||
},
|
||||
POSTPAID_MOBILE_BILL: {
|
||||
sides: 1,
|
||||
category: 'Utility bills',
|
||||
requiresDate: true,
|
||||
info: 'Postpaid mobile bill — must not be older than 3 months.'
|
||||
},
|
||||
BANK_STATEMENT: {
|
||||
sides: 1,
|
||||
category: 'Financial & other',
|
||||
requiresDate: true,
|
||||
info: 'Bank statement / passbook — not older than 3 months.'
|
||||
},
|
||||
RENT_AGREEMENT: {
|
||||
sides: 1,
|
||||
category: 'Financial & other',
|
||||
info: 'Registered rent agreement — upload full document. Registration stamp must be visible.'
|
||||
},
|
||||
PROPERTY_TAX_RECEIPT: {
|
||||
sides: 1,
|
||||
category: 'Financial & other',
|
||||
requiresDate: true,
|
||||
info: 'Property tax receipt — latest receipt required.'
|
||||
},
|
||||
EMPLOYER_LETTER: {
|
||||
sides: 1,
|
||||
category: 'Financial & other',
|
||||
info: 'Employer letter — must be on official letterhead with HR/management signature.'
|
||||
},
|
||||
PENSION_PAYMENT_ORDER: {
|
||||
sides: 1,
|
||||
category: 'Financial & other',
|
||||
info: 'Pension payment order — upload full document issued by pension authority.'
|
||||
}
|
||||
};
|
||||
|
||||
@Component({
|
||||
selector: 'mifosx-upload-document-dialog',
|
||||
@@ -35,67 +169,66 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
export class UploadDocumentDialogComponent implements OnInit {
|
||||
dialogRef = inject<MatDialogRef<UploadDocumentDialogComponent>>(MatDialogRef);
|
||||
private formBuilder = inject(UntypedFormBuilder);
|
||||
private clientsService = inject(ClientsService);
|
||||
private dialog = inject(MatDialog);
|
||||
data = inject(MAT_DIALOG_DATA);
|
||||
|
||||
/** Upload Document form. */
|
||||
uploadDocumentForm: UntypedFormGroup;
|
||||
/** Upload Document Data */
|
||||
uploadDocumentData: any = [];
|
||||
/** Triggers identity fields (documentType, status, documentKey) */
|
||||
documentIdentifier = false;
|
||||
/** Entity Type */
|
||||
entityType: string;
|
||||
/** Allowed Document Types for identifiers */
|
||||
allowedDocumentTypes: any[] = [];
|
||||
/** Status options for identifiers */
|
||||
statusOptions: any[] = [];
|
||||
|
||||
/**
|
||||
* @param {MatDialogRef} dialogRef Dialog reference element
|
||||
* @param {FormBuilder} formBuilder Form Builder
|
||||
* @param {any} data Dialog Data
|
||||
*/
|
||||
/** Prefilled values from client registration (identifier mode) */
|
||||
clientName = '';
|
||||
clientAddress = '';
|
||||
|
||||
/** Identifier-mode state */
|
||||
idProofs: ProofType[] = [];
|
||||
addressProofs: ProofType[] = [];
|
||||
addressGroups: { label: string; items: ProofType[] }[] = [];
|
||||
loadingProofs = false;
|
||||
|
||||
selectedIdProof: ProofType | null = null;
|
||||
selectedAddrProof: ProofType | null = null;
|
||||
|
||||
idFiles: { front: File | null; back: File | null } = { front: null, back: null };
|
||||
addrFiles: { front: File | null; back: File | null } = { front: null, back: null };
|
||||
|
||||
/** Merged front+back PNG previews (data URLs) and File objects */
|
||||
idMergedFile: File | null = null;
|
||||
idMergedPreview: string | null = null;
|
||||
addrMergedFile: File | null = null;
|
||||
addrMergedPreview: string | null = null;
|
||||
|
||||
constructor() {
|
||||
const data = this.data;
|
||||
|
||||
this.documentIdentifier = data.documentIdentifier;
|
||||
this.entityType = data.entityType;
|
||||
this.allowedDocumentTypes = data.allowedDocumentTypes || [];
|
||||
this.statusOptions = data.statusOptions || [];
|
||||
this.documentIdentifier = !!data?.documentIdentifier;
|
||||
this.entityType = data?.entityType;
|
||||
this.clientName = data?.clientName || '';
|
||||
this.clientAddress = data?.clientAddress || '';
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.createUploadDocumentForm();
|
||||
if (this.documentIdentifier) {
|
||||
this.loadProofTypes();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the upload Document form.
|
||||
*/
|
||||
createUploadDocumentForm() {
|
||||
if (this.documentIdentifier) {
|
||||
// Unified form for identity: identifier fields + document upload
|
||||
this.uploadDocumentForm = this.formBuilder.group({
|
||||
documentTypeId: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
status: [
|
||||
'Active',
|
||||
Validators.required
|
||||
],
|
||||
documentKey: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
description: [''],
|
||||
fileName: [
|
||||
'',
|
||||
Validators.required
|
||||
],
|
||||
file: ['']
|
||||
id: this.formBuilder.group({
|
||||
proofId: [''],
|
||||
documentKey: ['']
|
||||
}),
|
||||
address: this.formBuilder.group({
|
||||
proofId: [''],
|
||||
issueDate: ['']
|
||||
}),
|
||||
description: ['']
|
||||
});
|
||||
} else {
|
||||
// Standard document upload form
|
||||
this.uploadDocumentForm = this.formBuilder.group({
|
||||
fileName: [
|
||||
'',
|
||||
@@ -107,10 +240,311 @@ export class UploadDocumentDialogComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
|
||||
private loadProofTypes(): void {
|
||||
this.loadingProofs = true;
|
||||
this.clientsService.getKycProofTypes().subscribe({
|
||||
next: (res: any) => {
|
||||
const activeFilter = (p: any) => p?.active !== false;
|
||||
this.idProofs = (res?.idProofs || []).filter(activeFilter).map((p: any) => this.normalizeProof(p));
|
||||
this.addressProofs = (res?.addressProofs || []).filter(activeFilter).map((p: any) => this.normalizeProof(p));
|
||||
this.buildAddressGroups();
|
||||
this.loadingProofs = false;
|
||||
},
|
||||
error: () => {
|
||||
this.loadingProofs = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private normalizeProof(p: any): ProofType {
|
||||
const code = (p.code || p.description || '').toString().toUpperCase();
|
||||
const meta = PROOF_META[code] || {};
|
||||
return {
|
||||
id: p.id,
|
||||
name: p.name,
|
||||
code,
|
||||
sides: p.sides || meta.sides || 1,
|
||||
category: p.category || meta.category || 'Other',
|
||||
documentKeyLabel: p.documentKeyLabel || meta.documentKeyLabel || 'Document number',
|
||||
documentKeyPlaceholder: p.documentKeyPlaceholder || meta.documentKeyPlaceholder || '',
|
||||
requiresDate: p.requiresDate ?? meta.requiresDate ?? false,
|
||||
info: p.info || meta.info || ''
|
||||
};
|
||||
}
|
||||
|
||||
private buildAddressGroups(): void {
|
||||
const map = new Map<string, ProofType[]>();
|
||||
this.addressProofs.forEach((p) => {
|
||||
const key = p.category || 'Other';
|
||||
if (!map.has(key)) map.set(key, []);
|
||||
map.get(key).push(p);
|
||||
});
|
||||
this.addressGroups = Array.from(map.entries()).map(
|
||||
([
|
||||
label,
|
||||
items
|
||||
]) => ({ label, items })
|
||||
);
|
||||
}
|
||||
|
||||
onIdProofChange(proofId: any): void {
|
||||
this.selectedIdProof = this.idProofs.find((p) => String(p.id) === String(proofId)) || null;
|
||||
this.idFiles = { front: null, back: null };
|
||||
this.idMergedFile = null;
|
||||
this.idMergedPreview = null;
|
||||
}
|
||||
|
||||
selectAddrProof(proof: ProofType): void {
|
||||
this.selectedAddrProof = proof;
|
||||
this.uploadDocumentForm.get('address.proofId').setValue(proof.id);
|
||||
this.addrFiles = { front: null, back: null };
|
||||
this.addrMergedFile = null;
|
||||
this.addrMergedPreview = null;
|
||||
}
|
||||
|
||||
isAddrProofSelected(proof: ProofType): boolean {
|
||||
return this.selectedAddrProof?.id === proof.id;
|
||||
}
|
||||
|
||||
onFilePick(section: SectionKey, slot: 'front' | 'back', event: any): void {
|
||||
const file = event?.target?.files?.[0];
|
||||
// reset the input so picking the same file again still triggers change
|
||||
if (event?.target) event.target.value = '';
|
||||
if (!file) return;
|
||||
|
||||
// Non-image (e.g. PDF) — bypass cropping
|
||||
if (!file.type.startsWith('image/')) {
|
||||
this.setSlotFile(section, slot, file);
|
||||
this.maybeMerge(section);
|
||||
return;
|
||||
}
|
||||
|
||||
const ref = this.dialog.open(ImageCropperDialogComponent, {
|
||||
width: '720px',
|
||||
maxWidth: '95vw',
|
||||
data: { file, title: `Crop ${slot} side` }
|
||||
});
|
||||
|
||||
ref.afterClosed().subscribe((cropped: File | null) => {
|
||||
if (!cropped) return;
|
||||
this.setSlotFile(section, slot, cropped);
|
||||
this.maybeMerge(section);
|
||||
});
|
||||
}
|
||||
|
||||
reCrop(section: SectionKey, slot: 'front' | 'back'): void {
|
||||
const current = section === 'id' ? this.idFiles[slot] : this.addrFiles[slot];
|
||||
if (!current || !current.type.startsWith('image/')) return;
|
||||
const ref = this.dialog.open(ImageCropperDialogComponent, {
|
||||
width: '720px',
|
||||
maxWidth: '95vw',
|
||||
data: { file: current, title: `Re-crop ${slot} side` }
|
||||
});
|
||||
ref.afterClosed().subscribe((cropped: File | null) => {
|
||||
if (!cropped) return;
|
||||
this.setSlotFile(section, slot, cropped);
|
||||
this.maybeMerge(section);
|
||||
});
|
||||
}
|
||||
|
||||
private setSlotFile(section: SectionKey, slot: 'front' | 'back', file: File): void {
|
||||
if (section === 'id') this.idFiles[slot] = file;
|
||||
else this.addrFiles[slot] = file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets file form control value.
|
||||
* @param {any} $event file change event.
|
||||
* If a section has both image sides ready, build a single merged PNG
|
||||
* (front stacked above back) and store it as the file we'll actually upload.
|
||||
*/
|
||||
private maybeMerge(section: SectionKey): void {
|
||||
const proof = section === 'id' ? this.selectedIdProof : this.selectedAddrProof;
|
||||
if (!proof || proof.sides !== 2) {
|
||||
this.resetMerged(section);
|
||||
return;
|
||||
}
|
||||
const front = section === 'id' ? this.idFiles.front : this.addrFiles.front;
|
||||
const back = section === 'id' ? this.idFiles.back : this.addrFiles.back;
|
||||
if (!front || !back) {
|
||||
this.resetMerged(section);
|
||||
return;
|
||||
}
|
||||
if (!front.type.startsWith('image/') || !back.type.startsWith('image/')) {
|
||||
// Cannot merge non-image files (PDFs); leave as separate.
|
||||
this.resetMerged(section);
|
||||
return;
|
||||
}
|
||||
|
||||
Promise.all([
|
||||
this.fileToImage(front),
|
||||
this.fileToImage(back)
|
||||
])
|
||||
.then(
|
||||
([
|
||||
fImg,
|
||||
bImg
|
||||
]) => {
|
||||
const targetWidth = Math.max(fImg.width, bImg.width);
|
||||
const fScale = targetWidth / fImg.width;
|
||||
const bScale = targetWidth / bImg.width;
|
||||
const fH = fImg.height * fScale;
|
||||
const bH = bImg.height * bScale;
|
||||
const gap = 20;
|
||||
const totalH = Math.round(fH + bH + gap);
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = targetWidth;
|
||||
canvas.height = totalH;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
|
||||
ctx.fillStyle = '#ffffff';
|
||||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||||
ctx.drawImage(fImg, 0, 0, targetWidth, fH);
|
||||
ctx.drawImage(bImg, 0, fH + gap, targetWidth, bH);
|
||||
|
||||
canvas.toBlob((blob) => {
|
||||
if (!blob) return;
|
||||
const merged = new File([blob], `${proof.name || 'document'}-combined.png`, { type: 'image/png' });
|
||||
const preview = canvas.toDataURL('image/png');
|
||||
if (section === 'id') {
|
||||
this.idMergedFile = merged;
|
||||
this.idMergedPreview = preview;
|
||||
} else {
|
||||
this.addrMergedFile = merged;
|
||||
this.addrMergedPreview = preview;
|
||||
}
|
||||
}, 'image/png');
|
||||
}
|
||||
)
|
||||
.catch(() => this.resetMerged(section));
|
||||
}
|
||||
|
||||
private resetMerged(section: SectionKey): void {
|
||||
if (section === 'id') {
|
||||
this.idMergedFile = null;
|
||||
this.idMergedPreview = null;
|
||||
} else {
|
||||
this.addrMergedFile = null;
|
||||
this.addrMergedPreview = null;
|
||||
}
|
||||
}
|
||||
|
||||
private fileToImage(file: File): Promise<HTMLImageElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = URL.createObjectURL(file);
|
||||
const img = new Image();
|
||||
img.onload = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
resolve(img);
|
||||
};
|
||||
img.onerror = () => {
|
||||
URL.revokeObjectURL(url);
|
||||
reject(new Error('image load failed'));
|
||||
};
|
||||
img.src = url;
|
||||
});
|
||||
}
|
||||
|
||||
getFileName(section: SectionKey, slot: 'front' | 'back'): string {
|
||||
const f = section === 'id' ? this.idFiles[slot] : this.addrFiles[slot];
|
||||
if (!f) return '';
|
||||
return f.name.length > 30 ? f.name.slice(0, 27) + '...' : f.name;
|
||||
}
|
||||
|
||||
isIdComplete(): boolean {
|
||||
return this.isSectionComplete('id');
|
||||
}
|
||||
|
||||
isAddressComplete(): boolean {
|
||||
return this.isSectionComplete('address');
|
||||
}
|
||||
|
||||
/** Section is "filled in" — has a proof selected */
|
||||
private isSectionTouched(section: SectionKey): boolean {
|
||||
return section === 'id' ? !!this.selectedIdProof : !!this.selectedAddrProof;
|
||||
}
|
||||
|
||||
/** Section has all required fields + files */
|
||||
private isSectionComplete(section: SectionKey): boolean {
|
||||
if (section === 'id') {
|
||||
const proof = this.selectedIdProof;
|
||||
if (!proof) return false;
|
||||
const v = this.uploadDocumentForm.get('id').value;
|
||||
if (!v.documentKey) return false;
|
||||
if (!this.idFiles.front) return false;
|
||||
if (proof.sides === 2 && !this.idFiles.back) return false;
|
||||
return true;
|
||||
}
|
||||
const proof = this.selectedAddrProof;
|
||||
if (!proof) return false;
|
||||
if (!this.addrFiles.front) return false;
|
||||
if (proof.sides === 2 && !this.addrFiles.back) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
get canSubmit(): boolean {
|
||||
if (!this.documentIdentifier) {
|
||||
return this.uploadDocumentForm?.valid;
|
||||
}
|
||||
const idTouched = this.isSectionTouched('id');
|
||||
const addrTouched = this.isSectionTouched('address');
|
||||
if (!idTouched && !addrTouched) return false;
|
||||
if (idTouched && !this.isSectionComplete('id')) return false;
|
||||
if (addrTouched && !this.isSectionComplete('address')) return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
submit(): void {
|
||||
if (!this.documentIdentifier) {
|
||||
this.dialogRef.close(this.uploadDocumentForm.value);
|
||||
return;
|
||||
}
|
||||
const result: any = { description: this.uploadDocumentForm.get('description').value, id: null, address: null };
|
||||
if (this.isSectionComplete('id')) {
|
||||
const v = this.uploadDocumentForm.get('id').value;
|
||||
result.id = {
|
||||
proof: this.selectedIdProof,
|
||||
proofId: this.selectedIdProof.id,
|
||||
documentKey: v.documentKey,
|
||||
fullName: this.clientName,
|
||||
files: this.buildSectionFiles('id')
|
||||
};
|
||||
}
|
||||
if (this.isSectionComplete('address')) {
|
||||
const v = this.uploadDocumentForm.get('address').value;
|
||||
result.address = {
|
||||
proof: this.selectedAddrProof,
|
||||
proofId: this.selectedAddrProof.id,
|
||||
address: this.clientAddress,
|
||||
issueDate: v.issueDate,
|
||||
files: this.buildSectionFiles('address')
|
||||
};
|
||||
}
|
||||
this.dialogRef.close(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the files payload for a section. When both sides are images and
|
||||
* have been merged into a single combined PNG, send only that one file.
|
||||
* Otherwise fall back to the individual front/back files.
|
||||
*/
|
||||
private buildSectionFiles(section: SectionKey): { slot: string; file: File }[] {
|
||||
const proof = section === 'id' ? this.selectedIdProof : this.selectedAddrProof;
|
||||
const front = section === 'id' ? this.idFiles.front : this.addrFiles.front;
|
||||
const back = section === 'id' ? this.idFiles.back : this.addrFiles.back;
|
||||
const merged = section === 'id' ? this.idMergedFile : this.addrMergedFile;
|
||||
|
||||
if (proof?.sides === 2 && merged) {
|
||||
return [{ slot: 'Combined', file: merged }];
|
||||
}
|
||||
|
||||
const files: { slot: string; file: File }[] = [];
|
||||
if (front) files.push({ slot: 'Front', file: front });
|
||||
if (proof?.sides === 2 && back) files.push({ slot: 'Back', file: back });
|
||||
return files;
|
||||
}
|
||||
|
||||
/** Legacy: kept for non-identifier mode file upload component */
|
||||
onFileSelect($event: any) {
|
||||
if ($event.target.files.length > 0) {
|
||||
const file = $event.target.files[0];
|
||||
|
||||
+110
-26
@@ -1,60 +1,144 @@
|
||||
.ec-view { padding: 16px 0; }
|
||||
/**
|
||||
* 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/.
|
||||
*/
|
||||
|
||||
.ec-view {
|
||||
padding: 16px 0;
|
||||
}
|
||||
|
||||
.ec-loading {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 12px;
|
||||
padding: 48px; color: #64748b;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 48px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.ec-header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 16px;
|
||||
h3 { font-size: 16px; font-weight: 600; color: #1e293b; display: flex; align-items: center; gap: 8px;
|
||||
fa-icon { color: #1565c0; }
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
|
||||
fa-icon {
|
||||
color: #1565c0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.ec-count {
|
||||
font-size: 12px; font-weight: 600; color: #059669;
|
||||
background: #ecfdf5; padding: 3px 10px; border-radius: 12px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #059669;
|
||||
background: #ecfdf5;
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.ec-empty {
|
||||
display: flex; flex-direction: column; align-items: center; gap: 8px;
|
||||
padding: 40px; color: #94a3b8; font-size: 14px;
|
||||
background: #f8fafc; border: 2px dashed #e2e8f0; border-radius: 10px;
|
||||
mat-icon { font-size: 36px; width: 36px; height: 36px; }
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 40px;
|
||||
color: #94a3b8;
|
||||
font-size: 14px;
|
||||
background: #f8fafc;
|
||||
border: 2px dashed #e2e8f0;
|
||||
border-radius: 10px;
|
||||
|
||||
mat-icon {
|
||||
font-size: 36px;
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
}
|
||||
}
|
||||
|
||||
.ec-card {
|
||||
border: 1px solid #e2e8f0; border-radius: 10px; margin-bottom: 12px; overflow: hidden;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.ec-card__header {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 12px 16px; background: #f8fafc; border-bottom: 1px solid #f1f5f9;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 12px 16px;
|
||||
background: #f8fafc;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.ec-card__tag {
|
||||
font-size: 11px; font-weight: 600; padding: 2px 8px; border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
.ec-card__title { font-size: 14px; font-weight: 600; color: #1e293b; flex: 1; }
|
||||
.ec-card__title {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.ec-card__badge {
|
||||
font-size: 10px; font-weight: 700; color: #059669;
|
||||
background: #ecfdf5; padding: 2px 8px; border-radius: 10px; border: 1px solid #a7f3d0;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
color: #059669;
|
||||
background: #ecfdf5;
|
||||
padding: 2px 8px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid #a7f3d0;
|
||||
}
|
||||
|
||||
.ec-card__body { padding: 14px 16px; }
|
||||
.ec-card__body {
|
||||
padding: 14px 16px;
|
||||
}
|
||||
|
||||
.ec-card__grid {
|
||||
display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px 16px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px 16px;
|
||||
}
|
||||
|
||||
.ec-card__field { display: flex; flex-direction: column; gap: 2px; }
|
||||
.ec-card__label { font-size: 10px; text-transform: uppercase; letter-spacing: 0.5px; color: #94a3b8; font-weight: 500; }
|
||||
.ec-card__value { font-size: 13px; font-weight: 500; color: #1e293b; }
|
||||
.ec-card__field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ec-card__grid { grid-template-columns: repeat(2, 1fr); }
|
||||
.ec-card__label {
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: #94a3b8;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.ec-card__value {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
@media (width <= 768px) {
|
||||
.ec-card__grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
}
|
||||
|
||||
+51
-11
@@ -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, inject } from '@angular/core';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
@@ -29,19 +37,45 @@ export class EntityClassificationTabComponent implements OnInit {
|
||||
categories = [
|
||||
{ key: 'msme', label: 'MSME', tag: 'Udyam / MSME', tagColor: '#0A5C6B', tagBg: '#E6F4F6', icon: 'cogs' },
|
||||
{ key: 'startup', label: 'Startup India', tag: 'DPIIT', tagColor: '#1A5FAB', tagBg: '#EAF2FB', icon: 'rocket' },
|
||||
{ key: 'mudra', label: 'Mudra Borrower', tag: 'PMMY', tagColor: '#8A5A00', tagBg: '#FEF3DC', icon: 'hand-holding-usd' },
|
||||
{
|
||||
key: 'mudra',
|
||||
label: 'Mudra Borrower',
|
||||
tag: 'PMMY',
|
||||
tagColor: '#8A5A00',
|
||||
tagBg: '#FEF3DC',
|
||||
icon: 'hand-holding-usd'
|
||||
},
|
||||
{ key: 'eou', label: 'Export Oriented Unit', tag: 'EOU', tagColor: '#1B7A4A', tagBg: '#E8F5EE', icon: 'ship' },
|
||||
{ key: 'importexport', label: 'Import Export', tag: 'DGFT / IEC', tagColor: '#5B3A8A', tagBg: '#F0EBF8', icon: 'exchange-alt' },
|
||||
{
|
||||
key: 'importexport',
|
||||
label: 'Import Export',
|
||||
tag: 'DGFT / IEC',
|
||||
tagColor: '#5B3A8A',
|
||||
tagBg: '#F0EBF8',
|
||||
icon: 'exchange-alt'
|
||||
},
|
||||
{ key: 'psl', label: 'Priority Sector', tag: 'RBI PSL', tagColor: '#7B1F1F', tagBg: '#FCEAEA', icon: 'bullseye' },
|
||||
{ key: 'nps', label: 'Non-Priority Sector', tag: 'General', tagColor: '#444441', tagBg: '#F1EFE8', icon: 'building' }
|
||||
{
|
||||
key: 'nps',
|
||||
label: 'Non-Priority Sector',
|
||||
tag: 'General',
|
||||
tagColor: '#444441',
|
||||
tagBg: '#F1EFE8',
|
||||
icon: 'building'
|
||||
}
|
||||
];
|
||||
|
||||
ngOnInit() {
|
||||
this.clientId = this.route.parent?.snapshot.paramMap.get('clientId') || '';
|
||||
if (this.clientId) {
|
||||
this.clientsService.getEntityClassifications(this.clientId).subscribe({
|
||||
next: (res: any) => { this.data = res; this.loading = false; },
|
||||
error: () => { this.loading = false; }
|
||||
next: (res: any) => {
|
||||
this.data = res;
|
||||
this.loading = false;
|
||||
},
|
||||
error: () => {
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.loading = false;
|
||||
@@ -50,19 +84,25 @@ export class EntityClassificationTabComponent implements OnInit {
|
||||
|
||||
get enabledCategories(): any[] {
|
||||
if (!this.data) return [];
|
||||
return this.categories.filter(c => this.data[c.key]?.enabled);
|
||||
return this.categories.filter((c) => this.data[c.key]?.enabled);
|
||||
}
|
||||
|
||||
getCategoryData(key: string): any {
|
||||
return this.data?.[key] || {};
|
||||
}
|
||||
|
||||
getFieldPairs(catData: any): { label: string, value: any }[] {
|
||||
getFieldPairs(catData: any): { label: string; value: any }[] {
|
||||
if (!catData) return [];
|
||||
const skip = ['id', 'clientId', 'enabled', 'locale', 'dateFormat'];
|
||||
const skip = [
|
||||
'id',
|
||||
'clientId',
|
||||
'enabled',
|
||||
'locale',
|
||||
'dateFormat'
|
||||
];
|
||||
return Object.keys(catData)
|
||||
.filter(k => !skip.includes(k) && catData[k] != null && catData[k] !== '' && catData[k] !== false)
|
||||
.map(k => ({
|
||||
.filter((k) => !skip.includes(k) && catData[k] != null && catData[k] !== '' && catData[k] !== false)
|
||||
.map((k) => ({
|
||||
label: this.formatLabel(k),
|
||||
value: this.formatValue(catData[k])
|
||||
}));
|
||||
@@ -71,7 +111,7 @@ export class EntityClassificationTabComponent implements OnInit {
|
||||
private formatLabel(key: string): string {
|
||||
return key
|
||||
.replace(/([A-Z])/g, ' $1')
|
||||
.replace(/^./, s => s.toUpperCase())
|
||||
.replace(/^./, (s) => s.toUpperCase())
|
||||
.replace(/Id$/, '')
|
||||
.trim();
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
-->
|
||||
|
||||
<div class="nom-container mat-typography">
|
||||
|
||||
<!-- ═══ FAMILY MEMBERS SECTION ═══ -->
|
||||
<div class="nom-header">
|
||||
<div class="nom-header__left">
|
||||
@@ -50,7 +49,12 @@
|
||||
<button mat-stroked-button color="primary" class="nom-actions__edit" (click)="editFamilyMember(member, i)">
|
||||
<fa-icon icon="edit" class="m-r-10"></fa-icon> Edit
|
||||
</button>
|
||||
<button mat-stroked-button color="warn" class="nom-actions__delete" (click)="deleteMember(member, 'Family Member')">
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="warn"
|
||||
class="nom-actions__delete"
|
||||
(click)="deleteMember(member, 'Family Member')"
|
||||
>
|
||||
<fa-icon icon="trash" class="m-r-10"></fa-icon> Delete
|
||||
</button>
|
||||
</div>
|
||||
@@ -91,5 +95,4 @@
|
||||
</mat-expansion-panel>
|
||||
}
|
||||
</mat-accordion>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -113,7 +113,7 @@ $orange-light: #fff3e0;
|
||||
margin-bottom: 12px !important;
|
||||
border-radius: 10px !important;
|
||||
border: 1px solid $gray-200 !important;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.06) !important;
|
||||
box-shadow: 0 1px 3px rgb(0 0 0 / 6%) !important;
|
||||
|
||||
&::ng-deep .mat-expansion-panel-body {
|
||||
padding: 0 20px 20px !important;
|
||||
@@ -224,11 +224,11 @@ $orange-light: #fff3e0;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (width <= 768px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
@media (width <= 480px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -263,7 +263,7 @@ $orange-light: #fff3e0;
|
||||
word-break: break-word;
|
||||
|
||||
&--mono {
|
||||
font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
|
||||
font-family: 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
@@ -80,12 +80,12 @@ export class FamilyMembersTabComponent {
|
||||
|
||||
/** Family members (non-nominee entries) */
|
||||
get familyMembers(): any[] {
|
||||
return this.clientFamilyMembers.filter(m => !m.nominationType || m.nominationType === '');
|
||||
return this.clientFamilyMembers.filter((m) => !m.nominationType || m.nominationType === '');
|
||||
}
|
||||
|
||||
/** Nominees (entries with nominationType set) */
|
||||
get nominees(): any[] {
|
||||
return this.clientFamilyMembers.filter(m => m.nominationType && m.nominationType !== '');
|
||||
return this.clientFamilyMembers.filter((m) => m.nominationType && m.nominationType !== '');
|
||||
}
|
||||
|
||||
/** Get relationship display name */
|
||||
|
||||
+4
-2
@@ -44,7 +44,9 @@ $white: #fff;
|
||||
height: 32px !important;
|
||||
line-height: 32px !important;
|
||||
color: $gray-500;
|
||||
transition: color 0.15s ease, background 0.15s ease;
|
||||
transition:
|
||||
color 0.15s ease,
|
||||
background 0.15s ease;
|
||||
|
||||
&:hover {
|
||||
color: $gray-900;
|
||||
@@ -176,7 +178,7 @@ $white: #fff;
|
||||
}
|
||||
|
||||
// Responsive
|
||||
@media (max-width: 600px) {
|
||||
@media (width <= 600px) {
|
||||
.fd-grid--cols-2 {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
+8
-2
@@ -48,8 +48,14 @@ export class NomineeDialogComponent implements OnInit {
|
||||
const member = this.data.member;
|
||||
this.nomineeForm = this.formBuilder.group({
|
||||
nominationType: [member?.nominationType || 'DA1'],
|
||||
firstName: [member?.firstName || '', Validators.required],
|
||||
relationshipId: [member?.relationshipId || '', Validators.required],
|
||||
firstName: [
|
||||
member?.firstName || '',
|
||||
Validators.required
|
||||
],
|
||||
relationshipId: [
|
||||
member?.relationshipId || '',
|
||||
Validators.required
|
||||
],
|
||||
dateOfBirth: [member?.dateOfBirth ? this.dateUtils.formatDate(member.dateOfBirth, 'yyyy-MM-dd') : null],
|
||||
percentageShare: [member?.percentageShare ?? 100],
|
||||
isDependent: [member?.isDependent || false],
|
||||
|
||||
@@ -61,13 +61,17 @@
|
||||
}
|
||||
@if (form60Data.dateOfBirth) {
|
||||
<div class="f60-detail">
|
||||
<span class="f60-detail__label"><mat-icon class="f60-detail__icon">calendar_today</mat-icon> Date of Birth</span>
|
||||
<span class="f60-detail__label"
|
||||
><mat-icon class="f60-detail__icon">calendar_today</mat-icon> Date of Birth</span
|
||||
>
|
||||
<span class="f60-detail__value">{{ form60Data.dateOfBirth | dateFormat }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (form60Data.fatherName) {
|
||||
<div class="f60-detail">
|
||||
<span class="f60-detail__label"><mat-icon class="f60-detail__icon">family_restroom</mat-icon> Father's Name</span>
|
||||
<span class="f60-detail__label"
|
||||
><mat-icon class="f60-detail__icon">family_restroom</mat-icon> Father's Name</span
|
||||
>
|
||||
<span class="f60-detail__value">{{ form60Data.fatherName }}</span>
|
||||
</div>
|
||||
}
|
||||
@@ -79,25 +83,33 @@
|
||||
}
|
||||
@if (form60Data.transactionType) {
|
||||
<div class="f60-detail">
|
||||
<span class="f60-detail__label"><mat-icon class="f60-detail__icon">receipt_long</mat-icon> Transaction Type</span>
|
||||
<span class="f60-detail__label"
|
||||
><mat-icon class="f60-detail__icon">receipt_long</mat-icon> Transaction Type</span
|
||||
>
|
||||
<span class="f60-detail__value">{{ form60Data.transactionType }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (form60Data.transactionAmount) {
|
||||
<div class="f60-detail">
|
||||
<span class="f60-detail__label"><mat-icon class="f60-detail__icon">currency_rupee</mat-icon> Transaction Amount</span>
|
||||
<span class="f60-detail__label"
|
||||
><mat-icon class="f60-detail__icon">currency_rupee</mat-icon> Transaction Amount</span
|
||||
>
|
||||
<span class="f60-detail__value">₹ {{ form60Data.transactionAmount }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (form60Data.estimatedTotalIncome) {
|
||||
<div class="f60-detail">
|
||||
<span class="f60-detail__label"><mat-icon class="f60-detail__icon">account_balance_wallet</mat-icon> Estimated Total Income</span>
|
||||
<span class="f60-detail__label"
|
||||
><mat-icon class="f60-detail__icon">account_balance_wallet</mat-icon> Estimated Total Income</span
|
||||
>
|
||||
<span class="f60-detail__value">₹ {{ form60Data.estimatedTotalIncome }}</span>
|
||||
</div>
|
||||
}
|
||||
@if (form60Data.reasonForNoPan) {
|
||||
<div class="f60-detail">
|
||||
<span class="f60-detail__label"><mat-icon class="f60-detail__icon">help_outline</mat-icon> Reason for No PAN</span>
|
||||
<span class="f60-detail__label"
|
||||
><mat-icon class="f60-detail__icon">help_outline</mat-icon> Reason for No PAN</span
|
||||
>
|
||||
<span class="f60-detail__value">{{ form60Data.reasonForNoPan }}</span>
|
||||
</div>
|
||||
}
|
||||
@@ -181,7 +193,8 @@
|
||||
<!-- Declaration -->
|
||||
<div class="f60-declaration">
|
||||
<mat-checkbox formControlName="declaration">
|
||||
I hereby declare that I do not have a Permanent Account Number (PAN) and the information provided above is true and correct to the best of my knowledge and belief.
|
||||
I hereby declare that I do not have a Permanent Account Number (PAN) and the information provided above
|
||||
is true and correct to the best of my knowledge and belief.
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -6,19 +6,26 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
$primary: #1E6BB8;
|
||||
$primary-light: #EBF3FF;
|
||||
$primary: #1e6bb8;
|
||||
$primary-light: #ebf3ff;
|
||||
$green: #16a34a;
|
||||
$green-light: #dcfce7;
|
||||
$amber: #f59e0b;
|
||||
$amber-light: #fef3c7;
|
||||
$border: #E2E8F0;
|
||||
$text: #1A2332;
|
||||
$muted: #64748B;
|
||||
$bg: #F0F4F8;
|
||||
$border: #e2e8f0;
|
||||
$text: #1a2332;
|
||||
$muted: #64748b;
|
||||
$bg: #f0f4f8;
|
||||
|
||||
.f60-container { padding: 20px 24px; }
|
||||
.f60-loading { display: flex; justify-content: center; padding: 40px; }
|
||||
.f60-container {
|
||||
padding: 20px 24px;
|
||||
}
|
||||
|
||||
.f60-loading {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
/* ── PAN OK — Not Required ── */
|
||||
.f60-pan-ok {
|
||||
@@ -29,96 +36,260 @@ $bg: #F0F4F8;
|
||||
border-radius: 12px;
|
||||
|
||||
&__icon-wrap {
|
||||
width: 72px; height: 72px; border-radius: 50%;
|
||||
background: #fff; display: inline-flex; align-items: center;
|
||||
justify-content: center; margin-bottom: 16px;
|
||||
width: 72px;
|
||||
height: 72px;
|
||||
border-radius: 50%;
|
||||
background: #fff;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba($green, 0.15);
|
||||
}
|
||||
&__icon { font-size: 40px; width: 40px; height: 40px; color: $green; }
|
||||
&__title { font-size: 20px; font-weight: 700; color: $text; margin: 0 0 8px; }
|
||||
|
||||
&__icon {
|
||||
font-size: 40px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: $green;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 20px;
|
||||
font-weight: 700;
|
||||
color: $text;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
font-size: 15px; color: $text; margin: 0 0 6px;
|
||||
display: flex; align-items: center; justify-content: center; gap: 6px; flex-wrap: wrap;
|
||||
strong { font-family: 'JetBrains Mono', monospace; letter-spacing: 0.5px; }
|
||||
font-size: 15px;
|
||||
color: $text;
|
||||
margin: 0 0 6px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
|
||||
strong {
|
||||
font-family: 'JetBrains Mono', monospace;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
}
|
||||
|
||||
&__tick {
|
||||
font-size: 18px !important;
|
||||
width: 18px !important;
|
||||
height: 18px !important;
|
||||
color: $green;
|
||||
}
|
||||
|
||||
&__verified {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: $green;
|
||||
}
|
||||
|
||||
&__rule {
|
||||
font-size: 12px;
|
||||
color: $muted;
|
||||
margin: 12px 0 0;
|
||||
}
|
||||
&__tick { font-size: 18px !important; width: 18px !important; height: 18px !important; color: $green; }
|
||||
&__verified { font-size: 13px; font-weight: 600; color: $green; }
|
||||
&__rule { font-size: 12px; color: $muted; margin: 12px 0 0; }
|
||||
}
|
||||
|
||||
/* ── Warning Banner ── */
|
||||
.f60-warning {
|
||||
display: flex; align-items: center; gap: 12px;
|
||||
background: $amber-light; padding: 14px 20px;
|
||||
border-left: 4px solid $amber; border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: $amber-light;
|
||||
padding: 14px 20px;
|
||||
border-left: 4px solid $amber;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
|
||||
&__icon { color: $amber; font-size: 24px; flex-shrink: 0; }
|
||||
&__text { font-size: 13px; color: #92400e; line-height: 1.5; strong { font-weight: 700; } }
|
||||
&__icon {
|
||||
color: $amber;
|
||||
font-size: 24px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__text {
|
||||
font-size: 13px;
|
||||
color: #92400e;
|
||||
line-height: 1.5;
|
||||
|
||||
strong {
|
||||
font-weight: 700;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Card ── */
|
||||
.f60-card {
|
||||
background: #fff; border: 1px solid $border; border-radius: 12px; overflow: hidden;
|
||||
background: #fff;
|
||||
border: 1px solid $border;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
|
||||
&__header {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
padding: 14px 20px; background: $bg; border-bottom: 1px solid $border;
|
||||
h3 { margin: 0; font-size: 15px; font-weight: 600; color: $text; flex: 1; }
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 14px 20px;
|
||||
background: $bg;
|
||||
border-bottom: 1px solid $border;
|
||||
|
||||
h3 {
|
||||
margin: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: $text;
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
&__header-icon { color: $primary; font-size: 22px; }
|
||||
|
||||
&__header-icon {
|
||||
color: $primary;
|
||||
font-size: 22px;
|
||||
}
|
||||
|
||||
&__status {
|
||||
font-size: 11px; font-weight: 600; color: $green; background: $green-light;
|
||||
padding: 3px 10px; border-radius: 12px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: $green;
|
||||
background: $green-light;
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
|
||||
&__edit {
|
||||
font-size: 12px !important; height: 30px !important; border-radius: 6px !important;
|
||||
mat-icon { font-size: 16px; width: 16px; height: 16px; margin-right: 4px; }
|
||||
font-size: 12px !important;
|
||||
height: 30px !important;
|
||||
border-radius: 6px !important;
|
||||
|
||||
mat-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 20px;
|
||||
}
|
||||
&__body { padding: 20px; }
|
||||
}
|
||||
|
||||
/* ── Detail Grid (read-only) ── */
|
||||
.f60-detail-grid {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 16px 24px;
|
||||
@media (max-width: 600px) { grid-template-columns: 1fr; }
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 16px 24px;
|
||||
|
||||
@media (width <= 600px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.f60-detail {
|
||||
display: flex; flex-direction: column;
|
||||
&--full { grid-column: 1 / -1; }
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
&--full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 11px; font-weight: 600; text-transform: uppercase;
|
||||
color: $muted; letter-spacing: 0.5px; margin-bottom: 4px;
|
||||
display: flex; align-items: center; gap: 4px;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
color: $muted;
|
||||
letter-spacing: 0.5px;
|
||||
margin-bottom: 4px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
font-size: 15px !important;
|
||||
width: 15px !important;
|
||||
height: 15px !important;
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: $text;
|
||||
}
|
||||
&__icon { font-size: 15px !important; width: 15px !important; height: 15px !important; color: $primary; }
|
||||
&__value { font-size: 15px; font-weight: 600; color: $text; }
|
||||
}
|
||||
|
||||
/* ── Form Grid ── */
|
||||
.f60-form-grid {
|
||||
display: grid; grid-template-columns: 1fr 1fr; gap: 4px 16px;
|
||||
@media (max-width: 600px) { grid-template-columns: 1fr; }
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4px 16px;
|
||||
|
||||
@media (width <= 600px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.f60-ff {
|
||||
width: 100%;
|
||||
|
||||
&--full {
|
||||
grid-column: 1 / -1;
|
||||
}
|
||||
}
|
||||
.f60-ff { width: 100%; &--full { grid-column: 1 / -1; } }
|
||||
|
||||
.f60-prefix {
|
||||
display: inline-flex; align-items: center;
|
||||
padding-left: 4px; padding-right: 8px; margin-right: 8px;
|
||||
border-right: 1.5px solid #cbd5e1; line-height: 1;
|
||||
mat-icon { font-size: 20px; width: 20px; height: 20px; color: $primary; }
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding-left: 4px;
|
||||
padding-right: 8px;
|
||||
margin-right: 8px;
|
||||
border-right: 1.5px solid #cbd5e1;
|
||||
line-height: 1;
|
||||
|
||||
mat-icon {
|
||||
font-size: 20px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
/* ── Declaration ── */
|
||||
.f60-declaration {
|
||||
margin: 12px 0 20px; padding: 14px 16px;
|
||||
background: $bg; border-radius: 8px; border: 1px solid $border;
|
||||
font-size: 13px; line-height: 1.5; color: $muted;
|
||||
margin: 12px 0 20px;
|
||||
padding: 14px 16px;
|
||||
background: $bg;
|
||||
border-radius: 8px;
|
||||
border: 1px solid $border;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
color: $muted;
|
||||
}
|
||||
|
||||
/* ── Actions ── */
|
||||
.f60-actions {
|
||||
display: flex; justify-content: flex-end; gap: 10px;
|
||||
button { border-radius: 8px !important; font-weight: 600 !important; }
|
||||
mat-icon { font-size: 18px; width: 18px; height: 18px; margin-right: 4px; vertical-align: middle; }
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
|
||||
button {
|
||||
border-radius: 8px !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
margin-right: 4px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,33 +90,37 @@ export class Form60TabComponent implements OnInit, OnDestroy {
|
||||
|
||||
private loadForm60Purposes(): void {
|
||||
this.loadingPurposes = true;
|
||||
this.clientsService.getCodeValuesByName('FORM_60_PURPOSE').pipe(
|
||||
takeUntil(this.destroy$)
|
||||
).subscribe((values: any[]) => {
|
||||
this.transactionTypes = (values || [])
|
||||
.sort((a: any, b: any) => (a.position || 0) - (b.position || 0))
|
||||
.map((v: any) => ({ value: v.name, label: v.name }));
|
||||
this.loadingPurposes = false;
|
||||
});
|
||||
this.clientsService
|
||||
.getCodeValuesByName('FORM_60_PURPOSE')
|
||||
.pipe(takeUntil(this.destroy$))
|
||||
.subscribe((values: any[]) => {
|
||||
this.transactionTypes = (values || [])
|
||||
.sort((a: any, b: any) => (a.position || 0) - (b.position || 0))
|
||||
.map((v: any) => ({ value: v.name, label: v.name }));
|
||||
this.loadingPurposes = false;
|
||||
});
|
||||
}
|
||||
|
||||
private checkPanStatus(): void {
|
||||
this.checkingPan = true;
|
||||
this.clientsService.getClientKyc(this.clientId).pipe(
|
||||
takeUntil(this.destroy$),
|
||||
catchError(() => of(null))
|
||||
).subscribe((kyc: any) => {
|
||||
this.checkingPan = false;
|
||||
if (kyc?.panNumber) {
|
||||
this.hasPan = true;
|
||||
this.panNumber = kyc.panNumber;
|
||||
this.panVerified = kyc.panVerified || false;
|
||||
this.loading = false;
|
||||
} else {
|
||||
this.hasPan = false;
|
||||
this.loadForm60Data();
|
||||
}
|
||||
});
|
||||
this.clientsService
|
||||
.getClientKyc(this.clientId)
|
||||
.pipe(
|
||||
takeUntil(this.destroy$),
|
||||
catchError(() => of(null))
|
||||
)
|
||||
.subscribe((kyc: any) => {
|
||||
this.checkingPan = false;
|
||||
if (kyc?.panNumber) {
|
||||
this.hasPan = true;
|
||||
this.panNumber = kyc.panNumber;
|
||||
this.panVerified = kyc.panVerified || false;
|
||||
this.loading = false;
|
||||
} else {
|
||||
this.hasPan = false;
|
||||
this.loadForm60Data();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private createForm(): void {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
-->
|
||||
|
||||
<div class="general-tab-container mat-typography">
|
||||
|
||||
<!-- Performance History - KPI Stat Cards -->
|
||||
<section class="performance-section">
|
||||
<h3 class="performance-title">{{ 'labels.heading.Performance History' | translate }}</h3>
|
||||
@@ -164,7 +163,9 @@
|
||||
<td mat-footer-cell *matFooterCellDef [attr.colspan]="openSavingsColumns.length">
|
||||
<div class="table-empty-state">
|
||||
<i class="fa fa-folder-open-o table-empty-state__icon"></i>
|
||||
<p class="table-empty-state__text">{{ 'labels.messages.No Active Saving Accounts Available' | translate }}</p>
|
||||
<p class="table-empty-state__text">
|
||||
{{ 'labels.messages.No Active Saving Accounts Available' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@@ -211,7 +212,9 @@
|
||||
<td mat-footer-cell *matFooterCellDef [attr.colspan]="closedSavingsColumns.length">
|
||||
<div class="table-empty-state">
|
||||
<i class="fa fa-folder-open-o table-empty-state__icon"></i>
|
||||
<p class="table-empty-state__text">{{ 'labels.messages.No Closed Saving Accounts Available' | translate }}</p>
|
||||
<p class="table-empty-state__text">
|
||||
{{ 'labels.messages.No Closed Saving Accounts Available' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@@ -258,7 +261,8 @@
|
||||
<ng-container matColumnDef="Name">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.name' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let element">
|
||||
<i class="fa fa-stop" [ngClass]="!(element.isWaived || element.isPaid) | statusLookup"></i> {{ element.name }}
|
||||
<i class="fa fa-stop" [ngClass]="!(element.isWaived || element.isPaid) | statusLookup"></i>
|
||||
{{ element.name }}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@@ -322,7 +326,11 @@
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="upcomingChargesColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: upcomingChargesColumns" [routerLink]="['../', 'charges', row.id]"></tr>
|
||||
<tr
|
||||
mat-row
|
||||
*matRowDef="let row; columns: upcomingChargesColumns"
|
||||
[routerLink]="['../', 'charges', row.id]"
|
||||
></tr>
|
||||
<tr mat-footer-row *matFooterRowDef="upcomingCharges?.length === 0 ? ['no-data'] : []"></tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -407,7 +415,9 @@
|
||||
<td mat-footer-cell *matFooterCellDef [attr.colspan]="openSavingsColumns.length">
|
||||
<div class="table-empty-state">
|
||||
<i class="fa fa-folder-open-o table-empty-state__icon"></i>
|
||||
<p class="table-empty-state__text">{{ 'labels.messages.No Active Fixed Deposit Accounts Available' | translate }}</p>
|
||||
<p class="table-empty-state__text">
|
||||
{{ 'labels.messages.No Active Fixed Deposit Accounts Available' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@@ -454,7 +464,9 @@
|
||||
<td mat-footer-cell *matFooterCellDef [attr.colspan]="closedSavingsColumns.length">
|
||||
<div class="table-empty-state">
|
||||
<i class="fa fa-folder-open-o table-empty-state__icon"></i>
|
||||
<p class="table-empty-state__text">{{ 'labels.messages.No Closed Fixed Deposit Accounts Available' | translate }}</p>
|
||||
<p class="table-empty-state__text">
|
||||
{{ 'labels.messages.No Closed Fixed Deposit Accounts Available' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@@ -559,7 +571,9 @@
|
||||
<td mat-footer-cell *matFooterCellDef [attr.colspan]="openSavingsColumns.length">
|
||||
<div class="table-empty-state">
|
||||
<i class="fa fa-folder-open-o table-empty-state__icon"></i>
|
||||
<p class="table-empty-state__text">{{ 'labels.messages.No Active Recurring Deposit Accounts Available' | translate }}</p>
|
||||
<p class="table-empty-state__text">
|
||||
{{ 'labels.messages.No Active Recurring Deposit Accounts Available' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@@ -607,7 +621,9 @@
|
||||
<td mat-footer-cell *matFooterCellDef [attr.colspan]="closedSavingsColumns.length">
|
||||
<div class="table-empty-state">
|
||||
<i class="fa fa-folder-open-o table-empty-state__icon"></i>
|
||||
<p class="table-empty-state__text">{{ 'labels.messages.No Closed Recurring Deposit Accounts Available' | translate }}</p>
|
||||
<p class="table-empty-state__text">
|
||||
{{ 'labels.messages.No Closed Recurring Deposit Accounts Available' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@@ -775,7 +791,9 @@
|
||||
<td mat-footer-cell *matFooterCellDef [attr.colspan]="openLoansColumns.length">
|
||||
<div class="table-empty-state">
|
||||
<i class="fa fa-folder-open-o table-empty-state__icon"></i>
|
||||
<p class="table-empty-state__text">{{ 'labels.messages.No Active Loan Accounts Available' | translate }}</p>
|
||||
<p class="table-empty-state__text">
|
||||
{{ 'labels.messages.No Active Loan Accounts Available' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@@ -867,7 +885,9 @@
|
||||
<td mat-footer-cell *matFooterCellDef [attr.colspan]="closedLoansColumns.length">
|
||||
<div class="table-empty-state">
|
||||
<i class="fa fa-folder-open-o table-empty-state__icon"></i>
|
||||
<p class="table-empty-state__text">{{ 'labels.messages.No Closed Loan Accounts Available' | translate }}</p>
|
||||
<p class="table-empty-state__text">
|
||||
{{ 'labels.messages.No Closed Loan Accounts Available' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@@ -924,7 +944,8 @@
|
||||
<th mat-header-cell class="r-amount" *matHeaderCellDef>{{ 'labels.inputs.Total Value' | translate }}</th>
|
||||
<td mat-cell class="r-amount" *matCellDef="let element">
|
||||
{{
|
||||
element.basePrice * element.quantity | currency: element.currency?.code || 'USD' : 'symbol-narrow' : '1.2-2'
|
||||
element.basePrice * element.quantity
|
||||
| currency: element.currency?.code || 'USD' : 'symbol-narrow' : '1.2-2'
|
||||
}}
|
||||
</td>
|
||||
</ng-container>
|
||||
@@ -1043,7 +1064,9 @@
|
||||
<td mat-footer-cell *matFooterCellDef [attr.colspan]="openSharesColumns.length">
|
||||
<div class="table-empty-state">
|
||||
<i class="fa fa-folder-open-o table-empty-state__icon"></i>
|
||||
<p class="table-empty-state__text">{{ 'labels.messages.No Active Share Accounts Available' | translate }}</p>
|
||||
<p class="table-empty-state__text">
|
||||
{{ 'labels.messages.No Active Share Accounts Available' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
@@ -1094,7 +1117,9 @@
|
||||
<td mat-footer-cell *matFooterCellDef [attr.colspan]="closedSharesColumns.length">
|
||||
<div class="table-empty-state">
|
||||
<i class="fa fa-folder-open-o table-empty-state__icon"></i>
|
||||
<p class="table-empty-state__text">{{ 'labels.messages.No Closed Share Accounts Available' | translate }}</p>
|
||||
<p class="table-empty-state__text">
|
||||
{{ 'labels.messages.No Closed Share Accounts Available' | translate }}
|
||||
</p>
|
||||
</div>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@@ -24,7 +24,7 @@ $gray-300: #dadce0;
|
||||
$gray-600: #5f6368;
|
||||
$gray-700: #3c4043;
|
||||
$gray-900: #202124;
|
||||
$white: #ffffff;
|
||||
$white: #fff;
|
||||
$red: #d93025;
|
||||
|
||||
// ── Main Container ──
|
||||
@@ -41,7 +41,7 @@ $red: #d93025;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: $gray-900;
|
||||
margin: 0 0 16px 0;
|
||||
margin: 0 0 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -58,12 +58,16 @@ $red: #d93025;
|
||||
background: $white;
|
||||
border-radius: 10px;
|
||||
padding: 18px 20px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08), 0 1px 2px rgba(0, 0, 0, 0.06);
|
||||
box-shadow:
|
||||
0 1px 3px rgb(0 0 0 / 8%),
|
||||
0 1px 2px rgb(0 0 0 / 6%);
|
||||
border-left: 4px solid transparent;
|
||||
transition: box-shadow 0.2s ease, transform 0.2s ease;
|
||||
transition:
|
||||
box-shadow 0.2s ease,
|
||||
transform 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 10%);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
@@ -106,6 +110,7 @@ $red: #d93025;
|
||||
// Color variants
|
||||
&--blue {
|
||||
border-left-color: $primary-blue;
|
||||
|
||||
.stat-card__icon {
|
||||
background: $primary-blue-light;
|
||||
color: $primary-blue;
|
||||
@@ -114,6 +119,7 @@ $red: #d93025;
|
||||
|
||||
&--green {
|
||||
border-left-color: $green;
|
||||
|
||||
.stat-card__icon {
|
||||
background: $green-light;
|
||||
color: $green;
|
||||
@@ -122,6 +128,7 @@ $red: #d93025;
|
||||
|
||||
&--purple {
|
||||
border-left-color: $purple;
|
||||
|
||||
.stat-card__icon {
|
||||
background: $purple-light;
|
||||
color: $purple;
|
||||
@@ -130,6 +137,7 @@ $red: #d93025;
|
||||
|
||||
&--teal {
|
||||
border-left-color: $teal;
|
||||
|
||||
.stat-card__icon {
|
||||
background: $teal-light;
|
||||
color: $teal;
|
||||
@@ -138,6 +146,7 @@ $red: #d93025;
|
||||
|
||||
&--orange {
|
||||
border-left-color: $orange;
|
||||
|
||||
.stat-card__icon {
|
||||
background: $orange-light;
|
||||
color: $orange;
|
||||
@@ -204,7 +213,7 @@ $red: #d93025;
|
||||
.table-wrapper {
|
||||
border-radius: 0 0 8px 8px;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 1px 3px rgb(0 0 0 / 8%);
|
||||
border: 1px solid $gray-200;
|
||||
border-top: none;
|
||||
|
||||
@@ -214,6 +223,7 @@ $red: #d93025;
|
||||
}
|
||||
|
||||
// ── Mat Table Styling ──
|
||||
/* stylelint-disable selector-no-qualifying-type */
|
||||
table[mat-table] {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
@@ -307,6 +317,7 @@ table[mat-table] {
|
||||
font-size: 12px;
|
||||
}
|
||||
}
|
||||
/* stylelint-enable selector-no-qualifying-type */
|
||||
|
||||
// Status color classes (for the fa-stop dots)
|
||||
:host ::ng-deep {
|
||||
@@ -370,7 +381,7 @@ table[mat-table] {
|
||||
left: 0;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
background: rgb(0 0 0 / 50%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
@@ -384,7 +395,7 @@ table[mat-table] {
|
||||
width: 85%;
|
||||
max-width: 900px;
|
||||
position: relative;
|
||||
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
|
||||
box-shadow: 0 20px 60px rgb(0 0 0 / 30%);
|
||||
}
|
||||
|
||||
.pdf-modal-close {
|
||||
|
||||
@@ -14,7 +14,13 @@
|
||||
<h3 class="idt-header__title">{{ 'labels.heading.Identities' | translate }}</h3>
|
||||
<span class="idt-header__badge">{{ clientIdentities?.length || 0 }}</span>
|
||||
</div>
|
||||
<button mat-flat-button color="primary" class="idt-header__btn" (click)="addIdentifier()" *mifosxHasPermission="'CREATE_CLIENTIDENTIFIER'">
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
class="idt-header__btn"
|
||||
(click)="addIdentifier()"
|
||||
*mifosxHasPermission="'CREATE_CLIENTIDENTIFIER'"
|
||||
>
|
||||
<fa-icon icon="plus" class="m-r-10"></fa-icon>{{ 'labels.buttons.Add' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
@@ -35,30 +41,38 @@
|
||||
<div class="idt-card__title">
|
||||
<i class="fa fa-file-text idt-card__type-icon"></i>
|
||||
<span class="idt-card__type">{{ identity.documentType?.name }}</span>
|
||||
<span class="idt-card__status" [class.idt-card__status--active]="identity.status === 'clientIdentifierStatusType.active'" [class.idt-card__status--inactive]="identity.status !== 'clientIdentifierStatusType.active'">
|
||||
<span
|
||||
class="idt-card__status"
|
||||
[class.idt-card__status--active]="identity.status === 'clientIdentifierStatusType.active'"
|
||||
[class.idt-card__status--inactive]="identity.status !== 'clientIdentifierStatusType.active'"
|
||||
>
|
||||
{{ identity.status === 'clientIdentifierStatusType.active' ? 'Active' : 'Inactive' }}
|
||||
</span>
|
||||
</div>
|
||||
<button mat-stroked-button color="warn" class="idt-card__delete" (click)="deleteIdentifier(identity.clientId, identity.id, i)" *mifosxHasPermission="'DELETE_CLIENTIDENTIFIER'">
|
||||
<button
|
||||
mat-stroked-button
|
||||
color="warn"
|
||||
class="idt-card__delete"
|
||||
(click)="deleteIdentifier(identity.clientId, identity.id, i)"
|
||||
*mifosxHasPermission="'DELETE_CLIENTIDENTIFIER'"
|
||||
>
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Card Body -->
|
||||
<div class="idt-card__body">
|
||||
<div class="idt-grid">
|
||||
<div class="idt-field">
|
||||
<div class="idt-field__label">{{ 'labels.inputs.Id' | translate }}</div>
|
||||
<div class="idt-field__value idt-field__value--mono">{{ identity.id }}</div>
|
||||
</div>
|
||||
<div class="idt-field">
|
||||
<div class="idt-field__label">{{ 'labels.inputs.Document Key' | translate }}</div>
|
||||
<div class="idt-field__value idt-field__value--mono">{{ identity.documentKey }}</div>
|
||||
<div class="idt-meta">
|
||||
<div class="idt-meta__row">
|
||||
<i class="fa fa-key idt-meta__icon"></i>
|
||||
<span class="idt-meta__label">{{ 'labels.inputs.Document Key' | translate }}</span>
|
||||
<span class="idt-meta__value idt-meta__value--mono">{{ identity.documentKey || '—' }}</span>
|
||||
</div>
|
||||
@if (identity.description) {
|
||||
<div class="idt-field">
|
||||
<div class="idt-field__label">{{ 'labels.inputs.Description' | translate }}</div>
|
||||
<div class="idt-field__value">{{ identity.description }}</div>
|
||||
<div class="idt-meta__row">
|
||||
<i class="fa fa-sticky-note-o idt-meta__icon"></i>
|
||||
<span class="idt-meta__label">{{ 'labels.inputs.Description' | translate }}</span>
|
||||
<span class="idt-meta__value">{{ identity.description }}</span>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -68,25 +82,35 @@
|
||||
<div class="idt-docs">
|
||||
<div class="idt-docs__title">
|
||||
<i class="fa fa-paperclip"></i> {{ 'labels.inputs.Identity Documents' | translate }}
|
||||
<span class="idt-docs__count">{{ identity.documents.length }}</span>
|
||||
</div>
|
||||
<div class="idt-docs__grid">
|
||||
@for (document of identity.documents; track trackByDocumentId($index, document)) {
|
||||
<div class="idt-doc" [class.idt-doc--clickable]="isPreviewable(document)"
|
||||
<div
|
||||
class="idt-doc"
|
||||
[class.idt-doc--clickable]="isPreviewable(document)"
|
||||
(click)="isPreviewable(document) && openDocumentPreview(identity, document)"
|
||||
(keydown.enter)="isPreviewable(document) && openDocumentPreview(identity, document)"
|
||||
tabindex="0" role="button"
|
||||
[attr.aria-label]="('labels.buttons.Preview' | translate) + ': ' + document.name">
|
||||
tabindex="0"
|
||||
role="button"
|
||||
[attr.aria-label]="('labels.buttons.Preview' | translate) + ': ' + document.name"
|
||||
>
|
||||
@if (previewThumbnails[document.id]) {
|
||||
<img [src]="previewThumbnails[document.id]" [alt]="document.name" class="idt-doc__thumb" loading="lazy" />
|
||||
<img
|
||||
[src]="previewThumbnails[document.id]"
|
||||
[alt]="document.name"
|
||||
class="idt-doc__thumb"
|
||||
loading="lazy"
|
||||
/>
|
||||
} @else {
|
||||
<div class="idt-doc__placeholder">
|
||||
<i class="fa fa-file-o"></i>
|
||||
<i class="fa fa-file-text-o"></i>
|
||||
</div>
|
||||
}
|
||||
<div class="idt-doc__info">
|
||||
<span class="idt-doc__name">{{ document.name }}</span>
|
||||
@if (document.fileName) {
|
||||
<span class="idt-doc__file">{{ document.fileName }}</span>
|
||||
<span class="idt-doc__file"> <i class="fa fa-paperclip"></i> {{ document.fileName }} </span>
|
||||
}
|
||||
</div>
|
||||
@if (isPreviewable(document)) {
|
||||
|
||||
@@ -37,7 +37,10 @@ $red-light: #fce8e6;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&__icon { font-size: 16px; color: $primary; }
|
||||
&__icon {
|
||||
font-size: 16px;
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
@@ -71,198 +74,318 @@ $red-light: #fce8e6;
|
||||
border-radius: 10px;
|
||||
border: 1px dashed $gray-200;
|
||||
|
||||
&__icon { font-size: 32px; color: $gray-500; margin-bottom: 8px; }
|
||||
&__text { color: $gray-500; font-size: 13px; margin: 0; }
|
||||
&__icon {
|
||||
font-size: 32px;
|
||||
color: $gray-500;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&__text {
|
||||
color: $gray-500;
|
||||
font-size: 13px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Identity Card
|
||||
.idt-card {
|
||||
background: $white;
|
||||
border: 1px solid $gray-200;
|
||||
border-radius: 8px;
|
||||
border-radius: 10px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 12px;
|
||||
margin-bottom: 14px;
|
||||
transition:
|
||||
box-shadow 0.15s,
|
||||
border-color 0.15s;
|
||||
position: relative;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
inset: 0 auto 0 0;
|
||||
width: 3px;
|
||||
background: $primary;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
border-color: $primary-light;
|
||||
box-shadow: 0 2px 8px rgb(26 115 232 / 8%);
|
||||
}
|
||||
|
||||
&__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 12px 18px;
|
||||
background: $gray-50;
|
||||
border-bottom: 1px solid $gray-200;
|
||||
padding: 14px 18px 14px 22px;
|
||||
background: linear-gradient(to right, $primary-light 0%, $gray-50 40%, $white 100%);
|
||||
border-bottom: 1px solid $gray-100;
|
||||
}
|
||||
|
||||
&__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
gap: 12px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__type-icon {
|
||||
font-size: 14px;
|
||||
color: $primary;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
font-size: 15px;
|
||||
color: $white;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: $primary-light;
|
||||
border-radius: 6px;
|
||||
background: linear-gradient(135deg, $primary, #4285f4);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 6px rgb(26 115 232 / 25%);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__type {
|
||||
font-size: 14px;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: $gray-900;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
&__status {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 2px 9px;
|
||||
border-radius: 10px;
|
||||
font-weight: 700;
|
||||
padding: 3px 10px;
|
||||
border-radius: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.4px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
&--active { background: $green-light; color: $green; }
|
||||
&--inactive { background: $red-light; color: $red; }
|
||||
&::before {
|
||||
content: '';
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
&--active {
|
||||
background: $green-light;
|
||||
color: $green;
|
||||
|
||||
&::before {
|
||||
background: $green;
|
||||
}
|
||||
}
|
||||
|
||||
&--inactive {
|
||||
background: $red-light;
|
||||
color: $red;
|
||||
|
||||
&::before {
|
||||
background: $red;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__delete {
|
||||
width: 30px !important;
|
||||
height: 30px !important;
|
||||
min-width: 30px !important;
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
min-width: 32px !important;
|
||||
padding: 0 !important;
|
||||
border-radius: 6px !important;
|
||||
border-radius: 8px !important;
|
||||
border-color: $gray-200 !important;
|
||||
color: $gray-500 !important;
|
||||
background: $white !important;
|
||||
|
||||
&:hover { color: $red !important; border-color: $red !important; background: $red-light !important; }
|
||||
&:hover {
|
||||
color: $red !important;
|
||||
border-color: $red !important;
|
||||
background: $red-light !important;
|
||||
}
|
||||
|
||||
i { font-size: 12px; }
|
||||
i {
|
||||
font-size: 13px;
|
||||
}
|
||||
}
|
||||
|
||||
&__body {
|
||||
padding: 14px 18px;
|
||||
padding: 16px 18px 18px 22px;
|
||||
}
|
||||
}
|
||||
|
||||
// Details grid
|
||||
.idt-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0;
|
||||
border: 1px solid $gray-100;
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
// Metadata rows (compact, no grid)
|
||||
.idt-meta {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
background: $gray-50;
|
||||
border-radius: 8px;
|
||||
padding: 4px 0;
|
||||
margin-bottom: 14px;
|
||||
|
||||
@media (max-width: 600px) { grid-template-columns: 1fr; }
|
||||
}
|
||||
&__row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 14px;
|
||||
border-bottom: 1px dashed $gray-200;
|
||||
|
||||
.idt-field {
|
||||
padding: 8px 12px;
|
||||
border-bottom: 1px solid $gray-100;
|
||||
border-right: 1px solid $gray-100;
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-child(3n) { border-right: none; }
|
||||
&:hover { background: $gray-50; }
|
||||
&__icon {
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: $primary;
|
||||
font-size: 12px;
|
||||
background: $primary-light;
|
||||
border-radius: 5px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-size: 9px;
|
||||
font-weight: 600;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: $gray-500;
|
||||
margin-bottom: 2px;
|
||||
min-width: 95px;
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: $gray-900;
|
||||
word-break: break-word;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
|
||||
&--mono {
|
||||
font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
|
||||
font-family: 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 12px;
|
||||
letter-spacing: 0.3px;
|
||||
color: $primary;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 600px) {
|
||||
&__row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
&__label {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Documents section
|
||||
.idt-docs {
|
||||
margin-top: 12px;
|
||||
margin-top: 4px;
|
||||
|
||||
&__title {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
color: $gray-500;
|
||||
margin-bottom: 8px;
|
||||
margin-bottom: 10px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
|
||||
i { color: $primary; margin-right: 4px; }
|
||||
i {
|
||||
color: $primary;
|
||||
}
|
||||
}
|
||||
|
||||
&__count {
|
||||
background: $primary-light;
|
||||
color: $primary;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
padding: 1px 7px;
|
||||
border-radius: 8px;
|
||||
margin-left: 2px;
|
||||
}
|
||||
|
||||
&__grid {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.idt-doc {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
background: $gray-50;
|
||||
border: 1px solid $gray-100;
|
||||
border-radius: 6px;
|
||||
transition: all 0.12s;
|
||||
min-width: 180px;
|
||||
gap: 12px;
|
||||
padding: 10px 12px;
|
||||
background: $white;
|
||||
border: 1px solid $gray-200;
|
||||
border-radius: 8px;
|
||||
transition: all 0.15s;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&--clickable {
|
||||
cursor: pointer;
|
||||
|
||||
&:hover {
|
||||
border-color: $primary;
|
||||
background: $primary-light;
|
||||
box-shadow: 0 2px 8px rgb(26 115 232 / 10%);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
}
|
||||
|
||||
&__thumb {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 4px;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 6px;
|
||||
object-fit: cover;
|
||||
border: 1px solid $gray-200;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: $gray-200;
|
||||
border-radius: 4px;
|
||||
color: $gray-500;
|
||||
font-size: 14px;
|
||||
background: linear-gradient(135deg, $primary-light, $gray-100);
|
||||
border: 1px solid $gray-200;
|
||||
border-radius: 6px;
|
||||
color: $primary;
|
||||
font-size: 16px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: $gray-900;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
}
|
||||
|
||||
&__file {
|
||||
@@ -271,18 +394,36 @@ $red-light: #fce8e6;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
max-width: 150px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
|
||||
i {
|
||||
font-size: 9px;
|
||||
}
|
||||
}
|
||||
|
||||
&__preview {
|
||||
font-size: 12px;
|
||||
font-size: 13px;
|
||||
color: $primary;
|
||||
margin-left: auto;
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: $primary-light;
|
||||
border-radius: 6px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&:hover .idt-doc__preview { opacity: 1; }
|
||||
&:hover .idt-doc__preview {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.document-lightbox-host { display: none; }
|
||||
.document-lightbox-host {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -86,6 +86,10 @@ export class IdentitiesTabComponent implements OnDestroy {
|
||||
clientIdentifierTemplate: any;
|
||||
/** Client Id */
|
||||
clientId: string;
|
||||
/** Client display name (prefilled into dialog) */
|
||||
clientName = '';
|
||||
/** Formatted permanent address (prefilled into dialog) */
|
||||
clientAddress = '';
|
||||
/** Identities Columns */
|
||||
identitiesColumns: string[] = [
|
||||
'id',
|
||||
@@ -126,6 +130,36 @@ export class IdentitiesTabComponent implements OnDestroy {
|
||||
this.clientIdentifierTemplate = data.clientIdentifierTemplate;
|
||||
this.prefetchThumbnails();
|
||||
});
|
||||
|
||||
// Pull display name from parent route's clientViewData
|
||||
this.route.parent.data.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((data: any) => {
|
||||
this.clientName = data?.clientViewData?.displayName || '';
|
||||
});
|
||||
|
||||
// Pull permanent address (falls back to first available address)
|
||||
this.clientService.getClientAddressData(this.clientId).subscribe({
|
||||
next: (addresses: any[]) => {
|
||||
if (!addresses?.length) return;
|
||||
const permanent = addresses.find((a) => /permanent/i.test(a?.addressType || '')) || addresses[0];
|
||||
this.clientAddress = this.formatAddress(permanent);
|
||||
},
|
||||
error: () => undefined
|
||||
});
|
||||
}
|
||||
|
||||
private formatAddress(addr: any): string {
|
||||
if (!addr) return '';
|
||||
return [
|
||||
addr.addressLine1,
|
||||
addr.addressLine2,
|
||||
addr.townVillage,
|
||||
addr.city,
|
||||
addr.stateName,
|
||||
addr.countryName,
|
||||
addr.postalCode
|
||||
]
|
||||
.filter((p) => !!p && String(p).trim())
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
@@ -143,97 +177,94 @@ export class IdentitiesTabComponent implements OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Client Identifier with unified form (identifier + document upload)
|
||||
* Add Client Identifier — opens KYC dialog that may create up to two
|
||||
* identifiers (identity proof + address proof), each with front/back docs.
|
||||
*/
|
||||
addIdentifier() {
|
||||
// Translate document type names
|
||||
const translatedDocTypes = this.clientIdentifierTemplate.allowedDocumentTypes.map((docType: any) => ({
|
||||
...docType,
|
||||
name: this.translateService.instant(`labels.catalogs.${docType.name}`)
|
||||
}));
|
||||
|
||||
const statusOptions = [
|
||||
{ label: this.translateService.instant('labels.catalogs.Active'), value: 'Active' },
|
||||
{ label: this.translateService.instant('labels.catalogs.Inactive'), value: 'Inactive' }
|
||||
];
|
||||
|
||||
const dialogRef = this.dialog.open(UploadDocumentDialogComponent, {
|
||||
width: '700px',
|
||||
width: '880px',
|
||||
maxWidth: '95vw',
|
||||
data: {
|
||||
documentIdentifier: true,
|
||||
allowedDocumentTypes: translatedDocTypes,
|
||||
statusOptions: statusOptions
|
||||
clientName: this.clientName,
|
||||
clientAddress: this.clientAddress
|
||||
}
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe((response: any) => {
|
||||
if (response) {
|
||||
// Create identifier data
|
||||
const identifierData = {
|
||||
documentTypeId: response.documentTypeId,
|
||||
status: response.status,
|
||||
documentKey: response.documentKey,
|
||||
description: response.description
|
||||
if (!response) return;
|
||||
const description = response.description || '';
|
||||
if (response.id) {
|
||||
this.createIdentifierWithDocs(response.id, description, 'id');
|
||||
}
|
||||
if (response.address) {
|
||||
this.createIdentifierWithDocs(response.address, description, 'address');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Create one identifier from a KYC section payload, then upload each file
|
||||
* as a document attached to that identifier.
|
||||
*/
|
||||
private createIdentifierWithDocs(section: any, description: string, kind: 'id' | 'address'): void {
|
||||
const identifierData = {
|
||||
documentTypeId: section.proofId,
|
||||
status: 'Active',
|
||||
documentKey: kind === 'id' ? section.documentKey : `${section.proof?.name || 'Address Proof'} - ${this.clientId}`,
|
||||
description: kind === 'address' && section.address ? section.address : description
|
||||
};
|
||||
|
||||
this.clientService.addClientIdentifier(this.clientId, identifierData).subscribe({
|
||||
next: (res: any) => {
|
||||
const newIdentifierId = res.resourceId;
|
||||
const newIdentity: any = {
|
||||
id: newIdentifierId,
|
||||
description,
|
||||
documentType: { id: section.proof?.id, name: section.proof?.name },
|
||||
documentKey: identifierData.documentKey,
|
||||
documents: [] as any[],
|
||||
clientId: this.clientId,
|
||||
status: 'clientIdentifierStatusType.active'
|
||||
};
|
||||
|
||||
// First create the identifier
|
||||
this.clientService.addClientIdentifier(this.clientId, identifierData).subscribe({
|
||||
next: (res: any) => {
|
||||
const newIdentifierId = res.resourceId;
|
||||
const selectedDocType = this.clientIdentifierTemplate.allowedDocumentTypes.find(
|
||||
(doc: any) => doc.id === response.documentTypeId
|
||||
);
|
||||
const files: { slot: string; file: File }[] = section.files || [];
|
||||
if (!files.length) {
|
||||
this.clientIdentities.push(newIdentity);
|
||||
return;
|
||||
}
|
||||
|
||||
// Create new identity entry
|
||||
const newIdentity: any = {
|
||||
id: newIdentifierId,
|
||||
description: response.description,
|
||||
documentType: selectedDocType,
|
||||
documentKey: response.documentKey,
|
||||
documents: [] as any[],
|
||||
clientId: this.clientId,
|
||||
status:
|
||||
response.status === 'Active'
|
||||
? 'clientIdentifierStatusType.active'
|
||||
: 'clientIdentifierStatusType.inactive'
|
||||
};
|
||||
|
||||
// If file was uploaded, attach document to the identifier
|
||||
if (response.file) {
|
||||
const formData: FormData = new FormData();
|
||||
formData.append('name', response.fileName);
|
||||
formData.append('file', response.file);
|
||||
this.clientService.uploadClientIdentifierDocument(newIdentifierId, formData).subscribe({
|
||||
next: (docRes: any) => {
|
||||
const newDoc = {
|
||||
id: docRes.resourceId,
|
||||
parentEntityType: 'client_identifiers',
|
||||
parentEntityId: newIdentifierId,
|
||||
name: response.fileName,
|
||||
fileName: response.file.name
|
||||
};
|
||||
newIdentity.documents.push(newDoc);
|
||||
this.clientIdentities.push(newIdentity);
|
||||
// Array mutation triggers @for re-render
|
||||
this.setThumbnail(newDoc);
|
||||
},
|
||||
error: (err: any) => {
|
||||
console.error('Failed to upload document', err);
|
||||
// Still add the identifier even if document upload fails
|
||||
this.clientIdentities.push(newIdentity);
|
||||
// Array mutation triggers @for re-render
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// No file, just add the identifier
|
||||
this.clientIdentities.push(newIdentity);
|
||||
// Array mutation triggers @for re-render
|
||||
let pending = files.length;
|
||||
files.forEach((entry) => {
|
||||
const formData = new FormData();
|
||||
formData.append('name', `${section.proof?.name || 'Document'} — ${entry.slot}`);
|
||||
formData.append('file', entry.file);
|
||||
this.clientService.uploadClientIdentifierDocument(newIdentifierId, formData).subscribe({
|
||||
next: (docRes: any) => {
|
||||
const newDoc = {
|
||||
id: docRes.resourceId,
|
||||
parentEntityType: 'client_identifiers',
|
||||
parentEntityId: newIdentifierId,
|
||||
name: `${section.proof?.name || 'Document'} — ${entry.slot}`,
|
||||
fileName: entry.file.name
|
||||
};
|
||||
newIdentity.documents.push(newDoc);
|
||||
this.setThumbnail(newDoc);
|
||||
if (--pending === 0) {
|
||||
this.clientIdentities.push(newIdentity);
|
||||
}
|
||||
},
|
||||
error: (err: any) => {
|
||||
console.error('Failed to upload document', err);
|
||||
if (--pending === 0) {
|
||||
this.clientIdentities.push(newIdentity);
|
||||
}
|
||||
}
|
||||
},
|
||||
error: (err: any) => {
|
||||
console.error('Failed to create identifier', err);
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
error: (err: any) => {
|
||||
console.error('Failed to create identifier', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -245,14 +276,16 @@ export class IdentitiesTabComponent implements OnDestroy {
|
||||
* @param {number} index Index
|
||||
*/
|
||||
deleteIdentifier(clientId: string, identifierId: string, index: number) {
|
||||
const identity = this.clientIdentities?.[index];
|
||||
const typeName = identity?.documentType?.name || this.translateService.instant('labels.heading.identifier id');
|
||||
const key = identity?.documentKey ? ` (${identity.documentKey})` : '';
|
||||
const deleteIdentifierDialogRef = this.dialog.open(DeleteDialogComponent, {
|
||||
data: { deleteContext: `${this.translateService.instant('labels.heading.identifier id')} : ${identifierId}` }
|
||||
data: { deleteContext: `${typeName}${key}` }
|
||||
});
|
||||
deleteIdentifierDialogRef.afterClosed().subscribe((response: any) => {
|
||||
if (response.delete) {
|
||||
this.clientService.deleteClientIdentifier(clientId, identifierId).subscribe((res) => {
|
||||
if (response?.delete) {
|
||||
this.clientService.deleteClientIdentifier(clientId, identifierId).subscribe(() => {
|
||||
this.clientIdentities.splice(index, 1);
|
||||
// Array mutation triggers @for re-render
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -27,10 +27,17 @@
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
<button mat-raised-button color="primary" (click)="toggleEditMode()" type="button">
|
||||
<fa-icon [icon]="isEditMode ? 'times' : 'edit'" class="m-r-10"></fa-icon>
|
||||
{{ isEditMode ? ('labels.buttons.Cancel' | translate) : ('labels.buttons.Edit' | translate) }}
|
||||
</button>
|
||||
<div class="kyc-header-actions">
|
||||
@if (!isEditMode && kycData) {
|
||||
<button mat-stroked-button class="kyc-print-btn" type="button" (click)="printSelfDeclaration()">
|
||||
<fa-icon icon="file-pdf" class="m-r-6"></fa-icon> Self Declaration
|
||||
</button>
|
||||
}
|
||||
<button mat-raised-button color="primary" (click)="toggleEditMode()" type="button">
|
||||
<fa-icon [icon]="isEditMode ? 'times' : 'edit'" class="m-r-10"></fa-icon>
|
||||
{{ isEditMode ? ('labels.buttons.Cancel' | translate) : ('labels.buttons.Edit' | translate) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KYC Summary (read-only when not in edit mode) -->
|
||||
@@ -58,12 +65,22 @@
|
||||
<span class="kv-status kv-status--pending"><mat-icon>radio_button_unchecked</mat-icon> Pending</span>
|
||||
}
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--date">{{ kycData.panVerifiedOn ? formatDate(kycData.panVerifiedOn) : '—' }}</span>
|
||||
<span class="kv-table__col kv-table__col--date">{{
|
||||
kycData.panVerifiedOn ? formatDate(kycData.panVerifiedOn) : '—'
|
||||
}}</span>
|
||||
<span class="kv-table__col kv-table__col--action">
|
||||
<button mat-stroked-button class="kv-action-btn" type="button"
|
||||
[disabled]="verifying.pan || !kycData.panNumber" (click)="verifyPAN()">
|
||||
@if (verifying.pan) { <mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner> }
|
||||
@else { Re-Verify }
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="kv-action-btn"
|
||||
type="button"
|
||||
[disabled]="verifying.pan || !kycData.panNumber"
|
||||
(click)="verifyPAN()"
|
||||
>
|
||||
@if (verifying.pan) {
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
Re-Verify
|
||||
}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -81,11 +98,18 @@
|
||||
<span class="kv-status kv-status--pending"><mat-icon>radio_button_unchecked</mat-icon> Pending</span>
|
||||
}
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--date">{{ kycData.aadhaarVerifiedOn ? formatDate(kycData.aadhaarVerifiedOn) : '—' }}</span>
|
||||
<span class="kv-table__col kv-table__col--date">{{
|
||||
kycData.aadhaarVerifiedOn ? formatDate(kycData.aadhaarVerifiedOn) : '—'
|
||||
}}</span>
|
||||
<span class="kv-table__col kv-table__col--action">
|
||||
@if (!showAadhaarReVerify && !aadhaarOtpSent) {
|
||||
<button mat-stroked-button class="kv-action-btn" type="button"
|
||||
[disabled]="verifying.aadhaar || !kycData.aadhaarNumberMasked" (click)="startAadhaarReVerify()">
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="kv-action-btn"
|
||||
type="button"
|
||||
[disabled]="verifying.aadhaar || !kycData.aadhaarNumberMasked"
|
||||
(click)="startAadhaarReVerify()"
|
||||
>
|
||||
Re-Verify
|
||||
</button>
|
||||
}
|
||||
@@ -106,12 +130,22 @@
|
||||
<span class="kv-status kv-status--pending"><mat-icon>radio_button_unchecked</mat-icon> Pending</span>
|
||||
}
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--date">{{ kycData.gstinVerifiedOn ? formatDate(kycData.gstinVerifiedOn) : '—' }}</span>
|
||||
<span class="kv-table__col kv-table__col--date">{{
|
||||
kycData.gstinVerifiedOn ? formatDate(kycData.gstinVerifiedOn) : '—'
|
||||
}}</span>
|
||||
<span class="kv-table__col kv-table__col--action">
|
||||
<button mat-stroked-button class="kv-action-btn" type="button"
|
||||
[disabled]="verifying.gstin || !kycData.gstinNumber" (click)="verifyGSTIN()">
|
||||
@if (verifying.gstin) { <mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner> }
|
||||
@else { Re-Verify }
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="kv-action-btn"
|
||||
type="button"
|
||||
[disabled]="verifying.gstin || !kycData.gstinNumber"
|
||||
(click)="verifyGSTIN()"
|
||||
>
|
||||
@if (verifying.gstin) {
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
Re-Verify
|
||||
}
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
@@ -131,7 +165,9 @@
|
||||
<span class="kv-status kv-status--pending"><mat-icon>radio_button_unchecked</mat-icon> Pending</span>
|
||||
}
|
||||
</span>
|
||||
<span class="kv-table__col kv-table__col--date">{{ kycData.enterprisePanVerifiedOn ? formatDate(kycData.enterprisePanVerifiedOn) : '—' }}</span>
|
||||
<span class="kv-table__col kv-table__col--date">{{
|
||||
kycData.enterprisePanVerifiedOn ? formatDate(kycData.enterprisePanVerifiedOn) : '—'
|
||||
}}</span>
|
||||
<span class="kv-table__col kv-table__col--action"></span>
|
||||
</div>
|
||||
}
|
||||
@@ -195,12 +231,27 @@
|
||||
<div class="kv-otp-bar">
|
||||
<mat-form-field appearance="outline" class="kv-otp-input">
|
||||
<mat-label>Enter 12-digit Aadhaar</mat-label>
|
||||
<input matInput [(ngModel)]="aadhaarFullNumber" [ngModelOptions]="{standalone: true}" maxlength="12" type="password" />
|
||||
<input
|
||||
matInput
|
||||
[(ngModel)]="aadhaarFullNumber"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
maxlength="12"
|
||||
type="password"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" type="button" class="kv-otp-btn"
|
||||
[disabled]="!aadhaarFullNumber || aadhaarFullNumber.length !== 12 || verifying.aadhaar" (click)="sendAadhaarOtp()">
|
||||
@if (verifying.aadhaar) { <mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner> }
|
||||
@else { Send OTP }
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
class="kv-otp-btn"
|
||||
[disabled]="!aadhaarFullNumber || aadhaarFullNumber.length !== 12 || verifying.aadhaar"
|
||||
(click)="sendAadhaarOtp()"
|
||||
>
|
||||
@if (verifying.aadhaar) {
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
Send OTP
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
@@ -209,32 +260,69 @@
|
||||
<span class="kv-otp-label"><mat-icon>sms</mat-icon> OTP sent</span>
|
||||
<mat-form-field appearance="outline" class="kv-otp-input">
|
||||
<mat-label>Enter OTP</mat-label>
|
||||
<input matInput [(ngModel)]="aadhaarOtp" [ngModelOptions]="{standalone: true}" maxlength="6" autocomplete="one-time-code" />
|
||||
<input
|
||||
matInput
|
||||
[(ngModel)]="aadhaarOtp"
|
||||
[ngModelOptions]="{ standalone: true }"
|
||||
maxlength="6"
|
||||
autocomplete="one-time-code"
|
||||
/>
|
||||
</mat-form-field>
|
||||
<button mat-raised-button color="primary" type="button" class="kv-otp-btn"
|
||||
[disabled]="!aadhaarOtp || aadhaarOtp.length < 4 || aadhaarOtpVerifying" (click)="verifyAadhaarOtp()">
|
||||
@if (aadhaarOtpVerifying) { <mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner> }
|
||||
@else { Verify }
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
class="kv-otp-btn"
|
||||
[disabled]="!aadhaarOtp || aadhaarOtp.length < 4 || aadhaarOtpVerifying"
|
||||
(click)="verifyAadhaarOtp()"
|
||||
>
|
||||
@if (aadhaarOtpVerifying) {
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
Verify
|
||||
}
|
||||
</button>
|
||||
<button
|
||||
mat-stroked-button
|
||||
type="button"
|
||||
class="kv-otp-btn"
|
||||
[disabled]="verifying.aadhaar"
|
||||
(click)="sendAadhaarOtp()"
|
||||
>
|
||||
Resend
|
||||
</button>
|
||||
<button mat-stroked-button type="button" class="kv-otp-btn" [disabled]="verifying.aadhaar" (click)="sendAadhaarOtp()">Resend</button>
|
||||
</div>
|
||||
}
|
||||
@if (verificationError.aadhaar) {
|
||||
<div class="kyc-error-msg"><mat-icon class="kyc-error-icon">error_outline</mat-icon> {{ verificationError.aadhaar }}</div>
|
||||
<div class="kyc-error-msg">
|
||||
<mat-icon class="kyc-error-icon">error_outline</mat-icon> {{ verificationError.aadhaar }}
|
||||
</div>
|
||||
}
|
||||
@if (verificationError.pan) {
|
||||
<div class="kyc-error-msg"><mat-icon class="kyc-error-icon">error_outline</mat-icon> {{ verificationError.pan }}</div>
|
||||
<div class="kyc-error-msg">
|
||||
<mat-icon class="kyc-error-icon">error_outline</mat-icon> {{ verificationError.pan }}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- KYC Meta Grid -->
|
||||
<div class="kv-meta-grid">
|
||||
<div class="kv-meta-item">
|
||||
<span class="kv-meta-label">KYC Type</span>
|
||||
<span class="kv-meta-value">{{ kycData.kycType === 1 ? 'Individual' : kycData.kycType === 2 ? 'Non-Individual' : '—' }}</span>
|
||||
<span class="kv-meta-value">{{
|
||||
kycData.kycType === 1 ? 'Individual' : kycData.kycType === 2 ? 'Non-Individual' : '—'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="kv-meta-item">
|
||||
<span class="kv-meta-label">Risk Category</span>
|
||||
<span class="kv-meta-value">{{ kycData.riskCategory === 1 ? 'Low' : kycData.riskCategory === 2 ? 'Medium' : kycData.riskCategory === 3 ? 'High' : '—' }}</span>
|
||||
<span class="kv-meta-value">{{
|
||||
kycData.riskCategory === 1
|
||||
? 'Low'
|
||||
: kycData.riskCategory === 2
|
||||
? 'Medium'
|
||||
: kycData.riskCategory === 3
|
||||
? 'High'
|
||||
: '—'
|
||||
}}</span>
|
||||
</div>
|
||||
<div class="kv-meta-item">
|
||||
<span class="kv-meta-label">Initiated On</span>
|
||||
@@ -255,6 +343,105 @@
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<!-- Re-KYC: Upload Signed Declaration & Complete -->
|
||||
<div class="rekyc-section">
|
||||
<div class="rekyc-header">
|
||||
<div class="rekyc-icon-wrap">
|
||||
<fa-icon icon="sync-alt" class="rekyc-icon"></fa-icon>
|
||||
</div>
|
||||
<div class="rekyc-header-text">
|
||||
<h4 class="rekyc-title">Re-KYC Completion</h4>
|
||||
<p class="rekyc-desc">Print self-declaration, get it signed, upload & complete re-KYC.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 1 -->
|
||||
<div class="rekyc-timeline">
|
||||
<div class="rekyc-tl-item">
|
||||
<div class="rekyc-tl-dot">
|
||||
<span class="rekyc-tl-num">1</span>
|
||||
<div class="rekyc-tl-line"></div>
|
||||
</div>
|
||||
<div class="rekyc-tl-content">
|
||||
<div class="rekyc-tl-label">Download Self Declaration Form</div>
|
||||
<div class="rekyc-tl-sub">System-generated PDF with PAN, Aadhaar & KYC details</div>
|
||||
<button mat-flat-button class="rekyc-btn-print" type="button" (click)="printSelfDeclaration()">
|
||||
<fa-icon icon="file-pdf"></fa-icon> Download PDF
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 2 -->
|
||||
<div class="rekyc-tl-item">
|
||||
<div class="rekyc-tl-dot">
|
||||
<span class="rekyc-tl-num">2</span>
|
||||
<div class="rekyc-tl-line"></div>
|
||||
</div>
|
||||
<div class="rekyc-tl-content">
|
||||
<div class="rekyc-tl-label">Upload Signed Declaration</div>
|
||||
<div class="rekyc-tl-sub">Get the form signed by the client and upload the scanned copy</div>
|
||||
<label class="rekyc-upload-zone" [class.has-file]="selectedDeclarationFile">
|
||||
<input type="file" accept=".pdf,.jpg,.jpeg,.png" (change)="onDeclarationFileSelected($event)" hidden />
|
||||
@if (!selectedDeclarationFile) {
|
||||
<mat-icon class="upload-zone-icon">cloud_upload</mat-icon>
|
||||
<span class="upload-zone-text">Click to upload signed form</span>
|
||||
<span class="upload-zone-hint">PDF, JPG or PNG</span>
|
||||
} @else {
|
||||
<fa-icon icon="check-circle" class="upload-zone-check"></fa-icon>
|
||||
<span class="upload-zone-filename">{{ selectedDeclarationFile.name }}</span>
|
||||
<span class="upload-zone-hint">Click to change file</span>
|
||||
}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Step 3 -->
|
||||
<div class="rekyc-tl-item rekyc-tl-item--last">
|
||||
<div class="rekyc-tl-dot">
|
||||
<span class="rekyc-tl-num">3</span>
|
||||
</div>
|
||||
<div class="rekyc-tl-content">
|
||||
<div class="rekyc-tl-label">Complete Re-KYC</div>
|
||||
<div class="rekyc-tl-sub">Updates Last Verified date and sets new Expiry Date as per RBI norms</div>
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
class="rekyc-btn-complete"
|
||||
type="button"
|
||||
[disabled]="!selectedDeclarationFile || reKycCompleting || !kycId"
|
||||
(click)="completeReKyc()"
|
||||
>
|
||||
@if (reKycCompleting) {
|
||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||||
Completing...
|
||||
} @else {
|
||||
<fa-icon icon="check-circle"></fa-icon> Complete Re-KYC
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (reKycSuccess) {
|
||||
<div class="rekyc-result rekyc-result--success">
|
||||
<fa-icon icon="check-circle" class="rekyc-result-icon"></fa-icon>
|
||||
<div>
|
||||
<strong>Re-KYC completed successfully</strong>
|
||||
<p>Last Verified and Expiry Date have been updated.</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (reKycError) {
|
||||
<div class="rekyc-result rekyc-result--error">
|
||||
<mat-icon class="rekyc-result-icon">error_outline</mat-icon>
|
||||
<div>
|
||||
<strong>Re-KYC failed</strong>
|
||||
<p>{{ reKycError }}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- No KYC Data State -->
|
||||
|
||||
@@ -164,6 +164,24 @@
|
||||
color: #757575;
|
||||
border: 1px solid #e0e0e0;
|
||||
}
|
||||
|
||||
&.linked {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
border: 1px solid #a7f3d0;
|
||||
}
|
||||
|
||||
&.not-linked {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
&.exempt {
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
border: 1px solid #bfdbfe;
|
||||
}
|
||||
}
|
||||
|
||||
.status-chip-icon {
|
||||
@@ -375,26 +393,6 @@
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.status-chip {
|
||||
&.linked {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
border: 1px solid #a7f3d0;
|
||||
}
|
||||
|
||||
&.not-linked {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
border: 1px solid #fecaca;
|
||||
}
|
||||
|
||||
&.exempt {
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
border: 1px solid #bfdbfe;
|
||||
}
|
||||
}
|
||||
|
||||
.pan-aadhaar-detail {
|
||||
margin: 12px 0;
|
||||
display: flex;
|
||||
@@ -468,10 +466,17 @@
|
||||
padding: 0;
|
||||
transition: background 0.1s;
|
||||
|
||||
&:last-child { border-bottom: none; }
|
||||
&:hover { background: #f8fafc; }
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&--link { background: #fafbfc; }
|
||||
&:hover {
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
&--link {
|
||||
background: #fafbfc;
|
||||
}
|
||||
}
|
||||
|
||||
.kv-table__col {
|
||||
@@ -486,10 +491,25 @@
|
||||
gap: 8px;
|
||||
font-weight: 600;
|
||||
}
|
||||
&--num { flex: 1; }
|
||||
&--status { flex: 0 0 150px; }
|
||||
&--date { flex: 0 0 150px; color: #64748b; font-size: 12px; }
|
||||
&--action { flex: 0 0 110px; text-align: right; }
|
||||
|
||||
&--num {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&--status {
|
||||
flex: 0 0 150px;
|
||||
}
|
||||
|
||||
&--date {
|
||||
flex: 0 0 150px;
|
||||
color: #64748b;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&--action {
|
||||
flex: 0 0 110px;
|
||||
text-align: right;
|
||||
}
|
||||
}
|
||||
|
||||
.kv-doc-icon {
|
||||
@@ -520,10 +540,25 @@
|
||||
height: 14px !important;
|
||||
}
|
||||
|
||||
&--ok { background: #ecfdf5; color: #059669; }
|
||||
&--pending { background: #f8fafc; color: #94a3b8; }
|
||||
&--fail { background: #fef2f2; color: #dc2626; }
|
||||
&--info { background: #eff6ff; color: #2563eb; }
|
||||
&--ok {
|
||||
background: #ecfdf5;
|
||||
color: #059669;
|
||||
}
|
||||
|
||||
&--pending {
|
||||
background: #f8fafc;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
&--fail {
|
||||
background: #fef2f2;
|
||||
color: #dc2626;
|
||||
}
|
||||
|
||||
&--info {
|
||||
background: #eff6ff;
|
||||
color: #2563eb;
|
||||
}
|
||||
}
|
||||
|
||||
.kv-action-btn {
|
||||
@@ -558,14 +593,21 @@
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
|
||||
mat-icon { font-size: 16px !important; width: 16px !important; height: 16px !important; }
|
||||
mat-icon {
|
||||
font-size: 16px !important;
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.kv-otp-input {
|
||||
width: 200px;
|
||||
flex-shrink: 0;
|
||||
|
||||
::ng-deep .mat-mdc-form-field-infix { padding: 6px 0 !important; min-height: 36px !important; }
|
||||
::ng-deep .mat-mdc-form-field-infix {
|
||||
padding: 6px 0 !important;
|
||||
min-height: 36px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.kv-otp-btn {
|
||||
@@ -594,8 +636,14 @@
|
||||
border-right: 1px solid #f1f5f9;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
|
||||
&:nth-child(5n) { border-right: none; }
|
||||
&--full { grid-column: 1 / -1; border-right: none; }
|
||||
&:nth-child(5n) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
&--full {
|
||||
grid-column: 1 / -1;
|
||||
border-right: none;
|
||||
}
|
||||
}
|
||||
|
||||
.kv-meta-label {
|
||||
@@ -612,20 +660,332 @@
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
@media (width <= 960px) {
|
||||
.kv-table__col {
|
||||
&--doc { flex: 0 0 120px; }
|
||||
&--date { display: none; }
|
||||
&--action { flex: 0 0 90px; }
|
||||
&--doc {
|
||||
flex: 0 0 120px;
|
||||
}
|
||||
|
||||
&--date {
|
||||
display: none;
|
||||
}
|
||||
|
||||
&--action {
|
||||
flex: 0 0 90px;
|
||||
}
|
||||
}
|
||||
|
||||
.kv-meta-grid {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
.kv-meta-item:nth-child(5n) {
|
||||
border-right: 1px solid #f1f5f9;
|
||||
}
|
||||
|
||||
.kv-meta-item:nth-child(3n) {
|
||||
border-right: none;
|
||||
}
|
||||
.kv-meta-grid { grid-template-columns: repeat(3, 1fr); }
|
||||
.kv-meta-item:nth-child(5n) { border-right: 1px solid #f1f5f9; }
|
||||
.kv-meta-item:nth-child(3n) { border-right: none; }
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.kv-table__row { flex-wrap: wrap; }
|
||||
.kv-table__col { &--status, &--action { flex: 0 0 50%; } }
|
||||
.kv-meta-grid { grid-template-columns: 1fr 1fr; }
|
||||
.kv-otp-bar { flex-wrap: wrap; }
|
||||
@media (width <= 640px) {
|
||||
.kv-table__row {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.kv-table__col {
|
||||
&--status,
|
||||
&--action {
|
||||
flex: 0 0 50%;
|
||||
}
|
||||
}
|
||||
|
||||
.kv-meta-grid {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.kv-otp-bar {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
}
|
||||
|
||||
/* ─── Header Actions ─── */
|
||||
.kyc-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.kyc-print-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
color: #1074b9 !important;
|
||||
border-color: #93c5fd !important;
|
||||
}
|
||||
|
||||
.m-r-6 {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
/* ─── Re-KYC Section ─── */
|
||||
.rekyc-section {
|
||||
margin-top: 20px;
|
||||
background: linear-gradient(135deg, #fafbff 0%, #f0f4ff 100%);
|
||||
border: 1px solid #dbe4f0;
|
||||
border-radius: 14px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.rekyc-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
margin-bottom: 22px;
|
||||
}
|
||||
|
||||
.rekyc-icon-wrap {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
border-radius: 10px;
|
||||
background: linear-gradient(135deg, #1074b9, #0c5da0);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.rekyc-icon {
|
||||
font-size: 17px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.rekyc-header-text {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.rekyc-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
|
||||
.rekyc-desc {
|
||||
margin: 3px 0 0;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* ─── Timeline Layout ─── */
|
||||
.rekyc-timeline {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.rekyc-tl-item {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.rekyc-tl-dot {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
.rekyc-tl-num {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border-radius: 50%;
|
||||
background: #1074b9;
|
||||
color: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-shrink: 0;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.rekyc-tl-line {
|
||||
width: 2px;
|
||||
flex: 1;
|
||||
background: #c7d8ed;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
.rekyc-tl-item--last .rekyc-tl-dot {
|
||||
.rekyc-tl-line {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.rekyc-tl-content {
|
||||
flex: 1;
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.rekyc-tl-item--last .rekyc-tl-content {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.rekyc-tl-label {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #1e293b;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
|
||||
.rekyc-tl-sub {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
/* ─── Buttons ─── */
|
||||
.rekyc-btn-print {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
font-size: 12px !important;
|
||||
font-weight: 600 !important;
|
||||
color: #1074b9 !important;
|
||||
background: #fff !important;
|
||||
border: 1px solid #bfdbfe !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 6px 16px !important;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
|
||||
&:hover {
|
||||
background: #eff6ff !important;
|
||||
border-color: #93c5fd !important;
|
||||
}
|
||||
}
|
||||
|
||||
.rekyc-btn-complete {
|
||||
display: inline-flex !important;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
font-size: 12px !important;
|
||||
font-weight: 600 !important;
|
||||
border-radius: 8px !important;
|
||||
padding: 7px 20px !important;
|
||||
}
|
||||
|
||||
/* ─── Upload Zone ─── */
|
||||
.rekyc-upload-zone {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 4px;
|
||||
padding: 16px 24px;
|
||||
max-width: 280px;
|
||||
background: #fff;
|
||||
border: 2px dashed #cbd5e1;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
text-align: center;
|
||||
|
||||
&:hover {
|
||||
background: #f8fafc;
|
||||
border-color: #93c5fd;
|
||||
}
|
||||
|
||||
&.has-file {
|
||||
background: #f0fdf4;
|
||||
border-color: #86efac;
|
||||
border-style: solid;
|
||||
}
|
||||
}
|
||||
|
||||
.upload-zone-icon {
|
||||
font-size: 28px !important;
|
||||
width: 28px !important;
|
||||
height: 28px !important;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.upload-zone-text {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #475569;
|
||||
}
|
||||
|
||||
.upload-zone-hint {
|
||||
font-size: 10px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.upload-zone-check {
|
||||
font-size: 24px;
|
||||
color: #22c55e;
|
||||
}
|
||||
|
||||
.upload-zone-filename {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #059669;
|
||||
max-width: 230px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* ─── Result Banners ─── */
|
||||
.rekyc-result {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 10px;
|
||||
margin-top: 18px;
|
||||
padding: 12px 16px;
|
||||
border-radius: 10px;
|
||||
font-size: 13px;
|
||||
line-height: 1.4;
|
||||
|
||||
p {
|
||||
margin: 2px 0 0;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
&--success {
|
||||
background: #ecfdf5;
|
||||
border: 1px solid #a7f3d0;
|
||||
color: #065f46;
|
||||
|
||||
.rekyc-result-icon {
|
||||
color: #059669;
|
||||
font-size: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
&--error {
|
||||
background: #fef2f2;
|
||||
border: 1px solid #fecaca;
|
||||
color: #991b1b;
|
||||
|
||||
.rekyc-result-icon {
|
||||
color: #dc2626;
|
||||
font-size: 20px !important;
|
||||
width: 20px !important;
|
||||
height: 20px !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (width <= 640px) {
|
||||
.rekyc-upload-zone {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import { catchError, of, Subject, takeUntil } from 'rxjs';
|
||||
/** Custom Services */
|
||||
import { ClientsService } from '../../clients.service';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { jsPDF } from 'jspdf';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatTooltip } from '@angular/material/tooltip';
|
||||
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||
@@ -51,6 +52,7 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
clientId: string;
|
||||
kycId: string | null = null;
|
||||
kycData: any = null;
|
||||
clientData: any = null;
|
||||
clientDateOfBirth: string | null = null;
|
||||
isNonIndividual = false;
|
||||
loading = true;
|
||||
@@ -58,6 +60,13 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
isEditMode = false;
|
||||
errorMessage: string | null = null;
|
||||
|
||||
/** Re-KYC flow state */
|
||||
reKycUploading = false;
|
||||
reKycCompleting = false;
|
||||
reKycSuccess = false;
|
||||
reKycError: string | null = null;
|
||||
selectedDeclarationFile: File | null = null;
|
||||
|
||||
verifying: { pan: boolean; aadhaar: boolean; gstin: boolean } = {
|
||||
pan: false,
|
||||
aadhaar: false,
|
||||
@@ -154,6 +163,7 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
/** Load client's date of birth and legal form from parent route resolved data */
|
||||
loadClientDateOfBirth(): void {
|
||||
this.route.parent.data.subscribe((data: any) => {
|
||||
this.clientData = data?.clientViewData || null;
|
||||
if (data?.clientViewData?.dateOfBirth) {
|
||||
const dob = data.clientViewData.dateOfBirth;
|
||||
if (Array.isArray(dob) && dob.length >= 3) {
|
||||
@@ -437,13 +447,264 @@ export class KycTabComponent implements OnInit, OnDestroy {
|
||||
|
||||
getPanAadhaarStatusLabel(status: string): string {
|
||||
switch (status) {
|
||||
case 'Y': return 'Linked (Seeded)';
|
||||
case 'N': return 'Not Linked';
|
||||
case 'E': return 'Exempt';
|
||||
default: return 'Pending Verification';
|
||||
case 'Y':
|
||||
return 'Linked (Seeded)';
|
||||
case 'N':
|
||||
return 'Not Linked';
|
||||
case 'E':
|
||||
return 'Exempt';
|
||||
default:
|
||||
return 'Pending Verification';
|
||||
}
|
||||
}
|
||||
|
||||
/** Generate Self Declaration PDF with KYC details */
|
||||
printSelfDeclaration(): void {
|
||||
const client = this.clientData || {};
|
||||
const kyc = this.kycData || {};
|
||||
const clientName = client.displayName || client.fullname || '';
|
||||
const cifNumber = client.accountNo || client.cifNumber || this.clientId;
|
||||
const dob = this.clientDateOfBirth ? this.formatDate(client.dateOfBirth) : '—';
|
||||
const officeName = client.officeName || '';
|
||||
const today = new Date().toLocaleDateString('en-IN', { day: '2-digit', month: 'long', year: 'numeric' });
|
||||
|
||||
const doc = new jsPDF({ orientation: 'portrait', unit: 'mm', format: 'a4' });
|
||||
const pageWidth = doc.internal.pageSize.getWidth();
|
||||
const margin = 20;
|
||||
const contentWidth = pageWidth - margin * 2;
|
||||
let y = 20;
|
||||
|
||||
// Helper
|
||||
const addLine = (label: string, value: string, yPos: number): number => {
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setFontSize(10);
|
||||
doc.text(label, margin, yPos);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(value || '—', margin + 55, yPos);
|
||||
return yPos + 7;
|
||||
};
|
||||
|
||||
const drawHr = (yPos: number): number => {
|
||||
doc.setDrawColor(200, 200, 200);
|
||||
doc.setLineWidth(0.3);
|
||||
doc.line(margin, yPos, pageWidth - margin, yPos);
|
||||
return yPos + 5;
|
||||
};
|
||||
|
||||
// ── Header ──
|
||||
doc.setFillColor(15, 23, 42);
|
||||
doc.rect(0, 0, pageWidth, 30, 'F');
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setFontSize(16);
|
||||
doc.text('Self Declaration Form', pageWidth / 2, 13, { align: 'center' });
|
||||
doc.setFontSize(9);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text('KYC / Re-KYC Compliance — As per RBI Master Direction on KYC', pageWidth / 2, 21, { align: 'center' });
|
||||
doc.setTextColor(0, 0, 0);
|
||||
|
||||
y = 40;
|
||||
|
||||
// ── Branch / Date ──
|
||||
doc.setFontSize(10);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text(`Branch: ${officeName}`, margin, y);
|
||||
doc.text(`Date: ${today}`, pageWidth - margin, y, { align: 'right' });
|
||||
y += 8;
|
||||
y = drawHr(y);
|
||||
|
||||
// ── Section: Customer Details ──
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setFontSize(12);
|
||||
doc.setTextColor(16, 116, 185);
|
||||
doc.text('Customer Details', margin, y);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
y += 8;
|
||||
|
||||
y = addLine('Customer Name:', clientName, y);
|
||||
y = addLine('CIF Number:', cifNumber, y);
|
||||
y = addLine('Date of Birth:', dob, y);
|
||||
if (client.mobileNo) y = addLine('Mobile:', client.mobileNo, y);
|
||||
if (client.emailAddress) y = addLine('Email:', client.emailAddress, y);
|
||||
y += 3;
|
||||
y = drawHr(y);
|
||||
|
||||
// ── Section: KYC Documents ──
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setFontSize(12);
|
||||
doc.setTextColor(16, 116, 185);
|
||||
doc.text('KYC Document Details', margin, y);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
y += 8;
|
||||
|
||||
// PAN
|
||||
y = addLine('PAN Number:', kyc.panNumber || '—', y);
|
||||
y = addLine('PAN Status:', kyc.panVerified ? 'Verified' : 'Pending', y);
|
||||
if (kyc.panVerifiedOn) y = addLine('PAN Verified On:', this.formatDate(kyc.panVerifiedOn), y);
|
||||
|
||||
// Aadhaar
|
||||
y = addLine('Aadhaar Number:', kyc.aadhaarNumberMasked || '—', y);
|
||||
y = addLine('Aadhaar Status:', kyc.aadhaarVerified ? 'Verified' : 'Pending', y);
|
||||
if (kyc.aadhaarVerifiedOn) y = addLine('Aadhaar Verified On:', this.formatDate(kyc.aadhaarVerifiedOn), y);
|
||||
|
||||
// PAN-Aadhaar Link
|
||||
const seedingStatus =
|
||||
kyc.panAadhaarSeedingStatus === 'Y'
|
||||
? 'Linked'
|
||||
: kyc.panAadhaarSeedingStatus === 'N'
|
||||
? 'Not Linked'
|
||||
: kyc.panAadhaarSeedingStatus === 'E'
|
||||
? 'Exempt'
|
||||
: 'Pending';
|
||||
y = addLine('PAN-Aadhaar Link:', seedingStatus, y);
|
||||
|
||||
// GSTIN (Non-Individual)
|
||||
if (this.isNonIndividual && kyc.gstinNumber) {
|
||||
y = addLine('GSTIN:', kyc.gstinNumber, y);
|
||||
y = addLine('GSTIN Status:', kyc.gstinVerified ? 'Verified' : 'Pending', y);
|
||||
}
|
||||
|
||||
// Enterprise PAN / CIN
|
||||
if (this.isNonIndividual && kyc.enterprisePan) {
|
||||
y = addLine('Enterprise PAN:', kyc.enterprisePan, y);
|
||||
}
|
||||
if (this.isNonIndividual && kyc.cinNumber) {
|
||||
y = addLine('CIN:', kyc.cinNumber, y);
|
||||
}
|
||||
|
||||
y += 3;
|
||||
y = drawHr(y);
|
||||
|
||||
// ── Section: KYC Summary ──
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setFontSize(12);
|
||||
doc.setTextColor(16, 116, 185);
|
||||
doc.text('KYC Summary', margin, y);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
y += 8;
|
||||
|
||||
y = addLine('KYC Type:', kyc.kycType === 1 ? 'Individual' : kyc.kycType === 2 ? 'Non-Individual' : '—', y);
|
||||
y = addLine(
|
||||
'Risk Category:',
|
||||
kyc.riskCategory === 1 ? 'Low' : kyc.riskCategory === 2 ? 'Medium' : kyc.riskCategory === 3 ? 'High' : '—',
|
||||
y
|
||||
);
|
||||
y = addLine('Initiated On:', this.formatDate(kyc.initiatedOn), y);
|
||||
y = addLine('Last Verified:', this.formatDate(kyc.verifiedOn), y);
|
||||
y = addLine('Expiry Date:', this.formatDate(kyc.expiryDate), y);
|
||||
|
||||
y += 3;
|
||||
y = drawHr(y);
|
||||
|
||||
// ── Declaration Text ──
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setFontSize(12);
|
||||
doc.setTextColor(16, 116, 185);
|
||||
doc.text('Declaration', margin, y);
|
||||
doc.setTextColor(0, 0, 0);
|
||||
y += 8;
|
||||
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setFontSize(9.5);
|
||||
const declarationText = `I, ${clientName || '_______________'}, hereby declare that the information provided above is true, correct and complete to the best of my knowledge and belief. I understand that furnishing false information is punishable under applicable laws. I authorize the Bank to verify the above details through appropriate channels as per RBI Master Direction on KYC (DBR.AML.BC.No.81/14.01.001/2015-16) and its subsequent amendments.
|
||||
|
||||
I further confirm that:
|
||||
1. I am not a Politically Exposed Person (PEP) / relative of a PEP (unless declared otherwise).
|
||||
2. The source of funds in my account is from legitimate sources.
|
||||
3. I shall promptly notify the Bank of any change in the above information.
|
||||
4. I have read and understood the Bank's terms and conditions regarding KYC compliance.`;
|
||||
|
||||
const lines = doc.splitTextToSize(declarationText, contentWidth);
|
||||
doc.text(lines, margin, y);
|
||||
y += lines.length * 4.5 + 10;
|
||||
|
||||
// ── Signature section ──
|
||||
y = drawHr(y);
|
||||
y += 5;
|
||||
|
||||
// Left: Client signature
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setFontSize(9);
|
||||
doc.text('________________________', margin, y + 15);
|
||||
doc.text('Signature of Customer', margin, y + 21);
|
||||
doc.text(`Name: ${clientName}`, margin, y + 27);
|
||||
doc.text(`Date: _______________`, margin, y + 33);
|
||||
doc.text(`Place: _______________`, margin, y + 39);
|
||||
|
||||
// Right: Bank official
|
||||
doc.text('________________________', pageWidth - margin - 50, y + 15);
|
||||
doc.text('Authorized Bank Official', pageWidth - margin - 50, y + 21);
|
||||
doc.text('Name: _______________', pageWidth - margin - 50, y + 27);
|
||||
doc.text('Designation: _______________', pageWidth - margin - 50, y + 33);
|
||||
doc.text('Stamp & Seal', pageWidth - margin - 50, y + 39);
|
||||
|
||||
// ── Footer ──
|
||||
const footerY = doc.internal.pageSize.getHeight() - 10;
|
||||
doc.setFontSize(7);
|
||||
doc.setTextColor(150, 150, 150);
|
||||
doc.text('This is a system-generated document. Generated from CBS Nexora — KYC Module.', pageWidth / 2, footerY, {
|
||||
align: 'center'
|
||||
});
|
||||
|
||||
// Save
|
||||
doc.save(`Self_Declaration_${cifNumber}_${clientName.replace(/\s/g, '_')}.pdf`);
|
||||
}
|
||||
|
||||
/** Handle file selection for signed declaration upload */
|
||||
onDeclarationFileSelected(event: Event): void {
|
||||
const input = event.target as HTMLInputElement;
|
||||
if (input.files?.length) {
|
||||
this.selectedDeclarationFile = input.files[0];
|
||||
this.reKycError = null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Upload signed declaration and complete re-KYC */
|
||||
completeReKyc(): void {
|
||||
if (!this.selectedDeclarationFile || !this.clientId || !this.kycId) return;
|
||||
|
||||
this.reKycCompleting = true;
|
||||
this.reKycError = null;
|
||||
this.reKycSuccess = false;
|
||||
|
||||
// Step 1: Upload the signed declaration as a client document
|
||||
const formData = new FormData();
|
||||
formData.append('file', this.selectedDeclarationFile);
|
||||
formData.append('name', 'KYC Self Declaration');
|
||||
formData.append(
|
||||
'description',
|
||||
`Signed self-declaration form uploaded on ${new Date().toLocaleDateString('en-IN')}`
|
||||
);
|
||||
|
||||
this.clientsService.uploadClientDocument(this.clientId, formData).subscribe({
|
||||
next: () => {
|
||||
// Step 2: Call re-KYC completion API to update verified/expiry dates
|
||||
this.clientsService.completeReKyc(this.clientId, this.kycId!).subscribe({
|
||||
next: () => {
|
||||
this.reKycCompleting = false;
|
||||
this.reKycSuccess = true;
|
||||
this.selectedDeclarationFile = null;
|
||||
this.loadKycData(); // Refresh to show updated dates
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.reKycCompleting = false;
|
||||
this.reKycError =
|
||||
err?.error?.defaultUserMessage ||
|
||||
err?.error?.developerMessage ||
|
||||
'Failed to complete re-KYC. Please try again.';
|
||||
}
|
||||
});
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.reKycCompleting = false;
|
||||
this.reKycError =
|
||||
err?.error?.defaultUserMessage ||
|
||||
err?.error?.developerMessage ||
|
||||
'Failed to upload declaration. Please try again.';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.destroy$.next();
|
||||
this.destroy$.complete();
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
<div class="nom-container mat-typography">
|
||||
|
||||
<!-- Header -->
|
||||
<div class="nom-page-header">
|
||||
<div class="nom-page-header__left">
|
||||
@@ -39,9 +38,12 @@
|
||||
<div class="nom-type-chips">
|
||||
@for (group of accountGroups; track group.type) {
|
||||
@if (group.accounts.length > 0) {
|
||||
<button type="button" class="nom-type-chip"
|
||||
<button
|
||||
type="button"
|
||||
class="nom-type-chip"
|
||||
[class.active]="selectedType === group.type"
|
||||
(click)="selectedType = group.type; onTypeChange()">
|
||||
(click)="selectedType = group.type; onTypeChange()"
|
||||
>
|
||||
<mat-icon>{{ group.icon }}</mat-icon>
|
||||
{{ group.label }}
|
||||
<span class="nom-type-chip__count">{{ group.accounts.length }}</span>
|
||||
@@ -55,13 +57,23 @@
|
||||
<mat-label>Account</mat-label>
|
||||
<mat-select [(ngModel)]="selectedAccountId" (ngModelChange)="onAccountChange()">
|
||||
@for (acc of selectedGroup.accounts; track acc.id) {
|
||||
<mat-option [value]="acc.id">{{ acc.productName }} — {{ acc.accountNo }}@if (acc.accountBalance != null) { ({{ acc.accountBalance | number:'1.2-2' }}) }</mat-option>
|
||||
<mat-option [value]="acc.id"
|
||||
>{{ acc.productName }} — {{ acc.accountNo }}
|
||||
@if (acc.accountBalance != null) {
|
||||
({{ acc.accountBalance | number: '1.2-2' }})
|
||||
}
|
||||
</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
@if (selectedAccountId && !showAddForm) {
|
||||
<button mat-flat-button color="primary" class="nom-add-btn"
|
||||
[disabled]="totalShare >= 100" (click)="showAddForm = true">
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
class="nom-add-btn"
|
||||
[disabled]="totalShare >= 100"
|
||||
(click)="showAddForm = true"
|
||||
>
|
||||
<mat-icon>add</mat-icon> Add Nominee
|
||||
</button>
|
||||
}
|
||||
@@ -83,10 +95,12 @@
|
||||
<div class="nom-share-bar">
|
||||
<span class="nom-share-bar__label">Share</span>
|
||||
<div class="nom-share-bar__track">
|
||||
<div class="nom-share-bar__fill"
|
||||
<div
|
||||
class="nom-share-bar__fill"
|
||||
[style.width.%]="totalShare > 100 ? 100 : totalShare"
|
||||
[class.over]="totalShare > 100"
|
||||
[class.complete]="totalShare === 100"></div>
|
||||
[class.complete]="totalShare === 100"
|
||||
></div>
|
||||
</div>
|
||||
<span class="nom-share-bar__value" [class.over]="totalShare > 100">{{ totalShare }}%</span>
|
||||
</div>
|
||||
@@ -97,8 +111,12 @@
|
||||
<div class="nom-card__main">
|
||||
<div class="nom-card__avatar">
|
||||
@if (nominee.profileImageUrl) {
|
||||
<img [src]="nominee.profileImageUrl" alt="" class="nom-card__photo zoomable"
|
||||
(click)="zoomImage(nominee.profileImageUrl, 'Nominee Photo')" />
|
||||
<img
|
||||
[src]="nominee.profileImageUrl"
|
||||
alt=""
|
||||
class="nom-card__photo zoomable"
|
||||
(click)="zoomImage(nominee.profileImageUrl, 'Nominee Photo')"
|
||||
/>
|
||||
} @else {
|
||||
<mat-icon>person</mat-icon>
|
||||
}
|
||||
@@ -107,14 +125,22 @@
|
||||
<span class="nom-card__name">{{ nominee.nomineeDisplayName || nominee.firstName || '—' }}</span>
|
||||
<span class="nom-card__meta">
|
||||
{{ getRelationship(nominee) }}
|
||||
@if (nominee.nomineeDateOfBirth || nominee.dateOfBirth) { · DOB: {{ (nominee.nomineeDateOfBirth || nominee.dateOfBirth) | dateFormat }} }
|
||||
@if (nominee.nomineeMobileNo || nominee.mobileNo) { · {{ nominee.nomineeMobileNo || nominee.mobileNo }} }
|
||||
@if (nominee.nomineeDateOfBirth || nominee.dateOfBirth) {
|
||||
· DOB: {{ nominee.nomineeDateOfBirth || nominee.dateOfBirth | dateFormat }}
|
||||
}
|
||||
@if (nominee.nomineeMobileNo || nominee.mobileNo) {
|
||||
· {{ nominee.nomineeMobileNo || nominee.mobileNo }}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
@if (nominee.signatureUrl) {
|
||||
<div class="nom-card__sig">
|
||||
<img [src]="nominee.signatureUrl" alt="Signature" class="zoomable"
|
||||
(click)="zoomImage(nominee.signatureUrl, 'Nominee Signature')" />
|
||||
<img
|
||||
[src]="nominee.signatureUrl"
|
||||
alt="Signature"
|
||||
class="zoomable"
|
||||
(click)="zoomImage(nominee.signatureUrl, 'Nominee Signature')"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
@if (nominee.nomineeIdentifier) {
|
||||
@@ -124,16 +150,30 @@
|
||||
<span class="nom-card__share-value">{{ nominee.percentageShare || 0 }}%</span>
|
||||
</div>
|
||||
<div class="nom-card__actions">
|
||||
<button mat-icon-button type="button" matTooltip="View" (click)="viewNominee(nominee)"><mat-icon>visibility</mat-icon></button>
|
||||
<button mat-icon-button type="button" matTooltip="Edit" (click)="editNominee(nominee, i)"><mat-icon>edit</mat-icon></button>
|
||||
<button mat-icon-button type="button" matTooltip="Delete" class="nom-card__delete" (click)="deleteNominee(nominee, i)"><mat-icon>delete</mat-icon></button>
|
||||
<button mat-icon-button type="button" matTooltip="View" (click)="viewNominee(nominee)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button type="button" matTooltip="Edit" (click)="editNominee(nominee, i)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
type="button"
|
||||
matTooltip="Delete"
|
||||
class="nom-card__delete"
|
||||
(click)="deleteNominee(nominee, i)"
|
||||
>
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@if (nominee.nomineeIsMinor && (nominee.guardianDisplayName || nominee.guardianName)) {
|
||||
<div class="nom-card__guardian">
|
||||
<mat-icon>shield</mat-icon>
|
||||
Guardian: <strong>{{ nominee.guardianDisplayName || nominee.guardianName }}</strong>
|
||||
@if (nominee.guardianRelationshipName) { ({{ nominee.guardianRelationshipName }}) }
|
||||
@if (nominee.guardianRelationshipName) {
|
||||
({{ nominee.guardianRelationshipName }})
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<!-- Inline Edit -->
|
||||
@@ -143,24 +183,39 @@
|
||||
<mat-form-field appearance="outline" class="nom-edit-field">
|
||||
<mat-label>Type</mat-label>
|
||||
<mat-select [(ngModel)]="editNomType">
|
||||
@for (opt of nominationTypeOptions; track opt.value) { <mat-option [value]="opt.value">{{ opt.label }}</mat-option> }
|
||||
@for (opt of nominationTypeOptions; track opt.value) {
|
||||
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="nom-edit-field">
|
||||
<mat-label>Relationship</mat-label>
|
||||
<mat-select [(ngModel)]="editRelId">
|
||||
@for (rel of relationshipOptions; track rel) { <mat-option [value]="rel.id">{{ rel.name }}</mat-option> }
|
||||
@for (rel of relationshipOptions; track rel) {
|
||||
<mat-option [value]="rel.id">{{ rel.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="nom-edit-field nom-edit-field--sm">
|
||||
<mat-label>Share %</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="editShare" min="1" [max]="maxShareForEdit(i)" />
|
||||
@if (editShare && editShare > maxShareForEdit(i)) { <mat-error>Max {{ maxShareForEdit(i) }}%</mat-error> }
|
||||
@if (editShare && editShare > maxShareForEdit(i)) {
|
||||
<mat-error>Max {{ maxShareForEdit(i) }}%</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<button mat-flat-button color="primary" type="button" class="nom-edit-save"
|
||||
<button
|
||||
mat-flat-button
|
||||
color="primary"
|
||||
type="button"
|
||||
class="nom-edit-save"
|
||||
[disabled]="!editRelId || !editShare || editShare > maxShareForEdit(i)"
|
||||
(click)="saveEdit(nominee, i)"><mat-icon>check</mat-icon></button>
|
||||
<button mat-stroked-button type="button" class="nom-edit-cancel" (click)="cancelEdit()"><mat-icon>close</mat-icon></button>
|
||||
(click)="saveEdit(nominee, i)"
|
||||
>
|
||||
<mat-icon>check</mat-icon>
|
||||
</button>
|
||||
<button mat-stroked-button type="button" class="nom-edit-cancel" (click)="cancelEdit()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -185,17 +240,39 @@
|
||||
<mat-form-field appearance="outline" class="nom-search-field">
|
||||
<mat-label>Search by Name, Client ID or Mobile</mat-label>
|
||||
<span matPrefix class="nom-search-prefix"><mat-icon>search</mat-icon></span>
|
||||
<input matInput [value]="searchText" (input)="onSearch($any($event.target).value)" (blur)="dismissSearch()" placeholder="Type at least 2 characters..." autocomplete="off" />
|
||||
<input
|
||||
matInput
|
||||
[value]="searchText"
|
||||
(input)="onSearch($any($event.target).value)"
|
||||
(blur)="dismissSearch()"
|
||||
placeholder="Type at least 2 characters..."
|
||||
autocomplete="off"
|
||||
/>
|
||||
</mat-form-field>
|
||||
@if (searchResults.length > 0 || searching) {
|
||||
<div class="nom-search-results">
|
||||
@if (searching) { <div class="nom-search-loading"><mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner> Searching...</div> }
|
||||
@if (searching) {
|
||||
<div class="nom-search-loading">
|
||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner> Searching...
|
||||
</div>
|
||||
}
|
||||
@for (client of searchResults; track client.id) {
|
||||
<div class="nom-search-item" (mousedown)="selectClient(client); $event.preventDefault()">
|
||||
<mat-icon class="nom-search-item__icon">person</mat-icon>
|
||||
<div class="nom-search-item__info">
|
||||
<span class="nom-search-item__name">{{ client.displayName }}</span>
|
||||
<span class="nom-search-item__meta">ID: {{ client.id }} · {{ client.officeName }}@if (client.dateOfBirth) { · DOB: {{ client.dateOfBirth | dateFormat }} }@if (client.fatherSpouseName) { · {{ client.fatherSpouseName }} }@if (client.mobileNo) { · {{ client.mobileNo }} }</span>
|
||||
<span class="nom-search-item__meta"
|
||||
>ID: {{ client.id }} · {{ client.officeName }}
|
||||
@if (client.dateOfBirth) {
|
||||
· DOB: {{ client.dateOfBirth | dateFormat }}
|
||||
}
|
||||
@if (client.fatherSpouseName) {
|
||||
· {{ client.fatherSpouseName }}
|
||||
}
|
||||
@if (client.mobileNo) {
|
||||
· {{ client.mobileNo }}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@@ -212,32 +289,68 @@
|
||||
<div class="nom-linked-card">
|
||||
<div class="nom-linked-card__left">
|
||||
@if (linkedProfileImage) {
|
||||
<img [src]="linkedProfileImage" class="nom-linked-card__photo zoomable" (click)="zoomImage(linkedProfileImage, 'Photo')" />
|
||||
} @else { <mat-icon class="nom-linked-card__icon">person</mat-icon> }
|
||||
<img
|
||||
[src]="linkedProfileImage"
|
||||
class="nom-linked-card__photo zoomable"
|
||||
(click)="zoomImage(linkedProfileImage, 'Photo')"
|
||||
/>
|
||||
} @else {
|
||||
<mat-icon class="nom-linked-card__icon">person</mat-icon>
|
||||
}
|
||||
</div>
|
||||
<div class="nom-linked-card__info">
|
||||
<span class="nom-linked-card__name">{{ selectedClient.displayName }}</span>
|
||||
<span class="nom-linked-card__meta">ID: {{ selectedClient.id }}@if (selectedClient.mobileNo) { · {{ selectedClient.mobileNo }} }</span>
|
||||
<span class="nom-linked-card__meta"
|
||||
>ID: {{ selectedClient.id }}
|
||||
@if (selectedClient.mobileNo) {
|
||||
· {{ selectedClient.mobileNo }}
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
@if (linkedSignatureImage) {
|
||||
<div class="nom-linked-card__sig">
|
||||
<img [src]="linkedSignatureImage" alt="Signature" class="zoomable" (click)="zoomImage(linkedSignatureImage, 'Signature')" />
|
||||
<img
|
||||
[src]="linkedSignatureImage"
|
||||
alt="Signature"
|
||||
class="zoomable"
|
||||
(click)="zoomImage(linkedSignatureImage, 'Signature')"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
<button mat-icon-button type="button" (click)="removeSelectedClient()"><mat-icon>close</mat-icon></button>
|
||||
</div>
|
||||
|
||||
@if (fetchingDetails) {
|
||||
<div class="nom-fetching-hint"><mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner> Fetching details...</div>
|
||||
<div class="nom-fetching-hint">
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner> Fetching details...
|
||||
</div>
|
||||
}
|
||||
|
||||
@if (!fetchingDetails) {
|
||||
<!-- KYC details -->
|
||||
@if (selectedClient.dateOfBirth || selectedClient.panNumber || selectedClient.aadhaarMasked) {
|
||||
<div class="nom-readonly-row">
|
||||
@if (selectedClient.dateOfBirth) { <span class="nom-ro-item"><span class="nom-ro-label">DOB</span> {{ selectedClient.dateOfBirth | dateFormat }}</span> }
|
||||
@if (selectedClient.panNumber) { <span class="nom-ro-item"><span class="nom-ro-label">PAN</span> {{ selectedClient.panNumber }} @if (linkedKycStatus?.panVerified) { <mat-icon class="nom-ro-ok">check_circle</mat-icon> }</span> }
|
||||
@if (selectedClient.aadhaarMasked) { <span class="nom-ro-item"><span class="nom-ro-label">Aadhaar</span> {{ selectedClient.aadhaarMasked }} @if (linkedKycStatus?.aadhaarVerified) { <mat-icon class="nom-ro-ok">check_circle</mat-icon> }</span> }
|
||||
@if (selectedClient.dateOfBirth) {
|
||||
<span class="nom-ro-item"
|
||||
><span class="nom-ro-label">DOB</span> {{ selectedClient.dateOfBirth | dateFormat }}</span
|
||||
>
|
||||
}
|
||||
@if (selectedClient.panNumber) {
|
||||
<span class="nom-ro-item"
|
||||
><span class="nom-ro-label">PAN</span> {{ selectedClient.panNumber }}
|
||||
@if (linkedKycStatus?.panVerified) {
|
||||
<mat-icon class="nom-ro-ok">check_circle</mat-icon>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
@if (selectedClient.aadhaarMasked) {
|
||||
<span class="nom-ro-item"
|
||||
><span class="nom-ro-label">Aadhaar</span> {{ selectedClient.aadhaarMasked }}
|
||||
@if (linkedKycStatus?.aadhaarVerified) {
|
||||
<mat-icon class="nom-ro-ok">check_circle</mat-icon>
|
||||
}
|
||||
</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -246,19 +359,25 @@
|
||||
<mat-form-field appearance="outline" class="nom-field-item">
|
||||
<mat-label>Nomination Type</mat-label>
|
||||
<mat-select [(ngModel)]="nomineeNomType">
|
||||
@for (opt of nominationTypeOptions; track opt.value) { <mat-option [value]="opt.value">{{ opt.label }}</mat-option> }
|
||||
@for (opt of nominationTypeOptions; track opt.value) {
|
||||
<mat-option [value]="opt.value">{{ opt.label }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="nom-field-item">
|
||||
<mat-label>Relationship</mat-label>
|
||||
<mat-select [(ngModel)]="nomineeRelationshipId">
|
||||
@for (rel of relationshipOptions; track rel) { <mat-option [value]="rel.id">{{ rel.name }}</mat-option> }
|
||||
@for (rel of relationshipOptions; track rel) {
|
||||
<mat-option [value]="rel.id">{{ rel.name }}</mat-option>
|
||||
}
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="outline" class="nom-field-item nom-field-item--sm">
|
||||
<mat-label>Share %</mat-label>
|
||||
<input matInput type="number" [(ngModel)]="nomineeShare" min="1" [max]="maxAllowedShare" />
|
||||
@if (nomineeShare && nomineeShare > maxAllowedShare) { <mat-error>Max {{ maxAllowedShare }}%</mat-error> }
|
||||
@if (nomineeShare && nomineeShare > maxAllowedShare) {
|
||||
<mat-error>Max {{ maxAllowedShare }}%</mat-error>
|
||||
}
|
||||
</mat-form-field>
|
||||
<div class="nom-field-item nom-field-item--toggle">
|
||||
<mat-slide-toggle [(ngModel)]="nomineeIsMinor">Minor</mat-slide-toggle>
|
||||
@@ -270,11 +389,21 @@
|
||||
<span class="nom-guardian-label"><mat-icon>shield</mat-icon> Guardian</span>
|
||||
@if (!selectedGuardian) {
|
||||
<mat-form-field appearance="outline" class="nom-search-field">
|
||||
<input matInput [ngModel]="guardianSearchText" (ngModelChange)="onGuardianSearch($event)" placeholder="Search guardian..." autocomplete="off" />
|
||||
<input
|
||||
matInput
|
||||
[ngModel]="guardianSearchText"
|
||||
(ngModelChange)="onGuardianSearch($event)"
|
||||
placeholder="Search guardian..."
|
||||
autocomplete="off"
|
||||
/>
|
||||
</mat-form-field>
|
||||
@if (guardianSearchResults.length > 0 || guardianSearching) {
|
||||
<div class="nom-search-results nom-search-results--inline">
|
||||
@if (guardianSearching) { <div class="nom-search-loading"><mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner></div> }
|
||||
@if (guardianSearching) {
|
||||
<div class="nom-search-loading">
|
||||
<mat-progress-spinner diameter="14" mode="indeterminate"></mat-progress-spinner>
|
||||
</div>
|
||||
}
|
||||
@for (client of guardianSearchResults; track client.id) {
|
||||
<div class="nom-search-item" (click)="selectGuardian(client)">
|
||||
<mat-icon class="nom-search-item__icon">person</mat-icon>
|
||||
@@ -284,7 +413,12 @@
|
||||
</div>
|
||||
}
|
||||
} @else {
|
||||
<span class="nom-guardian-selected">{{ selectedGuardian.displayName }} <button mat-icon-button (click)="selectedGuardian = null"><mat-icon>close</mat-icon></button></span>
|
||||
<span class="nom-guardian-selected"
|
||||
>{{ selectedGuardian.displayName }}
|
||||
<button mat-icon-button (click)="selectedGuardian = null">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button></span
|
||||
>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -296,11 +430,24 @@
|
||||
|
||||
<div class="nom-form-actions">
|
||||
<button mat-stroked-button type="button" (click)="resetAddForm()">Cancel</button>
|
||||
<button mat-raised-button color="primary" type="button"
|
||||
[disabled]="!nomineeRelationshipId || !nomineeShare || nomineeSaving || (nomineeShare > maxAllowedShare) || (nomineeIsMinor && !selectedGuardian)"
|
||||
(click)="saveNominee()">
|
||||
@if (nomineeSaving) { <mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner> }
|
||||
@else { <mat-icon>check</mat-icon> Save }
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
type="button"
|
||||
[disabled]="
|
||||
!nomineeRelationshipId ||
|
||||
!nomineeShare ||
|
||||
nomineeSaving ||
|
||||
nomineeShare > maxAllowedShare ||
|
||||
(nomineeIsMinor && !selectedGuardian)
|
||||
"
|
||||
(click)="saveNominee()"
|
||||
>
|
||||
@if (nomineeSaving) {
|
||||
<mat-progress-spinner diameter="16" mode="indeterminate"></mat-progress-spinner>
|
||||
} @else {
|
||||
<mat-icon>check</mat-icon> Save
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -8,176 +8,708 @@
|
||||
$primary: #1074b9;
|
||||
$primary-light: #e8f0fe;
|
||||
|
||||
.nom-container { padding: 16px 24px; }
|
||||
.nom-container {
|
||||
padding: 16px 24px;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.nom-page-header {
|
||||
background: #0f172a; padding: 14px 20px; border-radius: 10px; margin-bottom: 16px;
|
||||
&__left { display: flex; align-items: center; gap: 10px; }
|
||||
&__icon { color: #60a5fa; }
|
||||
&__title { font-size: 15px; font-weight: 600; color: #fff; margin: 0; }
|
||||
&__sub { font-size: 10px; color: rgba(255,255,255,0.45); margin: 1px 0 0; }
|
||||
background: #0f172a;
|
||||
padding: 14px 20px;
|
||||
border-radius: 10px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
&__left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
color: #60a5fa;
|
||||
}
|
||||
|
||||
&__title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
font-size: 10px;
|
||||
color: rgb(255 255 255 / 45%);
|
||||
margin: 1px 0 0;
|
||||
}
|
||||
}
|
||||
|
||||
.nom-loading { display: flex; align-items: center; gap: 8px; padding: 24px 0; justify-content: center; font-size: 13px; color: #64748b;
|
||||
&--sm { padding: 12px 0; }
|
||||
.nom-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 24px 0;
|
||||
justify-content: center;
|
||||
font-size: 13px;
|
||||
color: #64748b;
|
||||
|
||||
&--sm {
|
||||
padding: 12px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.nom-no-accounts {
|
||||
text-align: center; padding: 36px 20px; background: #f8fafc; border-radius: 8px; border: 1px dashed #e2e8f0;
|
||||
&__icon { font-size: 40px; width: 40px; height: 40px; color: #94a3b8; margin-bottom: 8px; }
|
||||
p { color: #64748b; font-size: 13px; margin: 2px 0; }
|
||||
&__sub { font-size: 11px; color: #94a3b8; }
|
||||
text-align: center;
|
||||
padding: 36px 20px;
|
||||
background: #f8fafc;
|
||||
border-radius: 8px;
|
||||
border: 1px dashed #e2e8f0;
|
||||
|
||||
&__icon {
|
||||
font-size: 40px;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
color: #94a3b8;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
p {
|
||||
color: #64748b;
|
||||
font-size: 13px;
|
||||
margin: 2px 0;
|
||||
}
|
||||
|
||||
&__sub {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
/* Toolbar: chips on top, account + add below */
|
||||
.nom-toolbar {
|
||||
display: flex; flex-direction: column; gap: 10px; margin-bottom: 12px;
|
||||
&__right { display: flex; align-items: center; gap: 10px; }
|
||||
}
|
||||
.nom-type-chips { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.nom-type-chip {
|
||||
display: inline-flex; align-items: center; gap: 5px; padding: 6px 12px;
|
||||
border: 1.5px solid #e2e8f0; border-radius: 6px; background: #fff;
|
||||
font-size: 12px; font-weight: 500; color: #64748b; cursor: pointer; transition: all 0.15s;
|
||||
mat-icon { font-size: 16px; width: 16px; height: 16px; }
|
||||
&__count { font-size: 9px; font-weight: 700; background: #f1f5f9; color: #64748b; padding: 0 5px; border-radius: 6px; }
|
||||
&.active { border-color: $primary; background: $primary-light; color: $primary;
|
||||
.nom-type-chip__count { background: $primary; color: #fff; }
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
|
||||
&__right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
&:hover:not(.active) { border-color: #94a3b8; }
|
||||
}
|
||||
.nom-account-select { flex: 1; min-width: 280px; }
|
||||
.nom-add-btn { border-radius: 6px !important; font-size: 12px !important; height: 36px !important; white-space: nowrap; }
|
||||
|
||||
.nom-type-chips {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.nom-type-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 6px 12px;
|
||||
border: 1.5px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: #64748b;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s;
|
||||
|
||||
mat-icon {
|
||||
font-size: 16px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
&__count {
|
||||
font-size: 9px;
|
||||
font-weight: 700;
|
||||
background: #f1f5f9;
|
||||
color: #64748b;
|
||||
padding: 0 5px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
|
||||
&.active {
|
||||
border-color: $primary;
|
||||
background: $primary-light;
|
||||
color: $primary;
|
||||
|
||||
.nom-type-chip__count {
|
||||
background: $primary;
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover:not(.active) {
|
||||
border-color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
.nom-account-select {
|
||||
flex: 1;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.nom-add-btn {
|
||||
border-radius: 6px !important;
|
||||
font-size: 12px !important;
|
||||
height: 36px !important;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Share bar */
|
||||
.nom-share-bar {
|
||||
display: flex; align-items: center; gap: 8px; padding: 6px 12px;
|
||||
background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; margin-bottom: 8px;
|
||||
&__label { font-size: 11px; font-weight: 600; color: #64748b; white-space: nowrap; }
|
||||
&__track { flex: 1; height: 6px; background: #e2e8f0; border-radius: 3px; overflow: hidden; }
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 6px 12px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&__label {
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: #64748b;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
&__track {
|
||||
flex: 1;
|
||||
height: 6px;
|
||||
background: #e2e8f0;
|
||||
border-radius: 3px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&__fill {
|
||||
height: 100%; border-radius: 3px; transition: width 0.3s;
|
||||
height: 100%;
|
||||
border-radius: 3px;
|
||||
transition: width 0.3s;
|
||||
background: linear-gradient(90deg, #3b82f6, #10b981);
|
||||
&.over { background: #ef4444; } &.complete { background: linear-gradient(90deg, #10b981, #059669); }
|
||||
|
||||
&.over {
|
||||
background: #ef4444;
|
||||
}
|
||||
|
||||
&.complete {
|
||||
background: linear-gradient(90deg, #10b981, #059669);
|
||||
}
|
||||
}
|
||||
|
||||
&__value {
|
||||
font-size: 13px;
|
||||
font-weight: 700;
|
||||
color: #10b981;
|
||||
min-width: 35px;
|
||||
text-align: right;
|
||||
|
||||
&.over {
|
||||
color: #ef4444;
|
||||
}
|
||||
}
|
||||
&__value { font-size: 13px; font-weight: 700; color: #10b981; min-width: 35px; text-align: right; &.over { color: #ef4444; } }
|
||||
}
|
||||
|
||||
/* Nominee card — compact */
|
||||
.nom-card { border: 1px solid #e2e8f0; border-radius: 6px; margin-bottom: 6px; overflow: hidden; }
|
||||
.nom-card__main { display: flex; align-items: center; gap: 10px; padding: 8px 12px; }
|
||||
.nom-card {
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nom-card__main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 8px 12px;
|
||||
}
|
||||
|
||||
.nom-card__avatar {
|
||||
width: 32px; height: 32px; border-radius: 50%; background: #dbeafe;
|
||||
display: flex; align-items: center; justify-content: center; color: #1565c0; flex-shrink: 0;
|
||||
mat-icon { font-size: 18px; }
|
||||
}
|
||||
.nom-card__photo { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; }
|
||||
.nom-card__sig {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 50%;
|
||||
background: #dbeafe;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #1565c0;
|
||||
flex-shrink: 0;
|
||||
img { height: 24px; max-width: 60px; object-fit: contain; border: 1px solid #e2e8f0; border-radius: 3px; background: #fff; padding: 1px 3px; }
|
||||
}
|
||||
.nom-card__info { flex: 1; min-width: 0; }
|
||||
.nom-card__name { font-weight: 600; font-size: 13px; color: #1e293b; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.nom-card__meta { font-size: 11px; color: #64748b; display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.nom-card__id {
|
||||
font-size: 10px; font-weight: 600; color: #1d4ed8; background: #eff6ff;
|
||||
padding: 2px 8px; border-radius: 4px; white-space: nowrap;
|
||||
font-family: 'Roboto Mono', monospace; letter-spacing: 0.3px; flex-shrink: 0;
|
||||
}
|
||||
.nom-card__share { text-align: center; min-width: 40px; flex-shrink: 0; }
|
||||
.nom-card__share-value { font-size: 15px; font-weight: 700; color: #1565c0; }
|
||||
.nom-card__actions { display: flex; gap: 0; flex-shrink: 0;
|
||||
button { width: 32px !important; height: 32px !important; line-height: 32px !important;
|
||||
mat-icon { font-size: 18px; }
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
.nom-card__delete { color: #94a3b8 !important; &:hover { color: #ef4444 !important; } }
|
||||
|
||||
.nom-card__photo {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.nom-card__sig {
|
||||
flex-shrink: 0;
|
||||
|
||||
img {
|
||||
height: 24px;
|
||||
max-width: 60px;
|
||||
object-fit: contain;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
padding: 1px 3px;
|
||||
}
|
||||
}
|
||||
|
||||
.nom-card__info {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.nom-card__name {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: #1e293b;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.nom-card__meta {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
display: block;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.nom-card__id {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #1d4ed8;
|
||||
background: #eff6ff;
|
||||
padding: 2px 8px;
|
||||
border-radius: 4px;
|
||||
white-space: nowrap;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
letter-spacing: 0.3px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nom-card__share {
|
||||
text-align: center;
|
||||
min-width: 40px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.nom-card__share-value {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
color: #1565c0;
|
||||
}
|
||||
|
||||
.nom-card__actions {
|
||||
display: flex;
|
||||
gap: 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
button {
|
||||
width: 32px !important;
|
||||
height: 32px !important;
|
||||
line-height: 32px !important;
|
||||
|
||||
mat-icon {
|
||||
font-size: 18px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.nom-card__delete {
|
||||
color: #94a3b8 !important;
|
||||
|
||||
&:hover {
|
||||
color: #ef4444 !important;
|
||||
}
|
||||
}
|
||||
|
||||
.nom-card__guardian {
|
||||
display: flex; align-items: center; gap: 5px; padding: 5px 12px; background: #fff7ed;
|
||||
font-size: 11px; color: #92400e; border-top: 1px solid #fde68a;
|
||||
mat-icon { font-size: 14px; width: 14px; height: 14px; }
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 5px 12px;
|
||||
background: #fff7ed;
|
||||
font-size: 11px;
|
||||
color: #92400e;
|
||||
border-top: 1px solid #fde68a;
|
||||
|
||||
mat-icon {
|
||||
font-size: 14px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Inline edit */
|
||||
.nom-card__edit { padding: 8px 12px; border-top: 1px solid #e2e8f0; background: #f8fafc; }
|
||||
.nom-edit-row { display: flex; align-items: center; gap: 8px; }
|
||||
.nom-edit-field { flex: 1; &--sm { max-width: 100px; } }
|
||||
.nom-edit-save, .nom-edit-cancel { min-width: 36px !important; height: 36px !important; padding: 0 !important; }
|
||||
.nom-card__edit {
|
||||
padding: 8px 12px;
|
||||
border-top: 1px solid #e2e8f0;
|
||||
background: #f8fafc;
|
||||
}
|
||||
|
||||
.nom-edit-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.nom-edit-field {
|
||||
flex: 1;
|
||||
|
||||
&--sm {
|
||||
max-width: 100px;
|
||||
}
|
||||
}
|
||||
|
||||
.nom-edit-save,
|
||||
.nom-edit-cancel {
|
||||
min-width: 36px !important;
|
||||
height: 36px !important;
|
||||
padding: 0 !important;
|
||||
}
|
||||
|
||||
/* Empty */
|
||||
.nom-empty {
|
||||
text-align: center; padding: 24px; background: #f8fafc; border-radius: 6px; border: 1px dashed #e2e8f0;
|
||||
mat-icon { font-size: 28px; width: 28px; height: 28px; color: #94a3b8; margin-bottom: 4px; display: block; margin: 0 auto 4px; }
|
||||
span { font-size: 12px; color: #94a3b8; }
|
||||
text-align: center;
|
||||
padding: 24px;
|
||||
background: #f8fafc;
|
||||
border-radius: 6px;
|
||||
border: 1px dashed #e2e8f0;
|
||||
|
||||
mat-icon {
|
||||
font-size: 28px;
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
color: #94a3b8;
|
||||
display: block;
|
||||
margin: 0 auto 4px;
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: 12px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
}
|
||||
|
||||
/* ═══ Add Form ═══ */
|
||||
.nom-add-form {
|
||||
border: 1px solid #e2e8f0; border-radius: 8px; padding: 14px; margin-top: 8px; background: #fafbfc;
|
||||
&__title { display: flex; align-items: center; gap: 6px; font-size: 13px; font-weight: 600; color: #1e293b; margin-bottom: 12px; }
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 8px;
|
||||
padding: 14px;
|
||||
margin-top: 8px;
|
||||
background: #fafbfc;
|
||||
|
||||
&__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Search */
|
||||
.nom-search-wrapper { position: relative; margin-bottom: 8px; }
|
||||
.nom-search-field { width: 100%; }
|
||||
.nom-search-prefix { color: #94a3b8; }
|
||||
.nom-search-wrapper {
|
||||
position: relative;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.nom-search-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nom-search-prefix {
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.nom-search-results {
|
||||
position: absolute; top: 56px; left: 0; right: 0; z-index: 100;
|
||||
background: #fff; border: 1px solid #e2e8f0; border-radius: 6px;
|
||||
box-shadow: 0 6px 20px rgba(0,0,0,0.1); max-height: 220px; overflow-y: auto;
|
||||
&--inline { position: relative; top: 0; margin-top: 4px; }
|
||||
position: absolute;
|
||||
top: 56px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
background: #fff;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
box-shadow: 0 6px 20px rgb(0 0 0 / 10%);
|
||||
max-height: 220px;
|
||||
overflow-y: auto;
|
||||
|
||||
&--inline {
|
||||
position: relative;
|
||||
top: 0;
|
||||
margin-top: 4px;
|
||||
}
|
||||
}
|
||||
.nom-search-loading { display: flex; align-items: center; gap: 6px; padding: 10px 12px; font-size: 12px; color: #64748b; }
|
||||
|
||||
.nom-search-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 10px 12px;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.nom-search-item {
|
||||
display: flex; align-items: center; gap: 8px; padding: 8px 12px; cursor: pointer; border-bottom: 1px solid #f1f5f9;
|
||||
&:last-child { border-bottom: none; } &:hover { background: #f0f7ff; }
|
||||
&__icon { color: $primary; font-size: 18px; }
|
||||
&__info { display: flex; flex-direction: column; flex: 1; }
|
||||
&__name { font-size: 13px; font-weight: 600; color: #1e293b; }
|
||||
&__meta { font-size: 10px; color: #64748b; }
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
background: #f0f7ff;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
color: $primary;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
&__info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
font-size: 10px;
|
||||
color: #64748b;
|
||||
}
|
||||
}
|
||||
|
||||
.search-no-results-hint {
|
||||
font-size: 11px;
|
||||
color: #94a3b8;
|
||||
text-align: center;
|
||||
padding: 4px 0;
|
||||
}
|
||||
.search-no-results-hint { font-size: 11px; color: #94a3b8; text-align: center; padding: 4px 0; }
|
||||
|
||||
/* Linked card */
|
||||
.nom-linked-card {
|
||||
display: flex; align-items: center; gap: 8px; padding: 8px 12px;
|
||||
background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; margin-bottom: 8px;
|
||||
&__left { flex-shrink: 0; }
|
||||
&__icon { color: $primary; }
|
||||
&__photo { width: 36px; height: 36px; border-radius: 50%; object-fit: cover; border: 2px solid #e2e8f0; }
|
||||
&__info { flex: 1; display: flex; flex-direction: column; }
|
||||
&__name { font-size: 13px; font-weight: 600; color: #1e293b; }
|
||||
&__meta { font-size: 11px; color: #64748b; font-family: 'Roboto Mono', monospace; }
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 8px 12px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&__left {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
color: $primary;
|
||||
}
|
||||
|
||||
&__photo {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 2px solid #e2e8f0;
|
||||
}
|
||||
|
||||
&__info {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
|
||||
&__meta {
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
font-family: 'Roboto Mono', monospace;
|
||||
}
|
||||
|
||||
&__sig {
|
||||
flex-shrink: 0;
|
||||
img { height: 24px; max-width: 60px; object-fit: contain; border: 1px solid #e2e8f0; border-radius: 3px; background: #fff; padding: 1px 3px; }
|
||||
|
||||
img {
|
||||
height: 24px;
|
||||
max-width: 60px;
|
||||
object-fit: contain;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 3px;
|
||||
background: #fff;
|
||||
padding: 1px 3px;
|
||||
}
|
||||
}
|
||||
}
|
||||
.nom-fetching-hint { display: flex; align-items: center; gap: 6px; font-size: 11px; color: #64748b; margin: -4px 0 8px; }
|
||||
|
||||
.nom-fetching-hint {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 11px;
|
||||
color: #64748b;
|
||||
margin: -4px 0 8px;
|
||||
}
|
||||
|
||||
/* Read-only KYC row */
|
||||
.nom-readonly-row {
|
||||
display: flex; flex-wrap: wrap; gap: 16px; padding: 8px 12px;
|
||||
background: #f8fafc; border: 1px solid #e2e8f0; border-radius: 6px; margin-bottom: 10px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
padding: 8px 12px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.nom-ro-item {
|
||||
font-size: 12px;
|
||||
color: #1e293b;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.nom-ro-label {
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
color: #94a3b8;
|
||||
text-transform: uppercase;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.nom-ro-ok {
|
||||
font-size: 14px !important;
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
color: #16a34a;
|
||||
}
|
||||
.nom-ro-item { font-size: 12px; color: #1e293b; display: inline-flex; align-items: center; gap: 4px; }
|
||||
.nom-ro-label { font-size: 10px; font-weight: 600; color: #94a3b8; text-transform: uppercase; margin-right: 2px; }
|
||||
.nom-ro-ok { font-size: 14px !important; width: 14px !important; height: 14px !important; color: #16a34a; }
|
||||
|
||||
/* Nominee fields */
|
||||
.nom-fields-row { display: flex; flex-wrap: wrap; gap: 0 12px; align-items: center; margin-bottom: 4px; }
|
||||
.nom-field-item { flex: 1; min-width: 160px; &--sm { max-width: 100px; min-width: 80px; } &--toggle { padding-top: 8px; } }
|
||||
.nom-remarks-field { width: 100%; }
|
||||
.nom-form-actions { display: flex; justify-content: flex-end; gap: 8px; margin-top: 4px; }
|
||||
.nom-fields-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0 12px;
|
||||
align-items: center;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.nom-field-item {
|
||||
flex: 1;
|
||||
min-width: 160px;
|
||||
|
||||
&--sm {
|
||||
max-width: 100px;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
&--toggle {
|
||||
padding-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.nom-remarks-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nom-form-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* Guardian */
|
||||
.nom-guardian-section { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
|
||||
.nom-guardian-label { display: flex; align-items: center; gap: 4px; font-size: 12px; font-weight: 600; color: #92400e; mat-icon { font-size: 16px; } }
|
||||
.nom-guardian-selected { display: flex; align-items: center; gap: 4px; font-size: 13px; font-weight: 500; color: #1e293b;
|
||||
button { width: 24px !important; height: 24px !important; line-height: 24px !important; mat-icon { font-size: 14px; } }
|
||||
.nom-guardian-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.nom-guardian-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: #92400e;
|
||||
|
||||
mat-icon {
|
||||
font-size: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.nom-guardian-selected {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: #1e293b;
|
||||
|
||||
button {
|
||||
width: 24px !important;
|
||||
height: 24px !important;
|
||||
line-height: 24px !important;
|
||||
|
||||
mat-icon {
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Zoom */
|
||||
.zoomable { cursor: zoom-in; transition: transform 0.15s; &:hover { transform: scale(1.08); box-shadow: 0 4px 12px rgba(0,0,0,0.15); } }
|
||||
::ng-deep .image-zoom-panel { .mat-mdc-dialog-container { padding: 0 !important; border-radius: 8px !important; overflow: hidden; } }
|
||||
.zoomable {
|
||||
cursor: zoom-in;
|
||||
transition: transform 0.15s;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.08);
|
||||
box-shadow: 0 4px 12px rgb(0 0 0 / 15%);
|
||||
}
|
||||
}
|
||||
|
||||
::ng-deep .image-zoom-panel {
|
||||
.mat-mdc-dialog-container {
|
||||
padding: 0 !important;
|
||||
border-radius: 8px !important;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,11 +109,11 @@ export class NomineeTabComponent implements OnInit {
|
||||
editNomType: string = 'DA1';
|
||||
|
||||
get hasAccounts(): boolean {
|
||||
return this.accountGroups.some(g => g.accounts.length > 0);
|
||||
return this.accountGroups.some((g) => g.accounts.length > 0);
|
||||
}
|
||||
|
||||
get selectedGroup(): AccountGroup | undefined {
|
||||
return this.accountGroups.find(g => g.type === this.selectedType);
|
||||
return this.accountGroups.find((g) => g.type === this.selectedType);
|
||||
}
|
||||
|
||||
get selectedAccount(): any {
|
||||
@@ -154,13 +154,43 @@ export class NomineeTabComponent implements OnInit {
|
||||
next: (data: any) => {
|
||||
this.loading = false;
|
||||
this.accountGroups = [
|
||||
{ type: 'savings', apiType: 'SB', label: 'Savings Account', icon: 'savings', accounts: this.filterActive(data.savingsAccounts) },
|
||||
{ type: 'current', apiType: 'CB', label: 'Current Account', icon: 'account_balance', accounts: this.filterActive(data.currentAccounts) },
|
||||
{ type: 'fd', apiType: 'FD', label: 'Fixed Deposit', icon: 'lock', accounts: this.filterActive(data.fixedDepositAccounts) },
|
||||
{ type: 'rd', apiType: 'RD', label: 'Recurring Deposit', icon: 'autorenew', accounts: this.filterActive(data.recurringDepositAccounts) },
|
||||
{ type: 'loan', apiType: 'LA', label: 'Loan Account', icon: 'credit_score', accounts: this.filterActive(data.loanAccounts) }
|
||||
{
|
||||
type: 'savings',
|
||||
apiType: 'SB',
|
||||
label: 'Savings Account',
|
||||
icon: 'savings',
|
||||
accounts: this.filterActive(data.savingsAccounts)
|
||||
},
|
||||
{
|
||||
type: 'current',
|
||||
apiType: 'CB',
|
||||
label: 'Current Account',
|
||||
icon: 'account_balance',
|
||||
accounts: this.filterActive(data.currentAccounts)
|
||||
},
|
||||
{
|
||||
type: 'fd',
|
||||
apiType: 'FD',
|
||||
label: 'Fixed Deposit',
|
||||
icon: 'lock',
|
||||
accounts: this.filterActive(data.fixedDepositAccounts)
|
||||
},
|
||||
{
|
||||
type: 'rd',
|
||||
apiType: 'RD',
|
||||
label: 'Recurring Deposit',
|
||||
icon: 'autorenew',
|
||||
accounts: this.filterActive(data.recurringDepositAccounts)
|
||||
},
|
||||
{
|
||||
type: 'loan',
|
||||
apiType: 'LA',
|
||||
label: 'Loan Account',
|
||||
icon: 'credit_score',
|
||||
accounts: this.filterActive(data.loanAccounts)
|
||||
}
|
||||
];
|
||||
const firstWithAccounts = this.accountGroups.find(g => g.accounts.length > 0);
|
||||
const firstWithAccounts = this.accountGroups.find((g) => g.accounts.length > 0);
|
||||
if (firstWithAccounts) {
|
||||
this.selectedType = firstWithAccounts.type;
|
||||
// Auto-select if only 1 account in the group
|
||||
@@ -170,18 +200,16 @@ export class NomineeTabComponent implements OnInit {
|
||||
}
|
||||
}
|
||||
},
|
||||
error: () => { this.loading = false; }
|
||||
error: () => {
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private filterActive(accounts: any[]): any[] {
|
||||
if (!accounts) return [];
|
||||
// Include all non-closed accounts (active, disbursed, pending, etc.)
|
||||
return accounts.filter((a: any) =>
|
||||
a.status?.active ||
|
||||
a.status?.value === 'Active' ||
|
||||
!a.status?.closed
|
||||
);
|
||||
return accounts.filter((a: any) => a.status?.active || a.status?.value === 'Active' || !a.status?.closed);
|
||||
}
|
||||
|
||||
onTypeChange(): void {
|
||||
@@ -198,7 +226,9 @@ export class NomineeTabComponent implements OnInit {
|
||||
onAccountChange(): void {
|
||||
this.nominees = [];
|
||||
this.resetAddForm();
|
||||
if (this.selectedAccountId) { this.loadNominees(); }
|
||||
if (this.selectedAccountId) {
|
||||
this.loadNominees();
|
||||
}
|
||||
}
|
||||
|
||||
private loadNominees(): void {
|
||||
@@ -210,9 +240,12 @@ export class NomineeTabComponent implements OnInit {
|
||||
this.nomineesLoading = false;
|
||||
this.nominees = Array.isArray(data) ? data : [];
|
||||
// Enrich each nominee with profile image & signature
|
||||
this.nominees.forEach(n => this.enrichNomineeImages(n));
|
||||
this.nominees.forEach((n) => this.enrichNomineeImages(n));
|
||||
},
|
||||
error: () => { this.nomineesLoading = false; this.nominees = []; }
|
||||
error: () => {
|
||||
this.nomineesLoading = false;
|
||||
this.nominees = [];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -224,7 +257,9 @@ export class NomineeTabComponent implements OnInit {
|
||||
// Profile image
|
||||
this.clientsService.getClientProfileImage(cid).subscribe({
|
||||
next: (img: any) => {
|
||||
if (img) { nominee.profileImageUrl = this.sanitizer.bypassSecurityTrustResourceUrl(img); }
|
||||
if (img) {
|
||||
nominee.profileImageUrl = this.sanitizer.bypassSecurityTrustResourceUrl(img);
|
||||
}
|
||||
},
|
||||
error: () => {}
|
||||
});
|
||||
@@ -232,8 +267,8 @@ export class NomineeTabComponent implements OnInit {
|
||||
// Signature
|
||||
this.clientsService.getClientDocuments(cid).subscribe({
|
||||
next: (docs: any[]) => {
|
||||
const sigDoc = docs?.find((d: any) =>
|
||||
d.name?.toLowerCase().includes('signature') || d.description?.toLowerCase().includes('signature')
|
||||
const sigDoc = docs?.find(
|
||||
(d: any) => d.name?.toLowerCase().includes('signature') || d.description?.toLowerCase().includes('signature')
|
||||
);
|
||||
if (sigDoc) {
|
||||
this.clientsService.getClientSignatureImage(cid, sigDoc.id.toString()).subscribe({
|
||||
@@ -253,7 +288,10 @@ export class NomineeTabComponent implements OnInit {
|
||||
onSearch(text: string): void {
|
||||
this.searchText = text;
|
||||
clearTimeout(this.searchTimer);
|
||||
if (!text || text.length < 2) { this.searchResults = []; return; }
|
||||
if (!text || text.length < 2) {
|
||||
this.searchResults = [];
|
||||
return;
|
||||
}
|
||||
this.searching = true;
|
||||
this.searchTimer = setTimeout(() => {
|
||||
this.clientsService.searchByText(text, 0, 8).subscribe({
|
||||
@@ -262,13 +300,19 @@ export class NomineeTabComponent implements OnInit {
|
||||
this.searching = false;
|
||||
this.clientsService.enrichSearchResults(this.searchResults);
|
||||
},
|
||||
error: () => { this.searchResults = []; this.searching = false; }
|
||||
error: () => {
|
||||
this.searchResults = [];
|
||||
this.searching = false;
|
||||
}
|
||||
});
|
||||
}, 400);
|
||||
}
|
||||
|
||||
dismissSearch(): void {
|
||||
setTimeout(() => { this.searchResults = []; this.searching = false; }, 200);
|
||||
setTimeout(() => {
|
||||
this.searchResults = [];
|
||||
this.searching = false;
|
||||
}, 200);
|
||||
}
|
||||
|
||||
selectClient(client: any): void {
|
||||
@@ -291,11 +335,18 @@ export class NomineeTabComponent implements OnInit {
|
||||
this.linkedSignatureImage = null;
|
||||
this.linkedKycStatus = null;
|
||||
let pending = 4;
|
||||
const done = () => { pending--; if (pending <= 0) { this.fetchingDetails = false; } };
|
||||
const done = () => {
|
||||
pending--;
|
||||
if (pending <= 0) {
|
||||
this.fetchingDetails = false;
|
||||
}
|
||||
};
|
||||
|
||||
this.clientsService.getClientData(clientId.toString()).subscribe({
|
||||
next: (data: any) => {
|
||||
if (data?.dateOfBirth) { this.selectedClient = { ...this.selectedClient, dateOfBirth: data.dateOfBirth }; }
|
||||
if (data?.dateOfBirth) {
|
||||
this.selectedClient = { ...this.selectedClient, dateOfBirth: data.dateOfBirth };
|
||||
}
|
||||
done();
|
||||
},
|
||||
error: () => done()
|
||||
@@ -304,8 +355,15 @@ export class NomineeTabComponent implements OnInit {
|
||||
this.clientsService.getClientKyc(clientId.toString()).subscribe({
|
||||
next: (kyc: any) => {
|
||||
if (kyc) {
|
||||
this.selectedClient = { ...this.selectedClient, panNumber: kyc.panNumber, aadhaarMasked: kyc.aadhaarNumberMasked };
|
||||
this.linkedKycStatus = { panVerified: kyc.panVerified || false, aadhaarVerified: kyc.aadhaarVerified || false };
|
||||
this.selectedClient = {
|
||||
...this.selectedClient,
|
||||
panNumber: kyc.panNumber,
|
||||
aadhaarMasked: kyc.aadhaarNumberMasked
|
||||
};
|
||||
this.linkedKycStatus = {
|
||||
panVerified: kyc.panVerified || false,
|
||||
aadhaarVerified: kyc.aadhaarVerified || false
|
||||
};
|
||||
}
|
||||
done();
|
||||
},
|
||||
@@ -313,19 +371,31 @@ export class NomineeTabComponent implements OnInit {
|
||||
});
|
||||
|
||||
this.clientsService.getClientProfileImage(clientId.toString()).subscribe({
|
||||
next: (img: any) => { if (img) { this.linkedProfileImage = this.sanitizer.bypassSecurityTrustResourceUrl(img); } done(); },
|
||||
next: (img: any) => {
|
||||
if (img) {
|
||||
this.linkedProfileImage = this.sanitizer.bypassSecurityTrustResourceUrl(img);
|
||||
}
|
||||
done();
|
||||
},
|
||||
error: () => done()
|
||||
});
|
||||
|
||||
this.clientsService.getClientDocuments(clientId.toString()).subscribe({
|
||||
next: (docs: any[]) => {
|
||||
const sigDoc = docs?.find((d: any) => d.name?.toLowerCase().includes('signature') || d.description?.toLowerCase().includes('signature'));
|
||||
const sigDoc = docs?.find(
|
||||
(d: any) => d.name?.toLowerCase().includes('signature') || d.description?.toLowerCase().includes('signature')
|
||||
);
|
||||
if (sigDoc) {
|
||||
this.clientsService.getClientSignatureImage(clientId.toString(), sigDoc.id.toString()).subscribe({
|
||||
next: (blob: Blob) => { this.linkedSignatureImage = this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(blob)); done(); },
|
||||
next: (blob: Blob) => {
|
||||
this.linkedSignatureImage = this.sanitizer.bypassSecurityTrustUrl(URL.createObjectURL(blob));
|
||||
done();
|
||||
},
|
||||
error: () => done()
|
||||
});
|
||||
} else { done(); }
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
},
|
||||
error: () => done()
|
||||
});
|
||||
@@ -336,12 +406,22 @@ export class NomineeTabComponent 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, 8).subscribe({
|
||||
next: (data: any) => { this.guardianSearchResults = data?.content || []; this.guardianSearching = false; this.clientsService.enrichSearchResults(this.guardianSearchResults); },
|
||||
error: () => { this.guardianSearchResults = []; this.guardianSearching = false; }
|
||||
next: (data: any) => {
|
||||
this.guardianSearchResults = data?.content || [];
|
||||
this.guardianSearching = false;
|
||||
this.clientsService.enrichSearchResults(this.guardianSearchResults);
|
||||
},
|
||||
error: () => {
|
||||
this.guardianSearchResults = [];
|
||||
this.guardianSearching = false;
|
||||
}
|
||||
});
|
||||
}, 400);
|
||||
}
|
||||
@@ -385,7 +465,9 @@ export class NomineeTabComponent implements OnInit {
|
||||
this.loadNominees();
|
||||
this.resetAddForm();
|
||||
},
|
||||
error: () => { this.nomineeSaving = false; }
|
||||
error: () => {
|
||||
this.nomineeSaving = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -395,26 +477,30 @@ export class NomineeTabComponent implements OnInit {
|
||||
// Resolve the linked client ID — could be nomineeClientId or nested
|
||||
const clientId = nominee.nomineeClientId || nominee.linkedClientId || nominee.clientId;
|
||||
const displayName = nominee.nomineeDisplayName || nominee.firstName || '';
|
||||
import('../../client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component').then(m => {
|
||||
this.dialog.open(m.ClientFamilyMemberDialogComponent, {
|
||||
data: {
|
||||
context: 'View',
|
||||
member: {
|
||||
firstName: displayName,
|
||||
relationshipId: nominee.relationshipId,
|
||||
dateOfBirth: nominee.nomineeDateOfBirth || nominee.dateOfBirth,
|
||||
linkedClientId: clientId,
|
||||
linkedClientName: displayName,
|
||||
isAlive: true,
|
||||
isDependent: nominee.nomineeIsMinor,
|
||||
panNumber: nominee.panNumber || '',
|
||||
aadhaarNumber: nominee.aadhaarMasked || ''
|
||||
import('../../client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component').then(
|
||||
(m) => {
|
||||
this.dialog.open(m.ClientFamilyMemberDialogComponent, {
|
||||
data: {
|
||||
context: 'View',
|
||||
member: {
|
||||
firstName: displayName,
|
||||
relationshipId: nominee.relationshipId,
|
||||
dateOfBirth: nominee.nomineeDateOfBirth || nominee.dateOfBirth,
|
||||
linkedClientId: clientId,
|
||||
linkedClientName: displayName,
|
||||
isAlive: true,
|
||||
isDependent: nominee.nomineeIsMinor,
|
||||
panNumber: nominee.panNumber || '',
|
||||
aadhaarNumber: nominee.aadhaarMasked || ''
|
||||
},
|
||||
options: { relationshipIdOptions: this.relationshipOptions }
|
||||
},
|
||||
options: { relationshipIdOptions: this.relationshipOptions }
|
||||
},
|
||||
width: '640px', maxWidth: '95vw', panelClass: 'fm-dialog-panel'
|
||||
});
|
||||
});
|
||||
width: '640px',
|
||||
maxWidth: '95vw',
|
||||
panelClass: 'fm-dialog-panel'
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
editNominee(nominee: any, index: number): void {
|
||||
@@ -424,10 +510,12 @@ export class NomineeTabComponent implements OnInit {
|
||||
this.editNomType = nominee.nominationType || 'DA1';
|
||||
}
|
||||
|
||||
cancelEdit(): void { this.editingIndex = null; }
|
||||
cancelEdit(): void {
|
||||
this.editingIndex = null;
|
||||
}
|
||||
|
||||
maxShareForEdit(index: number): number {
|
||||
return 100 - this.nominees.reduce((sum, n, i) => i === index ? sum : sum + (n.percentageShare || 0), 0);
|
||||
return 100 - this.nominees.reduce((sum, n, i) => (i === index ? sum : sum + (n.percentageShare || 0)), 0);
|
||||
}
|
||||
|
||||
saveEdit(nominee: any, index: number): void {
|
||||
@@ -468,7 +556,8 @@ export class NomineeTabComponent implements OnInit {
|
||||
this.dialog.open(ImageZoomDialogComponent, {
|
||||
data: { imageSrc, title },
|
||||
panelClass: 'image-zoom-panel',
|
||||
maxWidth: '90vw', maxHeight: '90vh'
|
||||
maxWidth: '90vw',
|
||||
maxHeight: '90vh'
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
-->
|
||||
|
||||
<div class="personal-data-container mat-typography">
|
||||
|
||||
<!-- Personal Information -->
|
||||
<div class="pd-section">
|
||||
<div class="pd-section__header">
|
||||
@@ -50,13 +49,15 @@
|
||||
@if (clientViewData.dateOfBirth) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Date of Birth' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.dateOfBirth | dateFormat:'DD-MMMM-YYYY' }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.dateOfBirth | dateFormat: 'DD-MMMM-YYYY' }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.ageYears != null) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Age</div>
|
||||
<div class="pd-field__value">{{ clientViewData.ageYears }}y {{ clientViewData.ageMonths || 0 }}m {{ clientViewData.ageDays || 0 }}d</div>
|
||||
<div class="pd-field__value">
|
||||
{{ clientViewData.ageYears }}y {{ clientViewData.ageMonths || 0 }}m {{ clientViewData.ageDays || 0 }}d
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.gender) {
|
||||
@@ -75,7 +76,11 @@
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Minor</div>
|
||||
<div class="pd-field__value">
|
||||
<span class="pd-verify-badge" [class.pd-verify-badge--verified]="!clientViewData.isMinor" [class.pd-verify-badge--pending]="clientViewData.isMinor">
|
||||
<span
|
||||
class="pd-verify-badge"
|
||||
[class.pd-verify-badge--verified]="!clientViewData.isMinor"
|
||||
[class.pd-verify-badge--pending]="clientViewData.isMinor"
|
||||
>
|
||||
{{ clientViewData.isMinor ? 'Yes (Age < 10)' : 'No' }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -84,7 +89,11 @@
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Is staff' | translate }}</div>
|
||||
<div class="pd-field__value">
|
||||
<span class="pd-verify-badge" [class.pd-verify-badge--verified]="clientViewData.isStaff" [class.pd-verify-badge--pending]="!clientViewData.isStaff">
|
||||
<span
|
||||
class="pd-verify-badge"
|
||||
[class.pd-verify-badge--verified]="clientViewData.isStaff"
|
||||
[class.pd-verify-badge--pending]="!clientViewData.isStaff"
|
||||
>
|
||||
{{ clientViewData.isStaff ? 'Yes' : 'No' }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -100,7 +109,7 @@
|
||||
@if (clientViewData.dateOfBirth) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Incorporation Date' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.dateOfBirth | dateFormat:'DD-MM-YYYY' }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.dateOfBirth | dateFormat: 'DD-MM-YYYY' }}</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -112,7 +121,9 @@
|
||||
}
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Nationality</div>
|
||||
<div class="pd-field__value">{{ clientViewData.nationalityId || clientViewData.nationality || 'Indian' }}</div>
|
||||
<div class="pd-field__value">
|
||||
{{ clientViewData.nationalityId || clientViewData.nationality || 'Indian' }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -176,7 +187,9 @@
|
||||
@if (clientViewData.clientNonPersonDetails.incorpValidityTillDate) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Incorporation Validity Till Date' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.clientNonPersonDetails.incorpValidityTillDate | dateFormat:'DD-MM-YYYY' }}</div>
|
||||
<div class="pd-field__value">
|
||||
{{ clientViewData.clientNonPersonDetails.incorpValidityTillDate | dateFormat: 'DD-MM-YYYY' }}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.clientNonPersonDetails.incorpNumber) {
|
||||
@@ -221,7 +234,9 @@
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Email' | translate }}</div>
|
||||
<div class="pd-field__value">
|
||||
<span class="pd-contact-icon"><i class="fa fa-envelope-o"></i> {{ clientViewData.emailAddress || '-' }}</span>
|
||||
<span class="pd-contact-icon"
|
||||
><i class="fa fa-envelope-o"></i> {{ clientViewData.emailAddress || '-' }}</span
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -240,15 +255,19 @@
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Mobile DIP Status</div>
|
||||
<div class="pd-field__value">
|
||||
@if (clientViewData.mobileVerified === true || clientViewData.mobileVerified === 'true' || clientViewData.mobileVerified === 1
|
||||
|| clientViewData.dipVerified === true || clientViewData.dipVerified === 'true' || clientViewData.dipVerified === 1) {
|
||||
@if (
|
||||
clientViewData.mobileVerified === true ||
|
||||
clientViewData.mobileVerified === 'true' ||
|
||||
clientViewData.mobileVerified === 1 ||
|
||||
clientViewData.dipVerified === true ||
|
||||
clientViewData.dipVerified === 'true' ||
|
||||
clientViewData.dipVerified === 1
|
||||
) {
|
||||
<span class="pd-verify-badge pd-verify-badge--verified">
|
||||
<i class="fa fa-check-circle"></i> Verified
|
||||
</span>
|
||||
} @else {
|
||||
<span class="pd-verify-badge pd-verify-badge--pending">
|
||||
<i class="fa fa-clock-o"></i> Pending
|
||||
</span>
|
||||
<span class="pd-verify-badge pd-verify-badge--pending"> <i class="fa fa-clock-o"></i> Pending </span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -270,14 +289,16 @@
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Alternate Mobile DIP Status</div>
|
||||
<div class="pd-field__value">
|
||||
@if (clientViewData.alternateDipVerified === true || clientViewData.alternateDipVerified === 'true' || clientViewData.alternateDipVerified === 1) {
|
||||
@if (
|
||||
clientViewData.alternateDipVerified === true ||
|
||||
clientViewData.alternateDipVerified === 'true' ||
|
||||
clientViewData.alternateDipVerified === 1
|
||||
) {
|
||||
<span class="pd-verify-badge pd-verify-badge--verified">
|
||||
<i class="fa fa-check-circle"></i> Verified
|
||||
</span>
|
||||
} @else {
|
||||
<span class="pd-verify-badge pd-verify-badge--pending">
|
||||
<i class="fa fa-clock-o"></i> Pending
|
||||
</span>
|
||||
<span class="pd-verify-badge pd-verify-badge--pending"> <i class="fa fa-clock-o"></i> Pending </span>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@@ -299,10 +320,20 @@
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Name Screening (Sanctions)</div>
|
||||
<div class="pd-field__value">
|
||||
@if (clientViewData.nameVerified === true || clientViewData.nameVerified === 'true' || clientViewData.nameVerified === 1) {
|
||||
@if (
|
||||
clientViewData.nameVerified === true ||
|
||||
clientViewData.nameVerified === 'true' ||
|
||||
clientViewData.nameVerified === 1
|
||||
) {
|
||||
<span class="pd-verify-badge pd-verify-badge--verified"><i class="fa fa-check-circle"></i> Clear</span>
|
||||
} @else if (clientViewData.nameVerified === false || clientViewData.nameVerified === 'false' || clientViewData.nameVerified === 0) {
|
||||
<span class="pd-verify-badge pd-verify-badge--danger"><i class="fa fa-exclamation-triangle"></i> Match Found</span>
|
||||
} @else if (
|
||||
clientViewData.nameVerified === false ||
|
||||
clientViewData.nameVerified === 'false' ||
|
||||
clientViewData.nameVerified === 0
|
||||
) {
|
||||
<span class="pd-verify-badge pd-verify-badge--danger"
|
||||
><i class="fa fa-exclamation-triangle"></i> Match Found</span
|
||||
>
|
||||
} @else {
|
||||
<span class="pd-verify-badge pd-verify-badge--pending"><i class="fa fa-clock-o"></i> Pending</span>
|
||||
}
|
||||
@@ -320,60 +351,60 @@
|
||||
|
||||
<!-- Financial & Classification (Individual only) -->
|
||||
@if (clientViewData?.legalForm?.id !== 2) {
|
||||
<div class="pd-section">
|
||||
<div class="pd-section__header">
|
||||
<i class="fa fa-chart-line"></i>
|
||||
<h3>Financial & Classification</h3>
|
||||
</div>
|
||||
<div class="pd-section__body">
|
||||
<div class="pd-grid">
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Monthly Income</div>
|
||||
<div class="pd-field__value">
|
||||
@if (clientViewData.monthlyIncome != null && clientViewData.monthlyIncome !== '') {
|
||||
<span class="monospace">₹ {{ clientViewData.monthlyIncome | number }}</span>
|
||||
} @else {
|
||||
-
|
||||
}
|
||||
<div class="pd-section">
|
||||
<div class="pd-section__header">
|
||||
<i class="fa fa-chart-line"></i>
|
||||
<h3>Financial & Classification</h3>
|
||||
</div>
|
||||
<div class="pd-section__body">
|
||||
<div class="pd-grid">
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Monthly Income</div>
|
||||
<div class="pd-field__value">
|
||||
@if (clientViewData.monthlyIncome != null && clientViewData.monthlyIncome !== '') {
|
||||
<span class="monospace">₹ {{ clientViewData.monthlyIncome | number }}</span>
|
||||
} @else {
|
||||
-
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Annual Turnover (Last FY)</div>
|
||||
<div class="pd-field__value">
|
||||
@if (clientViewData.annualTurnover != null && clientViewData.annualTurnover !== '') {
|
||||
<span class="monospace">₹ {{ clientViewData.annualTurnover | number }}</span>
|
||||
} @else {
|
||||
-
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (clientViewData.clientType) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Client Type' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.clientType.name }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.clientClassification) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Client Classification' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.clientClassification.name }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.occupationCategory) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Occupation Category' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.occupationCategory.name }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.occupation) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Occupation' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.occupation.name }}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">Annual Turnover (Last FY)</div>
|
||||
<div class="pd-field__value">
|
||||
@if (clientViewData.annualTurnover != null && clientViewData.annualTurnover !== '') {
|
||||
<span class="monospace">₹ {{ clientViewData.annualTurnover | number }}</span>
|
||||
} @else {
|
||||
-
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
@if (clientViewData.clientType) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Client Type' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.clientType.name }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.clientClassification) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Client Classification' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.clientClassification.name }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.occupationCategory) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Occupation Category' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.occupationCategory.name }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.occupation) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Occupation' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.occupation.name }}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
<!-- Guardian Details (Minor) -->
|
||||
@@ -436,19 +467,19 @@
|
||||
@if (clientViewData.timeline?.submittedOnDate) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Submitted On' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.timeline.submittedOnDate | dateFormat:'DD-MM-YYYY' }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.timeline.submittedOnDate | dateFormat: 'DD-MM-YYYY' }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.activationDate) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Activation Date' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.activationDate | dateFormat:'DD-MM-YYYY' }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.activationDate | dateFormat: 'DD-MM-YYYY' }}</div>
|
||||
</div>
|
||||
}
|
||||
@if (clientViewData.timeline?.closedOnDate) {
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Closed Date' | translate }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.timeline.closedOnDate | dateFormat:'DD-MM-YYYY' }}</div>
|
||||
<div class="pd-field__value">{{ clientViewData.timeline.closedOnDate | dateFormat: 'DD-MM-YYYY' }}</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
@@ -467,10 +498,14 @@
|
||||
<div class="pd-field">
|
||||
<div class="pd-field__label">{{ 'labels.inputs.Client Status' | translate }}</div>
|
||||
<div class="pd-field__value">
|
||||
<span class="pd-field__value status-badge"
|
||||
<span
|
||||
class="pd-field__value status-badge"
|
||||
[class.status-active]="clientViewData.status.value === 'Active'"
|
||||
[class.status-closed]="clientViewData.status.value === 'Closed'"
|
||||
[class.status-pending]="clientViewData.status.value !== 'Active' && clientViewData.status.value !== 'Closed'">
|
||||
[class.status-pending]="
|
||||
clientViewData.status.value !== 'Active' && clientViewData.status.value !== 'Closed'
|
||||
"
|
||||
>
|
||||
{{ 'labels.status.' + clientViewData.status.value | translate }}
|
||||
</span>
|
||||
</div>
|
||||
@@ -485,5 +520,4 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
*/
|
||||
|
||||
@use 'sass:color';
|
||||
|
||||
$primary: #1a73e8;
|
||||
$primary-light: #e8f0fe;
|
||||
$gray-50: #f8f9fa;
|
||||
@@ -83,15 +85,15 @@ $teal: #00897b;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
gap: 0;
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
@media (width <= 1024px) {
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@media (width <= 768px) {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
@media (width <= 480px) {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
@@ -165,7 +167,7 @@ $teal: #00897b;
|
||||
|
||||
&.status-active {
|
||||
background: rgba($green, 0.1);
|
||||
color: darken($green, 10%);
|
||||
color: color.adjust($green, $lightness: -10%);
|
||||
|
||||
&::before {
|
||||
background: $green;
|
||||
@@ -183,7 +185,7 @@ $teal: #00897b;
|
||||
|
||||
&.status-pending {
|
||||
background: rgba($orange, 0.1);
|
||||
color: darken($orange, 15%);
|
||||
color: color.adjust($orange, $lightness: -15%);
|
||||
|
||||
&::before {
|
||||
background: $orange;
|
||||
@@ -192,7 +194,7 @@ $teal: #00897b;
|
||||
}
|
||||
|
||||
&.monospace {
|
||||
font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace;
|
||||
font-family: 'SF Mono', 'Cascadia Code', Consolas, monospace;
|
||||
font-size: 13px;
|
||||
letter-spacing: 0.3px;
|
||||
}
|
||||
@@ -230,7 +232,9 @@ $teal: #00897b;
|
||||
padding: 3px 12px;
|
||||
border-radius: 12px;
|
||||
|
||||
i { font-size: 12px; }
|
||||
i {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
&--verified {
|
||||
background: #ecfdf5;
|
||||
|
||||
@@ -9,28 +9,14 @@
|
||||
<mat-card class="container">
|
||||
<!-- Header Bar -->
|
||||
<div class="table-header">
|
||||
<div class="table-title-group">
|
||||
<h2 class="table-title">
|
||||
<div class="t-logomark"><span></span><span></span><span></span><span></span></div>
|
||||
{{ 'labels.menus.Clients' | translate }}
|
||||
</h2>
|
||||
<span class="table-count" *ngIf="totalRows">{{ totalRows }} records</span>
|
||||
</div>
|
||||
|
||||
<div class="table-actions">
|
||||
<mat-form-field class="table-search" appearance="outline">
|
||||
<input
|
||||
matInput
|
||||
placeholder="{{ 'labels.text.SearchByClient' | translate }}"
|
||||
(keydown.enter)="search($event.target.value)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
@if (existsClientsToFilter) {
|
||||
<mat-checkbox #showClosedAccounts labelPosition="after" class="table-checkbox">
|
||||
{{ 'labels.inputs.Show Closed Accounts' | translate }}
|
||||
</mat-checkbox>
|
||||
}
|
||||
<div class="table-header-row">
|
||||
<div class="table-title-group">
|
||||
<h2 class="table-title">
|
||||
<div class="t-logomark"><span></span><span></span><span></span><span></span></div>
|
||||
{{ 'labels.menus.Clients' | translate }}
|
||||
</h2>
|
||||
<span class="table-count" *ngIf="totalRows">{{ totalRows }} records</span>
|
||||
</div>
|
||||
|
||||
<div class="table-buttons">
|
||||
<button
|
||||
@@ -54,6 +40,41 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-filters">
|
||||
<mat-form-field class="table-search" appearance="outline">
|
||||
<input
|
||||
matInput
|
||||
placeholder="{{ 'labels.text.SearchByClient' | translate }}"
|
||||
(keydown.enter)="search($event.target.value)"
|
||||
/>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="table-filter" appearance="outline">
|
||||
<mat-select [(value)]="statusFilter" (selectionChange)="applyClientFilters()" placeholder="Status">
|
||||
<mat-option value="">All Status</mat-option>
|
||||
<mat-option value="active">Active</mat-option>
|
||||
<mat-option value="pending">Pending</mat-option>
|
||||
<mat-option value="closed">Closed</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="table-filter" appearance="outline">
|
||||
<mat-select [(value)]="legalFormFilter" (selectionChange)="applyClientFilters()" placeholder="Legal Form">
|
||||
<mat-option value="">All Forms</mat-option>
|
||||
<mat-option value="1">Individual</mat-option>
|
||||
<mat-option value="2">Non-Individual</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="table-filter" appearance="outline">
|
||||
<mat-select [(value)]="ckycFilter" (selectionChange)="applyClientFilters()" placeholder="CKYC">
|
||||
<mat-option value="">All CKYC</mat-option>
|
||||
<mat-option value="done">CKYC Done</mat-option>
|
||||
<mat-option value="pending">CKYC Pending</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
@@ -67,8 +88,13 @@
|
||||
<ng-container matColumnDef="displayName">
|
||||
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.name' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let row" class="cell-link">
|
||||
<mat-icon class="row-legal-icon" [ngClass]="row.legalForm?.id === 2 ? 'entity' : 'person'">{{ row.legalForm?.id === 2 ? 'business' : 'person' }}</mat-icon>
|
||||
<mat-icon class="row-legal-icon" [ngClass]="row.legalForm?.id === 2 ? 'entity' : 'person'">{{
|
||||
row.legalForm?.id === 2 ? 'business' : 'person'
|
||||
}}</mat-icon>
|
||||
{{ hideClientData ? maskName(row.displayName) : row.displayName }}
|
||||
@if (row._ckycDone) {
|
||||
<mat-icon class="row-ckyc-icon" matTooltip="CKYC Done">verified</mat-icon>
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
@@ -86,6 +112,18 @@
|
||||
<td mat-cell *matCellDef="let row" class="cell-mono">{{ row.cifNumber }}</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- CKYC Number Column -->
|
||||
<ng-container matColumnDef="ckycNumber">
|
||||
<th mat-header-cell *matHeaderCellDef>CKYC Number</th>
|
||||
<td mat-cell *matCellDef="let row" class="cell-mono">
|
||||
@if (row._ckycNumber) {
|
||||
{{ row._ckycNumber }}
|
||||
} @else {
|
||||
<span class="ckyc-pending">—</span>
|
||||
}
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Legal Form Column -->
|
||||
<ng-container matColumnDef="legalForm">
|
||||
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Legal Form' | translate }}</th>
|
||||
@@ -112,7 +150,11 @@
|
||||
<td mat-cell *matCellDef="let row">
|
||||
<div class="profile-steps">
|
||||
@for (step of getProfileSteps(row); track step.label) {
|
||||
<span class="step-dot" [class.done]="step.done" [matTooltip]="step.label + (step.done ? ' ✓' : ' — Pending')">
|
||||
<span
|
||||
class="step-dot"
|
||||
[class.done]="step.done"
|
||||
[matTooltip]="step.label + (step.done ? ' ✓' : ' — Pending')"
|
||||
>
|
||||
{{ step.label.charAt(0) }}
|
||||
</span>
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@
|
||||
|
||||
/** Angular Imports. */
|
||||
import { Component, OnInit, ViewChild, inject } from '@angular/core';
|
||||
import { MatCheckbox } from '@angular/material/checkbox';
|
||||
import { MatPaginator, PageEvent } from '@angular/material/paginator';
|
||||
import { MatSort, Sort, MatSortHeader } from '@angular/material/sort';
|
||||
import {
|
||||
@@ -46,7 +45,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
styleUrls: ['./clients.component.scss'],
|
||||
imports: [
|
||||
...STANDALONE_SHARED_IMPORTS,
|
||||
MatCheckbox,
|
||||
FaIconComponent,
|
||||
MatProgressBar,
|
||||
MatTable,
|
||||
@@ -88,12 +86,11 @@ export class ClientsComponent implements OnInit {
|
||||
.join(' ');
|
||||
}
|
||||
|
||||
@ViewChild('showClosedAccounts') showClosedAccounts: MatCheckbox;
|
||||
|
||||
displayedColumns = [
|
||||
'displayName',
|
||||
'accountNumber',
|
||||
'cifNumber',
|
||||
'ckycNumber',
|
||||
'legalForm',
|
||||
'status',
|
||||
'profileCompletion',
|
||||
@@ -115,6 +112,12 @@ export class ClientsComponent implements OnInit {
|
||||
sortAttribute = '';
|
||||
sortDirection = '';
|
||||
|
||||
statusFilter = '';
|
||||
legalFormFilter = '';
|
||||
ckycFilter = '';
|
||||
|
||||
private allRows: any[] = [];
|
||||
|
||||
@ViewChild(MatPaginator) paginator: MatPaginator;
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
|
||||
@@ -139,14 +142,16 @@ export class ClientsComponent implements OnInit {
|
||||
.searchByText(this.filterText, this.currentPage, this.pageSize, this.sortAttribute, this.sortDirection)
|
||||
.subscribe(
|
||||
(data: any) => {
|
||||
this.dataSource.data = data.content;
|
||||
this.allRows = data.content || [];
|
||||
this.totalRows = data.totalElements;
|
||||
this.existsClientsToFilter = data.numberOfElements > 0;
|
||||
this.notExistsClientsToFilter = !this.existsClientsToFilter;
|
||||
this.isLoading = false;
|
||||
|
||||
this.applyClientFilters();
|
||||
|
||||
// Fetch KYC & address status for visible rows
|
||||
this.loadProfileStatus(data.content);
|
||||
this.loadProfileStatus(this.allRows);
|
||||
},
|
||||
(error: any) => {
|
||||
this.isLoading = false;
|
||||
@@ -162,35 +167,66 @@ export class ClientsComponent implements OnInit {
|
||||
|
||||
// Use individual subscriptions instead of forkJoin to avoid EMPTY observable issues
|
||||
clients.forEach((client, i) => {
|
||||
this.clientService.getClientData(client.id?.toString()).pipe(
|
||||
catchError(() => of(null))
|
||||
).subscribe((data: any) => {
|
||||
if (data?.legalForm) {
|
||||
clients[i].legalForm = data.legalForm;
|
||||
this.dataSource.data = [...clients];
|
||||
}
|
||||
});
|
||||
this.clientService
|
||||
.getClientData(client.id?.toString())
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe((data: any) => {
|
||||
if (data?.legalForm) {
|
||||
clients[i].legalForm = data.legalForm;
|
||||
this.applyClientFilters();
|
||||
}
|
||||
});
|
||||
|
||||
this.clientService.getClientKyc(client.id?.toString()).pipe(
|
||||
catchError(() => of(null))
|
||||
).subscribe((kyc: any) => {
|
||||
if (kyc) {
|
||||
clients[i]._kycDone = kyc.kycStatus === 300;
|
||||
this.dataSource.data = [...clients];
|
||||
}
|
||||
});
|
||||
this.clientService
|
||||
.getClientKyc(client.id?.toString())
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe((kyc: any) => {
|
||||
if (kyc) {
|
||||
clients[i]._kycDone = kyc.kycStatus === 300;
|
||||
clients[i]._ckycDone = !!kyc.ckycNumber;
|
||||
clients[i]._ckycNumber = kyc.ckycNumber || '';
|
||||
this.applyClientFilters();
|
||||
}
|
||||
});
|
||||
|
||||
this.clientService.getClientAddressData(client.id?.toString()).pipe(
|
||||
catchError(() => of(null))
|
||||
).subscribe((addr: any) => {
|
||||
if (addr && Array.isArray(addr) && addr.length > 0) {
|
||||
clients[i]._addressDone = true;
|
||||
this.dataSource.data = [...clients];
|
||||
}
|
||||
});
|
||||
this.clientService
|
||||
.getClientAddressData(client.id?.toString())
|
||||
.pipe(catchError(() => of(null)))
|
||||
.subscribe((addr: any) => {
|
||||
if (addr && Array.isArray(addr) && addr.length > 0) {
|
||||
clients[i]._addressDone = true;
|
||||
this.applyClientFilters();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
applyClientFilters() {
|
||||
const status = this.statusFilter;
|
||||
const legal = this.legalFormFilter;
|
||||
const ckyc = this.ckycFilter;
|
||||
|
||||
const filtered = (this.allRows || []).filter((row) => {
|
||||
const code = (row?.status?.code || '').toLowerCase();
|
||||
|
||||
if (status && !code.includes(status)) {
|
||||
return false;
|
||||
}
|
||||
if (legal && String(row?.legalForm?.id ?? '') !== legal) {
|
||||
return false;
|
||||
}
|
||||
if (ckyc === 'done' && !row._ckycDone) {
|
||||
return false;
|
||||
}
|
||||
if (ckyc === 'pending' && row._ckycDone) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
this.dataSource.data = filtered;
|
||||
}
|
||||
|
||||
pageChanged(event: PageEvent) {
|
||||
this.pageSize = event.pageSize;
|
||||
this.currentPage = event.pageIndex;
|
||||
@@ -222,7 +258,7 @@ export class ClientsComponent implements OnInit {
|
||||
}
|
||||
|
||||
getCompletedCount(row: any): number {
|
||||
return this.getProfileSteps(row).filter(s => s.done).length;
|
||||
return this.getProfileSteps(row).filter((s) => s.done).length;
|
||||
}
|
||||
|
||||
private resetPaginator() {
|
||||
|
||||
@@ -320,6 +320,94 @@ export class ClientsService {
|
||||
return this.http.get(`/clients/${clientId}/identifiers`);
|
||||
}
|
||||
|
||||
getKycProofTypes(type?: 'id' | 'address'): Observable<any> {
|
||||
const options = type ? { params: new HttpParams().set('type', type) } : {};
|
||||
return this.http.get(`/kyc/proof-types`, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 1 – Record Master: fetch clients missing CKYC registration
|
||||
* or artefacts. Backed by GET /ckyc/record-master.
|
||||
*/
|
||||
getCkycRecordMaster(
|
||||
filters: {
|
||||
officeId?: number | string;
|
||||
productId?: number | string;
|
||||
fromDate?: string;
|
||||
toDate?: string;
|
||||
missing?: 'photo' | 'signature' | 'id' | 'address' | 'any';
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
} = {}
|
||||
): Observable<any> {
|
||||
let params = new HttpParams();
|
||||
Object.entries(filters).forEach(
|
||||
([
|
||||
key,
|
||||
value
|
||||
]) => {
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
params = params.set(key, String(value));
|
||||
}
|
||||
}
|
||||
);
|
||||
return this.http.get(`/ckyc/record-master`, { params });
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 2 – Auto ZIP Creation: bundle selected clients into a CERSAI
|
||||
* ZIP on the server. Backed by POST /ckyc/batches.
|
||||
*/
|
||||
createCkycBatch(body: { clientIds: (number | string)[]; remarks?: string }): Observable<any> {
|
||||
return this.http.post(`/ckyc/batches`, body);
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 2/3 – List CKYC batches. Supports filters and an optional
|
||||
* groupByDate=true to bucket results by created_on for the UI.
|
||||
*/
|
||||
getCkycBatches(
|
||||
filters: {
|
||||
fromDate?: string;
|
||||
toDate?: string;
|
||||
uploadedToCersai?: boolean;
|
||||
groupByDate?: boolean;
|
||||
} = {}
|
||||
): Observable<any> {
|
||||
let params = new HttpParams();
|
||||
Object.entries(filters).forEach(
|
||||
([
|
||||
key,
|
||||
value
|
||||
]) => {
|
||||
if (value !== null && value !== undefined && value !== '') {
|
||||
params = params.set(key, String(value));
|
||||
}
|
||||
}
|
||||
);
|
||||
return this.http.get(`/ckyc/batches`, { params });
|
||||
}
|
||||
|
||||
/** Fetch a single CKYC batch with parsed manifest. */
|
||||
getCkycBatchById(id: number | string): Observable<any> {
|
||||
return this.http.get(`/ckyc/batches/${id}`);
|
||||
}
|
||||
|
||||
/** Download the ZIP attached to a batch (via generic document endpoint). */
|
||||
downloadCkycBatchZip(parentEntityId: number | string, documentId: number | string): Observable<Blob> {
|
||||
return this.http.get(`/ckyc_batch/${parentEntityId}/documents/${documentId}/attachment`, {
|
||||
responseType: 'blob'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Phase 3 – SFTP upload to CERSAI. Uploads the batch ZIP over the
|
||||
* SFTP channel and returns the ACK + per-client CKYC numbers.
|
||||
*/
|
||||
uploadCkycBatchToSftp(batchId: number | string): Observable<any> {
|
||||
return this.http.post(`/ckyc/batches/${batchId}/sftp-upload`, {});
|
||||
}
|
||||
|
||||
getClientIdentifierTemplate(clientId: string) {
|
||||
return this.http.get(`/clients/${clientId}/identifiers/template`);
|
||||
}
|
||||
@@ -653,6 +741,29 @@ export class ClientsService {
|
||||
return this.http.post(`/clients/${clientId}/kyc/${kycId}?command=verify-gstin`, {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies DIN (Director Identification Number) for an authorized signatory.
|
||||
* Sends signatory clientId, DIN number and the non-individual entity name.
|
||||
* Backend saves DIN & verification status in KYC table and returns company list.
|
||||
* @param clientId Signatory's Client ID
|
||||
* @param kycId KYC record ID
|
||||
* @param dinNumber DIN number to verify
|
||||
* @param entityName Non-individual entity name (for company match)
|
||||
*/
|
||||
verifyDIN(clientId: string, kycId: string, dinNumber: string, entityName: string): Observable<any> {
|
||||
return this.http.post(`/clients/${clientId}/kyc/${kycId}?command=verify-din`, { dinNumber, entityName });
|
||||
}
|
||||
|
||||
/**
|
||||
* Completes Re-KYC after uploading signed self-declaration form.
|
||||
* Updates Last Verified date to today and sets new Expiry Date (today + 10 years for low risk).
|
||||
* @param clientId Client ID
|
||||
* @param kycId KYC record ID
|
||||
*/
|
||||
completeReKyc(clientId: string, kycId: string): Observable<any> {
|
||||
return this.http.post(`/clients/${clientId}/kyc/${kycId}?command=complete-rekyc`, {});
|
||||
}
|
||||
|
||||
// ═══ Entity Classification APIs ═══
|
||||
|
||||
getEntityClassifications(clientId: string): Observable<any> {
|
||||
|
||||
@@ -12,7 +12,12 @@
|
||||
<h2>{{ 'labels.heading.Create Client' | translate }}</h2>
|
||||
</div>
|
||||
|
||||
<mat-tab-group class="client-tabs" animationDuration="200ms" [preserveContent]="true" [(selectedIndex)]="selectedTabIndex">
|
||||
<mat-tab-group
|
||||
class="client-tabs"
|
||||
animationDuration="200ms"
|
||||
[preserveContent]="true"
|
||||
[(selectedIndex)]="selectedTabIndex"
|
||||
>
|
||||
<!-- Tab 1: General Details -->
|
||||
<mat-tab>
|
||||
<ng-template mat-tab-label>
|
||||
@@ -62,7 +67,8 @@
|
||||
</div>
|
||||
}
|
||||
<div class="tab-section">
|
||||
<mifosx-client-kyc-step #clientKyc
|
||||
<mifosx-client-kyc-step
|
||||
#clientKyc
|
||||
[isNonIndividual]="legalFormType === 2"
|
||||
[clientId]="createdClientId"
|
||||
[constitutionMeta]="clientGeneralStep?.selectedConstitution"
|
||||
@@ -127,13 +133,18 @@
|
||||
<mat-icon>edit</mat-icon> Edit
|
||||
</button>
|
||||
} @else {
|
||||
<button mat-stroked-button class="tab-mode-btn tab-mode-btn--cancel" (click)="cancelTabEdit(addressTabIdx)">
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="tab-mode-btn tab-mode-btn--cancel"
|
||||
(click)="cancelTabEdit(addressTabIdx)"
|
||||
>
|
||||
<mat-icon>close</mat-icon> Cancel
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<mifosx-client-india-address-step #clientIndiaAddress
|
||||
<mifosx-client-india-address-step
|
||||
#clientIndiaAddress
|
||||
[readOnly]="isTabReadOnly(addressTabIdx)"
|
||||
></mifosx-client-india-address-step>
|
||||
</div>
|
||||
@@ -155,13 +166,18 @@
|
||||
<mat-icon>edit</mat-icon> Edit
|
||||
</button>
|
||||
} @else {
|
||||
<button mat-stroked-button class="tab-mode-btn tab-mode-btn--cancel" (click)="cancelTabEdit(familyTabIdx)">
|
||||
<button
|
||||
mat-stroked-button
|
||||
class="tab-mode-btn tab-mode-btn--cancel"
|
||||
(click)="cancelTabEdit(familyTabIdx)"
|
||||
>
|
||||
<mat-icon>close</mat-icon> Cancel
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<mifosx-client-family-members-step #clientFamilyMembers
|
||||
<mifosx-client-family-members-step
|
||||
#clientFamilyMembers
|
||||
[clientTemplate]="clientTemplate"
|
||||
[clientId]="createdClientId"
|
||||
[maritalStatusId]="clientGeneralStep?.createClientForm?.get('maritalStatusId')?.value"
|
||||
@@ -220,9 +236,19 @@
|
||||
}
|
||||
@if (selectedTabIndex < totalTabs - 1) {
|
||||
<div class="save-next-wrapper">
|
||||
<button mat-raised-button color="primary" class="save-next-btn" (click)="onNext()" [disabled]="savingClient || (selectedTabIndex === 0 && clientGeneralStep?.sanctionsBlocked)">
|
||||
<button
|
||||
mat-raised-button
|
||||
color="primary"
|
||||
class="save-next-btn"
|
||||
(click)="onNext()"
|
||||
[disabled]="savingClient || (selectedTabIndex === 0 && clientGeneralStep?.sanctionsBlocked)"
|
||||
>
|
||||
@if (savingClient) {
|
||||
<mat-progress-spinner diameter="18" mode="indeterminate" class="m-r-10 save-spinner"></mat-progress-spinner>
|
||||
<mat-progress-spinner
|
||||
diameter="18"
|
||||
mode="indeterminate"
|
||||
class="m-r-10 save-spinner"
|
||||
></mat-progress-spinner>
|
||||
Saving...
|
||||
} @else {
|
||||
<mat-icon class="m-r-6">save</mat-icon>
|
||||
|
||||
@@ -48,11 +48,11 @@
|
||||
.client-tabs {
|
||||
background: var(--md-sys-color-surface, #fff);
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
box-shadow: 0 2px 8px rgb(0 0 0 / 8%);
|
||||
|
||||
::ng-deep {
|
||||
.mat-mdc-tab-header {
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-bottom: 1px solid rgb(0 0 0 / 8%);
|
||||
}
|
||||
|
||||
.mat-mdc-tab {
|
||||
@@ -96,11 +96,11 @@
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 32px 16px;
|
||||
color: rgba(0, 0, 0, 0.54);
|
||||
color: rgb(0 0 0 / 54%);
|
||||
font-size: 14px;
|
||||
|
||||
mat-icon {
|
||||
color: rgba(0, 0, 0, 0.38);
|
||||
color: rgb(0 0 0 / 38%);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
align-items: center;
|
||||
margin-top: 20px;
|
||||
padding: 16px 0;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
border-top: 1px solid rgb(0 0 0 / 8%);
|
||||
}
|
||||
|
||||
.action-bar-right {
|
||||
@@ -119,7 +119,7 @@
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
color: rgb(0 0 0 / 60%);
|
||||
}
|
||||
|
||||
/* ─── Read-only tab state ─── */
|
||||
@@ -196,8 +196,9 @@
|
||||
::ng-deep .mdc-linear-progress__bar-inner {
|
||||
border-color: #fff !important;
|
||||
}
|
||||
|
||||
::ng-deep .mdc-linear-progress__buffer-bar {
|
||||
background-color: rgba(255, 255, 255, 0.3) !important;
|
||||
background-color: rgb(255 255 255 / 30%) !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -222,7 +223,9 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
span { flex: 1; }
|
||||
span {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
button {
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -236,10 +236,14 @@ export class CreateClientComponent {
|
||||
}
|
||||
// Upload photo & signature
|
||||
if (this.clientKycStep?.photographFile) {
|
||||
this.clientsService.uploadClientProfileImage(clientId, this.clientKycStep.photographFile).subscribe({ error: () => {} });
|
||||
this.clientsService
|
||||
.uploadClientProfileImage(clientId, this.clientKycStep.photographFile)
|
||||
.subscribe({ error: () => {} });
|
||||
}
|
||||
if (this.clientKycStep?.signatureFile) {
|
||||
this.clientsService.uploadClientSignatureImage(clientId, this.clientKycStep.signatureFile).subscribe({ error: () => {} });
|
||||
this.clientsService
|
||||
.uploadClientSignatureImage(clientId, this.clientKycStep.signatureFile)
|
||||
.subscribe({ error: () => {} });
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -368,16 +372,27 @@ export class CreateClientComponent {
|
||||
* Finalize: upload photo/signature, save KYC, save addresses, then navigate.
|
||||
*/
|
||||
private finalizeClient(clientId: string) {
|
||||
const navigateToClient = () => this.router.navigate(['../', clientId], { relativeTo: this.route });
|
||||
const navigateToClient = () =>
|
||||
this.router.navigate(
|
||||
[
|
||||
'../',
|
||||
clientId
|
||||
],
|
||||
{ relativeTo: this.route }
|
||||
);
|
||||
|
||||
// Upload photo if selected
|
||||
if (this.clientKycStep?.photographFile) {
|
||||
this.clientsService.uploadClientProfileImage(clientId, this.clientKycStep.photographFile).subscribe({ error: () => {} });
|
||||
this.clientsService
|
||||
.uploadClientProfileImage(clientId, this.clientKycStep.photographFile)
|
||||
.subscribe({ error: () => {} });
|
||||
}
|
||||
|
||||
// Upload signature if selected
|
||||
if (this.clientKycStep?.signatureFile) {
|
||||
this.clientsService.uploadClientSignatureImage(clientId, this.clientKycStep.signatureFile).subscribe({ error: () => {} });
|
||||
this.clientsService
|
||||
.uploadClientSignatureImage(clientId, this.clientKycStep.signatureFile)
|
||||
.subscribe({ error: () => {} });
|
||||
}
|
||||
|
||||
// Save addresses from India address step
|
||||
@@ -409,16 +424,36 @@ export class CreateClientComponent {
|
||||
const addressData = this.clientIndiaAddressStep.addressData;
|
||||
// Map tab names to Fineract address type names (as defined in address template)
|
||||
const typeNameMap: Record<string, string[]> = {
|
||||
permanent: ['Permanent', 'Permanent Address'],
|
||||
current: ['Residential', 'Current', 'Current / Residential', 'Communication'],
|
||||
office: ['Office', 'Office / Work', 'Business', 'Work'],
|
||||
correspondence: ['Correspondence', 'Mailing', 'Postal']
|
||||
permanent: [
|
||||
'Permanent',
|
||||
'Permanent Address'
|
||||
],
|
||||
current: [
|
||||
'Residential',
|
||||
'Current',
|
||||
'Current / Residential',
|
||||
'Communication'
|
||||
],
|
||||
office: [
|
||||
'Office',
|
||||
'Office / Work',
|
||||
'Business',
|
||||
'Work'
|
||||
],
|
||||
correspondence: [
|
||||
'Correspondence',
|
||||
'Mailing',
|
||||
'Postal'
|
||||
]
|
||||
};
|
||||
|
||||
this.clientsService.getClientAddressTemplate().subscribe({
|
||||
next: (template: any) => {
|
||||
const typeOptions = template?.addressTypeIdOptions || [];
|
||||
for (const [tab, addr] of Object.entries(addressData)) {
|
||||
for (const [
|
||||
tab,
|
||||
addr
|
||||
] of Object.entries(addressData)) {
|
||||
if (!addr || !(addr as any).addressLine1) continue;
|
||||
|
||||
// Find matching address type ID
|
||||
@@ -448,8 +483,8 @@ export class CreateClientComponent {
|
||||
|
||||
// Try to resolve state to stateProvinceId
|
||||
if ((addr as any).state && template?.stateProvinceIdOptions?.length) {
|
||||
const stateMatch = template.stateProvinceIdOptions.find((s: any) =>
|
||||
s.name?.toLowerCase() === (addr as any).state?.toLowerCase()
|
||||
const stateMatch = template.stateProvinceIdOptions.find(
|
||||
(s: any) => s.name?.toLowerCase() === (addr as any).state?.toLowerCase()
|
||||
);
|
||||
if (stateMatch) payload.stateProvinceId = stateMatch.id;
|
||||
}
|
||||
@@ -535,34 +570,98 @@ export class CreateClientComponent {
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
styles: [`
|
||||
.scd { background: #fff; border-radius: 12px; overflow: hidden; }
|
||||
.scd-header {
|
||||
display: flex; align-items: center; gap: 14px;
|
||||
padding: 20px 24px 16px;
|
||||
}
|
||||
.scd-header__icon-wrap {
|
||||
width: 44px; height: 44px; border-radius: 12px;
|
||||
background: #eff6ff; display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.scd-header__icon { color: #1074b9; font-size: 24px; width: 24px; height: 24px; }
|
||||
.scd-header__title { margin: 0; font-size: 17px; font-weight: 700; color: #0f172a; }
|
||||
.scd-header__sub { margin: 2px 0 0; font-size: 12px; color: #64748b; }
|
||||
.scd-body { padding: 0 24px 20px; }
|
||||
.scd-body p { margin: 0 0 14px; font-size: 14px; color: #374151; line-height: 1.5; }
|
||||
.scd-info {
|
||||
display: flex; align-items: flex-start; gap: 8px;
|
||||
padding: 10px 14px; background: #f0f9ff; border: 1px solid #bae6fd;
|
||||
border-radius: 8px; font-size: 12px; color: #0c4a6e;
|
||||
}
|
||||
.scd-info__icon { font-size: 16px !important; width: 16px !important; height: 16px !important; color: #0284c7; flex-shrink: 0; margin-top: 1px; }
|
||||
.scd-actions {
|
||||
display: flex; justify-content: flex-end; gap: 10px;
|
||||
padding: 14px 24px; border-top: 1px solid #f1f5f9; background: #f8fafc;
|
||||
}
|
||||
.scd-cancel, .scd-confirm { display: flex !important; align-items: center; gap: 6px; font-size: 13px !important; height: 38px !important; border-radius: 8px !important; }
|
||||
.scd-confirm { font-weight: 600 !important; }
|
||||
`],
|
||||
imports: [MatIcon, MatDialogClose]
|
||||
styles: [
|
||||
`
|
||||
.scd {
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.scd-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 20px 24px 16px;
|
||||
}
|
||||
.scd-header__icon-wrap {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
border-radius: 12px;
|
||||
background: #eff6ff;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.scd-header__icon {
|
||||
color: #1074b9;
|
||||
font-size: 24px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
}
|
||||
.scd-header__title {
|
||||
margin: 0;
|
||||
font-size: 17px;
|
||||
font-weight: 700;
|
||||
color: #0f172a;
|
||||
}
|
||||
.scd-header__sub {
|
||||
margin: 2px 0 0;
|
||||
font-size: 12px;
|
||||
color: #64748b;
|
||||
}
|
||||
.scd-body {
|
||||
padding: 0 24px 20px;
|
||||
}
|
||||
.scd-body p {
|
||||
margin: 0 0 14px;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.scd-info {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 8px;
|
||||
padding: 10px 14px;
|
||||
background: #f0f9ff;
|
||||
border: 1px solid #bae6fd;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: #0c4a6e;
|
||||
}
|
||||
.scd-info__icon {
|
||||
font-size: 16px !important;
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
color: #0284c7;
|
||||
flex-shrink: 0;
|
||||
margin-top: 1px;
|
||||
}
|
||||
.scd-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 14px 24px;
|
||||
border-top: 1px solid #f1f5f9;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.scd-cancel,
|
||||
.scd-confirm {
|
||||
display: flex !important;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 13px !important;
|
||||
height: 38px !important;
|
||||
border-radius: 8px !important;
|
||||
}
|
||||
.scd-confirm {
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
`
|
||||
],
|
||||
imports: [
|
||||
MatIcon,
|
||||
MatDialogClose
|
||||
]
|
||||
})
|
||||
export class SaveConfirmDialogComponent {}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user