diff --git a/.ai/mcp/mcp.json b/.ai/mcp/mcp.json new file mode 100644 index 000000000..e69de29bb diff --git a/.qwen/settings.json b/.qwen/settings.json new file mode 100644 index 000000000..d2edcaf7c --- /dev/null +++ b/.qwen/settings.json @@ -0,0 +1,17 @@ +{ + "permissions": { + "allow": [ + "Bash(npx *)", + "Bash(git add *)", + "Bash(git commit *)", + "Bash(select-object *)", + "Bash(git config *)", + "Bash(set *)", + "Bash(git push)", + "Bash(xcopy *)", + "Bash(powershell *)", + "mcp__idea__build_project" + ] + }, + "$version": 3 +} diff --git a/.qwen/settings.json.orig b/.qwen/settings.json.orig new file mode 100644 index 000000000..0399b79c8 --- /dev/null +++ b/.qwen/settings.json.orig @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Bash(npx *)" + ] + } +} \ No newline at end of file diff --git a/backend-schema/README.md b/backend-schema/README.md index 009b04974..f11c42c92 100644 --- a/backend-schema/README.md +++ b/backend-schema/README.md @@ -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 | diff --git a/postman/CBS_Client_Creation_API.postman_collection.json b/postman/CBS_Client_Creation_API.postman_collection.json index a2bcf7480..2cab16946 100644 --- a/postman/CBS_Client_Creation_API.postman_collection.json +++ b/postman/CBS_Client_Creation_API.postman_collection.json @@ -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." } diff --git a/src/app/accounting/accounting.component.html b/src/app/accounting/accounting.component.html index bb83b8e09..eeb29f912 100644 --- a/src/app/accounting/accounting.component.html +++ b/src/app/accounting/accounting.component.html @@ -18,7 +18,12 @@
- +
{{ 'labels.heading.Frequent Postings' | translate }} @@ -27,11 +32,18 @@ - +
{{ 'labels.heading.Create Journal Entries' | translate }} - {{ 'labels.text.Manual journal entry transactions recorded in a journal' | translate }} + {{ + 'labels.text.Manual journal entry transactions recorded in a journal' | translate + }}
@@ -45,20 +57,34 @@ - +
{{ 'labels.heading.Accounts Linked to Financial Activities' | translate }} - {{ 'labels.text.List of Financial Activities and GL Account Mappings' | translate }} + {{ + 'labels.text.List of Financial Activities and GL Account Mappings' | translate + }}
- +
{{ 'labels.heading.Migrate Opening Balances (Office-wise)' | translate }} - {{ 'labels.text.Set or update office-level opening balances for GL accounts' | translate }} + {{ + 'labels.text.Set or update office-level opening balances for GL accounts' | translate + }}
@@ -76,7 +102,9 @@
{{ 'labels.heading.Closing Entries' | translate }} - {{ 'labels.text.Journal entries made at the end of accounting period' | translate }} + {{ + 'labels.text.Journal entries made at the end of accounting period' | translate + }}
@@ -114,9 +142,15 @@

{{ 'labels.heading.Chart of Accounts' | translate }}

{{ 'labels.text.The Chart of Accounts - General Ledger Setup' | translate }}

- - - + + +
@@ -124,9 +158,15 @@

{{ 'labels.heading.Accounts Linked to Financial Activities' | translate }}

{{ 'labels.text.Accounts linked to Financial Activities' | translate }}

- - - + + +
@@ -134,9 +174,15 @@

{{ 'labels.heading.Migrate Opening Balances (Office-wise)' | translate }}

{{ 'labels.text.GLAccount Balances Application' | translate }}

- - - + + +
@@ -144,9 +190,15 @@

{{ 'labels.heading.Closing Entries' | translate }}

{{ 'labels.text.Accounting to journal entries' | translate }}

- - - + + +
@@ -154,8 +206,14 @@

{{ 'labels.heading.Create Journal Entries' | translate }}

{{ 'labels.text.Journal Entries Transactions' | translate }}

- - - + + +
diff --git a/src/app/accounting/accounting.component.scss b/src/app/accounting/accounting.component.scss index b86bfc60f..2e153a32e 100644 --- a/src/app/accounting/accounting.component.scss +++ b/src/app/accounting/accounting.component.scss @@ -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 (width <= 768px) { grid-template-columns: 1fr; } + @media (width <= 768px) { + grid-template-columns: 1fr; + } } .acc-card { @@ -72,7 +85,9 @@ $accent: #0ea5e9; 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 rgb(0 0 0 / 8%); @@ -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 { @@ -128,7 +166,15 @@ $accent: #0ea5e9; 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; + } } diff --git a/src/app/bank-calendar/add-holiday-dialog/add-holiday-dialog.component.html b/src/app/bank-calendar/add-holiday-dialog/add-holiday-dialog.component.html index 688645f25..e98d6bc35 100644 --- a/src/app/bank-calendar/add-holiday-dialog/add-holiday-dialog.component.html +++ b/src/app/bank-calendar/add-holiday-dialog/add-holiday-dialog.component.html @@ -41,7 +41,14 @@
From Date - + @if (holidayForm.controls.fromDate.hasError('required')) { @@ -51,7 +58,14 @@ To Date - + @if (holidayForm.controls.toDate.hasError('required')) { @@ -90,7 +104,14 @@
Repayments Rescheduled To - + diff --git a/src/app/bank-calendar/add-holiday-dialog/add-holiday-dialog.component.ts b/src/app/bank-calendar/add-holiday-dialog/add-holiday-dialog.component.ts index 1ebc9251d..e10d3d65d 100644 --- a/src/app/bank-calendar/add-holiday-dialog/add-holiday-dialog.component.ts +++ b/src/app/bank-calendar/add-holiday-dialog/add-holiday-dialog.component.ts @@ -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(); diff --git a/src/app/bank-calendar/bank-calendar.component.html b/src/app/bank-calendar/bank-calendar.component.html index d8768ec66..b7f263ac6 100644 --- a/src/app/bank-calendar/bank-calendar.component.html +++ b/src/app/bank-calendar/bank-calendar.component.html @@ -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 : '' }}
diff --git a/src/app/bank-calendar/bank-calendar.component.scss b/src/app/bank-calendar/bank-calendar.component.scss index 62f55ee0f..a5ba73b92 100644 --- a/src/app/bank-calendar/bank-calendar.component.scss +++ b/src/app/bank-calendar/bank-calendar.component.scss @@ -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%); } } diff --git a/src/app/bank-calendar/bank-calendar.component.ts b/src/app/bank-calendar/bank-calendar.component.ts index 92c7d09c8..754f956ab 100644 --- a/src/app/bank-calendar/bank-calendar.component.ts +++ b/src/app/bank-calendar/bank-calendar.component.ts @@ -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 => `${h.sno}${h.date}${h.day}${h.occasion}`).join(''); + list.map((h) => `${h.sno}${h.date}${h.day}${h.occasion}`).join(''); const html = ` diff --git a/src/app/centers/centers.component.html b/src/app/centers/centers.component.html index c41c52ce4..e12dbbb4f 100644 --- a/src/app/centers/centers.component.html +++ b/src/app/centers/centers.component.html @@ -25,16 +25,32 @@
- + {{ 'labels.inputs.Show Closed Centers' | translate }}
- - @@ -75,7 +91,12 @@ - + @if (activeSection === 'zip') { -
-
-
- -
-

ZIP Batch Details

-

CERSAI ZIP archives grouped by creation date

-
-
- Phase 2 -
- - @if (loading) { -
- - Loading batches… -
- } - - @if (error && !loading) { -
- - {{ error }} -
- } - - @if (!loading && !error && groups.length === 0) { -
- -

No batches found

-

Create a batch from the Record Master to see it here.

-
- } - - @if (!loading && groups.length > 0) { -
- @for (group of groups; track group.dateLabel) { -
- - - @if (group.expanded) { -
- - - - - - - - - - - - - @for (batch of group.batches; track trackBatch($index, batch)) { - - - - - - - - - } - -
Batch IDFilenameRecordsSizeStatusActions
- - {{ batch.batchId }} - - - {{ batch.filename }} - {{ batch.recordCount }}{{ formatBytes(batch.sizeBytes) }} - @if (batch.uploadedToCersai) { - - Uploaded - - } @else { - - Pending - - } - - - @if (batch.documentId) { - - } -
-
- } +
+
+
+ +
+

ZIP Batch Details

+

CERSAI ZIP archives grouped by creation date

- } +
+ Phase 2
- } -
+ @if (loading) { +
+ + Loading batches… +
+ } + + @if (error && !loading) { +
+ + {{ error }} +
+ } + + @if (!loading && !error && groups.length === 0) { +
+ +

No batches found

+

Create a batch from the Record Master to see it here.

+
+ } + + @if (!loading && groups.length > 0) { +
+ @for (group of groups; track group.dateLabel) { +
+ + + @if (group.expanded) { +
+ + + + + + + + + + + + + @for (batch of group.batches; track trackBatch($index, batch)) { + + + + + + + + + } + +
Batch IDFilenameRecordsSizeStatusActions
+ + {{ batch.batchId }} + + + {{ batch.filename }} + {{ batch.recordCount }}{{ formatBytes(batch.sizeBytes) }} + @if (batch.uploadedToCersai) { + + Uploaded + + } @else { + + Pending + + } + + + @if (batch.documentId) { + + } +
+
+ } +
+ } +
+ } +
} @if (activeSection === 'sftp') { -
-
-
- -
-

SFTP Upload & ACK · API Match

-

CERSAI SFTP transfer history with ACK receipts and per-batch logs

+
+
+
+ +
+

SFTP Upload & ACK · API Match

+

CERSAI SFTP transfer history with ACK receipts and per-batch logs

+
+
+ Phase 3-4 +
+ +
+
+
+
+
{{ sftpSuccessCount }}
+
Uploaded successfully
+
+
+
+
+
+
{{ sftpFailedCount }}
+
Failed
+
+
+
+
+
+
{{ sftpPendingCount }}
+
Pending upload
+
- Phase 3-4 -
-
-
-
-
-
{{ sftpSuccessCount }}
-
Uploaded successfully
+ @if (loading) { +
+ + Loading logs…
-
-
-
-
-
{{ sftpFailedCount }}
-
Failed
+ } + + @if (!loading && sftpLogs.length === 0) { +
+ +

No SFTP upload history yet

+

Upload a batch from the Record Master to see CERSAI transfer logs here.

-
-
-
-
-
{{ sftpPendingCount }}
-
Pending upload
-
-
-
+ } - @if (loading) { -
- - Loading logs… -
- } - - @if (!loading && sftpLogs.length === 0) { -
- -

No SFTP upload history yet

-

Upload a batch from the Record Master to see CERSAI transfer logs here.

-
- } - - @if (!loading && sftpLogs.length > 0) { -
- @for (log of sftpLogs; track log.id) { -
-
- @if (isSftpSuccess(log)) { - - } @else if (isSftpFailed(log)) { - - } @else { - - } -
-
-
- - {{ log.batchId }} - + @if (!loading && sftpLogs.length > 0) { +
+ @for (log of sftpLogs; track log.id) { +
+
@if (isSftpSuccess(log)) { - - Upload successful - + } @else if (isSftpFailed(log)) { - - Upload failed - + } @else { - - Pending - + } - - - {{ log.uploadedOnLabel || log.createdOnLabel || '—' }} -
+
+
+ {{ log.batchId }} + @if (isSftpSuccess(log)) { + + Upload successful + + } @else if (isSftpFailed(log)) { + Upload failed + } @else { + Pending + } + + + {{ log.uploadedOnLabel || log.createdOnLabel || '—' }} + +
-
-
- Filename - {{ log.filename }} -
-
- Records - {{ log.recordCount }} -
-
- Size - {{ formatBytes(log.sizeBytes) }} -
- @if (log.cersaiReference) { +
- CERSAI Ref - {{ log.cersaiReference }} + Filename + {{ log.filename }} +
+
+ Records + {{ log.recordCount }} +
+
+ Size + {{ formatBytes(log.sizeBytes) }} +
+ @if (log.cersaiReference) { +
+ CERSAI Ref + {{ log.cersaiReference }} +
+ } +
+ + @if (log.uploadRemarks) { +
+ + {{ log.uploadRemarks }}
}
- @if (log.uploadRemarks) { -
- - {{ log.uploadRemarks }} -
- } -
- -
- - @if (log.documentId) { - - } + @if (log.documentId) { + + } +
-
- } -
- } -
+ } +
+ } +
} @@ -451,13 +444,9 @@
Status @if (manifestBatch.uploadedToCersai) { - - Uploaded - + Uploaded } @else { - - Pending - + Pending }
@@ -526,7 +515,10 @@ 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 : '')" + [title]=" + (entry.idProof?.type || 'ID') + + (entry.idProof?.zipEntry ? ' — ' + entry.idProof.zipEntry : '') + " > @@ -536,13 +528,23 @@ 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 : '')" + [title]=" + (entry.addressProof?.type || 'Address') + + (entry.addressProof?.zipEntry ? ' — ' + entry.addressProof.zipEntry : '') + " > - {{ formatBytes((entry.photo?.bytes || 0) + (entry.signature?.bytes || 0) + (entry.idProof?.bytes || 0) + (entry.addressProof?.bytes || 0)) }} + {{ + formatBytes( + (entry.photo?.bytes || 0) + + (entry.signature?.bytes || 0) + + (entry.idProof?.bytes || 0) + + (entry.addressProof?.bytes || 0) + ) + }} } @@ -554,9 +556,7 @@
-
- Open -
+
Open
@@ -124,9 +118,7 @@ MIS
-
- Open -
+
Open
@@ -142,9 +134,7 @@ Aadhaar
-
- Open -
+
Open
diff --git a/src/app/ckyc/ckyc.component.scss b/src/app/ckyc/ckyc.component.scss index 3ade750b6..d4c221397 100644 --- a/src/app/ckyc/ckyc.component.scss +++ b/src/app/ckyc/ckyc.component.scss @@ -45,7 +45,7 @@ $purple-bg: #f8f5ff; margin-bottom: 22px; position: relative; overflow: hidden; - box-shadow: 0 6px 24px rgba(26, 115, 232, 0.2); + box-shadow: 0 6px 24px rgb(26 115 232 / 20%); &::before { content: ''; @@ -55,7 +55,7 @@ $purple-bg: #f8f5ff; width: 320px; height: 320px; border-radius: 50%; - background: radial-gradient(circle, rgba(255, 255, 255, 0.15), transparent 70%); + background: radial-gradient(circle, rgb(255 255 255 / 15%), transparent 70%); pointer-events: none; } @@ -71,8 +71,8 @@ $purple-bg: #f8f5ff; width: 58px; height: 58px; border-radius: 14px; - background: rgba(255, 255, 255, 0.18); - border: 1px solid rgba(255, 255, 255, 0.3); + background: rgb(255 255 255 / 18%); + border: 1px solid rgb(255 255 255 / 30%); display: flex; align-items: center; justify-content: center; @@ -86,8 +86,8 @@ $purple-bg: #f8f5ff; align-items: center; gap: 8px; padding: 8px 16px; - background: rgba(255, 255, 255, 0.15); - border: 1px solid rgba(255, 255, 255, 0.3); + background: rgb(255 255 255 / 15%); + border: 1px solid rgb(255 255 255 / 30%); border-radius: 30px; font-size: 12px; font-weight: 600; @@ -95,7 +95,10 @@ $purple-bg: #f8f5ff; flex-shrink: 0; position: relative; - fa-icon { color: #9fe1cb; font-size: 14px; } + fa-icon { + color: #9fe1cb; + font-size: 14px; + } } } @@ -139,7 +142,9 @@ $purple-bg: #f8f5ff; align-items: center; gap: 8px; - fa-icon { color: $primary; } + fa-icon { + color: $primary; + } } &__hint { @@ -155,8 +160,14 @@ $purple-bg: #f8f5ff; overflow-x: auto; padding: 4px 2px; - &::-webkit-scrollbar { height: 4px; } - &::-webkit-scrollbar-thumb { background: $gray-200; border-radius: 4px; } + &::-webkit-scrollbar { + height: 4px; + } + + &::-webkit-scrollbar-thumb { + background: $gray-200; + border-radius: 4px; + } } } @@ -171,7 +182,9 @@ $purple-bg: #f8f5ff; position: relative; transition: transform 0.15s; - &:hover { transform: translateY(-2px); } + &:hover { + transform: translateY(-2px); + } &__num { position: absolute; @@ -249,7 +262,9 @@ $purple-bg: #f8f5ff; background: $amber; } - .ckyc-phase__title { color: #633806; } + .ckyc-phase__title { + color: #633806; + } } &--wide { @@ -270,7 +285,10 @@ $purple-bg: #f8f5ff; text-transform: uppercase; letter-spacing: 0.4px; - fa-icon { color: $primary; font-size: 14px; } + fa-icon { + color: $primary; + font-size: 14px; + } } /* ---------- Menu grid ---------- */ @@ -289,7 +307,10 @@ $purple-bg: #f8f5ff; border-radius: 16px; text-decoration: none; color: $gray-900; - transition: transform 0.2s, box-shadow 0.2s, border-color 0.2s; + transition: + transform 0.2s, + box-shadow 0.2s, + border-color 0.2s; cursor: pointer; position: relative; overflow: hidden; @@ -304,12 +325,14 @@ $purple-bg: #f8f5ff; border-radius: 50%; opacity: 0.12; pointer-events: none; - transition: transform 0.4s, opacity 0.3s; + transition: + transform 0.4s, + opacity 0.3s; } &:hover { transform: translateY(-3px); - box-shadow: 0 10px 30px rgba(0, 0, 0, 0.08); + box-shadow: 0 10px 30px rgb(0 0 0 / 8%); .ckyc-card__glow { transform: scale(1.25); @@ -320,7 +343,9 @@ $purple-bg: #f8f5ff; background: $primary; color: $white; - fa-icon { transform: translateX(3px); } + fa-icon { + transform: translateX(3px); + } } } @@ -336,7 +361,7 @@ $purple-bg: #f8f5ff; margin-bottom: 16px; position: relative; z-index: 1; - box-shadow: 0 6px 18px rgba(0, 0, 0, 0.1); + box-shadow: 0 6px 18px rgb(0 0 0 / 10%); } &__body { @@ -394,9 +419,11 @@ $purple-bg: #f8f5ff; .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; @@ -407,9 +434,11 @@ $purple-bg: #f8f5ff; .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; @@ -420,9 +449,11 @@ $purple-bg: #f8f5ff; .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; @@ -433,15 +464,16 @@ $purple-bg: #f8f5ff; .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 { @@ -458,12 +490,26 @@ $purple-bg: #f8f5ff; /* ---------- Responsive ---------- */ @media (width <= 900px) { - .ckyc-dash { padding: 16px 18px 30px; } - .ckyc-dash__hero { flex-direction: column; align-items: flex-start; } - .ckyc-pipeline__track { flex-direction: row; flex-wrap: nowrap; } + .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; } + .ckyc-dash__grid { + grid-template-columns: 1fr; + } + + .ckyc-phase { + min-width: 120px; + } } diff --git a/src/app/ckyc/record-master/record-master.component.html b/src/app/ckyc/record-master/record-master.component.html index c37e72f77..e38c5bd8d 100644 --- a/src/app/ckyc/record-master/record-master.component.html +++ b/src/app/ckyc/record-master/record-master.component.html @@ -28,12 +28,7 @@
-
-
- +
@if (slot.files?.length) { @@ -475,9 +498,7 @@
-
- {{ phase.label }} -
+
{{ phase.label }}
@if (phase.message) {
{{ phase.message }}
} @@ -677,7 +696,10 @@ 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 : '')" + [title]=" + (entry.idProof?.type || 'Identity Proof') + + (entry.idProof?.zipEntry ? ' — ' + entry.idProof.zipEntry : '') + " > @@ -687,13 +709,23 @@ 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 : '')" + [title]=" + (entry.addressProof?.type || 'Address Proof') + + (entry.addressProof?.zipEntry ? ' — ' + entry.addressProof.zipEntry : '') + " > - {{ formatBytes((entry.photo?.bytes || 0) + (entry.signature?.bytes || 0) + (entry.idProof?.bytes || 0) + (entry.addressProof?.bytes || 0)) }} + {{ + formatBytes( + (entry.photo?.bytes || 0) + + (entry.signature?.bytes || 0) + + (entry.idProof?.bytes || 0) + + (entry.addressProof?.bytes || 0) + ) + }} } @@ -718,14 +750,14 @@
Financial Thresholds (as per Udyam Portal)
- Annual Turnover — Last FY (₹ Cr) - - Investment — Plant & Machinery / Equipment (₹ Cr) - - Financial Year of Last Assessment + Annual Turnover — Last FY (₹ Cr) + + Investment — Plant & Machinery / Equipment (₹ Cr) + + Financial Year of Last Assessment 2025-26 2024-25 @@ -91,48 +129,72 @@ 2022-23 2021-22 2020-21 - +
Owner / Promoter Details
- Social Category of Owner + Social Category of Owner General Scheduled Caste (SC) Scheduled Tribe (ST) Other Backward Class (OBC) Minority - - Gender of Primary Owner + + Gender of Primary Owner Male Female Other - - No. of Employees (approx.) - + + No. of Employees (approx.) +
-
Women-led EnterpriseMajority ownership / management by women entrepreneur
+
+ Women-led EnterpriseMajority ownership / management by women entrepreneur +
-
Divyang (PWD) OwnedPersons with Disability — eligible for special scheme benefits
+
+ Divyang (PWD) OwnedPersons with Disability — eligible for special scheme benefits +
- MSMED Act 2006 (Revised): Classification based on composite criteria of investment in plant & machinery AND annual turnover. Udyam Registration is mandatory for availing PSL benefits, government procurement preference, and CGSS coverage. + MSMED Act 2006 (Revised): Classification based on composite criteria of investment in plant + & machinery AND annual turnover. Udyam Registration is mandatory for availing PSL benefits, government + procurement preference, and CGSS coverage.
-
@@ -143,14 +205,18 @@
- DPIIT + DPIIT
Startup India Registered
DPIIT recognition under Startup India initiative
- + expand_more
@@ -158,21 +224,36 @@
Registration Details
- DPIIT Recognition Number - - Recognition Date + DPIIT Recognition Number + + Recognition Date - - Certificate Expiry Date + + Certificate Expiry Date - - Entity Stage + + Entity Stage Early (0–2 years) Growth (2–5 years) Scale (5+ years) - - Sector as per DPIIT + + Sector as per DPIIT Fintech Agritech @@ -183,9 +264,12 @@ SaaS / B2B Tech Information Technology Other - - NIC Industry Code - + + NIC Industry Code +
Government Scheme Benefits Availed
@@ -200,25 +284,39 @@
PSL Linkage
- Loan Limit for PSL Eligibility (Cr) - - PSL Sub-category + Loan Limit for PSL Eligibility (Cr) + + PSL Sub-category Others (Startup India) MSME — Micro MSME — Small - +
- RBI Circular RBI/2023-24/62: DPIIT-recognized startups eligible for PSL classification up to ₹50 Cr. Recognition certificate to be retained in document management system. + RBI Circular RBI/2023-24/62: DPIIT-recognized startups eligible for PSL classification up to + ₹50 Cr. Recognition certificate to be retained in document management system.
-
@@ -229,14 +327,18 @@
- PMMY + PMMY
Mudra Borrower
Pradhan Mantri Mudra Yojana — SIDBI / Mudra scheme borrower
- + expand_more
@@ -244,22 +346,34 @@
Loan Classification
- Mudra Category + Mudra Category Shishu (up to ₹50,000) Kishore (₹50,001 – ₹5,00,000) Tarun (₹5,00,001 – ₹10,00,000) - - Mudra Loan Account No. - - Lending Institution Name - - Sanctioned Amount (₹) - - Disbursement Date + + Mudra Loan Account No. + + Lending Institution Name + + Sanctioned Amount (₹) + + Disbursement Date - - Loan Purpose + + Loan Purpose Manufacturing Trading @@ -267,37 +381,57 @@ Agriculture Allied Activity Transport Working Capital - +
Scheme Details
- MUDRA Card Number - - Current Outstanding (₹) - - Repayment Status + MUDRA Card Number + + Current Outstanding (₹) + + Repayment Status Regular Overdue (1–30 days) Overdue (31–90 days) NPA - +
-
NPA FlagMark if account classified as Non-Performing Asset
+
+ NPA FlagMark if account classified as Non-Performing Asset +
- PSL Tagging: Mudra loans must be tagged at both entity and account level for FIDD PSL returns. NPA flag mandatory for RBI quarterly reporting. + PSL Tagging: Mudra loans must be tagged at both entity and account level for FIDD PSL + returns. NPA flag mandatory for RBI quarterly reporting.
-
@@ -308,14 +442,18 @@
- EOU + EOU
Export Oriented Unit
Customs-registered EOU under DGFT / Ministry of Commerce
- + expand_more
@@ -323,9 +461,12 @@
EOU Registration
- EOU Registration Number - - Registered DC Zone + EOU Registration Number + + Registered DC Zone Mumbai (SEEPZ) Delhi (NSEZ) @@ -335,49 +476,89 @@ Hyderabad (VSEZ) Cochin (CSEZ) Other - - Bond / LUT Number - - Validity From + + Bond / LUT Number + + Validity From - - Validity To + + Validity To - - GSTIN under EOU Scheme - + + GSTIN under EOU Scheme +
Export Obligation & Compliance
- Annual Export Obligation (₹ Cr) - - NFE Target (₹ Cr) - - SoftEx Filing Status + Annual Export Obligation (₹ Cr) + + NFE Target (₹ Cr) + SoftEx Filing Status - CurrentPendingNot Applicable - - BRC Submission Status + CurrentPendingNot Applicable + + BRC Submission Status - CurrentPendingNot Applicable - - AD Category Bank Code - - FEMA Declaration Date + CurrentPendingNot Applicable + + AD Category Bank Code + + FEMA Declaration Date - +
- FEMA / RBI: EOU accounts must be tagged for FEMA compliance on forex receipts. AD Category-I bank declaration mandatory. EDPMS reporting required for all export proceeds. + FEMA / RBI: EOU accounts must be tagged for FEMA compliance on forex receipts. AD Category-I + bank declaration mandatory. EDPMS reporting required for all export proceeds.
-
@@ -388,14 +569,18 @@
- DGFT / IEC + DGFT / IEC
Import Export Business
Entity with valid DGFT Importer Exporter Code
- + expand_more
@@ -403,61 +588,106 @@
IEC Registration
- IEC Code (10-digit) - - IEC Issue Date + IEC Code (10-digit) + + IEC Issue Date - - IEC Status + + IEC Status ActiveSuspended - CancelledDeactivated - - Trade Type + CancelledDeactivated + + Trade Type Export OnlyImport Only Both Import & Export - - Primary HS Code (Commodity) - - Key Trading Countries - + + Primary HS Code (Commodity) + + Key Trading Countries +
Banking & Compliance
- AD Category Bank Code - - ECGC Policy Number - - ECGC Policy Expiry + AD Category Bank Code + + ECGC Policy Number + + ECGC Policy Expiry - +
-
EEFC Account RequiredExchange Earners Foreign Currency account
+
+ EEFC Account RequiredExchange Earners Foreign Currency account +
-
PCFC Facility RequiredPre-shipment Credit in Foreign Currency
+
+ PCFC Facility RequiredPre-shipment Credit in Foreign Currency +
-
FEMA Declaration ObtainedForm A2 / LRS declaration on file
+
+ FEMA Declaration ObtainedForm A2 / LRS declaration on file +
- RBI / FEMA: IEC mandatory before any import/export transaction. AD bank must report all export proceeds via EDPMS and import payments via IDPMS. ECGC cover recommended for high-risk countries. + RBI / FEMA: IEC mandatory before any import/export transaction. AD bank must report all + export proceeds via EDPMS and import payments via IDPMS. ECGC cover recommended for high-risk countries.
-
@@ -468,14 +698,18 @@
- RBI PSL + RBI PSL
Priority Sector Borrower
Eligible under RBI Priority Sector Lending guidelines
- + expand_more
@@ -483,7 +717,8 @@
PSL Classification
- PSL Category + PSL Category Agriculture MSME @@ -494,8 +729,10 @@ Export Credit Weaker Section Others - - PSL Sub-category + + PSL Sub-category Direct Agriculture — Crop Loan Direct Agriculture — Investment @@ -509,68 +746,115 @@ Weaker Section — Minorities Export Credit Others — Startup India - - PSL Loan Purpose Code - - PSL Eligibility Basis - - FIDD Reporting Code - - PSL Sanctioned Limit (₹) - + + PSL Loan Purpose Code + + PSL Eligibility Basis + + FIDD Reporting Code + + PSL Sanctioned Limit (₹) +
@if (pslForm.get('pslCategory')?.value === 'MICRO_ENTERPRISE') {
MSME Sub-classification
- MSME Classification + MSME Classification Micro (T/O ≤ ₹5 Cr, Inv ≤ ₹1 Cr) Small (T/O ≤ ₹50 Cr, Inv ≤ ₹10 Cr) Medium (T/O ≤ ₹250 Cr, Inv ≤ ₹50 Cr) - - Udyam Registration Number - - Udyam Registration Date + + Udyam Registration Number + + Udyam Registration Date - - Annual Turnover (₹ Cr) - - Investment — Plant & Machinery (₹ Cr) - + + Annual Turnover (₹ Cr) + + Investment — Plant & Machinery (₹ Cr) +
}
Special Category Flags
-
Weaker Section FlagSHG, JLG, SC/ST, OBC, Minorities, PWD borrowers
+
+ Weaker Section FlagSHG, JLG, SC/ST, OBC, Minorities, PWD borrowers +
-
SC / ST Borrower FlagScheduled Caste or Scheduled Tribe classification
+
+ SC / ST Borrower FlagScheduled Caste or Scheduled Tribe classification +
-
Women Entrepreneur FlagMajority-owned / managed by women
+
+ Women Entrepreneur FlagMajority-owned / managed by women +
-
Minority Community FlagAs defined by Ministry of Minority Affairs
+
+ Minority Community FlagAs defined by Ministry of Minority Affairs +
- RBI FIDD Circular: PSL tagging mandatory at entity AND loan account level. MSME classification must be verified from Udyam portal — self-declaration not acceptable. Weaker section loans reported separately in FIDD quarterly returns. + RBI FIDD Circular: PSL tagging mandatory at entity AND loan account level. MSME + classification must be verified from Udyam portal — self-declaration not acceptable. Weaker section loans + reported separately in FIDD quarterly returns.
-
@@ -581,14 +865,20 @@
- General + General
Non-Priority Sector
-
Standard corporate / retail borrower — no special priority sector classification
+
+ Standard corporate / retail borrower — no special priority sector classification +
- + expand_more
@@ -596,7 +886,8 @@
Sector Classification
- Industry Sector + Industry Sector Large Corporate Mid-Market Corporate @@ -607,62 +898,95 @@ Trust / NGO / Society Real Estate Other - - NIC Code (5-digit) - - Industry Description - - Annual Revenue Band + + NIC Code (5-digit) + + Industry Description + + Annual Revenue Band Below ₹1 Cr ₹1 Cr – ₹10 Cr ₹10 Cr – ₹100 Cr ₹100 Cr – ₹500 Cr Above ₹500 Cr - +
Risk Classification
- Risk Category + Risk Category Low Risk Medium Risk High Risk - - Customer Risk Score - - AML Risk Classification + + Customer Risk Score + + AML Risk Classification Standard Due Diligence (SDD) Customer Due Diligence (CDD) Enhanced Due Diligence (EDD) - +
-
PEP Flag — Politically Exposed PersonRequires mandatory EDD as per RBI KYC Master Circular
+
+ PEP Flag — Politically Exposed PersonRequires mandatory EDD as per RBI KYC Master Circular +
-
AML Watchlist MatchUN Sanctions / OFAC / EU / MHA match detected
+
+ AML Watchlist MatchUN Sanctions / OFAC / EU / MHA match detected +
-
Adverse Media FlagNegative news screening result — requires review
+
+ Adverse Media FlagNegative news screening result — requires review +
- RBI KYC Master Circular 2023: PEP flag triggers mandatory EDD regardless of transaction size. AML watchlist match must be escalated to MLRO within 24 hours. Risk score reviewed annually for High Risk customers. + RBI KYC Master Circular 2023: PEP flag triggers mandatory EDD regardless of transaction size. + AML watchlist match must be escalated to MLRO within 24 hours. Risk score reviewed annually for High Risk + customers.
-
diff --git a/src/app/clients/client-stepper/client-entity-kyc-step/client-entity-kyc-step.component.scss b/src/app/clients/client-stepper/client-entity-kyc-step/client-entity-kyc-step.component.scss index 174abaa2c..36e7a013d 100644 --- a/src/app/clients/client-stepper/client-entity-kyc-step/client-entity-kyc-step.component.scss +++ b/src/app/clients/client-stepper/client-entity-kyc-step/client-entity-kyc-step.component.scss @@ -1,18 +1,48 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + /** * Entity Classification — Scheme & Sector Classification */ -.ec-page { padding: 0; } +.ec-page { + padding: 0; +} .step-header { background: #0f172a; padding: 16px 24px; border-radius: 10px 10px 0 0; margin-bottom: 20px; - &__left { display: flex; align-items: center; gap: 12px; } - &__icon { font-size: 18px; color: #60a5fa; } - &__title { font-size: 16px; font-weight: 600; color: #fff; margin: 0; } - &__sub { font-size: 11px; color: rgb(255 255 255 / 50%); margin: 2px 0 0; } + + &__left { + display: flex; + align-items: center; + gap: 12px; + } + + &__icon { + font-size: 18px; + color: #60a5fa; + } + + &__title { + font-size: 16px; + font-weight: 600; + color: #fff; + margin: 0; + } + + &__sub { + font-size: 11px; + color: rgb(255 255 255 / 50%); + margin: 2px 0 0; + } } .ec-chips-label { @@ -32,7 +62,11 @@ min-height: 28px; } -.ec-no-chip { font-size: 12px; color: #94a3b8; padding: 4px 0; } +.ec-no-chip { + font-size: 12px; + color: #94a3b8; + padding: 4px 0; +} .ec-chip { padding: 4px 10px; @@ -50,7 +84,10 @@ height: 14px !important; cursor: pointer; opacity: 0.6; - &:hover { opacity: 1; } + + &:hover { + opacity: 1; + } } .ec-card { @@ -60,7 +97,10 @@ margin-bottom: 12px; overflow: hidden; transition: border-color 0.2s; - &--open { border-color: #93c5fd; } + + &--open { + border-color: #93c5fd; + } } .ec-card-head { @@ -71,10 +111,17 @@ cursor: pointer; user-select: none; transition: background 0.15s; - &:hover { background: #f8fafc; } + + &:hover { + background: #f8fafc; + } } -.ec-card-head-left { display: flex; align-items: center; gap: 12px; } +.ec-card-head-left { + display: flex; + align-items: center; + gap: 12px; +} .ec-tag { padding: 3px 10px; @@ -85,14 +132,31 @@ white-space: nowrap; } -.ec-card-title { font-size: 14px; font-weight: 600; color: #1e293b; } -.ec-card-desc { font-size: 12px; color: #64748b; margin-top: 2px; } -.ec-card-toggle { display: flex; align-items: center; gap: 10px; } +.ec-card-title { + font-size: 14px; + font-weight: 600; + color: #1e293b; +} + +.ec-card-desc { + font-size: 12px; + color: #64748b; + margin-top: 2px; +} + +.ec-card-toggle { + display: flex; + align-items: center; + gap: 10px; +} .ec-chevron { transition: transform 0.25s; color: #94a3b8; - &.open { transform: rotate(180deg); } + + &.open { + transform: rotate(180deg); + } } .ec-card-body { @@ -105,7 +169,10 @@ grid-template-columns: repeat(3, 1fr); gap: 4px 16px; margin-bottom: 8px; - mat-form-field { width: 100%; } + + mat-form-field { + width: 100%; + } } .ec-grid-2 { @@ -113,7 +180,10 @@ grid-template-columns: repeat(2, 1fr); gap: 4px 16px; margin-bottom: 8px; - mat-form-field { width: 100%; } + + mat-form-field { + width: 100%; + } } .ec-sub-title { @@ -125,7 +195,12 @@ margin: 16px 0 12px; padding-top: 12px; border-top: 1px dashed #e2e8f0; - &:first-child { margin-top: 0; padding-top: 0; border-top: none; } + + &:first-child { + margin-top: 0; + padding-top: 0; + border-top: none; + } } .ec-check-grid { @@ -139,8 +214,14 @@ border: 1px solid #e2e8f0; border-radius: 6px; font-size: 12px; - transition: background 0.12s, border-color 0.12s; - &:hover { background: #f8fafc; border-color: #93c5fd; } + transition: + background 0.12s, + border-color 0.12s; + + &:hover { + background: #f8fafc; + border-color: #93c5fd; + } } } @@ -161,8 +242,19 @@ border-radius: 6px; } -.ec-flag-label { font-size: 13px; font-weight: 500; color: #1e293b; display: block; } -.ec-flag-sub { font-size: 11px; color: #94a3b8; display: block; margin-top: 1px; } +.ec-flag-label { + font-size: 13px; + font-weight: 500; + color: #1e293b; + display: block; +} + +.ec-flag-sub { + font-size: 11px; + color: #94a3b8; + display: block; + margin-top: 1px; +} .ec-reg-note { margin-top: 16px; @@ -173,7 +265,10 @@ font-size: 12px; color: #0c4a6e; line-height: 1.6; - strong { font-weight: 600; } + + strong { + font-weight: 600; + } } .ec-card-actions { @@ -182,7 +277,12 @@ margin-top: 16px; padding-top: 12px; border-top: 1px solid #f1f5f9; - button { display: flex; align-items: center; gap: 6px; } + + button { + display: flex; + align-items: center; + gap: 6px; + } } .ec-info-banner { @@ -198,11 +298,27 @@ margin-top: 12px; } -@media (width <= 900px) { .ec-grid-3 { grid-template-columns: repeat(2, 1fr); } } +@media (width <= 900px) { + .ec-grid-3 { + grid-template-columns: repeat(2, 1fr); + } +} @media (width <= 600px) { - .ec-grid-3, .ec-grid-2 { grid-template-columns: 1fr; } - .ec-check-grid { grid-template-columns: 1fr; } - .ec-card-head { padding: 12px 14px; } - .ec-card-body { padding: 14px; } + .ec-grid-3, + .ec-grid-2 { + grid-template-columns: 1fr; + } + + .ec-check-grid { + grid-template-columns: 1fr; + } + + .ec-card-head { + padding: 12px 14px; + } + + .ec-card-body { + padding: 14px; + } } diff --git a/src/app/clients/client-stepper/client-entity-kyc-step/client-entity-kyc-step.component.ts b/src/app/clients/client-stepper/client-entity-kyc-step/client-entity-kyc-step.component.ts index fb75cf2be..b3e6b9afc 100644 --- a/src/app/clients/client-stepper/client-entity-kyc-step/client-entity-kyc-step.component.ts +++ b/src/app/clients/client-stepper/client-entity-kyc-step/client-entity-kyc-step.component.ts @@ -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; + } } diff --git a/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.html b/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.html index 9053104fc..f21112a45 100644 --- a/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.html +++ b/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.html @@ -1,3 +1,11 @@ + + @@ -10,7 +18,6 @@
- @if (isViewMode) { @@ -18,20 +25,31 @@
@if (linkedProfileImage) { - Profile + Profile } @else { person }
{{ selectedClient.displayName }} - Client ID: {{ selectedClient.id }}{{ selectedClient.accountNumber ? ' · A/C: ' + selectedClient.accountNumber : '' }} + Client ID: {{ selectedClient.id + }}{{ selectedClient.accountNumber ? ' · A/C: ' + selectedClient.accountNumber : '' }}
@if (linkedSignatureImage) {
- Signature + Signature Signature
} @@ -57,7 +75,11 @@
Date of Birth - {{ familyMemberForm.get('dateOfBirth')?.value ? (familyMemberForm.get('dateOfBirth')?.value | date:'dd MMM yyyy') : '—' }} + {{ + familyMemberForm.get('dateOfBirth')?.value + ? (familyMemberForm.get('dateOfBirth')?.value | date: 'dd MMM yyyy') + : '—' + }}
Nationality @@ -89,19 +111,25 @@ Status {{ familyMemberForm.get('isAlive')?.value !== false ? 'Alive' : 'Deceased' }} - @if (familyMemberForm.get('isDependent')?.value) { · Dependent } + @if (familyMemberForm.get('isDependent')?.value) { + · Dependent + }
@if (isSpouse && familyMemberForm.get('marriageDate')?.value) {
Marriage Date - {{ familyMemberForm.get('marriageDate')?.value | date:'dd MMM yyyy' }} + {{ + familyMemberForm.get('marriageDate')?.value | date: 'dd MMM yyyy' + }}
} @if (isSpouse && familyMemberForm.get('marriageAuthority')?.value) {
Issuing Authority - {{ getMarriageAuthorityLabel(familyMemberForm.get('marriageAuthority')?.value) }} + {{ + getMarriageAuthorityLabel(familyMemberForm.get('marriageAuthority')?.value) + }}
}
@@ -136,7 +164,9 @@ @if (searching) {
-
Searching...
+
+ Searching... +
} @if (!searching && searchResults.length > 0) { @@ -146,7 +176,18 @@ person
{{ r.displayName }} - ID: {{ r.id }} · {{ r.officeName }}@if (r.dateOfBirth) { · DOB: {{ r.dateOfBirth | dateFormat }} }@if (r.fatherSpouseName) { · {{ r.fatherSpouseName }} }@if (r.mobileNo) { · {{ r.mobileNo }} } + ID: {{ r.id }} · {{ r.officeName }} + @if (r.dateOfBirth) { + · DOB: {{ r.dateOfBirth | dateFormat }} + } + @if (r.fatherSpouseName) { + · {{ r.fatherSpouseName }} + } + @if (r.mobileNo) { + · {{ r.mobileNo }} + } +
} @@ -162,24 +203,37 @@
@if (linkedProfileImage) { - Profile + Profile } @else { person }
{{ selectedClient.displayName }} - Client ID: {{ selectedClient.id }}{{ selectedClient.accountNumber ? ' · A/C: ' + selectedClient.accountNumber : '' }} + Client ID: {{ selectedClient.id + }}{{ selectedClient.accountNumber ? ' · A/C: ' + selectedClient.accountNumber : '' }}
@if (linkedSignatureImage) {
- Signature + Signature Signature
} - +
@if (fetchingLinkedData) { @@ -198,7 +252,11 @@
Date of Birth - {{ familyMemberForm.get('dateOfBirth')?.value ? (familyMemberForm.get('dateOfBirth')?.value | date:'dd MMM yyyy') : '—' }} + {{ + familyMemberForm.get('dateOfBirth')?.value + ? (familyMemberForm.get('dateOfBirth')?.value | date: 'dd MMM yyyy') + : '—' + }}
Aadhaar @@ -292,8 +350,13 @@ - + PDF, JPG or PNG (max 5 MB) } @else {
@@ -313,15 +376,32 @@
Aadhaar Number - + PAN Number - - @if (familyMemberForm.get('panNumber')?.hasError('pattern') && familyMemberForm.get('panNumber')?.touched) { + + @if ( + familyMemberForm.get('panNumber')?.hasError('pattern') && familyMemberForm.get('panNumber')?.touched + ) { Invalid PAN format } @@ -348,9 +428,13 @@ } @else { - } diff --git a/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.scss b/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.scss index 2494bef6a..90aa50ae5 100644 --- a/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.scss +++ b/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.scss @@ -1,3 +1,11 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + .fm-dialog__header { display: flex; align-items: center; @@ -9,7 +17,9 @@ font-weight: 600; color: #1e293b; - .fm-dialog__icon { color: #1074b9; } + .fm-dialog__icon { + color: #1074b9; + } } .fm-mode-toggle { @@ -32,7 +42,11 @@ cursor: pointer; transition: all 0.15s; - mat-icon { font-size: 18px; width: 18px; height: 18px; } + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + } &.active { border-color: #1074b9; @@ -40,11 +54,19 @@ color: #1074b9; } - &:hover:not(.active) { border-color: #94a3b8; } + &:hover:not(.active) { + border-color: #94a3b8; + } } -.fm-search-wrapper { position: relative; margin-bottom: 12px; } -.fm-search-field { width: 100%; } +.fm-search-wrapper { + position: relative; + margin-bottom: 12px; +} + +.fm-search-field { + width: 100%; +} .fm-search-results { position: absolute; @@ -92,13 +114,34 @@ cursor: pointer; border-bottom: 1px solid #f1f5f9; - &:last-child { border-bottom: none; } - &:hover { background: #f0f7ff; } + &:last-child { + border-bottom: none; + } - &__icon { color: #1074b9; font-size: 20px; } - &__info { display: flex; flex-direction: column; } - &__name { font-size: 13px; font-weight: 600; color: #1e293b; } - &__meta { font-size: 11px; color: #64748b; } + &:hover { + background: #f0f7ff; + } + + &__icon { + color: #1074b9; + font-size: 20px; + } + + &__info { + display: flex; + flex-direction: column; + } + + &__name { + font-size: 13px; + font-weight: 600; + color: #1e293b; + } + + &__meta { + font-size: 11px; + color: #64748b; + } } .fm-linked-card { @@ -111,8 +154,13 @@ border-radius: 8px; margin-bottom: 16px; - &__left { flex-shrink: 0; } - &__icon { color: #1074b9; } + &__left { + flex-shrink: 0; + } + + &__icon { + color: #1074b9; + } &__photo { width: 40px; @@ -121,9 +169,24 @@ object-fit: cover; border: 2px solid #e2e8f0; } - &__info { flex: 1; display: flex; flex-direction: column; } - &__name { font-size: 14px; font-weight: 600; color: #1e293b; } - &__meta { font-size: 12px; color: #64748b; font-family: 'Roboto Mono', monospace; } + + &__info { + flex: 1; + display: flex; + flex-direction: column; + } + + &__name { + font-size: 14px; + font-weight: 600; + color: #1e293b; + } + + &__meta { + font-size: 12px; + color: #64748b; + font-family: 'Roboto Mono', monospace; + } &__sig { display: flex; @@ -149,7 +212,14 @@ letter-spacing: 0.04em; } } - &__remove { color: #94a3b8 !important; &:hover { color: #ef4444 !important; } } + + &__remove { + color: #94a3b8 !important; + + &:hover { + color: #ef4444 !important; + } + } } .fm-fetching-hint { @@ -204,8 +274,16 @@ padding: 10px 16px; margin-bottom: 8px; - .fm-readonly-label { display: block; margin-bottom: 4px; } - .fm-readonly-value { font-size: 13px; color: #475569; line-height: 1.5; } + .fm-readonly-label { + display: block; + margin-bottom: 4px; + } + + .fm-readonly-value { + font-size: 13px; + color: #475569; + line-height: 1.5; + } } .fm-kyc-badge { @@ -266,7 +344,9 @@ .zoomable { cursor: zoom-in; - transition: transform 0.15s ease, box-shadow 0.15s ease; + transition: + transform 0.15s ease, + box-shadow 0.15s ease; &:hover { transform: scale(1.08); @@ -275,7 +355,11 @@ } ::ng-deep .image-zoom-panel { - .mat-mdc-dialog-container { padding: 0 !important; border-radius: 10px !important; overflow: hidden; } + .mat-mdc-dialog-container { + padding: 0 !important; + border-radius: 10px !important; + overflow: hidden; + } } .fm-file-upload { @@ -294,7 +378,11 @@ color: #1074b9; border-color: #cbd5e1; - mat-icon { font-size: 18px; width: 18px; height: 18px; } + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + } } .fm-file-hint { @@ -312,7 +400,10 @@ border-radius: 6px; width: 100%; - &__icon { color: #1074b9; font-size: 20px; } + &__icon { + color: #1074b9; + font-size: 20px; + } &__name { flex: 1; @@ -330,7 +421,14 @@ height: 28px !important; line-height: 28px !important; - mat-icon { font-size: 16px; width: 16px; height: 16px; } - &:hover { color: #ef4444 !important; } + mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + } + + &:hover { + color: #ef4444 !important; + } } } diff --git a/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.ts b/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.ts index 883a3f56e..bc326ad04 100644 --- a/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.ts +++ b/src/app/clients/client-stepper/client-family-members-step/client-family-member-dialog/client-family-member-dialog.component.ts @@ -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 {
`, - 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); data = inject(MAT_DIALOG_DATA); - close() { this.dialogRef.close(); } + close() { + this.dialogRef.close(); + } } diff --git a/src/app/clients/client-stepper/client-family-members-step/client-family-members-step.component.html b/src/app/clients/client-stepper/client-family-members-step/client-family-members-step.component.html index 9251015f7..aab5f1fb6 100644 --- a/src/app/clients/client-stepper/client-family-members-step/client-family-members-step.component.html +++ b/src/app/clients/client-stepper/client-family-members-step/client-family-members-step.component.html @@ -1,3 +1,11 @@ + + @@ -75,9 +83,13 @@ {{ member.dateOfBirth ? (member.dateOfBirth | dateFormat) : '—' }} - @if (member.panNumber) { {{ member.panNumber }} } - @else if (member.aadhaarNumber) { {{ member.aadhaarNumber }} } - @else { — } + @if (member.panNumber) { + {{ member.panNumber }} + } @else if (member.aadhaarNumber) { + {{ member.aadhaarNumber }} + } @else { + — + } @if (member.isAlive !== false) { @@ -87,14 +99,32 @@ } - @if (!readOnly) { - - } @@ -103,5 +133,4 @@ }
} -
diff --git a/src/app/clients/client-stepper/client-family-members-step/client-family-members-step.component.scss b/src/app/clients/client-stepper/client-family-members-step/client-family-members-step.component.scss index cac5743b4..0bdf7fb98 100644 --- a/src/app/clients/client-stepper/client-family-members-step/client-family-members-step.component.scss +++ b/src/app/clients/client-stepper/client-family-members-step/client-family-members-step.component.scss @@ -1,11 +1,29 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + .zoomable { cursor: zoom-in; - transition: transform 0.15s ease, box-shadow 0.15s ease; - &:hover { transform: scale(1.08); box-shadow: 0 4px 16px rgb(0 0 0 / 18%); } + transition: + transform 0.15s ease, + box-shadow 0.15s ease; + + &:hover { + transform: scale(1.08); + box-shadow: 0 4px 16px rgb(0 0 0 / 18%); + } } ::ng-deep .image-zoom-panel { - .mat-mdc-dialog-container { padding: 0 !important; border-radius: 10px !important; overflow: hidden; } + .mat-mdc-dialog-container { + padding: 0 !important; + border-radius: 10px !important; + overflow: hidden; + } } .search-no-results-hint { @@ -63,7 +81,10 @@ color: #1e293b; } -.fm-step__icon { color: #1074b9; font-size: 14px; } +.fm-step__icon { + color: #1074b9; + font-size: 14px; +} .fm-step__count { background: #1074b9; @@ -170,8 +191,13 @@ border-bottom: 1px solid #f1f5f9; transition: background 0.1s; - &:last-child { border-bottom: none; } - &:hover { background: #f8fafc; } + &:last-child { + border-bottom: none; + } + + &:hover { + background: #f8fafc; + } } .fm-table__col { @@ -186,11 +212,31 @@ gap: 8px; min-width: 0; } - &--rel { flex: 0 0 130px; } - &--dob { flex: 0 0 120px; color: #64748b; font-size: 12px; } - &--id { flex: 0 0 140px; } - &--status { flex: 0 0 60px; text-align: center; } - &--actions { flex: 0 0 80px; display: flex; gap: 2px; } + + &--rel { + flex: 0 0 130px; + } + + &--dob { + flex: 0 0 120px; + color: #64748b; + font-size: 12px; + } + + &--id { + flex: 0 0 140px; + } + + &--status { + flex: 0 0 60px; + text-align: center; + } + + &--actions { + flex: 0 0 80px; + display: flex; + gap: 2px; + } } .fm-table__avatar { @@ -232,8 +278,15 @@ padding: 2px 8px; border-radius: 10px; - &--yes { background: #ecfdf5; color: #059669; } - &--no { background: #fef2f2; color: #dc2626; } + &--yes { + background: #ecfdf5; + color: #059669; + } + + &--no { + background: #fef2f2; + color: #dc2626; + } } .fm-action-btn { @@ -248,8 +301,13 @@ color: #64748b; } - &:hover mat-icon { color: #1074b9; } - &--delete:hover mat-icon { color: #ef4444; } + &:hover mat-icon { + color: #1074b9; + } + + &--delete:hover mat-icon { + color: #ef4444; + } } @media (width <= 768px) { @@ -260,7 +318,10 @@ } .fm-table__col { - &--type, &--id { display: none; } + &--type, + &--id { + display: none; + } } } @@ -284,11 +345,27 @@ gap: 10px; } -.nom-header__icon { font-size: 16px; color: #1565c0; } -.nom-header__title { font-size: 16px; font-weight: 600; color: #1e293b; display: block; } -.nom-header__sub { font-size: 11px; color: #94a3b8; display: block; } +.nom-header__icon { + font-size: 16px; + color: #1565c0; +} -.nom-add-btn { font-size: 13px; } +.nom-header__title { + font-size: 16px; + font-weight: 600; + color: #1e293b; + display: block; +} + +.nom-header__sub { + font-size: 11px; + color: #94a3b8; + display: block; +} + +.nom-add-btn { + font-size: 13px; +} /* Share bar */ .nom-share-bar { @@ -302,7 +379,11 @@ border-radius: 6px; } -.nom-share-bar__label { font-size: 12px; color: #64748b; white-space: nowrap; } +.nom-share-bar__label { + font-size: 12px; + color: #64748b; + white-space: nowrap; +} .nom-share-bar__track { flex: 1; @@ -317,8 +398,14 @@ background: #1565c0; border-radius: 4px; transition: width 0.3s; - &.complete { background: #059669; } - &.over { background: #dc2626; } + + &.complete { + background: #059669; + } + + &.over { + background: #dc2626; + } } .nom-share-bar__value { @@ -327,7 +414,10 @@ color: #1565c0; min-width: 36px; text-align: right; - &.over { color: #dc2626; } + + &.over { + color: #dc2626; + } } /* Nominee card */ @@ -346,24 +436,62 @@ } .nom-card__avatar { - width: 36px; height: 36px; + width: 36px; + height: 36px; border-radius: 50%; background: #dbeafe; - display: flex; align-items: center; justify-content: center; + display: flex; + align-items: center; + justify-content: center; color: #1565c0; - mat-icon { font-size: 20px; } + + mat-icon { + font-size: 20px; + } } -.nom-card__photo { width: 100%; height: 100%; object-fit: cover; border-radius: 50%; } -.nom-card__info { flex: 1; } -.nom-card__name { font-weight: 600; font-size: 14px; color: #1e293b; display: block; } -.nom-card__meta { font-size: 12px; color: #64748b; display: block; margin-top: 2px; } +.nom-card__photo { + width: 100%; + height: 100%; + object-fit: cover; + border-radius: 50%; +} + +.nom-card__info { + flex: 1; +} + +.nom-card__name { + font-weight: 600; + font-size: 14px; + color: #1e293b; + display: block; +} + +.nom-card__meta { + font-size: 12px; + color: #64748b; + display: block; + margin-top: 2px; +} .nom-card__sig { flex-shrink: 0; - img { height: 24px; max-width: 60px; object-fit: contain; border: 1px solid #e2e8f0; border-radius: 4px; padding: 2px; } + + img { + height: 24px; + max-width: 60px; + object-fit: contain; + border: 1px solid #e2e8f0; + border-radius: 4px; + padding: 2px; + } +} + +.nom-card__actions { + display: flex; + gap: 2px; } -.nom-card__actions { display: flex; gap: 2px; } .nom-card__edit { padding: 12px 16px; @@ -392,8 +520,18 @@ min-width: 50px; } -.nom-card__share-value { font-size: 18px; font-weight: 700; color: #1565c0; display: block; } -.nom-card__share-label { font-size: 10px; color: #94a3b8; text-transform: uppercase; } +.nom-card__share-value { + font-size: 18px; + font-weight: 700; + color: #1565c0; + display: block; +} + +.nom-card__share-label { + font-size: 10px; + color: #94a3b8; + text-transform: uppercase; +} .nom-card__guardian { display: flex; @@ -404,7 +542,13 @@ border-top: 1px solid #e0f2fe; font-size: 12px; color: #0c4a6e; - mat-icon { font-size: 16px; width: 16px; height: 16px; color: #0ea5e9; } + + mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + color: #0ea5e9; + } } /* Empty */ @@ -421,8 +565,16 @@ font-size: 14px; } -.nom-empty__icon { font-size: 36px; width: 36px; height: 36px; color: #cbd5e1; } -.nom-empty__sub { font-size: 12px; } +.nom-empty__icon { + font-size: 36px; + width: 36px; + height: 36px; + color: #cbd5e1; +} + +.nom-empty__sub { + font-size: 12px; +} /* Nominee form */ .nom-form { @@ -444,20 +596,30 @@ } /* Search */ -.nom-search-wrapper { position: relative; } -.nom-search-field { width: 100%; } +.nom-search-wrapper { + position: relative; +} + +.nom-search-field { + width: 100%; +} .nom-search-prefix { display: inline-flex; align-items: center; padding-right: 8px; - mat-icon { font-size: 20px; color: #94a3b8; } + + mat-icon { + font-size: 20px; + color: #94a3b8; + } } .nom-search-results { position: absolute; top: 100%; - left: 0; right: 0; + left: 0; + right: 0; z-index: 10; background: #fff; border: 1px solid #e2e8f0; @@ -469,9 +631,12 @@ } .nom-search-loading { - display: flex; align-items: center; gap: 8px; + display: flex; + align-items: center; + gap: 8px; padding: 12px 16px; - font-size: 13px; color: #64748b; + font-size: 13px; + color: #64748b; } .nom-search-item { @@ -480,14 +645,43 @@ gap: 10px; padding: 10px 16px; cursor: pointer; - &:hover { background: #f1f5f9; } + + &:hover { + background: #f1f5f9; + } } -.nom-search-item__icon { color: #94a3b8; font-size: 20px; } -.nom-search-item__info { flex: 1; } -.nom-search-item__name { font-weight: 500; font-size: 13px; color: #1e293b; display: block; } -.nom-search-item__meta { font-size: 11px; color: #94a3b8; display: block; } -.nom-search-item__status { font-size: 11px; font-weight: 600; color: #94a3b8; &.active { color: #22c55e; } } +.nom-search-item__icon { + color: #94a3b8; + font-size: 20px; +} + +.nom-search-item__info { + flex: 1; +} + +.nom-search-item__name { + font-weight: 500; + font-size: 13px; + color: #1e293b; + display: block; +} + +.nom-search-item__meta { + font-size: 11px; + color: #94a3b8; + display: block; +} + +.nom-search-item__status { + font-size: 11px; + font-weight: 600; + color: #94a3b8; + + &.active { + color: #22c55e; + } +} /* Selected nominee */ .nom-selected { @@ -507,31 +701,92 @@ } .nom-selected__avatar { - width: 40px; height: 40px; + width: 40px; + height: 40px; border-radius: 50%; background: #dbeafe; - display: flex; align-items: center; justify-content: center; + display: flex; + align-items: center; + justify-content: center; color: #1565c0; overflow: hidden; } -.nom-selected__photo { width: 100%; height: 100%; object-fit: cover; } -.nom-selected__info { flex: 1; } -.nom-selected__name { font-weight: 600; font-size: 14px; color: #1e293b; display: block; } -.nom-selected__meta { font-size: 12px; color: #64748b; display: block; } +.nom-selected__photo { + width: 100%; + height: 100%; + object-fit: cover; +} + +.nom-selected__info { + flex: 1; +} + +.nom-selected__name { + font-weight: 600; + font-size: 14px; + color: #1e293b; + display: block; +} + +.nom-selected__meta { + font-size: 12px; + color: #64748b; + display: block; +} .nom-selected__sig { - display: flex; flex-direction: column; align-items: center; gap: 2px; flex-shrink: 0; - img { height: 28px; max-width: 70px; object-fit: contain; border: 1px solid #e2e8f0; border-radius: 4px; background: #fff; padding: 2px 4px; } - span { font-size: 9px; color: #94a3b8; text-transform: uppercase; letter-spacing: 0.04em; } + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + flex-shrink: 0; + + img { + height: 28px; + max-width: 70px; + object-fit: contain; + border: 1px solid #e2e8f0; + border-radius: 4px; + background: #fff; + padding: 2px 4px; + } + + span { + font-size: 9px; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.04em; + } +} + +.nom-selected__remove { + color: #94a3b8; +} + +.nom-selected__loading { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + font-size: 12px; + color: #64748b; } -.nom-selected__remove { color: #94a3b8; } -.nom-selected__loading { display: flex; align-items: center; gap: 8px; padding: 12px 16px; font-size: 12px; color: #64748b; } .nom-kyc-badge { - display: inline-flex; align-items: center; margin-left: 4px; - mat-icon { font-size: 14px; width: 14px; height: 14px; } - &.verified { color: #16a34a; } + display: inline-flex; + align-items: center; + margin-left: 4px; + + mat-icon { + font-size: 14px; + width: 14px; + height: 14px; + } + + &.verified { + color: #16a34a; + } } .nom-selected__details { @@ -542,10 +797,29 @@ flex-wrap: wrap; } -.nom-detail { display: flex; flex-direction: column; gap: 2px; } -.nom-detail__label { font-size: 10px; text-transform: uppercase; color: #94a3b8; letter-spacing: 0.5px; } -.nom-detail__value { font-size: 13px; font-weight: 500; color: #1e293b; } -.nom-detail__value.mono { font-family: 'Roboto Mono', monospace; font-size: 12px; } +.nom-detail { + display: flex; + flex-direction: column; + gap: 2px; +} + +.nom-detail__label { + font-size: 10px; + text-transform: uppercase; + color: #94a3b8; + letter-spacing: 0.5px; +} + +.nom-detail__value { + font-size: 13px; + font-weight: 500; + color: #1e293b; +} + +.nom-detail__value.mono { + font-family: 'Roboto Mono', monospace; + font-size: 12px; +} /* Form fields */ .nom-form-fields { @@ -556,8 +830,13 @@ align-items: center; } -.nom-form-field { width: 100%; } -.nom-minor-toggle { padding-top: 8px; } +.nom-form-field { + width: 100%; +} + +.nom-minor-toggle { + padding-top: 8px; +} /* Guardian */ .nom-guardian-section { @@ -573,7 +852,11 @@ font-weight: 600; color: #0c4a6e; margin-bottom: 10px; - mat-icon { font-size: 18px; color: #0ea5e9; } + + mat-icon { + font-size: 18px; + color: #0ea5e9; + } } .nom-guardian-card { @@ -584,13 +867,32 @@ background: #f0f9ff; border: 1px solid #bae6fd; border-radius: 8px; - mat-icon { color: #0ea5e9; } + + mat-icon { + color: #0ea5e9; + } } -.nom-guardian-card__info { flex: 1; } -.nom-guardian-card__name { font-weight: 600; font-size: 13px; color: #1e293b; display: block; } -.nom-guardian-card__meta { font-size: 11px; color: #64748b; display: block; } -.nom-guardian-rel { width: 160px; } +.nom-guardian-card__info { + flex: 1; +} + +.nom-guardian-card__name { + font-weight: 600; + font-size: 13px; + color: #1e293b; + display: block; +} + +.nom-guardian-card__meta { + font-size: 11px; + color: #64748b; + display: block; +} + +.nom-guardian-rel { + width: 160px; +} /* Actions */ .nom-form-actions { @@ -599,11 +901,24 @@ gap: 10px; padding: 12px 16px; border-top: 1px solid #f1f5f9; - button { display: flex; align-items: center; gap: 6px; } + + button { + display: flex; + align-items: center; + gap: 6px; + } } @media (width <= 768px) { - .nom-form-fields { grid-template-columns: 1fr; } - .nom-guardian-card { flex-wrap: wrap; } - .nom-guardian-rel { width: 100%; } + .nom-form-fields { + grid-template-columns: 1fr; + } + + .nom-guardian-card { + flex-wrap: wrap; + } + + .nom-guardian-rel { + width: 100%; + } } diff --git a/src/app/clients/client-stepper/client-family-members-step/client-family-members-step.component.ts b/src/app/clients/client-stepper/client-family-members-step/client-family-members-step.component.ts index bd456cbab..07132b599 100644 --- a/src/app/clients/client-stepper/client-family-members-step/client-family-members-step.component.ts +++ b/src/app/clients/client-stepper/client-family-members-step/client-family-members-step.component.ts @@ -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: () => {} }); } diff --git a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts index ec3519fb9..c19dddb10 100644 --- a/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts +++ b/src/app/clients/client-stepper/client-general-step/client-general-step.component.ts @@ -2020,11 +2020,12 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges export class SignatoryDetailDialogComponent { private clientService = inject(ClientsService); private sanitizer = inject(DomSanitizer); + private inputData = inject(MAT_DIALOG_DATA); data: any; loading = false; constructor() { - const inputData = inject(MAT_DIALOG_DATA); + const inputData = this.inputData; this.data = { ...inputData }; const cid = inputData.id?.toString(); if (!cid) return; diff --git a/src/app/clients/client-stepper/client-general-step/din-verify-dialog.component.ts b/src/app/clients/client-stepper/client-general-step/din-verify-dialog.component.ts index 8aa41127b..d27edb9ab 100644 --- a/src/app/clients/client-stepper/client-general-step/din-verify-dialog.component.ts +++ b/src/app/clients/client-stepper/client-general-step/din-verify-dialog.component.ts @@ -1,3 +1,11 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + import { Component, OnInit, OnDestroy, inject } from '@angular/core'; import { CommonModule } from '@angular/common'; import { FormsModule } from '@angular/forms'; @@ -39,13 +47,23 @@ import { ClientsService } from 'app/clients/clients.service'; }
- Enter the Director Identification Number (DIN) 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. + Enter the Director Identification Number (DIN) 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.
DIN Number badge - + 8-digit DIN number from MCA
@@ -59,14 +77,17 @@ import { ClientsService } from 'app/clients/clients.service';
@for (step of progressSteps; track step.label; let i = $index) {
- + {{ step.label }}
}

- Verifying DIN {{ dinNumber }} for {{ signatoryName }} - against MCA (Ministry of Corporate Affairs) records. + Verifying DIN {{ dinNumber }} for {{ signatoryName }} against + MCA (Ministry of Corporate Affairs) records.

} @@ -80,9 +101,8 @@ import { ClientsService } from 'app/clients/clients.service';
VERIFIED

DIN Verified Successfully

- DIN {{ dinNumber }} for - {{ signatoryName }} has been verified against - MCA records. + DIN {{ dinNumber }} for {{ signatoryName }} has been + verified against MCA records.

@@ -124,7 +144,9 @@ import { ClientsService } from 'app/clients/clients.service';
Entity Match Found -

{{ entityName }} matches {{ matchedCompany }}

+

+ {{ entityName }} matches {{ matchedCompany }} +

} @else { @@ -132,7 +154,9 @@ import { ClientsService } from 'app/clients/clients.service';
No Entity Match -

{{ entityName }} does not match any of the director's associated companies.

+

+ {{ entityName }} does not match any of the director's associated companies. +

} @@ -148,11 +172,21 @@ import { ClientsService } from 'app/clients/clients.service';
{{ company.companyName }} - @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 }} } + @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 }} + }
@@ -201,15 +235,11 @@ import { ClientsService } from 'app/clients/clients.service'; } @if (!verifying && verified) { - + } @if (!verifying && failed) { - + } @if (!verifying && errorOccurred) { @@ -235,7 +265,9 @@ import { ClientsService } from 'app/clients/clients.service'; color: #1074b9; } - .dialog-title-icon { font-size: 20px; } + .dialog-title-icon { + font-size: 20px; + } mat-dialog-content { padding: 16px 24px; @@ -255,10 +287,24 @@ import { ClientsService } from 'app/clients/clients.service'; border: 1px solid #e2e8f0; } - .sig-icon { font-size: 22px; color: #1074b9; } - .sig-detail { display: flex; flex-direction: column; } - .sig-name { font-size: 15px; font-weight: 700; color: #1a2332; } - .sig-meta { font-size: 12px; color: #64748b; margin-top: 2px; } + .sig-icon { + font-size: 22px; + color: #1074b9; + } + .sig-detail { + display: flex; + flex-direction: column; + } + .sig-name { + font-size: 15px; + font-weight: 700; + color: #1a2332; + } + .sig-meta { + font-size: 12px; + color: #64748b; + margin-top: 2px; + } /* Entity info */ .entity-info { @@ -272,10 +318,26 @@ import { ClientsService } from 'app/clients/clients.service'; border: 1px solid #fde68a; } - .entity-icon { font-size: 18px; color: #b45309; } - .entity-detail { display: flex; flex-direction: column; } - .entity-label { font-size: 10px; font-weight: 600; text-transform: uppercase; letter-spacing: 0.5px; color: #92400e; } - .entity-name { font-size: 14px; font-weight: 700; color: #78350f; } + .entity-icon { + font-size: 18px; + color: #b45309; + } + .entity-detail { + display: flex; + flex-direction: column; + } + .entity-label { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + color: #92400e; + } + .entity-name { + font-size: 14px; + font-weight: 700; + color: #78350f; + } /* DIN info bar */ .din-info-bar { @@ -291,49 +353,180 @@ import { ClientsService } from 'app/clients/clients.service'; line-height: 1.5; } - .info-icon { font-size: 16px; color: #1074b9; flex-shrink: 0; margin-top: 2px; } + .info-icon { + font-size: 16px; + color: #1074b9; + flex-shrink: 0; + margin-top: 2px; + } /* DIN input */ - .din-input-field { width: 100%; } - .din-prefix-icon { color: #64748b; margin-right: 4px; } + .din-input-field { + width: 100%; + } + .din-prefix-icon { + color: #64748b; + margin-right: 4px; + } /* Progress */ - .progress-section { display: flex; flex-direction: column; align-items: center; gap: 12px; } - .progress-percent { font-size: 22px; font-weight: 700; color: #1074b9; margin-top: 4px; } + .progress-section { + display: flex; + flex-direction: column; + align-items: center; + gap: 12px; + } + .progress-percent { + font-size: 22px; + font-weight: 700; + color: #1074b9; + margin-top: 4px; + } - .progress-steps { display: flex; flex-direction: column; gap: 10px; width: 100%; padding: 8px 16px; } - .progress-step { display: flex; align-items: center; gap: 8px; font-size: 13px; color: #9ca3af; transition: color 0.3s ease; } - .progress-step.active { color: #1074b9; font-weight: 600; } - .progress-step.done { color: #388e3c; } - .done-icon { color: #388e3c; } + .progress-steps { + display: flex; + flex-direction: column; + gap: 10px; + width: 100%; + padding: 8px 16px; + } + .progress-step { + display: flex; + align-items: center; + gap: 8px; + font-size: 13px; + color: #9ca3af; + transition: color 0.3s ease; + } + .progress-step.active { + color: #1074b9; + font-weight: 600; + } + .progress-step.done { + color: #388e3c; + } + .done-icon { + color: #388e3c; + } - .verify-msg { font-size: 14px; color: #555; line-height: 1.6; text-align: center; margin: 4px 0 0; } + .verify-msg { + font-size: 14px; + color: #555; + line-height: 1.6; + text-align: center; + margin: 4px 0 0; + } /* Result cards */ - .result-card { display: flex; flex-direction: column; align-items: center; padding: 20px 16px; gap: 12px; } - .result-icon-ring { width: 64px; height: 64px; border-radius: 50%; display: flex; align-items: center; justify-content: center; } - .success-ring { background: linear-gradient(135deg, #ecfdf5, #d1fae5); border: 3px solid #a7f3d0; } - .fail-ring { background: linear-gradient(135deg, #fef2f2, #fecaca); border: 3px solid #fca5a5; } - .result-shield { font-size: 28px; } - .success-shield { color: #059669; } - .fail-shield { color: #dc2626; } + .result-card { + display: flex; + flex-direction: column; + align-items: center; + padding: 20px 16px; + gap: 12px; + } + .result-icon-ring { + width: 64px; + height: 64px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + } + .success-ring { + background: linear-gradient(135deg, #ecfdf5, #d1fae5); + border: 3px solid #a7f3d0; + } + .fail-ring { + background: linear-gradient(135deg, #fef2f2, #fecaca); + border: 3px solid #fca5a5; + } + .result-shield { + font-size: 28px; + } + .success-shield { + color: #059669; + } + .fail-shield { + color: #dc2626; + } - .result-badge { display: inline-block; font-size: 11px; font-weight: 800; letter-spacing: 1.5px; padding: 3px 14px; border-radius: 20px; } - .success-badge { color: #059669; background: #ecfdf5; border: 1px solid #a7f3d0; } - .fail-badge { color: #dc2626; background: #fef2f2; border: 1px solid #fecaca; } + .result-badge { + display: inline-block; + font-size: 11px; + font-weight: 800; + letter-spacing: 1.5px; + padding: 3px 14px; + border-radius: 20px; + } + .success-badge { + color: #059669; + background: #ecfdf5; + border: 1px solid #a7f3d0; + } + .fail-badge { + color: #dc2626; + background: #fef2f2; + border: 1px solid #fecaca; + } - .result-title { font-size: 17px; font-weight: 700; color: #111827; margin: 0; } - .result-detail { font-size: 13px; color: #6b7280; text-align: center; line-height: 1.6; margin: 0; max-width: 400px; } - .din-highlight { color: #059669; font-family: 'JetBrains Mono', 'Fira Code', monospace; } - .din-highlight-fail { color: #dc2626; font-family: 'JetBrains Mono', 'Fira Code', monospace; } + .result-title { + font-size: 17px; + font-weight: 700; + color: #111827; + margin: 0; + } + .result-detail { + font-size: 13px; + color: #6b7280; + text-align: center; + line-height: 1.6; + margin: 0; + max-width: 400px; + } + .din-highlight { + color: #059669; + font-family: 'JetBrains Mono', 'Fira Code', monospace; + } + .din-highlight-fail { + color: #dc2626; + font-family: 'JetBrains Mono', 'Fira Code', monospace; + } - .result-meta { width: 100%; background: #f9fafb; border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px 16px; margin-top: 4px; } - .meta-row { display: flex; justify-content: space-between; padding: 6px 0; border-bottom: 1px solid #f3f4f6; } - .meta-row:last-child { border-bottom: none; } - .meta-label { font-size: 12px; color: #9ca3af; font-weight: 500; } - .meta-value { font-size: 12px; color: #374151; font-weight: 600; } - .meta-mono { font-family: 'JetBrains Mono', 'Fira Code', monospace; letter-spacing: 0.5px; } - .meta-success { color: #059669; } + .result-meta { + width: 100%; + background: #f9fafb; + border: 1px solid #e5e7eb; + border-radius: 8px; + padding: 12px 16px; + margin-top: 4px; + } + .meta-row { + display: flex; + justify-content: space-between; + padding: 6px 0; + border-bottom: 1px solid #f3f4f6; + } + .meta-row:last-child { + border-bottom: none; + } + .meta-label { + font-size: 12px; + color: #9ca3af; + font-weight: 500; + } + .meta-value { + font-size: 12px; + color: #374151; + font-weight: 600; + } + .meta-mono { + font-family: 'JetBrains Mono', 'Fira Code', monospace; + letter-spacing: 0.5px; + } + .meta-success { + color: #059669; + } /* ═══ Company Match Section ═══ */ .company-match-section { @@ -351,7 +544,10 @@ import { ClientsService } from 'app/clients/clients.service'; margin: 0 0 10px; } - .company-title-icon { font-size: 14px; color: #64748b; } + .company-title-icon { + font-size: 14px; + color: #64748b; + } /* Match banner */ .match-banner { @@ -365,7 +561,10 @@ import { ClientsService } from 'app/clients/clients.service'; line-height: 1.5; } - .match-banner p { margin: 2px 0 0; font-size: 12px; } + .match-banner p { + margin: 2px 0 0; + font-size: 12px; + } .match-success { background: #ecfdf5; @@ -373,7 +572,12 @@ import { ClientsService } from 'app/clients/clients.service'; color: #065f46; } - .match-success .match-banner-icon { color: #059669; font-size: 18px; margin-top: 1px; flex-shrink: 0; } + .match-success .match-banner-icon { + color: #059669; + font-size: 18px; + margin-top: 1px; + flex-shrink: 0; + } .match-warning { background: #fffbeb; @@ -381,7 +585,12 @@ import { ClientsService } from 'app/clients/clients.service'; color: #92400e; } - .match-warning .match-banner-icon { color: #d97706; font-size: 18px; margin-top: 1px; flex-shrink: 0; } + .match-warning .match-banner-icon { + color: #d97706; + font-size: 18px; + margin-top: 1px; + flex-shrink: 0; + } /* Company list */ .company-list { @@ -399,9 +608,13 @@ import { ClientsService } from 'app/clients/clients.service'; transition: background 0.15s; } - .company-item:last-child { border-bottom: none; } + .company-item:last-child { + border-bottom: none; + } - .company-item:hover { background: #f8fafc; } + .company-item:hover { + background: #f8fafc; + } .company-matched { background: #f0fdf4 !important; @@ -415,28 +628,78 @@ import { ClientsService } from 'app/clients/clients.service'; flex-shrink: 0; } - .company-match-icon.match { color: #22c55e; } - .company-match-icon.no-match { color: #d1d5db; } + .company-match-icon.match { + color: #22c55e; + } + .company-match-icon.no-match { + color: #d1d5db; + } - .company-info { display: flex; flex-direction: column; min-width: 0; } - .company-name { font-size: 13px; font-weight: 600; color: #1e293b; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } - .company-meta { font-size: 11px; color: #94a3b8; margin-top: 2px; } + .company-info { + display: flex; + flex-direction: column; + min-width: 0; + } + .company-name { + font-size: 13px; + font-weight: 600; + color: #1e293b; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .company-meta { + font-size: 11px; + color: #94a3b8; + margin-top: 2px; + } /* Fail reason */ .fail-reason { - display: flex; align-items: center; gap: 10px; width: 100%; - background: #fef2f2; border: 1px solid #fecaca; padding: 10px 14px; - border-radius: 6px; font-size: 13px; color: #991b1b; line-height: 1.5; + display: flex; + align-items: center; + gap: 10px; + width: 100%; + background: #fef2f2; + border: 1px solid #fecaca; + padding: 10px 14px; + border-radius: 6px; + font-size: 13px; + color: #991b1b; + line-height: 1.5; + } + .fail-reason-icon { + font-size: 16px; + color: #dc2626; + flex-shrink: 0; } - .fail-reason-icon { font-size: 16px; color: #dc2626; flex-shrink: 0; } /* Error */ - .error-icon-large { font-size: 48px; color: #f57c00; } - .error-msg { font-size: 16px; font-weight: 600; color: #f57c00; margin: 0; text-align: center; } - .error-detail { font-size: 14px; color: #555; text-align: center; } + .error-icon-large { + font-size: 48px; + color: #f57c00; + } + .error-msg { + font-size: 16px; + font-weight: 600; + color: #f57c00; + margin: 0; + text-align: center; + } + .error-detail { + font-size: 14px; + color: #555; + text-align: center; + } - mat-dialog-actions { padding: 8px 24px 16px; } - mat-dialog-actions button { display: flex; align-items: center; gap: 6px; } + mat-dialog-actions { + padding: 8px 24px 16px; + } + mat-dialog-actions button { + display: flex; + align-items: center; + gap: 6px; + } ` ], imports: [ @@ -561,7 +824,11 @@ export class DinVerifyDialogComponent implements OnInit, OnDestroy { if (this.apiError) { this.errorOccurred = true; - this.errorMessage = this.apiError?.error?.defaultUserMessage || this.apiError?.error?.message || this.apiError?.message || 'Unable to verify DIN. Please try again.'; + this.errorMessage = + this.apiError?.error?.defaultUserMessage || + this.apiError?.error?.message || + this.apiError?.message || + 'Unable to verify DIN. Please try again.'; return; } @@ -590,7 +857,11 @@ export class DinVerifyDialogComponent implements OnInit, OnDestroy { this.matchCompanyName(); } else { this.failed = true; - this.failReason = changes?.dinFailReason || res?.failReason || res?.defaultUserMessage || 'DIN could not be verified against MCA records.'; + this.failReason = + changes?.dinFailReason || + res?.failReason || + res?.defaultUserMessage || + 'DIN could not be verified against MCA records.'; } } diff --git a/src/app/clients/client-stepper/client-general-step/sanctions-verify-dialog.component.ts b/src/app/clients/client-stepper/client-general-step/sanctions-verify-dialog.component.ts index 10fb27d39..43100b447 100644 --- a/src/app/clients/client-stepper/client-general-step/sanctions-verify-dialog.component.ts +++ b/src/app/clients/client-stepper/client-general-step/sanctions-verify-dialog.component.ts @@ -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';
@for (step of progressSteps; track step.label; let i = $index) {
- + {{ step.label }}
} @@ -42,8 +53,8 @@ import { ClientsService } from 'app/clients/clients.service';

No Sanctions Match Found

{{ clientName }} has been screened against the - UN Security Council Consolidated List and - FIU-IND records. No confirmed match was found. + UN Security Council Consolidated List and FIU-IND records. No confirmed + match was found.

@@ -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) { diff --git a/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.html b/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.html index 09e6f73cf..879172463 100644 --- a/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.html +++ b/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.html @@ -20,15 +20,18 @@ - - 1 Permanent Address - + 1 Permanent Address
Address Line 1 home - +
@@ -45,9 +48,15 @@ PIN Code pin_drop - + (keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()" + /> @if (permanentForm.get('pinCode')?.hasError('pattern') && permanentForm.get('pinCode')?.touched) { Enter a valid 6-digit PIN code } @@ -126,9 +135,7 @@ - - 2 Current / Residential - + 2 Current / Residential
@@ -140,7 +147,12 @@
Address Line 1 - +
@@ -155,9 +167,15 @@
PIN Code - + (keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()" + /> @if (currentForm.get('pinCode')?.hasError('pattern') && currentForm.get('pinCode')?.touched) { Enter a valid 6-digit PIN code } @@ -230,9 +248,7 @@ - - 3 Office / Work - + 3 Office / Work
@@ -268,9 +284,14 @@ PIN Code pin_drop - + (keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()" + /> @if (officeForm.get('pinCode')?.hasError('pattern') && officeForm.get('pinCode')?.touched) { Enter a valid 6-digit PIN code } @@ -348,13 +369,13 @@ - - 4 Correspondence - + 4 Correspondence
Copy address from: - +
@@ -363,7 +384,12 @@ Address Line 1 home - +
@@ -380,9 +406,15 @@ PIN Code pin_drop - + (keypress)="$event.charCode >= 48 && $event.charCode <= 57 ? true : $event.preventDefault()" + /> @if (correspondenceForm.get('pinCode')?.hasError('pattern') && correspondenceForm.get('pinCode')?.touched) { Enter a valid 6-digit PIN code } diff --git a/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.scss b/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.scss index 443be4642..b4638207c 100644 --- a/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.scss +++ b/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.scss @@ -1,3 +1,11 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + /** * Copyright since 2025 Mifos Initiative */ @@ -76,9 +84,17 @@ $muted: #64748b; gap: 4px 16px; margin-bottom: 4px; - &--1 { grid-template-columns: 1fr; } - &--2 { grid-template-columns: 1fr 1fr; } - &--3 { grid-template-columns: 1fr 1fr 1fr; } + &--1 { + grid-template-columns: 1fr; + } + + &--2 { + grid-template-columns: 1fr 1fr; + } + + &--3 { + grid-template-columns: 1fr 1fr 1fr; + } } .addr-ff { @@ -101,7 +117,8 @@ $muted: #64748b; white-space: nowrap; } - &::before, &::after { + &::before, + &::after { content: ''; flex: 1; height: 1px; @@ -174,7 +191,8 @@ $muted: #64748b; // Responsive @media (width <= 768px) { - .addr-grid--2, .addr-grid--3 { + .addr-grid--2, + .addr-grid--3 { grid-template-columns: 1fr; } diff --git a/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.ts b/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.ts index 6bbe82071..935ac2b63 100644 --- a/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.ts +++ b/src/app/clients/client-stepper/client-india-address-step/client-india-address-step.component.ts @@ -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 = {}; private pinCache: Record = {}; @@ -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 = { - 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(); - 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() }; diff --git a/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts index e5b944138..288384984 100644 --- a/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts +++ b/src/app/clients/client-stepper/client-kyc-step/client-kyc-step.component.ts @@ -832,7 +832,8 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy { } else { this.gstinVerifyStatus = 'FAILED'; this.showPanErrorAlert( - changes?.gstinVerificationMessage || 'GSTIN verification failed. Please check the GSTIN number and try again.' + changes?.gstinVerificationMessage || + 'GSTIN verification failed. Please check the GSTIN number and try again.' ); } }, @@ -840,7 +841,9 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy { this.gstinVerifying = false; this.gstinVerifyStatus = 'FAILED'; this.showPanErrorAlert( - err?.error?.errors?.[0]?.defaultUserMessage || err?.error?.defaultUserMessage || 'GSTIN verification failed.' + err?.error?.errors?.[0]?.defaultUserMessage || + err?.error?.defaultUserMessage || + 'GSTIN verification failed.' ); } }); diff --git a/src/app/clients/client-stepper/client-kyc-step/image-crop-dialog/image-crop-dialog.component.html b/src/app/clients/client-stepper/client-kyc-step/image-crop-dialog/image-crop-dialog.component.html index 80d2260ba..c3d248f03 100644 --- a/src/app/clients/client-stepper/client-kyc-step/image-crop-dialog/image-crop-dialog.component.html +++ b/src/app/clients/client-stepper/client-kyc-step/image-crop-dialog/image-crop-dialog.component.html @@ -1,3 +1,11 @@ + + @@ -20,11 +28,13 @@
- + (mouseleave)="onMouseUp()" + >
Drag to move. Drag corner to resize.
diff --git a/src/app/clients/client-stepper/client-kyc-step/image-crop-dialog/image-crop-dialog.component.scss b/src/app/clients/client-stepper/client-kyc-step/image-crop-dialog/image-crop-dialog.component.scss index 4086f3fc6..a2fb27009 100644 --- a/src/app/clients/client-stepper/client-kyc-step/image-crop-dialog/image-crop-dialog.component.scss +++ b/src/app/clients/client-stepper/client-kyc-step/image-crop-dialog/image-crop-dialog.component.scss @@ -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 */ diff --git a/src/app/clients/client-stepper/client-kyc-step/image-crop-dialog/image-crop-dialog.component.ts b/src/app/clients/client-stepper/client-kyc-step/image-crop-dialog/image-crop-dialog.component.ts index 4192ddbdf..75140c573 100644 --- a/src/app/clients/client-stepper/client-kyc-step/image-crop-dialog/image-crop-dialog.component.ts +++ b/src/app/clients/client-stepper/client-kyc-step/image-crop-dialog/image-crop-dialog.component.ts @@ -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, - @Inject(MAT_DIALOG_DATA) public data: ImageCropData - ) {} + dialogRef = inject(MatDialogRef); + data = inject(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) => { diff --git a/src/app/clients/client-stepper/client-preview-step/client-preview-step.component.scss b/src/app/clients/client-stepper/client-preview-step/client-preview-step.component.scss index acddf86ee..a5e542600 100644 --- a/src/app/clients/client-stepper/client-preview-step/client-preview-step.component.scss +++ b/src/app/clients/client-stepper/client-preview-step/client-preview-step.component.scss @@ -405,8 +405,13 @@ } .preview-field { - &:nth-child(3n) { border-right: 1px solid rgb(0 0 0 / 4%); } - &:nth-child(2n) { border-right: none; } + &:nth-child(3n) { + border-right: 1px solid rgb(0 0 0 / 4%); + } + + &:nth-child(2n) { + border-right: none; + } } } @@ -486,8 +491,13 @@ border-bottom: 1px solid #f1f5f9; transition: background 0.1s; - &:last-child { border-bottom: none; } - &:hover { background: #f8fafc; } + &:last-child { + border-bottom: none; + } + + &:hover { + background: #f8fafc; + } } .fm-preview-row__sno { diff --git a/src/app/clients/clients-view/address-tab/address-tab.component.html b/src/app/clients/clients-view/address-tab/address-tab.component.html index 9ac75b190..769fe665e 100644 --- a/src/app/clients/clients-view/address-tab/address-tab.component.html +++ b/src/app/clients/clients-view/address-tab/address-tab.component.html @@ -34,12 +34,18 @@ {{ address.addressType }} - + {{ address.isActive ? 'Active' : 'Inactive' }} - {{ 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 : '' }} @@ -101,12 +107,16 @@ @if (address.stateProvinceId) {
State
-
{{ getSelectedValue('stateProvinceIdOptions', address.stateProvinceId)?.name || '-' }}
+
+ {{ getSelectedValue('stateProvinceIdOptions', address.stateProvinceId)?.name || '-' }} +
}
Country
-
{{ getSelectedValue('countryIdOptions', address.countryId)?.name || 'India' }}
+
+ {{ getSelectedValue('countryIdOptions', address.countryId)?.name || 'India' }} +
diff --git a/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.html b/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.html index efd92a83b..8a428867d 100644 --- a/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.html +++ b/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.html @@ -80,23 +80,17 @@
- - CIF Number - + CIF Number {{ cifNumber || clientId }}
- - Full Name - + Full Name {{ clientName || '—' }}
- - Email - + Email @if (clientEmail) { {{ clientEmail }} } @else { @@ -105,9 +99,7 @@
- - Mobile - + Mobile @if (clientMobile) { {{ clientMobile }} } @else { @@ -152,7 +144,8 @@ } @if (existing.ADDRESS_PROOF.fileCount && existing.ADDRESS_PROOF.fileCount > 0) { - {{ existing.ADDRESS_PROOF.fileCount }} {{ existing.ADDRESS_PROOF.fileCount === 1 ? 'file' : 'files' }} + {{ existing.ADDRESS_PROOF.fileCount }} + {{ existing.ADDRESS_PROOF.fileCount === 1 ? 'file' : 'files' }} }
@@ -169,65 +162,65 @@ } @if (!existing.ADDRESS_PROOF.present || existing.ADDRESS_PROOF.expanded) { -

{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG, PDF

-
- - +

{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG, PDF

+
- - +
+ -
- - @if (uploadErrors['ADDRESS_PROOF']) { -
- error_outline - {{ uploadErrors['ADDRESS_PROOF'] }} -
- } - @if (uploadResponses['ADDRESS_PROOF']) { -
-

- {{ 'labels.inputs.Status' | translate }}: {{ uploadResponses['ADDRESS_PROOF'].status }} -

-

- {{ 'labels.inputs.Request ID' | translate }}: {{ uploadResponses['ADDRESS_PROOF'].requestId }} -

-

- {{ 'labels.inputs.Acknowledgement Number' | translate }}: - {{ uploadResponses['ADDRESS_PROOF'].acknowledgementNumber }} -

-

- {{ 'labels.inputs.Processing Status' | translate }}: - {{ uploadResponses['ADDRESS_PROOF'].processingStatus }} -

-
- } } @@ -274,65 +267,65 @@ } @if (!existing.PHOTO_PROOF.present || existing.PHOTO_PROOF.expanded) { -

{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG, PDF

-
- - +

{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG, PDF

+
- - +
+ -
- - @if (uploadErrors['PHOTO_PROOF']) { -
- error_outline - {{ uploadErrors['PHOTO_PROOF'] }} -
- } - @if (uploadResponses['PHOTO_PROOF']) { -
-

- {{ 'labels.inputs.Status' | translate }}: {{ uploadResponses['PHOTO_PROOF'].status }} -

-

- {{ 'labels.inputs.Request ID' | translate }}: {{ uploadResponses['PHOTO_PROOF'].requestId }} -

-

- {{ 'labels.inputs.Acknowledgement Number' | translate }}: - {{ uploadResponses['PHOTO_PROOF'].acknowledgementNumber }} -

-

- {{ 'labels.inputs.Processing Status' | translate }}: - {{ uploadResponses['PHOTO_PROOF'].processingStatus }} -

-
- } } @@ -370,65 +363,65 @@ } @if (!existing.CLIENT_IMAGE.present || existing.CLIENT_IMAGE.expanded) { -

{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG

-
- - +

{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG

+
- - +
+ -
- - @if (uploadErrors['CLIENT_IMAGE']) { -
- error_outline - {{ uploadErrors['CLIENT_IMAGE'] }} -
- } - @if (uploadResponses['CLIENT_IMAGE']) { -
-

- {{ 'labels.inputs.Status' | translate }}: {{ uploadResponses['CLIENT_IMAGE'].status }} -

-

- {{ 'labels.inputs.Request ID' | translate }}: {{ uploadResponses['CLIENT_IMAGE'].requestId }} -

-

- {{ 'labels.inputs.Acknowledgement Number' | translate }}: - {{ uploadResponses['CLIENT_IMAGE'].acknowledgementNumber }} -

-

- {{ 'labels.inputs.Processing Status' | translate }}: - {{ uploadResponses['CLIENT_IMAGE'].processingStatus }} -

-
- } } @@ -451,7 +444,11 @@
@if (existing.CLIENT_SIGNATURE.previewUrl) { - Client signature + Client signature }
@@ -470,65 +467,66 @@ } @if (!existing.CLIENT_SIGNATURE.present || existing.CLIENT_SIGNATURE.expanded) { -

{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG

-
- - +

{{ 'labels.inputs.Accepted formats' | translate }}: PNG, JPEG, JPG

+
- - +
+ -
- - @if (uploadErrors['CLIENT_SIGNATURE']) { -
- error_outline - {{ uploadErrors['CLIENT_SIGNATURE'] }} -
- } - @if (uploadResponses['CLIENT_SIGNATURE']) { -
-

- {{ 'labels.inputs.Status' | translate }}: {{ uploadResponses['CLIENT_SIGNATURE'].status }} -

-

- {{ 'labels.inputs.Request ID' | translate }}: {{ uploadResponses['CLIENT_SIGNATURE'].requestId }} -

-

- {{ 'labels.inputs.Acknowledgement Number' | translate }}: - {{ uploadResponses['CLIENT_SIGNATURE'].acknowledgementNumber }} -

-

- {{ 'labels.inputs.Processing Status' | translate }}: - {{ uploadResponses['CLIENT_SIGNATURE'].processingStatus }} -

-
- } } diff --git a/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.scss b/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.scss index efcb3347e..ba29cd983 100644 --- a/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.scss +++ b/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.scss @@ -61,7 +61,7 @@ overflow: hidden; margin-bottom: 18px; color: #fff; - box-shadow: 0 6px 22px rgba(0, 0, 0, 0.1); + box-shadow: 0 6px 22px rgb(0 0 0 / 10%); &::before { content: ''; @@ -71,7 +71,7 @@ width: 320px; height: 320px; border-radius: 50%; - background: radial-gradient(circle, rgba(255, 255, 255, 0.18), transparent 70%); + background: radial-gradient(circle, rgb(255 255 255 / 18%), transparent 70%); pointer-events: none; } @@ -96,8 +96,8 @@ width: 60px; height: 60px; border-radius: 16px; - background: rgba(255, 255, 255, 0.2); - border: 1px solid rgba(255, 255, 255, 0.35); + background: rgb(255 255 255 / 20%); + border: 1px solid rgb(255 255 255 / 35%); display: flex; align-items: center; justify-content: center; @@ -115,8 +115,8 @@ display: inline-block; padding: 4px 12px; border-radius: 20px; - background: rgba(255, 255, 255, 0.22); - border: 1px solid rgba(255, 255, 255, 0.4); + background: rgb(255 255 255 / 22%); + border: 1px solid rgb(255 255 255 / 40%); font-size: 10px; font-weight: 700; letter-spacing: 0.8px; @@ -150,11 +150,20 @@ opacity: 0.95; flex-wrap: wrap; - fa-icon { font-size: 11px; opacity: 0.9; margin-right: 3px; } - strong { font-weight: 700; } + fa-icon { + font-size: 11px; + opacity: 0.9; + margin-right: 3px; + } + + strong { + font-weight: 700; + } } - &__sep { opacity: 0.5; } + &__sep { + opacity: 0.5; + } &__hint { font-size: 12px; @@ -169,8 +178,8 @@ align-items: center; gap: 8px; padding: 7px 14px; - background: rgba(255, 255, 255, 0.2); - border: 1px solid rgba(255, 255, 255, 0.35); + background: rgb(255 255 255 / 20%); + border: 1px solid rgb(255 255 255 / 35%); border-radius: 20px; font-size: 11px; font-weight: 700; @@ -186,15 +195,23 @@ height: 8px; border-radius: 50%; background: #9fe1cb; - box-shadow: 0 0 0 0 rgba(159, 225, 203, 0.7); + 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 rgba(159, 225, 203, 0.7); } - 70% { box-shadow: 0 0 0 10px rgba(159, 225, 203, 0); } - 100% { box-shadow: 0 0 0 0 rgba(159, 225, 203, 0); } + 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 ===================== */ @@ -218,7 +235,7 @@ &:hover { border-color: #c6dafc; - box-shadow: 0 2px 8px rgba(26, 115, 232, 0.06); + box-shadow: 0 2px 8px rgb(26 115 232 / 6%); } } @@ -231,9 +248,19 @@ flex-direction: column; align-items: flex-start; - &__left { width: 100%; } - &__icon { width: 52px; height: 52px; font-size: 22px; } - &__ckyc-number { font-size: 20px; } + &__left { + width: 100%; + } + + &__icon { + width: 52px; + height: 52px; + font-size: 22px; + } + + &__ckyc-number { + font-size: 20px; + } } } @@ -253,7 +280,10 @@ align-items: center; gap: 6px; - fa-icon { color: #1a73e8; font-size: 11px; } + fa-icon { + color: #1a73e8; + font-size: 11px; + } } .ckyc-info-value { @@ -277,7 +307,9 @@ } } -.ckyc-info-hint { display: none; } +.ckyc-info-hint { + display: none; +} .ckyc-section-title { margin: 24px 0 12px; @@ -296,14 +328,16 @@ border-radius: 12px !important; border: 1px solid #e8eaed; box-shadow: none !important; - transition: border-color 0.15s, box-shadow 0.15s; + 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 rgba(26, 115, 232, 0.06) !important; + box-shadow: 0 2px 10px rgb(26 115 232 / 6%) !important; } .mat-mdc-card-content { @@ -336,7 +370,7 @@ display: flex !important; align-items: center; justify-content: center; - box-shadow: 0 2px 6px rgba(26, 115, 232, 0.25); + box-shadow: 0 2px 6px rgb(26 115 232 / 25%); flex-shrink: 0; } @@ -426,7 +460,9 @@ font-size: 12px; margin-bottom: 14px; - mat-progress-spinner { display: inline-block; } + mat-progress-spinner { + display: inline-block; + } } .ckyc-existing { @@ -494,7 +530,10 @@ font-weight: 600; } - fa-icon { color: #1d9e75; font-size: 12px; } + fa-icon { + color: #1d9e75; + font-size: 12px; + } } &__key { @@ -528,7 +567,7 @@ background: #fff; object-fit: cover; align-self: center; - box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); + box-shadow: 0 2px 8px rgb(0 0 0 / 6%); &--wide { width: 200px; @@ -550,7 +589,10 @@ padding: 0 12px !important; letter-spacing: 0.2px; - fa-icon { margin-right: 6px; font-size: 11px; } + fa-icon { + margin-right: 6px; + font-size: 11px; + } &:hover { background: #e1f5ee !important; diff --git a/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.ts b/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.ts index 2033c44e8..26a2c6b17 100644 --- a/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.ts +++ b/src/app/clients/clients-view/ckyc-tab/ckyc-tab.component.ts @@ -176,7 +176,11 @@ export class CkycTabComponent implements OnInit, OnDestroy { private formatJavaDate(value: any): string { if (!value) return ''; if (Array.isArray(value) && value.length >= 3) { - const [y, m, d] = value; + const [ + y, + m, + d + ] = value; return `${String(d).padStart(2, '0')}-${String(m).padStart(2, '0')}-${y}`; } if (typeof value === 'string') return value; @@ -236,13 +240,9 @@ export class CkycTabComponent implements OnInit, OnDestroy { // 3 & 4. ID proof + Address proof — client identifiers classified via proof-types const idProofIds = new Set((proofTypes?.idProofs || []).map((p: any) => String(p.id))); - const addressProofIds = new Set( - (proofTypes?.addressProofs || []).map((p: any) => String(p.id)) - ); + const addressProofIds = new Set((proofTypes?.addressProofs || []).map((p: any) => String(p.id))); - const idList = Array.isArray(identifiers) - ? identifiers - : (identifiers as any)?.pageItems || []; + const idList = Array.isArray(identifiers) ? identifiers : (identifiers as any)?.pageItems || []; for (const identity of idList) { const docTypeId = String(identity?.documentType?.id ?? ''); diff --git a/src/app/clients/clients-view/clients-view.component.html b/src/app/clients/clients-view/clients-view.component.html index 182f7657e..969075cf5 100644 --- a/src/app/clients/clients-view/clients-view.component.html +++ b/src/app/clients/clients-view/clients-view.component.html @@ -10,29 +10,36 @@
- +
- - -
- {{ 'labels.inputs.View Signature' | translate }} + {{ + 'labels.inputs.View Signature' | translate + }}
@@ -40,12 +47,19 @@
- + + [entityName]=" + complianceHideClientData ? maskName(clientViewData.displayName) : clientViewData.displayName + " + [display]="'right'" + [hideCopy]="complianceHideClientData" + > {{ 'labels.status.' + clientViewData.status.value | translate }} @@ -77,8 +91,12 @@ {{ 'labels.inputs.External Id' | translate }} + [externalId]=" + complianceHideClientData ? maskValue(clientViewData.externalId) : clientViewData.externalId + " + [completed]="true" + [hideCopy]="complianceHideClientData" + >
} @@ -118,7 +136,9 @@
+ [completed]="true" + [hideCopy]="complianceHideClientData" + >
} @@ -127,8 +147,12 @@
{{ 'labels.inputs.Email' | translate }}
+ [externalId]=" + complianceHideClientData ? maskEmail(clientViewData.emailAddress) : clientViewData.emailAddress + " + [completed]="true" + [hideCopy]="complianceHideClientData" + >
} @@ -148,10 +172,26 @@
DIP Status
- - {{ (clientViewData.dipVerified === true || clientViewData.dipVerified === 'true' || clientViewData.dipVerified === 1) ? 'Verified' : 'Pending' }} + + {{ + clientViewData.dipVerified === true || + clientViewData.dipVerified === 'true' || + clientViewData.dipVerified === 1 + ? 'Verified' + : 'Pending' + }}
@@ -160,10 +200,26 @@
Sanctions Screening
- - {{ (clientViewData.nameVerified === true || clientViewData.nameVerified === 'true' || clientViewData.nameVerified === 1) ? 'Clear' : 'Pending' }} + + {{ + clientViewData.nameVerified === true || + clientViewData.nameVerified === 'true' || + clientViewData.nameVerified === 1 + ? 'Clear' + : 'Pending' + }}
@@ -206,244 +262,299 @@
- - + + @if (isActive()) { + + } + + + + @if (!clientViewData.staffId) { + + } + + @if (clientViewData.staffId) { + + + + } - @if (isActive()) { - - } + + - + + + + + + + + - @if (!clientViewData.staffId) { - - } + + + + @if (clientViewData.status.value === 'Pending') { + + } + @if (clientViewData.status.value === 'Pending') { + + } + @if (clientViewData.status.value === 'Pending') { + + } + @if (clientViewData.status.value === 'Pending') { + + } + @if (clientViewData.status.value === 'Closed') { + + } + @if (clientViewData.status.value === 'Rejected') { + + } + @if (clientViewData.status.value === 'Transfer in progress') { + + } + @if (clientViewData.status.value === 'Transfer in progress') { + + } + @if (clientViewData.status.value === 'Transfer in progress') { + + } + - @if (clientViewData.staffId) { - - - - } - - - - - - + + + + + + + + + + @if (clientViewData.status.value !== 'Transfer on hold') { + - - - - - - - - - - @if (clientViewData.status.value === 'Pending') { - - } - @if (clientViewData.status.value === 'Pending') { - - } - @if (clientViewData.status.value === 'Pending') { - - } - @if (clientViewData.status.value === 'Pending') { - - } - @if (clientViewData.status.value === 'Closed') { - - } - @if (clientViewData.status.value === 'Rejected') { - - } - @if (clientViewData.status.value === 'Transfer in progress') { - - } - @if (clientViewData.status.value === 'Transfer in progress') { - - } - @if (clientViewData.status.value === 'Transfer in progress') { - - } - - - - - - - - - - - - @if (clientViewData.status.value !== 'Transfer on hold') { - - - - } - + + } +
@if (loadingProofs) { -
- Loading document types… -
+
Loading document types…
} @@ -51,12 +48,8 @@
-

- Identity proof -

-

- Select document type — upload requirements will update automatically -

+

Identity proof

+

Select document type — upload requirements will update automatically

@@ -73,9 +66,7 @@ }
- +
- {{ selectedIdProof.sides === 2 ? 'Front side' : (selectedIdProof.name + ' (single copy)') }} - - - Required + {{ selectedIdProof.sides === 2 ? 'Front side' : selectedIdProof.name + ' (single copy)' }} + Required
@@ -258,14 +231,10 @@ }
- +
@for (group of addressGroups; track group.label; let last = $last) { -
- {{ group.label }} -
+
{{ group.label }}
@for (p of group.items; track p.id) { - {{ selectedAddrProof.sides === 2 ? 'Front side' : (selectedAddrProof.name + ' (single copy)') }} - - - Required + {{ selectedAddrProof.sides === 2 ? 'Front side' : selectedAddrProof.name + ' (single copy)' }} + Required
@@ -91,5 +95,4 @@ } -
diff --git a/src/app/clients/clients-view/family-members-tab/family-members-tab.component.ts b/src/app/clients/clients-view/family-members-tab/family-members-tab.component.ts index b30a9fcf4..285ca163d 100644 --- a/src/app/clients/clients-view/family-members-tab/family-members-tab.component.ts +++ b/src/app/clients/clients-view/family-members-tab/family-members-tab.component.ts @@ -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 */ diff --git a/src/app/clients/clients-view/family-members-tab/nominee-dialog/nominee-dialog.component.scss b/src/app/clients/clients-view/family-members-tab/nominee-dialog/nominee-dialog.component.scss index e95385d43..011ef1ce4 100644 --- a/src/app/clients/clients-view/family-members-tab/nominee-dialog/nominee-dialog.component.scss +++ b/src/app/clients/clients-view/family-members-tab/nominee-dialog/nominee-dialog.component.scss @@ -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; diff --git a/src/app/clients/clients-view/family-members-tab/nominee-dialog/nominee-dialog.component.ts b/src/app/clients/clients-view/family-members-tab/nominee-dialog/nominee-dialog.component.ts index 454f44f50..74b11319b 100644 --- a/src/app/clients/clients-view/family-members-tab/nominee-dialog/nominee-dialog.component.ts +++ b/src/app/clients/clients-view/family-members-tab/nominee-dialog/nominee-dialog.component.ts @@ -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], diff --git a/src/app/clients/clients-view/form60-tab/form60-tab.component.html b/src/app/clients/clients-view/form60-tab/form60-tab.component.html index b93e3ddb6..97a73d9a8 100644 --- a/src/app/clients/clients-view/form60-tab/form60-tab.component.html +++ b/src/app/clients/clients-view/form60-tab/form60-tab.component.html @@ -61,13 +61,17 @@ } @if (form60Data.dateOfBirth) {
- calendar_today Date of Birth + calendar_today Date of Birth {{ form60Data.dateOfBirth | dateFormat }}
} @if (form60Data.fatherName) {
- family_restroom Father's Name + family_restroom Father's Name {{ form60Data.fatherName }}
} @@ -79,25 +83,33 @@ } @if (form60Data.transactionType) {
- receipt_long Transaction Type + receipt_long Transaction Type {{ form60Data.transactionType }}
} @if (form60Data.transactionAmount) {
- currency_rupee Transaction Amount + currency_rupee Transaction Amount ₹ {{ form60Data.transactionAmount }}
} @if (form60Data.estimatedTotalIncome) {
- account_balance_wallet Estimated Total Income + account_balance_wallet Estimated Total Income ₹ {{ form60Data.estimatedTotalIncome }}
} @if (form60Data.reasonForNoPan) {
- help_outline Reason for No PAN + help_outline Reason for No PAN {{ form60Data.reasonForNoPan }}
} @@ -181,7 +193,8 @@
- 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.
diff --git a/src/app/clients/clients-view/form60-tab/form60-tab.component.scss b/src/app/clients/clients-view/form60-tab/form60-tab.component.scss index 9881de69e..8ac9db610 100644 --- a/src/app/clients/clients-view/form60-tab/form60-tab.component.scss +++ b/src/app/clients/clients-view/form60-tab/form60-tab.component.scss @@ -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,103 +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; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @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; + display: grid; + grid-template-columns: 1fr 1fr; + gap: 4px 16px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @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; + } } diff --git a/src/app/clients/clients-view/form60-tab/form60-tab.component.ts b/src/app/clients/clients-view/form60-tab/form60-tab.component.ts index c47e8d60c..5f66796af 100644 --- a/src/app/clients/clients-view/form60-tab/form60-tab.component.ts +++ b/src/app/clients/clients-view/form60-tab/form60-tab.component.ts @@ -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 { diff --git a/src/app/clients/clients-view/general-tab/general-tab.component.html b/src/app/clients/clients-view/general-tab/general-tab.component.html index 461b246c8..0da32a41a 100644 --- a/src/app/clients/clients-view/general-tab/general-tab.component.html +++ b/src/app/clients/clients-view/general-tab/general-tab.component.html @@ -7,7 +7,6 @@ -->
-

{{ 'labels.heading.Performance History' | translate }}

@@ -164,7 +163,9 @@
-

{{ 'labels.messages.No Active Saving Accounts Available' | translate }}

+

+ {{ 'labels.messages.No Active Saving Accounts Available' | translate }} +

@@ -211,7 +212,9 @@
-

{{ 'labels.messages.No Closed Saving Accounts Available' | translate }}

+

+ {{ 'labels.messages.No Closed Saving Accounts Available' | translate }} +

@@ -233,6 +236,89 @@ }
+ + @if (currentAccounts?.length > 0) { + + } +
-
\ No newline at end of file +
diff --git a/src/app/clients/clients-view/general-tab/general-tab.component.scss b/src/app/clients/clients-view/general-tab/general-tab.component.scss index d4fb31d2d..11848e360 100644 --- a/src/app/clients/clients-view/general-tab/general-tab.component.scss +++ b/src/app/clients/clients-view/general-tab/general-tab.component.scss @@ -58,9 +58,13 @@ $red: #d93025; background: $white; border-radius: 10px; padding: 18px 20px; - box-shadow: 0 1px 3px rgb(0 0 0 / 8%), 0 1px 2px rgb(0 0 0 / 6%); + 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 rgb(0 0 0 / 10%); @@ -219,6 +223,7 @@ $red: #d93025; } // ── Mat Table Styling ── +/* stylelint-disable selector-no-qualifying-type */ table[mat-table] { width: 100%; border-collapse: collapse; @@ -312,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 { diff --git a/src/app/clients/clients-view/general-tab/general-tab.component.ts b/src/app/clients/clients-view/general-tab/general-tab.component.ts index ed9585096..dbbd1538d 100644 --- a/src/app/clients/clients-view/general-tab/general-tab.component.ts +++ b/src/app/clients/clients-view/general-tab/general-tab.component.ts @@ -228,6 +228,8 @@ export class GeneralTabComponent implements OnDestroy { loanAccounts: any[] = []; /** Savings Accounts Data */ savingAccounts: any[] = []; + /** Current Accounts Data */ + currentAccounts: any[] = []; /** Shares Accounts Data */ shareAccounts: any[] = []; /** Upcoming Charges Data */ @@ -273,6 +275,7 @@ export class GeneralTabComponent implements OnDestroy { (data: { clientAccountsData: any; clientChargesData: any; clientSummary: any; clientCollateralData: any }) => { this.clientAccountData = data.clientAccountsData; this.savingAccounts = data.clientAccountsData?.savingsAccounts ?? []; + this.currentAccounts = data.clientAccountsData?.currentAccounts ?? []; this.loanAccounts = data.clientAccountsData?.loanAccounts ?? []; this.shareAccounts = data.clientAccountsData?.shareAccounts ?? []; diff --git a/src/app/clients/clients-view/identities-tab/identities-tab.component.html b/src/app/clients/clients-view/identities-tab/identities-tab.component.html index 5628ee003..1800736f5 100644 --- a/src/app/clients/clients-view/identities-tab/identities-tab.component.html +++ b/src/app/clients/clients-view/identities-tab/identities-tab.component.html @@ -14,7 +14,13 @@

{{ 'labels.heading.Identities' | translate }}

{{ clientIdentities?.length || 0 }}
-
@@ -35,11 +41,21 @@
{{ identity.documentType?.name }} - + {{ identity.status === 'clientIdentifierStatusType.active' ? 'Active' : 'Inactive' }}
-
@@ -70,13 +86,22 @@
@for (document of identity.documents; track trackByDocumentId($index, document)) { -
+ tabindex="0" + role="button" + [attr.aria-label]="('labels.buttons.Preview' | translate) + ': ' + document.name" + > @if (previewThumbnails[document.id]) { - + } @else {
@@ -85,9 +110,7 @@
{{ document.name }} @if (document.fileName) { - - {{ document.fileName }} - + {{ document.fileName }} }
@if (isPreviewable(document)) { diff --git a/src/app/clients/clients-view/identities-tab/identities-tab.component.scss b/src/app/clients/clients-view/identities-tab/identities-tab.component.scss index b92d2f107..b90577cd5 100644 --- a/src/app/clients/clients-view/identities-tab/identities-tab.component.scss +++ b/src/app/clients/clients-view/identities-tab/identities-tab.component.scss @@ -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,8 +74,17 @@ $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 @@ -82,7 +94,9 @@ $red-light: #fce8e6; border-radius: 10px; overflow: hidden; margin-bottom: 14px; - transition: box-shadow 0.15s, border-color 0.15s; + transition: + box-shadow 0.15s, + border-color 0.15s; position: relative; &::before { @@ -96,7 +110,7 @@ $red-light: #fce8e6; &:hover { border-color: $primary-light; - box-shadow: 0 2px 8px rgba(26, 115, 232, 0.08); + box-shadow: 0 2px 8px rgb(26 115 232 / 8%); } &__header { @@ -126,7 +140,7 @@ $red-light: #fce8e6; justify-content: center; background: linear-gradient(135deg, $primary, #4285f4); border-radius: 8px; - box-shadow: 0 2px 6px rgba(26, 115, 232, 0.25); + box-shadow: 0 2px 6px rgb(26 115 232 / 25%); flex-shrink: 0; } @@ -162,14 +176,18 @@ $red-light: #fce8e6; background: $green-light; color: $green; - &::before { background: $green; } + &::before { + background: $green; + } } &--inactive { background: $red-light; color: $red; - &::before { background: $red; } + &::before { + background: $red; + } } } @@ -189,7 +207,9 @@ $red-light: #fce8e6; background: $red-light !important; } - i { font-size: 13px; } + i { + font-size: 13px; + } } &__body { @@ -214,7 +234,9 @@ $red-light: #fce8e6; padding: 8px 14px; border-bottom: 1px dashed $gray-200; - &:last-child { border-bottom: none; } + &:last-child { + border-bottom: none; + } } &__icon { @@ -257,8 +279,13 @@ $red-light: #fce8e6; } @media (width <= 600px) { - &__row { flex-wrap: wrap; } - &__label { min-width: 0; } + &__row { + flex-wrap: wrap; + } + + &__label { + min-width: 0; + } } } @@ -277,7 +304,9 @@ $red-light: #fce8e6; align-items: center; gap: 6px; - i { color: $primary; } + i { + color: $primary; + } } &__count { @@ -314,7 +343,7 @@ $red-light: #fce8e6; &:hover { border-color: $primary; - box-shadow: 0 2px 8px rgba(26, 115, 232, 0.1); + box-shadow: 0 2px 8px rgb(26 115 232 / 10%); transform: translateY(-1px); } } @@ -369,7 +398,9 @@ $red-light: #fce8e6; align-items: center; gap: 4px; - i { font-size: 9px; } + i { + font-size: 9px; + } } &__preview { @@ -388,7 +419,11 @@ $red-light: #fce8e6; 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; +} diff --git a/src/app/clients/clients-view/identities-tab/identities-tab.component.ts b/src/app/clients/clients-view/identities-tab/identities-tab.component.ts index 56b395755..80f69613d 100644 --- a/src/app/clients/clients-view/identities-tab/identities-tab.component.ts +++ b/src/app/clients/clients-view/identities-tab/identities-tab.component.ts @@ -132,18 +132,15 @@ export class IdentitiesTabComponent implements OnDestroy { }); // Pull display name from parent route's clientViewData - this.route.parent.data - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((data: any) => { - this.clientName = data?.clientViewData?.displayName || ''; - }); + 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]; + const permanent = addresses.find((a) => /permanent/i.test(a?.addressType || '')) || addresses[0]; this.clientAddress = this.formatAddress(permanent); }, error: () => undefined @@ -214,10 +211,7 @@ export class IdentitiesTabComponent implements OnDestroy { const identifierData = { documentTypeId: section.proofId, status: 'Active', - documentKey: - kind === 'id' - ? section.documentKey - : `${section.proof?.name || 'Address Proof'} - ${this.clientId}`, + documentKey: kind === 'id' ? section.documentKey : `${section.proof?.name || 'Address Proof'} - ${this.clientId}`, description: kind === 'address' && section.address ? section.address : description }; diff --git a/src/app/clients/clients-view/kyc-tab/kyc-tab.component.html b/src/app/clients/clients-view/kyc-tab/kyc-tab.component.html index b2b0cef04..ca112928d 100644 --- a/src/app/clients/clients-view/kyc-tab/kyc-tab.component.html +++ b/src/app/clients/clients-view/kyc-tab/kyc-tab.component.html @@ -65,12 +65,22 @@ radio_button_unchecked Pending } - {{ kycData.panVerifiedOn ? formatDate(kycData.panVerifiedOn) : '—' }} + {{ + kycData.panVerifiedOn ? formatDate(kycData.panVerifiedOn) : '—' + }} -
@@ -88,11 +98,18 @@ radio_button_unchecked Pending } - {{ kycData.aadhaarVerifiedOn ? formatDate(kycData.aadhaarVerifiedOn) : '—' }} + {{ + kycData.aadhaarVerifiedOn ? formatDate(kycData.aadhaarVerifiedOn) : '—' + }} @if (!showAadhaarReVerify && !aadhaarOtpSent) { - } @@ -113,12 +130,22 @@ radio_button_unchecked Pending } - {{ kycData.gstinVerifiedOn ? formatDate(kycData.gstinVerifiedOn) : '—' }} + {{ + kycData.gstinVerifiedOn ? formatDate(kycData.gstinVerifiedOn) : '—' + }} -
@@ -138,7 +165,9 @@ radio_button_unchecked Pending } - {{ kycData.enterprisePanVerifiedOn ? formatDate(kycData.enterprisePanVerifiedOn) : '—' }} + {{ + kycData.enterprisePanVerifiedOn ? formatDate(kycData.enterprisePanVerifiedOn) : '—' + }}
} @@ -202,12 +231,27 @@
Enter 12-digit Aadhaar - + -
} @@ -216,32 +260,69 @@ sms OTP sent Enter OTP - + - + -
} @if (verificationError.aadhaar) { -
error_outline {{ verificationError.aadhaar }}
+
+ error_outline {{ verificationError.aadhaar }} +
} @if (verificationError.pan) { -
error_outline {{ verificationError.pan }}
+
+ error_outline {{ verificationError.pan }} +
}
KYC Type - {{ kycData.kycType === 1 ? 'Individual' : kycData.kycType === 2 ? 'Non-Individual' : '—' }} + {{ + kycData.kycType === 1 ? 'Individual' : kycData.kycType === 2 ? 'Non-Individual' : '—' + }}
Risk Category - {{ kycData.riskCategory === 1 ? 'Low' : kycData.riskCategory === 2 ? 'Medium' : kycData.riskCategory === 3 ? 'High' : '—' }} + {{ + kycData.riskCategory === 1 + ? 'Low' + : kycData.riskCategory === 2 + ? 'Medium' + : kycData.riskCategory === 3 + ? 'High' + : '—' + }}
Initiated On @@ -323,9 +404,14 @@
Complete Re-KYC
Updates Last Verified date and sets new Expiry Date as per RBI norms
- } @@ -83,10 +95,12 @@
@@ -97,8 +111,12 @@
@if (nominee.profileImageUrl) { - + } @else { person } @@ -107,14 +125,22 @@ {{ nominee.nomineeDisplayName || nominee.firstName || '—' }} {{ 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 }} + }
@if (nominee.signatureUrl) {
- Signature + Signature
} @if (nominee.nomineeIdentifier) { @@ -124,16 +150,30 @@
- - - + + +
@if (nominee.nomineeIsMinor && (nominee.guardianDisplayName || nominee.guardianName)) {
shield Guardian: {{ nominee.guardianDisplayName || nominee.guardianName }} - @if (nominee.guardianRelationshipName) { ({{ nominee.guardianRelationshipName }}) } + @if (nominee.guardianRelationshipName) { + ({{ nominee.guardianRelationshipName }}) + }
} @@ -143,24 +183,39 @@ Type - @for (opt of nominationTypeOptions; track opt.value) { {{ opt.label }} } + @for (opt of nominationTypeOptions; track opt.value) { + {{ opt.label }} + } Relationship - @for (rel of relationshipOptions; track rel) { {{ rel.name }} } + @for (rel of relationshipOptions; track rel) { + {{ rel.name }} + } Share % - @if (editShare && editShare > maxShareForEdit(i)) { Max {{ maxShareForEdit(i) }}% } + @if (editShare && editShare > maxShareForEdit(i)) { + Max {{ maxShareForEdit(i) }}% + } - - + (click)="saveEdit(nominee, i)" + > + check + +
} @@ -185,17 +240,39 @@ Search by Name, Client ID or Mobile search - + @if (searchResults.length > 0 || searching) {
- @if (searching) {
Searching...
} + @if (searching) { +
+ Searching... +
+ } @for (client of searchResults; track client.id) {
person
{{ client.displayName }} - ID: {{ client.id }} · {{ client.officeName }}@if (client.dateOfBirth) { · DOB: {{ client.dateOfBirth | dateFormat }} }@if (client.fatherSpouseName) { · {{ client.fatherSpouseName }} }@if (client.mobileNo) { · {{ client.mobileNo }} } + ID: {{ client.id }} · {{ client.officeName }} + @if (client.dateOfBirth) { + · DOB: {{ client.dateOfBirth | dateFormat }} + } + @if (client.fatherSpouseName) { + · {{ client.fatherSpouseName }} + } + @if (client.mobileNo) { + · {{ client.mobileNo }} + } +
} @@ -212,32 +289,68 @@
@if (linkedProfileImage) { - - } @else { person } + + } @else { + person + }
{{ selectedClient.displayName }} - ID: {{ selectedClient.id }}@if (selectedClient.mobileNo) { · {{ selectedClient.mobileNo }} } + ID: {{ selectedClient.id }} + @if (selectedClient.mobileNo) { + · {{ selectedClient.mobileNo }} + } +
@if (linkedSignatureImage) {
- Signature + Signature
}
@if (fetchingDetails) { -
Fetching details...
+
+ Fetching details... +
} @if (!fetchingDetails) { @if (selectedClient.dateOfBirth || selectedClient.panNumber || selectedClient.aadhaarMasked) {
- @if (selectedClient.dateOfBirth) { DOB {{ selectedClient.dateOfBirth | dateFormat }} } - @if (selectedClient.panNumber) { PAN {{ selectedClient.panNumber }} @if (linkedKycStatus?.panVerified) { check_circle } } - @if (selectedClient.aadhaarMasked) { Aadhaar {{ selectedClient.aadhaarMasked }} @if (linkedKycStatus?.aadhaarVerified) { check_circle } } + @if (selectedClient.dateOfBirth) { + DOB {{ selectedClient.dateOfBirth | dateFormat }} + } + @if (selectedClient.panNumber) { + PAN {{ selectedClient.panNumber }} + @if (linkedKycStatus?.panVerified) { + check_circle + } + + } + @if (selectedClient.aadhaarMasked) { + Aadhaar {{ selectedClient.aadhaarMasked }} + @if (linkedKycStatus?.aadhaarVerified) { + check_circle + } + + }
} @@ -246,19 +359,25 @@ Nomination Type - @for (opt of nominationTypeOptions; track opt.value) { {{ opt.label }} } + @for (opt of nominationTypeOptions; track opt.value) { + {{ opt.label }} + } Relationship - @for (rel of relationshipOptions; track rel) { {{ rel.name }} } + @for (rel of relationshipOptions; track rel) { + {{ rel.name }} + } Share % - @if (nomineeShare && nomineeShare > maxAllowedShare) { Max {{ maxAllowedShare }}% } + @if (nomineeShare && nomineeShare > maxAllowedShare) { + Max {{ maxAllowedShare }}% + }
Minor @@ -270,11 +389,21 @@ shield Guardian @if (!selectedGuardian) { - + @if (guardianSearchResults.length > 0 || guardianSearching) {
- @if (guardianSearching) {
} + @if (guardianSearching) { +
+ +
+ } @for (client of guardianSearchResults; track client.id) {
person @@ -284,7 +413,12 @@
} } @else { - {{ selectedGuardian.displayName }} + {{ selectedGuardian.displayName }} + }
} @@ -296,11 +430,24 @@
-
} diff --git a/src/app/clients/clients-view/nominee-tab/nominee-tab.component.scss b/src/app/clients/clients-view/nominee-tab/nominee-tab.component.scss index e05ba4652..c5980ff99 100644 --- a/src/app/clients/clients-view/nominee-tab/nominee-tab.component.scss +++ b/src/app/clients/clients-view/nominee-tab/nominee-tab.component.scss @@ -8,189 +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: rgb(255 255 255 / 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; } + 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-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; } + 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; - &.active { border-color: $primary; background: $primary-light; color: $primary; - .nom-type-chip__count { background: $primary; color: #fff; } + 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; } - &: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-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; } + 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__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; } + 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; + 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__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; +.nom-card__delete { + color: #94a3b8 !important; - button { width: 32px !important; height: 32px !important; line-height: 32px !important; - mat-icon { font-size: 18px; } + &:hover { + color: #ef4444 !important; } } -.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 rgb(0 0 0 / 10%); 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-section { + display: flex; + align-items: center; + gap: 8px; + flex-wrap: wrap; + margin-bottom: 8px; +} -.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-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 rgb(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; + } +} diff --git a/src/app/clients/clients-view/nominee-tab/nominee-tab.component.ts b/src/app/clients/clients-view/nominee-tab/nominee-tab.component.ts index 32b24cb34..eb6e4a236 100644 --- a/src/app/clients/clients-view/nominee-tab/nominee-tab.component.ts +++ b/src/app/clients/clients-view/nominee-tab/nominee-tab.component.ts @@ -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' }); } diff --git a/src/app/clients/clients-view/personal-data-tab/personal-data-tab.component.scss b/src/app/clients/clients-view/personal-data-tab/personal-data-tab.component.scss index 0bcbdec8c..1ca6f0ca9 100644 --- a/src/app/clients/clients-view/personal-data-tab/personal-data-tab.component.scss +++ b/src/app/clients/clients-view/personal-data-tab/personal-data-tab.component.scss @@ -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; @@ -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; @@ -230,7 +232,9 @@ $teal: #00897b; padding: 3px 12px; border-radius: 12px; - i { font-size: 12px; } + i { + font-size: 12px; + } &--verified { background: #ecfdf5; diff --git a/src/app/clients/clients.service.ts b/src/app/clients/clients.service.ts index 8937f50ae..de9b603a2 100644 --- a/src/app/clients/clients.service.ts +++ b/src/app/clients/clients.service.ts @@ -329,21 +329,28 @@ export class ClientsService { * 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 { + getCkycRecordMaster( + filters: { + officeId?: number | string; + productId?: number | string; + fromDate?: string; + toDate?: string; + missing?: 'photo' | 'signature' | 'id' | 'address' | 'any'; + limit?: number; + offset?: number; + } = {} + ): Observable { let params = new HttpParams(); - Object.entries(filters).forEach(([key, value]) => { - if (value !== null && value !== undefined && value !== '') { - params = params.set(key, String(value)); + 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 }); } @@ -359,18 +366,25 @@ export class ClientsService { * 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 { + getCkycBatches( + filters: { + fromDate?: string; + toDate?: string; + uploadedToCersai?: boolean; + groupByDate?: boolean; + } = {} + ): Observable { let params = new HttpParams(); - Object.entries(filters).forEach(([key, value]) => { - if (value !== null && value !== undefined && value !== '') { - params = params.set(key, String(value)); + 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 }); } diff --git a/src/app/clients/create-client/create-client.component.scss b/src/app/clients/create-client/create-client.component.scss index dfdfc354c..b42a06104 100644 --- a/src/app/clients/create-client/create-client.component.scss +++ b/src/app/clients/create-client/create-client.component.scss @@ -223,7 +223,9 @@ flex-shrink: 0; } - span { flex: 1; } + span { + flex: 1; + } button { flex-shrink: 0; diff --git a/src/app/core/shell/toolbar/toolbar.component.html b/src/app/core/shell/toolbar/toolbar.component.html index ebca2c6b6..5e59a8253 100644 --- a/src/app/core/shell/toolbar/toolbar.component.html +++ b/src/app/core/shell/toolbar/toolbar.component.html @@ -16,14 +16,27 @@
-
 LIVE
+
+
+  LIVE +
Branch: HO
-
Date: {{ today | date:'dd MMM yyyy' }}
-
Last login: {{ lastLoginTime }}
+
+ Date: {{ today | date: 'dd MMM yyyy' }} +
+
+ Last login: {{ lastLoginTime }} +
-
SSL/TLS 256-bit
-
ISO 27001 Certified
-
Session {{ sessionTimer }}
+
+ SSL/TLS 256-bit +
+
+ ISO 27001 Certified +
+
+ Session {{ sessionTimer }} +
@@ -43,7 +56,11 @@
- +
@@ -66,7 +83,11 @@
Transactions
- + Cash / Clearing / Transfer @@ -114,7 +135,12 @@
Customer
-
+ Client Master @@ -131,10 +157,18 @@ @@ -185,27 +223,66 @@
@@ -348,7 +425,11 @@ -
@@ -364,7 +445,11 @@ - @@ -377,7 +462,11 @@ - - + + + - - - + + + DIGITAL - @if (currentAccountFormValid) { + + +
+ + +
+
+ + {{ 'labels.inputs.PREVIEW' | translate }} diff --git a/src/app/current-accounts/create-current-account/create-current-account.component.scss b/src/app/current-accounts/create-current-account/create-current-account.component.scss index 4d4789083..94c2d9d26 100644 --- a/src/app/current-accounts/create-current-account/create-current-account.component.scss +++ b/src/app/current-accounts/create-current-account/create-current-account.component.scss @@ -27,11 +27,14 @@ .mat-step-content { padding: 1.5rem; margin: 1rem 0; + overflow: visible !important; } } // Form field spacing within steps :host ::ng-deep mat-step-content { + overflow: visible !important; + mat-form-field { margin-bottom: 1rem; width: 100%; diff --git a/src/app/current-accounts/create-current-account/create-current-account.component.ts b/src/app/current-accounts/create-current-account/create-current-account.component.ts index 89e7978e6..4c4c17a11 100644 --- a/src/app/current-accounts/create-current-account/create-current-account.component.ts +++ b/src/app/current-accounts/create-current-account/create-current-account.component.ts @@ -12,17 +12,25 @@ import { Router, ActivatedRoute } from '@angular/router'; /** Custom Components */ import { CurrentAccountDetailsStepComponent } from '../current-account-stepper/current-account-details-step/current-account-details-step.component'; -import { CurrentAccountTermsStepComponent } from '../current-account-stepper/current-account-terms-step/current-account-terms-step.component'; -import { CurrentAccountChargesStepComponent } from '../current-account-stepper/current-account-charges-step/current-account-charges-step.component'; +import { CurrentAccountCurrencyStepComponent } from '../current-account-stepper/current-account-currency-step/current-account-currency-step.component'; +import { CurrentAccountDigitalStepComponent } from '../current-account-stepper/current-account-digital-step/current-account-digital-step.component'; /** Custom Services */ import { CurrentAccountsService } from '../current-accounts.service'; import { SettingsService } from 'app/settings/settings.service'; import { Dates } from 'app/core/utils/dates'; -import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper'; +import { + MatStepper, + MatStepperIcon, + MatStep, + MatStepLabel, + MatStepperNext, + MatStepperPrevious +} from '@angular/material/stepper'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { CurrentAccountPreviewStepComponent } from '../current-account-stepper/current-account-preview-step/current-account-preview-step.component'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { FormGroup } from '@angular/forms'; /** * Create Current Account Component @@ -35,12 +43,14 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; ...STANDALONE_SHARED_IMPORTS, MatStepper, MatStepperIcon, + MatStepperNext, + MatStepperPrevious, FaIconComponent, MatStep, MatStepLabel, CurrentAccountDetailsStepComponent, - CurrentAccountTermsStepComponent, - CurrentAccountChargesStepComponent, + CurrentAccountCurrencyStepComponent, + CurrentAccountDigitalStepComponent, CurrentAccountPreviewStepComponent ] }) @@ -56,24 +66,15 @@ export class CreateCurrentAccountComponent { /** Current Account Product Template */ currentAccountProductTemplate: any; - /** Current Account Details Step */ @ViewChild(CurrentAccountDetailsStepComponent, { static: true }) currentAccountDetailsStep: CurrentAccountDetailsStepComponent; - /** Current Account Terms Step */ - @ViewChild(CurrentAccountTermsStepComponent, { static: true }) - currentAccountTermsStep: CurrentAccountTermsStepComponent; - /** Current Account Charges Step */ - @ViewChild(CurrentAccountChargesStepComponent, { static: true }) - currentAccountChargesStep: CurrentAccountChargesStepComponent; - /** - * Fetches current account template from `resolve` - * @param {ActivatedRoute} route Activated Route - * @param {Router} router Router - * @param {Dates} dateUtils Date Utils - * @param {CurrentAccountsService} currentAccountsService Current Accounts Service - * @param {SettingsService} settingsService Settings Service - */ + @ViewChild(CurrentAccountCurrencyStepComponent) + currentAccountCurrencyStep: CurrentAccountCurrencyStepComponent; + + @ViewChild(CurrentAccountDigitalStepComponent) + currentAccountDigitalStep: CurrentAccountDigitalStepComponent; + constructor() { this.route.data.subscribe((data: { currentAccountTemplate: any }) => { this.currentAccountTemplate = data.currentAccountTemplate; @@ -82,7 +83,6 @@ export class CreateCurrentAccountComponent { /** * Sets current account product template. - * @param {any} $event API response */ setTemplate($event: any) { this.currentAccountProductTemplate = $event; @@ -95,60 +95,77 @@ export class CreateCurrentAccountComponent { return this.currentAccountDetailsStep.currentAccountDetailsForm; } - /** - * Retrieves current account terms form. - */ - get currentAccountTermsForm() { - return this.currentAccountTermsStep.currentAccountTermsForm; + /** Stub forms needed by preview step inputs */ + get currentAccountTermForm(): FormGroup { + return new FormGroup({}); } - /** - * Checks validity of overall current account form. - */ - get currentAccountFormValid() { - return this.currentAccountDetailsForm.valid && this.currentAccountTermsForm.valid; + get currentAccountSettingsForm(): FormGroup { + return new FormGroup({}); + } + + get currentAccountTermsForm(): FormGroup { + return new FormGroup({}); } - /** - * Retrieves current account object. - */ get currentAccount() { + return { ...this.currentAccountDetailsStep.currentAccountDetails }; + } + + get currentAccountDigital() { + return this.currentAccountDigitalStep?.currentAccountDigital ?? null; + } + + get currentAccountNominee() { + return this.currentAccountCurrencyStep?.nomineeData ?? null; + } + + get currentAccountCurrencyFlags() { return { - ...this.currentAccountDetailsStep.currentAccountDetails, - ...this.currentAccountTermsStep.currentAccountTerms, - ...this.currentAccountChargesStep.currentAccountCharges + balanceSwapOption: this.currentAccountCurrencyStep?.balanceSwapOption ?? false, + msme: this.currentAccountCurrencyStep?.msme ?? false, + autoDebitFacility: this.currentAccountCurrencyStep?.autoDebitFacility ?? false }; } /** - * Creates a new share account. + * Creates a new current account. */ submit() { - // TODO: Update once language and date settings are setup const locale = this.settingsService.language.code; const dateFormat = this.settingsService.dateFormat; const monthDayFormat = 'dd MMMM'; - const currentAccount = { - ...this.currentAccount, - charges: this.currentAccount.charges.map((charge: any) => ({ - chargeId: charge.id, - amount: charge.amount, - dueDate: charge.dueDate ? this.dateUtils.formatDate(charge.dueDate, dateFormat) : charge.dueDate, - feeOnMonthDay: charge.feeOnMonthDay - ? this.dateUtils.formatDate(charge.feeOnMonthDay, monthDayFormat) - : charge.feeOnMonthDay, - feeInterval: charge.feeInterval - })), - submittedOnDate: this.dateUtils.formatDate(this.currentAccount.submittedOnDate, dateFormat), + const details = this.currentAccount; + + const currentAccount: any = { + ...details, + ...this.currentAccountCurrencyStep.currencyStepData, + ...(this.currentAccountDigitalStep?.currentAccountDigital ?? {}), + charges: [], + submittedOnDate: this.dateUtils.formatDate(details.submittedOnDate, dateFormat), dateFormat, monthDayFormat, locale }; + + if (details.establishmentDate) { + currentAccount.establishmentDate = this.dateUtils.formatDate(details.establishmentDate, dateFormat); + } + if (details.startOperationDate) { + currentAccount.startOperationDate = this.dateUtils.formatDate(details.startOperationDate, dateFormat); + } + + // strip UI-only flags not needed by backend + delete currentAccount.isNonIndividualClient; + if (this.currentAccountTemplate.clientId) { currentAccount.clientId = this.currentAccountTemplate.clientId; } else { currentAccount.groupId = this.currentAccountTemplate.groupId; } + + console.log('Current Account Payload:', JSON.stringify(currentAccount, null, 2)); + this.currentAccountsService.createCurrentAccount(currentAccount).subscribe((response: any) => { this.router.navigate( [ diff --git a/src/app/current-accounts/current-account-stepper/current-account-charges-step/current-account-charges-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-charges-step/current-account-charges-step.component.ts index 8ac930f21..c9c5a906d 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-charges-step/current-account-charges-step.component.ts +++ b/src/app/current-accounts/current-account-stepper/current-account-charges-step/current-account-charges-step.component.ts @@ -7,7 +7,7 @@ */ /** Angular Imports */ -import { Component, OnInit, OnChanges, Input, inject } from '@angular/core'; +import { Component, OnInit, Input, inject } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { UntypedFormControl } from '@angular/forms'; @@ -69,13 +69,27 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; FormatNumberPipe ] }) -export class CurrentAccountChargesStepComponent implements OnInit, OnChanges { +export class CurrentAccountChargesStepComponent implements OnInit { private dialog = inject(MatDialog); private dateUtils = inject(Dates); private translateService = inject(TranslateService); /** Current Account Product Template */ - @Input() currentAccountProductTemplate: any; + private _currentAccountProductTemplate: any; + @Input() + set currentAccountProductTemplate(value: any) { + this._currentAccountProductTemplate = value; + if (value) { + this.chargeData = value.chargeOptions || []; + // Only pre-populate from product template for new accounts + if (!this.currentAccountTemplate?.id) { + this.chargesDataSource = (value.charges || []).map((charge: any) => ({ ...charge, id: charge.chargeId })); + } + } + } + get currentAccountProductTemplate(): any { + return this._currentAccountProductTemplate; + } /** Current Account Template */ @Input() currentAccountTemplate: any; /** Currency Code */ @@ -88,8 +102,6 @@ export class CurrentAccountChargesStepComponent implements OnInit, OnChanges { chargesDataSource: {}[] = []; /** Component is pristine if there has been no changes by user interaction */ pristine = true; - /** For Edit Current Account Form */ - isChargesPatched = false; /** Display columns for charges table */ displayedColumns: string[] = [ 'name', @@ -114,22 +126,10 @@ export class CurrentAccountChargesStepComponent implements OnInit, OnChanges { ]; ngOnInit() { - if (this.currentAccountTemplate) { - if (!this.isChargesPatched && this.currentAccountTemplate.charges) { - this.chargesDataSource = - this.currentAccountProductTemplate.charges.map((charge: any) => ({ ...charge, id: charge.chargeId })) || []; - this.isChargesPatched = true; - } else { - this.chargesDataSource = []; - } - } - } - - ngOnChanges() { - if (this.currentAccountProductTemplate) { - this.chargeData = this.currentAccountProductTemplate.chargeOptions; + if (this.currentAccountTemplate?.id && this.currentAccountTemplate?.charges) { + // Edit mode: load saved charges from existing account this.chargesDataSource = - this.currentAccountProductTemplate.charges.map((charge: any) => ({ ...charge, id: charge.chargeId })) || []; + this.currentAccountTemplate.charges.map((charge: any) => ({ ...charge, id: charge.chargeId })) || []; } } diff --git a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html new file mode 100644 index 000000000..935e312dd --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.html @@ -0,0 +1,79 @@ + + +
+
+ + Currency & Account Status Configuration +
+
+
+ + Currency + + Inherited from selected product + + + + Decimal Places + + Inherited from selected product + +
+ +
+
+

Balance Swap Option

+

Allows balance to be swapped or transferred between linked accounts as per product configuration.

+
+ +
+ +
+
+

MSME

+

Account is categorized under the Micro, Small and Medium Enterprises segment.

+
+ +
+ +
+
+

Auto Debit Facility

+

Enable automatic debit instructions for recurring payments linked to this account.

+
+ +
+ +
+
+

Dormancy Alert

+

Enable automated notifications when an account becomes inactive based on product rules.

+
+ + {{ currentAccountProductTemplate?.dormancyAlert ? 'Yes' : 'No' }} + +
+ @if (currentAccountProductTemplate?.dormancyAlert) { + + } + +
+
+

Nomination Facility

+

Allow account holders to designate individuals to receive funds in the event of account holder's death.

+
+ + {{ currentAccountProductTemplate?.nominationFacility ? 'Yes' : 'No' }} + +
+ @if (currentAccountProductTemplate?.nominationFacility) { + + } +
+
diff --git a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.scss b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.scss new file mode 100644 index 000000000..68c9a94a2 --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.scss @@ -0,0 +1,130 @@ +/** + * 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/. + */ + +$accent: #0ea5e9; +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-900: #202124; + +.sac-section { + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; + background: #fff; + + & + & { + margin-top: 16px; + } + + &__header { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 24px; + background: $gray-50; + border-bottom: 1px solid $gray-200; + + i { + font-size: 14px; + color: $accent; + } + + span { + margin: 0; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + } +} + +.sac-body { + background: #fff; +} + +.sac-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px 16px; + padding: 16px 24px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.sac-field { + width: 100%; +} + +.sac-alert { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + padding: 18px 24px; + border-top: 1px solid $border; + background: #f8fafc; + + @media (width <= 760px) { + flex-direction: column; + align-items: flex-start; + } + + &__content { + h4 { + margin: 0 0 6px; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + + p { + margin: 0; + font-size: 12px; + line-height: 1.5; + color: #64748b; + max-width: 540px; + } + } +} + +.sac-badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 48px; + padding: 4px 14px; + border-radius: 999px; + font-size: 12px; + font-weight: 700; + background: #fee2e2; + color: #b91c1c; + border: 1px solid #fecaca; + + &--yes { + background: #dcfce7; + color: #15803d; + border-color: #bbf7d0; + } +} + +.sac-alert--editable { + border-color: #bae6fd; + background: #f0f9ff; + + &:hover { + border-color: #7dd3fc; + box-shadow: 0 2px 8px rgb(2 132 199 / 10%); + } + + h4 { + color: #0369a1; + } +} diff --git a/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts new file mode 100644 index 000000000..c3cae7dce --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-currency-step/current-account-currency-step.component.ts @@ -0,0 +1,60 @@ +/** + * 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, OnChanges, SimpleChanges } from '@angular/core'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { NomineeFormComponent } from 'app/shared/nominee-form/nominee-form.component'; +import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dormancy-alert-panel.component'; +import { MatSlideToggle } from '@angular/material/slide-toggle'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'mifosx-current-account-currency-step', + templateUrl: './current-account-currency-step.component.html', + styleUrls: ['./current-account-currency-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + NomineeFormComponent, + DormancyAlertPanelComponent, + MatSlideToggle, + FormsModule + ] +}) +export class CurrentAccountCurrencyStepComponent implements OnChanges { + @Input() currentAccountProductTemplate: any; + + nomineeData: any = null; + + // Editable feature toggles (pre-filled from product, overridable by user) + balanceSwapOption = false; + msme = false; + autoDebitFacility = false; + + ngOnChanges(changes: SimpleChanges): void { + if (changes['currentAccountProductTemplate']?.currentValue) { + const pt = changes['currentAccountProductTemplate'].currentValue; + this.balanceSwapOption = !!pt.balanceSwapOption; + this.msme = !!pt.msme; + this.autoDebitFacility = !!pt.autoDebitFacility; + } + } + + onNomineeChange(event: any): void { + this.nomineeData = event; + } + + /** Called by parent to collect currency step values */ + get currencyStepData() { + return { + balanceSwapOption: this.balanceSwapOption, + msme: this.msme, + autoDebitFacility: this.autoDebitFacility, + ...(this.nomineeData ? { nominee: this.nomineeData } : {}) + }; + } +} diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-beneficiary-dialog/add-beneficiary-dialog.component.html b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-beneficiary-dialog/add-beneficiary-dialog.component.html new file mode 100644 index 000000000..1b0c61fc7 --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-beneficiary-dialog/add-beneficiary-dialog.component.html @@ -0,0 +1,190 @@ + + + +
+
+ person_pin +
+
+

Add Beneficiary / Nominee

+

Register a nominee for this account as per Indian banking norms

+
+ +
+ + +
+ +
+ + +
+ + + @if (mode === 'search') { +
+ + Search Client by Name / CIF + search + + @if (isSearching) { + + } + @if (selectedClient) { + + } + + + @if (searchResults.length > 0) { +
+ @for (client of searchResults; track client) { +
+
{{ (client.displayName || '?')[0].toUpperCase() }}
+
+ {{ client.displayName }} + + @if (client.cifNumber) { + {{ client.cifNumber }} + } + @if (client.officeName) { + {{ client.officeName }} + } + +
+ chevron_right +
+ } +
+ } + + @if (selectedClient) { +
+
{{ (selectedClient.displayName || '?')[0].toUpperCase() }}
+
+ {{ selectedClient.displayName }} + {{ selectedClient.cifNumber || selectedClient.accountNumber }} +
+ Linked +
+ } +
+ } + + + + Nominee / Beneficiary Name * + + Name is required + + + +
+ + Relationship * + + @for (r of relationships; track r) { + {{ r }} + } + + Relationship is required + + + + Date of Birth + + + + +
+ + +
+ + Share % * + + % + Enter 1–100 + +
+ @for (p of [25, 33, 50, 100]; track p) { + + } +
+
+ + +
+ + Nominee is a Minor (below 18 years) + +
+ + + @if (isMinor) { +
+
+ supervisor_account + Guardian Details +
+
+ + Guardian Name + + + + Guardian Relationship + + @for (r of guardianRelationships; track r) { + {{ r }} + } + + +
+
+ } +
+
+ + + + + diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-beneficiary-dialog/add-beneficiary-dialog.component.scss b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-beneficiary-dialog/add-beneficiary-dialog.component.scss new file mode 100644 index 000000000..4c9e71b0c --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-beneficiary-dialog/add-beneficiary-dialog.component.scss @@ -0,0 +1,415 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +$primary: #1a56db; +$primary-light: #eff6ff; +$primary-border: #bfdbfe; +$success: #057a55; +$success-light: #f3faf7; +$success-border: #84e1bc; +$orange: #d03801; +$orange-light: #fff8f1; +$orange-border: #fcd9bd; +$border: #e5e7eb; +$text: #111827; +$muted: #6b7280; +$radius: 10px; + +mifosx-add-beneficiary-dialog { + display: block; +} + +// Header +.abd-header { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 18px 20px 14px; + border-bottom: 1px solid $border; + background: linear-gradient(135deg, #f0f7ff 0%, #fff 60%); + + &__icon { + width: 42px; + height: 42px; + border-radius: 10px; + background: $primary; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + mat-icon { + color: #fff; + font-size: 22px; + width: 22px; + height: 22px; + } + } + + &__text { + flex: 1; + } + + &__title { + margin: 0; + font-size: 15px; + font-weight: 700; + color: $text; + } + + &__sub { + margin: 3px 0 0; + font-size: 11.5px; + color: $muted; + } + + &__close { + color: $muted; + margin: -4px -8px 0 0; + } +} + +// Form +.abd-form { + display: flex; + flex-direction: column; + gap: 0; + width: 500px; + max-width: 100%; + padding: 4px 0; +} + +.abd-full-width { + width: 100%; +} + +.abd-mt { + margin-top: 8px; +} + +// Mode toggle +.abd-mode-toggle { + display: flex; + gap: 8px; + margin: 14px 0 10px; + background: #f9fafb; + border: 1px solid $border; + border-radius: 10px; + padding: 5px; +} + +.abd-mode-btn { + flex: 1; + display: flex; + align-items: center; + justify-content: center; + gap: 6px; + padding: 8px 14px; + border-radius: 8px; + border: 1.5px solid transparent; + background: transparent; + font-size: 13px; + font-weight: 600; + color: $muted; + cursor: pointer; + transition: all 0.15s; + + mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + } + + &--active { + background: #fff; + border-color: $primary-border; + color: $primary; + box-shadow: 0 1px 4px rgb(0 0 0 / 8%); + } +} + +// Search +.abd-search-section { + position: relative; +} + +.abd-prefix-icon { + font-size: 18px; + width: 18px; + height: 18px; + color: $muted; + margin-right: 4px; +} + +.abd-search-results { + position: absolute; + z-index: 200; + top: calc(100% - 8px); + left: 0; + right: 0; + background: #fff; + border: 1px solid $border; + border-radius: $radius; + box-shadow: 0 8px 24px rgb(0 0 0 / 12%); + max-height: 220px; + overflow-y: auto; +} + +.abd-result-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + cursor: pointer; + border-bottom: 1px solid #f3f4f6; + transition: background 0.1s; + + &:last-child { + border-bottom: none; + } + + &:hover { + background: $primary-light; + } +} + +.abd-result-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background: linear-gradient(135deg, #6366f1, $primary); + color: #fff; + font-size: 13px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.abd-result-body { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; +} + +.abd-result-name { + font-size: 13px; + font-weight: 600; + color: $text; +} + +.abd-result-meta { + display: flex; + gap: 5px; + flex-wrap: wrap; +} + +.abd-chip { + font-size: 10px; + padding: 1px 7px; + border-radius: 999px; + background: #f3f4f6; + color: $muted; + font-weight: 500; +} + +.abd-result-arrow { + font-size: 18px; + width: 18px; + height: 18px; + color: $primary; + opacity: 0.4; + transition: all 0.1s; + + .abd-result-item:hover & { + opacity: 1; + transform: translateX(2px); + } +} + +// Selected card +.abd-selected-card { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + background: $success-light; + border: 1.5px solid $success-border; + border-radius: $radius; + margin-top: 6px; +} + +.abd-selected-avatar { + width: 36px; + height: 36px; + border-radius: 50%; + background: linear-gradient(135deg, #10b981, $success); + color: #fff; + font-size: 14px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.abd-selected-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; +} + +.abd-selected-name { + font-size: 13px; + font-weight: 700; + color: #065f46; +} + +.abd-selected-sub { + font-size: 11px; + color: #047857; +} + +.abd-badge { + font-size: 10px; + font-weight: 700; + padding: 3px 10px; + border-radius: 999px; + text-transform: uppercase; + letter-spacing: 0.04em; + + &--linked { + background: $success; + color: #fff; + } +} + +// Row layout +.abd-row { + display: flex; + gap: 12px; + margin-top: 8px; + + @media (width <= 500px) { + flex-direction: column; + gap: 0; + } +} + +.abd-flex { + flex: 1; +} + +// Share % +.abd-share-row { + display: flex; + align-items: flex-start; + gap: 12px; + margin-top: 8px; +} + +.abd-share-field { + flex: 0 0 150px; +} + +.abd-pct { + font-size: 15px; + font-weight: 600; + color: $muted; + margin-right: 4px; +} + +.abd-quick-pct { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding-top: 10px; +} + +.abd-pct-chip { + padding: 5px 12px; + border-radius: 8px; + border: 1.5px solid $border; + background: #fff; + font-size: 12px; + font-weight: 600; + color: $muted; + cursor: pointer; + transition: all 0.12s; + + &:hover { + border-color: $primary; + background: $primary-light; + color: $primary; + } +} + +// Required star +.abd-required { + color: #e02424; +} + +// Minor toggle +.abd-minor-toggle { + margin-top: 14px; + padding: 12px 14px; + background: #fffbeb; + border: 1px solid #fde68a; + border-radius: 8px; +} + +// Guardian box +.abd-guardian-box { + margin-top: 10px; + padding: 14px; + background: $orange-light; + border: 1.5px solid $orange-border; + border-radius: $radius; +} + +.abd-guardian-label { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + font-weight: 700; + color: $orange; + text-transform: uppercase; + letter-spacing: 0.04em; + margin-bottom: 10px; + + mat-icon { + font-size: 16px; + width: 16px; + height: 16px; + color: $orange; + } +} + +// Actions +.abd-actions { + padding: 12px 20px 16px !important; + border-top: 1px solid $border; + gap: 10px; + justify-content: flex-end !important; +} + +.abd-btn-add { + border-radius: 8px !important; + padding: 0 18px !important; + gap: 4px; + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + } +} diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-beneficiary-dialog/add-beneficiary-dialog.component.ts b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-beneficiary-dialog/add-beneficiary-dialog.component.ts new file mode 100644 index 000000000..bd691bbdf --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-beneficiary-dialog/add-beneficiary-dialog.component.ts @@ -0,0 +1,167 @@ +/** + * 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, OnDestroy, ViewEncapsulation } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; +import { MatDialogRef, MatDialogContent, MatDialogActions } from '@angular/material/dialog'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { MatIcon } from '@angular/material/icon'; +import { MatSlideToggle } from '@angular/material/slide-toggle'; +import { ClientsService } from 'app/clients/clients.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-add-beneficiary-dialog', + templateUrl: './add-beneficiary-dialog.component.html', + styleUrl: './add-beneficiary-dialog.component.scss', + encapsulation: ViewEncapsulation.None, + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatDialogContent, + MatDialogActions, + MatProgressSpinner, + MatIcon, + MatSlideToggle, + ReactiveFormsModule + ] +}) +export class AddBeneficiaryDialogComponent implements OnDestroy { + private fb = inject(UntypedFormBuilder); + private clientsService = inject(ClientsService); + dialogRef = inject(MatDialogRef); + + /** Toggle between linked client and manual entry */ + mode: 'search' | 'manual' = 'search'; + + form: UntypedFormGroup = this.fb.group({ + searchText: [''], + name: [ + '', + Validators.required + ], + relationship: [ + '', + Validators.required + ], + dateOfBirth: [null], + sharePercentage: [ + null, + [ + Validators.required, + Validators.min(1), + Validators.max(100) + ] + ], + isMinor: [false], + guardianName: [''], + guardianRelationship: [''] + }); + + searchResults: any[] = []; + selectedClient: any = null; + isSearching = false; + private searchTimer: any = null; + + readonly relationships = [ + 'Spouse', + 'Son', + 'Daughter', + 'Father', + 'Mother', + 'Brother', + 'Sister', + 'Grandson', + 'Granddaughter', + 'Father-in-law', + 'Mother-in-law', + 'Other' + ]; + + readonly guardianRelationships = [ + 'Father', + 'Mother', + 'Guardian', + 'Natural Guardian' + ]; + + get isMinor(): boolean { + return this.form.get('isMinor').value; + } + + switchMode(m: 'search' | 'manual'): void { + this.mode = m; + this.selectedClient = null; + this.searchResults = []; + this.form.patchValue({ searchText: '', name: '' }); + } + + onSearchInput(value: string): void { + if (this.searchTimer) clearTimeout(this.searchTimer); + this.selectedClient = null; + if (!value || value.trim().length < 2) { + this.searchResults = []; + return; + } + this.isSearching = true; + this.searchTimer = setTimeout(() => { + this.clientsService.searchByText(value.trim(), 0, 10).subscribe({ + next: (res: any) => { + this.searchResults = res?.content || []; + this.isSearching = false; + }, + error: () => { + this.searchResults = []; + this.isSearching = false; + } + }); + }, 400); + } + + selectClient(client: any): void { + this.selectedClient = client; + this.form.patchValue({ + searchText: client.displayName, + name: client.displayName + }); + this.searchResults = []; + } + + clearSelection(): void { + this.selectedClient = null; + this.form.patchValue({ searchText: '', name: '' }); + this.searchResults = []; + } + + confirm(): void { + if ( + this.form.get('name').invalid || + this.form.get('relationship').invalid || + this.form.get('sharePercentage').invalid + ) + return; + const v = this.form.getRawValue(); + this.dialogRef.close({ + clientId: this.selectedClient?.id || null, + name: v.name, + relationship: v.relationship, + dateOfBirth: v.dateOfBirth, + sharePercentage: v.sharePercentage, + isMinor: v.isMinor, + guardianName: v.isMinor ? v.guardianName : null, + guardianRelationship: v.isMinor ? v.guardianRelationship : null + }); + } + + cancel(): void { + this.dialogRef.close(null); + } + + ngOnDestroy(): void { + if (this.searchTimer) clearTimeout(this.searchTimer); + } +} diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.html b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.html new file mode 100644 index 000000000..29ffc269e --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.html @@ -0,0 +1,135 @@ + + + +
+
+ person_add +
+
+

Add Related Client / Partner

+

Search and link an existing client with their ownership share

+
+ +
+ + +
+ +
+ 1 + Select Client +
+ +
+ + Search by Name / CIF / Account No + search + + @if (isSearching) { + + } + @if (selectedClient) { + + } + + + + @if (searchResults.length > 0) { +
+ @for (client of searchResults; track client) { +
+
+ {{ (client.displayName || '?')[0].toUpperCase() }} +
+
+ {{ client.displayName }} + + @if (client.cifNumber) { + {{ client.cifNumber }} + } + @if (client.officeName) { + {{ client.officeName }} + } + @if (client.status?.value) { + + {{ client.status.value }} + + } + +
+ chevron_right +
+ } +
+ } +
+ + + @if (selectedClient) { +
+
+ {{ (selectedClient.displayName || '?')[0].toUpperCase() }} +
+
+ {{ selectedClient.displayName }} + {{ selectedClient.cifNumber || selectedClient.accountNumber || 'N/A' }} +
+ Selected +
+ } + + +
+ 2 + Ownership Share +
+ + +
+
+ + + + + diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.scss b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.scss new file mode 100644 index 000000000..3e9fcdc7e --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.scss @@ -0,0 +1,359 @@ +/** + * 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/. + */ + +// ── Tokens ─────────────────────────────────────────────────────────────────── +$primary: #2563eb; +$primary-light: #eff6ff; +$primary-border: #bfdbfe; +$success: #16a34a; +$success-light: #f0fdf4; +$success-border: #bbf7d0; +$border: #e5e7eb; +$text: #111827; +$text-muted: #6b7280; +$radius: 10px; + +// ── Dialog wrapper ──────────────────────────────────────────────────────────── +mifosx-add-related-client-dialog { + display: block; +} + +// ── Header ──────────────────────────────────────────────────────────────────── +.arc-header { + display: flex; + align-items: flex-start; + gap: 12px; + padding: 20px 20px 14px; + border-bottom: 1px solid $border; + + &__icon { + width: 40px; + height: 40px; + border-radius: 10px; + background: $primary-light; + border: 1px solid $primary-border; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + mat-icon { + color: $primary; + font-size: 20px; + width: 20px; + height: 20px; + } + } + + &__text { + flex: 1; + } + + &__title { + margin: 0; + font-size: 15px; + font-weight: 700; + color: $text; + line-height: 1.3; + } + + &__sub { + margin: 3px 0 0; + font-size: 12px; + color: $text-muted; + } + + &__close { + margin-top: -4px; + margin-right: -8px; + color: $text-muted; + } +} + +// ── Dialog content ──────────────────────────────────────────────────────────── +.arc-dialog-form { + display: flex; + flex-direction: column; + gap: 0; + width: 440px; + max-width: 100%; + padding-top: 4px; +} + +// ── Step labels ─────────────────────────────────────────────────────────────── +.arc-step-label { + display: flex; + align-items: center; + gap: 8px; + margin: 16px 0 10px; + font-size: 12px; + font-weight: 700; + color: $text-muted; + text-transform: uppercase; + letter-spacing: 0.04em; + + &--mt { + margin-top: 20px; + } +} + +.arc-step-badge { + width: 20px; + height: 20px; + border-radius: 50%; + background: $primary; + color: #fff; + font-size: 11px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +// ── Search ──────────────────────────────────────────────────────────────────── +.arc-full-width { + width: 100%; +} + +.arc-search-section { + position: relative; +} + +.arc-search-icon { + font-size: 18px; + width: 18px; + height: 18px; + color: $text-muted; + margin-right: 4px; +} + +.arc-search-results { + position: absolute; + z-index: 200; + top: calc(100% - 8px); + left: 0; + right: 0; + background: #fff; + border: 1px solid $border; + border-radius: $radius; + box-shadow: 0 8px 24px rgb(0 0 0 / 12%); + max-height: 240px; + overflow-y: auto; +} + +.arc-search-result-item { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 14px; + cursor: pointer; + border-bottom: 1px solid #f3f4f6; + transition: background 0.12s; + + &:last-child { + border-bottom: none; + } + + &:hover { + background: $primary-light; + + .arc-result-arrow { + opacity: 1; + transform: translateX(2px); + } + } +} + +.arc-result-avatar { + width: 34px; + height: 34px; + border-radius: 50%; + background: linear-gradient(135deg, #6366f1, #2563eb); + color: #fff; + font-size: 13px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.arc-result-body { + flex: 1; + display: flex; + flex-direction: column; + gap: 3px; +} + +.arc-result-name { + font-size: 13px; + font-weight: 600; + color: $text; +} + +.arc-result-meta { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +.arc-chip { + font-size: 10px; + padding: 1px 7px; + border-radius: 999px; + background: #f3f4f6; + color: $text-muted; + font-weight: 500; + + &--status { + background: #fef3c7; + color: #92400e; + } + + &--active { + background: #dcfce7; + color: #166534; + } +} + +.arc-result-arrow { + font-size: 18px; + width: 18px; + height: 18px; + color: $primary; + opacity: 0; + transition: + opacity 0.12s, + transform 0.12s; +} + +// ── Selected client ─────────────────────────────────────────────────────────── +.arc-selected-client { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 14px; + background: $success-light; + border: 1.5px solid $success-border; + border-radius: $radius; + margin-top: 6px; +} + +.arc-selected-avatar { + width: 38px; + height: 38px; + border-radius: 50%; + background: linear-gradient(135deg, #22c55e, #16a34a); + color: #fff; + font-size: 14px; + font-weight: 700; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; +} + +.arc-client-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; +} + +.arc-client-name { + font-size: 13px; + font-weight: 700; + color: #166534; +} + +.arc-client-sub { + font-size: 11px; + color: #15803d; +} + +.arc-selected-badge { + font-size: 10px; + font-weight: 700; + padding: 3px 10px; + border-radius: 999px; + background: $success; + color: #fff; + text-transform: uppercase; + letter-spacing: 0.05em; +} + +// ── Share percentage ────────────────────────────────────────────────────────── +.arc-share-row { + display: flex; + align-items: flex-start; + gap: 12px; +} + +.arc-share-field { + flex: 0 0 160px; +} + +.arc-percent-suffix { + font-size: 15px; + font-weight: 600; + color: $text-muted; + margin-right: 4px; +} + +.arc-share-hints { + display: flex; + flex-wrap: wrap; + gap: 6px; + padding-top: 10px; +} + +.arc-hint-chip { + padding: 5px 12px; + border-radius: 8px; + border: 1.5px solid $border; + background: #fff; + font-size: 12px; + font-weight: 600; + color: $text-muted; + cursor: pointer; + transition: + border-color 0.12s, + background 0.12s, + color 0.12s; + + &:hover { + border-color: $primary; + background: $primary-light; + color: $primary; + } +} + +// ── Actions ─────────────────────────────────────────────────────────────────── +.arc-actions { + padding: 12px 20px 16px !important; + border-top: 1px solid $border; + gap: 10px; + justify-content: flex-end !important; +} + +.arc-btn-cancel { + border-radius: 8px !important; +} + +.arc-btn-add { + border-radius: 8px !important; + padding: 0 18px !important; + gap: 4px; + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + } +} diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.ts b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.ts new file mode 100644 index 000000000..bba5fd60d --- /dev/null +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component.ts @@ -0,0 +1,120 @@ +/** + * 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, OnDestroy, ViewEncapsulation } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators, ReactiveFormsModule } from '@angular/forms'; +import { MatDialogRef, MatDialogContent, MatDialogActions } from '@angular/material/dialog'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { MatIcon } from '@angular/material/icon'; +import { ClientsService } from 'app/clients/clients.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-add-related-client-dialog', + templateUrl: './add-related-client-dialog.component.html', + styleUrl: './add-related-client-dialog.component.scss', + encapsulation: ViewEncapsulation.None, + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatDialogContent, + MatDialogActions, + MatProgressSpinner, + MatIcon, + ReactiveFormsModule + ] +}) +export class AddRelatedClientDialogComponent implements OnDestroy { + private fb = inject(UntypedFormBuilder); + private clientsService = inject(ClientsService); + dialogRef = inject(MatDialogRef); + + form: UntypedFormGroup = this.fb.group({ + searchText: [''], + sharePercentage: [ + null, + [ + Validators.required, + Validators.min(1), + Validators.max(100) + ] + ], + signingAuthority: [null], + role: [null] + }); + + readonly signingAuthorityOptions = [ + { value: 'SINGLE', label: 'Single' }, + { value: 'JOINTLY', label: 'Jointly' }, + { value: 'EITHER_OR_SURVIVOR', label: 'Either or Survivor' } + ]; + + readonly roleOptions = [ + { value: 'PRIMARY', label: 'Primary' }, + { value: 'JOINT', label: 'Joint' }, + { value: 'NOMINEE', label: 'Nominee' } + ]; + + searchResults: any[] = []; + selectedClient: any = null; + isSearching = false; + private searchTimer: any = null; + + onSearchInput(value: string): void { + if (this.searchTimer) clearTimeout(this.searchTimer); + this.selectedClient = null; + if (!value || value.trim().length < 2) { + this.searchResults = []; + return; + } + this.isSearching = true; + this.searchTimer = setTimeout(() => { + this.clientsService.searchByText(value.trim(), 0, 10).subscribe({ + next: (res: any) => { + this.searchResults = res?.content || []; + this.isSearching = false; + }, + error: () => { + this.searchResults = []; + this.isSearching = false; + } + }); + }, 400); + } + + selectClient(client: any): void { + this.selectedClient = client; + this.form.patchValue({ searchText: client.displayName }); + this.searchResults = []; + } + + clearSelection(): void { + this.selectedClient = null; + this.form.patchValue({ searchText: '' }); + this.searchResults = []; + } + + confirm(): void { + if (!this.selectedClient || this.form.get('sharePercentage').invalid) return; + this.dialogRef.close({ + clientId: this.selectedClient.id, + clientName: this.selectedClient.displayName, + clientCif: this.selectedClient.cifNumber || this.selectedClient.cifNo || '', + sharePercentage: this.form.get('sharePercentage').value, + signingAuthority: this.form.get('signingAuthority').value || null, + role: this.form.get('role').value || null + }); + } + + cancel(): void { + this.dialogRef.close(null); + } + + ngOnDestroy(): void { + if (this.searchTimer) clearTimeout(this.searchTimer); + } +} diff --git a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html index 52c79fcb1..215fe53f1 100644 --- a/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html +++ b/src/app/current-accounts/current-account-stepper/current-account-details-step/current-account-details-step.component.html @@ -7,63 +7,910 @@ -->
-
- - {{ 'labels.inputs.Product Name' | translate }} - - @for (product of productData; track product) { - - {{ product.name }} - - } - - - {{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - @if (currentAccountProductSelected) { - - {{ 'labels.inputs.Submitted On' | translate }} - - - + +
+
+ +

Account Details

+
+
+ + {{ 'labels.inputs.Product Name' | translate }} + + @for (product of productData; track product) { + {{ product.name }} + } + - {{ 'labels.inputs.Submitted On' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} - } - @if (currentAccountProductSelected) { - - {{ 'labels.inputs.Field Officer' | translate }} - - @for (officer of fieldOfficerData; track officer) { - - {{ officer.displayName }} - - } - - - } + @if (currentAccountProductSelected) { + + {{ 'labels.inputs.Submitted On' | translate }} + + + + + {{ 'labels.inputs.Submitted On' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + - @if (currentAccountProductSelected) { - - {{ 'labels.inputs.External ID' | translate }} - - - } + + {{ 'labels.inputs.Field Officer' | translate }} + + @for (officer of fieldOfficerData; track officer) { + {{ officer.displayName }} + } + + + + + {{ 'labels.inputs.External ID' | translate }} + + + + + Establishment Date + + + + Date of entity / business establishment + + + + Start of Operation Date + + + + Date when operations commenced + + } +
+ @if (currentAccountProductSelected && productTemplate) { + +
+
+ +

Product Classification

+ From Product +
+
+ + Account Type + + @for (opt of accountTypeOptions; track opt) { + {{ opt }} + } + + + + + Customer Segment + + @for (opt of customerSegmentOptions; track opt) { + {{ opt }} + } + + + + + Account No + + + + @if (productTemplate.effectiveDate) { + + Effective Date + + + } + + @if (productTemplate.effectiveTo) { + + Effective To + + + } + + @if (productTemplate.endDate) { + + End Date + + + } + + + Introducer Required + + + +
+ Share Linking Enabled + + {{ productTemplate.shareLinkingEnabled ? 'Yes' : 'No' }} + +
+
+
+ + @if (productTemplate?.introducerRequired) { + + } + } + + + @if (currentAccountProductSelected) { +
+
+
+
+ +
+
+

Status of Account

+ As per RBI guidelines — select account holder type +
+
+ RBI Compliant +
+ + + @if (clientDob) { +
+
+ cake + Date of Birth: {{ formatTemplateDate(clientDob) }} +
+
+ person_pin + Age: {{ clientAge }} years +
+ @if (clientAge !== null && clientAge < 18) { + + child_care Auto-detected: Minor + + } @else if (clientAge !== null && clientAge >= 60) { + + elderly Auto-detected: Senior Citizen + + } @else { + check_circle Age Verified + } +
+ } + + +
+ @for (opt of accountStatusOptions; track opt.value) { + + } +
+ + + @if (selectedAccountStatus === 'JOINT') { +
+
+
+ A +
+ Joint Account Holders + Add all co-applicants linked to this joint current account +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + @for (h of jointHolders; track h; let i = $index) { + + + + + + + + + + } + + + + + + + + + + +
Sr.Holder NameCIF No.Ownership %Signing AuthorityRoleAction
1 +
+
+ {{ primaryClientName ? primaryClientName[0].toUpperCase() : '?' }} +
+
+ {{ primaryClientName || 'Primary Holder' }} + Primary Account Holder +
+
+
+ {{ primaryClientCif || '—' }} + + + % + + + lock Mandatory + + Primary Account Holder—
{{ i + 2 }} +
+
+ {{ h.clientName[0].toUpperCase() }} +
+
+ {{ h.clientName }} + @if (h.isMandatory) { + Mandatory Signatory + } +
+
+
+ {{ h.clientCif || '—' }} + + + {{ h.sharePercentage }}% + + + + Co-Holder + +
Total Ownership + + {{ totalJointSharePercentage }}% + + + + lock {{ mandatoryJointCount + 1 }} Mandatory + +
+
+ + @if (jointHolders.length === 0) { +
+ group_add + Click Add Holder to add joint account holders. +
+ } + + @if (totalJointSharePercentage !== 100) { +
+ warning_amber + Total ownership is {{ totalJointSharePercentage }}%. It must equal 100%. +
+ } @else { +
+ check_circle + Ownership allocation is complete (100%). +
+ } +
+ + +
+
+
+ B +
+ Mode of Operation + Auto-set from mandatory holders — override manually if required +
+
+ @if (modeOfOperation) { + auto_awesome Auto + } +
+ + @if (jointHolders.length === 0) { +
+ how_to_vote + Add joint holders in Section A first. +
+ } @else { +
+ @for (opt of modeOfOperationOptions; track opt.value) { + + } +
+ +
+ @if (modeOfOperation) { + how_to_vote + Mode of Operation: {{ selectedModeLabel }} + + ({{ mandatoryJointCount }} Mandatory + {{ jointHolders.length - mandatoryJointCount }} Optional) + + } @else { + info + Set Mandatory/Optional for holders above to auto-select mode. + } +
+ } +
+ } + + + @if (selectedAccountStatus === 'MINOR') { +
+
+
+ + child_care + +
+ Minor Account Details + As per RBI — guardian must be an existing registered client +
+
+ Guardian Required +
+ +
+ + Minor's Date of Birth + + + + @if (clientDob) { + Auto-filled from client profile + } + + + + Age + + Must be below 18 years + + + + Date of Majority (Auto) + + Auto-calculated: DOB + 18 years + + + + Guardian Relationship + + @for (rel of guardianRelationshipOptions; track rel) { + {{ rel }} + } + + +
+ + + +
+ info +
    +
  • Minor below 10 years — Guardian must operate the account.
  • +
  • Minor above 10 years — Can operate independently (bank discretion).
  • +
  • Account must be converted to regular savings on attaining majority.
  • +
  • Fresh KYC of the minor must be done on majority.
  • +
+
+
+ } + + + @if (selectedAccountStatus === 'SENIOR_CITIZEN') { +
+
+
+ + elderly + +
+ Senior Citizen Account + Age ≥ 60 years — eligible for preferential banking services +
+
+ Age ≥ 60 +
+ +
+ + Date of Birth + + From client profile + + + + Age + + Age ≥ 60 required for Senior Citizen + +
+ +
+
+ priority_high +
+ Priority Service + Dedicated counters / assisted banking at branches +
+
+
+ percent +
+ Preferential Interest Rate + Additional 0.25%–0.50% on deposits (as per product) +
+
+
+ health_and_safety +
+ Doorstep Banking + Eligible for home banking services (RBI circular) +
+
+
+ gavel +
+ Nomination Mandatory + RBI mandate — ensure nomination is registered +
+
+
+ +
+ info +
    +
  • Age proof (DOB document) mandatory at time of account opening.
  • +
  • Nomination registration is compulsory (Form DA-1).
  • +
  • Bank must offer dedicated helpline and priority grievance redressal.
  • +
  • Door-step banking services must be offered (RBI Master Direction 2023).
  • +
+
+
+ } + + + @if (selectedAccountStatus === 'INDIVIDUAL_MALE') { +
+
+
+ + person + +
+ Individual Account (Male) + Select special category if applicable — governs applicable schemes & benefits +
+
+ Special Category +
+ +
+ @for (opt of specialCategoryOptions; track opt.value) { + + } +
+ +
+ info +
    +
  • Valid KYC documents required (Aadhaar / PAN / Passport).
  • +
  • Nomination registration is mandatory (Form DA-1).
  • +
  • Defence / Ex-Serviceman — eligible for special savings schemes (DSSP).
  • +
  • NRI — FCNR / NRE / NRO account rules apply as per FEMA guidelines.
  • +
+
+
+ } + + + @if (selectedAccountStatus === 'INDIVIDUAL_FEMALE') { +
+
+
+ + person_outline + +
+ Individual Account (Female) + Select weaker section category if applicable — governs PMJDY & priority schemes +
+
+ Weaker Section +
+ +
+ @for (opt of weakerSectionOptions; track opt.value) { + + } +
+ +
+ info +
    +
  • Valid KYC documents required (Aadhaar / PAN / Passport).
  • +
  • Nomination registration is mandatory (Form DA-1).
  • +
  • BPL / SC-ST / Minority — eligible for zero-balance accounts (RBI Priority Sector norms).
  • +
  • PMJDY beneficiary — ₹10,000 overdraft facility & accident insurance cover applicable.
  • +
  • SHG members — group linkage lending & special deposit rates applicable.
  • +
+
+
+ } +
+ } + + + @if (isNonIndividualClient && currentAccountProductSelected) { +
+ +
+
+
+ +
+
+

Non-Individual Account Details

+ As per RBI / Indian banking norms for non-individual entities +
+
+ Entity Account +
+ + +
+
+
+ A +
+ Partners / Members + Authorised persons linked to this entity account +
+
+ +
+ + @if (relatedClients.length > 0) { +
+ + + + + + + + + + + + + @for (c of relatedClients; track c; let i = $index) { + + + + + + + + + } + + + + + + + + + +
Sr.Partner NameCIF No.Ownership %Signing AuthorityAction
{{ i + 1 }} +
+
+ {{ c.clientName[0].toUpperCase() }} +
+
+ {{ c.clientName }} + @if (c.isMandatory) { + Mandatory Signatory + } +
+
+
+ {{ c.clientCif || '—' }} + + + {{ c.sharePercentage }}% + + + + + +
Total Ownership + + {{ totalSharePercentage }}% + + + + lock {{ mandatoryCount }} Mandatory + +
+
+ @if (totalSharePercentage !== 100) { +
+ warning_amber + Total ownership is {{ totalSharePercentage }}%. It must equal 100%. +
+ } @else { +
+ check_circle + Ownership allocation is complete (100%). +
+ } + } @else { +
+ group_add + No partners added yet. Click Add Partner to begin. +
+ } +
+ + +
+
+
+ B +
+ Mode of Operation / Signatory Rights + Auto-computed from Mandatory partners — you can override manually +
+
+ @if (signatoryRights) { + auto_awesome Auto + } +
+ + @if (relatedClients.length === 0) { +
+ how_to_vote + Add partners in Section A and set Mandatory/Optional to auto-compute the mode. +
+ } @else { +
+ @for (opt of signatoryRightsOptions; track opt.value) { + + } +
+ +
+ @if (signatoryRights) { + how_to_vote + Mode of Operation: {{ selectedSignatoryLabel }} + + ({{ mandatoryCount }} Mandatory + {{ relatedClients.length - mandatoryCount }} Optional partner(s)) + + } @else { + info + Set Mandatory/Optional for partners above to auto-select mode. + } +
+ } +
+
+ } +
+ +
+ - - - + + + {{ 'labels.inputs.INTEREST RATE CHART' | translate }} - - {{ 'labels.inputs.INTEREST RATE CHART' | translate }} + + - - - +
+ + +
+
- - {{ 'labels.inputs.CHARGES' | translate }} - - - - - - @if (recurringDepositAccountFormValid) { + {{ 'labels.inputs.PREVIEW' | translate }} diff --git a/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.scss b/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.scss index af9f4c075..684642c73 100644 --- a/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.scss +++ b/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.scss @@ -5,3 +5,15 @@ * 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 ::ng-deep mat-stepper { + background: transparent; + + .mat-step-content { + overflow: visible !important; + } +} + +:host ::ng-deep mat-step-content { + overflow: visible !important; +} diff --git a/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts b/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts index 4d459251f..46d217f0f 100644 --- a/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts +++ b/src/app/deposits/recurring-deposits/create-recurring-deposits-account/create-recurring-deposits-account.component.ts @@ -13,15 +13,21 @@ import { ActivatedRoute, Router } from '@angular/router'; /** Custom Services */ import { RecurringDepositsService } from '../recurring-deposits.service'; import { SettingsService } from 'app/settings/settings.service'; +import { ProductsService } from 'app/products/products.service'; /** Step Components */ import { RecurringDepositsAccountDetailsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component'; -import { RecurringDepositsAccountTermsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-terms-step/recurring-deposits-account-terms-step.component'; -import { RecurringDepositsAccountSettingsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-settings-step/recurring-deposits-account-settings-step.component'; -import { RecurringDepositsAccountChargesStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-charges-step/recurring-deposits-account-charges-step.component'; import { Dates } from 'app/core/utils/dates'; -import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper'; +import { + MatStepper, + MatStepperIcon, + MatStep, + MatStepLabel, + MatStepperNext, + MatStepperPrevious +} from '@angular/material/stepper'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { RecurringDepositsAccountCurrencyStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component'; import { RecurringDepositsAccountInterestRateChartStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component'; import { RecurringDepositsAccountPreviewStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @@ -37,14 +43,14 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; ...STANDALONE_SHARED_IMPORTS, MatStepper, MatStepperIcon, + MatStepperNext, + MatStepperPrevious, FaIconComponent, MatStep, MatStepLabel, RecurringDepositsAccountDetailsStepComponent, - RecurringDepositsAccountTermsStepComponent, - RecurringDepositsAccountSettingsStepComponent, + RecurringDepositsAccountCurrencyStepComponent, RecurringDepositsAccountInterestRateChartStepComponent, - RecurringDepositsAccountChargesStepComponent, RecurringDepositsAccountPreviewStepComponent ] }) @@ -54,21 +60,23 @@ export class CreateRecurringDepositsAccountComponent { private dateUtils = inject(Dates); private recurringDepositsService = inject(RecurringDepositsService); private settingsService = inject(SettingsService); + private productsService = inject(ProductsService); - /** Imports all the step component */ @ViewChild(RecurringDepositsAccountDetailsStepComponent, { static: true }) recurringDepositsAccountDetailsStep: RecurringDepositsAccountDetailsStepComponent; - @ViewChild(RecurringDepositsAccountTermsStepComponent, { static: true }) - recurringDepositAccountTermsStep: RecurringDepositsAccountTermsStepComponent; - @ViewChild(RecurringDepositsAccountSettingsStepComponent, { static: true }) - recurringDepositAccountSettingsStep: RecurringDepositsAccountSettingsStepComponent; - @ViewChild(RecurringDepositsAccountChargesStepComponent, { static: true }) - recurringDepositAccountChargesStep: RecurringDepositsAccountChargesStepComponent; + + @ViewChild(RecurringDepositsAccountCurrencyStepComponent) + recurringDepositsAccountCurrencyStep: RecurringDepositsAccountCurrencyStepComponent; + + @ViewChild(RecurringDepositsAccountInterestRateChartStepComponent) + recurringDepositsAccountChartStep: RecurringDepositsAccountInterestRateChartStepComponent; /** Recurring Deposits Account Template */ recurringDepositsAccountTemplate: any; /** Recurring Deposit Account Product Template */ recurringDepositsAccountProductTemplate: any; + /** Active CBS base interest rate chart */ + rdInterestRates: any[] = []; constructor() { this.route.data.subscribe((data: { recurringDepositsAccountTemplate: any }) => { @@ -78,10 +86,18 @@ export class CreateRecurringDepositsAccountComponent { /** * Sets recurring deposits account product template. - * @param {any} $event API response + * Also fetches the active CBS base interest rate chart. */ setTemplate($event: any) { this.recurringDepositsAccountProductTemplate = $event; + this.productsService.getRdBaseInterestRates().subscribe({ + next: (rates: any[]) => { + this.rdInterestRates = rates || []; + }, + error: () => { + this.rdInterestRates = []; + } + }); } /** Get Recurring Deposit Account Details Form Data */ @@ -89,42 +105,36 @@ export class CreateRecurringDepositsAccountComponent { return this.recurringDepositsAccountDetailsStep.recurringDepositAccountDetailsForm; } - /** Get Recurring Deposit Account Terms Form Data */ - get recurringDepositAccountTermsForm() { - return this.recurringDepositAccountTermsStep.recurringDepositAccountTermsForm; - } - - /** Get Recurring Deposit Account Settings Form Data */ - get recurringDepositAccountSettingsForm() { - return this.recurringDepositAccountSettingsStep.recurringDepositAccountSettingsForm; - } - - /** Checks wheter all the forms in different steps are valid or not */ - get recurringDepositAccountFormValid() { - return ( - this.recurringDepositAccountDetailsForm.valid && - this.recurringDepositAccountTermsForm.valid && - this.recurringDepositAccountSettingsForm.valid - ); - } - - /** Retrieves Data of all the forms */ + /** Retrieves Data of all forms */ get recurringDepositAccountData() { return { - ...this.recurringDepositsAccountDetailsStep.recurringDepositAccountDetails, - ...this.recurringDepositAccountTermsStep.recurringDepositAccountTerms, - ...this.recurringDepositAccountSettingsStep.recurringDepositAccountSettings, - ...this.recurringDepositAccountChargesStep.recurringDepositAccountCharges + ...this.recurringDepositsAccountDetailsStep.recurringDepositAccountDetails }; } - /** Retrieves Data of all forms except Currency to submit the data */ get recurringDepositAccount() { return { - ...this.recurringDepositsAccountDetailsStep.recurringDepositAccountDetails, - ...this.recurringDepositAccountTermsStep.recurringDepositAccountTerms, - ...this.recurringDepositAccountSettingsStep.recurringDepositAccountSettings, - ...this.recurringDepositAccountChargesStep.recurringDepositAccountCharges + ...this.recurringDepositsAccountDetailsStep.recurringDepositAccountDetails + }; + } + + get rdNominee() { + return this.recurringDepositsAccountCurrencyStep?.nomineeData ?? null; + } + + get rdTenureDays(): number | null { + return this.recurringDepositsAccountChartStep?.tenureDays ?? null; + } + + get rdApplicableSlab(): any | null { + return this.recurringDepositsAccountChartStep?.applicableSlab ?? null; + } + + get rdCurrencyFlags() { + return { + balanceSwapOption: this.recurringDepositsAccountCurrencyStep?.balanceSwapOption ?? false, + msme: this.recurringDepositsAccountCurrencyStep?.msme ?? false, + autoDebitFacility: this.recurringDepositsAccountCurrencyStep?.autoDebitFacility ?? false }; } @@ -137,17 +147,10 @@ export class CreateRecurringDepositsAccountComponent { const monthDayFormat = 'dd MMMM'; const recurringDepositAccount = { ...this.recurringDepositAccount, + ...this.recurringDepositsAccountCurrencyStep.currencyStepData, clientId: this.recurringDepositsAccountTemplate.clientId, - charges: this.recurringDepositAccount.charges.map((charge: any) => ({ - chargeId: charge.id, - amount: charge.amount, - dueDate: charge.dueDate && this.dateUtils.formatDate(charge.dueDate, dateFormat), - feeOnMonthDay: charge.feeOnMonthDay, - feeInterval: charge.feeInterval - })), - isCalendarInherited: this.recurringDepositAccount.recurringDepositAccount - ? this.recurringDepositAccount.recurringDepositAccount - : false, + charges: [], + isCalendarInherited: false, submittedOnDate: this.dateUtils.formatDate(this.recurringDepositAccount.submittedOnDate, dateFormat), expectedFirstDepositOnDate: this.dateUtils.formatDate( this.recurringDepositAccount.expectedFirstDepositOnDate, diff --git a/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.html b/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.html index 0ac77d4cb..372966041 100644 --- a/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.html +++ b/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.html @@ -71,6 +71,7 @@ @@ -93,6 +94,7 @@ [recurringDepositsAccountTemplate]="recurringDepositsAccountAndTemplate" [recurringDepositsAccountProductTemplate]="recurringDepositsAccountProductTemplate" [recurringDepositAccountData]="recurringDepositAccountData" + [rdInterestRates]="rdInterestRates" (submitEvent)="submit()" > diff --git a/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.ts b/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.ts index ffb6c9821..278e8e501 100644 --- a/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.ts +++ b/src/app/deposits/recurring-deposits/edit-recurring-deposit-account/edit-recurring-deposit-account.component.ts @@ -13,6 +13,7 @@ import { ActivatedRoute, Router } from '@angular/router'; /** Custom Services */ import { RecurringDepositsService } from '../recurring-deposits.service'; import { SettingsService } from 'app/settings/settings.service'; +import { ProductsService } from 'app/products/products.service'; /** Step Components */ import { RecurringDepositsAccountDetailsStepComponent } from '../recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component'; @@ -54,6 +55,7 @@ export class EditRecurringDepositAccountComponent { private dateUtils = inject(Dates); private recurringDepositsService = inject(RecurringDepositsService); private settingsService = inject(SettingsService); + private productsService = inject(ProductsService); /** Imports all the step component */ @ViewChild(RecurringDepositsAccountDetailsStepComponent, { static: true }) @@ -69,6 +71,8 @@ export class EditRecurringDepositAccountComponent { recurringDepositsAccountAndTemplate: any; /** Recurring Deposit Account Product Template */ recurringDepositsAccountProductTemplate: any; + /** Active CBS base interest rate chart */ + rdInterestRates: any[] = []; constructor() { this.route.data.subscribe((data: { recurringDepositsAccountAndTemplate: any }) => { @@ -82,6 +86,14 @@ export class EditRecurringDepositAccountComponent { */ setTemplate($event: any) { this.recurringDepositsAccountProductTemplate = $event; + this.productsService.getRdBaseInterestRates().subscribe({ + next: (rates: any[]) => { + this.rdInterestRates = rates || []; + }, + error: () => { + this.rdInterestRates = []; + } + }); } /** Get Recurring Deposit Account Details Form Data */ diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-charges-step/recurring-deposits-account-charges-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-charges-step/recurring-deposits-account-charges-step.component.ts index 8690f6248..f0ed84272 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-charges-step/recurring-deposits-account-charges-step.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-charges-step/recurring-deposits-account-charges-step.component.ts @@ -79,7 +79,7 @@ export class RecurringDepositsAccountChargesStepComponent implements OnInit, OnC @Input() recurringDepositAccountFormValid: boolean; /** Charges Data */ - chargeData: any; + chargeData: any[] = []; /** Charges Data Source */ chargesDataSource: {}[] = []; /** Charges table columns */ @@ -102,8 +102,10 @@ export class RecurringDepositsAccountChargesStepComponent implements OnInit, OnC ngOnInit() { this.chargesDataSource = []; if (this.recurringDepositsAccountTemplate.id && this.recurringDepositsAccountTemplate.charges) { + // Edit mode: load saved charges from existing account this.chargesDataSource = this.recurringDepositsAccountTemplate.charges.map((charge: any) => ({ ...charge, id: charge.chargeId })) || []; + this.isChargesPatched = true; } } @@ -120,8 +122,17 @@ export class RecurringDepositsAccountChargesStepComponent implements OnInit, OnC } if (this.recurringDepositsAccountProductTemplate) { this.chargeData = this.recurringDepositsAccountProductTemplate.chargeOptions.filter( - (c: Charge) => c.currency.code === this.currency.code + (c: Charge) => c.currency.code === this.currency?.code ); + + // New account: pre-populate charges from product template + if (!this.isChargesPatched && !this.recurringDepositsAccountTemplate?.id) { + const productCharges = this.recurringDepositsAccountProductTemplate.charges; + if (Array.isArray(productCharges) && productCharges.length > 0) { + this.chargesDataSource = productCharges.map((charge: any) => ({ ...charge, id: charge.chargeId })); + } + this.isChargesPatched = true; + } } } diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.html new file mode 100644 index 000000000..569e917ee --- /dev/null +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.html @@ -0,0 +1,82 @@ + + +
+
+ + Currency & Account Status Configuration + From Product +
+
+
+ + Currency + + Inherited from selected product + + + + Decimal Places + + Inherited from selected product + +
+ +
+
+

Balance Swap Option

+

Enable automated transfer of excess funds to linked deposits or vice-versa to maintain liquidity.

+
+ +
+ +
+
+

MSME

+

Micro, Small and Medium Enterprises — mark this account under MSME category.

+
+ +
+ +
+
+

Auto Debit Facility

+

Enable automatic deduction of funds for recurring payments, utility bills, or EMI obligations.

+
+ +
+ +
+
+

Dormancy Alert

+

Enable automated notifications when an account becomes inactive based on product rules.

+
+ + {{ recurringDepositsAccountProductTemplate?.dormancyAlert ? 'Yes' : 'No' }} + +
+ @if (recurringDepositsAccountProductTemplate?.dormancyAlert) { + + } + +
+
+

Nomination Facility

+

Allow account holders to designate individuals to receive funds in the event of account holder's death.

+
+ + {{ recurringDepositsAccountProductTemplate?.nominationFacility ? 'Yes' : 'No' }} + +
+ @if (recurringDepositsAccountProductTemplate?.nominationFacility) { + + } +
+
diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.scss b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.scss new file mode 100644 index 000000000..3ed604577 --- /dev/null +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.scss @@ -0,0 +1,141 @@ +/** + * 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/. + */ + +$accent: #0ea5e9; +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-900: #202124; + +.racc-section { + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; + background: #fff; + + &__header { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 24px; + background: $gray-50; + border-bottom: 1px solid $gray-200; + + i { + font-size: 14px; + color: $accent; + } + + span { + margin: 0; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + } +} + +.racc-body { + background: #fff; +} + +.racc-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px 16px; + padding: 16px 24px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.racc-field { + width: 100%; +} + +.racc-alert { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + padding: 18px 24px; + border-top: 1px solid $border; + background: #f8fafc; + + @media (width <= 760px) { + flex-direction: column; + align-items: flex-start; + } + + &__content { + h4 { + margin: 0 0 6px; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + + p { + margin: 0; + font-size: 12px; + line-height: 1.5; + color: #64748b; + max-width: 540px; + } + } +} + +.racc-yn { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 48px; + padding: 4px 14px; + border-radius: 999px; + font-size: 12px; + font-weight: 700; + background: #fee2e2; + color: #b91c1c; + border: 1px solid #fecaca; + + &--yes { + background: #dcfce7; + color: #15803d; + border-color: #bbf7d0; + } +} + +.racc-badge { + display: inline-flex; + align-items: center; + padding: 2px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; + + &--from { + background: #eff6ff; + color: #1d4ed8; + border: 1px solid #bfdbfe; + } +} + +.racc-alert--editable { + border-color: #bae6fd; + background: #f0f9ff; + + &:hover { + border-color: #7dd3fc; + box-shadow: 0 2px 8px rgb(2 132 199 / 10%); + } + + h4 { + color: #0369a1; + } +} diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.ts new file mode 100644 index 000000000..d793ae8fe --- /dev/null +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-currency-step/recurring-deposits-account-currency-step.component.ts @@ -0,0 +1,60 @@ +/** + * 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, OnChanges, SimpleChanges } from '@angular/core'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { NomineeFormComponent } from 'app/shared/nominee-form/nominee-form.component'; +import { DormancyAlertPanelComponent } from 'app/shared/dormancy-alert-panel/dormancy-alert-panel.component'; +import { MatSlideToggle } from '@angular/material/slide-toggle'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'mifosx-recurring-deposits-account-currency-step', + templateUrl: './recurring-deposits-account-currency-step.component.html', + styleUrls: ['./recurring-deposits-account-currency-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + NomineeFormComponent, + DormancyAlertPanelComponent, + MatSlideToggle, + FormsModule + ] +}) +export class RecurringDepositsAccountCurrencyStepComponent implements OnChanges { + @Input() recurringDepositsAccountProductTemplate: any; + + nomineeData: any = null; + + // Editable feature toggles (pre-filled from product, overridable by user) + balanceSwapOption = false; + msme = false; + autoDebitFacility = false; + + ngOnChanges(changes: SimpleChanges): void { + if (changes['recurringDepositsAccountProductTemplate']?.currentValue) { + const pt = changes['recurringDepositsAccountProductTemplate'].currentValue; + this.balanceSwapOption = !!pt.balanceSwapOption; + this.msme = !!pt.msme; + this.autoDebitFacility = !!pt.autoDebitFacility; + } + } + + onNomineeChange(event: any): void { + this.nomineeData = event; + } + + /** Called by parent to collect currency step values */ + get currencyStepData() { + return { + balanceSwapOption: this.balanceSwapOption, + msme: this.msme, + autoDebitFacility: this.autoDebitFacility, + ...(this.nomineeData ? { nominee: this.nomineeData } : {}) + }; + } +} diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html index df70f6d6c..be84c1d84 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.html @@ -7,82 +7,821 @@ -->
-
- - {{ 'labels.inputs.Product Name' | translate }} - - @for (product of productData; track product) { - - {{ product.name }} - - } - - - {{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - @if (isProductSelected) { - - {{ 'labels.inputs.Submitted On' | translate }} - - - + +
+
+ +

Account Details

+
+
+ + {{ 'labels.inputs.Product Name' | translate }} + + @for (product of productData; track product) { + {{ product.name }} + } + - {{ 'labels.inputs.Submission Date' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} - } - @if (isProductSelected) { - - {{ 'labels.inputs.Field Officer' | translate }} - - @for (officer of fieldOfficerData; track officer) { - - {{ officer.displayName }} - + @if (isProductSelected) { + + {{ 'labels.inputs.Submitted On' | translate }} + + + + + {{ 'labels.inputs.Submitted On' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + + {{ 'labels.inputs.Field Officer' | translate }} + + @for (officer of fieldOfficerData; track officer) { + {{ officer.displayName }} + } + + + + + {{ 'labels.inputs.External id' | translate }} + + + } +
+
+ + @if (isProductSelected && productTemplate) { + +
+
+ +

Product Classification

+ From Product +
+
+ + Account Type + + @for (opt of accountTypeOptions; track opt) { + {{ opt }} + } + + + + + Customer Segment + + @for (opt of customerSegmentOptions; track opt) { + {{ opt }} + } + + + + + Account No + + + + @if (productTemplate.effectiveDate) { + + Effective Date + + + } + + @if (productTemplate.endDate) { + + End Date + + + } + +
+ Deposit Account Link + + {{ productTemplate.shareLinkingEnabled ? 'Yes' : 'No' }} + +
+
+
+ + + @if (productTemplate?.shareLinkingEnabled) { +
+
+
+
+ link +
+
+

Deposit Account Link

+ Link an existing Savings or Current account to this RD +
+
+
+ +
+ @if (dalLoading) { +
+ hourglass_top + Loading accounts… +
+ } @else if (selectedLinkedAccount) { + +
+
+ + {{ + selectedLinkedAccount.accountType === 'SAVINGS' ? 'savings' : 'account_balance' + }} + {{ selectedLinkedAccount.accountType === 'SAVINGS' ? 'Savings' : 'Current' }} + +
+ {{ selectedLinkedAccount.accountNo }} + {{ selectedLinkedAccount.productName }} +
+
+ Balance + {{ selectedLinkedAccount.currencyCode }} + {{ selectedLinkedAccount.balance | number: '1.2-2' }} +
+
+ +
+ } @else { + + @if (!dalSelectedType) { +
+

Select account type to link:

+
+ + +
+
+ } @else { + +
+
+ + + {{ dalSelectedType === 'SAVINGS' ? 'savings' : 'account_balance' }} + {{ dalSelectedType === 'SAVINGS' ? 'Savings Accounts' : 'Current Accounts' }} + +
+ + + Search by Account No. or Product + search + + @if (dalSearch) { + + } + + + @if (dalFilteredAccounts.length === 0) { +
+ search_off + No {{ dalSelectedType === 'SAVINGS' ? 'Savings' : 'Current' }} accounts found. +
+ } @else { +
+ @for (account of dalFilteredAccounts; track account.id) { +
+
+ {{ account.accountNo }} + {{ account.productName }} +
+
+ {{ account.status }} + {{ account.currencyCode }} {{ account.balance | number: '1.2-2' }} +
+ chevron_right +
+ } +
+ } +
+ } } - - +
+
} + } - @if (isProductSelected) { - - {{ 'labels.inputs.External id' | translate }} - - - } -
+ + @if (isProductSelected) { +
+
+
+
+ +
+
+

Status of Account

+ As per RBI guidelines — select account holder type +
+
+ RBI Compliant +
-
- - -
+ + @if (clientDob) { +
+
+ cake + Date of Birth: {{ formatTemplateDate(clientDob) }} +
+
+ person_pin + Age: {{ clientAge }} years +
+ @if (clientAge !== null && clientAge < 18) { + + child_care Auto-detected: Minor + + } @else if (clientAge !== null && clientAge >= 60) { + + elderly Auto-detected: Senior Citizen + + } @else { + check_circle Age Verified + } +
+ } + + +
+ @for (opt of accountStatusOptions; track opt.value) { + + } +
+ + + @if (selectedAccountStatus === 'JOINT') { +
+
+
+ A +
+ Joint Account Holders + Add all co-applicants linked to this joint RD account +
+
+ +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + @for (h of jointHolders; track h; let i = $index) { + + + + + + + + + + } + + + + + + + + + + +
Sr.Holder NameCIF No.Ownership %Signing AuthorityRoleAction
1 +
+
+ {{ primaryClientName ? primaryClientName[0].toUpperCase() : '?' }} +
+
+ {{ primaryClientName || 'Primary Holder' }} + Primary Account Holder +
+
+
+ {{ primaryClientCif || '—' }} + + + % + + + lock Mandatory + + Primary Account Holder—
{{ i + 2 }} +
+
+ {{ h.clientName[0].toUpperCase() }} +
+
+ {{ h.clientName }} + @if (h.isMandatory) { + Mandatory Signatory + } +
+
+
+ {{ h.clientCif || '—' }} + + + {{ h.sharePercentage }}% + + + + Co-Holder + +
Total Ownership + + {{ totalJointSharePercentage }}% + + + + lock {{ mandatoryJointCount + 1 }} Mandatory + +
+
+ + @if (jointHolders.length === 0) { +
+ group_add + Click Add Holder to add joint account holders. +
+ } + + @if (totalJointSharePercentage !== 100) { +
+ warning_amber + Total ownership is {{ totalJointSharePercentage }}%. It must equal 100%. +
+ } @else { +
+ check_circle + Ownership allocation is complete (100%). +
+ } +
+ +
+
+
+ B +
+ Mode of Operation + Auto-set from mandatory holders — override manually if required +
+
+ @if (modeOfOperation) { + auto_awesome Auto + } +
+ + @if (jointHolders.length === 0) { +
+ how_to_vote + Add joint holders in Section A first. +
+ } @else { +
+ @for (opt of modeOfOperationOptions; track opt.value) { + + } +
+ +
+ @if (modeOfOperation) { + how_to_vote + Mode of Operation: {{ selectedModeLabel }} + + ({{ mandatoryJointCount }} Mandatory + {{ jointHolders.length - mandatoryJointCount }} Optional) + + } @else { + info + Set Mandatory/Optional for holders above to auto-select mode. + } +
+ } +
+ } + + + @if (selectedAccountStatus === 'MINOR') { +
+
+
+ + child_care + +
+ Minor Account Details + As per RBI — guardian must be an existing registered client +
+
+ Guardian Required +
+ +
+ + Minor's Date of Birth + + + + @if (clientDob) { + Auto-filled from client profile + } + + + + Age + + Must be below 18 years + + + + Date of Majority (Auto) + + Auto-calculated: DOB + 18 years + + + + Guardian Relationship + + @for (rel of guardianRelationshipOptions; track rel) { + {{ rel }} + } + + +
+ + + +
+ info +
    +
  • Minor below 10 years — Guardian must operate the account.
  • +
  • Minor above 10 years — Can operate independently (bank discretion).
  • +
  • Account must be converted to regular savings on attaining majority.
  • +
  • Fresh KYC of the minor must be done on majority.
  • +
+
+
+ } + + + @if (selectedAccountStatus === 'SENIOR_CITIZEN') { +
+
+
+ + elderly + +
+ Senior Citizen Account + Age ≥ 60 years — eligible for preferential banking services +
+
+ Age ≥ 60 +
+ +
+ + Date of Birth + + From client profile + + + + Age + + Age ≥ 60 required for Senior Citizen + +
+ +
+
+ priority_high +
+ Priority Service + Dedicated counters / assisted banking at branches +
+
+
+ percent +
+ Preferential Interest Rate + Additional 0.25%–0.50% on deposits (as per product) +
+
+
+ health_and_safety +
+ Doorstep Banking + Eligible for home banking services (RBI circular) +
+
+
+ gavel +
+ Nomination Mandatory + RBI mandate — ensure nomination is registered +
+
+
+ +
+ info +
    +
  • Age proof (DOB document) mandatory at time of account opening.
  • +
  • Nomination registration is compulsory (Form DA-1).
  • +
  • Bank must offer dedicated helpline and priority grievance redressal.
  • +
  • Door-step banking services must be offered (RBI Master Direction 2023).
  • +
+
+
+ } + + + @if (selectedAccountStatus === 'INDIVIDUAL_MALE') { +
+
+
+ + person + +
+ Individual Account (Male) + Select special category if applicable — governs applicable schemes & benefits +
+
+ Special Category +
+ +
+ @for (opt of specialCategoryOptions; track opt.value) { + + } +
+ +
+ info +
    +
  • Valid KYC documents required (Aadhaar / PAN / Passport).
  • +
  • Nomination registration is mandatory (Form DA-1).
  • +
  • Defence / Ex-Serviceman — eligible for special savings schemes (DSSP).
  • +
  • NRI — FCNR / NRE / NRO account rules apply as per FEMA guidelines.
  • +
+
+
+ } + + + @if (selectedAccountStatus === 'INDIVIDUAL_FEMALE') { +
+
+
+ + person_outline + +
+ Individual Account (Female) + Select weaker section category if applicable — governs PMJDY & priority schemes +
+
+ Weaker Section +
+ +
+ @for (opt of weakerSectionOptions; track opt.value) { + + } +
+ +
+ info +
    +
  • Valid KYC documents required (Aadhaar / PAN / Passport).
  • +
  • Nomination registration is mandatory (Form DA-1).
  • +
  • BPL / SC-ST / Minority — eligible for zero-balance accounts (RBI Priority Sector norms).
  • +
  • PMJDY beneficiary — ₹10,000 overdraft facility & accident insurance cover applicable.
  • +
  • SHG members — group linkage lending & special deposit rates applicable.
  • +
+
+
+ } +
+ }
diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.scss b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.scss index d59bc82ee..a78fa8828 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.scss +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.scss @@ -6,6 +6,1562 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -.margin-t { - margin-top: 1em; +$accent: #0ea5e9; +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-900: #202124; + +// ── Section card ─────────────────────────────────────────────── +.rad-section { + border: 1px solid $border; + border-radius: 10px; + overflow: visible; + background: #fff; + + &--mt { + margin-top: 16px; + } + + &__header { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 24px; + background: $gray-50; + border-bottom: 1px solid $gray-200; + + i { + font-size: 14px; + color: $accent; + } + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: $gray-900; + flex: 1; + } + } +} + +// ── 2-col grid (Account Details inputs) ─────────────────────── +.rad-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px 16px; + padding: 16px 24px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +// ── 3-col grid (Product Classification) ─────────────────────── +.rad-grid--classification { + grid-template-columns: repeat(3, 1fr); + + @media (width <= 900px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.rad-field { + width: 100%; +} + +// ── Share Linking badge row ──────────────────────────────────── +.rad-info-row { + display: flex; + align-items: center; + gap: 12px; + min-height: 56px; +} + +.rad-info-label { + font-size: 14px; + color: $gray-900; + font-weight: 500; +} + +.rad-badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 3px 12px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + background: #dbeafe; + color: #1d4ed8; + border: 1px solid #bfdbfe; + + &--yes { + background: #dcfce7; + color: #15803d; + border-color: #bbf7d0; + } +} + +// ── Currency section body ────────────────────────────────────── +.rad-body { + background: #fff; +} + +// ── Yes / No alert rows (same as rdpc-alert) ────────────────── +.rad-alert { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + padding: 18px 24px 24px; + border-top: 1px solid $border; + background: #f8fafc; + + @media (width <= 760px) { + flex-direction: column; + align-items: flex-start; + } + + &__content { + h4 { + margin: 0 0 6px; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + + p { + margin: 0; + font-size: 12px; + line-height: 1.5; + color: #64748b; + max-width: 540px; + } + } + + &__choices { + display: flex; + align-items: center; + gap: 18px; + flex-wrap: wrap; + } +} + +// ── yes / no indicator pills ─────────────────────────────────── +.rad-yn { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 14px; + color: #94a3b8; + cursor: default; + + &::before { + content: ''; + display: inline-block; + width: 18px; + height: 18px; + border-radius: 50%; + border: 2px solid #cbd5e1; + background: #fff; + flex-shrink: 0; + } + + &--active { + color: $gray-900; + font-weight: 500; + + &::before { + border-color: #22c55e; + background: radial-gradient(circle, #22c55e 45%, #fff 46%); + } + } +} + +// ── Deposit Account Link ────────────────────────────────────────────────── + +.dal-card { + margin-top: 16px; + border: 1px solid #e2e8f0; + border-radius: 12px; + background: #fff; + overflow: hidden; + + &__header { + display: flex; + align-items: center; + gap: 14px; + padding: 16px 24px; + border-bottom: 1px solid #f1f5f9; + background: linear-gradient(135deg, #f0f9ff 0%, #e0f2fe 100%); + } + + &__header-left { + display: flex; + align-items: center; + gap: 14px; + } + + &__icon { + width: 44px; + height: 44px; + border-radius: 10px; + background: #0ea5e9; + display: flex; + align-items: center; + justify-content: center; + color: #fff; + } + + &__title { + margin: 0; + font-size: 15px; + font-weight: 700; + color: #0c4a6e; + } + + &__sub { + display: block; + font-size: 12px; + color: #64748b; + margin-top: 2px; + } + + &__body { + padding: 20px 24px; + } +} + +// ── Loading ─────────────────────────────────────────────────────────────── +.dal-loading { + display: flex; + align-items: center; + gap: 10px; + padding: 28px; + color: #64748b; + font-size: 14px; + justify-content: center; + + &__icon { + animation: dal-spin 1.2s linear infinite; + color: #0ea5e9; + } +} + +@keyframes dal-spin { + from { + transform: rotate(0deg); + } + + to { + transform: rotate(360deg); + } +} + +// ── Step 1 — Type selection ─────────────────────────────────────────────── +.dal-type-step { + &__label { + font-size: 13px; + color: #64748b; + margin: 0 0 14px; + } +} + +.dal-type-grid { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 14px; +} + +.dal-type-btn { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 8px; + padding: 24px 16px; + border-radius: 12px; + border: 2px solid #e2e8f0; + background: #f8fafc; + cursor: pointer; + transition: all 0.18s; + + mat-icon { + font-size: 32px; + width: 32px; + height: 32px; + } + + &__name { + font-size: 14px; + font-weight: 700; + color: #0f172a; + } + + &__count { + font-size: 11px; + color: #94a3b8; + } + + &--savings { + &:hover { + border-color: #3b82f6; + background: #eff6ff; + box-shadow: 0 4px 12px rgb(59 130 246 / 15%); + + mat-icon { + color: #2563eb; + } + + .dal-type-btn__name { + color: #1d4ed8; + } + } + } + + &--current { + &:hover { + border-color: #16a34a; + background: #f0fdf4; + box-shadow: 0 4px 12px rgb(22 163 74 / 15%); + + mat-icon { + color: #15803d; + } + + .dal-type-btn__name { + color: #15803d; + } + } + } +} + +// ── Step 2 — Picker ─────────────────────────────────────────────────────── +.dal-picker { + &__topbar { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 14px; + } + + &__title { + display: flex; + align-items: center; + gap: 6px; + font-size: 14px; + font-weight: 700; + color: #0f172a; + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + color: #64748b; + } + } +} + +.dal-back-btn { + color: #64748b; +} + +.dal-search-field { + width: 100%; + margin-bottom: 8px; +} + +.dal-empty { + display: flex; + flex-direction: column; + align-items: center; + gap: 8px; + padding: 32px 16px; + color: #94a3b8; + font-size: 13px; + text-align: center; + + mat-icon { + font-size: 36px; + width: 36px; + height: 36px; + } +} + +.dal-list { + display: flex; + flex-direction: column; + gap: 8px; + max-height: 300px; + overflow-y: auto; +} + +.dal-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + border: 1px solid #e2e8f0; + border-radius: 10px; + cursor: pointer; + transition: + border-color 0.15s, + background 0.15s, + box-shadow 0.15s; + + &:hover { + border-color: #0ea5e9; + background: #f0f9ff; + box-shadow: 0 2px 8px rgb(14 165 233 / 12%); + } + + &__details { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; + } + + &__accno { + font-size: 14px; + font-weight: 600; + color: #0f172a; + font-family: monospace; + } + + &__product { + font-size: 12px; + color: #64748b; + } + + &__right { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 3px; + } + + &__status { + font-size: 11px; + font-weight: 600; + color: #94a3b8; + + &--active { + color: #16a34a; + } + } + + &__balance { + font-size: 13px; + font-weight: 600; + color: #0f172a; + } + + &__arrow { + color: #cbd5e1; + font-size: 20px; + width: 20px; + height: 20px; + } +} + +// ── Selected Account ────────────────────────────────────────────────────── +.dal-selected { + display: flex; + align-items: center; + gap: 16px; + padding: 16px 20px; + border: 2px solid #0ea5e9; + border-radius: 12px; + background: #f0f9ff; + + &__info { + display: flex; + align-items: center; + gap: 14px; + flex: 1; + flex-wrap: wrap; + } + + &__details { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1; + } + + &__accno { + font-size: 15px; + font-weight: 700; + color: #0c4a6e; + font-family: monospace; + } + + &__product { + font-size: 12px; + color: #64748b; + } + + &__balance { + display: flex; + flex-direction: column; + align-items: flex-end; + gap: 2px; + } + + &__bal-label { + font-size: 10px; + color: #94a3b8; + text-transform: uppercase; + letter-spacing: 0.5px; + } + + &__bal-amount { + font-size: 15px; + font-weight: 700; + color: #0c4a6e; + } +} + +.dal-change-btn { + white-space: nowrap; +} + +.dal-type-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 12px; + border-radius: 999px; + font-size: 12px; + font-weight: 700; + white-space: nowrap; + background: #f1f5f9; + color: #475569; + border: 1px solid #e2e8f0; + + mat-icon { + font-size: 14px; + width: 14px; + height: 14px; + } + + &--savings { + background: #eff6ff; + color: #1d4ed8; + border-color: #bfdbfe; + + mat-icon { + color: #2563eb; + } + } + + &--current { + background: #f0fdf4; + color: #15803d; + border-color: #bbf7d0; + + mat-icon { + color: #16a34a; + } + } +} + +// ── Status of Account (RBI) ─────────────────────────────────────────────── + +.soa-card { + margin-top: 16px; + border: 1px solid #e2e8f0; + border-radius: 10px; + background: #fff; + overflow: visible; + + &__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 12px 24px; + background: #f8f9fa; + border-bottom: 1px solid #e8eaed; + border-radius: 10px 10px 0 0; + } + + &__header-left { + display: flex; + align-items: center; + gap: 12px; + } + + &__icon { + width: 36px; + height: 36px; + border-radius: 8px; + background: #ede9fe; + display: flex; + align-items: center; + justify-content: center; + + i { + font-size: 16px; + color: #7c3aed; + } + } + + &__title { + margin: 0; + font-size: 14px; + font-weight: 600; + color: #202124; + } + + &__sub { + font-size: 11px; + color: #6b7280; + display: block; + } +} + +.soa-pill { + display: inline-flex; + align-items: center; + padding: 3px 12px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + background: #ede9fe; + color: #7c3aed; + border: 1px solid #ddd6fe; + + &--minor { + background: #fef3c7; + color: #b45309; + border-color: #fde68a; + } + + &--senior { + background: #d1fae5; + color: #065f46; + border-color: #a7f3d0; + } + + &--ok { + background: #dcfce7; + color: #15803d; + border-color: #bbf7d0; + } +} + +.soa-client-info-bar { + display: flex; + align-items: center; + gap: 20px; + padding: 10px 24px; + background: #f0f9ff; + border-bottom: 1px solid #bae6fd; + flex-wrap: wrap; + + &__item { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + color: #0369a1; + + mat-icon { + font-size: 16px; + height: 16px; + width: 16px; + } + + strong { + font-weight: 700; + } + } +} + +.soa-auto-pill { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + margin-left: auto; + + mat-icon { + font-size: 13px; + height: 13px; + width: 13px; + } + + &--minor { + background: #fef3c7; + color: #b45309; + border: 1px solid #fde68a; + } + + &--senior { + background: #d1fae5; + color: #065f46; + border: 1px solid #a7f3d0; + } + + &--ok { + background: #dcfce7; + color: #15803d; + border: 1px solid #bbf7d0; + } +} + +.soa-tiles { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 12px; + padding: 16px 24px; + border-bottom: 1px solid #f1f5f9; + + @media (width <= 900px) { + grid-template-columns: repeat(3, 1fr); + } + + @media (width <= 600px) { + grid-template-columns: repeat(2, 1fr); + } +} + +.soa-tile { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 14px 10px; + border: 2px solid #e2e8f0; + border-radius: 10px; + background: #fff; + cursor: pointer; + transition: + border-color 0.15s, + box-shadow 0.15s; + position: relative; + + &:hover { + border-color: #a5b4fc; + box-shadow: 0 2px 8px rgb(99 102 241 / 12%); + } + + &--selected { + border-color: #6366f1; + background: #eef2ff; + box-shadow: 0 2px 12px rgb(99 102 241 / 20%); + } + + &__icon { + font-size: 28px; + height: 28px; + width: 28px; + } + + &__label { + font-size: 12px; + font-weight: 600; + color: #374151; + text-align: center; + } + + &__check { + position: absolute; + top: 6px; + right: 6px; + font-size: 16px; + height: 16px; + width: 16px; + color: #6366f1; + } + + &__lock { + position: absolute; + top: 4px; + right: 4px; + display: flex; + align-items: center; + + mat-icon { + font-size: 13px; + height: 13px; + width: 13px; + color: #94a3b8; + } + } + + &--disabled { + opacity: 0.45; + cursor: not-allowed; + background: #f8fafc; + border-color: #e2e8f0; + box-shadow: none; + + &:hover { + border-color: #e2e8f0; + box-shadow: none; + } + } +} + +.soa-block { + padding: 16px 24px; + + &--sep { + border-top: 1px dashed #e2e8f0; + } + + &__head { + display: flex; + align-items: flex-start; + justify-content: space-between; + margin-bottom: 14px; + } + + &__head-left { + display: flex; + align-items: flex-start; + gap: 12px; + } + + &__letter { + width: 28px; + height: 28px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + font-size: 13px; + font-weight: 700; + color: #fff; + flex-shrink: 0; + margin-top: 2px; + } + + &__title { + font-size: 13px; + font-weight: 600; + color: #1f2937; + display: block; + } + + &__hint { + font-size: 11px; + color: #6b7280; + display: block; + } +} + +.soa-table-wrap { + overflow-x: auto; + border-radius: 8px; + border: 1px solid #e2e8f0; +} + +.soa-tbl { + width: 100%; + border-collapse: collapse; + font-size: 13px; +} + +.soa-th { + padding: 10px 14px; + background: #f8fafc; + text-align: left; + font-weight: 600; + font-size: 11px; + color: #64748b; + text-transform: uppercase; + letter-spacing: 0.04em; + border-bottom: 1px solid #e2e8f0; + + &--sr { + width: 44px; + } + + &--center { + text-align: center; + } +} + +.soa-td { + padding: 10px 14px; + border-bottom: 1px solid #f1f5f9; + vertical-align: middle; + + &--sr { + text-align: center; + font-size: 12px; + color: #94a3b8; + font-weight: 600; + } + + &--center { + text-align: center; + } + + &--total-label { + font-size: 12px; + font-weight: 600; + color: #475569; + text-align: right; + } +} + +.soa-tr { + transition: background 0.1s; + + &:hover { + background: #f8fafc; + } + + &--mandatory { + background: #fdf4ff; + } + + &--primary { + background: #f0f9ff; + border-left: 3px solid #0891b2; + } +} + +.soa-avatar--primary { + background: #cffafe; + color: #0e7490; +} + +.soa-primary-tag { + font-size: 10px; + color: #0e7490; + font-weight: 600; + background: #cffafe; + padding: 1px 7px; + border-radius: 999px; + display: inline-block; +} + +.soa-pct-input { + width: 56px; + padding: 4px 6px; + border: 1px solid #cbd5e1; + border-radius: 6px; + font-size: 13px; + font-weight: 600; + text-align: center; + color: #1f2937; + + &:focus { + outline: none; + border-color: #0891b2; + box-shadow: 0 0 0 2px rgb(8 145 178 / 15%); + } + + &::-webkit-inner-spin-button, + &::-webkit-outer-spin-button { + opacity: 1; + } +} + +.soa-pct-unit { + font-size: 13px; + font-weight: 600; + color: #64748b; + margin-left: 2px; +} + +.soa-no-action { + color: #cbd5e1; + font-size: 16px; +} + +.soa-tr-total { + background: #f8fafc; +} + +.soa-client-cell { + display: flex; + align-items: center; + gap: 10px; +} + +.soa-avatar { + width: 32px; + height: 32px; + border-radius: 50%; + background: #dbeafe; + color: #1d4ed8; + font-weight: 700; + font-size: 14px; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + &--mandatory { + background: #ede9fe; + color: #7c3aed; + } +} + +.soa-client-name { + font-weight: 600; + font-size: 13px; + color: #1f2937; + display: block; +} + +.soa-mandatory-tag { + font-size: 10px; + color: #7c3aed; + font-weight: 600; + background: #ede9fe; + padding: 1px 7px; + border-radius: 999px; +} + +.soa-mono { + font-family: 'Roboto Mono', monospace; + font-size: 12px; + color: #64748b; +} + +.soa-pct-badge { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 2px 10px; + border-radius: 999px; + font-size: 12px; + font-weight: 700; + background: #f1f5f9; + color: #475569; + + &--full, + &--ok { + background: #dcfce7; + color: #15803d; + } + + &--warn { + background: #fef3c7; + color: #b45309; + } +} + +.soa-sign-toggle { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + border-radius: 6px; + font-size: 12px; + font-weight: 600; + border: 1px solid #e2e8f0; + cursor: pointer; + transition: all 0.15s; + + mat-icon { + font-size: 14px; + height: 14px; + width: 14px; + } + + &--mandatory { + background: #ede9fe; + color: #7c3aed; + border-color: #ddd6fe; + } + + &--optional { + background: #f8fafc; + color: #94a3b8; + } +} + +.soa-mandatory-count { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 12px; + font-weight: 600; + color: #7c3aed; + + mat-icon { + font-size: 14px; + height: 14px; + width: 14px; + } +} + +.soa-alert { + display: flex; + align-items: center; + gap: 8px; + margin-top: 10px; + padding: 8px 14px; + border-radius: 8px; + font-size: 13px; + font-weight: 500; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + } + + &--warn { + background: #fef3c7; + color: #b45309; + } + + &--ok { + background: #dcfce7; + color: #15803d; + } +} + +.soa-empty { + display: flex; + align-items: center; + gap: 10px; + padding: 20px; + background: #f8fafc; + border-radius: 8px; + font-size: 13px; + color: #64748b; + + mat-icon { + color: #94a3b8; + font-size: 22px; + height: 22px; + width: 22px; + } +} + +.soa-add-btn { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; +} + +.soa-mode-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 10px; + + @media (width <= 700px) { + grid-template-columns: repeat(2, 1fr); + } +} + +.soa-mode-card { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + padding: 12px 8px; + border: 2px solid #e2e8f0; + border-radius: 10px; + background: #fff; + cursor: pointer; + transition: all 0.15s; + + &:hover { + border-color: #a5b4fc; + } + + &--selected { + border-color: #7c3aed; + background: #faf5ff; + } + + &__icon { + font-size: 24px; + height: 24px; + width: 24px; + color: #7c3aed; + } + + &__code { + font-size: 13px; + font-weight: 700; + color: #1f2937; + } + + &__desc { + font-size: 11px; + color: #6b7280; + text-align: center; + } + + &__check { + position: absolute; + top: 6px; + right: 6px; + font-size: 16px; + height: 16px; + width: 16px; + color: #7c3aed; + } +} + +.soa-mode-summary { + display: flex; + align-items: center; + gap: 8px; + margin-top: 12px; + padding: 8px 14px; + border-radius: 8px; + font-size: 13px; + background: #f8fafc; + color: #64748b; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + } + + &--set { + background: #faf5ff; + color: #7c3aed; + } + + &__from { + font-size: 11px; + color: #94a3b8; + } +} + +.soa-auto-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 3px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 700; + background: #fef9c3; + color: #854d0e; + border: 1px solid #fde68a; + + mat-icon { + font-size: 13px; + height: 13px; + width: 13px; + } +} + +.soa-minor-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 4px 16px; + margin-bottom: 16px; + + @media (width <= 700px) { + grid-template-columns: 1fr; + } +} + +.soa-field { + width: 100%; +} + +.soa-guardian-search { + margin-bottom: 12px; + + &__label { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 600; + color: #374151; + margin-bottom: 8px; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + color: #7c3aed; + } + } +} + +.soa-guardian-selected { + display: flex; + align-items: center; + gap: 12px; + padding: 10px 14px; + background: #f0fdf4; + border: 1px solid #bbf7d0; + border-radius: 8px; + margin-top: 10px; + + &__icon { + font-size: 24px; + height: 24px; + width: 24px; + color: #16a34a; + } + + &__name { + font-size: 13px; + font-weight: 600; + color: #1f2937; + } + + &__cif { + font-size: 11px; + color: #6b7280; + } +} + +.soa-sc-benefits { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 10px; + margin-bottom: 14px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.soa-sc-benefit { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px; + background: #f0fdf4; + border: 1px solid #bbf7d0; + border-radius: 8px; + + mat-icon { + color: #16a34a; + font-size: 20px; + height: 20px; + width: 20px; + flex-shrink: 0; + margin-top: 2px; + } + + strong { + display: block; + font-size: 13px; + color: #1f2937; + font-weight: 600; + } + + span { + font-size: 11px; + color: #6b7280; + } +} + +.soa-category-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 10px; + margin-bottom: 16px; + + @media (width <= 700px) { + grid-template-columns: repeat(2, 1fr); + } +} + +.soa-cat-tile { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + padding: 14px 10px; + border: 2px solid #e2e8f0; + border-radius: 10px; + background: #fff; + cursor: pointer; + transition: all 0.15s; + + &:hover { + border-color: #a5b4fc; + box-shadow: 0 2px 8px rgb(99 102 241 / 12%); + } + + &--selected { + &.soa-cat-tile--male { + border-color: #3b82f6; + background: #eff6ff; + box-shadow: 0 2px 12px rgb(59 130 246 / 20%); + } + + &.soa-cat-tile--female { + border-color: #ec4899; + background: #fdf2f8; + box-shadow: 0 2px 12px rgb(236 72 153 / 20%); + } + } + + &__icon { + font-size: 26px; + height: 26px; + width: 26px; + color: #64748b; + } + + &--selected.soa-cat-tile--male &__icon { + color: #3b82f6; + } + + &--selected.soa-cat-tile--female &__icon { + color: #ec4899; + } + + &__label { + font-size: 11px; + font-weight: 600; + color: #374151; + text-align: center; + line-height: 1.3; + } + + &__check { + position: absolute; + top: 6px; + right: 6px; + font-size: 16px; + height: 16px; + width: 16px; + + .soa-cat-tile--male & { + color: #3b82f6; + } + + .soa-cat-tile--female & { + color: #ec4899; + } + } +} + +.soa-info-banner { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px 14px; + border-radius: 8px; + font-size: 12px; + background: #eff6ff; + border: 1px solid #bfdbfe; + color: #1e40af; + + mat-icon { + font-size: 18px; + height: 18px; + width: 18px; + flex-shrink: 0; + margin-top: 2px; + } + + ul { + margin: 4px 0 0; + padding-left: 18px; + + li { + margin-bottom: 3px; + } + } + + &--minor { + background: #fefce8; + border-color: #fde68a; + color: #92400e; + } + + &--senior { + background: #f0fdf4; + border-color: #bbf7d0; + color: #065f46; + } + + &--male { + background: #eff6ff; + border-color: #bfdbfe; + color: #1e40af; + } + + &--female { + background: #fdf2f8; + border-color: #f9a8d4; + color: #9d174d; + } +} + +// ── Account Feature Toggles ──────────────────────────────────────────────── +.rad-toggles { + display: flex; + flex-direction: column; + gap: 10px; + padding: 4px 0; +} + +.rad-toggle-card { + display: flex; + align-items: center; + justify-content: space-between; + padding: 14px 18px; + border: 1.5px solid #e2e8f0; + border-radius: 10px; + background: #fff; + transition: + border-color 0.2s, + box-shadow 0.2s; + + &:hover { + border-color: #93c5fd; + box-shadow: 0 1px 6px rgb(59 130 246 / 10%); + } + + &__left { + display: flex; + align-items: center; + gap: 12px; + + > i { + width: 36px; + height: 36px; + border-radius: 8px; + background: #f0f9ff; + border: 1px solid #bae6fd; + color: #0284c7; + display: flex; + align-items: center; + justify-content: center; + font-size: 15px; + flex: 0 0 auto; + } + } + + &__label { + font-size: 13.5px; + font-weight: 600; + color: #1e293b; + } + + &__hint { + font-size: 11.5px; + color: #64748b; + margin-top: 2px; + } } diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts index 5deb072dc..1bf189d93 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-details-step/recurring-deposits-account-details-step.component.ts @@ -7,16 +7,21 @@ */ /** Angular Imports */ -import { Component, OnInit, Input, Output, EventEmitter, inject } from '@angular/core'; -import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +import { Component, OnInit, Input, Output, EventEmitter, inject, ChangeDetectorRef } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, Validators, FormsModule } from '@angular/forms'; import { SettingsService } from 'app/settings/settings.service'; +import { Dates } from 'app/core/utils/dates'; +import { MatDialog } from '@angular/material/dialog'; /** Custom Services */ import { RecurringDepositsService } from '../../recurring-deposits.service'; +import { ClientsService } from 'app/clients/clients.service'; import { MatTooltip } from '@angular/material/tooltip'; import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { MatIcon } from '@angular/material/icon'; +import { AddRelatedClientDialogComponent } from 'app/current-accounts/current-account-stepper/current-account-details-step/add-related-client-dialog/add-related-client-dialog.component'; /** * Recurring Deposits Account Details Step @@ -30,13 +35,19 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; MatTooltip, MatStepperPrevious, FaIconComponent, - MatStepperNext + MatStepperNext, + FormsModule, + MatIcon ] }) export class RecurringDepositsAccountDetailsStepComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); private recurringDepositsService = inject(RecurringDepositsService); private settingsService = inject(SettingsService); + private dateUtils = inject(Dates); + private cdr = inject(ChangeDetectorRef); + private clientsService = inject(ClientsService); + private dialog = inject(MatDialog); /** Recurring Deposits Account Template */ @Input() recurringDepositsAccountTemplate: any; @@ -52,19 +63,168 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { /** For edit recurring deposits form */ isFieldOfficerPatched = false; /** Recurring Deposits Account Details Form */ - recurringDepositAccountDetailsForm: UntypedFormGroup; + recurringDepositAccountDetailsForm!: UntypedFormGroup; isProductSelected = false; /** Recurring Deposits Account Template with product data */ @Output() recurringDepositsAccountProductTemplate = new EventEmitter(); - /** - * Sets recurring deposits account details form. - * @param {FormBuilder} formBuilder Form Builder. - * @param {RecurringDepositsService} recurringDepositsService Recurring Deposits Service. - * @param {SettingsService} settingsService Settings Service - */ + /** Local copy of product template for displaying classification info */ + productTemplate: any = null; + accountTypeOptions: string[] = []; + customerSegmentOptions: string[] = []; + selectedAccountType: string | null = null; + selectedCustomerSegment: string | null = null; + + // ── Client info (auto-fetched) ───────────────────────────────────────── + clientDob: Date | null = null; + clientAge: number | null = null; + clientGender: string | null = null; + primaryClientName = ''; + primaryClientCif = ''; + primaryClientId: number | null = null; + primaryHolderShare = 0; + + // ── Status of Account (RBI) ──────────────────────────────────────────── + readonly accountStatusOptions = [ + { value: 'INDIVIDUAL_MALE', label: 'Individual (Male)', icon: 'person', color: '#3b82f6' }, + { value: 'INDIVIDUAL_FEMALE', label: 'Individual (Female)', icon: 'person_outline', color: '#ec4899' }, + { value: 'SINGLE', label: 'Single Account', icon: 'account_circle', color: '#0891b2' }, + { value: 'JOINT', label: 'Joint Account', icon: 'group', color: '#8b5cf6' }, + { value: 'MINOR', label: 'Minor Account', icon: 'child_care', color: '#f59e0b' }, + { value: 'SENIOR_CITIZEN', label: 'Senior Citizen', icon: 'elderly', color: '#10b981' } + ]; + + selectedAccountStatus = ''; + + // ── Joint Account ────────────────────────────────────────────────────── + jointHolders: Array<{ + clientId: number; + clientName: string; + clientCif: string; + sharePercentage: number; + isMandatory: boolean; + signingAuthority?: string; + role?: string; + }> = []; + + modeOfOperation = ''; + + readonly modeOfOperationOptions = [ + { value: 'EITHER_OR_SURVIVOR', label: 'E or S', sublabel: 'Either or Survivor', icon: 'how_to_vote' }, + { value: 'FORMER_OR_SURVIVOR', label: 'F or S', sublabel: 'Former or Survivor', icon: 'person' }, + { value: 'JOINTLY', label: 'Jointly', sublabel: 'All Must Sign', icon: 'groups' }, + { value: 'ANYONE_OR_SURVIVOR', label: 'Anyone', sublabel: 'Anyone or Survivor', icon: 'group' } + ]; + + // ── Individual Male — Special Category ──────────────────────────────── + selectedSpecialCategory = ''; + + readonly specialCategoryOptions = [ + { value: 'DEFENCE', label: 'Defence Personnel', icon: 'military_tech' }, + { value: 'EX_SERVICEMAN', label: 'Ex-Serviceman', icon: 'shield' }, + { value: 'GOVT_EMPLOYEE', label: 'Government Employee', icon: 'account_balance' }, + { value: 'PHYSICALLY_CHALLENGED', label: 'Physically Challenged', icon: 'accessible' }, + { value: 'NRI', label: 'NRI (Non-Resident Indian)', icon: 'flight' }, + { value: 'NONE', label: 'None / General', icon: 'person' } + ]; + + // ── Individual Female — Weaker Section ──────────────────────────────── + selectedWeakerSection = ''; + + readonly weakerSectionOptions = [ + { value: 'BPL', label: 'BPL (Below Poverty Line)', icon: 'volunteer_activism' }, + { value: 'SC_ST', label: 'SC / ST Category', icon: 'groups' }, + { value: 'MINORITY', label: 'Minority Community', icon: 'diversity_3' }, + { value: 'SHG', label: 'Self Help Group Member', icon: 'handshake' }, + { value: 'PMJDY', label: 'PMJDY Beneficiary', icon: 'savings' }, + { value: 'NONE', label: 'None / General', icon: 'person_outline' } + ]; + + // ── Minor Account ────────────────────────────────────────────────────── + guardianClientId: number | null = null; + guardianName = ''; + guardianCif = ''; + guardianRelationship = ''; + minorDob: Date | null = null; + + readonly guardianRelationshipOptions = [ + 'Father', + 'Mother', + 'Legal Guardian', + 'Court Appointed Guardian' + ]; + + // ── Deposit Account Link ─────────────────────────────────────────────── + dalSelectedType: 'SAVINGS' | 'CURRENT' | null = null; + dalAllSavings: any[] = []; + dalAllCurrent: any[] = []; + dalSearch = ''; + dalLoading = false; + selectedLinkedAccount: any = null; + + get dalAccounts(): any[] { + return this.dalSelectedType === 'SAVINGS' ? this.dalAllSavings : this.dalAllCurrent; + } + + get dalFilteredAccounts(): any[] { + const q = this.dalSearch.toLowerCase().trim(); + if (!q) return this.dalAccounts; + return this.dalAccounts.filter( + (a) => a.accountNo.toLowerCase().includes(q) || a.productName.toLowerCase().includes(q) + ); + } + + selectDalType(type: 'SAVINGS' | 'CURRENT'): void { + this.dalSelectedType = type; + this.dalSearch = ''; + this.selectedLinkedAccount = null; + } + + selectLinkedAccount(account: any): void { + this.selectedLinkedAccount = account; + } + + clearLinkedAccount(): void { + this.selectedLinkedAccount = null; + this.dalSelectedType = null; + this.dalSearch = ''; + } + + private loadClientAccounts(): void { + const clientId = String(this.recurringDepositsAccountTemplate?.clientId); + if (!clientId) return; + this.dalLoading = true; + this.clientsService.getClientAccountData(clientId).subscribe({ + next: (data: any) => { + this.dalAllSavings = (data?.savingsAccounts ?? []).map((a: any) => ({ + id: a.id, + accountNo: a.accountNo, + productName: a.productName ?? '', + accountType: 'SAVINGS' as const, + status: a.status?.value ?? '', + balance: a.accountBalance ?? 0, + currencyCode: a.currency?.code ?? '' + })); + this.dalAllCurrent = (data?.currentAccounts ?? []).map((a: any) => ({ + id: a.id, + accountNo: a.accountNo, + productName: a.productName ?? '', + accountType: 'CURRENT' as const, + status: a.status?.value ?? '', + balance: a.accountBalance ?? 0, + currencyCode: a.currency?.code ?? '' + })); + this.dalLoading = false; + this.cdr.detectChanges(); + }, + error: () => { + this.dalLoading = false; + } + }); + } + constructor() { this.createRecurringDepositsAccountDetailsForm(); } @@ -82,8 +242,85 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { new Date(this.recurringDepositsAccountTemplate.timeline.submittedOnDate), externalId: this.recurringDepositsAccountTemplate.externalId }); + } else { + this.recurringDepositAccountDetailsForm.patchValue({ + submittedOnDate: this.settingsService.businessDate + }); } } + + // Auto-detect account status from client DOB + capture primary holder info + const clientId = this.recurringDepositsAccountTemplate?.clientId; + if (clientId) { + this.primaryClientId = clientId; + this.clientsService.getClientData(String(clientId)).subscribe((client: any) => { + this.primaryClientName = client?.displayName ?? client?.fullname ?? ''; + this.primaryClientCif = client?.cifNumber ?? client?.cifNo ?? ''; + if (client?.dateOfBirth) { + const dob = this.parseClientDob(client.dateOfBirth); + if (dob) { + this.clientDob = dob; + this.clientAge = this.calculateAge(dob); + this.clientGender = client.gender?.name?.toLowerCase() ?? null; + this.autoDetectAccountStatus(); + } + } + this.cdr.detectChanges(); + }); + } + } + + /** Parse DOB from backend — handles array [yyyy, mm, dd] or string */ + private parseClientDob(dob: any): Date | null { + if (!dob) return null; + if (Array.isArray(dob) && dob.length >= 3) { + return new Date(dob[0], dob[1] - 1, dob[2]); + } + const d = new Date(dob); + return isNaN(d.getTime()) ? null : d; + } + + /** Calculate full years from a date to today */ + private calculateAge(dob: Date): number { + const today = this.settingsService.businessDate ?? new Date(); + let age = today.getFullYear() - dob.getFullYear(); + const monthDiff = today.getMonth() - dob.getMonth(); + if (monthDiff < 0 || (monthDiff === 0 && today.getDate() < dob.getDate())) { + age--; + } + return age; + } + + isTileDisabled(value: string): boolean { + if (value === 'MINOR') return this.clientAge === null || this.clientAge >= 18; + if (value === 'SENIOR_CITIZEN') return this.clientAge === null || this.clientAge < 60; + return false; + } + + getTileTooltip(value: string): string { + if (value === 'MINOR' && this.clientAge !== null && this.clientAge >= 18) { + return `Age ${this.clientAge} yrs — Minor only allowed below 18 years`; + } + if (value === 'SENIOR_CITIZEN' && this.clientAge !== null && this.clientAge < 60) { + return `Age ${this.clientAge} yrs — Senior Citizen only allowed at 60+ years`; + } + if (value === 'MINOR' && this.clientAge === null) return 'DOB not available — cannot verify age'; + if (value === 'SENIOR_CITIZEN' && this.clientAge === null) return 'DOB not available — cannot verify age'; + return ''; + } + + private autoDetectAccountStatus(): void { + if (this.clientAge === null) return; + if (this.clientAge < 18) { + this.selectedAccountStatus = 'MINOR'; + this.minorDob = this.clientDob; + } else if (this.clientAge >= 60) { + this.selectedAccountStatus = 'SENIOR_CITIZEN'; + } else if (this.clientGender === 'female') { + this.selectedAccountStatus = 'INDIVIDUAL_FEMALE'; + } else { + this.selectedAccountStatus = 'INDIVIDUAL_MALE'; + } } /** @@ -100,7 +337,10 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { Validators.required ], fieldOfficerId: [''], - externalId: [''] + externalId: [''], + balanceSwapOption: [false], + msme: [false], + autoDebitFacility: [false] }); } @@ -109,29 +349,203 @@ export class RecurringDepositsAccountDetailsStepComponent implements OnInit { */ buildDependencies() { const clientId = this.recurringDepositsAccountTemplate.clientId; - this.recurringDepositAccountDetailsForm.get('productId').valueChanges.subscribe((productId: string) => { + this.recurringDepositAccountDetailsForm.get('productId')!.valueChanges.subscribe((productId: string) => { this.recurringDepositsService .getRecurringDepositsAccountTemplate(clientId, productId) .subscribe((response: any) => { + if (!response.currency) { + const selectedProduct = this.recurringDepositsAccountTemplate.productOptions?.find( + (product: any) => product.id == productId + ); + if (selectedProduct?.currency) { + response.currency = selectedProduct.currency; + } else if (this.recurringDepositsAccountTemplate.currency) { + response.currency = this.recurringDepositsAccountTemplate.currency; + } + } + this.productTemplate = response; + this.accountTypeOptions = this.extractOptions(response.accountTypeOptions, response.accountType); + this.customerSegmentOptions = this.extractOptions(response.customerSegmentOptions, response.customerSegment); + this.selectedAccountType = response.accountType ?? null; + this.selectedCustomerSegment = response.customerSegment ?? null; this.recurringDepositsAccountProductTemplate.emit(response); this.fieldOfficerData = response.fieldOfficerOptions; this.isProductSelected = true; + if (response.shareLinkingEnabled) { + this.loadClientAccounts(); + } + this.recurringDepositAccountDetailsForm.patchValue({ + balanceSwapOption: !!response.balanceSwapOption, + msme: !!response.msme, + autoDebitFacility: !!response.autoDebitFacility + }); + this.cdr.detectChanges(); if (!this.isFieldOfficerPatched && this.recurringDepositsAccountTemplate.fieldOfficerId) { this.recurringDepositAccountDetailsForm - .get('fieldOfficerId') + .get('fieldOfficerId')! .patchValue(this.recurringDepositsAccountTemplate.fieldOfficerId); this.isFieldOfficerPatched = true; } else { - this.recurringDepositAccountDetailsForm.get('fieldOfficerId').patchValue(''); + this.recurringDepositAccountDetailsForm.get('fieldOfficerId')!.patchValue(''); } }); }); } + hasValue(val: any): boolean { + return val !== null && val !== undefined && val !== ''; + } + + private extractOptions(options: any[], fallback?: string): string[] { + if (Array.isArray(options) && options.length > 0) { + return options.map((o: any) => o.value ?? o.name ?? o); + } + return fallback ? [fallback] : []; + } + + formatTemplateDate(value: any): string { + if (!value) return ''; + let dateValue: Date | null = null; + if (value instanceof Date) { + dateValue = value; + } else if (Array.isArray(value) && value.length >= 3) { + dateValue = new Date(value[0], value[1] - 1, value[2]); + } else { + dateValue = new Date(value); + } + if (!dateValue || Number.isNaN(dateValue.getTime())) { + return Array.isArray(value) ? value.join('/') : String(value); + } + return this.dateUtils.formatDate(dateValue, 'M/d/yyyy') || ''; + } + + onGuardianChange(event: { clientId: number; displayName: string; cifNumber?: string } | null): void { + this.guardianClientId = event?.clientId ?? null; + this.guardianName = event?.displayName ?? ''; + this.guardianCif = event?.cifNumber ?? ''; + } + + // ── Joint Account helpers ────────────────────────────────────────────── + openAddJointHolderDialog(): void { + const ref = this.dialog.open(AddRelatedClientDialogComponent, { + width: '500px', + panelClass: 'arc-dialog-panel', + disableClose: true + }); + ref.afterClosed().subscribe((result) => { + if (result) { + const duplicate = this.jointHolders.some((h) => h.clientCif && h.clientCif === result.clientCif); + if (duplicate) { + alert(`Account holder with CIF ${result.clientCif} is already added.`); + return; + } + this.jointHolders = [ + ...this.jointHolders, + { ...result, isMandatory: false }]; + this.autoSetModeOfOperation(); + } + }); + } + + removeJointHolder(index: number): void { + this.jointHolders = this.jointHolders.filter((_, i) => i !== index); + this.autoSetModeOfOperation(); + } + + toggleJointHolderMandatory(holder: any): void { + holder.isMandatory = !holder.isMandatory; + this.autoSetModeOfOperation(); + } + + autoSetModeOfOperation(): void { + const mandatory = this.jointHolders.filter((h) => h.isMandatory).length; + if (mandatory === 0) { + this.modeOfOperation = 'EITHER_OR_SURVIVOR'; + } else if (mandatory === 1) { + this.modeOfOperation = 'FORMER_OR_SURVIVOR'; + } else { + this.modeOfOperation = 'JOINTLY'; + } + } + + get totalJointSharePercentage(): number { + return (this.primaryHolderShare || 0) + this.jointHolders.reduce((sum, h) => sum + (h.sharePercentage || 0), 0); + } + + get mandatoryJointCount(): number { + return this.jointHolders.filter((h) => h.isMandatory).length; + } + + get selectedModeLabel(): string { + return this.modeOfOperationOptions.find((o) => o.value === this.modeOfOperation)?.sublabel ?? ''; + } + + get majorityDate(): string { + const dob = this.minorDob ?? this.clientDob; + if (!dob) return ''; + const d = new Date(dob); + d.setFullYear(d.getFullYear() + 18); + return this.dateUtils.formatDate(d, 'M/d/yyyy') || ''; + } + /** * Returns recurring deposits account details form value. */ get recurringDepositAccountDetails() { - return this.recurringDepositAccountDetailsForm.value; + const value = this.recurringDepositAccountDetailsForm.value; + + if (this.selectedAccountStatus) { + value.accountStatus = this.selectedAccountStatus; + } + + if (this.selectedAccountStatus === 'JOINT') { + value.modeOfOperation = this.modeOfOperation || null; + value.jointHolders = [ + { + clientId: this.primaryClientId, + clientName: this.primaryClientName, + clientCif: this.primaryClientCif, + sharePercentage: this.primaryHolderShare, + isMandatory: true, + isPrimary: true, + signingAuthority: 'Mandatory', + role: 'Primary Account Holder' + }, + ...this.jointHolders.map((h) => ({ + clientId: h.clientId, + clientName: h.clientName, + clientCif: h.clientCif, + sharePercentage: h.sharePercentage, + isMandatory: h.isMandatory, + isPrimary: false, + signingAuthority: h.signingAuthority || (h.isMandatory ? 'Mandatory' : 'Optional'), + role: h.role || 'Co-Holder' + })) + ]; + } + + if (this.selectedAccountStatus === 'INDIVIDUAL_MALE' && this.selectedSpecialCategory) { + value.specialCategory = this.selectedSpecialCategory; + } + + if (this.selectedAccountStatus === 'INDIVIDUAL_FEMALE' && this.selectedWeakerSection) { + value.weakerSection = this.selectedWeakerSection; + } + + if (this.selectedAccountStatus === 'MINOR') { + value.guardianClientId = this.guardianClientId; + value.guardianName = this.guardianName; + value.guardianCif = this.guardianCif; + value.guardianRelationship = this.guardianRelationship; + value.minorDob = this.minorDob ? this.dateUtils.formatDate(this.minorDob, 'yyyy-MM-dd') : null; + } + + if (this.selectedLinkedAccount) { + value.linkedAccountId = this.selectedLinkedAccount.id; + value.linkedAccountType = this.selectedLinkedAccount.accountType; + value.linkedAccountNo = this.selectedLinkedAccount.accountNo; + } + + return value; } } diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.html index 8c5d75118..aeda181b5 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.html @@ -7,164 +7,46 @@ -->
-
- {{ 'labels.inputs.name' | translate }}: - {{ recurringDepositsAccountProductTemplate.accountChart.name }} -
- -
- {{ 'labels.inputs.Valid from Date' | translate }}: - {{ recurringDepositsAccountProductTemplate?.accountChart.fromDate | dateFormat }} -
- -
- {{ 'labels.inputs.End Date' | translate }}: - {{ recurringDepositsAccountProductTemplate?.accountChart.endDate | dateFormat }} -
- -
- {{ 'labels.inputs.Description' | translate }}: - {{ recurringDepositsAccountProductTemplate?.accountChart.description }} -
- -
- {{ 'labels.inputs.Primary Grouping by Amount' | translate }}: - {{ - recurringDepositsAccountProductTemplate?.accountChart.isPrimaryGroupingByAmount | yesNo - }} -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Period' | translate }} - {{ chartSlab.fromPeriod }} - {{ chartSlab.toPeriod }} {{ - chartSlab.periodType.value | translateKey: 'catalogs' - }} - {{ 'labels.inputs.Amount Range' | translate }} - {{ chartSlab.amountRangeFrom | formatNumber }} - {{ chartSlab.amountRangeTo | formatNumber }} - {{ 'labels.inputs.Interest' | translate }}{{ chartSlab.annualInterestRate | formatNumber }} %{{ 'labels.inputs.Description' | translate }}{{ chartSlab.description }}{{ 'labels.inputs.Actions' | translate }} - - -
- -

- {{ 'labels.heading.Incentives' | translate }} -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Entity Type' | translate }} - {{ incentive.entityType.value }} - {{ 'labels.inputs.Attribute Name' | translate }} - {{ incentive.attributeName.value }} - {{ 'labels.inputs.Condition Type' | translate }} - {{ incentive.conditionType.value | titlecase }} - {{ 'labels.inputs.Attribute Value' | translate }} - {{ incentive.attributeValueDesc }} - {{ 'labels.inputs.Incentive Type' | translate }} - {{ incentive.incentiveType.value }} - {{ 'labels.inputs.Interest' | translate }} - {{ incentive.amount | formatNumber }} -
-
+ + @if (_activeRdVersion) { +
+
+ + Tenure & Interest Calculator +
+
+
+ + Enter Tenure (Days) + + Enter the number of days for your RD +
-
-
- -
- - + @if (tenureDays && tenureDays > 0) { + @if (applicableSlab) { +
+
+ Tenure + {{ tenureDays }} Days +
+
+ Applicable Slab + {{ applicableSlab.description }} +
+
+ Annual Interest Rate + {{ applicableSlab.annualInterestRate | number: '1.2-2' }}% +
+
+ } @else { +
+ + No matching slab found for {{ tenureDays }} days +
+ } + } +
+
+ } diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.scss b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.scss index 03ea8d084..12e438941 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.scss +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.scss @@ -6,6 +6,105 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +// ── Tenure Calculator ───────────────────────────────────────────────────────── +.tenure-calc { + border: 1px solid #e2e8f0; + border-radius: 10px; + overflow: hidden; + background: #fff; + box-shadow: 0 1px 4px rgb(0 0 0 / 5%); + margin-bottom: 1.25rem; + + &__header { + display: flex; + align-items: center; + gap: 8px; + background: #f0f9ff; + border-bottom: 1px solid #bae6fd; + padding: 0.65rem 1rem; + font-size: 0.85rem; + font-weight: 700; + color: #0369a1; + text-transform: uppercase; + letter-spacing: 0.05em; + + i { + font-size: 13px; + } + } + + &__body { + padding: 1rem 1.25rem; + } + + &__input-row { + display: flex; + align-items: flex-start; + gap: 1rem; + } + + &__field { + width: 260px; + } + + &__result { + margin-top: 1rem; + border: 1px solid #e0f2fe; + border-radius: 8px; + overflow: hidden; + } + + &__result-row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.55rem 1rem; + border-bottom: 1px solid #f0f9ff; + font-size: 0.88rem; + + &:last-child { + border-bottom: none; + } + + &--highlight { + background: #f0f9ff; + } + } + + &__result-label { + color: #64748b; + font-weight: 500; + } + + &__result-value { + color: #1e293b; + font-weight: 600; + } + + &__result-rate { + font-size: 1.25rem; + font-weight: 800; + color: #0369a1; + } + + &__no-match { + margin-top: 0.75rem; + padding: 0.6rem 1rem; + border-radius: 8px; + background: #fef2f2; + color: #dc2626; + font-size: 0.85rem; + font-weight: 600; + display: flex; + align-items: center; + gap: 6px; + + i { + font-size: 13px; + } + } +} + .margin-t { margin-top: 2em; } diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.ts index fdf29b5f3..07bc5a5e3 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-interest-rate-chart-step/recurring-deposits-account-interest-rate-chart-step.component.ts @@ -7,7 +7,7 @@ */ /** Angular Imports */ -import { Component, OnInit, Input, OnChanges, ViewChild } from '@angular/core'; +import { Component, OnInit, Input, OnChanges, ViewChild, Output, EventEmitter } from '@angular/core'; import { MatTable, MatColumnDef, @@ -21,13 +21,14 @@ import { MatRow } from '@angular/material/table'; import { trigger, state, style, transition, animate } from '@angular/animations'; -import { NgIf, NgSwitch, TitleCasePipe } from '@angular/common'; +import { NgIf, NgSwitch, TitleCasePipe, DecimalPipe } from '@angular/common'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper'; import { DateFormatPipe } from '../../../../pipes/date-format.pipe'; import { FormatNumberPipe } from '../../../../pipes/format-number.pipe'; import { YesnoPipe } from '../../../../pipes/yesno.pipe'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { FormsModule } from '@angular/forms'; /** * Recurring Deposits Account Interest Rate Chart Step @@ -45,6 +46,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; ], imports: [ ...STANDALONE_SHARED_IMPORTS, + FormsModule, MatTable, MatColumnDef, MatHeaderCellDef, @@ -69,7 +71,28 @@ export class RecurringDepositsAccountInterestRateChartStepComponent implements O @Input() recurringDepositsAccountTemplate: any; @Input() recurringDepositsAccountProductTemplate: any; - /** Interest Rate Chart Data */ + /** + * CBS base rate chart loaded from the active base-interest-rate service. + * When set, this table is shown instead of the legacy accountChart.chartSlabs. + */ + @Input() set rdInterestRates(rates: any[]) { + this._rdInterestRates = rates || []; + this._activeRdVersion = this._rdInterestRates.find((v: any) => v.isActive) ?? this._rdInterestRates[0] ?? null; + } + + _rdInterestRates: any[] = []; + _activeRdVersion: any = null; + + readonly rdSlabColumns = [ + 'srNo', + 'description', + 'fromPeriod', + 'toPeriod', + 'periodTypeLabel', + 'annualInterestRate' + ]; + + /** Interest Rate Chart Data (legacy – used when no custom rate chart is available) */ interestRateChartData: any = []; /** Columns to be displayed in interest rate chart table. */ chartSlabsDisplayedColumns: any[] = [ @@ -88,18 +111,34 @@ export class RecurringDepositsAccountInterestRateChartStepComponent implements O 'incentiveType', 'amount' ]; - /** Additional Column to disblac incentives table */ + /** Additional Column to display incentives table */ chartSlabsIncentivesDisplayedColumns: string[] = ['incentives']; /** Expand Chart Slab Index used in the view */ expandChartSlabIndex: number; + /** Tenure entered by user (in days) */ + tenureDays: number | null = null; + @ViewChild('chartsTable', { static: true }) chartsTableRef: MatTable; constructor() {} + /** Find the matching slab for the entered tenure */ + get applicableSlab(): any | null { + if (!this.tenureDays || !this._activeRdVersion?.slabs?.length) return null; + const days = this.tenureDays; + return ( + this._activeRdVersion.slabs.find((s: any) => { + const from = s.fromPeriod ?? 0; + const to = s.toPeriod ?? Infinity; + return days >= from && days <= to; + }) ?? null + ); + } + ngOnChanges() { if (this.recurringDepositsAccountProductTemplate) { - this.interestRateChartData = this.recurringDepositsAccountProductTemplate.accountChart.chartSlabs; + this.interestRateChartData = this.recurringDepositsAccountProductTemplate.accountChart?.chartSlabs ?? []; } } diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html index ed080c025..d3699f0a1 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.html @@ -6,416 +6,588 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
-

{{ 'labels.heading.Details' | translate }}

- - -
- {{ 'labels.inputs.Product' | translate }} - {{ - recurringDepositAccountData.productId | find: recurringDepositsAccountTemplate.productOptions : 'id' : 'name' - }} -
- -
- {{ 'labels.inputs.Submitted On' | translate }} - {{ recurringDepositAccountData.submittedOnDate | dateFormat }} -
- -
- {{ 'labels.inputs.Field Officer' | translate }} - {{ - recurringDepositAccountData.fieldOfficerId - | find: recurringDepositsAccountProductTemplate.fieldOfficerOptions : 'id' : 'displayName' - }} -
- -

{{ 'labels.heading.Currency' | translate }}

- - -
- {{ 'labels.inputs.Currency' | translate }} - {{ recurringDepositsAccountProductTemplate.currency.code }} -
- -
- {{ 'labels.inputs.Decimal Places' | translate }} - {{ recurringDepositsAccountProductTemplate.currency.decimalPlaces }} -
- -
- {{ 'labels.inputs.Currency In Multiples Of' | translate }} - {{ recurringDepositsAccountProductTemplate.currency.inMultiplesOf }} -
- -

{{ 'labels.heading.Terms' | translate }}

- -
- {{ 'labels.inputs.Interest Compounding Period' | translate }}: - {{ - recurringDepositAccountData.interestCompoundingPeriodType - | find: recurringDepositsAccountProductTemplate.interestCompoundingPeriodTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
- -
- {{ 'labels.inputs.Interest Posting Period' | translate }}: - {{ - recurringDepositAccountData.interestPostingPeriodType - | find: recurringDepositsAccountProductTemplate.interestPostingPeriodTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
- -
- {{ 'labels.inputs.Interest Calculated using' | translate }}: - {{ - recurringDepositAccountData.interestCalculationType - | find: recurringDepositsAccountProductTemplate.interestCalculationTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
- -
- {{ 'labels.inputs.Days in Year' | translate }}: - {{ - recurringDepositAccountData.interestCalculationDaysInYearType - | find: recurringDepositsAccountProductTemplate.interestCalculationDaysInYearTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
- -

{{ 'labels.heading.Settings' | translate }}

- - -
- {{ 'labels.inputs.Lock-in Period' | translate }}: - {{ recurringDepositAccountData.lockinPeriodFrequency }} - {{ - recurringDepositAccountData.lockinPeriodFrequencyType - | find: recurringDepositsAccountProductTemplate.lockinPeriodFrequencyTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
- -
- {{ 'labels.inputs.Recurring Deposit Amount' | translate }}: - {{ recurringDepositAccountData.mandatoryRecommendedDepositAmount | formatNumber }} -
- -
- {{ 'labels.inputs.Deposit Period' | translate }}: - {{ recurringDepositAccountData.depositPeriod }} - {{ - recurringDepositAccountData.depositPeriodFrequencyId - | find: recurringDepositsAccountProductTemplate.periodFrequencyTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
- -
- {{ 'labels.inputs.Deposit Frequency Same as Group/Center meeting' | translate }}: - {{ recurringDepositsAccountProductTemplate.isCalendarInherited | yesNo }} -
- - -
- {{ 'labels.inputs.Deposit Start Date' | translate }}: - {{ recurringDepositAccountData.expectedFirstDepositOnDate | dateFormat }} +
+ +
+
+ + {{ 'labels.heading.Details' | translate }}
- -
- {{ 'labels.inputs.Deposit Frequency' | translate }}: - {{ recurringDepositAccountData.recurringFrequency }} - {{ - recurringDepositAccountData.recurringFrequencyType - | find: recurringDepositsAccountProductTemplate.periodFrequencyTypeOptions : 'id' : 'value' - | translateKey: 'catalogs' - }} -
- - -
- {{ 'labels.inputs.Minimum Deposit Term' | translate }}: - {{ - recurringDepositsAccountProductTemplate.minDepositTerm - ? recurringDepositsAccountProductTemplate.minDepositTerm - : '' - }} - {{ - (this.recurringDepositsAccountProductTemplate.minDepositTermType - ? this.recurringDepositsAccountProductTemplate.minDepositTermType.value - : '' - ) | translateKey: 'catalogs' - }} -
- -
- {{ 'labels.inputs.In Multiples Of' | translate }}: - {{ - recurringDepositsAccountProductTemplate.inMultiplesOfDepositTerm - ? recurringDepositsAccountProductTemplate.inMultiplesOfDepositTerm - : '' - }} - {{ - (this.recurringDepositsAccountProductTemplate.inMultiplesOfDepositTermType - ? this.recurringDepositsAccountProductTemplate.inMultiplesOfDepositTermType.value - : '' - ) | translateKey: 'catalogs' - }} -
- -
- {{ 'labels.inputs.Maximum Deposit Term' | translate }} - {{ - recurringDepositsAccountProductTemplate.maxDepositTerm - ? recurringDepositsAccountProductTemplate.maxDepositTerm - : '' - }} - {{ - (this.recurringDepositsAccountProductTemplate.maxDepositTermType - ? this.recurringDepositsAccountProductTemplate.maxDepositTermType.value - : '' - ) | translateKey: 'catalogs' - }} -
- -

{{ 'labels.inputs.Interest Rate Chart' | translate }}

- - -
- {{ 'labels.inputs.name' | translate }}: - {{ recurringDepositsAccountProductTemplate?.accountChart.name }} -
- -
- {{ 'labels.inputs.Valid from Date' | translate }}: - {{ recurringDepositsAccountProductTemplate?.accountChart.fromDate | dateFormat }} -
- -
- {{ 'labels.inputs.End Date' | translate }}: - {{ recurringDepositsAccountProductTemplate?.accountChart.endDate | dateFormat }} -
- -
- {{ 'labels.inputs.Description' | translate }}: - {{ recurringDepositsAccountProductTemplate?.accountChart.description }} -
- -
- {{ 'labels.inputs.Primary Grouping by Amount' | translate }}: - {{ - recurringDepositsAccountProductTemplate?.accountChart.isPrimaryGroupingByAmount | yesNo - }} -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Period' | translate }} - {{ chartSlab.fromPeriod }} - {{ chartSlab.toPeriod }} {{ - chartSlab.periodType.value | translateKey: 'catalogs' - }} - {{ 'labels.inputs.Amount Range' | translate }} - {{ chartSlab.amountRangeFrom | formatNumber }} - {{ chartSlab.amountRangeTo | formatNumber }} - {{ 'labels.inputs.Interest' | translate }}{{ chartSlab.annualInterestRate | formatNumber }} %{{ 'labels.inputs.Description' | translate }}{{ chartSlab.description }}{{ 'labels.inputs.Actions' | translate }} - - -
- -

- {{ 'labels.heading.Incentives' | translate }} -

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Entity Type' | translate }} - {{ incentive.entityType.value }} - {{ 'labels.inputs.Attribute Name' | translate }} - {{ incentive.attributeName.value }} - {{ 'labels.inputs.Condition Type' | translate }} - {{ incentive.conditionType.value | titlecase }} - {{ 'labels.inputs.Attribute Value' | translate }} - {{ incentive.attributeValueDesc }} - {{ 'labels.inputs.Incentive Type' | translate }} - {{ incentive.incentiveType.value }} - {{ 'labels.inputs.Interest' | translate }} - {{ incentive.amount }} -
-
+
+
+
+ {{ 'labels.inputs.Product' | translate }} + {{ + recurringDepositAccountData?.productId + | find: recurringDepositsAccountTemplate?.productOptions : 'id' : 'name' + }}
-
- -
-

{{ 'labels.heading.Charges' | translate }}

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.name' | translate }} - {{ charge.name + ', ' + charge.currency.displaySymbol }} - {{ 'labels.inputs.Type' | translate }} - {{ charge.chargeCalculationType.value | translateKey: 'catalogs' }} - {{ 'labels.inputs.Amount' | translate }} - {{ charge.amount | formatNumber }} - {{ 'labels.inputs.Date' | translate }} - - {{ (charge.dueDate | dateFormat) || 'Unassigned' }} - - - {{ (charge.feeOnMonthDay | dateFormat) || 'Unassigned' }} - - - {{ 'labels.inputs.N/A' | translate }} - - {{ 'labels.inputs.Repayments Every' | translate }} - {{ charge.feeInterval || 'Not Provided' }} - {{ 'labels.inputs.Collected On' | translate }} - {{ charge.chargeTimeType.value }} -
+
+ {{ 'labels.inputs.Submitted On' | translate }} + {{ recurringDepositAccountData?.submittedOnDate | dateFormat }} +
+
+ {{ 'labels.inputs.Field Officer' | translate }} + {{ + recurringDepositAccountData?.fieldOfficerId + | find: recurringDepositsAccountProductTemplate?.fieldOfficerOptions : 'id' : 'displayName' + }} +
+ @if (recurringDepositAccountData?.externalId) { +
+ {{ 'labels.inputs.External Id' | translate }} + {{ recurringDepositAccountData.externalId }} +
+ } +
+
+ + + @if ( + recurringDepositsAccountProductTemplate?.accountType || + recurringDepositsAccountProductTemplate?.customerSegment || + recurringDepositsAccountProductTemplate?.productCode || + recurringDepositsAccountProductTemplate?.effectiveDate || + recurringDepositsAccountProductTemplate?.endDate || + recurringDepositsAccountProductTemplate?.introducerRequired !== undefined || + recurringDepositsAccountProductTemplate?.shareLinkingEnabled !== undefined + ) { +
+
+ + Product Classification + From Product +
+
+
+ @if (recurringDepositsAccountProductTemplate?.accountType) { +
+ Account Type + {{ recurringDepositsAccountProductTemplate.accountType }} +
+ } + @if (recurringDepositsAccountProductTemplate?.customerSegment) { +
+ Customer Segment + {{ recurringDepositsAccountProductTemplate.customerSegment }} +
+ } + @if (recurringDepositsAccountProductTemplate?.productCode) { +
+ Product Code + {{ recurringDepositsAccountProductTemplate.productCode }} +
+ } + @if (recurringDepositsAccountProductTemplate?.effectiveDate) { +
+ Effective Date + {{ + recurringDepositsAccountProductTemplate.effectiveDate | dateFormat + }} +
+ } + @if (recurringDepositsAccountProductTemplate?.endDate) { +
+ End Date + {{ recurringDepositsAccountProductTemplate.endDate | dateFormat }} +
+ } + @if ( + recurringDepositsAccountProductTemplate?.introducerRequired !== undefined && + recurringDepositsAccountProductTemplate?.introducerRequired !== null + ) { +
+ Introducer Required + + {{ recurringDepositsAccountProductTemplate.introducerRequired | yesNo }} + +
+ } + @if ( + recurringDepositsAccountProductTemplate?.shareLinkingEnabled !== undefined && + recurringDepositsAccountProductTemplate?.shareLinkingEnabled !== null + ) { +
+ Deposit Account Link + + {{ recurringDepositsAccountProductTemplate.shareLinkingEnabled | yesNo }} + +
+ } +
+
+
+ } + + + @if (recurringDepositAccountData?.introducerId) { +
+
+ + Introducer Details +
+
+
+
+ Name + {{ recurringDepositAccountData?.introducerName || '—' }} +
+ @if (recurringDepositAccountData?.introducerCif) { +
+ CIF No + {{ recurringDepositAccountData.introducerCif }} +
+ } +
+ Client ID + {{ recurringDepositAccountData.introducerId }} +
+
+
+
+ } + + +
+
+ + {{ 'labels.heading.Currency' | translate }} +
+
+
+
+ {{ 'labels.inputs.Currency' | translate }} + {{ currencyName }} +
+
+ {{ 'labels.inputs.Decimal Places' | translate }} + {{ decimalPlaces }} +
+
+ {{ 'labels.inputs.Currency In Multiples Of' | translate }} + {{ inMultiplesOf }} +
+ @if (recurringDepositsAccountProductTemplate?.withHoldTax) { +
+ {{ 'labels.inputs.Is Withhold Tax Applicable' | translate }} + + {{ recurringDepositAccountData?.withHoldTax | yesNo }} + +
+ } + @if (recurringDepositAccountData?.withHoldTax && recurringDepositsAccountProductTemplate?.taxGroup?.name) { +
+ {{ 'labels.inputs.Tax Group' | translate }} + {{ recurringDepositsAccountProductTemplate.taxGroup.name }} +
+ } +
+ Balance Swap Option + + {{ rdCurrencyFlags?.balanceSwapOption | yesNo }} + +
+
+ MSME + + {{ rdCurrencyFlags?.msme | yesNo }} + +
+
+ Auto Debit Facility + + {{ rdCurrencyFlags?.autoDebitFacility | yesNo }} + +
+ @if ( + recurringDepositsAccountProductTemplate?.dormancyAlert !== undefined && + recurringDepositsAccountProductTemplate?.dormancyAlert !== null + ) { +
+ Dormancy Alert + + {{ recurringDepositsAccountProductTemplate.dormancyAlert | yesNo }} + +
+ } + @if ( + recurringDepositsAccountProductTemplate?.nominationFacility !== undefined && + recurringDepositsAccountProductTemplate?.nominationFacility !== null + ) { +
+ Nomination Facility + + {{ recurringDepositsAccountProductTemplate.nominationFacility | yesNo }} + +
+ } +
+
+
+ + + @if ( + recurringDepositsAccountProductTemplate?.dormancyAlert || + recurringDepositsAccountProductTemplate?.isDormancyTrackingActiveFromProduct + ) { +
+
+ + Dormancy Monitoring + + {{ recurringDepositsAccountProductTemplate?.isDormancyTrackingActiveFromProduct ? 'Active' : 'Inactive' }} + +
+
+
+
+
+
+ Inactive + + @if (recurringDepositsAccountProductTemplate?.daysToInactiveFromProduct != null) { + After {{ recurringDepositsAccountProductTemplate.daysToInactiveFromProduct }} days of no transaction + } @else { + Not configured + } + +
+
+
+
+
+
+ Dormant + + @if (recurringDepositsAccountProductTemplate?.daysToDormancyFromProduct != null) { + {{ recurringDepositsAccountProductTemplate.daysToDormancyFromProduct }} days after inactive + } @else { + Not configured + } + +
+
+
+
+
+
+ Escheat + + @if (recurringDepositsAccountProductTemplate?.daysToEscheatFromProduct != null) { + {{ recurringDepositsAccountProductTemplate.daysToEscheatFromProduct }} days after dormant + } @else { + Not configured + } + +
+
+
+
+ + Alert + {{ recurringDepositsAccountProductTemplate?.dormancyAlert ? 'Enabled' : 'Disabled' }} + + + Tracking + {{ recurringDepositsAccountProductTemplate?.isDormancyTrackingActiveFromProduct ? 'Active' : 'Inactive' }} + +
+
+
+ } + + + @if (rdNominee) { +
+
+ + Nominee Details + + {{ rdNominee?.mode === 'linked' ? 'Linked Client' : 'Manual Entry' }} + +
+
+
+
+ Name + {{ rdNominee?.fullName || rdNominee?.displayName || '—' }} +
+ @if (rdNominee?.cifNumber) { +
+ CIF No + {{ rdNominee.cifNumber }} +
+ } +
+ Relationship + {{ rdNominee?.relationship || '—' }} +
+
+ Share % + {{ rdNominee?.nomineeShare ?? '—' }}% +
+ @if (rdNominee?.dateOfBirth) { +
+ Date of Birth + {{ rdNominee?.dateOfBirth | dateFormat }} +
+ } + @if (rdNominee?.nationality) { +
+ Nationality + {{ rdNominee.nationality }} +
+ } + @if (rdNominee?.mobileNo) { +
+ Mobile No + {{ rdNominee.mobileNo }} +
+ } + @if (rdNominee?.aadhaarNumber) { +
+ Aadhaar No + {{ rdNominee.aadhaarNumber }} +
+ } + @if (rdNominee?.panNumber) { +
+ PAN No + {{ rdNominee.panNumber }} +
+ } + @if (rdNominee?.guardianName) { +
+ Guardian Name + {{ rdNominee.guardianName }} +
+ } + @if (rdNominee?.guardianRelationship) { +
+ Guardian Relationship + {{ rdNominee.guardianRelationship }} +
+ } + @if (rdNominee?.remarks) { +
+ Remarks + {{ rdNominee.remarks }} +
+ } +
+
+
+ } + + + @if (_activeRdVersion || recurringDepositsAccountProductTemplate?.accountChart) { +
+
+ + Interest Rate + @if (_activeRdVersion) { + {{ _activeRdVersion.versionLabel }} + } +
+
+ @if (_activeRdVersion) { + @if (rdTenureDays && rdApplicableSlab) { +
+
+ Tenure + {{ rdTenureDays }} Days +
+
+ Applicable Slab + {{ rdApplicableSlab.description }} +
+
+ Annual Interest Rate + {{ rdApplicableSlab.annualInterestRate | formatNumber }}% +
+
+ } @else { + No tenure entered — interest rate not calculated + } + } @else { +
+ @if (recurringDepositsAccountProductTemplate?.accountChart?.name) { +
+ {{ 'labels.inputs.name' | translate }} + {{ recurringDepositsAccountProductTemplate.accountChart.name }} +
+ } + @if (recurringDepositsAccountProductTemplate?.accountChart?.fromDate) { +
+ {{ 'labels.inputs.Valid from Date' | translate }} + {{ + recurringDepositsAccountProductTemplate.accountChart.fromDate | dateFormat + }} +
+ } + @if (recurringDepositsAccountProductTemplate?.accountChart?.endDate) { +
+ {{ 'labels.inputs.End Date' | translate }} + {{ + recurringDepositsAccountProductTemplate.accountChart.endDate | dateFormat + }} +
+ } + @if (recurringDepositsAccountProductTemplate?.accountChart?.description) { +
+ {{ 'labels.inputs.Description' | translate }} + {{ + recurringDepositsAccountProductTemplate.accountChart.description + }} +
+ } +
+ {{ 'labels.inputs.Primary Grouping by Amount' | translate }} + {{ + recurringDepositsAccountProductTemplate?.accountChart?.isPrimaryGroupingByAmount | yesNo + }} +
+
+ + @if (interestRateChartData?.length) { + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.Period' | translate }} + {{ chartSlab.fromPeriod }} - {{ chartSlab.toPeriod }} {{ + chartSlab.periodType?.value | translateKey: 'catalogs' + }} + {{ 'labels.inputs.Amount Range' | translate }} + {{ chartSlab.amountRangeFrom | formatNumber }} - {{ chartSlab.amountRangeTo | formatNumber }} + {{ 'labels.inputs.Interest' | translate }}{{ chartSlab.annualInterestRate | formatNumber }} %{{ 'labels.inputs.Description' | translate }}{{ chartSlab.description }}{{ 'labels.inputs.Actions' | translate }} + + +
+ +

{{ 'labels.heading.Incentives' | translate }}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.Entity Type' | translate }}{{ incentive.entityType?.value }}{{ 'labels.inputs.Attribute Name' | translate }}{{ incentive.attributeName?.value }}{{ 'labels.inputs.Condition Type' | translate }}{{ incentive.conditionType?.value | titlecase }}{{ 'labels.inputs.Attribute Value' | translate }}{{ incentive.attributeValueDesc }}{{ 'labels.inputs.Incentive Type' | translate }}{{ incentive.incentiveType?.value }}{{ 'labels.inputs.Interest' | translate }}{{ incentive.amount }}
+
+
+
+ } + } +
+
+ }
diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.scss b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.scss index 4c9f978a6..99d36156b 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.scss +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.scss @@ -6,35 +6,150 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -table { - width: 100%; +// ── Wrapper ────────────────────────────────────────────────────────────────── +.preview-wrapper { + display: flex; + flex-direction: column; + gap: 1.25rem; + padding: 0.5rem 0; } -.mat-elevation-z1 { - margin: 1em 0 1.5em; +// ── Section card ───────────────────────────────────────────────────────────── +.preview-section { + border: 1px solid #e0e0e0; + border-radius: 8px; + overflow: hidden; + background: #fff; + box-shadow: 0 1px 3px rgb(0 0 0 / 6%); + + &__header { + display: flex; + align-items: center; + gap: 8px; + background: #f5f5f5; + border-bottom: 1px solid #e0e0e0; + padding: 0.6rem 1rem; + } + + &__icon { + font-size: 13px; + color: #0ea5e9; + width: 16px; + text-align: center; + } + + &__title { + flex: 1; + font-size: 0.95rem; + font-weight: 600; + color: #333; + text-transform: uppercase; + letter-spacing: 0.04em; + } + + &__badge { + display: inline-flex; + align-items: center; + padding: 2px 10px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + background: #dbeafe; + color: #1d4ed8; + border: 1px solid #bfdbfe; + text-transform: uppercase; + + &--active { + background: #dcfce7 !important; + color: #15803d !important; + border-color: #bbf7d0 !important; + } + } } -h2, -h3, -h4 { - margin: 0; +// ── Body ───────────────────────────────────────────────────────────────────── +.preview-body { + padding: 1rem 1.25rem; +} + +// ── Subsection (inside Settings) ───────────────────────────────────────────── +.preview-subsection { + margin-bottom: 1rem; + + &__title { + font-size: 0.82rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + color: #666; + margin-bottom: 0.6rem; + } +} + +.preview-divider { + margin: 0.75rem 0 1rem; +} + +// ── Grid ───────────────────────────────────────────────────────────────────── +.preview-grid { + display: grid; + gap: 0.75rem 1.5rem; + + &--three { + grid-template-columns: repeat(3, 1fr); + + @media (width <= 768px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (width <= 480px) { + grid-template-columns: 1fr; + } + } +} + +// ── Field ───────────────────────────────────────────────────────────────────── +.preview-field { + display: flex; + flex-direction: column; + gap: 0.15rem; +} + +.preview-label { + font-size: 0.72rem; + color: #888; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.preview-value { + font-size: 0.92rem; + color: #222; font-weight: 500; } -span { - margin: 0.5em 0; +// ── Yes/No badge ───────────────────────────────────────────────────────────── +.preview-badge { + display: inline-block; + padding: 0.1rem 0.55rem; + border-radius: 12px; + font-size: 0.78rem; + font-weight: 600; + background: #ffebee; + color: #c62828; + width: fit-content; + + &--yes { + background: #e8f5e9; + color: #2e7d32; + } } -mat-divider { - margin: 0 0 0.5em; -} - -.margin-t { - margin-top: 1em; -} - -.margin-b { - margin-bottom: 0.25em; +// ── Tables ─────────────────────────────────────────────────────────────────── +.preview-table { + width: 100%; + margin-top: 1rem; } .irc-table { @@ -56,3 +171,241 @@ mat-divider { } } } + +.margin-t { + margin-top: 1em; +} + +// ── Preview Tenure Result Card ───────────────────────────────────────────────── +.preview-tenure-result { + border: 1px solid #e0f2fe; + border-radius: 10px; + overflow: hidden; + + &__row { + display: flex; + justify-content: space-between; + align-items: center; + padding: 0.65rem 1rem; + border-bottom: 1px solid #f0f9ff; + + &:last-child { + border-bottom: none; + } + + &--highlight { + background: #f0f9ff; + } + } + + &__rate { + font-size: 1.5rem; + font-weight: 800; + color: #0369a1; + } +} + +// ── Misc ────────────────────────────────────────────────────────────────────── +.preview-muted { + color: #94a3b8; + font-style: italic; +} + +.preview-mono { + font-family: monospace; + font-size: 0.88rem; + color: #334155; +} + +// ── Dormancy timeline ───────────────────────────────────────────────────────── +.preview-dormancy-timeline { + display: flex; + align-items: flex-start; + margin-bottom: 14px; +} + +.preview-dorm-step { + display: flex; + flex-direction: column; + align-items: center; + gap: 6px; + flex: 1; + + &__dot { + width: 36px; + height: 36px; + border-radius: 50%; + border: 3px solid; + display: flex; + align-items: center; + justify-content: center; + flex-shrink: 0; + + &--inactive { + border-color: #f59e0b; + background: #fffbeb; + + &::after { + content: ''; + width: 10px; + height: 10px; + border-radius: 50%; + background: #f59e0b; + } + } + + &--dormant { + border-color: #ef4444; + background: #fef2f2; + + &::after { + content: ''; + width: 10px; + height: 10px; + border-radius: 50%; + background: #ef4444; + } + } + + &--escheat { + border-color: #7c3aed; + background: #f5f3ff; + + &::after { + content: ''; + width: 10px; + height: 10px; + border-radius: 50%; + background: #7c3aed; + } + } + } + + &__body { + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + gap: 2px; + } + + &__label { + font-size: 12px; + font-weight: 700; + color: #1e293b; + } + + &__days { + font-size: 10.5px; + color: #64748b; + line-height: 1.3; + } +} + +.preview-dorm-connector { + flex: 0 0 40px; + height: 3px; + background: linear-gradient(90deg, #e2e8f0, #cbd5e1); + margin-top: 16px; + border-radius: 2px; +} + +.preview-dorm-flags { + display: flex; + gap: 10px; + flex-wrap: wrap; +} + +.preview-dorm-flag { + display: inline-flex; + align-items: center; + gap: 5px; + padding: 4px 12px; + border-radius: 20px; + font-size: 11.5px; + font-weight: 600; + background: #f1f5f9; + color: #94a3b8; + border: 1px solid #e2e8f0; + + i { + font-size: 11px; + } + + &--on { + background: #f0fdf4; + color: #15803d; + border-color: #bbf7d0; + } +} + +// ── Product feature flag rows (same style as details step) ──────────────────── +.preview-feature-list { + margin-top: 1rem; + border-top: 1px solid #e0e0e0; +} + +.preview-feature-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.75rem 0; + border-bottom: 1px solid #f0f0f0; + gap: 1rem; +} + +.preview-feature-info { + display: flex; + flex-direction: column; + gap: 0.15rem; + + strong { + font-size: 0.88rem; + font-weight: 600; + color: #222; + } + + span { + font-size: 0.78rem; + color: #26a69a; + margin: 0; + } +} + +.preview-feature-choices { + display: flex; + gap: 0.5rem; + flex-shrink: 0; +} + +.rad-yn { + display: inline-flex; + align-items: center; + gap: 0.3rem; + padding: 0.2rem 0.75rem; + border-radius: 20px; + border: 2px solid #ccc; + font-size: 0.82rem; + color: #999; + cursor: default; + + &::before { + content: ''; + width: 12px; + height: 12px; + border-radius: 50%; + border: 2px solid #ccc; + display: inline-block; + } + + &--active { + border-color: #4caf50; + color: #2e7d32; + font-weight: 600; + + &::before { + border-color: #4caf50; + background: #4caf50; + } + } +} diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.ts b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.ts index d3d3a1a9f..f67e5f035 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.ts +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-preview-step/recurring-deposits-account-preview-step.component.ts @@ -73,9 +73,41 @@ export class RecurringDepositsAccountPreviewStepComponent implements OnChanges { @Input() recurringDepositsAccountTemplate: any; @Input() recurringDepositsAccountProductTemplate: any; @Input() recurringDepositAccountData: any; + @Input() rdNominee: any = null; + @Input() rdCurrencyFlags: any = null; + @Input() rdTenureDays: number | null = null; + @Input() rdApplicableSlab: any = null; + @Input() set rdInterestRates(rates: any[]) { + this._rdInterestRates = rates || []; + this._activeRdVersion = this._rdInterestRates.find((v: any) => v.isActive) ?? this._rdInterestRates[0] ?? null; + } /** Output the submit action */ @Output() submitEvent = new EventEmitter(); + get currencyName(): string { + return this.recurringDepositsAccountProductTemplate?.currency?.name || '—'; + } + + get decimalPlaces(): string | number { + return this.recurringDepositsAccountProductTemplate?.currency?.decimalPlaces ?? '—'; + } + + get inMultiplesOf(): string | number { + return this.recurringDepositsAccountProductTemplate?.currency?.inMultiplesOf ?? '—'; + } + + _rdInterestRates: any[] = []; + _activeRdVersion: any = null; + + readonly rdSlabColumns = [ + 'srNo', + 'description', + 'fromPeriod', + 'toPeriod', + 'periodTypeLabel', + 'annualInterestRate' + ]; + /** Charges Displayed Columns */ chargesDisplayedColumns: string[] = [ 'name', @@ -113,7 +145,7 @@ export class RecurringDepositsAccountPreviewStepComponent implements OnChanges { ngOnChanges() { if (this.recurringDepositsAccountProductTemplate) { - this.interestRateChartData = this.recurringDepositsAccountProductTemplate.accountChart.chartSlabs; + this.interestRateChartData = this.recurringDepositsAccountProductTemplate.accountChart?.chartSlabs ?? []; } } } diff --git a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-settings-step/recurring-deposits-account-settings-step.component.html b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-settings-step/recurring-deposits-account-settings-step.component.html index 083d8f2bb..696f8780a 100644 --- a/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-settings-step/recurring-deposits-account-settings-step.component.html +++ b/src/app/deposits/recurring-deposits/recurring-deposits-account-stepper/recurring-deposits-account-settings-step/recurring-deposits-account-settings-step.component.html @@ -7,219 +7,145 @@ -->
-
- - {{ 'labels.inputs.Is Mandatory Deposit' | translate }}? - - - - {{ 'labels.inputs.Adjust advance payments toward future installments' | translate }}? - - - - {{ 'labels.inputs.Allow Withdrawals' | translate }}? - - -

{{ 'labels.heading.Lock-in Period' | translate }}

- - - {{ 'labels.inputs.Frequency' | translate }} - - - - - {{ 'labels.inputs.Type' | translate }} - - @for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) { - - {{ lockinPeriodFrequencyType.value | translateKey: 'catalogs' }} - - } - - - -

{{ 'labels.heading.Recurring Deposit Details' | translate }}

- @if (currency) { - - - } - -

{{ 'labels.heading.Deposit Period' | translate }}

- - - {{ 'labels.inputs.Deposit Period' | translate }} - - - - - {{ 'labels.inputs.Type' | translate }} - - @for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) { - - {{ periodFrequencyType.value | translateKey: 'catalogs' }} - - } - - - - - {{ 'labels.inputs.Deposit Frequency Same as Group/Center meeting' | translate }} - - - @if (!recurringDepositAccountSettingsForm.value.isCalendarInherited) { - - {{ 'labels.inputs.Deposit Start Date' | translate }} - - - - -

{{ 'labels.heading.Deposit Frequency' | translate }}

- - {{ 'labels.inputs.Deposit Frequency' | translate }} - - - {{ 'labels.inputs.Deposit Frequency' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - {{ 'labels.inputs.Type' | translate }} - - @for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) { - - {{ periodFrequencyType.value | translateKey: 'catalogs' }} - - } - - - {{ 'labels.inputs.Deposit Frequency Type' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - } - - - -

{{ 'labels.heading.Minimum Deposit Term' | translate }}

- - - {{ 'labels.inputs.Frequency' | translate }} - - - {{ 'labels.inputs.Minimum Deposit Term Frequency' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.Frequency Type' | translate }} - - @for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) { - - {{ periodFrequencyType.value | translateKey: 'catalogs' }} - - } - - - {{ 'labels.inputs.Minimum Deposit Term Frequency Type' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - -

{{ 'labels.heading.And thereafter, in Multiples of' | translate }}

- - - {{ 'labels.inputs.Frequency' | translate }} - - - - - {{ 'labels.inputs.Type' | translate }} - - @for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) { - - {{ periodFrequencyType.value | translateKey: 'catalogs' }} - - } - - - -

{{ 'labels.heading.Maximum Deposit Term' | translate }}

- - - {{ 'labels.inputs.Frequency' | translate }} - - - - - {{ 'labels.inputs.Type' | translate }} - - @for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) { - - {{ periodFrequencyType.value | translateKey: 'catalogs' }} - - } - - - - - -

{{ 'labels.heading.For Pre-mature closure' | translate }}

- - - {{ 'labels.inputs.Apply Penal Interest (less)' | translate }} - - - - {{ 'labels.inputs.Penal Interest' | translate }} (%) - - - - - {{ 'labels.inputs.Period' | translate }} - - @for (preClosurePenalInterestOnType of preClosurePenalInterestOnTypeData; track preClosurePenalInterestOnType) { - - {{ preClosurePenalInterestOnType.value | translateKey: 'catalogs' }} - - } - - - - - - - {{ 'labels.inputs.Minimum Balance For Interest Calculation' | translate }} - - - - @if (taxGroup) { -

{{ 'labels.heading.Tax Group' | translate }}

- - {{ 'labels.inputs.Is Withhold Tax Applicable' | translate }}? - - @if (recurringDepositAccountSettingsForm.controls.taxGroupId) { - - {{ 'labels.inputs.Tax Group' | translate }} - - - } - } + +
+
+ + Premature option +
+
+
+ Loan Facility + Premature Withdrawal option + Tenure Flexibility + Lien Marked + Apply Withdrawal Fee for Transfers +
+
+ +
+
+ + {{ 'labels.inputs.Lock-in Period' | translate }} +
+
+
+ Enable Lock-in Period + +
+ + @if (recurringDepositAccountSettingsForm.value.enableLockinPeriod) { +
+ + Frequency* + + + + + Type* + + @for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) { + + {{ lockinPeriodFrequencyType.value | translateKey: 'catalogs' }} + + } + + +
+ } +
+
+ + +
+
+ + Maturity Instructions +
+
+
+ Maturity Instructions + +
+
+ Option to credit maturity proceeds directly to the linked account or reinvest in another product (like + FD). + +
+
+ Penalty / Grace Period for Missed Installments + +
+
+ Panel Request Applicable + +
+
+ No penalty for nominee / legal heir closure + +
+
+ Deceased Settlement + +
+
+ Allow Debit Txn after due date + +
+
+
+ + +
+
+ + Additional Rules +
+
+
+
+ + Account Closed with Zero balance in Day Closing + +
+ + + Balance Required for Interest Calculation + + +
+
+
+ + + @if (recurringDepositAccountSettingsForm.contains('withHoldTax')) { +
+
+ + {{ 'labels.inputs.Is Withhold Tax Applicable' | translate }} +
+
+
+ {{ 'labels.inputs.Is Withhold Tax Applicable' | translate }} + +
+ @if ( + recurringDepositAccountSettingsForm.value.withHoldTax && + recurringDepositAccountSettingsForm.contains('taxGroupId') + ) { + + {{ 'labels.inputs.Tax Group' | translate }} + + + } +
+
+ } +
- @@ -71,7 +77,12 @@ - +
-

{{ 'labels.text.Welcome' | translate }}, {{ username }}

+

+ {{ 'labels.text.Welcome' | translate }}, {{ username }} +

{{ tenant | titlecase }} · CBSNexora Core Banking Platform

@@ -166,7 +182,11 @@ - }
@@ -75,11 +78,7 @@
-
-
diff --git a/src/app/login/login.component.scss b/src/app/login/login.component.scss index 38559830d..45298bd6c 100644 --- a/src/app/login/login.component.scss +++ b/src/app/login/login.component.scss @@ -12,90 +12,177 @@ $dark: #060f1e; $text-muted: #7a96bc; // ═══ Page Layout ═══ -.login-page { display: flex; flex-direction: column; height: 100vh; background: $dark; color: #fff; overflow: hidden; } -.login-split { display: flex; flex: 1; overflow: hidden; } +.login-page { + display: flex; + flex-direction: column; + height: 100vh; + background: $dark; + color: #fff; + overflow: hidden; +} + +.login-split { + display: flex; + flex: 1; + overflow: hidden; +} // ═══ LEFT PANEL — SLIDER ═══ .left-panel { - flex: 0 0 62%; position: relative; overflow: hidden; + flex: 0 0 62%; + position: relative; + overflow: hidden; background: linear-gradient(135deg, #040d1a 0%, #081c38 50%, #0a2a50 100%); &__dots-bg { - position: absolute; inset: 0; + position: absolute; + inset: 0; background-image: radial-gradient(circle, rgb(0 180 216 / 18%) 1px, transparent 1px); - background-size: 34px 34px; opacity: 0.35; pointer-events: none; + background-size: 34px 34px; + opacity: 0.35; + pointer-events: none; } } -.slides-wrap { position: absolute; inset: 0; z-index: 1; } +.slides-wrap { + position: absolute; + inset: 0; + z-index: 1; +} .slide { - position: absolute; inset: 0; - display: flex; flex-direction: column; - justify-content: center; align-items: flex-start; + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + justify-content: center; + align-items: flex-start; padding: 56px 60px; - opacity: 0; transition: opacity 0.85s ease; pointer-events: none; + opacity: 0; + transition: opacity 0.85s ease; + pointer-events: none; - &.active { opacity: 1; pointer-events: auto; } + &.active { + opacity: 1; + pointer-events: auto; + } &__glow { - position: absolute; top: -80px; right: -80px; - width: 520px; height: 520px; border-radius: 50%; pointer-events: none; + position: absolute; + top: -80px; + right: -80px; + width: 520px; + height: 520px; + border-radius: 50%; + pointer-events: none; } &__tag { - display: inline-flex; align-items: center; gap: 7px; - font-size: 0.68rem; font-weight: 600; - letter-spacing: 0.18em; text-transform: uppercase; - border-radius: 20px; padding: 4px 14px; margin-bottom: 16px; + display: inline-flex; + align-items: center; + gap: 7px; + font-size: 0.68rem; + font-weight: 600; + letter-spacing: 0.18em; + text-transform: uppercase; + border-radius: 20px; + padding: 4px 14px; + margin-bottom: 16px; } &__pulse { - width: 6px; height: 6px; border-radius: 50%; + width: 6px; + height: 6px; + border-radius: 50%; animation: dpulse 1.4s infinite; } &__icon { - font-size: 3.4rem; margin-bottom: 14px; display: block; - filter: drop-shadow(0 0 14px rgb(0 180 216 / 40%)); line-height: 1; + font-size: 3.4rem; + margin-bottom: 14px; + display: block; + filter: drop-shadow(0 0 14px rgb(0 180 216 / 40%)); + line-height: 1; } &__title { - font-size: 2.55rem; font-weight: 700; line-height: 1.1; - margin-bottom: 14px; max-width: 500px; + font-size: 2.55rem; + font-weight: 700; + line-height: 1.1; + margin-bottom: 14px; + max-width: 500px; text-shadow: 0 2px 24px rgb(0 0 0 / 50%); } &__desc { - font-size: 0.86rem; color: $text-muted; line-height: 1.7; - max-width: 460px; margin-bottom: 26px; + font-size: 0.86rem; + color: $text-muted; + line-height: 1.7; + max-width: 460px; + margin-bottom: 26px; } - &__stats { display: flex; gap: 12px; flex-wrap: wrap; } + &__stats { + display: flex; + gap: 12px; + flex-wrap: wrap; + } &__flow { - position: absolute; right: 52px; top: 50%; transform: translateY(-50%); - display: flex; flex-direction: column; align-items: center; - z-index: 2; opacity: 0.78; + position: absolute; + right: 52px; + top: 50%; + transform: translateY(-50%); + display: flex; + flex-direction: column; + align-items: center; + z-index: 2; + opacity: 0.78; } } // Slide color themes @mixin slide-theme($cls, $color) { .#{$cls} { - .slide__glow { background: radial-gradient(circle, rgba($color, 0.14) 0%, transparent 65%); } - .slide__tag { color: $color; border: 1px solid rgba($color, 0.4); background: rgba($color, 0.08); } - .slide__pulse { background: $color; } - ::ng-deep .hl { color: $color; } - .flow-node { color: $color; border-color: rgba($color, 0.25); } - .flow-arrow { background: linear-gradient(to bottom, rgba($color, 0.45), rgba($color, 0.05)); } - .flow-arrow::after { color: rgba($color, 0.5); } + .slide__glow { + background: radial-gradient(circle, rgba($color, 0.14) 0%, transparent 65%); + } + + .slide__tag { + color: $color; + border: 1px solid rgba($color, 0.4); + background: rgba($color, 0.08); + } + + .slide__pulse { + background: $color; + } + + ::ng-deep .hl { + color: $color; + } + + .flow-node { + color: $color; + border-color: rgba($color, 0.25); + } + + .flow-arrow { + background: linear-gradient(to bottom, rgba($color, 0.45), rgba($color, 0.05)); + } + + .flow-arrow::after { + color: rgba($color, 0.5); + } } } @include slide-theme('s1', #00b4d8); @include slide-theme('s2', #34d05c); -.s2 .slide__title { font-size: 2.1rem; } + +.s2 .slide__title { + font-size: 2.1rem; +} @include slide-theme('s3', #ffcc55); @include slide-theme('s4', #ff7070); @@ -105,76 +192,147 @@ $text-muted: #7a96bc; .stat-chip { background: rgb(255 255 255 / 4%); border: 1px solid rgb(255 255 255 / 10%); - border-radius: 10px; padding: 10px 18px; + border-radius: 10px; + padding: 10px 18px; backdrop-filter: blur(6px); - &__value { font-size: 1.2rem; font-weight: 700; line-height: 1; } - &__label { font-size: 0.62rem; color: $text-muted; margin-top: 2px; letter-spacing: 0.05em; } + &__value { + font-size: 1.2rem; + font-weight: 700; + line-height: 1; + } + + &__label { + font-size: 0.62rem; + color: $text-muted; + margin-top: 2px; + letter-spacing: 0.05em; + } } .flow-node { background: rgb(255 255 255 / 4%); border: 1px solid rgb(0 180 216 / 25%); - border-radius: 10px; padding: 9px 18px; - font-size: 0.71rem; font-weight: 600; letter-spacing: 0.06em; - white-space: nowrap; text-align: center; - backdrop-filter: blur(4px); min-width: 128px; + border-radius: 10px; + padding: 9px 18px; + font-size: 0.71rem; + font-weight: 600; + letter-spacing: 0.06em; + white-space: nowrap; + text-align: center; + backdrop-filter: blur(4px); + min-width: 128px; } .flow-arrow { - width: 1px; height: 20px; position: relative; + width: 1px; + height: 20px; + position: relative; background: linear-gradient(to bottom, rgb(0 180 216 / 45%), rgb(0 180 216 / 5%)); &::after { - content: '▼'; position: absolute; bottom: -6px; left: 50%; - transform: translateX(-50%); font-size: 0.38rem; + content: '▼'; + position: absolute; + bottom: -6px; + left: 50%; + transform: translateX(-50%); + font-size: 0.38rem; color: rgb(0 180 216 / 45%); } } // Nav dots .slide-nav { - position: absolute; bottom: 24px; left: 50%; transform: translateX(-50%); - display: flex; gap: 8px; z-index: 10; + position: absolute; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + display: flex; + gap: 8px; + z-index: 10; } .slide-dot { - width: 7px; height: 7px; border-radius: 50%; + width: 7px; + height: 7px; + border-radius: 50%; background: rgb(255 255 255 / 20%); - cursor: pointer; border: none; transition: all 0.3s; + cursor: pointer; + border: none; + transition: all 0.3s; - &.active { background: $accent; width: 22px; border-radius: 4px; } + &.active { + background: $accent; + width: 22px; + border-radius: 4px; + } } .slide-progress { - position: absolute; bottom: 0; left: 0; height: 2px; + position: absolute; + bottom: 0; + left: 0; + height: 2px; background: linear-gradient(90deg, $teal, $accent); - z-index: 10; animation: pbfill 5s linear infinite; + z-index: 10; + animation: pbfill 5s linear infinite; } // LincShield logo .lincshield-logo { margin-bottom: 18px; - svg { display: block; max-width: 100%; height: auto; } + svg { + display: block; + max-width: 100%; + height: auto; + } } -@keyframes pbfill { from { width: 0%; } to { width: 100%; } } +@keyframes pbfill { + from { + width: 0%; + } -@keyframes dpulse { 0%, 100% { opacity: 1; transform: scale(1); } 50% { opacity: 0.4; transform: scale(0.6); } } + to { + width: 100%; + } +} + +@keyframes dpulse { + 0%, + 100% { + opacity: 1; + transform: scale(1); + } + + 50% { + opacity: 0.4; + transform: scale(0.6); + } +} // ═══ RIGHT PANEL — LOGIN ═══ .right-panel { flex: 0 0 38%; - display: flex; flex-direction: column; - justify-content: center; align-items: center; - padding: 40px 48px; position: relative; overflow: hidden; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 40px 48px; + position: relative; + overflow: hidden; background: linear-gradient(160deg, #07101f 0%, #0b1d35 65%, #081528 100%); border-left: 1px solid rgb(0 180 216 / 14%); &::before { - content: ''; position: absolute; top: -130px; right: -130px; - width: 500px; height: 500px; border-radius: 50%; + content: ''; + position: absolute; + top: -130px; + right: -130px; + width: 500px; + height: 500px; + border-radius: 50%; background: radial-gradient(circle, rgb(0 180 216 / 7%) 0%, transparent 65%); pointer-events: none; } @@ -182,41 +340,73 @@ $text-muted: #7a96bc; // Brand .brand { - margin-bottom: 24px; align-self: center; + margin-bottom: 24px; + align-self: center; - &__logo img { height: 72px; width: auto; opacity: 0.95; } + &__logo img { + height: 72px; + width: auto; + opacity: 0.95; + } } // Welcome .welcome { - text-align: center; margin-bottom: 28px; width: 100%; max-width: 340px; + text-align: center; + margin-bottom: 28px; + width: 100%; + max-width: 340px; - h2 { font-size: 1.7rem; font-weight: 700; margin: 0 0 4px; } - p { font-size: 0.74rem; color: $text-muted; margin: 0; } + h2 { + font-size: 1.7rem; + font-weight: 700; + margin: 0 0 4px; + } + + p { + font-size: 0.74rem; + color: $text-muted; + margin: 0; + } } // Login form host ::ng-deep mifosx-login-form, ::ng-deep mifosx-reset-password, ::ng-deep mifosx-two-factor-authentication { - display: block; width: 100%; max-width: 340px; + display: block; + width: 100%; + max-width: 340px; } .right-footer { - position: absolute; bottom: 18px; left: 0; right: 0; - text-align: center; font-size: 0.61rem; - color: rgb(255 255 255 / 18%); letter-spacing: 0.05em; + position: absolute; + bottom: 18px; + left: 0; + right: 0; + text-align: center; + font-size: 0.61rem; + color: rgb(255 255 255 / 18%); + letter-spacing: 0.05em; } // ═══ SETTINGS ═══ .settings-btn { - position: absolute; top: 16px; right: 16px; z-index: 100; - width: 42px; height: 42px; border-radius: 50%; + position: absolute; + top: 16px; + right: 16px; + z-index: 100; + width: 42px; + height: 42px; + border-radius: 50%; border: 1px solid rgb(255 255 255 / 20%); background: rgb(255 255 255 / 8%); backdrop-filter: blur(12px); - color: #fff; cursor: pointer; - display: flex; align-items: center; justify-content: center; + color: #fff; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; transition: all 0.3s ease; &:hover { @@ -226,13 +416,20 @@ $text-muted: #7a96bc; } .settings-overlay { - position: fixed; inset: 0; z-index: 99; - background: rgb(0 0 0 / 30%); backdrop-filter: blur(2px); + position: fixed; + inset: 0; + z-index: 99; + background: rgb(0 0 0 / 30%); + backdrop-filter: blur(2px); } .settings-panel { - position: absolute; top: 64px; right: 16px; z-index: 101; - width: 320px; max-width: calc(100vw - 2rem); + position: absolute; + top: 64px; + right: 16px; + z-index: 101; + width: 320px; + max-width: calc(100vw - 2rem); background: rgb(255 255 255 / 10%); backdrop-filter: blur(24px) saturate(1.5); border-radius: 16px; @@ -243,121 +440,266 @@ $text-muted: #7a96bc; &__header { padding: 16px 20px 10px; border-bottom: 1px solid rgb(255 255 255 / 10%); - display: flex; align-items: center; gap: 8px; + display: flex; + align-items: center; + gap: 8px; - h3 { margin: 0; font-size: 0.95rem; font-weight: 600; color: #fff; } - mifosx-m3-icon { font-size: 18px; opacity: 0.8; } + h3 { + margin: 0; + font-size: 0.95rem; + font-weight: 600; + color: #fff; + } + + mifosx-m3-icon { + font-size: 18px; + opacity: 0.8; + } } &__body { padding: 12px 20px; - display: flex; flex-direction: column; gap: 8px; + display: flex; + flex-direction: column; + gap: 8px; } } .settings-item { - display: flex; align-items: center; justify-content: space-between; - padding: 6px 0; border-bottom: 1px solid rgb(255 255 255 / 6%); + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 0; + border-bottom: 1px solid rgb(255 255 255 / 6%); - &:last-child { border-bottom: none; } + &:last-child { + border-bottom: none; + } &__label { - font-size: 0.82rem; font-weight: 500; color: rgb(255 255 255 / 85%); - display: flex; align-items: center; gap: 8px; + font-size: 0.82rem; + font-weight: 500; + color: rgb(255 255 255 / 85%); + display: flex; + align-items: center; + gap: 8px; - mifosx-m3-icon { font-size: 18px; opacity: 0.7; } + mifosx-m3-icon { + font-size: 18px; + opacity: 0.7; + } } // Tenant selector in settings ::ng-deep mifosx-tenant-selector { - min-width: 100px; max-width: 150px; + min-width: 100px; + max-width: 150px; #tenant-selector { - width: 100%; margin: 0; + width: 100%; + margin: 0; - .mat-mdc-text-field-wrapper { border-radius: 8px; background: rgb(255 255 255 / 6%); } - .mat-mdc-form-field-flex { height: 36px; } - .mat-mdc-form-field-infix { padding: 6px 0; min-height: 36px; } - .mat-mdc-floating-label { color: rgb(255 255 255 / 50%); font-size: 0.8rem; } - .mat-mdc-select-value { color: #fff; font-size: 0.82rem; } - .mat-mdc-select-arrow { color: rgb(255 255 255 / 60%); } - .mat-mdc-form-field-icon-prefix { color: rgb(255 255 255 / 40%); padding: 0 6px 0 4px; } - .mdc-line-ripple::after { border-bottom-color: $accent; } + .mat-mdc-text-field-wrapper { + border-radius: 8px; + background: rgb(255 255 255 / 6%); + } + + .mat-mdc-form-field-flex { + height: 36px; + } + + .mat-mdc-form-field-infix { + padding: 6px 0; + min-height: 36px; + } + + .mat-mdc-floating-label { + color: rgb(255 255 255 / 50%); + font-size: 0.8rem; + } + + .mat-mdc-select-value { + color: #fff; + font-size: 0.82rem; + } + + .mat-mdc-select-arrow { + color: rgb(255 255 255 / 60%); + } + + .mat-mdc-form-field-icon-prefix { + color: rgb(255 255 255 / 40%); + padding: 0 6px 0 4px; + } + + .mdc-line-ripple::after { + border-bottom-color: $accent; + } } } // Language selector ::ng-deep mifosx-language-selector { - min-width: 100px; max-width: 140px; + min-width: 100px; + max-width: 140px; #language-selector.outlined-variant { - width: 100%; margin: 0; - .mat-mdc-form-field-flex { height: 36px; } - .mat-mdc-form-field-infix { padding: 6px 0; min-height: 36px; } + width: 100%; + margin: 0; + + .mat-mdc-form-field-flex { + height: 36px; + } + + .mat-mdc-form-field-infix { + padding: 6px 0; + min-height: 36px; + } .mdc-notched-outline { - .mdc-notched-outline__leading { border-radius: 8px 0 0 8px; border-color: rgb(255 255 255 / 20%); } - .mdc-notched-outline__notch { border-color: rgb(255 255 255 / 20%); } - .mdc-notched-outline__trailing { border-radius: 0 8px 8px 0; border-color: rgb(255 255 255 / 20%); } + .mdc-notched-outline__leading { + border-radius: 8px 0 0 8px; + border-color: rgb(255 255 255 / 20%); + } + + .mdc-notched-outline__notch { + border-color: rgb(255 255 255 / 20%); + } + + .mdc-notched-outline__trailing { + border-radius: 0 8px 8px 0; + border-color: rgb(255 255 255 / 20%); + } + } + + .mat-mdc-select-value { + color: #fff; + } + + .mat-mdc-select-arrow { + color: rgb(255 255 255 / 60%); } - .mat-mdc-select-value { color: #fff; } - .mat-mdc-select-arrow { color: rgb(255 255 255 / 60%); } } } // Server selector ::ng-deep mifosx-server-selector { - min-width: 100px; max-width: 160px; + min-width: 100px; + max-width: 160px; #server-selector.outlined-variant { - width: 100%; margin: 0; - .mat-mdc-form-field-flex { height: 36px; } - .mat-mdc-form-field-infix { padding: 6px 0; min-height: 36px; } + width: 100%; + margin: 0; + + .mat-mdc-form-field-flex { + height: 36px; + } + + .mat-mdc-form-field-infix { + padding: 6px 0; + min-height: 36px; + } .mdc-notched-outline { - .mdc-notched-outline__leading { border-radius: 8px 0 0 8px; border-color: rgb(255 255 255 / 20%); } - .mdc-notched-outline__notch { border-color: rgb(255 255 255 / 20%); } - .mdc-notched-outline__trailing { border-radius: 0 8px 8px 0; border-color: rgb(255 255 255 / 20%); } + .mdc-notched-outline__leading { + border-radius: 8px 0 0 8px; + border-color: rgb(255 255 255 / 20%); + } + + .mdc-notched-outline__notch { + border-color: rgb(255 255 255 / 20%); + } + + .mdc-notched-outline__trailing { + border-radius: 0 8px 8px 0; + border-color: rgb(255 255 255 / 20%); + } + } + + .mat-mdc-select-value { + color: #fff; + } + + .mat-mdc-select-arrow { + color: rgb(255 255 255 / 60%); } - .mat-mdc-select-value { color: #fff; } - .mat-mdc-select-arrow { color: rgb(255 255 255 / 60%); } } } // Theme toggle ::ng-deep mifosx-theme-toggle { - button { color: #fff; &:hover { background: rgb(255 255 255 / 10%); } } + button { + color: #fff; + + &:hover { + background: rgb(255 255 255 / 10%); + } + } } } // ═══ FOOTER ═══ .login-footer { - flex-shrink: 0; background: rgb(3 8 18 / 97%); + flex-shrink: 0; + background: rgb(3 8 18 / 97%); border-top: 1px solid rgb(0 180 216 / 17%); - padding: 7px 32px; display: flex; align-items: center; justify-content: center; gap: 10px; + padding: 7px 32px; + display: flex; + align-items: center; + justify-content: center; + gap: 10px; &__text { - font-size: 0.61rem; color: $text-muted; letter-spacing: 0.03em; text-align: center; line-height: 1.5; - strong { color: rgb(0 180 216 / 90%); font-weight: 600; } + font-size: 0.61rem; + color: $text-muted; + letter-spacing: 0.03em; + text-align: center; + line-height: 1.5; + + strong { + color: rgb(0 180 216 / 90%); + font-weight: 600; + } } - &__sep { color: rgb(255 255 255 / 12%); font-size: 0.75rem; } + &__sep { + color: rgb(255 255 255 / 12%); + font-size: 0.75rem; + } } // ═══ RESPONSIVE ═══ @media (width <= 1024px) { - .left-panel { flex: 0 0 55%; } - .right-panel { flex: 0 0 45%; padding: 32px; } - .slide__title { font-size: 2rem; } - .slide__flow { display: none; } + .left-panel { + flex: 0 0 55%; + } + + .right-panel { + flex: 0 0 45%; + padding: 32px; + } + + .slide__title { + font-size: 2rem; + } + + .slide__flow { + display: none; + } } @media (width <= 768px) { - .login-split { flex-direction: column; } - .left-panel { display: none; } + .login-split { + flex-direction: column; + } + + .left-panel { + display: none; + } .right-panel { - flex: 1; padding: 24px; + flex: 1; + padding: 24px; background: linear-gradient(160deg, #060f1e 0%, #0b1d35 100%); border-left: none; } diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index 6ea43da82..ebd460339 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -97,46 +97,112 @@ export class LoginComponent implements OnInit, OnDestroy { /** Slider data */ slides = [ { - tag: 'Core Banking', icon: '🏦', + tag: 'Core Banking', + icon: '🏦', title: 'Real-Time CBS
Integration Platform', desc: 'Seamless account enquiry, transaction posting & balance fetch via secure host connectivity. Sub-second response with 99.99% uptime SLA.', - stats: [{ value: '<200ms', label: 'Response Time' }, { value: '99.99%', label: 'Uptime SLA' }, { value: 'ISO 8583', label: 'Protocol' }], - flow: ['CBS Host', 'Middleware API', 'Account Enquiry', 'Tx Posting', 'Balance Fetch'] + stats: [ + { value: '<200ms', label: 'Response Time' }, + { value: '99.99%', label: 'Uptime SLA' }, + { value: 'ISO 8583', label: 'Protocol' } + ], + flow: [ + 'CBS Host', + 'Middleware API', + 'Account Enquiry', + 'Tx Posting', + 'Balance Fetch' + ] }, { - tag: 'Fraud Risk Management', icon: '', + tag: 'Fraud Risk Management', + icon: '', title: 'AI-Powered Fraud Risk
Management Platform', desc: 'Real-time transaction scoring, anomaly detection & automated lien marking. Integrated with MHA CFCFRMS, UN sanctions screening, and rule-based alert engine for instant fraud response.', - stats: [{ value: 'Real-Time', label: 'Tx Scoring' }, { value: 'CFCFRMS', label: 'MHA Portal' }, { value: 'UN/OFAC', label: 'Sanctions' }], - flow: ['Transaction', 'Risk Engine', 'Alert & Lien', 'CFCFRMS API', 'MHA Portal'] + stats: [ + { value: 'Real-Time', label: 'Tx Scoring' }, + { value: 'CFCFRMS', label: 'MHA Portal' }, + { value: 'UN/OFAC', label: 'Sanctions' } + ], + flow: [ + 'Transaction', + 'Risk Engine', + 'Alert & Lien', + 'CFCFRMS API', + 'MHA Portal' + ] }, { - tag: 'HSM Security', icon: '🔐', + tag: 'HSM Security', + icon: '🔐', title: 'HSM-Secured PIN
& Key Management', desc: 'ATM PIN generation, verification & change secured via SafeNet Luna K6 HSM. Full ZPK/PVK/KEK key hierarchy with TR-31 key block.', - stats: [{ value: 'Luna K6', label: 'HSM Model' }, { value: 'TR-31', label: 'Key Block' }, { value: 'FIPS 140-2', label: 'Certified' }], - flow: ['LMK (Master)', 'ZPK / KEK', 'PVK / ZEK', 'PIN Block', 'VISA PVV'] + stats: [ + { value: 'Luna K6', label: 'HSM Model' }, + { value: 'TR-31', label: 'Key Block' }, + { value: 'FIPS 140-2', label: 'Certified' } + ], + flow: [ + 'LMK (Master)', + 'ZPK / KEK', + 'PVK / ZEK', + 'PIN Block', + 'VISA PVV' + ] }, { - tag: 'Cyber Fraud Reporting', icon: '🛡️', + tag: 'Cyber Fraud Reporting', + icon: '🛡️', title: 'CFCFRMS
Fraud Detection & Reporting', desc: 'Citizen Financial Cyber Fraud Reporting & Management System with real-time monitoring, MHA portal connectivity, and automated lien marking.', - stats: [{ value: 'MHA', label: 'Portal' }, { value: 'Real-Time', label: 'Alerts' }, { value: 'Auto Lien', label: 'Mark & Block' }], - flow: ['Transaction', 'Risk Engine', 'CFCFRMS API', 'MHA Portal', 'Lien / Block'] + stats: [ + { value: 'MHA', label: 'Portal' }, + { value: 'Real-Time', label: 'Alerts' }, + { value: 'Auto Lien', label: 'Mark & Block' } + ], + flow: [ + 'Transaction', + 'Risk Engine', + 'CFCFRMS API', + 'MHA Portal', + 'Lien / Block' + ] }, { - tag: 'Compliance', icon: '📋', + tag: 'Compliance', + icon: '📋', title: 'RBI & IDRBT
Compliant Architecture', desc: 'Built in full adherence to RBI circulars and IDRBT security standards — data localisation, audit trails, VAPT-certified APIs and AML screening.', - stats: [{ value: 'IDRBT', label: 'Standards' }, { value: 'VAPT', label: 'Certified' }, { value: 'AML/OFAC', label: 'Screening' }], - flow: ['RBI Circular', 'IDRBT Std.', 'Audit Trail', 'VAPT APIs', 'AML / OFAC'] + stats: [ + { value: 'IDRBT', label: 'Standards' }, + { value: 'VAPT', label: 'Certified' }, + { value: 'AML/OFAC', label: 'Screening' } + ], + flow: [ + 'RBI Circular', + 'IDRBT Std.', + 'Audit Trail', + 'VAPT APIs', + 'AML / OFAC' + ] }, { - tag: 'CKYC Compliance', icon: '🪪', + tag: 'CKYC Compliance', + icon: '🪪', title: 'CBS to CKYC
Data Submission & Verification', desc: 'End-to-end CERSAI CKYC compliance workflow — Non-CKYC identification, auto ZIP packaging, SFTP upload, API-based search & fetch.', - stats: [{ value: 'CERSAI', label: 'Registry' }, { value: 'SFTP', label: 'Auto Upload' }, { value: 'API Search', label: '& Fetch' }], - flow: ['CBS — Non-CKYC', 'Auto ZIP Pack', 'SFTP Upload', 'CERSAI API', 'Status Update'] + stats: [ + { value: 'CERSAI', label: 'Registry' }, + { value: 'SFTP', label: 'Auto Upload' }, + { value: 'API Search', label: '& Fetch' } + ], + flow: [ + 'CBS — Non-CKYC', + 'Auto ZIP Pack', + 'SFTP Upload', + 'CERSAI API', + 'Status Update' + ] } ]; currentSlide = 0; diff --git a/src/app/organization/adhoc-query/adhoc-query.component.html b/src/app/organization/adhoc-query/adhoc-query.component.html index 3ed8c3192..6754fda77 100644 --- a/src/app/organization/adhoc-query/adhoc-query.component.html +++ b/src/app/organization/adhoc-query/adhoc-query.component.html @@ -19,11 +19,21 @@
- +
- diff --git a/src/app/organization/adhoc-query/create-adhoc-query/create-adhoc-query.component.html b/src/app/organization/adhoc-query/create-adhoc-query/create-adhoc-query.component.html index aa7fde1a1..f8ed598f4 100644 --- a/src/app/organization/adhoc-query/create-adhoc-query/create-adhoc-query.component.html +++ b/src/app/organization/adhoc-query/create-adhoc-query/create-adhoc-query.component.html @@ -29,7 +29,10 @@ {{ 'labels.inputs.name' | translate }} @if (adhocQueryForm.controls.name.hasError('required')) { - {{ 'labels.inputs.name' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} + {{ 'labels.inputs.name' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} } @@ -37,7 +40,10 @@ {{ 'labels.inputs.Insert into table' | translate }} @if (adhocQueryForm.controls.tableName.hasError('required')) { - {{ 'labels.inputs.Insert into table' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} + {{ 'labels.inputs.Insert into table' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} } @@ -45,7 +51,10 @@ {{ 'labels.inputs.SQL Query' | translate }} @if (adhocQueryForm.controls.query.hasError('required')) { - {{ 'labels.inputs.SQL Query' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} + {{ 'labels.inputs.SQL Query' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} } @@ -53,7 +62,10 @@ {{ 'labels.inputs.Table Fields' | translate }} @if (adhocQueryForm.controls.tableFields.hasError('required')) { - {{ 'labels.inputs.Table Fields' | translate }} {{ 'labels.commons.are' | translate }} {{ 'labels.commons.required' | translate }} + {{ 'labels.inputs.Table Fields' | translate }} {{ 'labels.commons.are' | translate }} + {{ 'labels.commons.required' | translate }} }
@@ -70,7 +82,10 @@ {{ 'labels.inputs.Email' | translate }} @if (adhocQueryForm.controls.email.hasError('email')) { - {{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.invalid' | translate }} + {{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.invalid' | translate }} } @@ -88,10 +103,16 @@ {{ 'labels.inputs.Custom Report Run Frequency (Days)' | translate }} @if (adhocQueryForm.controls.reportRunEvery.hasError('required')) { - {{ 'labels.inputs.Custom Report Run Frequency' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} + {{ 'labels.inputs.Custom Report Run Frequency' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} } @if (adhocQueryForm.controls.reportRunEvery.hasError('min')) { - {{ 'labels.inputs.Custom Report Run Frequency' | translate }} {{ 'labels.commons.must be larger than 0' | translate }} + {{ 'labels.inputs.Custom Report Run Frequency' | translate }} + {{ 'labels.commons.must be larger than 0' | translate }} } } @@ -109,8 +130,14 @@ -
diff --git a/src/app/organization/adhoc-query/create-adhoc-query/create-adhoc-query.component.scss b/src/app/organization/adhoc-query/create-adhoc-query/create-adhoc-query.component.scss index bf9767eaf..b40c2f690 100644 --- a/src/app/organization/adhoc-query/create-adhoc-query/create-adhoc-query.component.scss +++ b/src/app/organization/adhoc-query/create-adhoc-query/create-adhoc-query.component.scss @@ -15,48 +15,129 @@ $gray-500: #9aa0a6; $gray-700: #5f6368; $gray-900: #202124; -.caq-container { max-width: 800px; margin: 0 auto; padding: 20px 16px; } +.caq-container { + max-width: 800px; + margin: 0 auto; + padding: 20px 16px; +} .caq-header { - display: flex; align-items: center; gap: 14px; margin-bottom: 20px; - &__title { font-size: 18px; font-weight: 600; color: $gray-900; margin: 0; } + display: flex; + align-items: center; + gap: 14px; + margin-bottom: 20px; + + &__title { + font-size: 18px; + font-weight: 600; + color: $gray-900; + margin: 0; + } } .caq-back { - border-radius: 8px !important; font-size: 12px !important; font-weight: 600 !important; - height: 32px !important; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } - &:hover { color: $primary !important; border-color: $primary !important; } + border-radius: 8px !important; + font-size: 12px !important; + font-weight: 600 !important; + height: 32px !important; + border-color: $border !important; + color: $gray-500 !important; + + i { + margin-right: 6px; + } + + &:hover { + color: $primary !important; + border-color: $primary !important; + } } -.caq-card { background: #fff; border: 1px solid $border; border-radius: 10px; overflow: hidden; } +.caq-card { + background: #fff; + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; +} .caq-section { &__header { - display: flex; align-items: center; gap: 10px; padding: 12px 24px; - background: $gray-50; border-bottom: 1px solid $gray-200; - i { font-size: 14px; color: $accent; } - h3 { margin: 0; font-size: 14px; font-weight: 600; color: $gray-900; } + display: flex; + align-items: center; + gap: 10px; + padding: 12px 24px; + background: $gray-50; + border-bottom: 1px solid $gray-200; + + i { + font-size: 14px; + color: $accent; + } + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + } + + & + & { + border-top: 1px solid $gray-200; } - & + & { border-top: 1px solid $gray-200; } } .caq-grid { - display: grid; grid-template-columns: repeat(2, 1fr); gap: 4px 16px; padding: 16px 24px; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px 16px; + padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .caq-field { width: 100%; - &--full { grid-column: 1 / -1; } + + &--full { + grid-column: 1 / -1; + } } -.caq-checkbox { grid-column: 1 / -1; padding: 8px 0; } +.caq-checkbox { + grid-column: 1 / -1; + padding: 8px 0; +} .caq-actions { - display: flex; justify-content: flex-end; gap: 10px; padding: 14px 24px; - border-top: 1px solid $gray-200; background: $gray-50; - &__cancel { border-radius: 8px !important; font-size: 13px !important; font-weight: 600 !important; height: 36px !important; border-color: $border !important; color: $gray-700 !important; } - &__submit { border-radius: 8px !important; font-size: 13px !important; font-weight: 600 !important; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; i { margin-right: 6px; font-size: 12px; } } + display: flex; + justify-content: flex-end; + gap: 10px; + padding: 14px 24px; + border-top: 1px solid $gray-200; + background: $gray-50; + + &__cancel { + border-radius: 8px !important; + font-size: 13px !important; + font-weight: 600 !important; + height: 36px !important; + border-color: $border !important; + color: $gray-700 !important; + } + + &__submit { + border-radius: 8px !important; + font-size: 13px !important; + font-weight: 600 !important; + height: 36px !important; + box-shadow: 0 1px 3px rgba($primary, 0.3) !important; + + i { + margin-right: 6px; + font-size: 12px; + } + } } diff --git a/src/app/organization/bulk-import/bulk-import.component.html b/src/app/organization/bulk-import/bulk-import.component.html index 035cf6187..3fd2fa265 100644 --- a/src/app/organization/bulk-import/bulk-import.component.html +++ b/src/app/organization/bulk-import/bulk-import.component.html @@ -16,7 +16,6 @@
-
@@ -32,7 +31,9 @@

{{ 'labels.heading.Users' | translate }}

-

{{ 'labels.text.Download offices template and Upload office excel files' | translate }}

+

+ {{ 'labels.text.Download offices template and Upload office excel files' | translate }} +

@@ -42,7 +43,9 @@

{{ 'labels.heading.Groups' | translate }}

-

{{ 'labels.text.Download groups template and Upload group excel files' | translate }}

+

+ {{ 'labels.text.Download groups template and Upload group excel files' | translate }} +

@@ -52,7 +55,9 @@

{{ 'labels.heading.Loan Accounts' | translate }}

-

{{ 'labels.text.Download loan accounts template and upload loan account excel files' | translate }}

+

+ {{ 'labels.text.Download loan accounts template and upload loan account excel files' | translate }} +

@@ -62,7 +67,9 @@

{{ 'labels.heading.Savings Accounts' | translate }}

-

{{ 'labels.text.Download savings accounts template and upload savings account excel files' | translate }}

+

+ {{ 'labels.text.Download savings accounts template and upload savings account excel files' | translate }} +

@@ -72,7 +79,12 @@

{{ 'labels.heading.Fixed Deposit Accounts' | translate }}

-

{{ 'labels.text.Download fixed deposit accounts template and upload fixed deposit account excel files' | translate }}

+

+ {{ + 'labels.text.Download fixed deposit accounts template and upload fixed deposit account excel files' + | translate + }} +

@@ -82,7 +94,9 @@

{{ 'labels.heading.Chart of Accounts' | translate }}

-

{{ 'labels.text.Download chart of accounts template and upload chart of account excel files' | translate }}

+

+ {{ 'labels.text.Download chart of accounts template and upload chart of account excel files' | translate }} +

@@ -92,7 +106,9 @@

{{ 'labels.heading.Share Accounts' | translate }}

-

{{ 'labels.text.Download share accounts template and upload share account excel files' | translate }}

+

+ {{ 'labels.text.Download share accounts template and upload share account excel files' | translate }} +

@@ -102,7 +118,9 @@

{{ 'labels.heading.Employees' | translate }}

-

{{ 'labels.text.Download loan accounts template and upload loan account excel files' | translate }}

+

+ {{ 'labels.text.Download loan accounts template and upload loan account excel files' | translate }} +

@@ -112,7 +130,9 @@

{{ 'labels.heading.Clients' | translate }}

-

{{ 'labels.text.Download clients template and upload clients excel files' | translate }}

+

+ {{ 'labels.text.Download clients template and upload clients excel files' | translate }} +

@@ -122,7 +142,9 @@

{{ 'labels.heading.Centers' | translate }}

-

{{ 'labels.text.Download centers template and upload centers excel files' | translate }}

+

+ {{ 'labels.text.Download centers template and upload centers excel files' | translate }} +

@@ -132,7 +154,9 @@

{{ 'labels.heading.Loan Repayments' | translate }}

-

{{ 'labels.text.Download loan repayments template and upload loan repayment excel files' | translate }}

+

+ {{ 'labels.text.Download loan repayments template and upload loan repayment excel files' | translate }} +

@@ -142,7 +166,11 @@

{{ 'labels.heading.Savings Transactions' | translate }}

-

{{ 'labels.text.Download savings transactions template and upload savings transaction excel files' | translate }}

+

+ {{ + 'labels.text.Download savings transactions template and upload savings transaction excel files' | translate + }} +

@@ -152,7 +180,12 @@

{{ 'labels.heading.Fixed Deposit Transactions' | translate }}

-

{{ 'labels.text.Download fixed deposit transactions template and upload fixed deposit transaction excel files' | translate }}

+

+ {{ + 'labels.text.Download fixed deposit transactions template and upload fixed deposit transaction excel files' + | translate + }} +

@@ -162,7 +195,12 @@

{{ 'labels.heading.Recurring Deposit Transactions' | translate }}

-

{{ 'labels.text.Download recurring deposit transactions template and upload recurring deposit transaction excel files' | translate }}

+

+ {{ + 'labels.text.Download recurring deposit transactions template and upload recurring deposit transaction excel files' + | translate + }} +

@@ -172,7 +210,9 @@

{{ 'labels.heading.Journal Entries' | translate }}

-

{{ 'labels.text.Download journal entries template and upload journal entries excel files' | translate }}

+

+ {{ 'labels.text.Download journal entries template and upload journal entries excel files' | translate }} +

@@ -182,10 +222,11 @@

{{ 'labels.heading.Guarantors' | translate }}

-

{{ 'labels.text.Download guarantors template and upload guarantor excel files' | translate }}

+

+ {{ 'labels.text.Download guarantors template and upload guarantor excel files' | translate }} +

-
diff --git a/src/app/organization/bulk-import/bulk-import.component.scss b/src/app/organization/bulk-import/bulk-import.component.scss index c6c3aeec8..c2a8e640a 100644 --- a/src/app/organization/bulk-import/bulk-import.component.scss +++ b/src/app/organization/bulk-import/bulk-import.component.scss @@ -196,8 +196,13 @@ $gray-900: #0f172a; // ── Dark mode ── :host-context(.dark-theme) { .org-header { - &__title { color: #e2e8f0; } - &__sub { color: #94a3b8; } + &__title { + color: #e2e8f0; + } + + &__sub { + color: #94a3b8; + } } .org-card { @@ -209,8 +214,16 @@ $gray-900: #0f172a; border-color: #444; } - &__title { color: #e2e8f0; } - &__desc { color: #64748b; } - &__arrow { color: #444; } + &__title { + color: #e2e8f0; + } + + &__desc { + color: #64748b; + } + + &__arrow { + color: #444; + } } } diff --git a/src/app/organization/bulk-loan-reassignmnet/bulk-loan-reassignmnet.component.scss b/src/app/organization/bulk-loan-reassignmnet/bulk-loan-reassignmnet.component.scss index c306fd8e0..fefb4811a 100644 --- a/src/app/organization/bulk-loan-reassignmnet/bulk-loan-reassignmnet.component.scss +++ b/src/app/organization/bulk-loan-reassignmnet/bulk-loan-reassignmnet.component.scss @@ -57,7 +57,9 @@ $gray-900: #202124; border: 1px solid $border; border-radius: 12px; padding: 0; - box-shadow: 0 1px 3px rgb(16 24 40 / 6%), 0 1px 2px rgb(16 24 40 / 4%); + box-shadow: + 0 1px 3px rgb(16 24 40 / 6%), + 0 1px 2px rgb(16 24 40 / 4%); } .bl-section { diff --git a/src/app/organization/currencies/currencies.component.html b/src/app/organization/currencies/currencies.component.html index dd1cfc5eb..7ee445713 100644 --- a/src/app/organization/currencies/currencies.component.html +++ b/src/app/organization/currencies/currencies.component.html @@ -22,7 +22,13 @@
- diff --git a/src/app/organization/currencies/manage-currencies/manage-currencies.component.scss b/src/app/organization/currencies/manage-currencies/manage-currencies.component.scss index 726dcca37..975ac1b5b 100644 --- a/src/app/organization/currencies/manage-currencies/manage-currencies.component.scss +++ b/src/app/organization/currencies/manage-currencies/manage-currencies.component.scss @@ -15,34 +15,82 @@ $gray-500: #9aa0a6; $gray-700: #5f6368; $gray-900: #202124; -.mc-container { max-width: 700px; margin: 0 auto; padding: 20px 16px; } +.mc-container { + max-width: 700px; + margin: 0 auto; + padding: 20px 16px; +} .mc-header { - display: flex; align-items: center; gap: 14px; margin-bottom: 20px; - &__title { font-size: 18px; font-weight: 600; color: $gray-900; margin: 0; } + display: flex; + align-items: center; + gap: 14px; + margin-bottom: 20px; + + &__title { + font-size: 18px; + font-weight: 600; + color: $gray-900; + margin: 0; + } } .mc-back { - border-radius: 8px !important; font-size: 12px !important; font-weight: 600 !important; - height: 32px !important; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } - &:hover { color: $primary !important; border-color: $primary !important; } + border-radius: 8px !important; + font-size: 12px !important; + font-weight: 600 !important; + height: 32px !important; + border-color: $border !important; + color: $gray-500 !important; + + i { + margin-right: 6px; + } + + &:hover { + color: $primary !important; + border-color: $primary !important; + } } -.mc-card { background: #fff; border: 1px solid $border; border-radius: 10px; overflow: hidden; } +.mc-card { + background: #fff; + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; +} .mc-section { &__header { - display: flex; align-items: center; gap: 10px; padding: 12px 24px; - background: $gray-50; border-bottom: 1px solid $gray-200; - i { font-size: 14px; color: $accent; } - h3 { margin: 0; font-size: 14px; font-weight: 600; color: $gray-900; } + display: flex; + align-items: center; + gap: 10px; + padding: 12px 24px; + background: $gray-50; + border-bottom: 1px solid $gray-200; + + i { + font-size: 14px; + color: $accent; + } + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + } + + & + & { + border-top: 1px solid $gray-200; } - & + & { border-top: 1px solid $gray-200; } } // Add currency form -.mc-add { padding: 16px 24px; } +.mc-add { + padding: 16px 24px; +} .mc-add__form { display: flex; @@ -50,7 +98,10 @@ $gray-900: #202124; gap: 12px; } -.mc-add__field { flex: 1; width: 100%; } +.mc-add__field { + flex: 1; + width: 100%; +} .mc-add__btn { flex-shrink: 0; @@ -61,11 +112,16 @@ $gray-900: #202124; margin-top: 4px; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } // Currency list -.mc-list { padding: 8px 24px 16px; } +.mc-list { + padding: 8px 24px 16px; +} .mc-currency { display: flex; @@ -78,7 +134,9 @@ $gray-900: #202124; margin-bottom: 6px; transition: background 0.12s; - &:hover { background: #f1f5f9; } + &:hover { + background: #f1f5f9; + } &__info { display: flex; @@ -110,7 +168,9 @@ $gray-900: #202124; line-height: 32px !important; font-size: 13px; - &:hover { background: rgb(239 68 68 / 8%) !important; } + &:hover { + background: rgb(239 68 68 / 8%) !important; + } } } @@ -121,6 +181,14 @@ $gray-900: #202124; padding: 32px 16px; color: #cbd5e1; - i { font-size: 28px; margin-bottom: 8px; } - p { font-size: 13px; color: $gray-500; margin: 0; } + i { + font-size: 28px; + margin-bottom: 8px; + } + + p { + font-size: 13px; + color: $gray-500; + margin: 0; + } } diff --git a/src/app/organization/employees/create-employee/create-employee.component.scss b/src/app/organization/employees/create-employee/create-employee.component.scss index 483139010..295219758 100644 --- a/src/app/organization/employees/create-employee/create-employee.component.scss +++ b/src/app/organization/employees/create-employee/create-employee.component.scss @@ -44,9 +44,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -92,7 +97,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .ce-field { @@ -140,6 +147,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/organization/employees/edit-employee/edit-employee.component.scss b/src/app/organization/employees/edit-employee/edit-employee.component.scss index b568bac7a..1343831a5 100644 --- a/src/app/organization/employees/edit-employee/edit-employee.component.scss +++ b/src/app/organization/employees/edit-employee/edit-employee.component.scss @@ -44,9 +44,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -92,7 +97,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .ee-field { @@ -140,6 +147,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/organization/employees/employees.component.html b/src/app/organization/employees/employees.component.html index 75b407b8c..ac2e9ba66 100644 --- a/src/app/organization/employees/employees.component.html +++ b/src/app/organization/employees/employees.component.html @@ -24,13 +24,24 @@
-
- diff --git a/src/app/organization/employees/view-employee/view-employee.component.html b/src/app/organization/employees/view-employee/view-employee.component.html index 15517fc15..011377aa3 100644 --- a/src/app/organization/employees/view-employee/view-employee.component.html +++ b/src/app/organization/employees/view-employee/view-employee.component.html @@ -15,7 +15,13 @@

{{ employeeData.firstname }} {{ employeeData.lastname }}

- @@ -43,13 +49,9 @@
{{ 'labels.inputs.Is Loan Officer' | translate }}
@if (employeeData.isLoanOfficer) { - - Yes - + Yes } @else { - - No - + No }
diff --git a/src/app/organization/employees/view-employee/view-employee.component.scss b/src/app/organization/employees/view-employee/view-employee.component.scss index b9d49f9ab..420e90c1d 100644 --- a/src/app/organization/employees/view-employee/view-employee.component.scss +++ b/src/app/organization/employees/view-employee/view-employee.component.scss @@ -60,9 +60,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -81,7 +86,9 @@ $gray-900: #202124; padding: 20px 28px 24px; background: #fff; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .ve-cell { @@ -142,7 +149,9 @@ $gray-900: #202124; font-weight: 600; font-size: 13px; - i { font-size: 11px; } + i { + font-size: 11px; + } &--yes { color: #16a34a; diff --git a/src/app/organization/entity-data-table-checks/create-entity-data-table-checks/create-entity-data-table-checks.component.html b/src/app/organization/entity-data-table-checks/create-entity-data-table-checks/create-entity-data-table-checks.component.html index 4e99a391a..45705425d 100644 --- a/src/app/organization/entity-data-table-checks/create-entity-data-table-checks/create-entity-data-table-checks.component.html +++ b/src/app/organization/entity-data-table-checks/create-entity-data-table-checks/create-entity-data-table-checks.component.html @@ -121,7 +121,13 @@ -
diff --git a/src/app/organization/entity-data-table-checks/create-entity-data-table-checks/create-entity-data-table-checks.component.scss b/src/app/organization/entity-data-table-checks/create-entity-data-table-checks/create-entity-data-table-checks.component.scss index b25a627b8..7b2e6ac60 100644 --- a/src/app/organization/entity-data-table-checks/create-entity-data-table-checks/create-entity-data-table-checks.component.scss +++ b/src/app/organization/entity-data-table-checks/create-entity-data-table-checks/create-entity-data-table-checks.component.scss @@ -44,9 +44,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -88,7 +93,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .ce-field { @@ -120,6 +127,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/organization/entity-data-table-checks/entity-data-table-checks.component.html b/src/app/organization/entity-data-table-checks/entity-data-table-checks.component.html index b3a40acf9..3ba2759a5 100644 --- a/src/app/organization/entity-data-table-checks/entity-data-table-checks.component.html +++ b/src/app/organization/entity-data-table-checks/entity-data-table-checks.component.html @@ -19,11 +19,21 @@
- +
- diff --git a/src/app/organization/fund-mapping/fund-mapping.component.html b/src/app/organization/fund-mapping/fund-mapping.component.html index e1dcf3893..c19da1d8b 100644 --- a/src/app/organization/fund-mapping/fund-mapping.component.html +++ b/src/app/organization/fund-mapping/fund-mapping.component.html @@ -136,10 +136,7 @@
- + {{ 'labels.inputs.Loan Outstanding Percentage' | translate }}
@@ -228,10 +225,7 @@
- + {{ 'labels.inputs.Loan Outstanding Amount' | translate }}
@@ -325,7 +319,13 @@ -
diff --git a/src/app/organization/fund-mapping/fund-mapping.component.scss b/src/app/organization/fund-mapping/fund-mapping.component.scss index 0fd24ceea..4f3b36d75 100644 --- a/src/app/organization/fund-mapping/fund-mapping.component.scss +++ b/src/app/organization/fund-mapping/fund-mapping.component.scss @@ -44,9 +44,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -99,7 +104,9 @@ $gray-900: #202124; @media (width <= 768px) { grid-template-columns: 1fr; - &--3col { grid-template-columns: 1fr; } + &--3col { + grid-template-columns: 1fr; + } } } @@ -157,7 +164,10 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/organization/holidays/create-holiday/create-holiday.component.scss b/src/app/organization/holidays/create-holiday/create-holiday.component.scss index 184580b05..64a9c236d 100644 --- a/src/app/organization/holidays/create-holiday/create-holiday.component.scss +++ b/src/app/organization/holidays/create-holiday/create-holiday.component.scss @@ -44,9 +44,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -92,7 +97,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .ch-field { @@ -143,6 +150,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/organization/holidays/edit-holiday/edit-holiday.component.scss b/src/app/organization/holidays/edit-holiday/edit-holiday.component.scss index 37152108c..d19852a5b 100644 --- a/src/app/organization/holidays/edit-holiday/edit-holiday.component.scss +++ b/src/app/organization/holidays/edit-holiday/edit-holiday.component.scss @@ -44,9 +44,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -92,7 +97,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .eh-field { @@ -128,6 +135,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/organization/holidays/holidays.component.html b/src/app/organization/holidays/holidays.component.html index f72ebd7d1..e74469ad5 100644 --- a/src/app/organization/holidays/holidays.component.html +++ b/src/app/organization/holidays/holidays.component.html @@ -22,7 +22,7 @@
- + {{ 'labels.inputs.Select Office' | translate }} @for (office of officeData; track office) { @@ -32,7 +32,13 @@
- diff --git a/src/app/organization/holidays/view-holidays/view-holidays.component.scss b/src/app/organization/holidays/view-holidays/view-holidays.component.scss index 3c3735cd2..e785a7498 100644 --- a/src/app/organization/holidays/view-holidays/view-holidays.component.scss +++ b/src/app/organization/holidays/view-holidays/view-holidays.component.scss @@ -52,9 +52,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } .vh-menu-btn { @@ -85,7 +90,9 @@ $gray-900: #202124; gap: 10px; padding: 20px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .vh-cell { diff --git a/src/app/organization/investors/investors.component.html b/src/app/organization/investors/investors.component.html index 2f3c003e5..5e3886a5a 100644 --- a/src/app/organization/investors/investors.component.html +++ b/src/app/organization/investors/investors.component.html @@ -9,9 +9,7 @@
- +

Investors

@@ -165,24 +163,40 @@

{{ 'labels.inputs.Details' | translate }}

- {{ 'labels.inputs.Principal Outstanding' | translate }} - {{ record.details.totalPrincipalOutstanding | formatNumber }} + {{ + 'labels.inputs.Principal Outstanding' | translate + }} + {{ + record.details.totalPrincipalOutstanding | formatNumber + }}
- {{ 'labels.inputs.Interest Outstanding' | translate }} - {{ record.details.totalInterestOutstanding | formatNumber }} + {{ + 'labels.inputs.Interest Outstanding' | translate + }} + {{ + record.details.totalInterestOutstanding | formatNumber + }}
{{ 'labels.inputs.Fees Outstanding' | translate }} - {{ record.details.totalFeeChargesOutstanding | formatNumber }} + {{ + record.details.totalFeeChargesOutstanding | formatNumber + }}
- {{ 'labels.inputs.Penalties Outstanding' | translate }} - {{ record.details.totalPenaltyChargesOutstanding | formatNumber }} + {{ + 'labels.inputs.Penalties Outstanding' | translate + }} + {{ + record.details.totalPenaltyChargesOutstanding | formatNumber + }}
{{ 'labels.inputs.Outstanding' | translate }} - {{ record.details.totalOutstanding | formatNumber }} + {{ + record.details.totalOutstanding | formatNumber + }}
{{ 'labels.inputs.Overpaid' | translate }} diff --git a/src/app/organization/investors/investors.component.scss b/src/app/organization/investors/investors.component.scss index 41b537318..9f922c52f 100644 --- a/src/app/organization/investors/investors.component.scss +++ b/src/app/organization/investors/investors.component.scss @@ -44,9 +44,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -92,9 +97,13 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 768px) { grid-template-columns: 1fr; } + @media (width <= 768px) { + grid-template-columns: 1fr; + } - @media (width <= 1024px) and (width >= 769px) { grid-template-columns: repeat(2, 1fr); } + @media (width <= 1024px) and (width >= 769px) { + grid-template-columns: repeat(2, 1fr); + } } .inv-field { @@ -106,7 +115,9 @@ $gray-900: #202124; align-items: center; padding-top: 4px; - @media (width <= 768px) { padding-top: 0; } + @media (width <= 768px) { + padding-top: 0; + } } .inv-search-btn { @@ -117,7 +128,10 @@ $gray-900: #202124; width: 100%; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } // Results area @@ -142,7 +156,9 @@ $gray-900: #202124; } @keyframes inv-spin { - to { transform: rotate(360deg); } + to { + transform: rotate(360deg); + } } // Accordion & Panels @@ -211,7 +227,9 @@ $gray-900: #202124; gap: 16px; margin-bottom: 16px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } &__item { diff --git a/src/app/organization/loan-originators/create-loan-originator/create-loan-originator.component.scss b/src/app/organization/loan-originators/create-loan-originator/create-loan-originator.component.scss index 47d4ba65b..1466b9616 100644 --- a/src/app/organization/loan-originators/create-loan-originator/create-loan-originator.component.scss +++ b/src/app/organization/loan-originators/create-loan-originator/create-loan-originator.component.scss @@ -43,9 +43,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } .cl-card { @@ -84,7 +89,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .cl-field { @@ -115,6 +122,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.scss b/src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.scss index 99b3e91a5..e6a4aafef 100644 --- a/src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.scss +++ b/src/app/organization/loan-originators/edit-loan-originator/edit-loan-originator.component.scss @@ -43,9 +43,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } .el-card { @@ -84,7 +89,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .el-field { @@ -115,6 +122,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/organization/loan-originators/loan-originators.component.html b/src/app/organization/loan-originators/loan-originators.component.html index ff6465678..6aed73ecb 100644 --- a/src/app/organization/loan-originators/loan-originators.component.html +++ b/src/app/organization/loan-originators/loan-originators.component.html @@ -19,11 +19,23 @@
- +
- diff --git a/src/app/organization/loan-originators/view-loan-originator/view-loan-originator.component.html b/src/app/organization/loan-originators/view-loan-originator/view-loan-originator.component.html index a908fd5aa..5e46b066a 100644 --- a/src/app/organization/loan-originators/view-loan-originator/view-loan-originator.component.html +++ b/src/app/organization/loan-originators/view-loan-originator/view-loan-originator.component.html @@ -13,7 +13,13 @@

{{ loanOriginatorData.name }}

-
diff --git a/src/app/organization/loan-originators/view-loan-originator/view-loan-originator.component.scss b/src/app/organization/loan-originators/view-loan-originator/view-loan-originator.component.scss index 465a7912a..60b830a6e 100644 --- a/src/app/organization/loan-originators/view-loan-originator/view-loan-originator.component.scss +++ b/src/app/organization/loan-originators/view-loan-originator/view-loan-originator.component.scss @@ -45,7 +45,10 @@ $gray-900: #202124; height: 32px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } @@ -57,9 +60,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } .vl-card { @@ -98,7 +106,9 @@ $gray-900: #202124; gap: 16px; padding: 20px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .vl-detail { diff --git a/src/app/organization/loan-provisioning-criteria/create-loan-provisioning-criteria/create-loan-provisioning-criteria.component.html b/src/app/organization/loan-provisioning-criteria/create-loan-provisioning-criteria/create-loan-provisioning-criteria.component.html index ec8c97469..9e7c13167 100644 --- a/src/app/organization/loan-provisioning-criteria/create-loan-provisioning-criteria/create-loan-provisioning-criteria.component.html +++ b/src/app/organization/loan-provisioning-criteria/create-loan-provisioning-criteria/create-loan-provisioning-criteria.component.html @@ -92,7 +92,8 @@ {{ 'labels.inputs.Liability Account' | translate }} {{ - (definition.liabilityAccount | find: loanProvisioningCriteriaTemplate.glAccounts : 'id' : 'name') || '-' + (definition.liabilityAccount | find: loanProvisioningCriteriaTemplate.glAccounts : 'id' : 'name') || + '-' }} @@ -100,7 +101,10 @@ {{ 'labels.inputs.Expense Account' | translate }} - {{ (definition.expenseAccount | find: loanProvisioningCriteriaTemplate.glAccounts : 'id' : 'name') || '-' }} + {{ + (definition.expenseAccount | find: loanProvisioningCriteriaTemplate.glAccounts : 'id' : 'name') || + '-' + }} diff --git a/src/app/organization/loan-provisioning-criteria/create-loan-provisioning-criteria/create-loan-provisioning-criteria.component.scss b/src/app/organization/loan-provisioning-criteria/create-loan-provisioning-criteria/create-loan-provisioning-criteria.component.scss index 4cd039be9..f2de11f9e 100644 --- a/src/app/organization/loan-provisioning-criteria/create-loan-provisioning-criteria/create-loan-provisioning-criteria.component.scss +++ b/src/app/organization/loan-provisioning-criteria/create-loan-provisioning-criteria/create-loan-provisioning-criteria.component.scss @@ -46,9 +46,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -94,7 +99,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .cp-field { @@ -138,6 +145,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/organization/loan-provisioning-criteria/edit-loan-provisioning-criteria/edit-loan-provisioning-criteria.component.html b/src/app/organization/loan-provisioning-criteria/edit-loan-provisioning-criteria/edit-loan-provisioning-criteria.component.html index debb9e7de..6ba2f9c60 100644 --- a/src/app/organization/loan-provisioning-criteria/edit-loan-provisioning-criteria/edit-loan-provisioning-criteria.component.html +++ b/src/app/organization/loan-provisioning-criteria/edit-loan-provisioning-criteria/edit-loan-provisioning-criteria.component.html @@ -86,8 +86,8 @@ {{ 'labels.inputs.Liability Account' | translate }} {{ - (definition.liabilityAccount | find: loanProvisioningCriteriaAndTemplate.glAccounts : 'id' : 'name') || - '-' + (definition.liabilityAccount + | find: loanProvisioningCriteriaAndTemplate.glAccounts : 'id' : 'name') || '-' }} @@ -96,7 +96,8 @@ {{ 'labels.inputs.Expense Account' | translate }} {{ - (definition.expenseAccount | find: loanProvisioningCriteriaAndTemplate.glAccounts : 'id' : 'name') || '-' + (definition.expenseAccount + | find: loanProvisioningCriteriaAndTemplate.glAccounts : 'id' : 'name') || '-' }} diff --git a/src/app/organization/loan-provisioning-criteria/edit-loan-provisioning-criteria/edit-loan-provisioning-criteria.component.scss b/src/app/organization/loan-provisioning-criteria/edit-loan-provisioning-criteria/edit-loan-provisioning-criteria.component.scss index 4f55d9cfc..7c3ca112e 100644 --- a/src/app/organization/loan-provisioning-criteria/edit-loan-provisioning-criteria/edit-loan-provisioning-criteria.component.scss +++ b/src/app/organization/loan-provisioning-criteria/edit-loan-provisioning-criteria/edit-loan-provisioning-criteria.component.scss @@ -46,9 +46,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -94,7 +99,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .ep-field { @@ -138,6 +145,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/organization/loan-provisioning-criteria/loan-provisioning-criteria.component.html b/src/app/organization/loan-provisioning-criteria/loan-provisioning-criteria.component.html index 7f276c3d0..5f15fdf21 100644 --- a/src/app/organization/loan-provisioning-criteria/loan-provisioning-criteria.component.html +++ b/src/app/organization/loan-provisioning-criteria/loan-provisioning-criteria.component.html @@ -19,11 +19,21 @@
- +
- @@ -45,7 +55,12 @@ - + diff --git a/src/app/organization/manage-funds/create-fund/create-fund.component.html b/src/app/organization/manage-funds/create-fund/create-fund.component.html index d7f50764e..9d544d430 100644 --- a/src/app/organization/manage-funds/create-fund/create-fund.component.html +++ b/src/app/organization/manage-funds/create-fund/create-fund.component.html @@ -44,7 +44,14 @@ -
diff --git a/src/app/organization/manage-funds/create-fund/create-fund.component.scss b/src/app/organization/manage-funds/create-fund/create-fund.component.scss index 9e4544806..a27f8e15a 100644 --- a/src/app/organization/manage-funds/create-fund/create-fund.component.scss +++ b/src/app/organization/manage-funds/create-fund/create-fund.component.scss @@ -43,9 +43,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } .cf-card { @@ -84,7 +89,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .cf-field { @@ -115,6 +122,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/organization/manage-funds/edit-fund/edit-fund.component.html b/src/app/organization/manage-funds/edit-fund/edit-fund.component.html index a68c76601..ac47104da 100644 --- a/src/app/organization/manage-funds/edit-fund/edit-fund.component.html +++ b/src/app/organization/manage-funds/edit-fund/edit-fund.component.html @@ -44,7 +44,14 @@ -
diff --git a/src/app/organization/manage-funds/edit-fund/edit-fund.component.scss b/src/app/organization/manage-funds/edit-fund/edit-fund.component.scss index 42c004127..5e54962b2 100644 --- a/src/app/organization/manage-funds/edit-fund/edit-fund.component.scss +++ b/src/app/organization/manage-funds/edit-fund/edit-fund.component.scss @@ -43,9 +43,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } .ef-card { @@ -84,7 +89,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .ef-field { @@ -115,6 +122,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/organization/manage-funds/manage-funds.component.html b/src/app/organization/manage-funds/manage-funds.component.html index 157ef5d26..29b482b10 100644 --- a/src/app/organization/manage-funds/manage-funds.component.html +++ b/src/app/organization/manage-funds/manage-funds.component.html @@ -22,7 +22,13 @@
- diff --git a/src/app/organization/manage-funds/view-fund/view-fund.component.html b/src/app/organization/manage-funds/view-fund/view-fund.component.html index 5665706bd..ed7672aa3 100644 --- a/src/app/organization/manage-funds/view-fund/view-fund.component.html +++ b/src/app/organization/manage-funds/view-fund/view-fund.component.html @@ -13,7 +13,13 @@

{{ fundData.name }}

-
diff --git a/src/app/organization/manage-funds/view-fund/view-fund.component.scss b/src/app/organization/manage-funds/view-fund/view-fund.component.scss index 13c08bae4..e131487f6 100644 --- a/src/app/organization/manage-funds/view-fund/view-fund.component.scss +++ b/src/app/organization/manage-funds/view-fund/view-fund.component.scss @@ -45,7 +45,10 @@ $gray-900: #202124; height: 32px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } @@ -57,9 +60,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } .vf-card { @@ -98,7 +106,9 @@ $gray-900: #202124; gap: 16px; padding: 20px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .vf-detail { diff --git a/src/app/organization/offices/create-office/create-office.component.html b/src/app/organization/offices/create-office/create-office.component.html index cf4c9ff6a..d0f0adc97 100644 --- a/src/app/organization/offices/create-office/create-office.component.html +++ b/src/app/organization/offices/create-office/create-office.component.html @@ -85,7 +85,14 @@ -
diff --git a/src/app/organization/offices/create-office/create-office.component.scss b/src/app/organization/offices/create-office/create-office.component.scss index a6554921b..d62bbf71d 100644 --- a/src/app/organization/offices/create-office/create-office.component.scss +++ b/src/app/organization/offices/create-office/create-office.component.scss @@ -44,9 +44,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -92,7 +97,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .co-field { @@ -128,6 +135,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/organization/offices/edit-office/edit-office.component.html b/src/app/organization/offices/edit-office/edit-office.component.html index a94f3bac9..d91aaa5cb 100644 --- a/src/app/organization/offices/edit-office/edit-office.component.html +++ b/src/app/organization/offices/edit-office/edit-office.component.html @@ -87,7 +87,14 @@ -
diff --git a/src/app/organization/offices/edit-office/edit-office.component.scss b/src/app/organization/offices/edit-office/edit-office.component.scss index 7523cfeaa..e6c0153fa 100644 --- a/src/app/organization/offices/edit-office/edit-office.component.scss +++ b/src/app/organization/offices/edit-office/edit-office.component.scss @@ -44,9 +44,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -92,7 +97,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .eo-field { @@ -128,6 +135,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/organization/offices/offices.component.html b/src/app/organization/offices/offices.component.html index c0dea616f..713500d70 100644 --- a/src/app/organization/offices/offices.component.html +++ b/src/app/organization/offices/offices.component.html @@ -32,13 +32,24 @@
-
- @@ -72,7 +83,12 @@ - +
diff --git a/src/app/organization/offices/view-office/general-tab/general-tab.component.html b/src/app/organization/offices/view-office/general-tab/general-tab.component.html index 127d3213d..3cb0f7f10 100644 --- a/src/app/organization/offices/view-office/general-tab/general-tab.component.html +++ b/src/app/organization/offices/view-office/general-tab/general-tab.component.html @@ -15,7 +15,9 @@
{{ 'labels.inputs.Opened On' | translate }}
-
{{ officeData.openingDate ? (officeData.openingDate | dateFormat) : 'Unassigned' }}
+
+ {{ officeData.openingDate ? (officeData.openingDate | dateFormat) : 'Unassigned' }} +
diff --git a/src/app/organization/offices/view-office/general-tab/general-tab.component.scss b/src/app/organization/offices/view-office/general-tab/general-tab.component.scss index 8fee46d85..499c34073 100644 --- a/src/app/organization/offices/view-office/general-tab/general-tab.component.scss +++ b/src/app/organization/offices/view-office/general-tab/general-tab.component.scss @@ -25,7 +25,9 @@ $gray-900: #202124; grid-template-columns: repeat(2, 1fr); gap: 10px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .gt-cell { diff --git a/src/app/organization/offices/view-office/view-office.component.html b/src/app/organization/offices/view-office/view-office.component.html index 406ce5c07..ac06a1c88 100644 --- a/src/app/organization/offices/view-office/view-office.component.html +++ b/src/app/organization/offices/view-office/view-office.component.html @@ -15,7 +15,13 @@

{{ 'labels.heading.Office Details' | translate }}

- diff --git a/src/app/organization/offices/view-office/view-office.component.scss b/src/app/organization/offices/view-office/view-office.component.scss index 570c6f71a..b05fab471 100644 --- a/src/app/organization/offices/view-office/view-office.component.scss +++ b/src/app/organization/offices/view-office/view-office.component.scss @@ -52,9 +52,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } .vo-edit-btn { diff --git a/src/app/organization/organization.component.html b/src/app/organization/organization.component.html index 4e1dff96a..d3d116ce9 100644 --- a/src/app/organization/organization.component.html +++ b/src/app/organization/organization.component.html @@ -98,7 +98,9 @@

{{ 'labels.heading.Manage Loan Originators' | translate }}

-

{{ 'labels.text.Loan Originators are associated with loan accounts' | translate }}

+

+ {{ 'labels.text.Loan Originators are associated with loan accounts' | translate }} +

@@ -106,7 +108,11 @@

{{ 'labels.text.Investors' | translate }}

-

{{ 'labels.text.View the loan account details associated with the investors' | translate }}

+

+ {{ 'labels.text.View the loan account details associated with the investors' | translate }} +

@@ -190,7 +198,11 @@
- +
@@ -246,7 +258,11 @@

{{ 'labels.heading.Bulk Import' | translate }}

-

{{ 'labels.text.Bulk data import using excel spreadsheet templates' | translate }}

+

+ {{ 'labels.text.Bulk data import using excel spreadsheet templates' | translate }} +

diff --git a/src/app/organization/organization.component.scss b/src/app/organization/organization.component.scss index c6c3aeec8..c2a8e640a 100644 --- a/src/app/organization/organization.component.scss +++ b/src/app/organization/organization.component.scss @@ -196,8 +196,13 @@ $gray-900: #0f172a; // ── Dark mode ── :host-context(.dark-theme) { .org-header { - &__title { color: #e2e8f0; } - &__sub { color: #94a3b8; } + &__title { + color: #e2e8f0; + } + + &__sub { + color: #94a3b8; + } } .org-card { @@ -209,8 +214,16 @@ $gray-900: #0f172a; border-color: #444; } - &__title { color: #e2e8f0; } - &__desc { color: #64748b; } - &__arrow { color: #444; } + &__title { + color: #e2e8f0; + } + + &__desc { + color: #64748b; + } + + &__arrow { + color: #444; + } } } diff --git a/src/app/organization/organization.component.ts b/src/app/organization/organization.component.ts index 425f61a21..bc8508799 100644 --- a/src/app/organization/organization.component.ts +++ b/src/app/organization/organization.component.ts @@ -214,5 +214,4 @@ export class OrganizationComponent implements AfterViewInit { this.configurationWizardService.showRecurringDepositProductsList = true; this.router.navigate(['/products/recurring-deposit-products']); } - } diff --git a/src/app/organization/password-preferences/password-preferences.component.html b/src/app/organization/password-preferences/password-preferences.component.html index cb88133f4..6674be0af 100644 --- a/src/app/organization/password-preferences/password-preferences.component.html +++ b/src/app/organization/password-preferences/password-preferences.component.html @@ -22,14 +22,20 @@
-
+ [class.pp-option--selected]=" + passwordPreferencesForm.get('validationPolicyId')?.value === passwordPreference.id + " + >
{{ getPasswordLabel(passwordPreference) | translate }}
-
{{ passwordPreference.description | translateKey: 'passwordPreferences' }}
+
+ {{ passwordPreference.description | translateKey: 'passwordPreferences' }} +
@@ -39,8 +45,14 @@ -
diff --git a/src/app/organization/password-preferences/password-preferences.component.scss b/src/app/organization/password-preferences/password-preferences.component.scss index ce72a1dd6..3f607f820 100644 --- a/src/app/organization/password-preferences/password-preferences.component.scss +++ b/src/app/organization/password-preferences/password-preferences.component.scss @@ -14,48 +14,148 @@ $gray-500: #9aa0a6; $gray-700: #5f6368; $gray-900: #202124; -.pp-container { max-width: 700px; margin: 0 auto; padding: 20px 16px; } +.pp-container { + max-width: 700px; + margin: 0 auto; + padding: 20px 16px; +} .pp-header { - display: flex; align-items: center; gap: 14px; margin-bottom: 20px; - &__title { font-size: 18px; font-weight: 600; color: $gray-900; margin: 0; } -} + display: flex; + align-items: center; + gap: 14px; + margin-bottom: 20px; -.pp-back { - border-radius: 8px !important; font-size: 12px !important; font-weight: 600 !important; - height: 32px !important; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } - &:hover { color: $primary !important; border-color: $primary !important; } -} - -.pp-card { background: #fff; border: 1px solid $border; border-radius: 10px; overflow: hidden; } - -.pp-section { - &__header { - display: flex; align-items: center; gap: 10px; padding: 12px 24px; - background: $gray-50; border-bottom: 1px solid $gray-200; - i { font-size: 14px; color: $accent; } - h3 { margin: 0; font-size: 14px; font-weight: 600; color: $gray-900; } + &__title { + font-size: 18px; + font-weight: 600; + color: $gray-900; + margin: 0; } } -.pp-body { padding: 20px 24px; } +.pp-back { + border-radius: 8px !important; + font-size: 12px !important; + font-weight: 600 !important; + height: 32px !important; + border-color: $border !important; + color: $gray-500 !important; -.pp-options { display: flex; flex-direction: column; gap: 12px; } + i { + margin-right: 6px; + } + + &:hover { + color: $primary !important; + border-color: $primary !important; + } +} + +.pp-card { + background: #fff; + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; +} + +.pp-section { + &__header { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 24px; + background: $gray-50; + border-bottom: 1px solid $gray-200; + + i { + font-size: 14px; + color: $accent; + } + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + } +} + +.pp-body { + padding: 20px 24px; +} + +.pp-options { + display: flex; + flex-direction: column; + gap: 12px; +} .pp-option { - border: 1px solid $border; border-radius: 8px; padding: 16px 20px; + border: 1px solid $border; + border-radius: 8px; + padding: 16px 20px; transition: all 0.15s; - &:hover { border-color: #cbd5e1; background: $gray-50; } - &--selected { border-color: $primary; border-width: 2px; background: rgba($primary, 0.02); } - &__header { display: flex; justify-content: space-between; align-items: center; margin-bottom: 8px; } - &__title { font-size: 14px; font-weight: 600; color: $gray-900; } - &__desc { font-size: 13px; color: $gray-700; line-height: 1.5; } + + &:hover { + border-color: #cbd5e1; + background: $gray-50; + } + + &--selected { + border-color: $primary; + border-width: 2px; + background: rgba($primary, 0.02); + } + + &__header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 8px; + } + + &__title { + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + + &__desc { + font-size: 13px; + color: $gray-700; + line-height: 1.5; + } } .pp-actions { - display: flex; justify-content: flex-end; gap: 10px; padding: 14px 24px; - border-top: 1px solid $gray-200; background: $gray-50; - &__cancel { border-radius: 8px !important; font-size: 13px !important; font-weight: 600 !important; height: 36px !important; border-color: $border !important; color: $gray-700 !important; } - &__submit { border-radius: 8px !important; font-size: 13px !important; font-weight: 600 !important; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; i { margin-right: 6px; font-size: 12px; } } + display: flex; + justify-content: flex-end; + gap: 10px; + padding: 14px 24px; + border-top: 1px solid $gray-200; + background: $gray-50; + + &__cancel { + border-radius: 8px !important; + font-size: 13px !important; + font-weight: 600 !important; + height: 36px !important; + border-color: $border !important; + color: $gray-700 !important; + } + + &__submit { + border-radius: 8px !important; + font-size: 13px !important; + font-weight: 600 !important; + height: 36px !important; + box-shadow: 0 1px 3px rgba($primary, 0.3) !important; + + i { + margin-right: 6px; + font-size: 12px; + } + } } diff --git a/src/app/organization/payment-types/create-payment-type/create-payment-type.component.scss b/src/app/organization/payment-types/create-payment-type/create-payment-type.component.scss index ae41c8c73..46ee5e127 100644 --- a/src/app/organization/payment-types/create-payment-type/create-payment-type.component.scss +++ b/src/app/organization/payment-types/create-payment-type/create-payment-type.component.scss @@ -46,9 +46,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } .cp-card { @@ -88,7 +93,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr !important; } + @media (width <= 600px) { + grid-template-columns: 1fr !important; + } } .cp-field { @@ -132,7 +139,10 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } } diff --git a/src/app/organization/payment-types/edit-payment-type/edit-payment-type.component.scss b/src/app/organization/payment-types/edit-payment-type/edit-payment-type.component.scss index 509e7e89d..43421c3e1 100644 --- a/src/app/organization/payment-types/edit-payment-type/edit-payment-type.component.scss +++ b/src/app/organization/payment-types/edit-payment-type/edit-payment-type.component.scss @@ -46,9 +46,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } .ep-card { @@ -88,7 +93,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr !important; } + @media (width <= 600px) { + grid-template-columns: 1fr !important; + } } .ep-field { @@ -132,7 +139,10 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } } diff --git a/src/app/organization/payment-types/payment-types.component.html b/src/app/organization/payment-types/payment-types.component.html index 1b0b7eec3..7cc16157c 100644 --- a/src/app/organization/payment-types/payment-types.component.html +++ b/src/app/organization/payment-types/payment-types.component.html @@ -21,7 +21,13 @@
- @@ -75,10 +81,12 @@ @if (!paymentType.isSystemDefined) { } diff --git a/src/app/organization/payment-types/payment-types.component.scss b/src/app/organization/payment-types/payment-types.component.scss index 246f03aff..0e73e74a1 100644 --- a/src/app/organization/payment-types/payment-types.component.scss +++ b/src/app/organization/payment-types/payment-types.component.scss @@ -33,14 +33,18 @@ $primary: #1353a0; background: #ecfdf5; color: #059669; - &::before { background: #059669; } + &::before { + background: #059669; + } } &--no { background: #f1f5f9; color: #94a3b8; - &::before { background: #94a3b8; } + &::before { + background: #94a3b8; + } } } @@ -54,5 +58,7 @@ $primary: #1353a0; color: #dc2626 !important; cursor: pointer; - &:hover { text-decoration: underline; } + &:hover { + text-decoration: underline; + } } diff --git a/src/app/organization/sms-campaigns/sms-campaigns.component.html b/src/app/organization/sms-campaigns/sms-campaigns.component.html index 9c571a69d..25557cbf7 100644 --- a/src/app/organization/sms-campaigns/sms-campaigns.component.html +++ b/src/app/organization/sms-campaigns/sms-campaigns.component.html @@ -19,11 +19,21 @@
- +
- diff --git a/src/app/organization/standing-instructions-history/standing-instructions-history.component.html b/src/app/organization/standing-instructions-history/standing-instructions-history.component.html index 29c7a788d..de20eab02 100644 --- a/src/app/organization/standing-instructions-history/standing-instructions-history.component.html +++ b/src/app/organization/standing-instructions-history/standing-instructions-history.component.html @@ -55,7 +55,13 @@ } {{ 'labels.inputs.From Date' | translate }} - + @@ -69,8 +75,16 @@
- - +
diff --git a/src/app/organization/standing-instructions-history/standing-instructions-history.component.scss b/src/app/organization/standing-instructions-history/standing-instructions-history.component.scss index c246b4d09..95eb85351 100644 --- a/src/app/organization/standing-instructions-history/standing-instructions-history.component.scss +++ b/src/app/organization/standing-instructions-history/standing-instructions-history.component.scss @@ -59,7 +59,9 @@ $gray-900: #202124; border: 1px solid $border; border-radius: 12px; padding: 0; - box-shadow: 0 1px 3px rgb(16 24 40 / 6%), 0 1px 2px rgb(16 24 40 / 4%); + box-shadow: + 0 1px 3px rgb(16 24 40 / 6%), + 0 1px 2px rgb(16 24 40 / 4%); } .si-section { @@ -150,7 +152,9 @@ $gray-900: #202124; border: 1px solid $border; border-radius: 12px; overflow: hidden; - box-shadow: 0 1px 3px rgb(16 24 40 / 6%), 0 1px 2px rgb(16 24 40 / 4%); + box-shadow: + 0 1px 3px rgb(16 24 40 / 6%), + 0 1px 2px rgb(16 24 40 / 4%); } .modern-table { diff --git a/src/app/organization/tellers/cashiers/allocate-cash/allocate-cash.component.scss b/src/app/organization/tellers/cashiers/allocate-cash/allocate-cash.component.scss index 824ab4073..d9ad6c775 100644 --- a/src/app/organization/tellers/cashiers/allocate-cash/allocate-cash.component.scss +++ b/src/app/organization/tellers/cashiers/allocate-cash/allocate-cash.component.scss @@ -46,9 +46,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } .ac-card { @@ -92,7 +97,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr !important; } + @media (width <= 600px) { + grid-template-columns: 1fr !important; + } } .ac-field { @@ -123,7 +130,10 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } } diff --git a/src/app/organization/tellers/cashiers/cashiers.component.html b/src/app/organization/tellers/cashiers/cashiers.component.html index 3a82ba242..02ff07016 100644 --- a/src/app/organization/tellers/cashiers/cashiers.component.html +++ b/src/app/organization/tellers/cashiers/cashiers.component.html @@ -21,7 +21,13 @@
- diff --git a/src/app/organization/tellers/cashiers/cashiers.component.scss b/src/app/organization/tellers/cashiers/cashiers.component.scss index 5581482b1..39508d9bd 100644 --- a/src/app/organization/tellers/cashiers/cashiers.component.scss +++ b/src/app/organization/tellers/cashiers/cashiers.component.scss @@ -24,7 +24,9 @@ $primary: #1353a0; border-radius: 6px; font-size: 12px; cursor: pointer; - transition: opacity 0.15s, transform 0.15s; + transition: + opacity 0.15s, + transform 0.15s; text-decoration: none; &:hover { diff --git a/src/app/organization/tellers/cashiers/create-cashier/create-cashier.component.html b/src/app/organization/tellers/cashiers/create-cashier/create-cashier.component.html index b5bf17f5f..898cdb60c 100644 --- a/src/app/organization/tellers/cashiers/create-cashier/create-cashier.component.html +++ b/src/app/organization/tellers/cashiers/create-cashier/create-cashier.component.html @@ -105,7 +105,9 @@ @if (!createCashierForm.value.isFullDay) {
- {{ 'labels.inputs.Start Time' | translate }} ({{ 'labels.inputs.Hour' | translate }}) + {{ 'labels.inputs.Start Time' | translate }} ({{ 'labels.inputs.Hour' | translate }}) @for (hour of hours; track hour) { {{ hour }} @@ -113,7 +115,9 @@ - {{ 'labels.inputs.Start Time' | translate }} ({{ 'labels.inputs.Minute' | translate }}) + {{ 'labels.inputs.Start Time' | translate }} ({{ 'labels.inputs.Minute' | translate }}) @for (minute of minutes; track minute) { {{ minute }} @@ -123,7 +127,9 @@
- {{ 'labels.inputs.End Time' | translate }} ({{ 'labels.inputs.Hour' | translate }}) + {{ 'labels.inputs.End Time' | translate }} ({{ 'labels.inputs.Hour' | translate }}) @for (hour of hours; track hour) { {{ hour }} @@ -131,7 +137,9 @@ - {{ 'labels.inputs.End Time' | translate }} ({{ 'labels.inputs.Minute' | translate }}) + {{ 'labels.inputs.End Time' | translate }} ({{ 'labels.inputs.Minute' | translate }}) @for (minute of minutes; track minute) { {{ minute }} diff --git a/src/app/organization/tellers/cashiers/create-cashier/create-cashier.component.scss b/src/app/organization/tellers/cashiers/create-cashier/create-cashier.component.scss index f9d60f713..0edd30a90 100644 --- a/src/app/organization/tellers/cashiers/create-cashier/create-cashier.component.scss +++ b/src/app/organization/tellers/cashiers/create-cashier/create-cashier.component.scss @@ -46,9 +46,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } .cc-card { @@ -92,7 +97,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr !important; } + @media (width <= 600px) { + grid-template-columns: 1fr !important; + } } .cc-field { @@ -107,7 +114,10 @@ $gray-900: #202124; display: flex !important; gap: 12px; - @media (width <= 600px) { flex-direction: column; gap: 4px; } + @media (width <= 600px) { + flex-direction: column; + gap: 4px; + } } .cc-time-field { @@ -148,7 +158,10 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } } diff --git a/src/app/organization/tellers/cashiers/edit-cashier/edit-cashier.component.html b/src/app/organization/tellers/cashiers/edit-cashier/edit-cashier.component.html index 07bc437db..a1180f83e 100644 --- a/src/app/organization/tellers/cashiers/edit-cashier/edit-cashier.component.html +++ b/src/app/organization/tellers/cashiers/edit-cashier/edit-cashier.component.html @@ -112,7 +112,9 @@ @if (!editCashierForm.value.isFullDay) {
- {{ 'labels.inputs.Start Time' | translate }} ({{ 'labels.inputs.Hour' | translate }}) + {{ 'labels.inputs.Start Time' | translate }} ({{ 'labels.inputs.Hour' | translate }}) @for (hour of hours; track hour) { {{ hour }} @@ -120,7 +122,9 @@ - {{ 'labels.inputs.Start Time' | translate }} ({{ 'labels.inputs.Minute' | translate }}) + {{ 'labels.inputs.Start Time' | translate }} ({{ 'labels.inputs.Minute' | translate }}) @for (minute of minutes; track minute) { {{ minute }} @@ -130,7 +134,9 @@
- {{ 'labels.inputs.End Time' | translate }} ({{ 'labels.inputs.Hour' | translate }}) + {{ 'labels.inputs.End Time' | translate }} ({{ 'labels.inputs.Hour' | translate }}) @for (hour of hours; track hour) { {{ hour }} @@ -138,7 +144,9 @@ - {{ 'labels.inputs.End Time' | translate }} ({{ 'labels.inputs.Minute' | translate }}) + {{ 'labels.inputs.End Time' | translate }} ({{ 'labels.inputs.Minute' | translate }}) @for (minute of minutes; track minute) { {{ minute }} diff --git a/src/app/organization/tellers/cashiers/edit-cashier/edit-cashier.component.scss b/src/app/organization/tellers/cashiers/edit-cashier/edit-cashier.component.scss index 8ceb7c57d..641e7f1d9 100644 --- a/src/app/organization/tellers/cashiers/edit-cashier/edit-cashier.component.scss +++ b/src/app/organization/tellers/cashiers/edit-cashier/edit-cashier.component.scss @@ -46,9 +46,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } .ec-card { @@ -92,7 +97,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr !important; } + @media (width <= 600px) { + grid-template-columns: 1fr !important; + } } .ec-field { @@ -107,7 +114,10 @@ $gray-900: #202124; display: flex !important; gap: 12px; - @media (width <= 600px) { flex-direction: column; gap: 4px; } + @media (width <= 600px) { + flex-direction: column; + gap: 4px; + } } .ec-time-field { @@ -148,7 +158,10 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } } diff --git a/src/app/organization/tellers/cashiers/settle-cash/settle-cash.component.scss b/src/app/organization/tellers/cashiers/settle-cash/settle-cash.component.scss index 6b1403ba9..4380cf2e2 100644 --- a/src/app/organization/tellers/cashiers/settle-cash/settle-cash.component.scss +++ b/src/app/organization/tellers/cashiers/settle-cash/settle-cash.component.scss @@ -46,9 +46,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } .sc-card { @@ -92,7 +97,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr !important; } + @media (width <= 600px) { + grid-template-columns: 1fr !important; + } } .sc-field { @@ -123,7 +130,10 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } } diff --git a/src/app/organization/tellers/cashiers/transactions/transactions.component.html b/src/app/organization/tellers/cashiers/transactions/transactions.component.html index 2e2c95257..653db8587 100644 --- a/src/app/organization/tellers/cashiers/transactions/transactions.component.html +++ b/src/app/organization/tellers/cashiers/transactions/transactions.component.html @@ -16,11 +16,22 @@
- - @@ -44,19 +55,21 @@
{{ 'labels.inputs.Net Cash' | translate }} - {{ cashierData.netCash | formatNumber }} [{{ currencySelector.value }}] + {{ cashierData.netCash | formatNumber }} [{{ currencySelector.value }}]
} -
+
{{ 'labels.inputs.Filter' | translate }} - + {{ 'labels.inputs.Select Currency' | translate }} @for (currency of currencyData; track currency) { diff --git a/src/app/organization/tellers/cashiers/view-cashier/view-cashier.component.html b/src/app/organization/tellers/cashiers/view-cashier/view-cashier.component.html index a8694867a..8ca681842 100644 --- a/src/app/organization/tellers/cashiers/view-cashier/view-cashier.component.html +++ b/src/app/organization/tellers/cashiers/view-cashier/view-cashier.component.html @@ -13,10 +13,22 @@

{{ cashierData.staffName }}

- -
diff --git a/src/app/organization/tellers/cashiers/view-cashier/view-cashier.component.scss b/src/app/organization/tellers/cashiers/view-cashier/view-cashier.component.scss index b2a5565d1..1f749f677 100644 --- a/src/app/organization/tellers/cashiers/view-cashier/view-cashier.component.scss +++ b/src/app/organization/tellers/cashiers/view-cashier/view-cashier.component.scss @@ -49,7 +49,10 @@ $gray-900: #202124; height: 32px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } &__delete { @@ -58,7 +61,10 @@ $gray-900: #202124; font-weight: 600 !important; height: 32px !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } @@ -70,9 +76,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } .vc-card { @@ -112,7 +123,9 @@ $gray-900: #202124; gap: 16px; padding: 20px 24px; - @media (width <= 600px) { grid-template-columns: 1fr !important; } + @media (width <= 600px) { + grid-template-columns: 1fr !important; + } } .vc-detail { diff --git a/src/app/organization/tellers/create-teller/create-teller.component.html b/src/app/organization/tellers/create-teller/create-teller.component.html index e5a794cc6..6c54655a0 100644 --- a/src/app/organization/tellers/create-teller/create-teller.component.html +++ b/src/app/organization/tellers/create-teller/create-teller.component.html @@ -117,7 +117,14 @@ -
diff --git a/src/app/organization/tellers/create-teller/create-teller.component.scss b/src/app/organization/tellers/create-teller/create-teller.component.scss index 04d0f5b8b..1c40a1f45 100644 --- a/src/app/organization/tellers/create-teller/create-teller.component.scss +++ b/src/app/organization/tellers/create-teller/create-teller.component.scss @@ -43,9 +43,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } .ct-card { @@ -84,7 +89,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .ct-field { @@ -119,6 +126,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/organization/tellers/edit-teller/edit-teller.component.html b/src/app/organization/tellers/edit-teller/edit-teller.component.html index d9657f786..9f6ded6da 100644 --- a/src/app/organization/tellers/edit-teller/edit-teller.component.html +++ b/src/app/organization/tellers/edit-teller/edit-teller.component.html @@ -111,7 +111,14 @@ -
diff --git a/src/app/organization/tellers/edit-teller/edit-teller.component.scss b/src/app/organization/tellers/edit-teller/edit-teller.component.scss index d6915bea8..97784ed96 100644 --- a/src/app/organization/tellers/edit-teller/edit-teller.component.scss +++ b/src/app/organization/tellers/edit-teller/edit-teller.component.scss @@ -43,9 +43,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } .et-card { @@ -84,7 +89,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .et-field { @@ -119,6 +126,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/organization/tellers/tellers.component.html b/src/app/organization/tellers/tellers.component.html index 08fc0b6de..8e38d98f3 100644 --- a/src/app/organization/tellers/tellers.component.html +++ b/src/app/organization/tellers/tellers.component.html @@ -21,7 +21,13 @@
- @@ -45,7 +51,11 @@ {{ 'labels.inputs.Status' | translate }} - {{ teller.status === 'ACTIVE' ? ('labels.inputs.Active' | translate) : ('labels.catalogs.Inactive' | translate) }} + {{ + teller.status === 'ACTIVE' + ? ('labels.inputs.Active' | translate) + : ('labels.catalogs.Inactive' | translate) + }} @@ -59,7 +69,8 @@ {{ 'labels.inputs.Actions' | translate }} - {{ 'labels.buttons.View Cashiers' | translate }} + {{ 'labels.buttons.View Cashiers' | translate }} diff --git a/src/app/organization/tellers/view-teller/view-teller.component.html b/src/app/organization/tellers/view-teller/view-teller.component.html index 7258f9e1a..20fa9295e 100644 --- a/src/app/organization/tellers/view-teller/view-teller.component.html +++ b/src/app/organization/tellers/view-teller/view-teller.component.html @@ -13,10 +13,22 @@

{{ tellerData.name }}

- -
@@ -55,7 +67,9 @@
{{ 'labels.inputs.Status' | translate }} - + {{ tellerData.status | titlecase }} diff --git a/src/app/organization/tellers/view-teller/view-teller.component.scss b/src/app/organization/tellers/view-teller/view-teller.component.scss index 5a02e993d..abefb3ae9 100644 --- a/src/app/organization/tellers/view-teller/view-teller.component.scss +++ b/src/app/organization/tellers/view-teller/view-teller.component.scss @@ -46,7 +46,10 @@ $gray-900: #202124; height: 32px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } &__delete { @@ -55,7 +58,10 @@ $gray-900: #202124; font-weight: 600 !important; height: 32px !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } @@ -67,9 +73,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } .vt-card { @@ -108,7 +119,9 @@ $gray-900: #202124; gap: 16px; padding: 20px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .vt-detail { @@ -159,12 +172,16 @@ $gray-900: #202124; background: #ecfdf5; color: #059669; - &::before { background: #059669; } + &::before { + background: #059669; + } } .status-closed { background: #fef2f2; color: #dc2626; - &::before { background: #dc2626; } + &::before { + background: #dc2626; + } } diff --git a/src/app/organization/working-days/working-days.component.html b/src/app/organization/working-days/working-days.component.html index 7b9d6f18b..f1b05517c 100644 --- a/src/app/organization/working-days/working-days.component.html +++ b/src/app/organization/working-days/working-days.component.html @@ -1,3 +1,11 @@ + +
-
diff --git a/src/app/organization/working-days/working-days.component.scss b/src/app/organization/working-days/working-days.component.scss index 7d6ec5544..85d02648b 100644 --- a/src/app/organization/working-days/working-days.component.scss +++ b/src/app/organization/working-days/working-days.component.scss @@ -14,46 +14,123 @@ $gray-500: #9aa0a6; $gray-700: #5f6368; $gray-900: #202124; -.wd-container { max-width: 700px; margin: 0 auto; padding: 20px 16px; } +.wd-container { + max-width: 700px; + margin: 0 auto; + padding: 20px 16px; +} .wd-header { - display: flex; align-items: center; gap: 14px; margin-bottom: 20px; - &__title { font-size: 18px; font-weight: 600; color: $gray-900; margin: 0; } -} + display: flex; + align-items: center; + gap: 14px; + margin-bottom: 20px; -.wd-back { - border-radius: 8px !important; font-size: 12px !important; font-weight: 600 !important; - height: 32px !important; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } - &:hover { color: $primary !important; border-color: $primary !important; } -} - -.wd-card { background: #fff; border: 1px solid $border; border-radius: 10px; overflow: hidden; } - -.wd-section { - &__header { - display: flex; align-items: center; gap: 10px; padding: 12px 24px; - background: $gray-50; border-bottom: 1px solid $gray-200; - i { font-size: 14px; color: $accent; } - h3 { margin: 0; font-size: 14px; font-weight: 600; color: $gray-900; } + &__title { + font-size: 18px; + font-weight: 600; + color: $gray-900; + margin: 0; } } -.wd-body { padding: 20px 24px; } +.wd-back { + border-radius: 8px !important; + font-size: 12px !important; + font-weight: 600 !important; + height: 32px !important; + border-color: $border !important; + color: $gray-500 !important; + + i { + margin-right: 6px; + } + + &:hover { + color: $primary !important; + border-color: $primary !important; + } +} + +.wd-card { + background: #fff; + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; +} + +.wd-section { + &__header { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 24px; + background: $gray-50; + border-bottom: 1px solid $gray-200; + + i { + font-size: 14px; + color: $accent; + } + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + } +} + +.wd-body { + padding: 20px 24px; +} .wd-days { - display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 8px; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 8px; } -.wd-day { padding: 8px 0; } +.wd-day { + padding: 8px 0; +} -.wd-field { width: 100%; } +.wd-field { + width: 100%; +} -.wd-checkbox { padding: 12px 0 4px; } +.wd-checkbox { + padding: 12px 0 4px; +} .wd-actions { - display: flex; justify-content: flex-end; gap: 10px; padding: 14px 24px; - border-top: 1px solid $gray-200; background: $gray-50; - &__cancel { border-radius: 8px !important; font-size: 13px !important; font-weight: 600 !important; height: 36px !important; border-color: $border !important; color: $gray-700 !important; } - &__submit { border-radius: 8px !important; font-size: 13px !important; font-weight: 600 !important; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; i { margin-right: 6px; font-size: 12px; } } + display: flex; + justify-content: flex-end; + gap: 10px; + padding: 14px 24px; + border-top: 1px solid $gray-200; + background: $gray-50; + + &__cancel { + border-radius: 8px !important; + font-size: 13px !important; + font-weight: 600 !important; + height: 36px !important; + border-color: $border !important; + color: $gray-700 !important; + } + + &__submit { + border-radius: 8px !important; + font-size: 13px !important; + font-weight: 600 !important; + height: 36px !important; + box-shadow: 0 1px 3px rgba($primary, 0.3) !important; + + i { + margin-right: 6px; + font-size: 12px; + } + } } diff --git a/src/app/pipes/accounts-filter.pipe.ts b/src/app/pipes/accounts-filter.pipe.ts index a9f3a02b1..3b7363383 100644 --- a/src/app/pipes/accounts-filter.pipe.ts +++ b/src/app/pipes/accounts-filter.pipe.ts @@ -48,6 +48,10 @@ export class AccountsFilterPipe implements PipeTransform { accounts = accounts.filter((account: any) => { return account.depositType.value === 'Savings'; }); + } else if (checkSavings === 'isCurrent') { + accounts = accounts.filter((account: any) => { + return account.depositType.id === 400 || account.depositType.value === 'Current Deposit'; + }); } if (status === 'closed') { accounts = accounts.filter((account: any) => { diff --git a/src/app/products/charges/charges.component.html b/src/app/products/charges/charges.component.html index e979b9928..7805863d1 100644 --- a/src/app/products/charges/charges.component.html +++ b/src/app/products/charges/charges.component.html @@ -34,7 +34,13 @@
- diff --git a/src/app/products/charges/create-charge/create-charge.component.html b/src/app/products/charges/create-charge/create-charge.component.html index 68dce2ef3..49633647e 100644 --- a/src/app/products/charges/create-charge/create-charge.component.html +++ b/src/app/products/charges/create-charge/create-charge.component.html @@ -207,8 +207,7 @@ } @if ( - (chargeForm.controls.feeInterval.hasError('min') || - chargeForm.controls.feeInterval.hasError('max')) && + (chargeForm.controls.feeInterval.hasError('min') || chargeForm.controls.feeInterval.hasError('max')) && !chargeForm.controls.feeInterval.hasError('pattern') ) { diff --git a/src/app/products/collaterals/collaterals.component.html b/src/app/products/collaterals/collaterals.component.html index 6f9427d9b..729d681c8 100644 --- a/src/app/products/collaterals/collaterals.component.html +++ b/src/app/products/collaterals/collaterals.component.html @@ -17,10 +17,20 @@
- +
- diff --git a/src/app/products/collaterals/create-collateral/create-collateral.component.html b/src/app/products/collaterals/create-collateral/create-collateral.component.html index 0c5802b68..07e8a1d6c 100644 --- a/src/app/products/collaterals/create-collateral/create-collateral.component.html +++ b/src/app/products/collaterals/create-collateral/create-collateral.component.html @@ -97,12 +97,7 @@ -
diff --git a/src/app/products/collaterals/edit-collateral/edit-collateral.component.html b/src/app/products/collaterals/edit-collateral/edit-collateral.component.html index f122419f7..414c46641 100644 --- a/src/app/products/collaterals/edit-collateral/edit-collateral.component.html +++ b/src/app/products/collaterals/edit-collateral/edit-collateral.component.html @@ -97,12 +97,7 @@ -
diff --git a/src/app/products/current-products/create-current-product/create-current-product.component.html b/src/app/products/current-products/create-current-product/create-current-product.component.html index 4114aa60a..6662df7d6 100644 --- a/src/app/products/current-products/create-current-product/create-current-product.component.html +++ b/src/app/products/current-products/create-current-product/create-current-product.component.html @@ -12,14 +12,21 @@

{{ 'labels.heading.Create Current Product' | translate }}

- + {{ 'labels.inputs.DETAILS' | translate }}
- +
@@ -29,9 +36,9 @@ {{ 'labels.inputs.CURRENCY' | translate }}
- +
@@ -41,9 +48,9 @@ {{ 'labels.inputs.TERMS' | translate }}
- +
@@ -53,9 +60,9 @@ {{ 'labels.inputs.SETTINGS' | translate }}
- +
@@ -65,10 +72,22 @@ {{ 'labels.inputs.CHARGES' | translate }}
- + > +
+ + + + + + Digital + +
+
@@ -78,13 +97,13 @@ {{ 'labels.inputs.ACCOUNTING' | translate }}
- + >
@@ -95,13 +114,13 @@ {{ 'labels.inputs.PREVIEW' | translate }}
- + >
} @@ -114,19 +133,19 @@
@if (selectedTabIndex > 0) { } @if (selectedTabIndex < totalTabs - 1) { } @if (formValid && selectedTabIndex === totalTabs - 1) { } diff --git a/src/app/products/current-products/create-current-product/create-current-product.component.ts b/src/app/products/current-products/create-current-product/create-current-product.component.ts index da403853c..f0b8477d5 100644 --- a/src/app/products/current-products/create-current-product/create-current-product.component.ts +++ b/src/app/products/current-products/create-current-product/create-current-product.component.ts @@ -8,13 +8,14 @@ import { Component, ViewChild, inject } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { SavingProductDetailsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component'; -import { SavingProductCurrencyStepComponent } from '../../saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component'; -import { SavingProductTermsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component'; -import { SavingProductSettingsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component'; -import { SavingProductChargesStepComponent } from '../../saving-products/saving-product-stepper/saving-product-charges-step/saving-product-charges-step.component'; -import { SavingProductAccountingStepComponent } from '../../saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component'; -import { SavingProductPreviewStepComponent } from '../../saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component'; +import { CurrentProductDetailsStepComponent } from '../current-product-stepper/current-product-details-step/current-product-details-step.component'; +import { CurrentProductCurrencyStepComponent } from '../current-product-stepper/current-product-currency-step/current-product-currency-step.component'; +import { CurrentProductTermsStepComponent } from '../current-product-stepper/current-product-terms-step/current-product-terms-step.component'; +import { CurrentProductSettingsStepComponent } from '../current-product-stepper/current-product-settings-step/current-product-settings-step.component'; +import { CurrentProductChargesStepComponent } from '../current-product-stepper/current-product-charges-step/current-product-charges-step.component'; +import { CurrentProductLimitsStepComponent } from '../current-product-stepper/current-product-limits-step/current-product-limits-step.component'; +import { CurrentProductAccountingStepComponent } from '../current-product-stepper/current-product-accounting-step/current-product-accounting-step.component'; +import { CurrentProductPreviewStepComponent } from '../current-product-stepper/current-product-preview-step/current-product-preview-step.component'; import { ProductsService } from 'app/products/products.service'; import { SettingsService } from 'app/settings/settings.service'; import { Accounting } from 'app/core/utils/accounting'; @@ -28,13 +29,14 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; imports: [ ...STANDALONE_SHARED_IMPORTS, MatTabsModule, - SavingProductDetailsStepComponent, - SavingProductCurrencyStepComponent, - SavingProductTermsStepComponent, - SavingProductSettingsStepComponent, - SavingProductChargesStepComponent, - SavingProductAccountingStepComponent, - SavingProductPreviewStepComponent + CurrentProductDetailsStepComponent, + CurrentProductCurrencyStepComponent, + CurrentProductTermsStepComponent, + CurrentProductSettingsStepComponent, + CurrentProductChargesStepComponent, + CurrentProductLimitsStepComponent, + CurrentProductAccountingStepComponent, + CurrentProductPreviewStepComponent ] }) export class CreateCurrentProductComponent { @@ -44,13 +46,14 @@ export class CreateCurrentProductComponent { private settingsService = inject(SettingsService); private accounting = inject(Accounting); - @ViewChild(SavingProductDetailsStepComponent, { static: true }) detailsStep: SavingProductDetailsStepComponent; - @ViewChild(SavingProductCurrencyStepComponent, { static: true }) currencyStep: SavingProductCurrencyStepComponent; - @ViewChild(SavingProductTermsStepComponent, { static: true }) termsStep: SavingProductTermsStepComponent; - @ViewChild(SavingProductSettingsStepComponent, { static: true }) settingsStep: SavingProductSettingsStepComponent; - @ViewChild(SavingProductChargesStepComponent, { static: true }) chargesStep: SavingProductChargesStepComponent; - @ViewChild(SavingProductAccountingStepComponent, { static: true }) - accountingStep: SavingProductAccountingStepComponent; + @ViewChild(CurrentProductDetailsStepComponent, { static: true }) detailsStep: CurrentProductDetailsStepComponent; + @ViewChild(CurrentProductCurrencyStepComponent, { static: true }) currencyStep: CurrentProductCurrencyStepComponent; + @ViewChild(CurrentProductTermsStepComponent, { static: true }) termsStep: CurrentProductTermsStepComponent; + @ViewChild(CurrentProductSettingsStepComponent, { static: true }) settingsStep: CurrentProductSettingsStepComponent; + @ViewChild(CurrentProductChargesStepComponent, { static: true }) chargesStep: CurrentProductChargesStepComponent; + @ViewChild(CurrentProductLimitsStepComponent, { static: true }) limitsStep: CurrentProductLimitsStepComponent; + @ViewChild(CurrentProductAccountingStepComponent, { static: true }) + accountingStep: CurrentProductAccountingStepComponent; currentProductsTemplate: any; accountingRuleData: string[] = []; @@ -64,23 +67,26 @@ export class CreateCurrentProductComponent { } get detailsForm() { - return this.detailsStep.savingProductDetailsForm; + return this.detailsStep.CurrentProductDetailsForm; } get currencyForm() { - return this.currencyStep.savingProductCurrencyForm; + return this.currencyStep.CurrentProductCurrencyForm; } get termsForm() { - return this.termsStep.savingProductTermsForm; + return this.termsStep.CurrentProductTermsForm; } get settingsForm() { - return this.settingsStep.savingProductSettingsForm; + return this.settingsStep.CurrentProductSettingsForm; + } + get limitsForm(): void { + return; // Limits step doesn't have a form, it's data-driven } get accountingForm() { - return this.accountingStep.savingProductAccountingForm; + return this.accountingStep.CurrentProductAccountingForm; } get totalTabs(): number { - return this.formValid ? 7 : 6; + return this.formValid ? 8 : 7; } get formValid() { @@ -95,23 +101,50 @@ export class CreateCurrentProductComponent { get currentProduct() { return { - ...this.detailsStep.savingProductDetails, - ...this.currencyStep.savingProductCurrency, - ...this.termsStep.savingProductTerms, - ...this.settingsStep.savingProductSettings, - ...this.chargesStep.savingProductCharges, - ...this.accountingStep.savingProductAccounting + ...this.detailsStep.CurrentProductDetails, + ...this.currencyStep.CurrentProductCurrency, + ...this.termsStep.CurrentProductTerms, + ...this.settingsStep.CurrentProductSettings, + ...this.chargesStep.CurrentProductCharges, + ...this.limitsStep.CurrentProductLimits, + ...this.accountingStep.CurrentProductAccounting }; } - submit() { - const product = { + private prepareCurrentProductPayload() { + const currentProduct = { ...this.currentProduct, charges: this.currentProduct.charges.map((charge: any) => ({ id: charge.id })), - locale: this.settingsService.language.code + locale: this.settingsService.language.code, + dateFormat: 'dd MMMM yyyy' }; - delete product.advancedAccountingRules; - this.productsService.createCurrentProduct(product).subscribe((response: any) => { + delete currentProduct.advancedAccountingRules; + + // Convert lockinPeriodFrequencyType from string to integer + // Backend expects: 0=DAYS, 1=WEEKS, 2=MONTHS, 3=YEARS + if (currentProduct.lockinPeriodFrequencyType) { + const frequencyTypeMapping: { [key: string]: number } = { + DAYS: 0, + WEEKS: 1, + MONTHS: 2, + YEARS: 3 + }; + currentProduct.lockinPeriodFrequencyType = frequencyTypeMapping[currentProduct.lockinPeriodFrequencyType]; + } + + Object.keys(currentProduct).forEach((key) => { + if (currentProduct[key] === '' || currentProduct[key] === null || currentProduct[key] === undefined) { + delete currentProduct[key]; + } + }); + + return currentProduct; + } + + submit() { + const currentProduct = this.prepareCurrentProductPayload(); + console.log('Current product submit payload:', currentProduct); + this.productsService.createCurrentProduct(currentProduct).subscribe((response: any) => { this.router.navigate( [ '../', diff --git a/src/app/products/current-products/current-product-stepper/current-product-accounting-step/current-product-accounting-step.component.html b/src/app/products/current-products/current-product-stepper/current-product-accounting-step/current-product-accounting-step.component.html new file mode 100644 index 000000000..b22ff2e6e --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-accounting-step/current-product-accounting-step.component.html @@ -0,0 +1,121 @@ + + + +
+
+ + + +

Accounting

+
+ +
+
+
+

Accounting Rule

+
+
+ Cash +
+
+ + @if (isCashOrAccrualAccounting()) { + @for (section of accountingSectionConfigs; track section.key) { +
+
+

{{ section.title }}

+ +
+
+ @for (row of section.rows; track row.key) { + @if ( + (!row.accrualOnly || isAccrualAccounting()) && (!row.dormancyOnly || isDormancyTrackingEnabled()) + ) { +
+
+ + {{ section.pillLabel }} + + {{ row.label }} +
+ +
+ } + } +
+ +
+ @for ( + otherRow of getOtherGlMappingsArray(section.otherMappingsControlName).controls; + track $index; + let i = $index + ) { +
+
+
+ + {{ section.pillLabel }} + + +
+ +
+ + GL Code + + @for (glAccount of section.otherAccountOptions; track glAccount.id) { + + {{ '(' + glAccount.glCode + ') ' + glAccount.name }} + + } + + +
+
+ +
+ } +
+
+ } + } +
+
+ diff --git a/src/app/products/current-products/current-product-stepper/current-product-accounting-step/current-product-accounting-step.component.scss b/src/app/products/current-products/current-product-stepper/current-product-accounting-step/current-product-accounting-step.component.scss new file mode 100644 index 000000000..30125e801 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-accounting-step/current-product-accounting-step.component.scss @@ -0,0 +1,314 @@ +/** + * 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/. + */ + +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-500: #6b7280; +$gray-700: #334155; +$gray-900: #202124; +$primary: #1353a0; + +.spa-section { + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; + background: #fff; +} + +.spa-section__header { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 24px; + background: $gray-50; + border-bottom: 1px solid $gray-200; + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } +} + +.spa-section__icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: #eff6ff; + color: #2563eb; + font-size: 13px; +} + +.spa-section__body { + padding: 16px 24px 20px; +} + +.spa-group { + & + & { + margin-top: 20px; + } + + &__header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin: 0 0 12px; + } + + h4 { + margin: 0 0 12px; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #94a3b8; + } +} + +.spa-add-btn { + border: 0; + background: transparent; + color: #2563eb; + font-size: 12px; + font-weight: 700; + padding: 0; + cursor: pointer; +} + +.spa-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.spa-list + .spa-list { + margin-top: 12px; +} + +.spa-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 10px 12px; + border: 1px solid #e2e8f0; + border-radius: 8px; + background: #fff; + + @media (width <= 900px) { + flex-direction: column; + align-items: stretch; + } +} + +.spa-row:has(mifosx-gl-account-selector) { + align-items: stretch; +} + +.spa-row--other { + border-style: dashed; + border-color: #cbd5e1; + background: #f8fafc; + display: grid; + grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr); + align-items: center; + padding: 12px 16px; + + @media (width <= 1100px) { + grid-template-columns: 1fr; + } +} + +.spa-other-row-wrap { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 12px; + + @media (width <= 1100px) { + grid-template-columns: 1fr; + gap: 8px; + } +} + +.spa-row__meta { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + flex: 1 1 auto; +} + +.spa-row__meta--other { + min-width: 0; + flex: 1 1 auto; + align-self: stretch; +} + +.spa-row__label { + font-size: 14px; + font-weight: 600; + color: $gray-700; +} + +.spa-row__input { + flex: 0 0 22rem; + max-width: 22rem; + min-width: 18rem; + + @media (width <= 900px) { + width: 100%; + max-width: 100%; + min-width: 0; + } +} + +.spa-row__input--other { + flex: 1 1 auto; + max-width: 100%; + min-width: 0; +} + +.spa-row__input :is(mifosx-gl-account-selector) { + width: 100%; + display: block; +} + +.spa-row__input :is(mifosx-gl-account-selector) :is(.layout-column) { + width: 100%; + margin-right: 0 !important; +} + +.spa-row__input :is(mifosx-gl-account-selector) :is(mat-form-field) { + width: 100%; +} + +.spa-row__actions { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.spa-row__actions--other { + justify-content: flex-end; + align-self: stretch; + width: 100%; + + @media (width <= 1100px) { + justify-content: flex-start; + } +} + +.spa-row__name { + width: 100%; + min-width: 0; + max-width: 24rem; + height: 40px; + border: 1px solid #cbd5e1; + border-radius: 6px; + padding: 0 12px; + font-size: 14px; + background: #fff; + + @media (width <= 900px) { + max-width: 100%; + } +} + +.spa-row__name--code { + max-width: 20rem; +} + +.spa-row__name--other-account { + max-width: 100%; + flex: 1 1 auto; + height: 48px; +} + +.spa-row__other-field { + width: 100%; + min-width: 0; +} + +.spa-row__other-field :is(mat-form-field) { + width: 100%; +} + +.spa-row__other-field :is(.mat-mdc-form-field-required-marker) { + display: none; +} + +.spa-remove-btn { + border: 0; + background: transparent; + color: #ef4444; + font-size: 18px; + line-height: 1; + cursor: pointer; + padding: 0 4px; +} + +.spa-remove-btn--side { + place-self: center end; + margin-right: 4px; + + @media (width <= 1100px) { + justify-self: start; + margin-right: 0; + } +} + +.spa-pill { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 2px 8px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + white-space: nowrap; + + &--green { + background: #dcfce7; + color: #16a34a; + } + + &--amber { + background: #fef3c7; + color: #d97706; + } + + &--blue { + background: #dbeafe; + color: #2563eb; + } +} + +datalist { + display: none; +} + +@media (width <= 900px) { + .spa-row__meta { + margin-bottom: 4px; + } + + .spa-row__input { + flex: 1 1 auto; + max-width: 100%; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-accounting-step/current-product-accounting-step.component.ts b/src/app/products/current-products/current-product-stepper/current-product-accounting-step/current-product-accounting-step.component.ts new file mode 100644 index 000000000..252629bc5 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-accounting-step/current-product-accounting-step.component.ts @@ -0,0 +1,495 @@ +/** + * 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'; +import { + FormsModule, + ReactiveFormsModule, + UntypedFormArray, + UntypedFormBuilder, + UntypedFormControl, + UntypedFormGroup, + Validators +} from '@angular/forms'; +import { MatRadioButton, MatRadioGroup } from '@angular/material/radio'; +import { GlAccountSelectorComponent } from 'app/shared/accounting/gl-account-selector/gl-account-selector.component'; +import { Accounting } from 'app/core/utils/accounting'; +import { GLAccount } from 'app/shared/models/general.model'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +type AccountingSectionKey = 'assets' | 'liabilities' | 'expenses' | 'income'; + +type AccountingRowConfig = { + key: string; + label: string; + inputLabel: string; + controlName: string; + options: GLAccount[]; + required?: boolean; + accrualOnly?: boolean; + dormancyOnly?: boolean; +}; + +type AccountingSectionConfig = { + key: AccountingSectionKey; + title: string; + pillLabel: string; + pillClass: 'green' | 'amber' | 'blue'; + rows: AccountingRowConfig[]; + otherMappingsControlName: string; + otherAccountOptions: GLAccount[]; +}; + +type OtherGlMappingValue = { + accountName: string; + glAccountId: number | null; +}; + +@Component({ + selector: 'mifosx-current-product-accounting-step', + templateUrl: './current-product-accounting-step.component.html', + styleUrls: ['./current-product-accounting-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FormsModule, + ReactiveFormsModule, + MatRadioGroup, + MatRadioButton, + GlAccountSelectorComponent + ] +}) +export class CurrentProductAccountingStepComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private accounting = inject(Accounting); + + @Input() CurrentProductsTemplate: any; + @Input() accountingRuleData: any; + @Input() isDormancyTrackingActive: any; + @Input() CurrentProductFormValid: boolean; + @Input() allowOverdraft: any; + + CurrentProductAccountingForm: UntypedFormGroup; + liabilityAccountOptions: GLAccount[] = []; + expenseAccountOptions: GLAccount[] = []; + incomeAccountOptions: GLAccount[] = []; + assetAccountOptions: GLAccount[] = []; + accountingSectionConfigs: AccountingSectionConfig[] = []; + + constructor() { + this.createCurrentProductAccountingForm(); + this.setConditionalControls(); + } + + ngOnInit() { + const accountingMappings = this.CurrentProductsTemplate?.accountingMappings || {}; + // otherGlMappingsMap is at the root of the template, not inside accountingMappings + const mappings = { + ...accountingMappings, + otherGlMappingsMap: this.CurrentProductsTemplate?.otherGlMappingsMap + }; + const accountingMappingOptions = this.CurrentProductsTemplate?.accountingMappingOptions || {}; + const accountingRuleId = 2; // Always Cash + this.assetAccountOptions = accountingMappingOptions.assetAccountOptions || []; + this.liabilityAccountOptions = accountingMappingOptions.liabilityAccountOptions || []; + this.expenseAccountOptions = accountingMappingOptions.expenseAccountOptions || []; + this.incomeAccountOptions = accountingMappingOptions.incomeAccountOptions || []; + this.buildAccountingSectionConfigs(); + this.CurrentProductAccountingForm.patchValue({ + accountingRule: accountingRuleId + }); + + this.syncAccountingControls(accountingRuleId); + this.patchAccountingValues(mappings); + this.setOverdraftControl(); + this.setDormancyControl(); + } + + createCurrentProductAccountingForm() { + this.CurrentProductAccountingForm = this.formBuilder.group({ + accountingRule: [2], + assetsOtherGlMappings: this.formBuilder.array([]), + liabilitiesOtherGlMappings: this.formBuilder.array([]), + expensesOtherGlMappings: this.formBuilder.array([]), + incomeOtherGlMappings: this.formBuilder.array([]) + }); + } + + private setConditionalControls() { + this.CurrentProductAccountingForm.get('accountingRule')?.valueChanges.subscribe((accountingRule: number) => { + this.syncAccountingControls(accountingRule); + }); + } + + private patchAccountingValues(mappings: any) { + this.CurrentProductAccountingForm.patchValue({ + savingsReferenceAccountId: mappings.savingsReferenceAccount?.id || '', + savingsControlAccountId: mappings.savingsControlAccount?.id || '', + transfersInSuspenseAccountId: mappings.transfersInSuspenseAccount?.id || '', + interestOnSavingsAccountId: mappings.interestOnSavingsAccount?.id || '', + incomeFromFeeAccountId: mappings.incomeFromFeeAccount?.id || '', + incomeFromPenaltyAccountId: mappings.incomeFromPenaltyAccount?.id || '', + incomeFromInterestId: mappings.incomeFromInterest?.id || '', + overdraftPortfolioControlId: mappings.overdraftPortfolioControl?.id || '', + writeOffAccountId: mappings.writeOffAccount?.id || '', + feesReceivableAccountId: mappings.feeReceivableAccount?.id || '', + penaltiesReceivableAccountId: mappings.penaltyReceivableAccount?.id || '', + interestPayableAccountId: mappings.interestPayableAccount?.id || '', + interestReceivableAccountId: mappings.interestReceivableAccount?.id || '', + escheatLiabilityId: mappings.escheatLiabilityAccount?.id || '' + }); + + if (this.isCashOrAccrualAccounting()) { + // Backend serializes as 'otherGlMappingsMap'; fall back to 'otherGlMappings' for compatibility + const glMappings = mappings.otherGlMappingsMap || mappings.otherGlMappings; + this.setOtherGlMappings('assetsOtherGlMappings', glMappings?.assets || [], this.assetAccountOptions); + this.setOtherGlMappings( + 'liabilitiesOtherGlMappings', + glMappings?.liabilities || [], + this.liabilityAccountOptions + ); + this.setOtherGlMappings('expensesOtherGlMappings', glMappings?.expenses || [], this.expenseAccountOptions); + this.setOtherGlMappings('incomeOtherGlMappings', glMappings?.income || [], this.incomeAccountOptions); + } + } + + private createOtherGlMappingGroup(otherGlMapping: Partial = {}) { + return this.formBuilder.group({ + accountName: [ + otherGlMapping.accountName || '', + Validators.required + ], + glAccountId: [ + otherGlMapping.glAccountId || '', + Validators.required + ] + }); + } + + getOtherGlMappingsArray(controlName: string): UntypedFormArray { + return this.CurrentProductAccountingForm.get(controlName) as UntypedFormArray; + } + + private setOtherGlMappings(controlName: string, otherGlMappings: any[], options: GLAccount[]) { + const otherGlMappingsArray = this.getOtherGlMappingsArray(controlName); + otherGlMappingsArray.clear(); + (otherGlMappings || []).forEach((otherGlMapping) => { + otherGlMappingsArray.push( + this.createOtherGlMappingGroup({ + accountName: otherGlMapping.accountName, + glAccountId: + otherGlMapping.glAccountId || options.find((option) => option.glCode === otherGlMapping.glCode)?.id || null + }) + ); + }); + } + + addOtherGlMapping(controlName: string) { + this.getOtherGlMappingsArray(controlName).push(this.createOtherGlMappingGroup()); + } + + removeOtherGlMapping(controlName: string, index: number) { + this.getOtherGlMappingsArray(controlName).removeAt(index); + } + + getOtherGlMappingControl(controlName: string, index: number, fieldName: string): UntypedFormControl { + return this.getOtherGlMappingsArray(controlName).at(index).get(fieldName) as UntypedFormControl; + } + + private clearOtherGlMappings() { + [ + 'assetsOtherGlMappings', + 'liabilitiesOtherGlMappings', + 'expensesOtherGlMappings', + 'incomeOtherGlMappings' + ].forEach((controlName) => this.getOtherGlMappingsArray(controlName).clear()); + } + + private setOverdraftControl() { + this.allowOverdraft?.valueChanges?.subscribe(() => { + this.syncAccountingControls(this.CurrentProductAccountingForm.value.accountingRule); + }); + } + + private setDormancyControl() { + this.isDormancyTrackingActive?.valueChanges?.subscribe(() => { + this.syncAccountingControls(this.CurrentProductAccountingForm.value.accountingRule); + }); + } + + private getOtherGlMappings() { + const mapOtherGlMappings = (controlName: string, options: GLAccount[]) => + this.getOtherGlMappingsArray(controlName) + .value.filter((item: OtherGlMappingValue) => !!item.accountName || !!item.glAccountId) + .map((item: OtherGlMappingValue) => ({ + accountName: item.accountName, + glCode: options.find((option) => option.id === item.glAccountId)?.glCode || '' + })) + .filter((item: { accountName: string; glCode: string }) => !!item.accountName || !!item.glCode); + + const otherGlMappings = { + assets: mapOtherGlMappings('assetsOtherGlMappings', this.assetAccountOptions), + liabilities: mapOtherGlMappings('liabilitiesOtherGlMappings', this.liabilityAccountOptions), + expenses: mapOtherGlMappings('expensesOtherGlMappings', this.expenseAccountOptions), + income: mapOtherGlMappings('incomeOtherGlMappings', this.incomeAccountOptions) + }; + + return Object.values(otherGlMappings).some((value) => value.length) ? otherGlMappings : null; + } + + isDormancyTrackingEnabled(): boolean { + return this.isDormancyTrackingActive?.value ?? this.isDormancyTrackingActive ?? false; + } + + private syncAccountingControls(accountingRule: number) { + const isDormancyActive = this.isDormancyTrackingEnabled(); + + const addControl = (controlName: string, required = false) => { + if (!this.CurrentProductAccountingForm.contains(controlName)) { + this.CurrentProductAccountingForm.addControl( + controlName, + new UntypedFormControl('', required ? Validators.required : undefined) + ); + } else if (required) { + this.CurrentProductAccountingForm.get(controlName)?.setValidators(Validators.required); + this.CurrentProductAccountingForm.get(controlName)?.updateValueAndValidity({ emitEvent: false }); + } + }; + + const removeControl = (controlName: string) => { + if (this.CurrentProductAccountingForm.contains(controlName)) { + this.CurrentProductAccountingForm.removeControl(controlName); + } + }; + + if (this.accounting.isCashOrAccrualAccountingRuleId(accountingRule)) { + addControl('savingsReferenceAccountId', true); + addControl('savingsControlAccountId', true); + addControl('transfersInSuspenseAccountId', true); + addControl('interestOnSavingsAccountId', true); + addControl('incomeFromFeeAccountId', true); + addControl('incomeFromPenaltyAccountId', true); + addControl('incomeFromInterestId', true); + addControl('writeOffAccountId', true); + addControl('overdraftPortfolioControlId', false); + + if (this.accounting.isAccrualAccountingRuleId(accountingRule)) { + addControl('feesReceivableAccountId', true); + addControl('penaltiesReceivableAccountId', true); + addControl('interestPayableAccountId', true); + addControl('interestReceivableAccountId', false); + } else { + removeControl('feesReceivableAccountId'); + removeControl('penaltiesReceivableAccountId'); + removeControl('interestPayableAccountId'); + removeControl('interestReceivableAccountId'); + } + + if (isDormancyActive) { + addControl('escheatLiabilityId', false); + } else { + removeControl('escheatLiabilityId'); + } + } else { + removeControl('savingsReferenceAccountId'); + removeControl('savingsControlAccountId'); + removeControl('transfersInSuspenseAccountId'); + removeControl('interestOnSavingsAccountId'); + removeControl('incomeFromFeeAccountId'); + removeControl('incomeFromPenaltyAccountId'); + removeControl('incomeFromInterestId'); + removeControl('writeOffAccountId'); + removeControl('overdraftPortfolioControlId'); + removeControl('feesReceivableAccountId'); + removeControl('penaltiesReceivableAccountId'); + removeControl('interestPayableAccountId'); + removeControl('interestReceivableAccountId'); + removeControl('escheatLiabilityId'); + this.clearOtherGlMappings(); + } + } + + get CurrentProductAccounting() { + const { + assetsOtherGlMappings, + liabilitiesOtherGlMappings, + expensesOtherGlMappings, + incomeOtherGlMappings, + ...CurrentProductAccounting + } = this.CurrentProductAccountingForm.value; + const otherGlMappings = this.getOtherGlMappings(); + + return { + ...CurrentProductAccounting, + ...(otherGlMappings ? { otherGlMappings } : {}) + }; + } + + isCashOrAccrualAccounting(): boolean { + return this.accounting.isCashOrAccrualAccountingRuleId(this.CurrentProductAccountingForm.value.accountingRule); + } + + isAccrualAccounting(): boolean { + return this.accounting.isAccrualAccountingRuleId(this.CurrentProductAccountingForm.value.accountingRule); + } + + buildAccountingSectionConfigs() { + this.accountingSectionConfigs = [ + { + key: 'assets', + title: 'Assets', + pillLabel: 'Asset', + pillClass: 'green', + rows: [ + { + key: 'savingsReferenceAccountId', + label: 'Savings Reference Account', + inputLabel: 'Savings reference account', + controlName: 'savingsReferenceAccountId', + options: this.assetAccountOptions, + required: true + }, + { + key: 'overdraftPortfolioControlId', + label: 'Overdraft Portfolio Control', + inputLabel: 'Overdraft portfolio control', + controlName: 'overdraftPortfolioControlId', + options: this.assetAccountOptions + }, + { + key: 'interestReceivableAccountId', + label: 'Interest Receivable Account', + inputLabel: 'Interest receivable account', + controlName: 'interestReceivableAccountId', + options: this.assetAccountOptions, + accrualOnly: true + }, + { + key: 'feesReceivableAccountId', + label: 'Fees Receivable Account', + inputLabel: 'Fees receivable account', + controlName: 'feesReceivableAccountId', + options: this.assetAccountOptions, + accrualOnly: true + }, + { + key: 'penaltiesReceivableAccountId', + label: 'Penalties Receivable Account', + inputLabel: 'Penalties receivable account', + controlName: 'penaltiesReceivableAccountId', + options: this.assetAccountOptions, + accrualOnly: true + } + ], + otherMappingsControlName: 'assetsOtherGlMappings', + otherAccountOptions: this.assetAccountOptions + }, + { + key: 'liabilities', + title: 'Liabilities', + pillLabel: 'Liability', + pillClass: 'green', + rows: [ + { + key: 'savingsControlAccountId', + label: 'Savings Control Account', + inputLabel: 'Savings control account', + controlName: 'savingsControlAccountId', + options: this.liabilityAccountOptions, + required: true + }, + { + key: 'transfersInSuspenseAccountId', + label: 'Transfers In Suspense Account', + inputLabel: 'Transfers in suspense account', + controlName: 'transfersInSuspenseAccountId', + options: this.liabilityAccountOptions, + required: true + }, + { + key: 'interestPayableAccountId', + label: 'Interest Payable Account', + inputLabel: 'Interest payable account', + controlName: 'interestPayableAccountId', + options: this.liabilityAccountOptions, + accrualOnly: true + }, + { + key: 'escheatLiabilityId', + label: 'Escheat Liability Account', + inputLabel: 'Escheat liability account', + controlName: 'escheatLiabilityId', + options: this.liabilityAccountOptions, + dormancyOnly: true + } + ], + otherMappingsControlName: 'liabilitiesOtherGlMappings', + otherAccountOptions: this.liabilityAccountOptions + }, + { + key: 'expenses', + title: 'Expenses', + pillLabel: 'Expense', + pillClass: 'amber', + rows: [ + { + key: 'interestOnSavingsAccountId', + label: 'Interest On Savings Account', + inputLabel: 'Interest on savings account', + controlName: 'interestOnSavingsAccountId', + options: this.expenseAccountOptions, + required: true + }, + { + key: 'writeOffAccountId', + label: 'Write Off Account', + inputLabel: 'Write off account', + controlName: 'writeOffAccountId', + options: this.expenseAccountOptions, + required: true + } + ], + otherMappingsControlName: 'expensesOtherGlMappings', + otherAccountOptions: this.expenseAccountOptions + }, + { + key: 'income', + title: 'Income', + pillLabel: 'Income', + pillClass: 'blue', + rows: [ + { + key: 'incomeFromFeeAccountId', + label: 'Income From Fees', + inputLabel: 'Income from fees', + controlName: 'incomeFromFeeAccountId', + options: this.incomeAccountOptions, + required: true + }, + { + key: 'incomeFromPenaltyAccountId', + label: 'Income From Penalties', + inputLabel: 'Income from penalties', + controlName: 'incomeFromPenaltyAccountId', + options: this.incomeAccountOptions, + required: true + }, + { + key: 'incomeFromInterestId', + label: 'Income From Interest', + inputLabel: 'Income from interest', + controlName: 'incomeFromInterestId', + options: this.incomeAccountOptions, + required: true + } + ], + otherMappingsControlName: 'incomeOtherGlMappings', + otherAccountOptions: this.incomeAccountOptions + } + ]; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-charges-step/current-product-charges-step.component.html b/src/app/products/current-products/current-product-stepper/current-product-charges-step/current-product-charges-step.component.html new file mode 100644 index 000000000..c72d119fe --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-charges-step/current-product-charges-step.component.html @@ -0,0 +1,93 @@ + + +
+
+ +

{{ 'labels.inputs.Charges' | translate }}

+
+
+
+ + {{ 'labels.inputs.Charge' | translate }} + + @for (charge of chargeData | chargesFilter: chargesDataSource : currencyCode.value; track charge) { + + {{ charge.name }} + + } + + + + +
+ + @if (chargesDataSource.length > 0) { +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.Name' | translate }} + {{ charge.name + ', ' + charge.currency.displaySymbol }} + {{ 'labels.inputs.Type' | translate }} + {{ charge.chargeCalculationType.value | translateKey: 'catalogs' }} + {{ 'labels.inputs.Amount' | translate }} + {{ charge.amount | formatNumber }} + {{ 'labels.inputs.Collected On' | translate }} + {{ charge.chargeTimeType.value | translateKey: 'catalogs' }} + {{ 'labels.inputs.Actions' | translate }} + +
+
+ } + + @if (chargesDataSource.length === 0) { +
+ + {{ 'labels.text.No charges added yet' | translate }} +
+ } +
+
diff --git a/src/app/products/current-products/current-product-stepper/current-product-charges-step/current-product-charges-step.component.scss b/src/app/products/current-products/current-product-stepper/current-product-charges-step/current-product-charges-step.component.scss new file mode 100644 index 000000000..f272b71f6 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-charges-step/current-product-charges-step.component.scss @@ -0,0 +1,103 @@ +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +$primary: #1353a0; +$accent: #0ea5e9; +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-500: #9aa0a6; +$gray-900: #202124; + +.spch-section { + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; + background: #fff; + + &__header { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 24px; + background: $gray-50; + border-bottom: 1px solid $gray-200; + + i { + font-size: 14px; + color: $accent; + } + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + } +} + +.spch-body { + padding: 16px 24px; +} + +.spch-add-row { + display: flex; + align-items: flex-start; + gap: 12px; + + &__select { + flex: 1; + max-width: 400px; + } + + &__btn { + margin-top: 4px; + height: 36px !important; + border-radius: 8px !important; + font-size: 13px !important; + font-weight: 600 !important; + box-shadow: 0 1px 3px rgba($primary, 0.3) !important; + } + + @media (width <= 600px) { + flex-direction: column; + + &__select { + max-width: 100%; + width: 100%; + } + } +} + +.spch-table-wrap { + margin-top: 8px; + border: 1px solid $border; + border-radius: 8px; + overflow: hidden; +} + +.spch-table { + width: 100%; +} + +.spch-empty { + display: flex; + align-items: center; + gap: 8px; + padding: 16px; + margin-top: 8px; + background: $gray-50; + border-radius: 8px; + color: $gray-500; + font-size: 13px; + + i { + font-size: 14px; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-charges-step/current-product-charges-step.component.ts b/src/app/products/current-products/current-product-stepper/current-product-charges-step/current-product-charges-step.component.ts new file mode 100644 index 000000000..d6cafa12d --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-charges-step/current-product-charges-step.component.ts @@ -0,0 +1,108 @@ +/** + * 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'; +import { UntypedFormControl } from '@angular/forms'; +import { MatDialog } from '@angular/material/dialog'; + +import { TranslateService } from '@ngx-translate/core'; +import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; +import { MatButton, MatIconButton } from '@angular/material/button'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow +} from '@angular/material/table'; +import { ChargesFilterPipe } from '../../../../pipes/charges-filter.pipe'; +import { FormatNumberPipe } from '../../../../pipes/format-number.pipe'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-current-product-charges-step', + templateUrl: './current-product-charges-step.component.html', + styleUrls: ['./current-product-charges-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatIconButton, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, + ChargesFilterPipe, + FormatNumberPipe + ] +}) +export class CurrentProductChargesStepComponent implements OnInit { + dialog = inject(MatDialog); + private translateService = inject(TranslateService); + + @Input() CurrentProductsTemplate: any; + @Input() currencyCode: UntypedFormControl; + + chargeData: any; + + chargesDataSource: {}[]; + displayedColumns: string[] = [ + 'name', + 'chargeCalculationType', + 'amount', + 'chargeTimeType', + 'action' + ]; + + pristine = true; + + ngOnInit() { + this.chargeData = this.CurrentProductsTemplate.chargeOptions; + + this.chargesDataSource = this.CurrentProductsTemplate.charges || []; + this.pristine = true; + + this.currencyCode.valueChanges.subscribe(() => (this.chargesDataSource = [])); + } + + addCharge(charge: any) { + this.chargesDataSource = this.chargesDataSource.concat([charge.value]); + charge.value = ''; + this.pristine = false; + } + + deleteCharge(charge: any) { + const deleteChargeDialogRef = this.dialog.open(DeleteDialogComponent, { + data: { deleteContext: this.translateService.instant('labels.inputs.Charge') + ' ' + charge.name } + }); + deleteChargeDialogRef.afterClosed().subscribe((response: any) => { + if (response.delete) { + this.chargesDataSource.splice(this.chargesDataSource.indexOf(charge), 1); + this.chargesDataSource = this.chargesDataSource.concat([]); + this.pristine = false; + } + }); + } + + get CurrentProductCharges() { + return { + charges: this.chargesDataSource + }; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-currency-step/current-product-currency-step.component.html b/src/app/products/current-products/current-product-stepper/current-product-currency-step/current-product-currency-step.component.html new file mode 100644 index 000000000..1fb165c2d --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-currency-step/current-product-currency-step.component.html @@ -0,0 +1,111 @@ + + +
+
+
+ + Currency & Account Status Configuration +
+
+
+ + {{ 'labels.inputs.Currency' | translate }} + + @for (currency of currencyData; track currency) { + + {{ currency.name }} + + } + + Default: Indian Rupee - auto-set for domestic products + + {{ 'labels.inputs.Currency' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + + {{ 'labels.inputs.Decimal Places' | translate }} + + @for (places of decimalPlacesData; track places) { + {{ places }} + } + + + {{ 'labels.inputs.Decimal Places' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + +
+ +
+
+

Dormancy Alert

+

Enable automated notifications when an account becomes inactive based on product rules.

+
+ + yes + no + +
+ +
+
+

Balance Swap Option

+

Enable automated transfer of excess funds to linked deposits or vice-versa to maintain liquidity.

+
+ + yes + no + +
+ +
+
+

MSME

+

Micro, Small and Medium Enterprises

+
+ + yes + no + +
+ +
+
+

Nomination Facility

+

Allow account holders to designate individuals to receive funds in the event of account holder's death.

+
+ + yes + no + +
+ +
+
+

Auto Debit Facility

+

Enable automatic deduction of funds for recurring payments, utility bills, or EMI obligations.

+
+ + yes + no + +
+
+
+
diff --git a/src/app/products/current-products/current-product-stepper/current-product-currency-step/current-product-currency-step.component.scss b/src/app/products/current-products/current-product-stepper/current-product-currency-step/current-product-currency-step.component.scss new file mode 100644 index 000000000..a287d691e --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-currency-step/current-product-currency-step.component.scss @@ -0,0 +1,114 @@ +/** + * 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/. + */ + +$accent: #0ea5e9; +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-900: #202124; + +.spc-section { + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; + background: #fff; + + & + & { + margin-top: 16px; + } + + &__header { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 24px; + background: $gray-50; + border-bottom: 1px solid $gray-200; + + i { + font-size: 14px; + color: $accent; + } + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + } +} + +.spc-section__badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: #eff6ff; + font-size: 14px; +} + +.spc-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px 16px; + padding: 16px 24px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.spc-body { + background: #fff; +} + +.spc-field { + width: 100%; +} + +.spc-alert { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + padding: 18px 24px 24px; + border-top: 1px solid $border; + background: #f8fafc; + + @media (width <= 760px) { + flex-direction: column; + align-items: flex-start; + } + + &__content { + h4 { + margin: 0 0 6px; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + + p { + margin: 0; + font-size: 12px; + line-height: 1.5; + color: #64748b; + max-width: 540px; + } + } + + &__choices { + display: flex; + align-items: center; + gap: 18px; + flex-wrap: wrap; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-currency-step/current-product-currency-step.component.ts b/src/app/products/current-products/current-product-stepper/current-product-currency-step/current-product-currency-step.component.ts new file mode 100644 index 000000000..37c7b4c56 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-currency-step/current-product-currency-step.component.ts @@ -0,0 +1,94 @@ +/** + * 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, Input, inject } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +import { MatTooltip } from '@angular/material/tooltip'; +import { MatRadioGroup, MatRadioButton } from '@angular/material/radio'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-current-product-currency-step', + templateUrl: './current-product-currency-step.component.html', + styleUrls: ['./current-product-currency-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatTooltip, + MatRadioGroup, + MatRadioButton, + FaIconComponent + ] +}) +export class CurrentProductCurrencyStepComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + + @Input() CurrentProductsTemplate: any; + + CurrentProductCurrencyForm: UntypedFormGroup; + + currencyData: any; + decimalPlacesData = [ + 2, + 0, + 3 + ]; + + constructor() { + this.createCurrentProductCurrencyForm(); + } + + ngOnInit() { + this.currencyData = this.CurrentProductsTemplate.currencyOptions; + + this.CurrentProductCurrencyForm.patchValue({ + currencyCode: this.CurrentProductsTemplate.currency?.code || this.currencyData?.[0]?.code || '', + digitsAfterDecimal: + this.CurrentProductsTemplate.digitsAfterDecimal ?? this.CurrentProductsTemplate.currency?.decimalPlaces ?? 2, + dormancyAlert: this.CurrentProductsTemplate.dormancyAlert ?? false, + balanceSwapOption: this.CurrentProductsTemplate.balanceSwapOption ?? false, + msme: this.CurrentProductsTemplate.msme ?? false, + nominationFacility: this.CurrentProductsTemplate.nominationFacility ?? false, + autoDebitFacility: this.CurrentProductsTemplate.autoDebitFacility ?? false + }); + } + + createCurrentProductCurrencyForm() { + this.CurrentProductCurrencyForm = this.formBuilder.group({ + currencyCode: [ + '', + Validators.required + ], + digitsAfterDecimal: [ + 2, + [ + Validators.required, + Validators.min(0) + ] + ], + dormancyAlert: [false], + balanceSwapOption: [false], + msme: [false], + nominationFacility: [false], + autoDebitFacility: [false] + }); + } + + get CurrentProductCurrency() { + const formValue = this.CurrentProductCurrencyForm.value; + return { + currencyCode: formValue.currencyCode, + digitsAfterDecimal: formValue.digitsAfterDecimal, + dormancyAlert: formValue.dormancyAlert, + balanceSwapOption: formValue.balanceSwapOption, + msme: formValue.msme, + nominationFacility: formValue.nominationFacility, + autoDebitFacility: formValue.autoDebitFacility + }; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.html b/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.html new file mode 100644 index 000000000..65c350380 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.html @@ -0,0 +1,121 @@ + + +
+
+
+ +

{{ 'labels.inputs.Product Name' | translate }} & {{ 'labels.inputs.Description' | translate }}

+
+
+ + {{ 'labels.inputs.Product Name' | translate }} + + + {{ 'labels.inputs.Product Name' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + + {{ 'labels.inputs.Short Name' | translate }} + + + {{ 'labels.inputs.Short Name' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + + {{ 'labels.inputs.Description' | translate }} + + +
+
+ +
+
+ +

Product Classification

+
+ +
+ + Account Type + + @for (accountType of accountTypeData; track accountType) { + {{ accountType }} + } + + Account Type is required + + + + Customer Segment + + @for (customerSegment of customerSegmentData; track customerSegment) { + {{ customerSegment }} + } + + + + + GL Account (Liability) + + @for (account of liabilityAccountOptions; track account.id) { + {{ account.name }} ({{ account.glCode }}) + } + + + + + Effective To + + + + + + + End Date + + + + + + + Introducer Required + + @for (option of yesNoOptions; track option.value) { + {{ option.label }} + } + + + +
+ Share Linking Enabled +
+
+
+
diff --git a/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.scss b/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.scss new file mode 100644 index 000000000..7b5c920ef --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.scss @@ -0,0 +1,70 @@ +/** + * 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/. + */ + +$accent: #0ea5e9; +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-900: #202124; + +.spd-section { + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; + background: #fff; + + &__header { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 24px; + background: $gray-50; + border-bottom: 1px solid $gray-200; + + i { + font-size: 14px; + color: $accent; + } + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + } +} + +.spd-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px 16px; + padding: 16px 24px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.spd-grid--classification { + grid-template-columns: repeat(3, 1fr); +} + +.spd-field { + width: 100%; + + &--full { + grid-column: 1 / -1; + } +} + +.spd-checkbox-row { + display: flex; + align-items: center; + min-height: 56px; +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.ts b/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.ts new file mode 100644 index 000000000..0c7125ca5 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-details-step/current-product-details-step.component.ts @@ -0,0 +1,198 @@ +/** + * 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, OnChanges, Input, SimpleChanges, inject, ChangeDetectorRef } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +import { MatTooltip } from '@angular/material/tooltip'; +import { CdkTextareaAutosize } from '@angular/cdk/text-field'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { Dates } from 'app/core/utils/dates'; +import { SettingsService } from 'app/settings/settings.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-current-product-details-step', + templateUrl: './current-product-details-step.component.html', + styleUrls: ['./current-product-details-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatTooltip, + CdkTextareaAutosize, + FaIconComponent + ] +}) +export class CurrentProductDetailsStepComponent implements OnInit, OnChanges { + private formBuilder = inject(UntypedFormBuilder); + private dateUtils = inject(Dates); + private settingsService = inject(SettingsService); + private cdr = inject(ChangeDetectorRef); + + @Input() CurrentProductsTemplate: any; + + CurrentProductDetailsForm: UntypedFormGroup; + accountTypeData: string[] = []; + customerSegmentData: string[] = []; + liabilityAccountOptions: any[] = []; + yesNoOptions: Array<{ + value: boolean; + label: string; + }> = [ + { + value: true, + label: 'Yes' + }, + { + value: false, + label: 'No' + } + ]; + + constructor() { + this.createCurrentProductDetailsForm(); + } + + ngOnInit() { + if (this.CurrentProductsTemplate) { + this.applyTemplate(this.CurrentProductsTemplate); + } + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.CurrentProductsTemplate?.currentValue) { + this.applyTemplate(changes.CurrentProductsTemplate.currentValue); + } + } + + createCurrentProductDetailsForm() { + this.CurrentProductDetailsForm = this.formBuilder.group({ + name: [ + '', + Validators.required + ], + shortName: [ + '', + Validators.required + ], + description: [''], + accountType: [ + null, + Validators.required + ], + customerSegment: [null], + glAccountId: [null], + effectiveTo: [''], + endDate: [''], + introducerRequired: [false], + shareLinkingEnabled: [false] + }); + } + + private getOptionNames(options: any[]): string[] { + if (!Array.isArray(options)) { + return []; + } + + return options + .map((option) => option?.name ?? option?.value ?? option) + .filter((value) => typeof value === 'string' && value.trim().length > 0); + } + + private parseDate(dateValue: any): Date | null { + if (!dateValue) return null; + + // Handle array format [year, month, day] + if (Array.isArray(dateValue)) { + return new Date(dateValue[0], dateValue[1] - 1, dateValue[2]); + } + + // Handle Date object + if (dateValue instanceof Date) { + return dateValue; + } + + // Handle string format + return new Date(dateValue); + } + + private applyTemplate(template: any) { + const accountTypeOptions = this.getOptionNames(template.accountTypeOptions); + const customerSegmentOptions = this.getOptionNames(template.customerSegmentOptions); + const yesNoOptions = this.getYesNoOptions(template.yesNoOptions); + + this.accountTypeData = accountTypeOptions; + this.customerSegmentData = customerSegmentOptions; + this.liabilityAccountOptions = template.accountingMappingOptions?.liabilityAccountOptions ?? []; + this.cdr.detectChanges(); + if (yesNoOptions.length) { + this.yesNoOptions = yesNoOptions; + } + + // Use existing product data if available (when editing), otherwise use template defaults + const isEditing = !!template.id; + this.CurrentProductDetailsForm.patchValue({ + name: isEditing ? template.name : template.name || '', + shortName: isEditing ? template.shortName : template.shortName || '', + description: template.description, + accountType: template.accountType ?? this.accountTypeData[0] ?? null, + customerSegment: template.customerSegment ?? this.customerSegmentData[0] ?? null, + glAccountId: template.glAccountId ?? null, + effectiveTo: this.parseDate(template.effectiveTo), + endDate: this.parseDate(template.endDate), + introducerRequired: template.introducerRequired === true || template.introducerRequired === 'Yes', + shareLinkingEnabled: !!template.shareLinkingEnabled + }); + } + + private getYesNoOptions(options: any[]): Array<{ value: boolean; label: string }> { + if (!Array.isArray(options)) { + return []; + } + + return options + .map((option) => { + const rawValue = option?.name ?? option?.value ?? option; + const normalized = typeof rawValue === 'string' ? rawValue.trim().toUpperCase() : rawValue; + + if (normalized === 'YES') { + return { + value: true, + label: 'Yes' + }; + } + + if (normalized === 'NO') { + return { + value: false, + label: 'No' + }; + } + + return null; + }) + .filter((value) => !!value); + } + + compareById(o1: any, o2: any): boolean { + return o1 != null && o2 != null && Number(o1) === Number(o2); + } + + get CurrentProductDetails() { + const formValue = { ...this.CurrentProductDetailsForm.value }; + const dateFormat = this.settingsService.dateFormat; + + if (formValue.effectiveTo instanceof Date) { + formValue.effectiveTo = this.dateUtils.formatDate(formValue.effectiveTo, dateFormat) || ''; + } + + if (formValue.endDate instanceof Date) { + formValue.endDate = this.dateUtils.formatDate(formValue.endDate, dateFormat) || ''; + } + + return formValue; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-limits-step/current-product-limits-step.component.html b/src/app/products/current-products/current-product-stepper/current-product-limits-step/current-product-limits-step.component.html new file mode 100644 index 000000000..0a78185a6 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-limits-step/current-product-limits-step.component.html @@ -0,0 +1,351 @@ + +
+
+ + + +
Transaction & Product Limits
+
R&D BASED DEFAULTS
+
+ +
+
+
Account Features (as per RBI guidelines)
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + @for (row of accountFacilityLimits; track row.limitType) { + + + + + + + } + +
Limit TypeGeneral Limit (Daily)Minor Account LimitSmall Saving Limit
{{ row.limitType }} + + + + + +
+
+ +
+

Deposit Limit (KYC)

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Account TypeKYC TypeLimit
General + + + +
Minor + + + +
Small Saving + + + +
+
+ +
+ Note: + Small Saving Account limits are strictly capped at Rs.1,00,000 maximum balance and Rs.10,000 monthly withdrawal + per RBI Master Circular. Minor accounts are restricted from RTGS and high-risk digital products (BEPG/BBPS) by + default. +
+
+
diff --git a/src/app/products/current-products/current-product-stepper/current-product-limits-step/current-product-limits-step.component.scss b/src/app/products/current-products/current-product-stepper/current-product-limits-step/current-product-limits-step.component.scss new file mode 100644 index 000000000..f0f7cad32 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-limits-step/current-product-limits-step.component.scss @@ -0,0 +1,257 @@ +/** + * 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/. + */ + +$border: #d9e2ec; +$border-strong: #c7d2de; +$text: #24364b; +$muted: #64748b; +$header-bg: #f8fafc; +$highlight-bg: #eff6ff; + +.spl-section { + border: 1px solid $border; + border-radius: 12px; + overflow: hidden; + background: #fff; +} + +.spl-section__header { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + background: $header-bg; + border-bottom: 1px solid $border; +} + +.spl-section__icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 8px; + background: #eff6ff; + color: #2563eb; + font-size: 14px; + flex: 0 0 auto; +} + +.spl-section__title { + flex: 1 1 auto; + font-size: 16px; + font-weight: 600; + color: $text; +} + +.spl-section__badge { + flex: 0 0 auto; + padding: 5px 10px; + border-radius: 999px; + background: #dbeafe; + color: #1d4ed8; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.05em; + white-space: nowrap; +} + +.spl-section__body { + padding: 18px 20px 20px; +} + +.spl-subsection { + border: 1px solid $border; + border-radius: 12px; + overflow: hidden; + background: #fff; + + & + & { + margin-top: 16px; + } +} + +.spl-subsection__header { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; + background: $header-bg; + border-bottom: 1px solid $border; + color: $text; + font-size: 14px; + font-weight: 600; +} + +.spl-subsection__icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + border-radius: 8px; + background: #eff6ff; + color: #2563eb; + font-size: 13px; + flex: 0 0 auto; +} + +.spl-subsection__body { + padding: 16px 16px 18px; +} + +.spl-table-wrap { + overflow-x: auto; +} + +.spl-table { + width: 100%; + min-width: 0; + table-layout: fixed; + border-collapse: collapse; + border: 1px solid $border; + + col.spl-table__col--label { + width: 22%; + } + + col.spl-table__col--value { + width: 26%; + } + + th, + td { + border: 1px solid $border; + padding: 6px 8px; + vertical-align: middle; + } + + th { + background: #f8fafc; + color: #64748b; + text-align: center; + font-size: 11px; + font-weight: 700; + line-height: 1.3; + white-space: normal; + word-break: break-word; + } + + td:first-child { + background: #f8fafc; + font-weight: 600; + color: $text; + white-space: nowrap; + } + + td { + font-size: 13px; + overflow: hidden; + } +} + +.spl-table__highlight td:first-child { + color: #1d4ed8; +} + +.spl-input, +.spl-select { + width: 100%; + box-sizing: border-box; + font: inherit; +} + +.spl-input { + height: 30px; + border: 1px solid $border-strong; + border-radius: 2px; + padding: 0 8px; + background: #fff; +} + +.spl-input:disabled, +.spl-select:disabled { + background: #f8fafc; + color: $muted; +} + +.spl-select { + height: 30px; + border: 1px solid $border-strong; + border-radius: 2px; + padding: 0 6px; + background: #fff; +} + +.spl-input--stacked { + margin-top: 6px; +} + +.spl-input--disabled { + background: #f8fafc; + color: $muted; +} + +.spl-table__muted { + font-size: 11px; + color: #94a3b8; + letter-spacing: 0.03em; +} + +.spl-note { + margin-top: 16px; + padding: 12px 14px; + border: 1px solid $border; + border-radius: 8px; + background: #f8fafc; + color: $muted; + font-size: 11px; + line-height: 1.6; +} + +.spl-features { + margin-bottom: 16px; +} + +.spl-features__grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px 16px; + + @media (width <= 900px) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + @media (width <= 640px) { + grid-template-columns: 1fr; + } +} + +.spl-feature { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 13px; + color: $text; +} + +.spl-feature__label { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 18px; + font-weight: 600; + color: #334155; + + i { + color: #2563eb; + font-size: 12px; + width: 14px; + text-align: center; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-limits-step/current-product-limits-step.component.ts b/src/app/products/current-products/current-product-stepper/current-product-limits-step/current-product-limits-step.component.ts new file mode 100644 index 000000000..a210e2ae2 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-limits-step/current-product-limits-step.component.ts @@ -0,0 +1,250 @@ +/** + * 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, OnChanges, SimpleChanges } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +type BooleanFeatureKey = + | 'atmFacility' + | 'atmWithdrawalFacility' + | 'upiFacility' + | 'posTransactionsFacility' + | 'chequeBook' + | 'impsFacility' + | 'smsAlerts' + | 'bbpsFacility' + | 'insuranceCover' + | 'bepgFacility' + | 'neftFacility' + | 'rtgsFacility' + | 'interBankTotalCapFacility' + | 'withdrawalFacility' + | 'depositFacilityKyc' + | 'virtualDebitCard'; + +interface FacilityLimitRow { + limitType: string; + generalLimit: string | number | null; + minorAccountLimit: string | number | null; + smallSavingLimit: string | number | null; +} + +@Component({ + selector: 'mifosx-current-product-limits-step', + templateUrl: './current-product-limits-step.component.html', + styleUrls: ['./current-product-limits-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FormsModule + ] +}) +export class CurrentProductLimitsStepComponent implements OnChanges { + @Input() CurrentProductsTemplate: any; + pristine = true; + + yesNoOptions: Array<{ + value: boolean; + label: string; + }> = [ + { + value: true, + label: 'Yes' + }, + { + value: false, + label: 'No' + } + ]; + + accountFacilityConfig: Record = { + atmFacility: true, + atmWithdrawalFacility: true, + upiFacility: true, + posTransactionsFacility: true, + chequeBook: true, + impsFacility: true, + smsAlerts: true, + bbpsFacility: true, + insuranceCover: false, + bepgFacility: true, + neftFacility: true, + rtgsFacility: true, + interBankTotalCapFacility: true, + withdrawalFacility: true, + depositFacilityKyc: true, + virtualDebitCard: false + }; + + accountFacilityLimits: FacilityLimitRow[] = [ + { + limitType: 'ATM Withdrawal', + generalLimit: 40000, + minorAccountLimit: 5000, + smallSavingLimit: 10000 + }, + { + limitType: 'POS Transactions', + generalLimit: 100000, + minorAccountLimit: 2000, + smallSavingLimit: 10000 + }, + { + limitType: 'UPI Limit', + generalLimit: 50000, + minorAccountLimit: 1000, + smallSavingLimit: 5000 + }, + { + limitType: 'IMPS Limit', + generalLimit: 50000, + minorAccountLimit: 100, + smallSavingLimit: 5000 + }, + { + limitType: 'BBPS', + generalLimit: 25000, + minorAccountLimit: 0, + smallSavingLimit: 2000 + }, + { + limitType: 'BEPG', + generalLimit: 25000, + minorAccountLimit: 0, + smallSavingLimit: 2000 + }, + { + limitType: 'NEFT Limit', + generalLimit: 1000000, + minorAccountLimit: 25000, + smallSavingLimit: 10000 + }, + { + limitType: 'RTGS Limit (Min 2L)', + generalLimit: 5000000, + minorAccountLimit: 'N/A', + smallSavingLimit: 'N/A' + }, + { + limitType: 'Inter-Bank Total Cap', + generalLimit: 5000000, + minorAccountLimit: 25000, + smallSavingLimit: 10000 + }, + { + limitType: 'Withdrawal Limit', + generalLimit: 'UNLIMITED', + minorAccountLimit: 10000, + smallSavingLimit: 10000 + } + ]; + + depositLimitKyc = { + general: { + kycType: 'Full KYC', + limit: 'No Limit' + }, + minor: { + kycType: 'Guardian KYC', + limit: 100000 + }, + smallSaving: { + kycType: 'Min KYC', + limit: 50000 + } + }; + + ngOnChanges(changes: SimpleChanges) { + if (changes.CurrentProductsTemplate?.currentValue) { + const template = changes.CurrentProductsTemplate.currentValue; + this.setYesNoOptions(template); + this.populateFromTemplate(template); + } + } + + markAsDirty() { + this.pristine = false; + } + + private populateFromTemplate(template: any) { + const isEditing = !!template.id; + + // Populate accountFacilityConfig from existing product + if (isEditing && template.accountFacilityConfig) { + this.accountFacilityConfig = { ...this.accountFacilityConfig, ...template.accountFacilityConfig }; + } + + // Populate accountFacilityLimits from existing product + if (isEditing && template.accountFacilityLimits && template.accountFacilityLimits.length > 0) { + this.accountFacilityLimits = template.accountFacilityLimits.map((limit: any) => ({ + limitType: limit.limitType || '', + generalLimit: limit.generalLimit ?? null, + minorAccountLimit: limit.minorAccountLimit ?? null, + smallSavingLimit: limit.smallSavingLimit ?? null + })); + } + + // Populate depositLimitKyc from existing product + if (isEditing && template.depositLimitKyc) { + this.depositLimitKyc = { + general: { + kycType: template.depositLimitKyc.general?.kycType || 'Full KYC', + limit: template.depositLimitKyc.general?.limit || 'No Limit' + }, + minor: { + kycType: template.depositLimitKyc.minor?.kycType || 'Guardian KYC', + limit: template.depositLimitKyc.minor?.limit ?? 100000 + }, + smallSaving: { + kycType: template.depositLimitKyc.smallSaving?.kycType || 'Min KYC', + limit: template.depositLimitKyc.smallSaving?.limit ?? 50000 + } + }; + } + } + + private setYesNoOptions(template: any) { + const options = Array.isArray(template?.yesNoOptions) ? template.yesNoOptions : []; + const mappedOptions = options + .map((option: any) => { + const rawValue = option?.name ?? option?.value ?? option; + const normalized = typeof rawValue === 'string' ? rawValue.trim().toUpperCase() : rawValue; + + if (normalized === 'YES') { + return { + value: true, + label: 'Yes' + }; + } + + if (normalized === 'NO') { + return { + value: false, + label: 'No' + }; + } + + return null; + }) + .filter((value: any) => !!value); + + if (mappedOptions.length) { + this.yesNoOptions = mappedOptions; + } + } + + get CurrentProductLimits() { + return { + accountFacilityConfig: { ...this.accountFacilityConfig }, + accountFacilityLimits: this.accountFacilityLimits.map((row) => ({ ...row })), + depositLimitKyc: { + ...this.depositLimitKyc + } + }; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.html b/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.html new file mode 100644 index 000000000..c74008227 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.html @@ -0,0 +1,799 @@ + + +
+

{{ CurrentProduct.name }}

+ + +

{{ 'labels.inputs.Details' | translate }}

+ + +
+ {{ 'labels.inputs.Short Name' | translate }}: + {{ CurrentProduct.shortName }} +
+ + @if (CurrentProduct.description) { +
+ {{ 'labels.inputs.Description' | translate }}: + {{ CurrentProduct.description }} +
+ } + + @if ( + CurrentProduct.accountType || + CurrentProduct.customerSegment || + CurrentProduct.glAccountId || + CurrentProduct.effectiveTo || + CurrentProduct.introducerRequired !== undefined || + CurrentProduct.shareLinkingEnabled !== undefined + ) { +

Product Classification

+ + + + @if (CurrentProduct.accountType) { +
+ Account Type: + {{ CurrentProduct.accountType }} +
+ } + + @if (CurrentProduct.customerSegment) { +
+ Customer Segment: + {{ CurrentProduct.customerSegment }} +
+ } + + @if (CurrentProduct.glAccountId) { +
+ GL Account (Liability): + {{ getGlAccountName(CurrentProduct.glAccountId) }} +
+ } + + @if (CurrentProduct.effectiveTo) { +
+ Effective To: + {{ CurrentProduct.effectiveTo | dateFormat }} +
+ } + + @if (CurrentProduct.endDate) { +
+ End Date: + {{ CurrentProduct.endDate | dateFormat }} +
+ } + + @if (CurrentProduct.introducerRequired !== undefined) { +
+ Introducer Required: + {{ + CurrentProduct.introducerRequired === true || CurrentProduct.introducerRequired === 'Yes' ? 'Yes' : 'No' + }} +
+ } + + @if (CurrentProduct.shareLinkingEnabled !== undefined) { +
+ Share Linking Enabled: + {{ CurrentProduct.shareLinkingEnabled | yesNo }} +
+ } + } + +

{{ 'labels.inputs.Currency' | translate }}

+ + +
+ {{ 'labels.inputs.Currency' | translate }}: + {{ + CurrentProduct.currency?.name || + CurrentProduct.currency?.displayLabel || + (CurrentProduct.currencyCode | find: CurrentProductsTemplate?.currencyOptions : 'code' : 'name') || + CurrentProduct.currencyCode || + '-' + }} +
+ +
+ {{ 'labels.inputs.Decimal Places' | translate }}: + {{ CurrentProduct.digitsAfterDecimal ?? CurrentProduct.currency?.decimalPlaces ?? 2 }} +
+ + @if (CurrentProduct.currency?.inMultiplesOf !== undefined && CurrentProduct.currency?.inMultiplesOf !== null) { +
+ {{ 'labels.inputs.Currency in multiples of' | translate }}: + {{ CurrentProduct.currency.inMultiplesOf }} +
+ } + + @if (CurrentProduct.dormancyAlert !== undefined) { +
+ Dormancy Alert: + {{ CurrentProduct.dormancyAlert | yesNo }} +
+ } + + @if (CurrentProduct.balanceSwapOption !== undefined) { +
+ Balance Swap Option: + {{ CurrentProduct.balanceSwapOption | yesNo }} +
+ } + + @if (CurrentProduct.msme !== undefined) { +
+ MSME: + {{ CurrentProduct.msme | yesNo }} +
+ } + + @if (CurrentProduct.nominationFacility !== undefined) { +
+ Nomination Facility: + {{ CurrentProduct.nominationFacility | yesNo }} +
+ } + + @if (CurrentProduct.autoDebitFacility !== undefined) { +
+ Auto Debit Facility: + {{ CurrentProduct.autoDebitFacility | yesNo }} +
+ } + +

Interest On OD

+ + +
+ Interest Posting Frequency: + {{ + CurrentProduct.interestPostingPeriodType + | find: CurrentProductsTemplate?.interestPostingPeriodTypeOptions : 'id' : 'value' + | translateKey: 'catalogs' + }} +
+ +
+ Auto Interest Post: + {{ CurrentProduct.autoPost | yesNo }} +
+ + @if ( + CurrentProduct.frequencyFixDay !== undefined && + CurrentProduct.frequencyFixDay !== null && + CurrentProduct.frequencyFixDay !== '' + ) { +
+ Frequency (Fix Day): + {{ CurrentProduct.frequencyFixDay }} Days +
+ } + + @if (CurrentProduct.lastInterestDate) { +
+ Last Interest Date: + {{ CurrentProduct.lastInterestDate | dateFormat }} +
+ } + +

{{ 'labels.inputs.Settings' | translate }}

+ + + @if (CurrentProduct.minRequiredOpeningBalance) { +
+ {{ 'labels.inputs.Minimum Opening Balance' | translate }}: + {{ CurrentProduct.minRequiredOpeningBalance | formatNumber }} +
+ } + + @if (CurrentProduct.lockinPeriodFrequency) { +
+ {{ 'labels.inputs.Lock-in Period' | translate }}: + {{ + CurrentProduct.lockinPeriodFrequency + + ' ' + + (CurrentProduct.lockinPeriodFrequencyType + | find: CurrentProductsTemplate?.lockinPeriodFrequencyTypeOptions : 'id' : 'value') + | translateKey: 'catalogs' + }} +
+ } + +
+ {{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}: + {{ CurrentProduct.withdrawalFeeForTransfers | yesNo }} +
+ + @if (CurrentProduct.balanceFrequency) { +
+ Balance Frequency: + {{ CurrentProduct.balanceFrequency }} +
+ } + +
+ Enable Lock-in Period: + {{ CurrentProduct.enableLockinPeriod | yesNo }} +
+ + @if (CurrentProduct.enableLockinPeriod && CurrentProduct.lockinPeriodFrequency) { +
+ Lock-in Period: + + {{ CurrentProduct.lockinPeriodFrequency }} + {{ getLockinPeriodFrequencyTypeLabel(CurrentProduct.lockinPeriodFrequencyType) }} + +
+ } + + @if (CurrentProduct.minBalanceForInterestCalculation) { +
+ {{ 'labels.inputs.Balance Required for Interest Calculation' | translate }}: + {{ CurrentProduct.minBalanceForInterestCalculation | formatNumber }} +
+ } + +
+ {{ 'labels.inputs.Enforce Minimum Balance' | translate }}: + {{ CurrentProduct.enforceMinRequiredBalance | yesNo }} +
+ + @if (CurrentProduct.minRequiredBalance) { +
+ {{ 'labels.inputs.Minimum Balance' | translate }}: + {{ CurrentProduct.minRequiredBalance | formatNumber }} +
+ } + +
+ {{ 'labels.inputs.Withhold Tax is Applicable' | translate }}: + {{ CurrentProduct.withHoldTax | yesNo }} +
+ + @if (CurrentProduct.withHoldTax) { +
+ {{ 'labels.inputs.Tax Group' | translate }}: + {{ + CurrentProduct.taxGroupId | find: CurrentProductsTemplate?.taxGroupOptions : 'id' : 'name' + }} +
+ } + +
+ Account Closed with Zero Balance in Day Closing: + {{ CurrentProduct.accountClosedZeroBalanceInDayClosing | yesNo }} +
+ +

Overdraft (OD) Facility

+ + +
+ Is Overdraft Allowed: + {{ CurrentProduct.allowOverdraft | yesNo }} +
+ + @if (CurrentProduct.allowOverdraft) { + @if (CurrentProduct.avgBalanceCalcPeriod) { +
+ Avg Balance Calculation Period: + {{ CurrentProduct.avgBalanceCalcPeriod }} +
+ } + + @if (CurrentProduct.validForPeriod) { +
+ Valid for Period: + {{ CurrentProduct.validForPeriod }} +
+ } + + @if (CurrentProduct.overdraftLimit !== undefined && CurrentProduct.overdraftLimit !== '') { +
+ OD Limit %: + {{ CurrentProduct.overdraftLimit }} +
+ } + + @if ( + CurrentProduct.nominalAnnualInterestRateOverdraft !== undefined && + CurrentProduct.nominalAnnualInterestRateOverdraft !== '' + ) { +
+ Interest on OD %: + {{ CurrentProduct.nominalAnnualInterestRateOverdraft }} +
+ } + +
+ OD Account Closed with Zero Balance in Day Closing: + {{ CurrentProduct.odAccountClosedZeroBalanceInDayClosing | yesNo }} +
+ } + +
+ {{ 'labels.inputs.Enable Dormancy Tracking' | translate }}: + {{ CurrentProduct.isDormancyTrackingActive | yesNo }} +
+ + @if (CurrentProduct.isDormancyTrackingActive) { +
+ {{ 'labels.inputs.Number of Days to Inactive sub-status' | translate }}: + {{ CurrentProduct.daysToInactive }} + {{ 'labels.inputs.Number of Days to Dormant sub-status' | translate }}: + {{ CurrentProduct.daysToDormancy }} + {{ 'labels.inputs.Number of Days to Escheat' | translate }}: + {{ CurrentProduct.daysToEscheat }} +
+ } + +

Account Facilities

+ + + @if (CurrentProduct.accountFacilityConfig) { +
+ @for (feature of accountFacilityFeatureItems; track feature.key) { +
+ {{ feature.label }}: + {{ CurrentProduct.accountFacilityConfig[feature.key] | yesNo }} +
+ } +
+ } + + @if (CurrentProduct.accountFacilityLimits?.length) { +
+

Account Facility Limits

+ + + + + + + + + + + + + + + + + + + +
Limit Type{{ row.limitType }}General Limit{{ row.generalLimit }}Minor Account Limit{{ row.minorAccountLimit }}Small Saving Limit{{ row.smallSavingLimit }}
+
+ } + + @if (CurrentProduct.depositLimitKyc) { +
+

Deposit Limit (KYC)

+
+ General: + {{ CurrentProduct.depositLimitKyc.general.kycType }} - + {{ CurrentProduct.depositLimitKyc.general.limit }} +
+
+ Minor: + {{ CurrentProduct.depositLimitKyc.minor.kycType }} - {{ CurrentProduct.depositLimitKyc.minor.limit }} +
+
+ Small Saving: + + {{ CurrentProduct.depositLimitKyc.smallSaving.kycType }} - + {{ CurrentProduct.depositLimitKyc.smallSaving.limit }} + +
+
+ } + + @if (CurrentProduct.charges.length) { +
+

{{ 'labels.inputs.Charges' | translate }}

+ + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.Name' | translate }} + {{ charge.name + ', ' + charge.currency.displaySymbol }} + {{ 'labels.inputs.Type' | translate }} + {{ charge.chargeCalculationType.value | translateKey: 'catalogs' }} + {{ 'labels.inputs.Amount' | translate }} + {{ charge.amount | formatNumber }} + {{ 'labels.inputs.Collected On' | translate }} + {{ charge.chargeTimeType.value | translateKey: 'catalogs' }} +
+
+ } + +

{{ 'labels.heading.Accounting' | translate }}

+ + + @if (isCashOrAccrualAccounting()) { +
+
+

GL Mapping

+
+ +
+
Assets
+
+ Savings reference account + {{ + formatGlAccountById( + CurrentProduct.savingsReferenceAccountId, + CurrentProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }} +
+
+ Overdraft portfolio control + {{ + formatGlAccountById( + CurrentProduct.overdraftPortfolioControlId, + CurrentProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }} +
+
+ Interest receivable account + {{ + formatGlAccountById( + CurrentProduct.interestReceivableAccountId, + CurrentProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }} +
+
+ Fees receivable account + {{ + formatGlAccountById( + CurrentProduct.feesReceivableAccountId, + CurrentProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }} +
+
+ Penalties receivable account + {{ + formatGlAccountById( + CurrentProduct.penaltiesReceivableAccountId, + CurrentProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }} +
+
+ +
+
Liabilities
+
+ Savings control account + {{ + formatGlAccountById( + CurrentProduct.savingsControlAccountId, + CurrentProductsTemplate?.accountingMappingOptions.liabilityAccountOptions + ) + }} +
+
+ Transfers in suspense account + {{ + formatGlAccountById( + CurrentProduct.transfersInSuspenseAccountId, + CurrentProductsTemplate?.accountingMappingOptions.liabilityAccountOptions + ) + }} +
+
+ Interest payable account + {{ + formatGlAccountById( + CurrentProduct.interestPayableAccountId, + CurrentProductsTemplate?.accountingMappingOptions.liabilityAccountOptions + ) + }} +
+ @if (CurrentProduct.isDormancyTrackingActive) { +
+ Escheat liability account + {{ + formatGlAccountById( + CurrentProduct.escheatLiabilityId, + CurrentProductsTemplate?.accountingMappingOptions.liabilityAccountOptions + ) + }} +
+ } +
+ +
+
Expenses
+
+ Interest on savings account + {{ + formatGlAccountById( + CurrentProduct.interestOnSavingsAccountId, + CurrentProductsTemplate?.accountingMappingOptions.expenseAccountOptions + ) + }} +
+
+ Write off account + {{ + formatGlAccountById( + CurrentProduct.writeOffAccountId, + CurrentProductsTemplate?.accountingMappingOptions.expenseAccountOptions + ) + }} +
+
+ +
+
Income
+
+ Income from fees + {{ + formatGlAccountById( + CurrentProduct.incomeFromFeeAccountId, + CurrentProductsTemplate?.accountingMappingOptions.incomeAccountOptions + ) + }} +
+
+ Income from penalties + {{ + formatGlAccountById( + CurrentProduct.incomeFromPenaltyAccountId, + CurrentProductsTemplate?.accountingMappingOptions.incomeAccountOptions + ) + }} +
+
+ Income from interest + {{ + formatGlAccountById( + CurrentProduct.incomeFromInterestId, + CurrentProductsTemplate?.accountingMappingOptions.incomeAccountOptions + ) + }} +
+
+
+ } + @if (CurrentProduct.otherGlMappings) { +
+
+

Other GL Mappings

+
+ + @if (CurrentProduct.otherGlMappings.assets?.length) { + + + + + + + + + + + +
Asset Account{{ row.accountName }}GL Account + {{ + formatGlAccountByCode(row.glCode, CurrentProductsTemplate?.accountingMappingOptions.assetAccountOptions) + }} +
+ } + + @if (CurrentProduct.otherGlMappings.liabilities?.length) { + + + + + + + + + + + +
Liability Account{{ row.accountName }}GL Account + {{ + formatGlAccountByCode( + row.glCode, + CurrentProductsTemplate?.accountingMappingOptions.liabilityAccountOptions + ) + }} +
+ } + + @if (CurrentProduct.otherGlMappings.expenses?.length) { + + + + + + + + + + + +
Expense Account{{ row.accountName }}GL Account + {{ + formatGlAccountByCode( + row.glCode, + CurrentProductsTemplate?.accountingMappingOptions.expenseAccountOptions + ) + }} +
+ } + + @if (CurrentProduct.otherGlMappings.income?.length) { + + + + + + + + + + + +
Income Account{{ row.accountName }}GL Account + {{ + formatGlAccountByCode( + row.glCode, + CurrentProductsTemplate?.accountingMappingOptions.incomeAccountOptions + ) + }} +
+ } +
+ } + + @if ( + CurrentProduct.advancedAccountingRules && + (CurrentProduct.paymentChannelToFundSourceMappings?.length || + CurrentProduct.feeToIncomeAccountMappings?.length || + CurrentProduct.penaltyToIncomeAccountMappings?.length) + ) { +
+

{{ 'labels.heading.Advanced Accounting Rules' | translate }}

+ + @if (CurrentProduct.paymentChannelToFundSourceMappings?.length) { +
+

{{ 'labels.heading.Fund Sources for Payment Channels' | translate }}

+ + + + + + + + + + + +
{{ 'labels.inputs.Payment Type' | translate }} + {{ paymentFundSource.paymentTypeId | find: CurrentProductsTemplate.paymentTypeOptions : 'id' : 'name' }} + {{ 'labels.inputs.Fund Source' | translate }} + {{ + paymentFundSource.fundSourceAccountId + | find: CurrentProductsTemplate?.accountingMappingOptions.assetAccountOptions : 'id' : 'name' + }} +
+
+ } + @if (CurrentProduct.feeToIncomeAccountMappings?.length) { +
+

{{ 'labels.heading.Fees to Specific Income Accounts' | translate }}

+ + + + + + + + + + + +
{{ 'labels.inputs.Fees' | translate }} + {{ feesIncome.chargeId | find: CurrentProductsTemplate.chargeOptions : 'id' : 'name' }} + {{ 'labels.inputs.Income Account' | translate }} + {{ + feesIncome.incomeAccountId + | find: CurrentProductsTemplate?.accountingMappingOptions.incomeAccountOptions : 'id' : 'name' + }} +
+
+ } + @if (CurrentProduct.penaltyToIncomeAccountMappings?.length) { +
+

{{ 'labels.heading.Penalties to Specific Income Accounts' | translate }}

+ + + + + + + + + + + +
{{ 'labels.inputs.Penalty' | translate }} + {{ penaltyIncome.chargeId | find: CurrentProductsTemplate.penaltyOptions : 'id' : 'name' }} + {{ 'labels.inputs.Income Account' | translate }} + {{ + penaltyIncome.incomeAccountId + | find: CurrentProductsTemplate?.accountingMappingOptions.incomeAccountOptions : 'id' : 'name' + }} +
+
+ } +
+ } +
diff --git a/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.scss b/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.scss new file mode 100644 index 000000000..f69c8fea0 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.scss @@ -0,0 +1,165 @@ +/** + * 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/. + */ + +@use 'assets/styles/helper'; +@use 'assets/styles/colours' as *; + +.tab-container { + padding: 1%; + margin: 1%; +} + +.mat-h3 { + margin-top: 10px !important; + font-weight: 300; + color: $status-approved; +} + +.mat-elevation-z1 { + margin: 1em 0 1.5em; +} + +.interestCarts { + float: right; +} + +h2, +h3, +h4 { + margin: 0; + font-weight: 500; +} + +table { + width: 100%; + + tr.incentives-row { + height: 0; + + > td { + padding: 0; + } + + .incentives { + overflow: hidden; + } + + .mat-card { + border-radius: 0; + + > h4 { + font-weight: 500; + margin: 0.5em 0 0; + } + } + } +} + +span { + margin: 0.5em 0; +} + +mat-divider { + margin: 0 0 0.5em; +} + +.margin-t { + margin-top: 1em; +} + +.spa-gl-summary, +.spa-other-gl-summary { + display: flex; + flex-direction: column; + gap: 0.9rem; + margin-top: 1.25rem; +} + +.spa-gl-summary__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.25rem 0 0.5rem; +} + +.spa-gl-section { + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 1rem 1.15rem; + border: 1px solid #e3e7f2; + border-radius: 12px; + background: #fff; +} + +.spa-gl-section h5 { + font-size: 0.9rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #91a0bc; +} + +.spa-gl-row { + display: grid; + grid-template-columns: minmax(220px, 40%) 1fr; + gap: 1rem; + align-items: baseline; + padding: 0.2rem 0; +} + +.spa-gl-row__label { + color: #1f2f4a; + font-weight: 500; +} + +.spa-gl-row__value { + color: #101828; + font-weight: 600; + word-break: break-word; +} + +.spa-other-gl-table { + margin-top: 0.5rem; + margin-bottom: 0.75rem; + table-layout: fixed; + width: 100%; +} + +.spa-other-gl-table:first-of-type { + margin-top: 0.25rem; +} + +.spa-other-gl-summary .mat-elevation-z1 { + border-radius: 10px; + overflow: hidden; +} + +.spa-other-gl-table th.mat-mdc-header-cell, +.spa-other-gl-table td.mat-mdc-cell { + padding-left: 1.25rem; + padding-right: 1.25rem; +} + +.spa-other-gl-table th.mat-mdc-header-cell:first-child, +.spa-other-gl-table td.mat-mdc-cell:first-child { + width: 38%; +} + +.spa-other-gl-table th.mat-mdc-header-cell:last-child, +.spa-other-gl-table td.mat-mdc-cell:last-child { + width: 62%; + word-break: break-word; +} + +@media (width <= 768px) { + .spa-gl-row { + grid-template-columns: 1fr; + gap: 0.35rem; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.ts b/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.ts new file mode 100644 index 000000000..8800b5b92 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-preview-step/current-product-preview-step.component.ts @@ -0,0 +1,256 @@ +/** + * 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, Input, Output, EventEmitter, OnChanges, SimpleChanges, inject } from '@angular/core'; +import { Accounting } from 'app/core/utils/accounting'; +import { OptionData } from 'app/shared/models/option-data.model'; +import { MatDivider } from '@angular/material/divider'; +import { + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow +} from '@angular/material/table'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { FindPipe } from '../../../../pipes/find.pipe'; +import { DateFormatPipe } from '../../../../pipes/date-format.pipe'; +import { FormatNumberPipe } from '../../../../pipes/format-number.pipe'; +import { YesnoPipe } from '../../../../pipes/yesno.pipe'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-current-product-preview-step', + templateUrl: './current-product-preview-step.component.html', + styleUrls: ['./current-product-preview-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatDivider, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, + FaIconComponent, + FindPipe, + DateFormatPipe, + FormatNumberPipe, + YesnoPipe + ] +}) +export class CurrentProductPreviewStepComponent implements OnInit, OnChanges { + private accounting = inject(Accounting); + + @Input() CurrentProductsTemplate: any; + @Input() accountingRuleData: any; + @Input() CurrentProduct: any; + @Input() taskPermission: string; + @Output() submitEvent = new EventEmitter(); + + chargesDisplayedColumns: string[] = [ + 'name', + 'chargeCalculationType', + 'amount', + 'chargeTimeType' + ]; + paymentFundSourceDisplayedColumns: string[] = [ + 'paymentTypeId', + 'fundSourceAccountId' + ]; + feesPenaltyIncomeDisplayedColumns: string[] = [ + 'chargeId', + 'incomeAccountId' + ]; + + accountingMappings: any = {}; + accountingRule: OptionData; + interestCalculationTypeOptionData: any[] = []; + accountFacilityFeatureItems = [ + { key: 'atmFacility', label: 'ATM Facility' }, + { key: 'atmWithdrawalFacility', label: 'ATM Withdrawal Facility' }, + { key: 'upiFacility', label: 'UPI Facility' }, + { key: 'posTransactionsFacility', label: 'POS Transactions Facility' }, + { key: 'chequeBook', label: 'Cheque Book' }, + { key: 'impsFacility', label: 'IMPS Facility' }, + { key: 'smsAlerts', label: 'SMS Alerts' }, + { key: 'bbpsFacility', label: 'BBPS Facility' }, + { key: 'insuranceCover', label: 'Insurance Cover' }, + { key: 'bepgFacility', label: 'BEPG Facility' }, + { key: 'neftFacility', label: 'NEFT Facility' }, + { key: 'rtgsFacility', label: 'RTGS Facility' }, + { key: 'interBankTotalCapFacility', label: 'Inter-Bank Total Cap Facility' }, + { key: 'withdrawalFacility', label: 'Withdrawal Facility' }, + { key: 'depositFacilityKyc', label: 'Deposit Facility (KYC)' }, + { key: 'virtualDebitCard', label: 'Virtual Debit Card' } + ]; + + ngOnInit() { + this.setCurrentValues(); + } + + ngOnChanges(changes: SimpleChanges): void { + this.setCurrentValues(); + } + + setCurrentValues(): void { + if (!this.CurrentProduct || !this.CurrentProductsTemplate) { + return; + } + + this.interestCalculationTypeOptionData = + this.CurrentProductsTemplate.interestCalculationTypeOption || + this.CurrentProductsTemplate.interestCalculationTypeOptions || + []; + this.accountingRule = this.accounting.getAccountingRuleFrom(this.CurrentProduct.accountingRule); + + if (!this.isCashOrAccrualAccounting()) { + return; + } + + const assetAccountData = this.CurrentProductsTemplate?.accountingMappingOptions?.assetAccountOptions || []; + const incomeAccountData = this.CurrentProductsTemplate?.accountingMappingOptions?.incomeAccountOptions || []; + const expenseAccountData = this.CurrentProductsTemplate?.accountingMappingOptions?.expenseAccountOptions || []; + const liabilityAccountData = this.CurrentProductsTemplate?.accountingMappingOptions?.liabilityAccountOptions || []; + + this.accountingMappings = { + savingsReferenceAccount: this.accounting.glAccountLookUp( + this.CurrentProduct.savingsReferenceAccountId, + assetAccountData + ), + overdraftPortfolioControl: this.accounting.glAccountLookUp( + this.CurrentProduct.overdraftPortfolioControlId, + assetAccountData + ), + savingsControlAccount: this.accounting.glAccountLookUp( + this.CurrentProduct.savingsControlAccountId, + liabilityAccountData + ), + transfersInSuspenseAccount: this.accounting.glAccountLookUp( + this.CurrentProduct.transfersInSuspenseAccountId, + liabilityAccountData + ), + interestOnSavingsAccount: this.accounting.glAccountLookUp( + this.CurrentProduct.interestOnSavingsAccountId, + expenseAccountData + ), + writeOffAccount: this.accounting.glAccountLookUp(this.CurrentProduct.writeOffAccountId, expenseAccountData), + incomeFromFeeAccount: this.accounting.glAccountLookUp( + this.CurrentProduct.incomeFromFeeAccountId, + incomeAccountData + ), + incomeFromPenaltyAccount: this.accounting.glAccountLookUp( + this.CurrentProduct.incomeFromPenaltyAccountId, + incomeAccountData + ), + incomeFromInterest: this.accounting.glAccountLookUp(this.CurrentProduct.incomeFromInterestId, incomeAccountData) + }; + + if (this.isAccrualAccounting()) { + this.accountingMappings = { + ...this.accountingMappings, + feeReceivableAccount: this.accounting.glAccountLookUp( + this.CurrentProduct.feesReceivableAccountId, + assetAccountData + ), + penaltyReceivableAccount: this.accounting.glAccountLookUp( + this.CurrentProduct.penaltiesReceivableAccountId, + assetAccountData + ), + interestPayableAccount: this.accounting.glAccountLookUp( + this.CurrentProduct.interestPayableAccountId, + liabilityAccountData + ), + interestReceivableAccount: this.accounting.glAccountLookUp( + this.CurrentProduct.interestReceivableAccountId, + assetAccountData + ) + }; + } + + if (this.CurrentProduct.isDormancyTrackingActive) { + this.accountingMappings = { + ...this.accountingMappings, + escheatLiabilityAccount: this.accounting.glAccountLookUp( + this.CurrentProduct.escheatLiabilityId, + liabilityAccountData + ) + }; + } + } + + isCashOrAccrualAccounting(): boolean { + return this.accounting.isCashOrAccrualAccounting(this.accountingRule); + } + + isAccrualAccounting(): boolean { + return this.accounting.isAccrualAccounting(this.accountingRule); + } + + getBooleanLabel(value: any): string { + return value === true || value === 'Yes' ? 'Yes' : 'No'; + } + + getGlAccountName(id: any): string { + if (!id) return ''; + const accounts = this.CurrentProductsTemplate?.accountingMappingOptions?.liabilityAccountOptions ?? []; + const found = accounts.find((a: any) => a.id === id || a.id === +id); + return found ? `${found.name} (${found.glCode})` : String(id); + } + + getLockinPeriodFrequencyTypeLabel(value: any): string { + switch (value) { + case 'DAYS': + return 'Days'; + case 'WEEKS': + return 'Weeks'; + case 'MONTHS': + return 'Months'; + case 'YEARS': + return 'Years'; + default: + return value ?? ''; + } + } + + getOptionLabel(value: any, options: any[], key: string, property: string): string { + if (value === null || value === undefined || !Array.isArray(options)) { + return '-'; + } + + const match = options.find((option) => option?.[key] === value); + return match?.[property] || '-'; + } + + formatGlAccountById(glAccountId: number, options: any[]): string { + if (!glAccountId || !Array.isArray(options)) { + return '-'; + } + + const account = this.accounting.glAccountLookUp(glAccountId, options as any); + return account ? `(${account.glCode}) ${account.name}` : '-'; + } + + formatGlAccountByCode(glCode: string, options: any[]): string { + if (!glCode || !Array.isArray(options)) { + return '-'; + } + + const account = options.find((option) => option?.glCode === glCode); + return account ? `(${account.glCode}) ${account.name}` : glCode; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-settings-step/current-product-settings-step.component.html b/src/app/products/current-products/current-product-stepper/current-product-settings-step/current-product-settings-step.component.html new file mode 100644 index 000000000..11467b21d --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-settings-step/current-product-settings-step.component.html @@ -0,0 +1,184 @@ + + +
+ +
+
+ + Balance & Minimum Balance +
+
+
+
+ + Minimum Opening Balance + + + +
+ + Enforce Minimum Balance + +
+ + + Minimum Balance (₹) * + + PENAL CHARGE APPLIES ON BREACH + +
+ +
+ + Balance Frequency + + @for (balanceFrequency of periodFrequencyData; track balanceFrequency.id) { + {{ balanceFrequency.value }} + } + + + +
+ + Apply Withdrawal Fee for Transfers + +
+
+
+
+
+ + +
+
+ + Lock-in Period +
+
+
+ Enable Lock-in Period + +
+ + @if (CurrentProductSettingsForm.value.enableLockinPeriod) { +
+ + Frequency * + + + + + Type * + + @for (type of lockinPeriodFrequencyTypeData; track type) { + + {{ type.value | translateKey: 'catalogs' }} + + } + + +
+ } +
+
+ + +
+
+ + Additional Rules +
+
+
+ + Account Closed with Zero Balance in Day Closing + +
+
+
+ + +
+
+ + Overdraft (OD) Facility +
+
+
+ +
+
+ + Is Overdraft Allowed + +
+ @if (CurrentProductSettingsForm.value.allowOverdraft) { +
+ + OD Account Closed with Zero Balance in Day Closing + +

+ Automatically closes OD accounts when balance reaches zero during day-end closing. +

+
+ } +
+ + + @if (CurrentProductSettingsForm.value.allowOverdraft) { +
+
+ Average Balance Calculation in Period + + 6M + 1Y + 3Y + +
+ +
+ Valid for Period + + 3M + 1Y + 3Y + +
+ +
+ + OD Limit % + + + + + Interest on OD % + + +
+
+ } +
+
+
+
diff --git a/src/app/products/current-products/current-product-stepper/current-product-settings-step/current-product-settings-step.component.scss b/src/app/products/current-products/current-product-stepper/current-product-settings-step/current-product-settings-step.component.scss new file mode 100644 index 000000000..0e3cd524f --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-settings-step/current-product-settings-step.component.scss @@ -0,0 +1,263 @@ +/** + * 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/. + */ + +$accent: #0ea5e9; +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-900: #202124; + +.sps-section { + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; + background: #fff; + + & + & { + margin-top: 16px; + } + + &__header { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 24px; + background: $gray-50; + border-bottom: 1px solid $gray-200; + + i { + font-size: 14px; + color: $accent; + } + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + } +} + +.sps-section__badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: #eff6ff; + font-size: 14px; +} + +.sps-body { + background: #fff; + padding: 18px 24px 24px; +} + +.sps-field { + width: 100%; + + &--full { + grid-column: 1 / -1; + } + + &--compact { + width: 100%; + } +} + +.sps-layout { + display: grid; + gap: 16px; +} + +.sps-layout--balance { + gap: 22px; +} + +.sps-layout--lockin { + grid-template-columns: repeat(2, minmax(0, 1fr)); + + @media (width <= 640px) { + grid-template-columns: 1fr; + } +} + +.sps-layout--additional { + grid-template-columns: repeat(2, minmax(0, 1fr)); + + @media (width <= 1100px) { + grid-template-columns: 1fr; + } +} + +.sps-row { + display: grid; + gap: 18px; + align-items: start; +} + +.sps-row--three { + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + align-items: center; + + .sps-field--compact:first-child { + min-width: 0; + } + + .sps-toggle--inline { + align-self: center; + min-height: 56px; + } +} + +.sps-row--two { + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + + .sps-field--compact { + min-width: 0; + } +} + +.sps-toggle { + display: flex; + align-items: center; + min-height: 56px; + + mat-checkbox { + font-size: 14px; + color: $gray-900; + } + + &--full { + min-height: auto; + margin-bottom: 8px; + } + + &--inline { + padding-top: 0; + } + + &--right { + justify-content: flex-start; + } +} + +.sps-lockin-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 16px; + padding-bottom: 16px; + border-bottom: 1px solid $gray-200; + font-size: 14px; + color: $gray-900; +} + +.sps-lockin-row mat-checkbox { + margin-right: 2px; +} + +.sps-lockin-fields { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + + @media (width <= 640px) { + grid-template-columns: 1fr; + } +} + +// ── Overdraft (OD) Facility ─────────────────────────────────────────────── + +.sps-od-layout { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 24px; + align-items: start; + + @media (width <= 900px) { + grid-template-columns: 1fr; + } +} + +.sps-od-checks { + display: flex; + flex-direction: column; + gap: 12px; +} + +.sps-od-check-row { + padding: 14px 16px; + border: 1px solid $border; + border-radius: 8px; + background: $gray-50; + + mat-checkbox { + font-size: 14px; + font-weight: 500; + color: $gray-900; + } +} + +.sps-od-check-hint { + margin: 6px 0 0 28px; + font-size: 11.5px; + color: #64748b; + line-height: 1.5; +} + +.sps-od-panel { + background: #f0f7ff; + border: 1px solid #bfdbfe; + border-radius: 10px; + padding: 20px 24px; + display: flex; + flex-direction: column; + gap: 20px; +} + +.sps-od-radio-group { + display: flex; + flex-direction: column; + gap: 8px; + padding-bottom: 16px; + border-bottom: 1px solid #dbeafe; + + &:last-of-type { + border-bottom: none; + padding-bottom: 0; + } +} + +.sps-od-radio-label { + font-size: 10.5px; + font-weight: 700; + letter-spacing: 0.07em; + color: #3b82f6; + text-transform: uppercase; +} + +.sps-od-radio-choices { + display: flex; + gap: 20px; + flex-wrap: wrap; +} + +.sps-od-inputs { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + + @media (width <= 640px) { + grid-template-columns: 1fr; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-settings-step/current-product-settings-step.component.ts b/src/app/products/current-products/current-product-stepper/current-product-settings-step/current-product-settings-step.component.ts new file mode 100644 index 000000000..11bb38781 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-settings-step/current-product-settings-step.component.ts @@ -0,0 +1,172 @@ +/** + * 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, Input, inject } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; +import { MatCheckbox } from '@angular/material/checkbox'; +import { MatRadioGroup, MatRadioButton } from '@angular/material/radio'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-current-product-settings-step', + templateUrl: './current-product-settings-step.component.html', + styleUrls: ['./current-product-settings-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatCheckbox, + MatRadioGroup, + MatRadioButton + ] +}) +export class CurrentProductSettingsStepComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + + @Input() CurrentProductsTemplate: any; + + CurrentProductSettingsForm: UntypedFormGroup; + + periodFrequencyData: any[] = []; + + lockinPeriodFrequencyTypeData = [ + { id: 'DAYS', value: 'value.days' }, + { id: 'WEEKS', value: 'value.weeks' }, + { id: 'MONTHS', value: 'value.months' }, + { id: 'YEARS', value: 'value.years' } + ]; + + constructor() { + this.createCurrentProductSettingsForm(); + this.setConditionalControls(); + } + + ngOnInit() { + this.periodFrequencyData = this.CurrentProductsTemplate.periodFrequencyOptions || []; + const defaultPeriod = this.periodFrequencyData[0]?.value || 'Monthly'; + const isEditing = !!this.CurrentProductsTemplate.id; + + this.CurrentProductSettingsForm.patchValue({ + minRequiredOpeningBalance: this.CurrentProductsTemplate.minRequiredOpeningBalance, + withdrawalFeeForTransfers: this.CurrentProductsTemplate.withdrawalFeeForTransfers, + enforceMinRequiredBalance: this.CurrentProductsTemplate.enforceMinRequiredBalance, + minRequiredBalance: this.CurrentProductsTemplate.minRequiredBalance, + balanceFrequency: isEditing ? this.CurrentProductsTemplate.balanceFrequency || defaultPeriod : defaultPeriod, + enableLockinPeriod: !!( + this.CurrentProductsTemplate.lockinPeriodFrequency && this.CurrentProductsTemplate.lockinPeriodFrequency > 0 + ), + accountClosedZeroBalanceInDayClosing: isEditing + ? (this.CurrentProductsTemplate.accountClosedZeroBalanceInDayClosing ?? false) + : false, + isDormancyTrackingActive: isEditing ? (this.CurrentProductsTemplate.isDormancyTrackingActive ?? true) : true, + daysToInactive: isEditing ? this.CurrentProductsTemplate.daysToInactive || 30 : 30, + daysToDormancy: isEditing ? this.CurrentProductsTemplate.daysToDormancy || 60 : 60, + daysToEscheat: isEditing ? this.CurrentProductsTemplate.daysToEscheat || 90 : 90, + // Overdraft fields + allowOverdraft: isEditing ? (this.CurrentProductsTemplate.allowOverdraft ?? false) : false, + overdraftLimit: isEditing ? this.CurrentProductsTemplate.overdraftLimit : '', + nominalAnnualInterestRateOverdraft: isEditing + ? this.CurrentProductsTemplate.nominalAnnualInterestRateOverdraft + : '', + avgBalanceCalcPeriod: isEditing ? this.CurrentProductsTemplate.avgBalanceCalcPeriod || '1Y' : '1Y', + validForPeriod: isEditing ? this.CurrentProductsTemplate.validForPeriod || '1Y' : '1Y', + odAccountClosedZeroBalanceInDayClosing: isEditing + ? (this.CurrentProductsTemplate.odAccountClosedZeroBalanceInDayClosing ?? false) + : false + }); + + if ( + isEditing && + this.CurrentProductsTemplate.lockinPeriodFrequency && + this.CurrentProductsTemplate.lockinPeriodFrequency > 0 + ) { + const lockinTypeIdToKey: { [key: number]: string } = { 0: 'DAYS', 1: 'WEEKS', 2: 'MONTHS', 3: 'YEARS' }; + const rawLockinType = this.CurrentProductsTemplate.lockinPeriodFrequencyType; + // Backend may return an EnumOptionData {id, code, value} or a plain integer + const lockinTypeId = typeof rawLockinType === 'object' ? rawLockinType?.id : rawLockinType; + this.CurrentProductSettingsForm.patchValue({ + lockinPeriodFrequency: this.CurrentProductsTemplate.lockinPeriodFrequency, + lockinPeriodFrequencyType: lockinTypeIdToKey[lockinTypeId] || '' + }); + } + } + + createCurrentProductSettingsForm() { + this.CurrentProductSettingsForm = this.formBuilder.group({ + minRequiredOpeningBalance: [ + '', + [Validators.min(0)] + ], + withdrawalFeeForTransfers: [false], + enforceMinRequiredBalance: [false], + minRequiredBalance: [ + '', + [Validators.min(0)] + ], + balanceFrequency: ['Monthly'], + enableLockinPeriod: [false], + lockinPeriodFrequency: [''], + lockinPeriodFrequencyType: [''], + accountClosedZeroBalanceInDayClosing: [false], + // Dormancy (used internally / by accounting step) + isDormancyTrackingActive: [true], + daysToInactive: [30], + daysToDormancy: [60], + daysToEscheat: [90], + // Overdraft (OD) Facility + allowOverdraft: [false], + overdraftLimit: [ + '', + [Validators.min(0)] + ], + nominalAnnualInterestRateOverdraft: [ + '', + [Validators.min(0)] + ], + avgBalanceCalcPeriod: ['1Y'], + validForPeriod: ['1Y'], + odAccountClosedZeroBalanceInDayClosing: [false] + }); + } + + setConditionalControls() { + this.CurrentProductSettingsForm.get('enableLockinPeriod').valueChanges.subscribe((enabled: boolean) => { + const freq = this.CurrentProductSettingsForm.get('lockinPeriodFrequency'); + const type = this.CurrentProductSettingsForm.get('lockinPeriodFrequencyType'); + if (enabled) { + freq.setValidators([ + Validators.required, + Validators.min(1), + Validators.pattern('^[1-9]\\d*$') + ]); + type.setValidators([Validators.required]); + } else { + freq.setValue(''); + type.setValue(''); + freq.clearValidators(); + type.clearValidators(); + } + freq.updateValueAndValidity(); + type.updateValueAndValidity(); + }); + } + + get CurrentProductSettings() { + const formValue = { ...this.CurrentProductSettingsForm.value }; + + delete formValue.withHoldTax; + + if (!formValue.allowOverdraft) { + delete formValue.overdraftLimit; + delete formValue.nominalAnnualInterestRateOverdraft; + delete formValue.avgBalanceCalcPeriod; + delete formValue.validForPeriod; + delete formValue.odAccountClosedZeroBalanceInDayClosing; + } + + return formValue; + } +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-terms-step/current-product-terms-step.component.html b/src/app/products/current-products/current-product-stepper/current-product-terms-step/current-product-terms-step.component.html new file mode 100644 index 000000000..f2350d95a --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-terms-step/current-product-terms-step.component.html @@ -0,0 +1,68 @@ + + +
+
+
+ + Interest On OD +
+
+ + Interest Posting Frequency * + + @for (item of interestPostingPeriodTypeData; track item) { + + {{ item.value | translateKey: 'catalogs' }} + + } + + + Interest Posting Frequency {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + +
+
+
diff --git a/src/app/products/current-products/current-product-stepper/current-product-terms-step/current-product-terms-step.component.scss b/src/app/products/current-products/current-product-stepper/current-product-terms-step/current-product-terms-step.component.scss new file mode 100644 index 000000000..807262b37 --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-terms-step/current-product-terms-step.component.scss @@ -0,0 +1,148 @@ +/** + * 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/. + */ + +$accent: #0ea5e9; +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-900: #202124; + +.spt-section { + border: 1px solid $border; + border-radius: 10px; + overflow: hidden; + background: #fff; + + & + & { + margin-top: 16px; + } + + &__header { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 24px; + background: $gray-50; + border-bottom: 1px solid $gray-200; + + i { + font-size: 14px; + color: $accent; + } + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + } +} + +.spt-section__badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: #eff6ff; + font-size: 14px; +} + +.spt-body { + background: #fff; + padding: 16px 24px; +} + +.spt-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px 16px; + padding: 16px 24px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.spt-grid--three { + grid-template-columns: repeat(3, 1fr); +} + +.spt-field { + width: 100%; + + &--full { + grid-column: 1 / -1; + } +} + +.spt-banner { + display: flex; + align-items: center; + gap: 10px; + margin: 16px 24px 0; + padding: 12px 14px; + background: #eff6ff; + border: 1px solid #dbeafe; + border-radius: 8px; + color: #1d4ed8; + font-size: 13px; + + &__icon { + font-size: 14px; + } +} + +.spt-footer { + padding: 0 24px 24px; + border-top: 1px solid #f1f5f9; +} + +.spt-footer__grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + padding-top: 18px; + + @media (width <= 900px) { + grid-template-columns: 1fr; + } +} + +.spt-toggle-card { + padding: 12px 0; + + h4 { + margin: 0 0 8px; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + + &__choices { + display: flex; + align-items: center; + gap: 16px; + flex-wrap: wrap; + } +} + +.spt-note { + display: flex; + align-items: center; + gap: 10px; + margin-top: 18px; + padding: 12px 14px; + border: 1px solid #fde68a; + background: #fffbeb; + color: #b45309; + border-radius: 8px; + font-size: 12px; +} diff --git a/src/app/products/current-products/current-product-stepper/current-product-terms-step/current-product-terms-step.component.ts b/src/app/products/current-products/current-product-stepper/current-product-terms-step/current-product-terms-step.component.ts new file mode 100644 index 000000000..aaab2e0aa --- /dev/null +++ b/src/app/products/current-products/current-product-stepper/current-product-terms-step/current-product-terms-step.component.ts @@ -0,0 +1,112 @@ +/** + * 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, Input, inject } from '@angular/core'; +import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; +import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { Dates } from 'app/core/utils/dates'; +import { SettingsService } from 'app/settings/settings.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-current-product-terms-step', + templateUrl: './current-product-terms-step.component.html', + styleUrls: ['./current-product-terms-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FaIconComponent + ] +}) +export class CurrentProductTermsStepComponent implements OnInit { + private formBuilder = inject(UntypedFormBuilder); + private dateUtils = inject(Dates); + private settingsService = inject(SettingsService); + + @Input() CurrentProductsTemplate: any; + + CurrentProductTermsForm: UntypedFormGroup; + + interestPostingPeriodTypeData: any[] = []; + yesNoOptions: Array<{ value: boolean; label: string }> = [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' } + ]; + + constructor() { + this.createCurrentProductTermsForm(); + } + + ngOnInit() { + this.interestPostingPeriodTypeData = this.CurrentProductsTemplate.interestPostingPeriodTypeOptions || []; + this.setYesNoOptions(this.CurrentProductsTemplate?.yesNoOptions); + + const isEditing = !!this.CurrentProductsTemplate.id; + + this.CurrentProductTermsForm.patchValue({ + interestPostingPeriodType: this.CurrentProductsTemplate.interestPostingPeriodType?.id || '', + autoPost: isEditing ? (this.CurrentProductsTemplate.autoPost ?? true) : true, + frequencyFixDay: isEditing ? (this.CurrentProductsTemplate.frequencyFixDay ?? 30) : 30, + lastInterestDate: this.parseDate(this.CurrentProductsTemplate.lastInterestDate) + }); + } + + private parseDate(dateValue: any): Date | null { + if (!dateValue) return null; + if (Array.isArray(dateValue)) { + return new Date(dateValue[0], dateValue[1] - 1, dateValue[2]); + } + if (dateValue instanceof Date) { + return dateValue; + } + return new Date(dateValue); + } + + private setYesNoOptions(options: any[]) { + if (!Array.isArray(options)) return; + + const mappedOptions = options + .map((option: any) => { + const rawValue = option?.name ?? option?.value ?? option; + const normalized = typeof rawValue === 'string' ? rawValue.trim().toUpperCase() : rawValue; + if (normalized === 'YES') return { value: true, label: 'Yes' }; + if (normalized === 'NO') return { value: false, label: 'No' }; + return null; + }) + .filter((v: any) => !!v); + + if (mappedOptions.length) { + this.yesNoOptions = mappedOptions; + } + } + + createCurrentProductTermsForm() { + this.CurrentProductTermsForm = this.formBuilder.group({ + interestPostingPeriodType: [ + '', + Validators.required + ], + autoPost: [true], + frequencyFixDay: [ + 30, + [Validators.min(0)] + ], + lastInterestDate: [''] + }); + } + + get CurrentProductTerms() { + const formValue = { ...this.CurrentProductTermsForm.value }; + const dateFormat = this.settingsService.dateFormat; + + if (formValue.lastInterestDate instanceof Date) { + formValue.lastInterestDate = this.dateUtils.formatDate(formValue.lastInterestDate, dateFormat) || ''; + } + + return { ...formValue }; + } +} diff --git a/src/app/products/current-products/edit-current-product/edit-current-product.component.html b/src/app/products/current-products/edit-current-product/edit-current-product.component.html index a8ab8c762..699f0bfce 100644 --- a/src/app/products/current-products/edit-current-product/edit-current-product.component.html +++ b/src/app/products/current-products/edit-current-product/edit-current-product.component.html @@ -1,69 +1,154 @@ -
- - - - - - + - - {{ 'labels.inputs.CURRENCY' | translate }} - - +
+
+ +

{{ 'labels.heading.Edit Current Product' | translate }}

+
- - {{ 'labels.inputs.TERMS' | translate }} - - + + + + + {{ 'labels.inputs.DETAILS' | translate }} + +
+ +
+
- - {{ 'labels.inputs.SETTINGS' | translate }} - - + + + + {{ 'labels.inputs.CURRENCY' | translate }} + +
+ +
+
- - {{ 'labels.inputs.CHARGES' | translate }} - - + + + + {{ 'labels.inputs.TERMS' | translate }} + +
+ +
+
- - {{ 'labels.inputs.ACCOUNTING' | translate }} - - + + + + {{ 'labels.inputs.SETTINGS' | translate }} + +
+ +
+
+ + + + + {{ 'labels.inputs.CHARGES' | translate }} + +
+ +
+
+ + + + + Digital + +
+ +
+
+ + + + + {{ 'labels.inputs.ACCOUNTING' | translate }} + +
+ +
+
@if (formValidAndNotPristine) { - - {{ 'labels.inputs.PREVIEW' | translate }} - - + + + + {{ 'labels.inputs.PREVIEW' | translate }} + +
+ +
+
} - +
+ +
+ +
+ @if (selectedTabIndex > 0) { + + } + @if (selectedTabIndex < totalTabs - 1) { + + } + @if (formValidAndNotPristine && selectedTabIndex === totalTabs - 1) { + + } +
+
diff --git a/src/app/products/current-products/edit-current-product/edit-current-product.component.scss b/src/app/products/current-products/edit-current-product/edit-current-product.component.scss index fbc6592ec..98f8a7bee 100644 --- a/src/app/products/current-products/edit-current-product/edit-current-product.component.scss +++ b/src/app/products/current-products/edit-current-product/edit-current-product.component.scss @@ -1,4 +1,90 @@ -fa-icon { - position: relative; - left: 5%; +/** + * Copyright since 2025 Mifos Initiative + * + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ + +$primary: #1353a0; + +.cp-container { + max-width: 960px; + margin: 0 auto; + padding: 24px 16px; +} + +.cp-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 20px; + + &__icon { + font-size: 20px; + color: $primary; + } + + &__title { + margin: 0; + font-size: 22px; + font-weight: 500; + } +} + +.cp-tabs { + background: var(--md-sys-color-surface, #fff); + border-radius: 8px; + box-shadow: 0 2px 8px rgb(0 0 0 / 8%); + + ::ng-deep { + .mat-mdc-tab-header { + border-bottom: 1px solid rgb(0 0 0 / 8%); + } + + .mat-mdc-tab { + min-width: 120px; + padding: 0 16px; + height: 52px; + } + + .mat-mdc-tab-body-wrapper { + min-height: 200px; + } + + .mat-mdc-tab-body { + overflow: visible !important; + } + + .mat-mdc-tab-body-content { + overflow: visible !important; + } + } +} + +.cp-tab-icon { + margin-right: 8px; + font-size: 14px; +} + +.cp-tab-content { + padding: 24px 16px; +} + +.cp-action-bar { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 20px; + padding: 16px 0; + border-top: 1px solid rgb(0 0 0 / 8%); + + &__cancel { + color: rgb(0 0 0 / 60%); + } + + &__right { + display: flex; + gap: 12px; + } } diff --git a/src/app/products/current-products/edit-current-product/edit-current-product.component.ts b/src/app/products/current-products/edit-current-product/edit-current-product.component.ts index 0083121a2..5d6d2441b 100644 --- a/src/app/products/current-products/edit-current-product/edit-current-product.component.ts +++ b/src/app/products/current-products/edit-current-product/edit-current-product.component.ts @@ -1,17 +1,24 @@ +/** + * 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, ViewChild, inject } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; -import { SavingProductDetailsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component'; -import { SavingProductCurrencyStepComponent } from '../../saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component'; -import { SavingProductTermsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component'; -import { SavingProductSettingsStepComponent } from '../../saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component'; -import { SavingProductChargesStepComponent } from '../../saving-products/saving-product-stepper/saving-product-charges-step/saving-product-charges-step.component'; -import { SavingProductAccountingStepComponent } from '../../saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component'; -import { SavingProductPreviewStepComponent } from '../../saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component'; +import { CurrentProductDetailsStepComponent } from '../current-product-stepper/current-product-details-step/current-product-details-step.component'; +import { CurrentProductCurrencyStepComponent } from '../current-product-stepper/current-product-currency-step/current-product-currency-step.component'; +import { CurrentProductTermsStepComponent } from '../current-product-stepper/current-product-terms-step/current-product-terms-step.component'; +import { CurrentProductSettingsStepComponent } from '../current-product-stepper/current-product-settings-step/current-product-settings-step.component'; +import { CurrentProductChargesStepComponent } from '../current-product-stepper/current-product-charges-step/current-product-charges-step.component'; +import { CurrentProductLimitsStepComponent } from '../current-product-stepper/current-product-limits-step/current-product-limits-step.component'; +import { CurrentProductAccountingStepComponent } from '../current-product-stepper/current-product-accounting-step/current-product-accounting-step.component'; +import { CurrentProductPreviewStepComponent } from '../current-product-stepper/current-product-preview-step/current-product-preview-step.component'; import { ProductsService } from 'app/products/products.service'; import { SettingsService } from 'app/settings/settings.service'; import { Accounting } from 'app/core/utils/accounting'; -import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper'; -import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { MatTabsModule } from '@angular/material/tabs'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ @@ -20,18 +27,15 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; styleUrls: ['./edit-current-product.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, - MatStepper, - MatStepperIcon, - FaIconComponent, - MatStep, - MatStepLabel, - SavingProductDetailsStepComponent, - SavingProductCurrencyStepComponent, - SavingProductTermsStepComponent, - SavingProductSettingsStepComponent, - SavingProductChargesStepComponent, - SavingProductAccountingStepComponent, - SavingProductPreviewStepComponent + MatTabsModule, + CurrentProductDetailsStepComponent, + CurrentProductCurrencyStepComponent, + CurrentProductTermsStepComponent, + CurrentProductSettingsStepComponent, + CurrentProductChargesStepComponent, + CurrentProductLimitsStepComponent, + CurrentProductAccountingStepComponent, + CurrentProductPreviewStepComponent ] }) export class EditCurrentProductComponent { @@ -41,16 +45,18 @@ export class EditCurrentProductComponent { private settingsService = inject(SettingsService); private accounting = inject(Accounting); - @ViewChild(SavingProductDetailsStepComponent, { static: true }) detailsStep: SavingProductDetailsStepComponent; - @ViewChild(SavingProductCurrencyStepComponent, { static: true }) currencyStep: SavingProductCurrencyStepComponent; - @ViewChild(SavingProductTermsStepComponent, { static: true }) termsStep: SavingProductTermsStepComponent; - @ViewChild(SavingProductSettingsStepComponent, { static: true }) settingsStep: SavingProductSettingsStepComponent; - @ViewChild(SavingProductChargesStepComponent, { static: true }) chargesStep: SavingProductChargesStepComponent; - @ViewChild(SavingProductAccountingStepComponent, { static: true }) - accountingStep: SavingProductAccountingStepComponent; + @ViewChild(CurrentProductDetailsStepComponent, { static: true }) detailsStep: CurrentProductDetailsStepComponent; + @ViewChild(CurrentProductCurrencyStepComponent, { static: true }) currencyStep: CurrentProductCurrencyStepComponent; + @ViewChild(CurrentProductTermsStepComponent, { static: true }) termsStep: CurrentProductTermsStepComponent; + @ViewChild(CurrentProductSettingsStepComponent, { static: true }) settingsStep: CurrentProductSettingsStepComponent; + @ViewChild(CurrentProductChargesStepComponent, { static: true }) chargesStep: CurrentProductChargesStepComponent; + @ViewChild(CurrentProductLimitsStepComponent, { static: true }) limitsStep: CurrentProductLimitsStepComponent; + @ViewChild(CurrentProductAccountingStepComponent, { static: true }) + accountingStep: CurrentProductAccountingStepComponent; currentProductAndTemplate: any; accountingRuleData: string[] = []; + selectedTabIndex = 0; constructor() { this.route.data.subscribe((data: { currentProductAndTemplate: any }) => { @@ -60,19 +66,26 @@ export class EditCurrentProductComponent { } get detailsForm() { - return this.detailsStep.savingProductDetailsForm; + return this.detailsStep.CurrentProductDetailsForm; } get currencyForm() { - return this.currencyStep.savingProductCurrencyForm; + return this.currencyStep.CurrentProductCurrencyForm; } get termsForm() { - return this.termsStep.savingProductTermsForm; + return this.termsStep.CurrentProductTermsForm; } get settingsForm() { - return this.settingsStep.savingProductSettingsForm; + return this.settingsStep.CurrentProductSettingsForm; + } + get limitsForm(): void { + return; // Limits step doesn't have a form, it's data-driven } get accountingForm() { - return this.accountingStep.savingProductAccountingForm; + return this.accountingStep.CurrentProductAccountingForm; + } + + get totalTabs(): number { + return this.formValidAndNotPristine ? 8 : 7; } get formValidAndNotPristine() { @@ -87,36 +100,64 @@ export class EditCurrentProductComponent { !this.termsForm.pristine || !this.settingsForm.pristine || !this.chargesStep.pristine || + !this.limitsStep.pristine || !this.accountingForm.pristine) ); } get currentProduct() { return { - ...this.detailsStep.savingProductDetails, - ...this.currencyStep.savingProductCurrency, - ...this.termsStep.savingProductTerms, - ...this.settingsStep.savingProductSettings, - ...this.chargesStep.savingProductCharges, - ...this.accountingStep.savingProductAccounting + ...this.detailsStep.CurrentProductDetails, + ...this.currencyStep.CurrentProductCurrency, + ...this.termsStep.CurrentProductTerms, + ...this.settingsStep.CurrentProductSettings, + ...this.chargesStep.CurrentProductCharges, + ...this.limitsStep.CurrentProductLimits, + ...this.accountingStep.CurrentProductAccounting }; } - submit() { - const product = { + private prepareCurrentProductPayload() { + const currentProduct = { ...this.currentProduct, charges: this.currentProduct.charges.map((charge: any) => ({ id: charge.id })), - locale: this.settingsService.language.code + locale: this.settingsService.language.code, + dateFormat: 'dd MMMM yyyy' }; - delete product.advancedAccountingRules; - this.productsService.updateCurrentProduct(this.currentProductAndTemplate.id, product).subscribe((response: any) => { - this.router.navigate( - [ - '../../', - response.resourceId - ], - { relativeTo: this.route } - ); + delete currentProduct.advancedAccountingRules; + + if (currentProduct.lockinPeriodFrequencyType) { + const frequencyTypeMapping: { [key: string]: number } = { + DAYS: 0, + WEEKS: 1, + MONTHS: 2, + YEARS: 3 + }; + currentProduct.lockinPeriodFrequencyType = frequencyTypeMapping[currentProduct.lockinPeriodFrequencyType]; + } + + Object.keys(currentProduct).forEach((key) => { + if (currentProduct[key] === '' || currentProduct[key] === null || currentProduct[key] === undefined) { + delete currentProduct[key]; + } }); + + return currentProduct; + } + + submit() { + const currentProduct = this.prepareCurrentProductPayload(); + console.log('Current product update payload:', currentProduct); + this.productsService + .updateCurrentProduct(this.currentProductAndTemplate.id, currentProduct) + .subscribe((response: any) => { + this.router.navigate( + [ + '../../', + response.resourceId + ], + { relativeTo: this.route } + ); + }); } } diff --git a/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.html b/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.html index 18491561c..0a68dd9d7 100644 --- a/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.html +++ b/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.html @@ -1,3 +1,11 @@ + +
-
-
-

{{ currentProduct.name }}

- +
+

{{ currentProduct.name }}

-

{{ 'labels.heading.Details' | translate }}

- + +

{{ 'labels.inputs.Details' | translate }}

-
- {{ 'labels.inputs.Short Name' | translate }}: - {{ currentProduct.shortName }} -
- - @if (currentProduct.description) { -
- {{ 'labels.inputs.Description' | translate }}: - {{ currentProduct.description }} -
- } - -

{{ 'labels.inputs.Currency' | translate }}

- - -
- {{ 'labels.inputs.Currency' | translate }}: - {{ currentProduct.currency.name }} -
- -
- {{ 'labels.inputs.Decimal Places' | translate }}: - {{ currentProduct.currency.decimalPlaces }} -
- -

{{ 'labels.heading.Terms' | translate }}

- - -
- {{ 'labels.inputs.Nominal Annual Interest' | translate }}: - {{ currentProduct.nominalAnnualInterestRate | formatNumber }} % -
- -
- {{ 'labels.inputs.Interest Compounding Period' | translate }}: - {{ currentProduct.interestCompoundingPeriodType?.value | translateKey: 'catalogs' }} -
- -
- {{ 'labels.inputs.Interest Posting Period' | translate }}: - {{ currentProduct.interestPostingPeriodType?.value | translateKey: 'catalogs' }} -
- - @if (currentProduct.charges?.length) { -
-

{{ 'labels.heading.Charges' | translate }}

- - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Name' | translate }}{{ charge.name + ', ' + charge.currency.displaySymbol }}{{ 'labels.inputs.Type' | translate }} - {{ charge.chargeCalculationType.value | translateKey: 'catalogs' }} - {{ 'labels.inputs.Amount' | translate }}{{ charge.amount | formatNumber }}{{ 'labels.inputs.Collected On' | translate }}{{ charge.chargeTimeType.value | translateKey: 'catalogs' }}
-
- } - -

{{ 'labels.heading.Accounting' | translate }}

- - - + +
+ {{ 'labels.inputs.Short Name' | translate }}: + {{ currentProduct.shortName }}
+ + @if (currentProduct.description) { +
+ {{ 'labels.inputs.Description' | translate }}: + {{ currentProduct.description }} +
+ } + + @if ( + currentProduct.accountType || + currentProduct.customerSegment || + currentProduct.glAccountId || + currentProduct.effectiveDate || + currentProduct.effectiveTo || + currentProduct.endDate || + currentProduct.introducerRequired !== undefined || + currentProduct.shareLinkingEnabled !== undefined + ) { +

Product Classification

+ + + + @if (currentProduct.accountType) { +
+ Account Type: + {{ currentProduct.accountType }} +
+ } + + @if (currentProduct.customerSegment) { +
+ Customer Segment: + {{ currentProduct.customerSegment }} +
+ } + + @if (currentProduct.glAccountId) { +
+ GL Account (Liability): + {{ currentProduct.glAccountName || currentProduct.glAccountId }} +
+ } + + @if (currentProduct.effectiveDate) { +
+ Effective Date: + {{ currentProduct.effectiveDate | dateFormat }} +
+ } + + @if (currentProduct.effectiveTo) { +
+ Effective To: + {{ currentProduct.effectiveTo | dateFormat }} +
+ } + + @if (currentProduct.endDate) { +
+ End Date: + {{ currentProduct.endDate | dateFormat }} +
+ } + + @if (currentProduct.introducerRequired !== undefined) { +
+ Introducer Required: + {{ + currentProduct.introducerRequired === true || currentProduct.introducerRequired === 'Yes' ? 'Yes' : 'No' + }} +
+ } + + @if (currentProduct.shareLinkingEnabled !== undefined) { +
+ Share Linking Enabled: + {{ currentProduct.shareLinkingEnabled | yesNo }} +
+ } + } + +

{{ 'labels.inputs.Currency' | translate }}

+ + + +
+ {{ 'labels.inputs.Currency' | translate }}: + {{ currentProduct.currency.name }} +
+ +
+ {{ 'labels.inputs.Decimal Places' | translate }}: + {{ currentProduct.currency.decimalPlaces }} +
+ + @if (currentProduct.currency.inMultiplesOf !== undefined && currentProduct.currency.inMultiplesOf !== null) { +
+ {{ 'labels.inputs.Currency in multiples of' | translate }}: + {{ currentProduct.currency.inMultiplesOf }} +
+ } + + @if (currentProduct.dormancyAlert !== undefined) { +
+ Dormancy Alert: + {{ currentProduct.dormancyAlert | yesNo }} +
+ } + + @if (currentProduct.nominationFacility !== undefined) { +
+ Nomination Facility: + {{ currentProduct.nominationFacility | yesNo }} +
+ } + + @if (currentProduct.balanceSwapOption !== undefined) { +
+ Balance Swap Option: + {{ currentProduct.balanceSwapOption | yesNo }} +
+ } + + @if (currentProduct.msme !== undefined) { +
+ MSME: + {{ currentProduct.msme | yesNo }} +
+ } + + @if (currentProduct.autoDebitFacility !== undefined) { +
+ Auto Debit Facility: + {{ currentProduct.autoDebitFacility | yesNo }} +
+ } + +

Interest On OD

+ + + + @if (currentProduct.interestPostingPeriodType?.value) { +
+ {{ 'labels.inputs.Interest Posting Period' | translate }}: + {{ currentProduct.interestPostingPeriodType.value | translateKey: 'catalogs' }} +
+ } + + @if (currentProduct.interestCalculateSelector) { +
+ Interest Calculate: + {{ currentProduct.interestCalculateSelector }} +
+ } + + @if (currentProduct.averagePeriod) { +
+ Average Period: + {{ currentProduct.averagePeriod }} +
+ } + + @if (currentProduct.autoPost !== undefined) { +
+ Auto Post: + {{ currentProduct.autoPost | yesNo }} +
+ } + + @if (currentProduct.frequencyFixDay) { +
+ Frequency Fix Day: + {{ currentProduct.frequencyFixDay }} +
+ } + + @if (currentProduct.lastInterestDate) { +
+ Last Interest Date: + {{ currentProduct.lastInterestDate | dateFormat }} +
+ } + +

{{ 'labels.inputs.Settings' | translate }}

+ + + + @if (currentProduct.minRequiredOpeningBalance) { +
+ {{ 'labels.inputs.Minimum Opening Balance' | translate }}: + {{ currentProduct.minRequiredOpeningBalance | formatNumber }} +
+ } + + @if (currentProduct.lockinPeriodFrequency) { +
+ {{ 'labels.inputs.Lock-in Period' | translate }}: + {{ + currentProduct.lockinPeriodFrequency + + ' ' + + (currentProduct.lockinPeriodFrequencyType?.value || currentProduct.lockinPeriodFrequencyType?.code || '') + }} +
+ } + +
+ {{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}: + {{ currentProduct.withdrawalFeeForTransfers | yesNo }} +
+ + @if (currentProduct.balanceFrequency) { +
+ Balance Frequency: + {{ currentProduct.balanceFrequency }} +
+ } + +
+ Enable Lock-in Period: + {{ currentProduct.enableLockinPeriod | yesNo }} +
+ + @if (currentProduct.minBalanceForInterestCalculation) { +
+ {{ 'labels.inputs.Balance Required for Interest Calculation' | translate }}: + {{ currentProduct.minBalanceForInterestCalculation | formatNumber }} +
+ } + +
+ {{ 'labels.inputs.Enforce Minimum Balance' | translate }}: + {{ currentProduct.enforceMinRequiredBalance | yesNo }} +
+ + @if (currentProduct.minRequiredBalance) { +
+ {{ 'labels.inputs.Minimum Balance' | translate }}: + {{ currentProduct.minRequiredBalance | formatNumber }} +
+ } + +
+ {{ 'labels.inputs.Withhold Tax is Applicable' | translate }}: + {{ currentProduct.withHoldTax | yesNo }} +
+ + @if (currentProduct.withHoldTax) { +
+ {{ 'labels.inputs.Tax Group' | translate }}: + {{ currentProduct.taxGroup.name }} +
+ } + +
+ {{ 'labels.inputs.Is Overdraft Allowed' | translate }}: + {{ currentProduct.allowOverdraft | yesNo }} +
+ +
+ Account Closed Zero Balance in Day Closing: + {{ currentProduct.accountClosedZeroBalanceInDayClosing | yesNo }} +
+ + @if (currentProduct.minOverdraftForInterestCalculation) { +
+ {{ 'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate }}: + {{ currentProduct.minOverdraftForInterestCalculation | formatNumber }} +
+ } + + @if (currentProduct.nominalAnnualInterestRateOverdraft) { +
+ {{ 'labels.inputs.Nominal Annual Interest for Overdraft' | translate }}: + {{ currentProduct.nominalAnnualInterestRateOverdraft | formatNumber }} % +
+ } + + @if (currentProduct.overdraftLimit) { +
+ {{ 'labels.inputs.Maximum Overdraft Amount Limit' | translate }}: + {{ currentProduct.overdraftLimit | formatNumber }} +
+ } + + @if (currentProduct.allowOverdraft) { + @if (currentProduct.avgBalanceCalcPeriod) { +
+ Average Balance Calculation Period: + {{ currentProduct.avgBalanceCalcPeriod }} +
+ } + + @if (currentProduct.validForPeriod) { +
+ Valid for Period: + {{ currentProduct.validForPeriod }} +
+ } + + @if (currentProduct.odAccountClosedZeroBalanceInDayClosing !== undefined) { +
+ OD Account Closed with Zero Balance in Day Closing: + {{ currentProduct.odAccountClosedZeroBalanceInDayClosing | yesNo }} +
+ } + } + +
+ {{ 'labels.inputs.Enable Dormancy Tracking' | translate }}: + {{ currentProduct.isDormancyTrackingActive | yesNo }} +
+ + @if (currentProduct.isDormancyTrackingActive) { +
+ {{ 'labels.inputs.Number of Days to Inactive sub-status' | translate }}: + {{ currentProduct.daysToInactive }} + {{ 'labels.inputs.Number of Days to Dormant sub-status' | translate }}: + {{ currentProduct.daysToDormancy }} + {{ 'labels.inputs.Number of Days to Escheat' | translate }}: + {{ currentProduct.daysToEscheat }} +
+ } + +

Account Facilities

+ + + @if (currentProduct.accountFacilityConfig) { +
+ @for (feature of accountFacilityFeatureItems; track feature.key) { + @if (currentProduct.accountFacilityConfig[feature.key] !== undefined) { +
+ {{ feature.label }}: + {{ currentProduct.accountFacilityConfig[feature.key] | yesNo }} +
+ } + } +
+ } + + @if (currentProduct.accountFacilityLimits?.length) { +
+

Account Facility Limits

+ + + + + + + + + + + + + + + + + + + + +
Limit Type{{ row.limitType }}General Limit{{ row.generalLimit }}Minor Account Limit{{ row.minorAccountLimit }}Small Saving Limit{{ row.smallSavingLimit }}
+
+ } + + @if (currentProduct.depositLimitKyc) { +
+

Deposit Limit (KYC)

+
+ General: + {{ currentProduct.depositLimitKyc.general.kycType }} - + {{ currentProduct.depositLimitKyc.general.limit }} +
+
+ Minor: + {{ currentProduct.depositLimitKyc.minor.kycType }} - {{ currentProduct.depositLimitKyc.minor.limit }} +
+
+ Small Saving: + {{ currentProduct.depositLimitKyc.smallSaving.kycType }} - + {{ currentProduct.depositLimitKyc.smallSaving.limit }} +
+
+ } + + @if (currentProduct.charges.length) { +
+

{{ 'labels.heading.Charges' | translate }}

+ + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.Name' | translate }} + {{ charge.name + ', ' + charge.currency.displaySymbol }} + {{ 'labels.inputs.Type' | translate }} + {{ charge.chargeCalculationType.value | translateKey: 'catalogs' }} + {{ 'labels.inputs.Amount' | translate }} + {{ charge.amount | formatNumber }} + {{ 'labels.inputs.Collected On' | translate }} + {{ charge.chargeTimeType.value | translateKey: 'catalogs' }} +
+
+ } + +

{{ 'labels.heading.Accounting' | translate }}

+ + + + + + + @if (currentProduct.otherGlMappingsMap) { +
+

Other GL Mappings

+ + + @if (currentProduct.otherGlMappingsMap.assets?.length) { +

Assets

+ + + + + + + + + + + +
Account Name{{ row.accountName }}GL Account{{ row.glCode || row.glAccountId }}
+ } + + @if (currentProduct.otherGlMappingsMap.liabilities?.length) { +

Liabilities

+ + + + + + + + + + + +
Account Name{{ row.accountName }}GL Account{{ row.glCode || row.glAccountId }}
+ } + + @if (currentProduct.otherGlMappingsMap.expenses?.length) { +

Expenses

+ + + + + + + + + + + +
Account Name{{ row.accountName }}GL Account{{ row.glCode || row.glAccountId }}
+ } + + @if (currentProduct.otherGlMappingsMap.income?.length) { +

Income

+ + + + + + + + + + + +
Account Name{{ row.accountName }}GL Account{{ row.glCode || row.glAccountId }}
+ } +
+ } + + @if ( + currentProduct.paymentChannelToFundSourceMappings?.length || + currentProduct.feeToIncomeAccountMappings?.length || + currentProduct.penaltyToIncomeAccountMappings?.length + ) { +
+

{{ 'labels.inputs.Advanced Accounting Rules' | translate }}

+ + @if (currentProduct.paymentChannelToFundSourceMappings?.length) { +
+

{{ 'labels.inputs.Fund Sources for Payment Channels' | translate }}

+ + + + + + + + + + + +
{{ 'labels.inputs.Payment Type' | translate }} + {{ paymentFundSource.paymentType.name }} + {{ 'labels.inputs.Fund Source' | translate }} + ({{ paymentFundSource.fundSourceAccount.glCode }}) {{ paymentFundSource.fundSourceAccount.name }} +
+
+ } + @if (currentProduct.feeToIncomeAccountMappings?.length) { +
+

{{ 'labels.inputs.Fees to Specific Income Accounts' | translate }}

+ + + + + + + + + + + +
{{ 'labels.inputs.Fee/Charge' | translate }} + {{ fee.charge.name }} + {{ 'labels.inputs.Income Account' | translate }}({{ fee.incomeAccount.glCode }}) {{ fee.incomeAccount.name }}
+
+ } + @if (currentProduct.penaltyToIncomeAccountMappings?.length) { +
+

{{ 'labels.inputs.Penalties to Specific Income Accounts' | translate }}

+ + + + + + + + + + + +
{{ 'labels.inputs.Penalty' | translate }} + {{ penalty.charge.name }} + {{ 'labels.inputs.Income Account' | translate }} + ({{ penalty.incomeAccount.glCode }}) {{ penalty.incomeAccount.name }} +
+
+ } +
+ }
diff --git a/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.ts b/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.ts index 54b58f02c..ecc8e2eb5 100644 --- a/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.ts +++ b/src/app/products/current-products/view-current-product/current-product-general-tab/current-product-general-tab.component.ts @@ -1,3 +1,10 @@ +/** + * 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 { ActivatedRoute, RouterLink } from '@angular/router'; import { Accounting } from 'app/core/utils/accounting'; @@ -16,6 +23,7 @@ import { MatRow } from '@angular/material/table'; import { ViewSavingsAccountingDetailsComponent } from '../../../../shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component'; +import { DateFormatPipe } from '../../../../pipes/date-format.pipe'; import { FormatNumberPipe } from '../../../../pipes/format-number.pipe'; import { YesnoPipe } from '../../../../pipes/yesno.pipe'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @@ -39,6 +47,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; MatRowDef, MatRow, ViewSavingsAccountingDetailsComponent, + DateFormatPipe, FormatNumberPipe, YesnoPipe ] @@ -56,6 +65,34 @@ export class CurrentProductGeneralTabComponent { 'chargeTimeType' ]; + paymentFundSourceDisplayedColumns: string[] = [ + 'paymentTypeId', + 'fundSourceAccountId' + ]; + feesPenaltyIncomeDisplayedColumns: string[] = [ + 'chargeId', + 'incomeAccountId' + ]; + + accountFacilityFeatureItems = [ + { key: 'atmFacility', label: 'ATM Facility' }, + { key: 'atmWithdrawalFacility', label: 'ATM Withdrawal Facility' }, + { key: 'upiFacility', label: 'UPI Facility' }, + { key: 'posTransactionsFacility', label: 'POS Transactions Facility' }, + { key: 'chequeBook', label: 'Cheque Book' }, + { key: 'impsFacility', label: 'IMPS Facility' }, + { key: 'smsAlerts', label: 'SMS Alerts' }, + { key: 'bbpsFacility', label: 'BBPS Facility' }, + { key: 'insuranceCover', label: 'Insurance Cover' }, + { key: 'bepgFacility', label: 'BEPG Facility' }, + { key: 'neftFacility', label: 'NEFT Facility' }, + { key: 'rtgsFacility', label: 'RTGS Facility' }, + { key: 'interBankTotalCapFacility', label: 'Inter-Bank Total Cap Facility' }, + { key: 'withdrawalFacility', label: 'Withdrawal Facility' }, + { key: 'depositFacilityKyc', label: 'Deposit Facility KYC' }, + { key: 'virtualDebitCard', label: 'Virtual Debit Card' } + ]; + constructor() { this.route.data.subscribe((data: { currentProduct: any }) => { this.currentProduct = data.currentProduct; diff --git a/src/app/products/fixed-deposit-products/create-fixed-deposit-product/create-fixed-deposit-product.component.html b/src/app/products/fixed-deposit-products/create-fixed-deposit-product/create-fixed-deposit-product.component.html index 2036fcd49..bc74f8541 100644 --- a/src/app/products/fixed-deposit-products/create-fixed-deposit-product/create-fixed-deposit-product.component.html +++ b/src/app/products/fixed-deposit-products/create-fixed-deposit-product/create-fixed-deposit-product.component.html @@ -12,7 +12,12 @@

{{ 'labels.heading.Create Fixed Deposit Product' | translate }}

- + @@ -124,19 +129,19 @@
@if (selectedTabIndex > 0) { } @if (selectedTabIndex < totalTabs - 1) { } @if (fixedDepositProductFormValid && selectedTabIndex === totalTabs - 1) { } diff --git a/src/app/products/fixed-deposit-products/create-fixed-deposit-product/create-fixed-deposit-product.component.scss b/src/app/products/fixed-deposit-products/create-fixed-deposit-product/create-fixed-deposit-product.component.scss index 8e2ab2292..ce39f556f 100644 --- a/src/app/products/fixed-deposit-products/create-fixed-deposit-product/create-fixed-deposit-product.component.scss +++ b/src/app/products/fixed-deposit-products/create-fixed-deposit-product/create-fixed-deposit-product.component.scss @@ -8,33 +8,83 @@ $primary: #1353a0; -.fdp-container { max-width: 960px; margin: 0 auto; padding: 24px 16px; } - -.fdp-header { - display: flex; align-items: center; gap: 10px; margin-bottom: 20px; - &__icon { font-size: 20px; color: $primary; } - &__title { margin: 0; font-size: 22px; font-weight: 500; } +.fdp-container { + max-width: 960px; + margin: 0 auto; + padding: 24px 16px; } -.fdp-tabs { - background: var(--md-sys-color-surface, #fff); border-radius: 8px; - box-shadow: 0 2px 8px rgb(0 0 0 / 8%); +.fdp-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 20px; - ::ng-deep { - .mat-mdc-tab-header { border-bottom: 1px solid rgb(0 0 0 / 8%); } - .mat-mdc-tab { min-width: 100px; padding: 0 12px; height: 52px; } - .mat-mdc-tab-body-wrapper { min-height: 200px; } - .mat-mdc-tab-body { overflow: visible !important; } - .mat-mdc-tab-body-content { overflow: visible !important; } + &__icon { + font-size: 20px; + color: $primary; + } + + &__title { + margin: 0; + font-size: 22px; + font-weight: 500; } } -.fdp-tab-icon { margin-right: 8px; font-size: 14px; } -.fdp-tab-content { padding: 24px 16px; } +.fdp-tabs { + background: var(--md-sys-color-surface, #fff); + border-radius: 8px; + box-shadow: 0 2px 8px rgb(0 0 0 / 8%); + + ::ng-deep { + .mat-mdc-tab-header { + border-bottom: 1px solid rgb(0 0 0 / 8%); + } + + .mat-mdc-tab { + min-width: 100px; + padding: 0 12px; + height: 52px; + } + + .mat-mdc-tab-body-wrapper { + min-height: 200px; + } + + .mat-mdc-tab-body { + overflow: visible !important; + } + + .mat-mdc-tab-body-content { + overflow: visible !important; + } + } +} + +.fdp-tab-icon { + margin-right: 8px; + font-size: 14px; +} + +.fdp-tab-content { + padding: 24px 16px; +} .fdp-action-bar { - display: flex; justify-content: space-between; align-items: center; - margin-top: 20px; padding: 16px 0; border-top: 1px solid rgb(0 0 0 / 8%); - &__cancel { color: rgb(0 0 0 / 60%); } - &__right { display: flex; gap: 12px; } + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 20px; + padding: 16px 0; + border-top: 1px solid rgb(0 0 0 / 8%); + + &__cancel { + color: rgb(0 0 0 / 60%); + } + + &__right { + display: flex; + gap: 12px; + } } diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.html b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.html index 782cd2f88..86939918d 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.html +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-accounting-step/fixed-deposit-product-accounting-step.component.html @@ -169,10 +169,7 @@
@if (paymentChannelToFundSourceMappings.value.length !== 0) {
- +
{{ 'labels.heading.Payment Type' | translate }} diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-charges-step/fixed-deposit-product-charges-step.component.html b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-charges-step/fixed-deposit-product-charges-step.component.html index 02fadc1c6..a8de6dfc4 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-charges-step/fixed-deposit-product-charges-step.component.html +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-charges-step/fixed-deposit-product-charges-step.component.html @@ -24,7 +24,14 @@
- @@ -33,10 +40,7 @@ @if (chargesDataSource.length > 0) {
- +
{{ 'labels.inputs.Name' | translate }} diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-interest-rate-chart-step/fixed-deposit-product-interest-rate-chart-step.component.html b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-interest-rate-chart-step/fixed-deposit-product-interest-rate-chart-step.component.html index 24e12e096..8620c0951 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-interest-rate-chart-step/fixed-deposit-product-interest-rate-chart-step.component.html +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-interest-rate-chart-step/fixed-deposit-product-interest-rate-chart-step.component.html @@ -78,7 +78,13 @@ {{ 'labels.inputs.End Date' | translate }} - + @@ -91,7 +97,13 @@
- @@ -105,11 +117,7 @@ @if (chart.value.chartSlabs.length !== 0) {
- +
{{ 'labels.inputs.Period' | translate }} @@ -182,7 +190,9 @@ >

{{ 'labels.heading.Incentives' | translate }}

@@ -201,25 +211,33 @@
- + - + - + - + - + diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-preview-step/fixed-deposit-product-preview-step.component.html b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-preview-step/fixed-deposit-product-preview-step.component.html index 2fb3e363c..716482fcf 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-preview-step/fixed-deposit-product-preview-step.component.html +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-preview-step/fixed-deposit-product-preview-step.component.html @@ -299,7 +299,9 @@ >

{{ 'labels.heading.Incentives' | translate }}

@@ -310,36 +312,54 @@
- + - + - + - + @@ -452,7 +480,10 @@
@if (!isNoneAccounting()) { - + }
@@ -474,10 +505,7 @@ @if (fixedDepositProduct.paymentChannelToFundSourceMappings?.length) {

{{ 'labels.heading.Fund Sources for Payment Channels' | translate }}

-
{{ 'labels.inputs.Entity Type' | translate }} + {{ 'labels.inputs.Entity Type' | translate }} + {{ incentive.entityType | find: entityTypeData : 'id' : 'value' }} {{ 'labels.inputs.Attribute Name' | translate }} + {{ 'labels.inputs.Attribute Name' | translate }} + {{ incentive.attributeName | find: attributeNameData : 'id' : 'value' }} {{ 'labels.inputs.Condition Type' | translate }} + {{ 'labels.inputs.Condition Type' | translate }} + {{ incentive.conditionType | find: conditionTypeData : 'id' : 'value' }} {{ 'labels.inputs.Attribute Value' | translate }} + {{ 'labels.inputs.Attribute Value' | translate }} + @switch (incentive.attributeName) { @case (2) { @@ -229,7 +247,9 @@ {{ incentive.attributeValue }} } @case (4) { - {{ incentive.attributeValue | find: clientTypeData : 'id' : 'name' }} + {{ + incentive.attributeValue | find: clientTypeData : 'id' : 'name' + }} } @case (5) { {{ @@ -240,7 +260,9 @@ {{ 'labels.inputs.Incentive Type' | translate }} + {{ 'labels.inputs.Incentive Type' | translate }} + {{ incentive.incentiveType | find: incentiveTypeData : 'id' : 'value' }} {{ incentive.entityType - | find: fixedDepositProductsTemplate.chartTemplate.entityTypeOptions : 'id' : 'value' + | find + : fixedDepositProductsTemplate.chartTemplate.entityTypeOptions + : 'id' + : 'value' }} {{ 'labels.inputs.Attribute Name' | translate }} + {{ 'labels.inputs.Attribute Name' | translate }} + {{ incentive.attributeName - | find: fixedDepositProductsTemplate.chartTemplate.attributeNameOptions : 'id' : 'value' + | find + : fixedDepositProductsTemplate.chartTemplate.attributeNameOptions + : 'id' + : 'value' }} {{ 'labels.inputs.Condition Type' | translate }} + {{ 'labels.inputs.Condition Type' | translate }} + {{ incentive.conditionType - | find: fixedDepositProductsTemplate.chartTemplate.conditionTypeOptions : 'id' : 'value' + | find + : fixedDepositProductsTemplate.chartTemplate.conditionTypeOptions + : 'id' + : 'value' }} {{ 'labels.inputs.Attribute Value' | translate }} + {{ 'labels.inputs.Attribute Value' | translate }} + @switch (incentive.attributeName) { @case (2) { {{ incentive.attributeValue - | find: fixedDepositProductsTemplate.chartTemplate.genderOptions : 'id' : 'name' + | find + : fixedDepositProductsTemplate.chartTemplate.genderOptions + : 'id' + : 'name' }} } @case (3) { @@ -348,7 +368,10 @@ @case (4) { {{ incentive.attributeValue - | find: fixedDepositProductsTemplate.chartTemplate.clientTypeOptions : 'id' : 'name' + | find + : fixedDepositProductsTemplate.chartTemplate.clientTypeOptions + : 'id' + : 'name' }} } @case (5) { @@ -364,11 +387,16 @@ {{ 'labels.inputs.Incentive Type' | translate }} + {{ 'labels.inputs.Incentive Type' | translate }} + {{ incentive.incentiveType - | find: fixedDepositProductsTemplate.chartTemplate.incentiveTypeOptions : 'id' : 'value' + | find + : fixedDepositProductsTemplate.chartTemplate.incentiveTypeOptions + : 'id' + : 'value' }}
+
{{ 'labels.inputs.Payment Type' | translate }} @@ -504,10 +532,7 @@ @if (fixedDepositProduct.feeToIncomeAccountMappings?.length) {

{{ 'labels.heading.Fees to Specific Income Accounts' | translate }}

- +
@@ -82,26 +91,60 @@

{{ 'labels.buttons.Create User' | translate }}

{{ 'labels.text.This option allows you to create new users in your organization' | translate }}.

- - - + + +

{{ 'labels.heading.Search bar to filter users' | translate }}.

- - - + + +

{{ 'labels.heading.List of Users with their details' | translate }}.

- - - + + +
diff --git a/src/app/users/users.component.scss b/src/app/users/users.component.scss index 237d4d20a..617145bd3 100644 --- a/src/app/users/users.component.scss +++ b/src/app/users/users.component.scss @@ -12,12 +12,16 @@ background: #ecfdf5 !important; color: #059669 !important; - &::before { background: #059669; } + &::before { + background: #059669; + } } .status-inactive { background: #fef2f2 !important; color: #dc2626 !important; - &::before { background: #dc2626; } + &::before { + background: #dc2626; + } } diff --git a/src/app/users/view-user/view-user.component.html b/src/app/users/view-user/view-user.component.html index 9059927d4..aea1e1c6c 100644 --- a/src/app/users/view-user/view-user.component.html +++ b/src/app/users/view-user/view-user.component.html @@ -34,9 +34,15 @@ {{ role.name }} } - + {{ 'labels.buttons.Self Service' | translate }} - {{ userData.selfService ? ('labels.buttons.Yes' | translate) : ('labels.buttons.No' | translate) }} + {{ + userData.selfService ? ('labels.buttons.Yes' | translate) : ('labels.buttons.No' | translate) + }} @@ -78,7 +84,11 @@
{{ 'labels.buttons.Is Self Service' | translate }}
- + {{ userData.selfService ? ('labels.buttons.Yes' | translate) : ('labels.buttons.No' | translate) }} diff --git a/src/app/users/view-user/view-user.component.scss b/src/app/users/view-user/view-user.component.scss index be626ba50..1811af90e 100644 --- a/src/app/users/view-user/view-user.component.scss +++ b/src/app/users/view-user/view-user.component.scss @@ -40,10 +40,17 @@ $accent: #0ea5e9; content: ''; position: absolute; 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); + 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; @@ -144,14 +151,18 @@ $accent: #0ea5e9; background: rgb(16 185 129 / 15%); border-color: rgb(16 185 129 / 25%); - .vu-chip__val { color: #6ee7b7; } + .vu-chip__val { + color: #6ee7b7; + } } &--inactive { background: rgb(239 68 68 / 12%); border-color: rgb(239 68 68 / 20%); - .vu-chip__val { color: #fca5a5; } + .vu-chip__val { + color: #fca5a5; + } } } @@ -221,13 +232,17 @@ $accent: #0ea5e9; &--yes { color: #16a34a; - .vu-status__dot { background: #16a34a; } + .vu-status__dot { + background: #16a34a; + } } &--no { color: #dc2626; - .vu-status__dot { background: #dc2626; } + .vu-status__dot { + background: #dc2626; + } } } @@ -273,13 +288,23 @@ $accent: #0ea5e9; text-align: center; } - .vu-name-row { justify-content: center; } - .vu-chips { justify-content: center; } - .vu-grid { grid-template-columns: repeat(2, 1fr); } + .vu-name-row { + justify-content: center; + } + + .vu-chips { + justify-content: center; + } + + .vu-grid { + grid-template-columns: repeat(2, 1fr); + } } @media (width <= 480px) { - .vu-grid { grid-template-columns: 1fr; } + .vu-grid { + grid-template-columns: 1fr; + } } // ── Dark mode ── @@ -297,10 +322,17 @@ $accent: #0ea5e9; background: #2a2a2a; border-color: rgb(255 255 255 / 6%); - &:hover { background: #333; } + &:hover { + background: #333; + } - &__label { color: #888; } - &__value { color: #e0e0e0; } + &__label { + color: #888; + } + + &__value { + color: #e0e0e0; + } } .vu-role-tag { diff --git a/src/app/web-app.component.ts b/src/app/web-app.component.ts index a3ccae8c6..b6c9e2445 100644 --- a/src/app/web-app.component.ts +++ b/src/app/web-app.component.ts @@ -188,21 +188,30 @@ export class WebAppComponent implements OnInit, OnDestroy { ) .subscribe((event) => { if (!event['title']) { - this.i18nService.translate('APP_NAME').pipe(take(1)).subscribe((t: any) => this.titleService.setTitle(t)); + this.i18nService + .translate('APP_NAME') + .pipe(take(1)) + .subscribe((t: any) => this.titleService.setTitle(t)); return; } const raw = event['title']; const textKey = `labels.text.${raw}`; const menuKey = `labels.menus.${raw}`; - this.i18nService.translate(textKey).pipe(take(1)).subscribe((textResult: any) => { - if (textResult !== textKey) { - this.titleService.setTitle(textResult); - } else { - this.i18nService.translate(menuKey).pipe(take(1)).subscribe((menuResult: any) => { - this.titleService.setTitle(menuResult !== menuKey ? menuResult : raw); - }); - } - }); + this.i18nService + .translate(textKey) + .pipe(take(1)) + .subscribe((textResult: any) => { + if (textResult !== textKey) { + this.titleService.setTitle(textResult); + } else { + this.i18nService + .translate(menuKey) + .pipe(take(1)) + .subscribe((menuResult: any) => { + this.titleService.setTitle(menuResult !== menuKey ? menuResult : raw); + }); + } + }); }); // Stores top 100 user activites as local storage object. diff --git a/src/environments/.env.ts b/src/environments/.env.ts index 970461220..2a3c8fdfb 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -1,9 +1,16 @@ +/** + * 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/. + */ // IMPORTANT: THIS FILE IS AUTO GENERATED! DO NOT MANUALLY EDIT OR CHECKIN! /* tslint:disable */ export default { 'mifos_x': { - 'version': '260415', - 'hash': 'g79e455c4e' + 'version': '260429', + 'hash': '0d3d8379' }, 'allow_switching_backend_instance': true }; diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 8112c07fa..92ff5e0ad 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -123,8 +123,7 @@ export const environment = { loadedEnv['productionModeEnableRBAC'] === 'true' || loadedEnv['productionModeEnableRBAC'] === true || false, /** OpenProject Integration (via Fineract backend) */ - openProjectEnabled: - loadedEnv['openProjectEnabled'] === 'true' || loadedEnv['openProjectEnabled'] === true || false, + openProjectEnabled: loadedEnv['openProjectEnabled'] === 'true' || loadedEnv['openProjectEnabled'] === true || false, /** CFCFRMS (Cyber Fraud Complaint) Integration */ cfcfrmsApiUrl: loadedEnv['cfcfrmsApiUrl'] || '', diff --git a/src/main.scss b/src/main.scss index 97c842e06..96bbc55f4 100644 --- a/src/main.scss +++ b/src/main.scss @@ -993,3 +993,51 @@ mat-card { background: rgb(15 23 42 / 45%); pointer-events: auto; } + +/* ─── Add Beneficiary Dialog Panel ─── */ +.abd-dialog-panel { + .mat-mdc-dialog-container { + padding: 0; + border-radius: 14px; + overflow: hidden; + } + + .mat-mdc-dialog-surface { + padding: 0 !important; + border-radius: 14px !important; + } + + .mat-mdc-dialog-content { + padding: 0 20px !important; + max-height: 72vh; + } + + .mat-mdc-dialog-actions { + padding: 0 !important; + margin: 0 !important; + } +} + +/* ─── Add Related Client Dialog Panel ─── */ +.arc-dialog-panel { + .mat-mdc-dialog-container { + padding: 0; + border-radius: 14px; + overflow: hidden; + } + + .mat-mdc-dialog-surface { + padding: 0 !important; + border-radius: 14px !important; + } + + .mat-mdc-dialog-content { + padding: 0 20px !important; + max-height: 70vh; + } + + .mat-mdc-dialog-actions { + padding: 0 !important; + margin: 0 !important; + } +}
{{ 'labels.inputs.Fees' | translate }} @@ -529,12 +554,11 @@ } @if (fixedDepositProduct.penaltyToIncomeAccountMappings?.length) { -

{{ 'labels.heading.Penalties to Specific Income Accounts' | translate }}

+

+ {{ 'labels.heading.Penalties to Specific Income Accounts' | translate }} +

- +
+ diff --git a/src/app/products/floating-rates/floating-rates.component.html b/src/app/products/floating-rates/floating-rates.component.html index 1f4a494be..ebab660bc 100644 --- a/src/app/products/floating-rates/floating-rates.component.html +++ b/src/app/products/floating-rates/floating-rates.component.html @@ -24,7 +24,13 @@
- diff --git a/src/app/products/loan-products/create-loan-product/create-loan-product.component.html b/src/app/products/loan-products/create-loan-product/create-loan-product.component.html index 44f05534d..260e066d8 100644 --- a/src/app/products/loan-products/create-loan-product/create-loan-product.component.html +++ b/src/app/products/loan-products/create-loan-product/create-loan-product.component.html @@ -12,7 +12,12 @@

{{ 'labels.heading.Create Loan Product' | translate }}

- + @@ -170,19 +175,19 @@
@if (selectedTabIndex > 0) { } @if (selectedTabIndex < totalTabs - 1) { } @if (loanProductFormValid && selectedTabIndex === totalTabs - 1) { } diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.html b/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.html index 7ecf2adef..61943160b 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.html +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-accounting-step/loan-product-accounting-step.component.html @@ -7,7 +7,6 @@ -->
-
@@ -16,10 +15,7 @@
- + @for (accountingRule of accountingRuleData; track accountingRule; let i = $index) { {{ 'labels.accounting.' + accountingRule | translate }} @@ -38,7 +34,6 @@
@if (loanProductAccountingForm.value.accountingRule >= 2 && loanProductAccountingForm.value.accountingRule <= 4) { -
@@ -475,10 +470,7 @@
@if (chargeOffReasonToExpenseAccountMappings.value.length !== 0) {
-
{{ 'labels.inputs.Penalty' | translate }} diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.html b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.html index 37e6b07b2..5c8f66550 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.html +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-settings-step/fixed-deposit-product-settings-step.component.html @@ -160,7 +160,10 @@ {{ 'labels.inputs.Period' | translate }} - @for (preClosurePenalInterestOnType of preClosurePenalInterestOnTypeData; track preClosurePenalInterestOnType) { + @for ( + preClosurePenalInterestOnType of preClosurePenalInterestOnTypeData; + track preClosurePenalInterestOnType + ) { {{ preClosurePenalInterestOnType.value }} diff --git a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.html b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.html index 448540345..c2228368b 100644 --- a/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.html +++ b/src/app/products/fixed-deposit-products/fixed-deposit-product-stepper/fixed-deposit-product-terms-step/fixed-deposit-product-terms-step.component.html @@ -66,7 +66,10 @@ matTooltip="{{ 'tooltips.The period at which interest rate' | translate }}" required > - @for (interestCompoundingPeriodType of interestCompoundingPeriodTypeData; track interestCompoundingPeriodType) { + @for ( + interestCompoundingPeriodType of interestCompoundingPeriodTypeData; + track interestCompoundingPeriodType + ) { {{ interestCompoundingPeriodType.value }} diff --git a/src/app/products/floating-rates/create-floating-rate/create-floating-rate.component.html b/src/app/products/floating-rates/create-floating-rate/create-floating-rate.component.html index a6d4948f3..c60da248c 100644 --- a/src/app/products/floating-rates/create-floating-rate/create-floating-rate.component.html +++ b/src/app/products/floating-rates/create-floating-rate/create-floating-rate.component.html @@ -88,7 +88,9 @@ > {{ 'labels.inputs.Interest Rate' | translate }} - {{ floatingRatePeriod.interestRate | formatNumber }} + {{ floatingRatePeriod.interestRate | formatNumber }} +
+
- +
{{ 'labels.inputs.Charge-off reason' | translate }} @@ -552,7 +544,5 @@ > } - } - diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-capitalized-income-step/loan-product-deferred-income-recognition-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-capitalized-income-step/loan-product-deferred-income-recognition-step.component.scss index 3b4fc2e89..65975ad97 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-capitalized-income-step/loan-product-deferred-income-recognition-step.component.scss +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-capitalized-income-step/loan-product-deferred-income-recognition-step.component.scss @@ -18,7 +18,9 @@ $gray-900: #202124; overflow: hidden; background: #fff; - & + & { margin-top: 16px; } + & + & { + margin-top: 16px; + } &__header { display: flex; @@ -28,8 +30,17 @@ $gray-900: #202124; background: $gray-50; border-bottom: 1px solid $gray-200; - i { font-size: 14px; color: $accent; } - h3 { margin: 0; font-size: 14px; font-weight: 600; color: $gray-900; } + i { + font-size: 14px; + color: $accent; + } + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } } } @@ -39,12 +50,17 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .lpdi-field { width: 100%; - &--full { grid-column: 1 / -1; } + + &--full { + grid-column: 1 / -1; + } } .lpdi-checkbox-row { diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.html b/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.html index c4763afa7..ec1e817bf 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.html +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.html @@ -43,11 +43,7 @@ @if ((chargesDataSource | chargesPenaltyFilter: false).length > 0) {
- +
@@ -90,7 +90,12 @@ - +
{{ 'labels.inputs.Name' | translate }} @@ -130,11 +126,7 @@ @if ((chargesDataSource | chargesPenaltyFilter: true).length > 0) {
- +
- +
{{ 'labels.inputs.Name' | translate }} diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.scss index a9cfa9a1f..d40566222 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.scss +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-charges-step/loan-product-charges-step.component.scss @@ -19,7 +19,9 @@ $gray-900: #202124; overflow: hidden; background: #fff; - & + & { margin-top: 16px; } + & + & { + margin-top: 16px; + } &__header { display: flex; @@ -29,8 +31,17 @@ $gray-900: #202124; background: $gray-50; border-bottom: 1px solid $gray-200; - i { font-size: 14px; color: $accent; } - h3 { margin: 0; font-size: 14px; font-weight: 600; color: $gray-900; } + i { + font-size: 14px; + color: $accent; + } + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } } } @@ -59,7 +70,11 @@ $gray-900: #202124; @media (width <= 600px) { flex-direction: column; - &__select { max-width: 100%; width: 100%; } + + &__select { + max-width: 100%; + width: 100%; + } } } diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.scss index 5dbbf06b8..6f7269cbe 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.scss +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-currency-step/loan-product-currency-step.component.scss @@ -18,7 +18,9 @@ $gray-900: #202124; overflow: hidden; background: #fff; - & + & { margin-top: 16px; } + & + & { + margin-top: 16px; + } &__header { display: flex; @@ -28,8 +30,17 @@ $gray-900: #202124; background: $gray-50; border-bottom: 1px solid $gray-200; - i { font-size: 14px; color: $accent; } - h3 { margin: 0; font-size: 14px; font-weight: 600; color: $gray-900; } + i { + font-size: 14px; + color: $accent; + } + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } } } @@ -39,12 +50,17 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .lpc-field { width: 100%; - &--full { grid-column: 1 / -1; } + + &--full { + grid-column: 1 / -1; + } } .lpc-checkbox-row { diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.scss index 01e69962d..e0ff5016a 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.scss +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-details-step/loan-product-details-step.component.scss @@ -18,7 +18,9 @@ $gray-900: #202124; overflow: hidden; background: #fff; - & + & { margin-top: 16px; } + & + & { + margin-top: 16px; + } &__header { display: flex; @@ -28,8 +30,17 @@ $gray-900: #202124; background: $gray-50; border-bottom: 1px solid $gray-200; - i { font-size: 14px; color: $accent; } - h3 { margin: 0; font-size: 14px; font-weight: 600; color: $gray-900; } + i { + font-size: 14px; + color: $accent; + } + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } } } @@ -39,12 +50,17 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .lpd-field { width: 100%; - &--full { grid-column: 1 / -1; } + + &--full { + grid-column: 1 / -1; + } } .lpd-checkbox-row { diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-interest-refund-step/loan-product-interest-refund-step.component.scss b/src/app/products/loan-products/loan-product-stepper/loan-product-interest-refund-step/loan-product-interest-refund-step.component.scss index c8dc9d8b9..0ab0abedd 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-interest-refund-step/loan-product-interest-refund-step.component.scss +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-interest-refund-step/loan-product-interest-refund-step.component.scss @@ -26,8 +26,17 @@ $gray-900: #202124; background: $gray-50; border-bottom: 1px solid $gray-200; - i { font-size: 14px; color: $accent; } - h3 { margin: 0; font-size: 14px; font-weight: 600; color: $gray-900; } + i { + font-size: 14px; + color: $accent; + } + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } } } @@ -37,7 +46,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .lpir-field { diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.html b/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.html index 1699b2a69..78499926d 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.html +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-settings-step/loan-product-settings-step.component.html @@ -7,12 +7,14 @@ -->
-
-

{{ 'labels.inputs.products.loan.Amortization' | translate }} & {{ 'labels.inputs.Interest Method' | translate }}

+

+ {{ 'labels.inputs.products.loan.Amortization' | translate }} & + {{ 'labels.inputs.Interest Method' | translate }} +

@@ -60,7 +62,10 @@ matTooltip="{{ 'tooltips.Daily - Will Calculate the interest' | translate }}" required > - @for (interestCalculationPeriodType of interestCalculationPeriodTypeData; track interestCalculationPeriodType) { + @for ( + interestCalculationPeriodType of interestCalculationPeriodTypeData; + track interestCalculationPeriodType + ) { {{ interestCalculationPeriodType.value | translateKey: 'catalogs' }} @@ -118,7 +123,10 @@ required [disabled]="advancedTransactionProcessingStrategyDisabled" > - @for (transactionProcessingStrategy of transactionProcessingStrategyData; track transactionProcessingStrategy) { + @for ( + transactionProcessingStrategy of transactionProcessingStrategyData; + track transactionProcessingStrategy + ) { {{ transactionProcessingStrategy.name | translateKey: 'catalogs' }} @@ -374,7 +382,10 @@

{{ 'labels.inputs.Moratorium' | translate }} - +

@@ -418,7 +429,9 @@
-

{{ 'labels.inputs.Number of days a loan may be overdue before moving into arrears' | translate }}

+

+ {{ 'labels.inputs.Number of days a loan may be overdue before moving into arrears' | translate }} +

@@ -432,10 +445,7 @@
- + {{ 'labels.inputs.Account moves out of NPA only after all arrears have been cleared' | translate }}
@@ -729,20 +739,14 @@ }
- + {{ 'labels.inputs.Is Arrears recognization based on original schedule' | translate }}?
@if (loanProductSettingsForm.value.loanScheduleType === 'PROGRESSIVE') {
- + {{ 'labels.inputs.Do not calculate interest on past due principal balances' | translate }}
@@ -791,7 +795,10 @@

{{ 'labels.heading.Event Settings' | translate }} - +

@@ -889,5 +896,4 @@ }
- diff --git a/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.html b/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.html index adc71db05..1912b6b49 100644 --- a/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.html +++ b/src/app/products/loan-products/loan-product-stepper/loan-product-terms-step/loan-product-terms-step.component.html @@ -7,7 +7,6 @@ -->
-
@@ -495,7 +494,11 @@ mat-icon-button color="primary" (click)=" - editVariationsForBorrowerCycle('NumberOfRepayments', numberOfRepaymentVariationsForBorrowerCycle, i) + editVariationsForBorrowerCycle( + 'NumberOfRepayments', + numberOfRepaymentVariationsForBorrowerCycle, + i + ) " > @@ -655,5 +658,4 @@
-
diff --git a/src/app/products/manage-delinquency-buckets/delinquency-bucket/create-bucket/create-bucket.component.html b/src/app/products/manage-delinquency-buckets/delinquency-bucket/create-bucket/create-bucket.component.html index 45a1cd4d5..2c4f8daf1 100644 --- a/src/app/products/manage-delinquency-buckets/delinquency-bucket/create-bucket/create-bucket.component.html +++ b/src/app/products/manage-delinquency-buckets/delinquency-bucket/create-bucket/create-bucket.component.html @@ -49,7 +49,12 @@
- +
@@ -203,7 +249,9 @@ - + @@ -219,7 +267,9 @@ - + diff --git a/src/app/system/audit-trails/audit-trails.component.scss b/src/app/system/audit-trails/audit-trails.component.scss index 2c7b55719..3963eeae5 100644 --- a/src/app/system/audit-trails/audit-trails.component.scss +++ b/src/app/system/audit-trails/audit-trails.component.scss @@ -6,6 +6,7 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +@use 'sass:color'; @use '../../shared/styles/list-layout' as *; $blue: #1353a0; @@ -90,7 +91,7 @@ $muted: #64748b; box-shadow: 0 1px 4px rgb(19 83 160 / 25%); &:hover { - background: darken($blue, 6%); + background: color.adjust($blue, $lightness: -6%); } } } diff --git a/src/app/system/codes/codes.component.html b/src/app/system/codes/codes.component.html index 5646f1ce4..8530ca49f 100644 --- a/src/app/system/codes/codes.component.html +++ b/src/app/system/codes/codes.component.html @@ -24,7 +24,13 @@
- diff --git a/src/app/system/codes/create-code/create-code.component.scss b/src/app/system/codes/create-code/create-code.component.scss index 1e650846b..6b03edc7b 100644 --- a/src/app/system/codes/create-code/create-code.component.scss +++ b/src/app/system/codes/create-code/create-code.component.scss @@ -44,9 +44,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -92,7 +97,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .cc-field { @@ -128,6 +135,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/system/codes/edit-code/edit-code.component.scss b/src/app/system/codes/edit-code/edit-code.component.scss index 681177af7..2552f62e0 100644 --- a/src/app/system/codes/edit-code/edit-code.component.scss +++ b/src/app/system/codes/edit-code/edit-code.component.scss @@ -44,9 +44,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -92,7 +97,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .ec-field { @@ -128,6 +135,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/system/codes/view-code/view-code.component.html b/src/app/system/codes/view-code/view-code.component.html index a252e7c19..80160fe68 100644 --- a/src/app/system/codes/view-code/view-code.component.html +++ b/src/app/system/codes/view-code/view-code.component.html @@ -17,13 +17,25 @@
@if (!codeData.systemDefined) { - } @if (!codeData.systemDefined) { - @@ -88,11 +100,20 @@ @for (codeValue of codeValues.controls; track codeValue; let i = $index) { -
+
- + @if (codeValues.at(i).controls.name.hasError('required')) { {{ 'labels.inputs.name' | translate }} {{ 'labels.commons.is' | translate }} @@ -103,12 +124,23 @@
- +
- + @if (codeValues.at(i).controls.position.hasError('required')) { {{ 'labels.inputs.Position' | translate }} {{ 'labels.commons.is' | translate }} diff --git a/src/app/system/codes/view-code/view-code.component.scss b/src/app/system/codes/view-code/view-code.component.scss index 9e71b637e..949e5145f 100644 --- a/src/app/system/codes/view-code/view-code.component.scss +++ b/src/app/system/codes/view-code/view-code.component.scss @@ -73,9 +73,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -125,7 +130,9 @@ $gray-900: #202124; padding: 10px 0; border-bottom: 1px solid #f1f5f9; - &:last-child { border-bottom: none; } + &:last-child { + border-bottom: none; + } &__label { flex: 0 0 40%; @@ -149,10 +156,17 @@ $gray-900: #202124; font-weight: 600; font-size: 13px; - i { font-size: 11px; } + i { + font-size: 11px; + } - &--yes { color: #16a34a; } - &--no { color: #dc2626; } + &--yes { + color: #16a34a; + } + + &--no { + color: #dc2626; + } } // Add button in section header @@ -192,9 +206,17 @@ $gray-900: #202124; min-width: 700px; transition: background 0.1s; - &:last-child { border-bottom: none; } - &:nth-child(even) { background: #fafbfc; } - &:hover { background: #f8fafc; } + &:last-child { + border-bottom: none; + } + + &:nth-child(even) { + background: #fafbfc; + } + + &:hover { + background: #f8fafc; + } &--editing { background: #eff6ff !important; @@ -208,10 +230,25 @@ $gray-900: #202124; } .vc-values-col { - &--name { flex: 0 0 22%; padding-right: 8px; } - &--desc { flex: 0 0 26%; padding-right: 8px; } - &--pos { flex: 0 0 14%; padding-right: 8px; } - &--active { flex: 0 0 12%; padding-right: 8px; } + &--name { + flex: 0 0 22%; + padding-right: 8px; + } + + &--desc { + flex: 0 0 26%; + padding-right: 8px; + } + + &--pos { + flex: 0 0 14%; + padding-right: 8px; + } + + &--active { + flex: 0 0 12%; + padding-right: 8px; + } &--actions { flex: 0 0 26%; @@ -226,7 +263,9 @@ $gray-900: #202124; width: 100%; ::ng-deep { - .mat-mdc-form-field-flex { height: 36px; } + .mat-mdc-form-field-flex { + height: 36px; + } .mat-mdc-form-field-infix { padding: 4px 0; @@ -234,11 +273,18 @@ $gray-900: #202124; } .mdc-notched-outline { - .mdc-notched-outline__leading { border-radius: 6px 0 0 6px; } - .mdc-notched-outline__trailing { border-radius: 0 6px 6px 0; } + .mdc-notched-outline__leading { + border-radius: 6px 0 0 6px; + } + + .mdc-notched-outline__trailing { + border-radius: 0 6px 6px 0; + } } - input { font-size: 13px; } + input { + font-size: 13px; + } } } diff --git a/src/app/system/roles-and-permissions/add-role/add-role.component.scss b/src/app/system/roles-and-permissions/add-role/add-role.component.scss index de51f75a0..4578bb785 100644 --- a/src/app/system/roles-and-permissions/add-role/add-role.component.scss +++ b/src/app/system/roles-and-permissions/add-role/add-role.component.scss @@ -44,9 +44,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -92,7 +97,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .ar-field { @@ -128,6 +135,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/system/roles-and-permissions/edit-role/edit-role.component.html b/src/app/system/roles-and-permissions/edit-role/edit-role.component.html index 00082819d..081f534de 100644 --- a/src/app/system/roles-and-permissions/edit-role/edit-role.component.html +++ b/src/app/system/roles-and-permissions/edit-role/edit-role.component.html @@ -48,7 +48,13 @@ -
diff --git a/src/app/system/roles-and-permissions/edit-role/edit-role.component.scss b/src/app/system/roles-and-permissions/edit-role/edit-role.component.scss index 960ff2333..70292ff84 100644 --- a/src/app/system/roles-and-permissions/edit-role/edit-role.component.scss +++ b/src/app/system/roles-and-permissions/edit-role/edit-role.component.scss @@ -44,9 +44,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -92,7 +97,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .er-field { @@ -128,6 +135,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/system/roles-and-permissions/view-role/view-role.component.html b/src/app/system/roles-and-permissions/view-role/view-role.component.html index ef64984ce..f1842258b 100644 --- a/src/app/system/roles-and-permissions/view-role/view-role.component.html +++ b/src/app/system/roles-and-permissions/view-role/view-role.component.html @@ -19,31 +19,45 @@
- @if (!isRoleEnable(rolePermissionService.disabled)) { - } @if (isRoleEnable(rolePermissionService.disabled)) { - } - @@ -54,7 +68,9 @@
-

{{ 'labels.buttons.Permissions' | translate }}: {{ formatName(previousGrouping) }}

+

+ {{ 'labels.buttons.Permissions' | translate }}: {{ formatName(previousGrouping) }} +

@if (!isDisabled) {
- } - +
@if (group.expanded) {
@for (cat of group.categories; track cat) { -
+ (click)="showPermissions(cat)" + > {{ formatName(cat) }} @if (!isDisabled) { - - } @@ -113,20 +151,33 @@
- @if (permissionSubGroups.length > 0) { @for (subGroup of permissionSubGroups; track subGroup.entity) {
- + {{ getEntityDisplayName(subGroup.entity) }} {{ getSubGroupCount(subGroup) }} @if (!isDisabled) { - - } @@ -137,10 +188,20 @@
Maker @if (!isDisabled && subGroup.makerPerms.length > 0) { - - } @@ -162,10 +223,20 @@
Checker @if (!isDisabled && subGroup.checkerPerms.length > 0) { - - } @@ -194,10 +265,20 @@
Maker @if (!isDisabled && flatMakerPerms.length > 0) { - - } @@ -216,10 +297,20 @@
Checker @if (!isDisabled && flatCheckerPerms.length > 0) { - - } @@ -239,14 +330,18 @@
} -
@if (!isDisabled) {

{{ 'labels.heading.Manage Roles and Permissions' | translate }}

-

{{ 'labels.text.Define or modify roles and associated permissions' | translate }}

+

+ {{ 'labels.text.Define or modify roles and associated permissions' | translate }} +

@@ -147,7 +149,9 @@

{{ 'labels.heading.Account Number Preferences' | translate }}

-

{{ 'labels.text.Preferences for generating account numbers for client' | translate }}

+

+ {{ 'labels.text.Preferences for generating account numbers for client' | translate }} +

@@ -207,7 +211,9 @@

{{ 'labels.heading.Manage External Events' | translate }}

-

{{ 'labels.text.External Events configuration, to enable or disable' | translate }}

+

+ {{ 'labels.text.External Events configuration, to enable or disable' | translate }} +

@@ -219,7 +225,9 @@

{{ 'labels.heading.System Information' | translate }}

-

{{ 'labels.text.View system version, server and licensing information' | translate }}

+

+ {{ 'labels.text.View system version, server and licensing information' | translate }} +

@@ -231,9 +239,15 @@

{{ 'labels.heading.Manage Data Tables' | translate }}

{{ 'labels.text.Mifos X data-tables allow the user to add custom fields' | translate }}

- - - + + +
@@ -241,9 +255,15 @@

{{ 'labels.heading.Manage Codes' | translate }}

{{ 'labels.text.Selection you can create new drop-downs and edit' | translate }}

- - - + + +
@@ -251,9 +271,15 @@

{{ 'labels.heading.Manage Roles and Permissions' | translate }}

{{ 'labels.text.Roles and Permissions allow the administrator to control access' | translate }}

- - - + + +
@@ -261,9 +287,15 @@

{{ 'labels.heading.Configure Maker-Checker Tasks' | translate }}

{{ 'labels.text."Maker-Checker" principle requires every tasks' | translate }}

- - - + + +
@@ -271,9 +303,15 @@

{{ 'labels.heading.Configuration' | translate }}

{{ 'labels.text.Configurations page allows you to further customize' | translate }}

- - - + + +
@@ -281,9 +319,15 @@

{{ 'labels.heading.Manage Scheduler Jobs' | translate }}

{{ 'labels.text.Scheduler Jobs can set batch jobs' | translate }}

- - - + + +
@@ -291,8 +335,14 @@

{{ 'labels.heading.Manage Reports' | translate }}

{{ 'labels.text.Add customized reports and edit core reports' | translate }}

- - - + + +
diff --git a/src/app/system/system.component.scss b/src/app/system/system.component.scss index 7408fded2..0fbc1822e 100644 --- a/src/app/system/system.component.scss +++ b/src/app/system/system.component.scss @@ -105,17 +105,60 @@ $gray-900: #0f172a; font-size: 15px; flex-shrink: 0; - &--blue { background: rgb(59 130 246 / 10%); color: #2563eb; } - &--amber { background: rgb(245 158 11 / 10%); color: #d97706; } - &--green { background: rgb(34 197 94 / 10%); color: #16a34a; } - &--emerald { background: rgb(16 185 129 / 10%); color: #059669; } - &--purple { background: rgb(139 92 246 / 10%); color: #7c3aed; } - &--indigo { background: rgb(99 102 241 / 10%); color: #4f46e5; } - &--slate { background: rgb(100 116 139 / 10%); color: #475569; } - &--orange { background: rgb(249 115 22 / 10%); color: #ea580c; } - &--teal { background: rgb(20 184 166 / 10%); color: #0d9488; } - &--cyan { background: rgb(6 182 212 / 10%); color: #0891b2; } - &--rose { background: rgb(244 63 94 / 10%); color: #e11d48; } + &--blue { + background: rgb(59 130 246 / 10%); + color: #2563eb; + } + + &--amber { + background: rgb(245 158 11 / 10%); + color: #d97706; + } + + &--green { + background: rgb(34 197 94 / 10%); + color: #16a34a; + } + + &--emerald { + background: rgb(16 185 129 / 10%); + color: #059669; + } + + &--purple { + background: rgb(139 92 246 / 10%); + color: #7c3aed; + } + + &--indigo { + background: rgb(99 102 241 / 10%); + color: #4f46e5; + } + + &--slate { + background: rgb(100 116 139 / 10%); + color: #475569; + } + + &--orange { + background: rgb(249 115 22 / 10%); + color: #ea580c; + } + + &--teal { + background: rgb(20 184 166 / 10%); + color: #0d9488; + } + + &--cyan { + background: rgb(6 182 212 / 10%); + color: #0891b2; + } + + &--rose { + background: rgb(244 63 94 / 10%); + color: #e11d48; + } } &__body { @@ -152,8 +195,13 @@ $gray-900: #0f172a; // ── Dark mode ── :host-context(.dark-theme) { .sys-header { - &__title { color: #e2e8f0; } - &__sub { color: #94a3b8; } + &__title { + color: #e2e8f0; + } + + &__sub { + color: #94a3b8; + } } .sys-card { @@ -165,8 +213,16 @@ $gray-900: #0f172a; border-color: #444; } - &__title { color: #e2e8f0; } - &__desc { color: #64748b; } - &__arrow { color: #444; } + &__title { + color: #e2e8f0; + } + + &__desc { + color: #64748b; + } + + &__arrow { + color: #444; + } } } diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.scss b/src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.scss index bcb3e38fa..c230c65ba 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.scss +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/checker-inbox/checker-inbox.component.scss @@ -32,7 +32,9 @@ $text-muted: #64748b; max-width: 320px; ::ng-deep { - .mat-mdc-form-field-flex { height: 36px; } + .mat-mdc-form-field-flex { + height: 36px; + } .mat-mdc-form-field-infix { padding: 6px 0; @@ -45,8 +47,13 @@ $text-muted: #64748b; } .mdc-notched-outline { - .mdc-notched-outline__leading { border-radius: 8px 0 0 8px; } - .mdc-notched-outline__trailing { border-radius: 0 8px 8px 0; } + .mdc-notched-outline__leading { + border-radius: 8px 0 0 8px; + } + + .mdc-notched-outline__trailing { + border-radius: 0 8px 8px 0; + } } .mat-mdc-form-field-icon-prefix { @@ -55,7 +62,9 @@ $text-muted: #64748b; font-size: 16px; } - input { font-size: 13px; } + input { + font-size: 13px; + } } } @@ -68,7 +77,9 @@ $text-muted: #64748b; color: $text-muted; background: #fff; - ::ng-deep .mat-mdc-button-persistent-ripple { border-radius: 8px; } + ::ng-deep .mat-mdc-button-persistent-ripple { + border-radius: 8px; + } } .cv-action-buttons { @@ -153,12 +164,19 @@ $text-muted: #64748b; } .mdc-notched-outline { - .mdc-notched-outline__leading { border-radius: 8px 0 0 8px; } - .mdc-notched-outline__trailing { border-radius: 0 8px 8px 0; } + .mdc-notched-outline__leading { + border-radius: 8px 0 0 8px; + } + + .mdc-notched-outline__trailing { + border-radius: 0 8px 8px 0; + } } input, - .mat-mdc-select-trigger { font-size: 13px; } + .mat-mdc-select-trigger { + font-size: 13px; + } } } @@ -183,7 +201,9 @@ $text-muted: #64748b; margin-left: 0; justify-content: stretch; - .cv-btn { flex: 1; } + .cv-btn { + flex: 1; + } } .cv-advanced-search { diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.html b/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.html index 431c1eedf..7c79d01ee 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.html +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.html @@ -62,18 +62,34 @@
- - - +
{{ 'labels.inputs.Classification' | translate }} diff --git a/src/app/products/manage-delinquency-buckets/delinquency-bucket/create-bucket/create-bucket.component.scss b/src/app/products/manage-delinquency-buckets/delinquency-bucket/create-bucket/create-bucket.component.scss index 4ba22656a..ad249f760 100644 --- a/src/app/products/manage-delinquency-buckets/delinquency-bucket/create-bucket/create-bucket.component.scss +++ b/src/app/products/manage-delinquency-buckets/delinquency-bucket/create-bucket/create-bucket.component.scss @@ -44,9 +44,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -92,7 +97,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .db-field { @@ -113,7 +120,10 @@ $gray-900: #202124; padding: 0 14px !important; box-shadow: 0 1px 3px rgba($primary, 0.25) !important; - fa-icon { margin-right: 6px; font-size: 11px; } + fa-icon { + margin-right: 6px; + font-size: 11px; + } } // Ranges table @@ -149,7 +159,9 @@ $gray-900: #202124; height: 40px; border-bottom: 1px solid #f1f5f9; - &:last-child { border-bottom: none; } + &:last-child { + border-bottom: none; + } } .mat-mdc-cell { @@ -208,6 +220,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/products/manage-delinquency-buckets/delinquency-bucket/edit-bucket/edit-bucket.component.html b/src/app/products/manage-delinquency-buckets/delinquency-bucket/edit-bucket/edit-bucket.component.html index 6b882d129..88c4f6073 100644 --- a/src/app/products/manage-delinquency-buckets/delinquency-bucket/edit-bucket/edit-bucket.component.html +++ b/src/app/products/manage-delinquency-buckets/delinquency-bucket/edit-bucket/edit-bucket.component.html @@ -49,7 +49,12 @@
- +
diff --git a/src/app/products/manage-delinquency-buckets/delinquency-bucket/edit-bucket/edit-bucket.component.scss b/src/app/products/manage-delinquency-buckets/delinquency-bucket/edit-bucket/edit-bucket.component.scss index 4ba22656a..ad249f760 100644 --- a/src/app/products/manage-delinquency-buckets/delinquency-bucket/edit-bucket/edit-bucket.component.scss +++ b/src/app/products/manage-delinquency-buckets/delinquency-bucket/edit-bucket/edit-bucket.component.scss @@ -44,9 +44,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -92,7 +97,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .db-field { @@ -113,7 +120,10 @@ $gray-900: #202124; padding: 0 14px !important; box-shadow: 0 1px 3px rgba($primary, 0.25) !important; - fa-icon { margin-right: 6px; font-size: 11px; } + fa-icon { + margin-right: 6px; + font-size: 11px; + } } // Ranges table @@ -149,7 +159,9 @@ $gray-900: #202124; height: 40px; border-bottom: 1px solid #f1f5f9; - &:last-child { border-bottom: none; } + &:last-child { + border-bottom: none; + } } .mat-mdc-cell { @@ -208,6 +220,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/products/manage-delinquency-buckets/delinquency-bucket/view-bucket/view-bucket.component.html b/src/app/products/manage-delinquency-buckets/delinquency-bucket/view-bucket/view-bucket.component.html index 52bda2454..d2a2d575c 100644 --- a/src/app/products/manage-delinquency-buckets/delinquency-bucket/view-bucket/view-bucket.component.html +++ b/src/app/products/manage-delinquency-buckets/delinquency-bucket/view-bucket/view-bucket.component.html @@ -16,7 +16,13 @@

{{ delinquencyBucketData.name }}

- @@ -59,7 +65,9 @@ @if (delinquencyBucketData.ranges && delinquencyBucketData.ranges.length > 0) {
- {{ 'labels.inputs.Classification' | translate }} {{ 'labels.inputs.Name' | translate }} + {{ 'labels.inputs.Classification' | translate }} {{ 'labels.inputs.Name' | translate }} {{ 'labels.inputs.Days From' | translate }} {{ 'labels.inputs.Days Till' | translate }}
diff --git a/src/app/products/manage-delinquency-buckets/delinquency-bucket/view-bucket/view-bucket.component.scss b/src/app/products/manage-delinquency-buckets/delinquency-bucket/view-bucket/view-bucket.component.scss index 76f8a511d..16429e7af 100644 --- a/src/app/products/manage-delinquency-buckets/delinquency-bucket/view-bucket/view-bucket.component.scss +++ b/src/app/products/manage-delinquency-buckets/delinquency-bucket/view-bucket/view-bucket.component.scss @@ -72,9 +72,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -127,7 +132,9 @@ $gray-900: #202124; padding: 10px 0; border-bottom: 1px solid #f1f3f4; - &:last-child { border-bottom: none; } + &:last-child { + border-bottom: none; + } &__label { flex: 0 0 40%; @@ -170,9 +177,13 @@ $gray-900: #202124; border-bottom: 1px solid #f1f5f9; transition: background 0.1s; - &:last-child { border-bottom: none; } + &:last-child { + border-bottom: none; + } - &:hover { background: #f8fafc; } + &:hover { + background: #f8fafc; + } .vb-ranges-cell { font-size: 13px; diff --git a/src/app/products/manage-delinquency-buckets/delinquency-range/create-range/create-range.component.scss b/src/app/products/manage-delinquency-buckets/delinquency-range/create-range/create-range.component.scss index c191f95f5..48c17f275 100644 --- a/src/app/products/manage-delinquency-buckets/delinquency-range/create-range/create-range.component.scss +++ b/src/app/products/manage-delinquency-buckets/delinquency-range/create-range/create-range.component.scss @@ -6,34 +6,76 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -.cr-container { max-width: 960px; margin: 0 auto; padding: 24px 16px; } +.cr-container { + max-width: 960px; + margin: 0 auto; + padding: 24px 16px; +} .cr-header { - display: flex; align-items: center; gap: 10px; margin-bottom: 20px; - &__icon { font-size: 20px; color: #1353a0; } - &__title { margin: 0; font-size: 22px; font-weight: 500; } + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 20px; + + &__icon { + font-size: 20px; + color: #1353a0; + } + + &__title { + margin: 0; + font-size: 22px; + font-weight: 500; + } } .cr-section { - border: 1px solid #e2e8f0; border-radius: 10px; overflow: hidden; background: #fff; + border: 1px solid #e2e8f0; + border-radius: 10px; + overflow: hidden; + background: #fff; &__header { - display: flex; align-items: center; gap: 10px; padding: 12px 24px; - background: #f8f9fa; border-bottom: 1px solid #e8eaed; - i { font-size: 14px; color: #0ea5e9; } - h3 { margin: 0; font-size: 14px; font-weight: 600; color: #202124; } + display: flex; + align-items: center; + gap: 10px; + padding: 12px 24px; + background: #f8f9fa; + border-bottom: 1px solid #e8eaed; + + i { + font-size: 14px; + color: #0ea5e9; + } + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: #202124; + } } } .cr-grid { - display: grid; grid-template-columns: repeat(2, 1fr); gap: 4px 16px; padding: 16px 24px; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px 16px; + padding: 16px 24px; } .cr-action-bar { - display: flex; justify-content: flex-end; gap: 12px; margin-top: 20px; padding: 16px 0; + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 20px; + padding: 16px 0; border-top: 1px solid rgb(0 0 0 / 8%); } @media (width <= 600px) { - .cr-grid { grid-template-columns: 1fr; } + .cr-grid { + grid-template-columns: 1fr; + } } diff --git a/src/app/products/manage-delinquency-buckets/delinquency-range/delinquency-range.component.html b/src/app/products/manage-delinquency-buckets/delinquency-range/delinquency-range.component.html index e4a99d247..4e56b3d84 100644 --- a/src/app/products/manage-delinquency-buckets/delinquency-range/delinquency-range.component.html +++ b/src/app/products/manage-delinquency-buckets/delinquency-range/delinquency-range.component.html @@ -23,7 +23,13 @@
- diff --git a/src/app/products/manage-delinquency-buckets/delinquency-range/edit-range/edit-range.component.html b/src/app/products/manage-delinquency-buckets/delinquency-range/edit-range/edit-range.component.html index e8f1fa05d..cdb5aa732 100644 --- a/src/app/products/manage-delinquency-buckets/delinquency-range/edit-range/edit-range.component.html +++ b/src/app/products/manage-delinquency-buckets/delinquency-range/edit-range/edit-range.component.html @@ -9,7 +9,9 @@
-

{{ 'labels.buttons.Edit' | translate }} {{ 'labels.inputs.Delinquency Range' | translate }}

+

+ {{ 'labels.buttons.Edit' | translate }} {{ 'labels.inputs.Delinquency Range' | translate }} +

diff --git a/src/app/products/manage-delinquency-buckets/delinquency-range/edit-range/edit-range.component.scss b/src/app/products/manage-delinquency-buckets/delinquency-range/edit-range/edit-range.component.scss index 53d95280c..cd48679b9 100644 --- a/src/app/products/manage-delinquency-buckets/delinquency-range/edit-range/edit-range.component.scss +++ b/src/app/products/manage-delinquency-buckets/delinquency-range/edit-range/edit-range.component.scss @@ -6,34 +6,76 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -.er-container { max-width: 960px; margin: 0 auto; padding: 24px 16px; } +.er-container { + max-width: 960px; + margin: 0 auto; + padding: 24px 16px; +} .er-header { - display: flex; align-items: center; gap: 10px; margin-bottom: 20px; - &__icon { font-size: 20px; color: #1353a0; } - &__title { margin: 0; font-size: 22px; font-weight: 500; } + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 20px; + + &__icon { + font-size: 20px; + color: #1353a0; + } + + &__title { + margin: 0; + font-size: 22px; + font-weight: 500; + } } .er-section { - border: 1px solid #e2e8f0; border-radius: 10px; overflow: hidden; background: #fff; + border: 1px solid #e2e8f0; + border-radius: 10px; + overflow: hidden; + background: #fff; &__header { - display: flex; align-items: center; gap: 10px; padding: 12px 24px; - background: #f8f9fa; border-bottom: 1px solid #e8eaed; - i { font-size: 14px; color: #0ea5e9; } - h3 { margin: 0; font-size: 14px; font-weight: 600; color: #202124; } + display: flex; + align-items: center; + gap: 10px; + padding: 12px 24px; + background: #f8f9fa; + border-bottom: 1px solid #e8eaed; + + i { + font-size: 14px; + color: #0ea5e9; + } + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: #202124; + } } } .er-grid { - display: grid; grid-template-columns: repeat(2, 1fr); gap: 4px 16px; padding: 16px 24px; + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 4px 16px; + padding: 16px 24px; } .er-action-bar { - display: flex; justify-content: flex-end; gap: 12px; margin-top: 20px; padding: 16px 0; + display: flex; + justify-content: flex-end; + gap: 12px; + margin-top: 20px; + padding: 16px 0; border-top: 1px solid rgb(0 0 0 / 8%); } @media (width <= 600px) { - .er-grid { grid-template-columns: 1fr; } + .er-grid { + grid-template-columns: 1fr; + } } diff --git a/src/app/products/manage-delinquency-buckets/delinquency-range/view-range/view-range.component.html b/src/app/products/manage-delinquency-buckets/delinquency-range/view-range/view-range.component.html index b5604a173..7bf7dad91 100644 --- a/src/app/products/manage-delinquency-buckets/delinquency-range/view-range/view-range.component.html +++ b/src/app/products/manage-delinquency-buckets/delinquency-range/view-range/view-range.component.html @@ -13,11 +13,21 @@

{{ delinquencyRangeData.classification }}

- - diff --git a/src/app/products/manage-delinquency-buckets/delinquency-range/view-range/view-range.component.scss b/src/app/products/manage-delinquency-buckets/delinquency-range/view-range/view-range.component.scss index c2b9146e6..c6fe89e2f 100644 --- a/src/app/products/manage-delinquency-buckets/delinquency-range/view-range/view-range.component.scss +++ b/src/app/products/manage-delinquency-buckets/delinquency-range/view-range/view-range.component.scss @@ -6,38 +6,94 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -.vr-container { max-width: 960px; margin: 0 auto; padding: 24px 16px; } +.vr-container { + max-width: 960px; + margin: 0 auto; + padding: 24px 16px; +} .vr-top-bar { - display: flex; align-items: center; justify-content: space-between; - flex-wrap: wrap; gap: 12px; margin-bottom: 20px; + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 20px; - &__actions { display: flex; gap: 12px; } + &__actions { + display: flex; + gap: 12px; + } } .vr-header { - display: flex; align-items: center; gap: 10px; - &__icon { font-size: 20px; color: #1353a0; } - &__title { margin: 0; font-size: 22px; font-weight: 500; } + display: flex; + align-items: center; + gap: 10px; + + &__icon { + font-size: 20px; + color: #1353a0; + } + + &__title { + margin: 0; + font-size: 22px; + font-weight: 500; + } } .vr-section { - border: 1px solid #e2e8f0; border-radius: 10px; overflow: hidden; background: #fff; + border: 1px solid #e2e8f0; + border-radius: 10px; + overflow: hidden; + background: #fff; &__header { - display: flex; align-items: center; gap: 10px; padding: 12px 24px; - background: #f8f9fa; border-bottom: 1px solid #e8eaed; - i { font-size: 14px; color: #0ea5e9; } - h3 { margin: 0; font-size: 14px; font-weight: 600; color: #202124; } + display: flex; + align-items: center; + gap: 10px; + padding: 12px 24px; + background: #f8f9fa; + border-bottom: 1px solid #e8eaed; + + i { + font-size: 14px; + color: #0ea5e9; + } + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: #202124; + } } - &__body { padding: 16px 24px; } + &__body { + padding: 16px 24px; + } } .vr-row { - display: flex; padding: 6px 0; border-bottom: 1px solid #f1f3f4; - &:last-child { border-bottom: none; } + display: flex; + padding: 6px 0; + border-bottom: 1px solid #f1f3f4; + + &:last-child { + border-bottom: none; + } } -.vr-label { flex: 0 0 40%; font-size: 13px; font-weight: 500; color: #5f6368; } -.vr-value { flex: 1; font-size: 13px; color: #202124; } +.vr-label { + flex: 0 0 40%; + font-size: 13px; + font-weight: 500; + color: #5f6368; +} + +.vr-value { + flex: 1; + font-size: 13px; + color: #202124; +} diff --git a/src/app/products/manage-delinquency-buckets/manage-delinquency-buckets.component.scss b/src/app/products/manage-delinquency-buckets/manage-delinquency-buckets.component.scss index cc835be50..4398e1cca 100644 --- a/src/app/products/manage-delinquency-buckets/manage-delinquency-buckets.component.scss +++ b/src/app/products/manage-delinquency-buckets/manage-delinquency-buckets.component.scss @@ -6,20 +6,48 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -.mdb-container { max-width: 960px; margin: 0 auto; padding: 24px 16px; } - -.mdb-header { - display: flex; align-items: center; gap: 10px; margin-bottom: 20px; - &__icon { font-size: 20px; color: #1353a0; } - &__title { margin: 0; font-size: 22px; font-weight: 500; } +.mdb-container { + max-width: 960px; + margin: 0 auto; + padding: 24px 16px; } -.mdb-cards { display: flex; flex-direction: column; gap: 12px; } +.mdb-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 20px; + + &__icon { + font-size: 20px; + color: #1353a0; + } + + &__title { + margin: 0; + font-size: 22px; + font-weight: 500; + } +} + +.mdb-cards { + display: flex; + flex-direction: column; + gap: 12px; +} .mdb-card { - display: flex; align-items: center; gap: 16px; padding: 16px 24px; - border: 1px solid #e2e8f0; border-radius: 10px; background: #fff; - cursor: pointer; transition: box-shadow 0.2s, border-color 0.2s; + display: flex; + align-items: center; + gap: 16px; + padding: 16px 24px; + border: 1px solid #e2e8f0; + border-radius: 10px; + background: #fff; + cursor: pointer; + transition: + box-shadow 0.2s, + border-color 0.2s; &:hover { border-color: #0ea5e9; @@ -27,16 +55,36 @@ } &__icon-wrap { - display: flex; align-items: center; justify-content: center; - width: 40px; height: 40px; border-radius: 8px; - background: #f0f9ff; color: #0ea5e9; font-size: 16px; + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border-radius: 8px; + background: #f0f9ff; + color: #0ea5e9; + font-size: 16px; } &__content { flex: 1; - h3 { margin: 0; font-size: 14px; font-weight: 600; color: #202124; } - p { margin: 4px 0 0; font-size: 12px; color: #5f6368; } + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: #202124; + } + + p { + margin: 4px 0 0; + font-size: 12px; + color: #5f6368; + } } - &__arrow { font-size: 12px; color: #9ca3af; } + &__arrow { + font-size: 12px; + color: #9ca3af; + } } diff --git a/src/app/products/manage-tax-components/create-tax-component/create-tax-component.component.scss b/src/app/products/manage-tax-components/create-tax-component/create-tax-component.component.scss index ddf54f2c6..b2ce5d857 100644 --- a/src/app/products/manage-tax-components/create-tax-component/create-tax-component.component.scss +++ b/src/app/products/manage-tax-components/create-tax-component/create-tax-component.component.scss @@ -44,9 +44,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -92,7 +97,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .ctc-field { @@ -128,6 +135,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/products/manage-tax-components/edit-tax-component/edit-tax-component.component.scss b/src/app/products/manage-tax-components/edit-tax-component/edit-tax-component.component.scss index 4eddbd8f5..880003338 100644 --- a/src/app/products/manage-tax-components/edit-tax-component/edit-tax-component.component.scss +++ b/src/app/products/manage-tax-components/edit-tax-component/edit-tax-component.component.scss @@ -44,9 +44,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -92,7 +97,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .etc-field { @@ -128,6 +135,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/products/manage-tax-components/manage-tax-components.component.html b/src/app/products/manage-tax-components/manage-tax-components.component.html index 8e9a15b80..c1f553d12 100644 --- a/src/app/products/manage-tax-components/manage-tax-components.component.html +++ b/src/app/products/manage-tax-components/manage-tax-components.component.html @@ -23,7 +23,13 @@
- diff --git a/src/app/products/manage-tax-components/view-tax-component/view-tax-component.component.html b/src/app/products/manage-tax-components/view-tax-component/view-tax-component.component.html index 89518fea8..7efef27a5 100644 --- a/src/app/products/manage-tax-components/view-tax-component/view-tax-component.component.html +++ b/src/app/products/manage-tax-components/view-tax-component/view-tax-component.component.html @@ -15,7 +15,13 @@

{{ taxComponentData.name }}

- diff --git a/src/app/products/manage-tax-components/view-tax-component/view-tax-component.component.scss b/src/app/products/manage-tax-components/view-tax-component/view-tax-component.component.scss index 5d3396259..9449cdf72 100644 --- a/src/app/products/manage-tax-components/view-tax-component/view-tax-component.component.scss +++ b/src/app/products/manage-tax-components/view-tax-component/view-tax-component.component.scss @@ -60,9 +60,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -81,7 +86,9 @@ $gray-900: #202124; padding: 20px 28px 24px; background: #fff; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .vtc-cell { diff --git a/src/app/products/manage-tax-configurations/manage-tax-configurations.component.html b/src/app/products/manage-tax-configurations/manage-tax-configurations.component.html index 3a8aa5ec6..b7c1718d7 100644 --- a/src/app/products/manage-tax-configurations/manage-tax-configurations.component.html +++ b/src/app/products/manage-tax-configurations/manage-tax-configurations.component.html @@ -14,7 +14,10 @@

{{ 'labels.heading.Manage Tax Configurations' | translate }}

-

{{ 'labels.heading.Define Tax Components' | translate }} & {{ 'labels.heading.Define Tax Groups' | translate }}

+

+ {{ 'labels.heading.Define Tax Components' | translate }} & + {{ 'labels.heading.Define Tax Groups' | translate }} +

diff --git a/src/app/products/manage-tax-configurations/manage-tax-configurations.component.scss b/src/app/products/manage-tax-configurations/manage-tax-configurations.component.scss index b107dad99..d3e9a2e05 100644 --- a/src/app/products/manage-tax-configurations/manage-tax-configurations.component.scss +++ b/src/app/products/manage-tax-configurations/manage-tax-configurations.component.scss @@ -78,7 +78,10 @@ $gray-900: #0f172a; border-radius: 10px; text-decoration: none; cursor: pointer; - transition: border-color 0.15s, box-shadow 0.15s, transform 0.12s; + transition: + border-color 0.15s, + box-shadow 0.15s, + transform 0.12s; &:hover { border-color: #cbd5e1; diff --git a/src/app/products/manage-tax-groups/create-tax-group/create-tax-group.component.scss b/src/app/products/manage-tax-groups/create-tax-group/create-tax-group.component.scss index 38a8f6a0a..0711862b6 100644 --- a/src/app/products/manage-tax-groups/create-tax-group/create-tax-group.component.scss +++ b/src/app/products/manage-tax-groups/create-tax-group/create-tax-group.component.scss @@ -45,9 +45,14 @@ $text-muted: #64748b; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -102,7 +107,9 @@ $text-muted: #64748b; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .ctg-field { @@ -143,7 +150,9 @@ $text-muted: #64748b; height: 36px; border-bottom: 1px solid #f1f5f9; - &:last-child { border-bottom: none; } + &:last-child { + border-bottom: none; + } } .mat-mdc-cell { @@ -192,6 +201,9 @@ $text-muted: #64748b; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/products/manage-tax-groups/edit-tax-group/edit-tax-group.component.scss b/src/app/products/manage-tax-groups/edit-tax-group/edit-tax-group.component.scss index 5558ddbaa..e436eb3d5 100644 --- a/src/app/products/manage-tax-groups/edit-tax-group/edit-tax-group.component.scss +++ b/src/app/products/manage-tax-groups/edit-tax-group/edit-tax-group.component.scss @@ -45,9 +45,14 @@ $text-muted: #64748b; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -102,7 +107,9 @@ $text-muted: #64748b; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .etg-field { @@ -143,7 +150,9 @@ $text-muted: #64748b; height: 36px; border-bottom: 1px solid #f1f5f9; - &:last-child { border-bottom: none; } + &:last-child { + border-bottom: none; + } } .mat-mdc-cell { @@ -192,6 +201,9 @@ $text-muted: #64748b; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/products/manage-tax-groups/manage-tax-groups.component.html b/src/app/products/manage-tax-groups/manage-tax-groups.component.html index 43e311431..3a0172e19 100644 --- a/src/app/products/manage-tax-groups/manage-tax-groups.component.html +++ b/src/app/products/manage-tax-groups/manage-tax-groups.component.html @@ -23,7 +23,13 @@
- diff --git a/src/app/products/manage-tax-groups/view-tax-group/view-tax-group.component.html b/src/app/products/manage-tax-groups/view-tax-group/view-tax-group.component.html index d954b17d7..b32dbd180 100644 --- a/src/app/products/manage-tax-groups/view-tax-group/view-tax-group.component.html +++ b/src/app/products/manage-tax-groups/view-tax-group/view-tax-group.component.html @@ -15,7 +15,13 @@

{{ taxGroupData.name }}

- diff --git a/src/app/products/manage-tax-groups/view-tax-group/view-tax-group.component.scss b/src/app/products/manage-tax-groups/view-tax-group/view-tax-group.component.scss index 107152abd..b947bd1ff 100644 --- a/src/app/products/manage-tax-groups/view-tax-group/view-tax-group.component.scss +++ b/src/app/products/manage-tax-groups/view-tax-group/view-tax-group.component.scss @@ -61,9 +61,14 @@ $text-muted: #64748b; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -109,7 +114,9 @@ $text-muted: #64748b; gap: 1px; padding: 20px 28px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .vtg-cell { diff --git a/src/app/products/products-mix/products-mix.component.html b/src/app/products/products-mix/products-mix.component.html index 18acae84b..725c8ceb0 100644 --- a/src/app/products/products-mix/products-mix.component.html +++ b/src/app/products/products-mix/products-mix.component.html @@ -24,8 +24,14 @@
-
@@ -40,12 +46,7 @@ - +
{{ 'labels.inputs.Classification' | translate }} {{ row.classification }}
diff --git a/src/app/products/products-mix/view-product-mix/view-product-mix.component.html b/src/app/products/products-mix/view-product-mix/view-product-mix.component.html index f529dacbe..bdff6e740 100644 --- a/src/app/products/products-mix/view-product-mix/view-product-mix.component.html +++ b/src/app/products/products-mix/view-product-mix/view-product-mix.component.html @@ -14,11 +14,11 @@
diff --git a/src/app/products/products-routing.module.ts b/src/app/products/products-routing.module.ts index 0c49649fd..c6cb43e70 100644 --- a/src/app/products/products-routing.module.ts +++ b/src/app/products/products-routing.module.ts @@ -49,6 +49,7 @@ import { EditTaxComponentComponent } from './manage-tax-components/edit-tax-comp import { ViewTaxGroupComponent } from './manage-tax-groups/view-tax-group/view-tax-group.component'; import { ShareProductsDividendsComponent } from './share-products/dividends-share-product/dividends.components'; import { CreateRecurringDepositProductComponent } from './recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component'; +import { BaseRecurringDepositInterestRateChartComponent } from './recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component'; import { CreateDividendComponent } from './share-products/create-dividend/create-dividend.component'; import { ViewFixedDepositProductComponent } from './fixed-deposit-products/view-fixed-deposit-product/view-fixed-deposit-product.component'; import { ViewDividendComponent } from './share-products/view-dividend/view-dividend.component'; @@ -129,6 +130,8 @@ import { FixedDepositGeneralTabComponent } from './fixed-deposit-products/view-f import { FixedDepositDatatableTabComponent } from './fixed-deposit-products/view-fixed-deposit-product/fixed-deposit-datatable-tab/fixed-deposit-datatable-tab.component'; import { RecurringDepositGeneralTabComponent } from './recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component'; import { RecurringDepositDatatableTabComponent } from './recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-datatable-tab/recurring-deposit-datatable-tab.component'; +import { RdInterestRatesTabComponent } from './recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates-tab.component'; +import { rdInterestRatesResolver } from './recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates.resolver'; import { ShareProductGeneralTabComponent } from './share-products/view-share-product/share-product-general-tab/share-product-general-tab.component'; import { ShareProductDatatableTabComponent } from './share-products/view-share-product/share-product-datatable-tab/share-product-datatable-tab.component'; import { ShareProductDatatablesResolver } from './share-products/share-product-datatables.resolver'; @@ -652,6 +655,14 @@ const routes: Routes = [ path: 'recurring-deposit-products', data: { title: 'Recurring Deposit Products', breadcrumb: 'Recurring Deposit Products' }, children: [ + { + path: 'base-interest-rates', + component: BaseRecurringDepositInterestRateChartComponent, + data: { title: 'CBS Base Interest Rates', breadcrumb: 'Base Interest Rates' }, + resolve: { + recurringDepositProductsTemplate: RecurringDepositProductsTemplateResolver + } + }, { path: 'create', component: CreateRecurringDepositProductComponent, @@ -704,6 +715,14 @@ const routes: Routes = [ } ] }, + { + path: 'interest-rates', + component: RdInterestRatesTabComponent, + data: { title: 'CBS Base Chart', breadcrumb: 'CBS Base Chart', routeParamBreadcrumb: false }, + resolve: { + rdInterestRates: rdInterestRatesResolver + } + }, { path: 'edit', data: { title: 'Edit Recurring Deposit Product', breadcrumb: 'edit', routeParamBreadcrumb: false }, diff --git a/src/app/products/products.component.html b/src/app/products/products.component.html index f61436ca1..b68ad5f2f 100644 --- a/src/app/products/products.component.html +++ b/src/app/products/products.component.html @@ -25,7 +25,9 @@

{{ 'labels.heading.Loan Products' | translate }}

-

{{ 'labels.text.Add new loan product or modify or inactivate loan product' | translate }}

+

+ {{ 'labels.text.Add new loan product or modify or inactivate loan product' | translate }} +

@@ -38,7 +40,9 @@

{{ 'labels.heading.Savings Products' | translate }}

-

{{ 'labels.text.Add new savings product or modify or inactivate savings product' | translate }}

+

+ {{ 'labels.text.Add new savings product or modify or inactivate savings product' | translate }} +

@@ -62,7 +66,9 @@

{{ 'labels.heading.Share Products' | translate }}

-

{{ 'labels.text.Add new share product or modify or inactivate share product' | translate }}

+

+ {{ 'labels.text.Add new share product or modify or inactivate share product' | translate }} +

@@ -82,13 +88,19 @@

{{ 'labels.heading.Charges' | translate }}

-

{{ 'labels.text.Define charges/penalties for loan products, savings and deposit products' | translate }}

+

+ {{ 'labels.text.Define charges/penalties for loan products, savings and deposit products' | translate }} +

@@ -129,13 +143,19 @@ - +

{{ 'labels.heading.Delinquency Buckets' | translate }}

-

{{ 'labels.text.Define delinquency day ranges and bucket set for loan products' | translate }}

+

+ {{ 'labels.text.Define delinquency day ranges and bucket set for loan products' | translate }} +

diff --git a/src/app/products/products.component.scss b/src/app/products/products.component.scss index 698497b5f..b7997935e 100644 --- a/src/app/products/products.component.scss +++ b/src/app/products/products.component.scss @@ -195,8 +195,13 @@ $gray-900: #0f172a; // ── Dark mode ── :host-context(.dark-theme) { .pd-header { - &__title { color: #e2e8f0; } - &__sub { color: #94a3b8; } + &__title { + color: #e2e8f0; + } + + &__sub { + color: #94a3b8; + } } .pd-card { @@ -208,8 +213,16 @@ $gray-900: #0f172a; border-color: #444; } - &__title { color: #e2e8f0; } - &__desc { color: #64748b; } - &__arrow { color: #444; } + &__title { + color: #e2e8f0; + } + + &__desc { + color: #64748b; + } + + &__arrow { + color: #444; + } } } diff --git a/src/app/products/products.module.ts b/src/app/products/products.module.ts index 27675d51e..d98b4d3a9 100644 --- a/src/app/products/products.module.ts +++ b/src/app/products/products.module.ts @@ -84,6 +84,7 @@ import { ViewTaxGroupComponent } from './manage-tax-groups/view-tax-group/view-t import { ShareProductsDividendsComponent } from './share-products/dividends-share-product/dividends.components'; import { ViewRecurringDepositProductComponent } from './recurring-deposit-products/view-recurring-deposit-product/view-recurring-deposit-product.component'; import { CreateRecurringDepositProductComponent } from './recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component'; +import { BaseRecurringDepositInterestRateChartComponent } from './recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component'; import { RecurringDepositProductAccountingStepComponent } from './recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component'; import { RecurringDepositProductChargesStepComponent } from './recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component'; import { RecurringDepositProductCurrencyStepComponent } from './recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component'; @@ -213,6 +214,7 @@ import { AdvancedAccountingMappingRuleComponent } from './loan-products/loan-pro DepositProductIncentiveFormDialogComponent, ViewRecurringDepositProductComponent, CreateRecurringDepositProductComponent, + BaseRecurringDepositInterestRateChartComponent, RecurringDepositProductAccountingStepComponent, RecurringDepositProductChargesStepComponent, RecurringDepositProductCurrencyStepComponent, diff --git a/src/app/products/products.service.ts b/src/app/products/products.service.ts index 97daf2e91..82e0f2035 100644 --- a/src/app/products/products.service.ts +++ b/src/app/products/products.service.ts @@ -526,6 +526,26 @@ export class ProductsService { return this.http.put(`/recurringdepositproducts/${recurringDepositProductId}`, recurringDepositProduct); } + /** Returns the active CBS base interest-rate version(s). */ + getAllRdInterestRates(): Observable { + return this.getRdBaseInterestRates(); + } + + /** Returns the active CBS base interest-rate version(s). */ + getRdBaseInterestRates(): Observable { + return this.http.get('/recurringdepositproducts/baseinterestrates'); + } + + /** Returns the full CBS base chart history. */ + getRdBaseInterestRateHistory(): Observable { + return this.http.get('/recurringdepositproducts/baseinterestrates/history'); + } + + /** Saves a new CBS base interest-rate version. */ + saveRdBaseInterestRates(payload: any): Observable { + return this.http.post('/recurringdepositproducts/baseinterestrates', payload); + } + /** * @returns {Observable} Collateral data. */ diff --git a/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.html b/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.html new file mode 100644 index 000000000..f8cdf923e --- /dev/null +++ b/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.html @@ -0,0 +1,119 @@ + + +
+ + + @if (recurringDepositProductsTemplate?.chartTemplate) { + + + +
+ + @if (saveMessage) { + {{ saveMessage }} + } +
+ } @else { +

Loading CBS base chart...

+ } + +
+
+
+

Chart History

+

Previous CBS base chart versions are listed here for comparison and audit.

+
+
+ + @if (isHistoryLoading) { +

Loading history...

+ } @else if (rdInterestRateHistory.length === 0) { +

No history found for the CBS base chart.

+ } @else { + + @for (version of rdInterestRateHistory; track version.id) { + + + + + {{ version.versionLabel }} + + {{ version.isActive ? 'Active' : 'Inactive' }} + + + + + From {{ version.effectiveFromDate }} + To {{ version.effectiveToDate || 'Current' }} + + + + @if (version.remarks) { +

{{ version.remarks }}

+ } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Sr.{{ slab.srNo }}Description{{ slab.description }}From{{ slab.fromPeriod }}To + {{ slab.toPeriod !== null && slab.toPeriod !== undefined ? slab.toPeriod : 'N/A' }} + Period Type{{ slab.periodTypeLabel }}Annual Rate (%){{ slab.annualInterestRate | number: '1.2-2' }}
+
+ } +
+ } +
+
diff --git a/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.scss b/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.scss new file mode 100644 index 000000000..00de402dc --- /dev/null +++ b/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.scss @@ -0,0 +1,95 @@ +/** + * 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/. + */ + +.page-layout { + display: flex; + flex-direction: column; + gap: 1.25rem; +} + +.page-header { + display: flex; + align-items: end; + justify-content: space-between; +} + +.page-subtitle { + margin: 0.25rem 0 0; + color: #6b7280; +} + +.actions-row { + display: flex; + align-items: center; + gap: 1rem; +} + +.save-message { + color: #2563eb; + font-weight: 600; +} + +.history-section { + margin-top: 1rem; + padding-top: 0.5rem; + border-top: 1px solid #e5e7eb; +} + +.section-header { + display: flex; + align-items: end; + justify-content: space-between; + margin-bottom: 0.75rem; +} + +.section-subtitle { + margin: 0.25rem 0 0; + color: #6b7280; +} + +.version-header { + display: inline-flex; + align-items: center; + gap: 0.75rem; + flex-wrap: wrap; +} + +.meta-text { + margin-left: 0.75rem; + color: #6b7280; + font-size: 0.85rem; +} + +.active-badge, +.inactive-badge { + display: inline-flex; + align-items: center; + padding: 0.2rem 0.5rem; + border-radius: 999px; + font-size: 0.75rem; + font-weight: 600; +} + +.active-badge { + background: #dcfce7; + color: #166534; +} + +.inactive-badge { + background: #f3f4f6; + color: #4b5563; +} + +.remarks-text { + margin: 0 0 0.75rem; + color: #374151; +} + +.history-table { + width: 100%; +} diff --git a/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.ts b/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.ts new file mode 100644 index 000000000..811810cfc --- /dev/null +++ b/src/app/products/recurring-deposit-products/base-recurring-deposit-interest-rate-chart/base-recurring-deposit-interest-rate-chart.component.ts @@ -0,0 +1,121 @@ +/** + * 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 { ProductsService } from 'app/products/products.service'; +import { RecurringDepositProductInterestRateChartStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component'; +import { MatButton } from '@angular/material/button'; +import { DecimalPipe } from '@angular/common'; +import { + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow +} from '@angular/material/table'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-base-recurring-deposit-interest-rate-chart', + templateUrl: './base-recurring-deposit-interest-rate-chart.component.html', + styleUrls: ['./base-recurring-deposit-interest-rate-chart.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatButton, + MatExpansionModule, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, + DecimalPipe, + RecurringDepositProductInterestRateChartStepComponent + ] +}) +export class BaseRecurringDepositInterestRateChartComponent implements OnInit { + private productsService = inject(ProductsService); + + recurringDepositProductsTemplate: any; + rdInterestRates: any[] = []; + rdInterestRateHistory: any[] = []; + readonly historySlabColumns = [ + 'srNo', + 'description', + 'fromPeriod', + 'toPeriod', + 'periodTypeLabel', + 'annualInterestRate' + ]; + isSaving = false; + isHistoryLoading = false; + saveMessage = ''; + + ngOnInit(): void { + this.productsService.getRecurringDepositProductsTemplate().subscribe({ + next: (template: any) => (this.recurringDepositProductsTemplate = template), + error: () => (this.recurringDepositProductsTemplate = null) + }); + + this.loadBaseRates(); + this.loadBaseRateHistory(); + } + + get chartStep(): RecurringDepositProductInterestRateChartStepComponent | null { + return null; + } + + save(stepComponent: RecurringDepositProductInterestRateChartStepComponent): void { + this.isSaving = true; + this.saveMessage = ''; + const payload = stepComponent.modifiedRdInterestRatePayload; + this.productsService.saveRdBaseInterestRates(payload).subscribe({ + next: () => { + this.saveMessage = 'CBS base chart saved successfully.'; + this.loadBaseRates(); + this.loadBaseRateHistory(); + this.isSaving = false; + }, + error: () => { + this.saveMessage = 'Unable to save CBS base chart.'; + this.isSaving = false; + } + }); + } + + private loadBaseRates(): void { + this.productsService.getRdBaseInterestRates().subscribe({ + next: (rates: any[]) => (this.rdInterestRates = rates || []), + error: () => (this.rdInterestRates = []) + }); + } + + private loadBaseRateHistory(): void { + this.isHistoryLoading = true; + this.productsService.getRdBaseInterestRateHistory().subscribe({ + next: (history: any[]) => { + this.rdInterestRateHistory = history || []; + this.isHistoryLoading = false; + }, + error: () => { + this.rdInterestRateHistory = []; + this.isHistoryLoading = false; + } + }); + } +} diff --git a/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.html b/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.html index 9395a61dd..3b01d9081 100644 --- a/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.html +++ b/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.html @@ -12,7 +12,12 @@

{{ 'labels.heading.Create Recurring Deposit Product' | translate }}

- + @@ -64,12 +69,60 @@ - {{ 'labels.inputs.INTEREST RATE CHART' | translate }} + CBS Base Chart
- + @if (activeBaseVersion) { +
+
+
+

{{ activeBaseVersion.versionLabel }}

+

+ Active CBS base chart + Effective from {{ activeBaseVersion.effectiveFromDate }} +

+
+ Active +
+ + @if (activeBaseVersion.remarks) { +

{{ activeBaseVersion.remarks }}

+ } + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Sr.{{ slab.srNo }}Particular{{ slab.description }}From{{ slab.fromPeriod }}To{{ slab.toPeriod ?? 'N/A' }}Period Type{{ slab.periodTypeLabel }}Annual Rate (%) + {{ slab.annualInterestRate | number: '1.2-2' }} +
+
+ } @else { +

Loading CBS base chart...

+ }
@@ -109,7 +162,7 @@
@if (selectedTabIndex > 0) { } @if (selectedTabIndex < totalTabs - 1) { } @if (recurringDepositProductFormValid && selectedTabIndex === totalTabs - 1) { } diff --git a/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.scss b/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.scss index 67ab3211c..c04c383f8 100644 --- a/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.scss +++ b/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.scss @@ -8,33 +8,131 @@ $primary: #1353a0; -.rdp-container { max-width: 960px; margin: 0 auto; padding: 24px 16px; } - -.rdp-header { - display: flex; align-items: center; gap: 10px; margin-bottom: 20px; - &__icon { font-size: 20px; color: $primary; } - &__title { margin: 0; font-size: 22px; font-weight: 500; } +.rdp-container { + max-width: 960px; + margin: 0 auto; + padding: 24px 16px; } -.rdp-tabs { - background: var(--md-sys-color-surface, #fff); border-radius: 8px; - box-shadow: 0 2px 8px rgb(0 0 0 / 8%); +.rdp-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 20px; - ::ng-deep { - .mat-mdc-tab-header { border-bottom: 1px solid rgb(0 0 0 / 8%); } - .mat-mdc-tab { min-width: 100px; padding: 0 12px; height: 52px; } - .mat-mdc-tab-body-wrapper { min-height: 200px; } - .mat-mdc-tab-body { overflow: visible !important; } - .mat-mdc-tab-body-content { overflow: visible !important; } + &__icon { + font-size: 20px; + color: $primary; + } + + &__title { + margin: 0; + font-size: 22px; + font-weight: 500; } } -.rdp-tab-icon { margin-right: 8px; font-size: 14px; } -.rdp-tab-content { padding: 24px 16px; } +.rdp-tabs { + background: var(--md-sys-color-surface, #fff); + border-radius: 8px; + box-shadow: 0 2px 8px rgb(0 0 0 / 8%); + + ::ng-deep { + .mat-mdc-tab-header { + border-bottom: 1px solid rgb(0 0 0 / 8%); + } + + .mat-mdc-tab { + min-width: 100px; + padding: 0 12px; + height: 52px; + } + + .mat-mdc-tab-body-wrapper { + min-height: 200px; + } + + .mat-mdc-tab-body { + overflow: visible !important; + } + + .mat-mdc-tab-body-content { + overflow: visible !important; + } + } +} + +.rdp-tab-icon { + margin-right: 8px; + font-size: 14px; +} + +.rdp-tab-content { + padding: 24px 16px; +} + +.base-rate-card { + padding: 16px; + border: 1px solid #dbe4f0; + border-radius: 14px; + background: linear-gradient(180deg, #f8fbff 0%, #fff 100%); +} + +.base-rate-header { + display: flex; + align-items: start; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +} + +.base-rate-title { + margin: 0; + font-size: 18px; + font-weight: 600; +} + +.base-rate-meta { + margin: 6px 0 0; + color: #5b6b7f; + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +.base-rate-badge { + padding: 4px 10px; + border-radius: 999px; + background: #dcfce7; + color: #166534; + font-size: 12px; + font-weight: 700; +} + +.base-rate-remarks { + margin: 0 0 12px; + color: #334155; +} + +.base-rate-table { + width: 100%; + border-collapse: collapse; +} .rdp-action-bar { - display: flex; justify-content: space-between; align-items: center; - margin-top: 20px; padding: 16px 0; border-top: 1px solid rgb(0 0 0 / 8%); - &__cancel { color: rgb(0 0 0 / 60%); } - &__right { display: flex; gap: 12px; } + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 20px; + padding: 16px 0; + border-top: 1px solid rgb(0 0 0 / 8%); + + &__cancel { + color: rgb(0 0 0 / 60%); + } + + &__right { + display: flex; + gap: 12px; + } } diff --git a/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.ts b/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.ts index b10a01985..3afd4956e 100644 --- a/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.ts +++ b/src/app/products/recurring-deposit-products/create-recurring-deposit-product/create-recurring-deposit-product.component.ts @@ -15,9 +15,20 @@ import { RecurringDepositProductDetailsStepComponent } from '../recurring-deposi import { RecurringDepositProductCurrencyStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component'; import { RecurringDepositProductTermsStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component'; import { RecurringDepositProductSettingsStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component'; -import { RecurringDepositProductInterestRateChartStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component'; import { RecurringDepositProductChargesStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component'; import { RecurringDepositProductAccountingStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component'; +import { + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow +} from '@angular/material/table'; /** Custom Services */ import { ProductsService } from 'app/products/products.service'; @@ -40,10 +51,19 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; RecurringDepositProductCurrencyStepComponent, RecurringDepositProductTermsStepComponent, RecurringDepositProductSettingsStepComponent, - RecurringDepositProductInterestRateChartStepComponent, RecurringDepositProductChargesStepComponent, RecurringDepositProductAccountingStepComponent, - RecurringDepositProductPreviewStepComponent + RecurringDepositProductPreviewStepComponent, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow ] }) export class CreateRecurringDepositProductComponent { @@ -61,8 +81,6 @@ export class CreateRecurringDepositProductComponent { recurringDepositProductTermsStep: RecurringDepositProductTermsStepComponent; @ViewChild(RecurringDepositProductSettingsStepComponent, { static: true }) recurringDepositProductSettingsStep: RecurringDepositProductSettingsStepComponent; - @ViewChild(RecurringDepositProductInterestRateChartStepComponent, { static: true }) - recurringDepositProductInterestRateChartStep: RecurringDepositProductInterestRateChartStepComponent; @ViewChild(RecurringDepositProductChargesStepComponent, { static: true }) recurringDepositProductChargesStep: RecurringDepositProductChargesStepComponent; @ViewChild(RecurringDepositProductAccountingStepComponent, { static: true }) @@ -71,6 +89,15 @@ export class CreateRecurringDepositProductComponent { recurringDepositProductsTemplate: any; accountingRuleData: string[] = []; selectedTabIndex = 0; + rdInterestRates: any[] = []; + readonly rdSlabColumns = [ + 'srNo', + 'description', + 'fromPeriod', + 'toPeriod', + 'periodTypeLabel', + 'annualInterestRate' + ]; /** * @param {ActivatedRoute} route Activated Route. @@ -84,6 +111,15 @@ export class CreateRecurringDepositProductComponent { this.recurringDepositProductsTemplate = data.recurringDepositProductsTemplate; }); this.accountingRuleData = this.accounting.getAccountingRulesForSavings(); + // Load the CBS base chart as the reference for the editable table + this.productsService.getRdBaseInterestRates().subscribe({ + next: (rates: any[]) => { + this.rdInterestRates = rates || []; + }, + error: () => { + this.rdInterestRates = []; + } + }); } get recurringDepositProductDetailsForm() { @@ -102,10 +138,6 @@ export class CreateRecurringDepositProductComponent { return this.recurringDepositProductSettingsStep.recurringDepositProductSettingsForm; } - get recurringDepositProductInterestRateChartForm() { - return this.recurringDepositProductInterestRateChartStep.recurringDepositProductInterestRateChartForm; - } - get recurringDepositProductAccountingForm() { return this.recurringDepositProductAccountingStep.recurringDepositProductAccountingForm; } @@ -120,20 +152,27 @@ export class CreateRecurringDepositProductComponent { this.recurringDepositProductCurrencyForm.valid && this.recurringDepositProductTermsForm.valid && this.recurringDepositProductSettingsForm.valid && - this.recurringDepositProductInterestRateChartForm.valid && this.recurringDepositProductAccountingForm.valid ); } + get activeBaseVersion() { + return this.rdInterestRates.find((version: any) => version.isActive) ?? this.rdInterestRates[0] ?? null; + } + + get activeBaseSlabs() { + return this.activeBaseVersion?.slabs || []; + } + get recurringDepositProduct() { return { ...this.recurringDepositProductDetailsStep.recurringDepositProductDetails, ...this.recurringDepositProductCurrencyStep.recurringDepositProductCurrency, ...this.recurringDepositProductTermsStep.recurringDepositProductTerms, ...this.recurringDepositProductSettingsStep.recurringDepositProductSettings, - ...this.recurringDepositProductInterestRateChartStep.recurringDepositProductInterestRateChart, ...this.recurringDepositProductChargesStep.recurringDepositProductCharges, - ...this.recurringDepositProductAccountingStep.recurringDepositProductAccounting + ...this.recurringDepositProductAccountingStep.recurringDepositProductAccounting, + rdSlabs: this.activeBaseSlabs }; } diff --git a/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.html b/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.html index 606a08fb4..975cc9065 100644 --- a/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.html +++ b/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.html @@ -6,112 +6,195 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
- +
+ +

{{ 'labels.heading.Edit Recurring Deposit Product' | translate }}

+
+ + - - - + + + + {{ 'labels.inputs.DETAILS' | translate }} + +
+ +
+
- - - + + + + {{ 'labels.inputs.CURRENCY' | translate }} + +
+ +
+
- - - + + + + {{ 'labels.inputs.TERMS' | translate }} + +
+ +
+
- - - + + + + {{ 'labels.inputs.SETTINGS' | translate }} + +
+ +
+
- - - + + + + CBS Base Chart + +
+ @if (activeBaseVersion) { +
+
+
+

{{ activeBaseVersion.versionLabel }}

+

+ Active CBS base chart + Effective from {{ activeBaseVersion.effectiveFromDate }} +

+
+ Active +
- - {{ 'labels.inputs.DETAILS' | translate }} + @if (activeBaseVersion.remarks) { +

{{ activeBaseVersion.remarks }}

+ } - - -
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Sr.{{ slab.srNo }}Particular{{ slab.description }}From{{ slab.fromPeriod }}To{{ slab.toPeriod ?? 'N/A' }}Period Type{{ slab.periodTypeLabel }}Annual Rate (%) + {{ slab.annualInterestRate | number: '1.2-2' }} +
+
+ } @else { +

Loading CBS base chart...

+ } +
+
- - {{ 'labels.inputs.CURRENCY' | translate }} + + + + {{ 'labels.inputs.CHARGES' | translate }} + +
+ +
+
- - -
- - - {{ 'labels.inputs.TERMS' | translate }} - - - - - - - {{ 'labels.inputs.SETTINGS' | translate }} - - - - - - - {{ 'labels.inputs.INTEREST RATE CHART' | translate }} - - - - - - - {{ 'labels.inputs.CHARGES' | translate }} - - - - - - - {{ 'labels.inputs.ACCOUNTING' | translate }} - - - - + + + + {{ 'labels.inputs.ACCOUNTING' | translate }} + +
+ +
+
@if (recurringDepositProductFormValidAndNotPrinstine) { - - {{ 'labels.inputs.PREVIEW' | translate }} - - - + + + + {{ 'labels.inputs.PREVIEW' | translate }} + +
+ +
+
} -
+ + +
+ +
+ @if (selectedTabIndex > 0) { + + } + @if (selectedTabIndex < totalTabs - 1) { + + } + @if (recurringDepositProductFormValidAndNotPrinstine && selectedTabIndex === totalTabs - 1) { + + } +
+
diff --git a/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.scss b/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.scss index af9f4c075..c04c383f8 100644 --- a/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.scss +++ b/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.scss @@ -5,3 +5,134 @@ * 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: #1353a0; + +.rdp-container { + max-width: 960px; + margin: 0 auto; + padding: 24px 16px; +} + +.rdp-header { + display: flex; + align-items: center; + gap: 10px; + margin-bottom: 20px; + + &__icon { + font-size: 20px; + color: $primary; + } + + &__title { + margin: 0; + font-size: 22px; + font-weight: 500; + } +} + +.rdp-tabs { + background: var(--md-sys-color-surface, #fff); + border-radius: 8px; + box-shadow: 0 2px 8px rgb(0 0 0 / 8%); + + ::ng-deep { + .mat-mdc-tab-header { + border-bottom: 1px solid rgb(0 0 0 / 8%); + } + + .mat-mdc-tab { + min-width: 100px; + padding: 0 12px; + height: 52px; + } + + .mat-mdc-tab-body-wrapper { + min-height: 200px; + } + + .mat-mdc-tab-body { + overflow: visible !important; + } + + .mat-mdc-tab-body-content { + overflow: visible !important; + } + } +} + +.rdp-tab-icon { + margin-right: 8px; + font-size: 14px; +} + +.rdp-tab-content { + padding: 24px 16px; +} + +.base-rate-card { + padding: 16px; + border: 1px solid #dbe4f0; + border-radius: 14px; + background: linear-gradient(180deg, #f8fbff 0%, #fff 100%); +} + +.base-rate-header { + display: flex; + align-items: start; + justify-content: space-between; + gap: 12px; + margin-bottom: 12px; +} + +.base-rate-title { + margin: 0; + font-size: 18px; + font-weight: 600; +} + +.base-rate-meta { + margin: 6px 0 0; + color: #5b6b7f; + display: flex; + gap: 12px; + flex-wrap: wrap; +} + +.base-rate-badge { + padding: 4px 10px; + border-radius: 999px; + background: #dcfce7; + color: #166534; + font-size: 12px; + font-weight: 700; +} + +.base-rate-remarks { + margin: 0 0 12px; + color: #334155; +} + +.base-rate-table { + width: 100%; + border-collapse: collapse; +} + +.rdp-action-bar { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 20px; + padding: 16px 0; + border-top: 1px solid rgb(0 0 0 / 8%); + + &__cancel { + color: rgb(0 0 0 / 60%); + } + + &__right { + display: flex; + gap: 12px; + } +} diff --git a/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.ts b/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.ts index 2c39bef1d..bdffd0776 100644 --- a/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.ts +++ b/src/app/products/recurring-deposit-products/edit-recurring-deposit-product/edit-recurring-deposit-product.component.ts @@ -15,16 +15,26 @@ import { RecurringDepositProductDetailsStepComponent } from '../recurring-deposi import { RecurringDepositProductCurrencyStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component'; import { RecurringDepositProductTermsStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component'; import { RecurringDepositProductSettingsStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component'; -import { RecurringDepositProductInterestRateChartStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component'; import { RecurringDepositProductChargesStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component'; import { RecurringDepositProductAccountingStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component'; +import { + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow +} from '@angular/material/table'; /** Custom Services */ import { ProductsService } from 'app/products/products.service'; import { SettingsService } from 'app/settings/settings.service'; import { Accounting } from 'app/core/utils/accounting'; -import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper'; -import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { MatTabsModule } from '@angular/material/tabs'; import { RecurringDepositProductPreviewStepComponent } from '../recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @@ -34,19 +44,24 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; styleUrls: ['./edit-recurring-deposit-product.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, - MatStepper, - MatStepperIcon, - FaIconComponent, - MatStep, - MatStepLabel, + MatTabsModule, RecurringDepositProductDetailsStepComponent, RecurringDepositProductCurrencyStepComponent, RecurringDepositProductTermsStepComponent, RecurringDepositProductSettingsStepComponent, - RecurringDepositProductInterestRateChartStepComponent, RecurringDepositProductChargesStepComponent, RecurringDepositProductAccountingStepComponent, - RecurringDepositProductPreviewStepComponent + RecurringDepositProductPreviewStepComponent, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow ] }) export class EditRecurringDepositProductComponent { @@ -64,8 +79,6 @@ export class EditRecurringDepositProductComponent { recurringDepositProductTermsStep: RecurringDepositProductTermsStepComponent; @ViewChild(RecurringDepositProductSettingsStepComponent, { static: true }) recurringDepositProductSettingsStep: RecurringDepositProductSettingsStepComponent; - @ViewChild(RecurringDepositProductInterestRateChartStepComponent, { static: true }) - recurringDepositProductInterestRateChartStep: RecurringDepositProductInterestRateChartStepComponent; @ViewChild(RecurringDepositProductChargesStepComponent, { static: true }) recurringDepositProductChargesStep: RecurringDepositProductChargesStepComponent; @ViewChild(RecurringDepositProductAccountingStepComponent, { static: true }) @@ -73,6 +86,16 @@ export class EditRecurringDepositProductComponent { recurringDepositProductsTemplate: any; accountingRuleData: string[] = []; + rdInterestRates: any[] = []; + readonly rdSlabColumns = [ + 'srNo', + 'description', + 'fromPeriod', + 'toPeriod', + 'periodTypeLabel', + 'annualInterestRate' + ]; + selectedTabIndex = 0; /** * @param {ActivatedRoute} route Activated Route. @@ -84,6 +107,14 @@ export class EditRecurringDepositProductComponent { constructor() { this.route.data.subscribe((data: { recurringDepositProductAndTemplate: any }) => { this.recurringDepositProductsTemplate = data.recurringDepositProductAndTemplate; + this.productsService.getRdBaseInterestRates().subscribe({ + next: (rates: any[]) => { + this.rdInterestRates = rates || []; + }, + error: () => { + this.rdInterestRates = []; + } + }); }); this.accountingRuleData = this.accounting.getAccountingRulesForSavings(); } @@ -104,43 +135,55 @@ export class EditRecurringDepositProductComponent { return this.recurringDepositProductSettingsStep.recurringDepositProductSettingsForm; } - get recurringDepositProductInterestRateChartForm() { - return this.recurringDepositProductInterestRateChartStep.recurringDepositProductInterestRateChartForm; - } - get recurringDepositProductAccountingForm() { return this.recurringDepositProductAccountingStep.recurringDepositProductAccountingForm; } + get totalTabs(): number { + return this.recurringDepositProductFormValidAndNotPrinstine ? 7 : 6; + } + get recurringDepositProductFormValidAndNotPrinstine() { return ( this.recurringDepositProductDetailsForm.valid && this.recurringDepositProductCurrencyForm.valid && this.recurringDepositProductTermsForm.valid && this.recurringDepositProductSettingsForm.valid && - this.recurringDepositProductInterestRateChartForm.valid && this.recurringDepositProductAccountingForm.valid && - (this.recurringDepositProductDetailsForm.pristine || - this.recurringDepositProductCurrencyForm.pristine || - this.recurringDepositProductTermsForm.pristine || - this.recurringDepositProductSettingsForm.pristine || - this.recurringDepositProductInterestRateChartForm.pristine || - this.recurringDepositProductAccountingForm.pristine) + (!this.recurringDepositProductDetailsForm.pristine || + !this.recurringDepositProductCurrencyForm.pristine || + !this.recurringDepositProductTermsForm.pristine || + !this.recurringDepositProductSettingsForm.pristine || + !this.recurringDepositProductAccountingForm.pristine) ); } + get activeBaseVersion() { + return this.rdInterestRates.find((version: any) => version.isActive) ?? this.rdInterestRates[0] ?? null; + } + + get activeBaseSlabs() { + return this.activeBaseVersion?.slabs || []; + } + get recurringDepositProduct() { return { ...this.recurringDepositProductDetailsStep.recurringDepositProductDetails, ...this.recurringDepositProductCurrencyStep.recurringDepositProductCurrency, ...this.recurringDepositProductTermsStep.recurringDepositProductTerms, ...this.recurringDepositProductSettingsStep.recurringDepositProductSettings, - ...this.recurringDepositProductInterestRateChartStep.recurringDepositProductInterestRateChart, ...this.recurringDepositProductChargesStep.recurringDepositProductCharges, ...this.recurringDepositProductAccountingStep.recurringDepositProductAccounting }; } + get recurringDepositProductPreview() { + return { + ...this.recurringDepositProduct, + rdSlabs: this.activeBaseSlabs + }; + } + submit() { // TODO: Update once language and date settings are setup const recurringDepositProduct = { @@ -152,8 +195,9 @@ export class EditRecurringDepositProductComponent { recurringDepositProduct.description = ''; } delete recurringDepositProduct.advancedAccountingRules; + const productId = this.recurringDepositProductsTemplate.id; this.productsService - .updateRecurringDepositProduct(this.recurringDepositProductsTemplate.id, recurringDepositProduct) + .updateRecurringDepositProduct(productId, recurringDepositProduct) .subscribe((response: any) => { this.router.navigate(['../'], { relativeTo: this.route }); }); diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.html index 10024514c..6d8eba561 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.html @@ -7,273 +7,113 @@ --> - -
- -

{{ 'labels.heading.Accounting Rule' | translate }}

+ + + +

Accounting

+
- - @for (accountingRule of accountingRuleData; track accountingRule; let i = $index) { - - {{ 'labels.accounting.' + accountingRule | translate }} - - } - -
-
- - @if (isCashOrAccrualAccounting()) { - - -
-
- -

{{ 'labels.heading.Assets' | translate }}

-
-
- - @if (isAccrualAccounting()) { - - - } -
-
- - -
-
- -

{{ 'labels.heading.Liabilities' | translate }}

-
-
- - - @if (isAccrualAccounting()) { - - } -
-
- - -
-
- -

{{ 'labels.heading.Expenses' | translate }}

-
-
- -
-
- - -
-
- -

{{ 'labels.heading.Income' | translate }}

-
-
- - -
-
- - -
-
- -

{{ 'labels.heading.Advanced Accounting Rules' | translate }}

- +
+
+

Accounting Rule

+
+
+ Cash +
- @if (recurringDepositProductAccountingForm.value.advancedAccountingRules) { -
- - -
-
-

{{ 'labels.heading.Configure Fund Sources for Payment Channels' | translate }}

-
- @if (paymentChannelToFundSourceMappings.value.length !== 0) { -
- - - - - - - - - - - - - - - -
{{ 'labels.inputs.Payment Type' | translate }} - {{ paymentFundSource.paymentTypeId | find: paymentTypeData : 'id' : 'name' }} - {{ 'labels.inputs.Fund Source' | translate }} - {{ paymentFundSource.fundSourceAccountId | find: assetAccountData : 'id' : 'name' }} - {{ 'labels.inputs.Actions' | translate }} - - -
-
- } +
+ @for (row of section.rows; track row.key) { + @if (!row.accrualOnly || isAccrualAccounting()) { +
+
+ + {{ section.pillLabel }} + + {{ row.label }} +
+ +
+ } + } +
+ +
+ @for ( + otherRow of getOtherGlMappingsArray(section.otherMappingsControlName).controls; + track $index; + let i = $index + ) { +
+
+
+ + {{ section.pillLabel }} + + +
+ +
+ + GL Code + + @for (glAccount of section.otherAccountOptions; track glAccount.id) { + + {{ '(' + glAccount.glCode + ') ' + glAccount.name }} + + } + + +
+
+ +
+ } +
- - - @if (existCharges()) { -
-
-

{{ 'labels.heading.Map Fees to Specific Income Accounts' | translate }}

- -
- @if (feeToIncomeAccountMappings.value.length !== 0) { -
- - - - - - - - - - - - - - - -
{{ 'labels.inputs.Fees' | translate }} - {{ feesIncome.chargeId | find: chargeData : 'id' : 'name' }} - {{ 'labels.inputs.Income Account' | translate }} - {{ feesIncome.incomeAccountId | find: incomeAccountData : 'id' : 'name' }} - {{ 'labels.inputs.Actions' | translate }} - - -
-
- } -
- } - - - @if (existCharges()) { -
-
-

{{ 'labels.heading.Map Penalties to Specific Income Accounts' | translate }}

- -
- @if (penaltyToIncomeAccountMappings.value.length !== 0) { -
- - - - - - - - - - - - - - - -
{{ 'labels.inputs.Penalty' | translate }} - {{ penaltyIncome.chargeId | find: penaltyData : 'id' : 'name' }} - {{ 'labels.inputs.Income Account' | translate }} - {{ penaltyIncome.incomeAccountId | find: incomeAccountData : 'id' : 'name' }} - {{ 'labels.inputs.Actions' | translate }} - - -
-
- } -
- } - -
+ } }
- } +
diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.scss b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.scss index dd5e8e63f..fe6ce3f2b 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.scss +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.scss @@ -6,33 +6,256 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-700: #334155; +$gray-900: #202124; + .rdpa-section { - border: 1px solid #e2e8f0; + border: 1px solid $border; border-radius: 10px; overflow: hidden; background: #fff; +} - & + & { margin-top: 16px; } +.rdpa-section__header { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 24px; + background: $gray-50; + border-bottom: 1px solid $gray-200; + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } +} + +.rdpa-section__icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: #eff6ff; + color: #2563eb; + font-size: 13px; +} + +.rdpa-section__body { + padding: 16px 24px 20px; +} + +.rdpa-group { + & + & { + margin-top: 20px; + } &__header { display: flex; align-items: center; - gap: 10px; - padding: 12px 24px; - background: #f8f9fa; - border-bottom: 1px solid #e8eaed; - - i { font-size: 14px; color: #0ea5e9; } - h3 { margin: 0; font-size: 14px; font-weight: 600; color: #202124; } + justify-content: space-between; + gap: 16px; + margin: 0 0 12px; } - &__body { padding: 16px 24px; } + h4 { + margin: 0 0 12px; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #94a3b8; + } } -.rdpa-grid { +.rdpa-add-btn { + border: 0; + background: transparent; + color: #2563eb; + font-size: 12px; + font-weight: 700; + padding: 0; + cursor: pointer; +} + +.rdpa-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.rdpa-list + .rdpa-list { + margin-top: 12px; +} + +.rdpa-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 10px 12px; + border: 1px solid $border; + border-radius: 8px; + background: #fff; + + @media (width <= 900px) { + flex-direction: column; + align-items: stretch; + } +} + +.rdpa-row:has(mifosx-gl-account-selector) { + align-items: stretch; +} + +.rdpa-row--other { + border-style: dashed; + border-color: #cbd5e1; + background: #f8fafc; display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 4px 16px; + grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr); + align-items: center; + padding: 12px 16px; + + @media (width <= 1100px) { + grid-template-columns: 1fr; + } +} + +.rdpa-other-row-wrap { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 12px; + + @media (width <= 1100px) { + grid-template-columns: 1fr; + gap: 8px; + } +} + +.rdpa-row__meta { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + flex: 1 1 auto; +} + +.rdpa-row__meta--other { + min-width: 0; + flex: 1 1 auto; + align-self: stretch; +} + +.rdpa-row__label { + font-size: 14px; + font-weight: 600; + color: $gray-700; +} + +.rdpa-row__input { + flex: 0 0 22rem; + max-width: 22rem; + min-width: 18rem; + + @media (width <= 900px) { + width: 100%; + max-width: 100%; + min-width: 0; + } +} + +.rdpa-row__input :is(mifosx-gl-account-selector) { + width: 100%; + display: block; +} + +.rdpa-row__input :is(mifosx-gl-account-selector) :is(.layout-column) { + width: 100%; + margin-right: 0 !important; +} + +.rdpa-row__input :is(mifosx-gl-account-selector) :is(mat-form-field) { + width: 100%; +} + +.rdpa-row__actions { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.rdpa-row__actions--other { + justify-content: flex-end; + align-self: stretch; + width: 100%; + + @media (width <= 1100px) { + justify-content: flex-start; + } +} + +.rdpa-row__name { + width: 100%; + min-width: 0; + max-width: 24rem; + height: 40px; + border: 1px solid #cbd5e1; + border-radius: 6px; + padding: 0 12px; + font-size: 14px; + background: #fff; + + @media (width <= 900px) { + max-width: 100%; + } +} + +.rdpa-row__name--other-account { + max-width: 100%; + flex: 1 1 auto; + height: 48px; +} + +.rdpa-row__other-field { + width: 100%; + min-width: 0; +} + +.rdpa-row__other-field :is(mat-form-field) { + width: 100%; +} + +.rdpa-row__other-field :is(.mat-mdc-form-field-required-marker) { + display: none; +} + +.rdpa-remove-btn { + border: 0; + background: transparent; + color: #ef4444; + font-size: 18px; + line-height: 1; + cursor: pointer; + padding: 0 4px; +} + +.rdpa-remove-btn--side { + place-self: center end; + margin-right: 4px; + + @media (width <= 1100px) { + justify-self: start; + margin-right: 0; + } } .rdpa-radio-group { @@ -41,39 +264,41 @@ flex-wrap: wrap; } -.rdpa-header-checkbox { margin-left: auto; } +.rdpa-pill { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 2px 8px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + white-space: nowrap; -.rdpa-subsection { - & + & { margin-top: 16px; } + &--green { + background: #dcfce7; + color: #16a34a; + } - &__header { - display: flex; - align-items: center; - justify-content: space-between; - flex-wrap: wrap; - gap: 8px; - margin-bottom: 8px; + &--amber { + background: #fef3c7; + color: #d97706; + } - h4 { margin: 0; font-size: 13px; font-weight: 600; color: #202124; } + &--blue { + background: #dbeafe; + color: #2563eb; } } -.rdpa-add-btn { - height: 36px; - border-radius: 8px; - font-size: 13px; - font-weight: 600; -} +@media (width <= 900px) { + .rdpa-row__meta { + margin-bottom: 4px; + } -.rdpa-table-wrapper { - border: 1px solid #e2e8f0; - border-radius: 8px; - overflow: hidden; - - table { width: 100%; } -} - -@media (width <= 600px) { - .rdpa-grid { grid-template-columns: 1fr; } - .rdpa-radio-group { flex-direction: column; } + .rdpa-row__input { + flex: 1 1 auto; + max-width: 100%; + } } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.ts b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.ts index 6dfca1628..9d8843dda 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.ts +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-accounting-step/recurring-deposit-product-accounting-step.component.ts @@ -6,441 +6,393 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, OnInit, Input, inject } from '@angular/core'; +import { Component, Input, OnInit, inject } from '@angular/core'; import { - UntypedFormGroup, - UntypedFormBuilder, + FormsModule, + ReactiveFormsModule, UntypedFormArray, - Validators, + UntypedFormBuilder, UntypedFormControl, - ReactiveFormsModule + UntypedFormGroup, + Validators } from '@angular/forms'; -import { MatDialog } from '@angular/material/dialog'; - -import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component'; -import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; - -import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base'; -import { SelectBase } from 'app/shared/form-dialog/formfield/model/select-base'; +import { MatRadioButton, MatRadioGroup } from '@angular/material/radio'; +import { GlAccountSelectorComponent } from 'app/shared/accounting/gl-account-selector/gl-account-selector.component'; import { Accounting } from 'app/core/utils/accounting'; -import { TranslateService } from '@ngx-translate/core'; -import { MatRadioGroup, MatRadioButton } from '@angular/material/radio'; -import { GlAccountSelectorComponent } from '../../../../shared/accounting/gl-account-selector/gl-account-selector.component'; -import { MatCheckbox } from '@angular/material/checkbox'; -import { MatButton, MatIconButton } from '@angular/material/button'; -import { FaIconComponent } from '@fortawesome/angular-fontawesome'; -import { - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow -} from '@angular/material/table'; -import { FindPipe } from '../../../../pipes/find.pipe'; +import { GLAccount } from 'app/shared/models/general.model'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +type AccountingSectionKey = 'assets' | 'liabilities' | 'expenses' | 'income'; + +type AccountingRowConfig = { + key: string; + label: string; + inputLabel: string; + controlName: string; + options: GLAccount[]; + required?: boolean; + accrualOnly?: boolean; +}; + +type AccountingSectionConfig = { + key: AccountingSectionKey; + title: string; + pillLabel: string; + pillClass: 'green' | 'amber' | 'blue'; + rows: AccountingRowConfig[]; + otherMappingsControlName: string; + otherAccountOptions: GLAccount[]; +}; + +type OtherGlMappingValue = { + accountName: string; + glAccountId: number | null; +}; + @Component({ selector: 'mifosx-recurring-deposit-product-accounting-step', templateUrl: './recurring-deposit-product-accounting-step.component.html', styleUrls: ['./recurring-deposit-product-accounting-step.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, + FormsModule, + ReactiveFormsModule, MatRadioGroup, MatRadioButton, - GlAccountSelectorComponent, - MatCheckbox, - FaIconComponent, - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatIconButton, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow, - FindPipe + GlAccountSelectorComponent ] }) export class RecurringDepositProductAccountingStepComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); - private dialog = inject(MatDialog); private accounting = inject(Accounting); - private translateService = inject(TranslateService); @Input() recurringDepositProductsTemplate: any; @Input() accountingRuleData: any; @Input() recurringDepositProductFormValid: boolean; recurringDepositProductAccountingForm: UntypedFormGroup; - - chargeData: any; - penaltyData: any; - paymentTypeData: any; - assetAccountData: any; - incomeAccountData: any; - expenseAccountData: any; - liabilityAccountData: any; - - paymentFundSourceDisplayedColumns: string[] = [ - 'paymentTypeId', - 'fundSourceAccountId', - 'actions' - ]; - feesPenaltyIncomeDisplayedColumns: string[] = [ - 'chargeId', - 'incomeAccountId', - 'actions' - ]; + assetAccountOptions: GLAccount[] = []; + liabilityAccountOptions: GLAccount[] = []; + expenseAccountOptions: GLAccount[] = []; + incomeAccountOptions: GLAccount[] = []; + accountingSectionConfigs: AccountingSectionConfig[] = []; constructor() { - this.createrecurringDepositProductAccountingForm(); + this.createRecurringDepositProductAccountingForm(); this.setConditionalControls(); } ngOnInit() { - this.chargeData = this.recurringDepositProductsTemplate.chargeOptions || []; - this.penaltyData = this.recurringDepositProductsTemplate.penaltyOptions || []; - this.paymentTypeData = this.recurringDepositProductsTemplate.paymentTypeOptions || []; - this.assetAccountData = this.recurringDepositProductsTemplate.accountingMappingOptions.assetAccountOptions || []; - this.incomeAccountData = this.recurringDepositProductsTemplate.accountingMappingOptions.incomeAccountOptions || []; - this.expenseAccountData = - this.recurringDepositProductsTemplate.accountingMappingOptions.expenseAccountOptions || []; - this.liabilityAccountData = - this.recurringDepositProductsTemplate.accountingMappingOptions.liabilityAccountOptions || []; - if (!(this.recurringDepositProductsTemplate === undefined) && this.recurringDepositProductsTemplate.id) { - this.assignAccountingStepData(); - } + const accountingMappings = this.recurringDepositProductsTemplate?.accountingMappings || {}; + const mappings = { + ...accountingMappings, + otherGlMappingsMap: this.recurringDepositProductsTemplate?.otherGlMappingsMap + }; + const accountingMappingOptions = this.recurringDepositProductsTemplate?.accountingMappingOptions || {}; + const accountingRuleId = 2; // Always Cash + + this.assetAccountOptions = accountingMappingOptions.assetAccountOptions || []; + this.liabilityAccountOptions = accountingMappingOptions.liabilityAccountOptions || []; + this.expenseAccountOptions = accountingMappingOptions.expenseAccountOptions || []; + this.incomeAccountOptions = accountingMappingOptions.incomeAccountOptions || []; + + this.buildAccountingSectionConfigs(); + + this.recurringDepositProductAccountingForm.patchValue({ accountingRule: accountingRuleId }); + this.syncAccountingControls(accountingRuleId); + this.patchAccountingValues(mappings); } - assignAccountingStepData() { - this.recurringDepositProductAccountingForm.patchValue({ - accountingRule: this.recurringDepositProductsTemplate.accountingRule.id - }); - if (this.isCashOrAccrualAccounting()) { - this.recurringDepositProductAccountingForm.patchValue({ - savingsReferenceAccountId: this.recurringDepositProductsTemplate.accountingMappings.savingsReferenceAccount.id, - savingsControlAccountId: this.recurringDepositProductsTemplate.accountingMappings.savingsControlAccount.id, - transfersInSuspenseAccountId: - this.recurringDepositProductsTemplate.accountingMappings.transfersInSuspenseAccount.id, - incomeFromFeeAccountId: this.recurringDepositProductsTemplate.accountingMappings.incomeFromFeeAccount.id, - incomeFromPenaltyAccountId: - this.recurringDepositProductsTemplate.accountingMappings.incomeFromPenaltyAccount.id, - interestOnSavingsAccountId: this.recurringDepositProductsTemplate.accountingMappings.interestOnSavingsAccount.id - }); - - if (this.isAccrualAccounting()) { - this.recurringDepositProductAccountingForm.patchValue({ - feesReceivableAccountId: this.recurringDepositProductsTemplate.accountingMappings.feeReceivableAccount.id, - penaltiesReceivableAccountId: - this.recurringDepositProductsTemplate.accountingMappings.penaltyReceivableAccount.id, - interestPayableAccountId: this.recurringDepositProductsTemplate.accountingMappings.interestPayableAccount.id - }); - } - - if ( - this.recurringDepositProductsTemplate.paymentChannelToFundSourceMappings || - this.recurringDepositProductsTemplate.feeToIncomeAccountMappings || - this.recurringDepositProductsTemplate.penaltyToIncomeAccountMappings - ) { - this.recurringDepositProductAccountingForm.patchValue({ - advancedAccountingRules: true - }); - } - if (this.recurringDepositProductsTemplate.paymentChannelToFundSourceMappings) { - this.recurringDepositProductsTemplate.paymentChannelToFundSourceMappings.forEach( - (paymentChannelToFundSourceMapping: any) => { - const paymentChannelToFundSourceMappingData = this.formBuilder.group({ - paymentTypeId: [ - paymentChannelToFundSourceMapping.paymentType.id, - Validators.required - ], - fundSourceAccountId: [ - paymentChannelToFundSourceMapping.fundSourceAccount.id, - Validators.required - ] - }); - const formArray = this.recurringDepositProductAccountingForm.controls[ - 'paymentChannelToFundSourceMappings' - ] as UntypedFormArray; - formArray.push(paymentChannelToFundSourceMappingData); - } - ); - } - if (this.recurringDepositProductsTemplate.feeToIncomeAccountMappings) { - this.recurringDepositProductsTemplate.feeToIncomeAccountMappings.forEach((feeToIncomeAccountMapping: any) => { - const feeToIncomeAccountMappingData = this.formBuilder.group({ - chargeId: [ - feeToIncomeAccountMapping.charge.id, - Validators.required - ], - incomeAccountId: [ - feeToIncomeAccountMapping.incomeAccount.id, - Validators.required - ] - }); - const formArray = this.recurringDepositProductAccountingForm.controls[ - 'feeToIncomeAccountMappings' - ] as UntypedFormArray; - formArray.push(feeToIncomeAccountMappingData); - }); - } - if (this.recurringDepositProductsTemplate.penaltyToIncomeAccountMappings) { - this.recurringDepositProductsTemplate.penaltyToIncomeAccountMappings.forEach( - (penaltyToIncomeAccountMapping: any) => { - const penaltyToIncomeAccountMappingData = this.formBuilder.group({ - chargeId: [ - penaltyToIncomeAccountMapping.charge.id, - Validators.required - ], - incomeAccountId: [ - penaltyToIncomeAccountMapping.incomeAccount.id, - Validators.required - ] - }); - const formArray = this.recurringDepositProductAccountingForm.controls[ - 'penaltyToIncomeAccountMappings' - ] as UntypedFormArray; - formArray.push(penaltyToIncomeAccountMappingData); - } - ); - } - } - } - - createrecurringDepositProductAccountingForm() { + createRecurringDepositProductAccountingForm() { this.recurringDepositProductAccountingForm = this.formBuilder.group({ - accountingRule: [1] + accountingRule: [2], + assetsOtherGlMappings: this.formBuilder.array([]), + liabilitiesOtherGlMappings: this.formBuilder.array([]), + expensesOtherGlMappings: this.formBuilder.array([]), + incomeOtherGlMappings: this.formBuilder.array([]) }); } - existCharges(): boolean { - return this.chargeData.length > 0; - } - - setConditionalControls() { - this.recurringDepositProductAccountingForm.get('accountingRule').valueChanges.subscribe((accountingRule: any) => { - if (accountingRule === 2 || accountingRule === 3) { - this.recurringDepositProductAccountingForm.addControl( - 'savingsReferenceAccountId', - new UntypedFormControl('', Validators.required) - ); - this.recurringDepositProductAccountingForm.addControl( - 'savingsControlAccountId', - new UntypedFormControl('', Validators.required) - ); - this.recurringDepositProductAccountingForm.addControl( - 'transfersInSuspenseAccountId', - new UntypedFormControl('', Validators.required) - ); - this.recurringDepositProductAccountingForm.addControl( - 'interestOnSavingsAccountId', - new UntypedFormControl('', Validators.required) - ); - this.recurringDepositProductAccountingForm.addControl( - 'incomeFromFeeAccountId', - new UntypedFormControl('', Validators.required) - ); - this.recurringDepositProductAccountingForm.addControl( - 'incomeFromPenaltyAccountId', - new UntypedFormControl('', Validators.required) - ); - this.recurringDepositProductAccountingForm.addControl('advancedAccountingRules', new UntypedFormControl(false)); - - if (accountingRule === 3) { - this.recurringDepositProductAccountingForm.addControl( - 'feesReceivableAccountId', - new UntypedFormControl('', Validators.required) - ); - this.recurringDepositProductAccountingForm.addControl( - 'penaltiesReceivableAccountId', - new UntypedFormControl('', Validators.required) - ); - this.recurringDepositProductAccountingForm.addControl( - 'interestPayableAccountId', - new UntypedFormControl('', Validators.required) - ); - } - - this.recurringDepositProductAccountingForm - .get('advancedAccountingRules') - .valueChanges.subscribe((advancedAccountingRules: boolean) => { - if (advancedAccountingRules) { - this.recurringDepositProductAccountingForm.addControl( - 'paymentChannelToFundSourceMappings', - this.formBuilder.array([]) - ); - this.recurringDepositProductAccountingForm.addControl( - 'feeToIncomeAccountMappings', - this.formBuilder.array([]) - ); - this.recurringDepositProductAccountingForm.addControl( - 'penaltyToIncomeAccountMappings', - this.formBuilder.array([]) - ); - } else { - this.recurringDepositProductAccountingForm.removeControl('paymentChannelToFundSourceMappings'); - this.recurringDepositProductAccountingForm.removeControl('feeToIncomeAccountMappings'); - this.recurringDepositProductAccountingForm.removeControl('penaltyToIncomeAccountMappings'); - } - }); - } else { - this.recurringDepositProductAccountingForm.removeControl('savingsReferenceAccountId'); - this.recurringDepositProductAccountingForm.removeControl('overdraftPortfolioControlId'); - this.recurringDepositProductAccountingForm.removeControl('savingsControlAccountId'); - this.recurringDepositProductAccountingForm.removeControl('transfersInSuspenseAccountId'); - this.recurringDepositProductAccountingForm.removeControl('interestOnSavingsAccountId'); - this.recurringDepositProductAccountingForm.removeControl('writeOffAccountId'); - this.recurringDepositProductAccountingForm.removeControl('incomeFromFeeAccountId'); - this.recurringDepositProductAccountingForm.removeControl('incomeFromPenaltyAccountId'); - this.recurringDepositProductAccountingForm.removeControl('incomeFromInterestId'); - this.recurringDepositProductAccountingForm.removeControl('advancedAccountingRules'); - this.recurringDepositProductAccountingForm.removeControl('escheatLiabilityId'); - this.recurringDepositProductAccountingForm.removeControl('feesReceivableAccountId'); - this.recurringDepositProductAccountingForm.removeControl('penaltiesReceivableAccountId'); - this.recurringDepositProductAccountingForm.removeControl('interestPayableAccountId'); - } + private setConditionalControls() { + this.recurringDepositProductAccountingForm.get('accountingRule')?.valueChanges.subscribe((rule: number) => { + this.syncAccountingControls(rule); }); } - get paymentChannelToFundSourceMappings(): UntypedFormArray { - return this.recurringDepositProductAccountingForm.get('paymentChannelToFundSourceMappings') as UntypedFormArray; - } - - get feeToIncomeAccountMappings(): UntypedFormArray { - return this.recurringDepositProductAccountingForm.get('feeToIncomeAccountMappings') as UntypedFormArray; - } - - get penaltyToIncomeAccountMappings(): UntypedFormArray { - return this.recurringDepositProductAccountingForm.get('penaltyToIncomeAccountMappings') as UntypedFormArray; - } - - add(formType: string, formArray: UntypedFormArray) { - const data = { ...this.getData(formType), pristine: false }; - const dialogRef = this.dialog.open(FormDialogComponent, { data }); - dialogRef.afterClosed().subscribe((response: any) => { - if (response.data) { - formArray.push(response.data); - } + private patchAccountingValues(mappings: any) { + this.recurringDepositProductAccountingForm.patchValue({ + savingsReferenceAccountId: mappings.savingsReferenceAccount?.id || '', + savingsControlAccountId: mappings.savingsControlAccount?.id || '', + transfersInSuspenseAccountId: mappings.transfersInSuspenseAccount?.id || '', + interestOnSavingsAccountId: mappings.interestOnSavingsAccount?.id || '', + incomeFromFeeAccountId: mappings.incomeFromFeeAccount?.id || '', + incomeFromPenaltyAccountId: mappings.incomeFromPenaltyAccount?.id || '', + feesReceivableAccountId: mappings.feeReceivableAccount?.id || '', + penaltiesReceivableAccountId: mappings.penaltyReceivableAccount?.id || '', + interestPayableAccountId: mappings.interestPayableAccount?.id || '' }); - } - edit(formType: string, formArray: UntypedFormArray, index: number) { - const data = { ...this.getData(formType, formArray.at(index).value), layout: { addButtonText: 'Edit' } }; - const dialogRef = this.dialog.open(FormDialogComponent, { data }); - dialogRef.afterClosed().subscribe((response: any) => { - if (response.data) { - formArray.at(index).patchValue(response.data.value); - } - }); - } - - delete(formArray: UntypedFormArray, index: number) { - const dialogRef = this.dialog.open(DeleteDialogComponent, { - data: { deleteContext: `this` } - }); - dialogRef.afterClosed().subscribe((response: any) => { - if (response.delete) { - formArray.removeAt(index); - } - }); - } - - getData(formType: string, values?: any) { - switch (formType) { - case 'PaymentFundSource': - return { - title: this.translateService.instant('labels.heading.Configure Fund Sources for Payment Channels'), - formfields: this.getPaymentFundSourceFormfields(values) - }; - case 'FeesIncome': - return { - title: this.translateService.instant('labels.heading.Map Fees to Specific Income Accounts'), - formfields: this.getFeesIncomeFormfields(values) - }; - case 'PenaltyIncome': - return { - title: this.translateService.instant('labels.heading.Map Penalties to Specific Income Accounts'), - formfields: this.getPenaltyIncomeFormfields(values) - }; + if (this.isCashOrAccrualAccounting()) { + const glMappings = mappings.otherGlMappingsMap || mappings.otherGlMappings; + this.setOtherGlMappings('assetsOtherGlMappings', glMappings?.assets || [], this.assetAccountOptions); + this.setOtherGlMappings( + 'liabilitiesOtherGlMappings', + glMappings?.liabilities || [], + this.liabilityAccountOptions + ); + this.setOtherGlMappings('expensesOtherGlMappings', glMappings?.expenses || [], this.expenseAccountOptions); + this.setOtherGlMappings('incomeOtherGlMappings', glMappings?.income || [], this.incomeAccountOptions); } } - getPaymentFundSourceFormfields(values?: any) { - const formfields: FormfieldBase[] = [ - new SelectBase({ - controlName: 'paymentTypeId', - label: this.translateService.instant('labels.inputs.Payment Type'), - value: values ? values.paymentTypeId : this.paymentTypeData[0].id, - options: { label: 'name', value: 'id', data: this.paymentTypeData }, - required: true, - order: 1 - }), - new SelectBase({ - controlName: 'fundSourceAccountId', - label: this.translateService.instant('labels.inputs.Fund Source'), - value: values ? values.fundSourceAccountId : this.assetAccountData[0].id, - options: { label: 'name', value: 'id', data: this.assetAccountData }, - required: true, - order: 2 - }) - ]; - return formfields; + private createOtherGlMappingGroup(otherGlMapping: Partial = {}) { + return this.formBuilder.group({ + accountName: [ + otherGlMapping.accountName || '', + Validators.required + ], + glAccountId: [ + otherGlMapping.glAccountId || '', + Validators.required + ] + }); } - getFeesIncomeFormfields(values?: any) { - const formfields: FormfieldBase[] = [ - new SelectBase({ - controlName: 'chargeId', - label: this.translateService.instant('labels.inputs.Fees'), - value: values ? values.chargeId : this.chargeData[0].id, - options: { label: 'name', value: 'id', data: this.chargeData }, - required: true, - order: 1 - }), - new SelectBase({ - controlName: 'incomeAccountId', - label: this.translateService.instant('labels.inputs.Income Account'), - value: values ? values.incomeAccountId : this.incomeAccountData[0].id, - options: { label: 'name', value: 'id', data: this.incomeAccountData }, - required: true, - order: 2 - }) - ]; - return formfields; + getOtherGlMappingsArray(controlName: string): UntypedFormArray { + return this.recurringDepositProductAccountingForm.get(controlName) as UntypedFormArray; } - getPenaltyIncomeFormfields(values?: any) { - const formfields: FormfieldBase[] = [ - new SelectBase({ - controlName: 'chargeId', - label: 'Penalty', - value: values ? values.chargeId : this.penaltyData[0].id, - options: { label: 'name', value: 'id', data: this.penaltyData }, - required: true, - order: 1 - }), - new SelectBase({ - controlName: 'incomeAccountId', - label: 'Income Account', - value: values ? values.incomeAccountId : this.incomeAccountData[0].id, - options: { label: 'name', value: 'id', data: this.incomeAccountData }, - required: true, - order: 2 - }) + private setOtherGlMappings(controlName: string, otherGlMappings: any[], options: GLAccount[]) { + const arr = this.getOtherGlMappingsArray(controlName); + arr.clear(); + (otherGlMappings || []).forEach((m) => { + arr.push( + this.createOtherGlMappingGroup({ + accountName: m.accountName, + glAccountId: m.glAccountId || options.find((o) => o.glCode === m.glCode)?.id || null + }) + ); + }); + } + + addOtherGlMapping(controlName: string) { + this.getOtherGlMappingsArray(controlName).push(this.createOtherGlMappingGroup()); + } + + removeOtherGlMapping(controlName: string, index: number) { + this.getOtherGlMappingsArray(controlName).removeAt(index); + } + + getOtherGlMappingControl(controlName: string, index: number, fieldName: string): UntypedFormControl { + return this.getOtherGlMappingsArray(controlName).at(index).get(fieldName) as UntypedFormControl; + } + + private clearOtherGlMappings() { + [ + 'assetsOtherGlMappings', + 'liabilitiesOtherGlMappings', + 'expensesOtherGlMappings', + 'incomeOtherGlMappings' + ].forEach((c) => this.getOtherGlMappingsArray(c).clear()); + } + + private getOtherGlMappings() { + const map = (controlName: string, options: GLAccount[]) => + this.getOtherGlMappingsArray(controlName) + .value.filter((item: OtherGlMappingValue) => !!item.accountName || !!item.glAccountId) + .map((item: OtherGlMappingValue) => ({ + accountName: item.accountName, + glCode: options.find((o) => o.id === item.glAccountId)?.glCode || '' + })) + .filter((item: { accountName: string; glCode: string }) => !!item.accountName || !!item.glCode); + + const otherGlMappings = { + assets: map('assetsOtherGlMappings', this.assetAccountOptions), + liabilities: map('liabilitiesOtherGlMappings', this.liabilityAccountOptions), + expenses: map('expensesOtherGlMappings', this.expenseAccountOptions), + income: map('incomeOtherGlMappings', this.incomeAccountOptions) + }; + return Object.values(otherGlMappings).some((v) => v.length) ? otherGlMappings : null; + } + + private syncAccountingControls(accountingRule: number) { + const addControl = (name: string, required = false) => { + if (!this.recurringDepositProductAccountingForm.contains(name)) { + this.recurringDepositProductAccountingForm.addControl( + name, + new UntypedFormControl('', required ? Validators.required : undefined) + ); + } else if (required) { + this.recurringDepositProductAccountingForm.get(name)?.setValidators(Validators.required); + this.recurringDepositProductAccountingForm.get(name)?.updateValueAndValidity({ emitEvent: false }); + } + }; + const removeControl = (name: string) => { + if (this.recurringDepositProductAccountingForm.contains(name)) { + this.recurringDepositProductAccountingForm.removeControl(name); + } + }; + + if (this.accounting.isCashOrAccrualAccountingRuleId(accountingRule)) { + addControl('savingsReferenceAccountId', true); + addControl('savingsControlAccountId', true); + addControl('transfersInSuspenseAccountId', true); + addControl('interestOnSavingsAccountId', true); + addControl('incomeFromFeeAccountId', true); + addControl('incomeFromPenaltyAccountId', true); + + if (this.accounting.isAccrualAccountingRuleId(accountingRule)) { + addControl('feesReceivableAccountId', true); + addControl('penaltiesReceivableAccountId', true); + addControl('interestPayableAccountId', true); + } else { + removeControl('feesReceivableAccountId'); + removeControl('penaltiesReceivableAccountId'); + removeControl('interestPayableAccountId'); + } + } else { + [ + 'savingsReferenceAccountId', + 'savingsControlAccountId', + 'transfersInSuspenseAccountId', + 'interestOnSavingsAccountId', + 'incomeFromFeeAccountId', + 'incomeFromPenaltyAccountId', + 'feesReceivableAccountId', + 'penaltiesReceivableAccountId', + 'interestPayableAccountId' + ].forEach(removeControl); + this.clearOtherGlMappings(); + } + } + + buildAccountingSectionConfigs() { + this.accountingSectionConfigs = [ + { + key: 'assets', + title: 'Assets', + pillLabel: 'Asset', + pillClass: 'green', + rows: [ + { + key: 'savingsReferenceAccountId', + label: 'Savings Reference Account', + inputLabel: 'Savings reference account', + controlName: 'savingsReferenceAccountId', + options: this.assetAccountOptions, + required: true + }, + { + key: 'feesReceivableAccountId', + label: 'Fees Receivable Account', + inputLabel: 'Fees receivable account', + controlName: 'feesReceivableAccountId', + options: this.assetAccountOptions, + accrualOnly: true + }, + { + key: 'penaltiesReceivableAccountId', + label: 'Penalties Receivable Account', + inputLabel: 'Penalties receivable account', + controlName: 'penaltiesReceivableAccountId', + options: this.assetAccountOptions, + accrualOnly: true + } + ], + otherMappingsControlName: 'assetsOtherGlMappings', + otherAccountOptions: this.assetAccountOptions + }, + { + key: 'liabilities', + title: 'Liabilities', + pillLabel: 'Liability', + pillClass: 'green', + rows: [ + { + key: 'savingsControlAccountId', + label: 'Savings Control Account', + inputLabel: 'Savings control account', + controlName: 'savingsControlAccountId', + options: this.liabilityAccountOptions, + required: true + }, + { + key: 'transfersInSuspenseAccountId', + label: 'Transfers In Suspense Account', + inputLabel: 'Transfers in suspense account', + controlName: 'transfersInSuspenseAccountId', + options: this.liabilityAccountOptions, + required: true + }, + { + key: 'interestPayableAccountId', + label: 'Interest Payable Account', + inputLabel: 'Interest payable account', + controlName: 'interestPayableAccountId', + options: this.liabilityAccountOptions, + accrualOnly: true + } + ], + otherMappingsControlName: 'liabilitiesOtherGlMappings', + otherAccountOptions: this.liabilityAccountOptions + }, + { + key: 'expenses', + title: 'Expenses', + pillLabel: 'Expense', + pillClass: 'amber', + rows: [ + { + key: 'interestOnSavingsAccountId', + label: 'Interest On Savings Account', + inputLabel: 'Interest on savings account', + controlName: 'interestOnSavingsAccountId', + options: this.expenseAccountOptions, + required: true + } + ], + otherMappingsControlName: 'expensesOtherGlMappings', + otherAccountOptions: this.expenseAccountOptions + }, + { + key: 'income', + title: 'Income', + pillLabel: 'Income', + pillClass: 'blue', + rows: [ + { + key: 'incomeFromFeeAccountId', + label: 'Income From Fees', + inputLabel: 'Income from fees', + controlName: 'incomeFromFeeAccountId', + options: this.incomeAccountOptions, + required: true + }, + { + key: 'incomeFromPenaltyAccountId', + label: 'Income From Penalties', + inputLabel: 'Income from penalties', + controlName: 'incomeFromPenaltyAccountId', + options: this.incomeAccountOptions, + required: true + } + ], + otherMappingsControlName: 'incomeOtherGlMappings', + otherAccountOptions: this.incomeAccountOptions + } ]; - return formfields; } get recurringDepositProductAccounting() { - return this.recurringDepositProductAccountingForm.value; + const { + assetsOtherGlMappings, + liabilitiesOtherGlMappings, + expensesOtherGlMappings, + incomeOtherGlMappings, + ...accounting + } = this.recurringDepositProductAccountingForm.value; + const otherGlMappings = this.getOtherGlMappings(); + return { ...accounting, ...(otherGlMappings ? { otherGlMappings } : {}) }; } isCashOrAccrualAccounting(): boolean { diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component.html index dd7563ee5..b2c701878 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component.html @@ -23,7 +23,14 @@ } - diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component.scss b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component.scss index 84a9cf676..661815e4c 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component.scss +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-charges-step/recurring-deposit-product-charges-step.component.scss @@ -20,8 +20,17 @@ background: #f8f9fa; border-bottom: 1px solid #e8eaed; - i { font-size: 14px; color: #0ea5e9; } - h3 { margin: 0; font-size: 14px; font-weight: 600; color: #202124; } + i { + font-size: 14px; + color: #0ea5e9; + } + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: #202124; + } } &__body { @@ -35,7 +44,10 @@ gap: 16px; flex-wrap: wrap; - &__select { flex: 1; min-width: 200px; } + &__select { + flex: 1; + min-width: 200px; + } } .rdpch-add-btn { @@ -51,5 +63,7 @@ overflow: hidden; margin-top: 12px; - table { width: 100%; } + table { + width: 100%; + } } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.html index baeea3de9..d21e17094 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.html @@ -9,71 +9,103 @@
- -

{{ 'labels.inputs.Currency' | translate }}

+ + Currency & Account Status Configuration
-
- - {{ 'labels.inputs.Currency' | translate }} - - @for (currency of currencyData; track currency) { - - {{ currency.name }} - - } - - - {{ 'labels.inputs.Currency' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.Decimal Places' | translate }} - - - {{ 'labels.inputs.Decimal Places' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - {{ 'labels.inputs.Decimal Places' | translate }} {{ 'labels.commons.Minimum Value must be' | translate }} - 0 - - - -
- - {{ 'labels.inputs.Set the recurring deposit installment in multiples of' | translate }} - -
- - @if (recurringDepositProductCurrencyForm.value.setMultiples) { +
+
- {{ 'labels.inputs.Currency in multiples of' | translate }} - + {{ 'labels.inputs.Currency' | translate }} + + @for (currency of currencyData; track currency) { + + {{ currency.name }} + + } + + Default: Indian Rupee - auto-set for domestic products - {{ 'labels.inputs.Currency in multiples of' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.inputs.Currency' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} - } + + + {{ 'labels.inputs.Decimal Places' | translate }} + + @for (places of decimalPlacesData; track places) { + {{ places }} + } + + + {{ 'labels.inputs.Decimal Places' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + +
+ +
+
+

Dormancy Alert

+

Enable automated notifications when an account becomes inactive based on product rules.

+
+ + yes + no + +
+ +
+
+

Balance Swap Option

+

Enable automated transfer of excess funds to linked deposits or vice-versa to maintain liquidity.

+
+ + yes + no + +
+ +
+
+

MSME

+

Micro, Small and Medium Enterprises

+
+ + yes + no + +
+ +
+
+

Nomination Facility

+

Allow account holders to designate individuals to receive funds in the event of account holder's death.

+
+ + yes + no + +
+ +
+
+

Auto Debit Facility

+

Enable automatic deduction of funds for recurring payments, utility bills, or EMI obligations.

+
+ + yes + no + +
diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.scss b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.scss index b223cc024..c2bcdef6b 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.scss +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.scss @@ -6,8 +6,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +$accent: #0ea5e9; +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-900: #202124; + .rdpc-section { - border: 1px solid #e2e8f0; + border: 1px solid $border; border-radius: 10px; overflow: hidden; background: #fff; @@ -21,46 +27,77 @@ align-items: center; gap: 10px; padding: 12px 24px; - background: #f8f9fa; - border-bottom: 1px solid #e8eaed; + background: $gray-50; + border-bottom: 1px solid $gray-200; i { font-size: 14px; - color: #0ea5e9; + color: $accent; } - h3 { + span { margin: 0; font-size: 14px; font-weight: 600; - color: #202124; + color: $gray-900; } } } +.rdpc-body { + background: #fff; +} + .rdpc-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 4px 16px; padding: 16px 24px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .rdpc-field { width: 100%; - - &--full { - grid-column: 1 / -1; - } } -.rdpc-checkbox-row { +.rdpc-alert { display: flex; align-items: center; - padding: 4px 0; -} + justify-content: space-between; + gap: 24px; + padding: 18px 24px 24px; + border-top: 1px solid $border; + background: #f8fafc; -@media (width <= 600px) { - .rdpc-grid { - grid-template-columns: 1fr; + @media (width <= 760px) { + flex-direction: column; + align-items: flex-start; + } + + &__content { + h4 { + margin: 0 0 6px; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + + p { + margin: 0; + font-size: 12px; + line-height: 1.5; + color: #64748b; + max-width: 540px; + } + } + + &__choices { + display: flex; + align-items: center; + gap: 18px; + flex-wrap: wrap; } } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.ts b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.ts index 50412b965..25e2a9c41 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.ts +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-currency-step/recurring-deposit-product-currency-step.component.ts @@ -6,12 +6,11 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, OnInit, Input, inject, DestroyRef } from '@angular/core'; +import { Component, OnInit, Input, inject } from '@angular/core'; import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; import { MatTooltip } from '@angular/material/tooltip'; -import { MatCheckbox } from '@angular/material/checkbox'; +import { MatRadioGroup, MatRadioButton } from '@angular/material/radio'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @Component({ selector: 'mifosx-recurring-deposit-product-currency-step', @@ -20,18 +19,23 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; imports: [ ...STANDALONE_SHARED_IMPORTS, MatTooltip, - MatCheckbox + MatRadioGroup, + MatRadioButton ] }) export class RecurringDepositProductCurrencyStepComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); - private destroyRef = inject(DestroyRef); @Input() recurringDepositProductsTemplate: any; recurringDepositProductCurrencyForm: UntypedFormGroup; currencyData: any; + decimalPlacesData = [ + 2, + 0, + 3 + ]; constructor() { this.createrecurringDepositProductCurrencyForm(); @@ -41,24 +45,21 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit { this.currencyData = this.recurringDepositProductsTemplate.currencyOptions; this.recurringDepositProductCurrencyForm.patchValue({ - currencyCode: this.recurringDepositProductsTemplate.currency?.code || this.currencyData[0].code, - digitsAfterDecimal: this.recurringDepositProductsTemplate.digitsAfterDecimal ?? '', - setMultiples: !!this.recurringDepositProductsTemplate.inMultiplesOf, - inMultiplesOf: this.recurringDepositProductsTemplate.inMultiplesOf ?? '' + currencyCode: this.recurringDepositProductsTemplate.currency?.code || this.currencyData?.[0]?.code || '', + digitsAfterDecimal: + this.recurringDepositProductsTemplate.digitsAfterDecimal ?? + this.recurringDepositProductsTemplate.currency?.decimalPlaces ?? + 2, + dormancyAlert: this.recurringDepositProductsTemplate.dormancyAlert ?? false, + isDormancyTrackingActive: this.recurringDepositProductsTemplate.isDormancyTrackingActive ?? false, + daysToInactive: this.recurringDepositProductsTemplate.daysToInactive ?? 30, + daysToDormancy: this.recurringDepositProductsTemplate.daysToDormancy ?? 60, + daysToEscheat: this.recurringDepositProductsTemplate.daysToEscheat ?? 90, + balanceSwapOption: this.recurringDepositProductsTemplate.balanceSwapOption ?? false, + msme: this.recurringDepositProductsTemplate.msme ?? false, + nominationFacility: this.recurringDepositProductsTemplate.nominationFacility ?? false, + autoDebitFacility: this.recurringDepositProductsTemplate.autoDebitFacility ?? false }); - - this.setupConditionalValidation(); - - // Apply initial validators based on the patched setMultiples value - const inMultiplesOfControl = this.recurringDepositProductCurrencyForm.get('inMultiplesOf'); - const setMultiplesControl = this.recurringDepositProductCurrencyForm.get('setMultiples'); - if (setMultiplesControl?.value) { - inMultiplesOfControl?.setValidators([ - Validators.required, - Validators.min(1) - ]); - inMultiplesOfControl?.updateValueAndValidity(); - } } createrecurringDepositProductCurrencyForm() { @@ -68,32 +69,21 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit { Validators.required ], digitsAfterDecimal: [ - '', + 2, [ Validators.required, Validators.min(0) ] ], - setMultiples: [false], - inMultiplesOf: [''] - }); - } - - setupConditionalValidation() { - const inMultiplesOfControl = this.recurringDepositProductCurrencyForm.get('inMultiplesOf'); - const setMultiplesControl = this.recurringDepositProductCurrencyForm.get('setMultiples'); - - setMultiplesControl?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((checked) => { - if (checked) { - inMultiplesOfControl?.setValidators([ - Validators.required, - Validators.min(1) - ]); - } else { - inMultiplesOfControl?.clearValidators(); - inMultiplesOfControl?.setValue(''); - } - inMultiplesOfControl?.updateValueAndValidity(); + dormancyAlert: [false], + isDormancyTrackingActive: [false], + daysToInactive: [30], + daysToDormancy: [60], + daysToEscheat: [90], + balanceSwapOption: [false], + msme: [false], + nominationFacility: [false], + autoDebitFacility: [false] }); } @@ -101,13 +91,18 @@ export class RecurringDepositProductCurrencyStepComponent implements OnInit { const formValue = this.recurringDepositProductCurrencyForm.value; const result: any = { currencyCode: formValue.currencyCode, - digitsAfterDecimal: formValue.digitsAfterDecimal + digitsAfterDecimal: formValue.digitsAfterDecimal, + dormancyAlert: formValue.dormancyAlert, + balanceSwapOption: formValue.balanceSwapOption, + msme: formValue.msme, + nominationFacility: formValue.nominationFacility, + autoDebitFacility: formValue.autoDebitFacility }; - - if (formValue.inMultiplesOf !== '' && formValue.inMultiplesOf !== null && formValue.inMultiplesOf !== undefined) { - result.inMultiplesOf = formValue.inMultiplesOf; + if (formValue.dormancyAlert) { + result.daysToInactive = formValue.daysToInactive; + result.daysToDormancy = formValue.daysToDormancy; + result.daysToEscheat = formValue.daysToEscheat; } - return result; } } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.html index d2d651a6e..5ec95ac4e 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.html @@ -10,7 +10,7 @@
-

{{ 'labels.inputs.Product Name' | translate }}

+

{{ 'labels.inputs.Product Name' | translate }} & {{ 'labels.inputs.Description' | translate }}

@@ -57,13 +57,71 @@ cdkTextareaAutosize cdkAutosizeMinRows="2" > - @if (recurringDepositProductDetailsForm.controls.description.hasError('required')) { - - {{ 'labels.inputs.Description' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - }
+ +
+
+ +

Product Classification

+
+ +
+ + Account Type + + @for (accountType of accountTypeData; track accountType) { + {{ accountType }} + } + + Account Type is required + + + + Customer Segment + + @for (customerSegment of customerSegmentData; track customerSegment) { + {{ customerSegment }} + } + + + + + GL Account (Liability) + + @for (account of liabilityAccountOptions; track account.id) { + {{ account.name }} ({{ account.glCode }}) + } + + + + + Effective To + + + + + + + End Date + + + + + + + Introducer Required + + @for (option of yesNoOptions; track option.value) { + {{ option.label }} + } + + + +
+ Deposit Account Link +
+
+
diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.scss b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.scss index 4e3d3848e..fd6cd1cb3 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.scss +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.scss @@ -6,8 +6,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +$accent: #0ea5e9; +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-900: #202124; + .rdpd-section { - border: 1px solid #e2e8f0; + border: 1px solid $border; border-radius: 10px; overflow: hidden; background: #fff; @@ -21,19 +27,19 @@ align-items: center; gap: 10px; padding: 12px 24px; - background: #f8f9fa; - border-bottom: 1px solid #e8eaed; + background: $gray-50; + border-bottom: 1px solid $gray-200; i { font-size: 14px; - color: #0ea5e9; + color: $accent; } h3 { margin: 0; font-size: 14px; font-weight: 600; - color: #202124; + color: $gray-900; } } } @@ -43,6 +49,22 @@ grid-template-columns: repeat(2, 1fr); gap: 4px 16px; padding: 16px 24px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.rdpd-grid--classification { + grid-template-columns: repeat(3, 1fr); + + @media (width <= 900px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .rdpd-field { @@ -53,8 +75,8 @@ } } -@media (width <= 600px) { - .rdpd-grid { - grid-template-columns: 1fr; - } +.rdpd-checkbox-row { + display: flex; + align-items: center; + min-height: 56px; } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.ts b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.ts index 1aaa3dac7..749ac8a3b 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.ts +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-details-step/recurring-deposit-product-details-step.component.ts @@ -6,10 +6,12 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, OnInit, Input, inject } from '@angular/core'; +import { Component, OnInit, OnChanges, Input, SimpleChanges, inject, ChangeDetectorRef } from '@angular/core'; import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; import { MatTooltip } from '@angular/material/tooltip'; import { CdkTextareaAutosize } from '@angular/cdk/text-field'; +import { Dates } from 'app/core/utils/dates'; +import { SettingsService } from 'app/settings/settings.service'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ @@ -22,24 +24,36 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; CdkTextareaAutosize ] }) -export class RecurringDepositProductDetailsStepComponent implements OnInit { +export class RecurringDepositProductDetailsStepComponent implements OnInit, OnChanges { private formBuilder = inject(UntypedFormBuilder); + private dateUtils = inject(Dates); + private settingsService = inject(SettingsService); + private cdr = inject(ChangeDetectorRef); @Input() recurringDepositProductsTemplate: any; recurringDepositProductDetailsForm: UntypedFormGroup; + accountTypeData: string[] = []; + customerSegmentData: string[] = []; + liabilityAccountOptions: any[] = []; + yesNoOptions: Array<{ value: boolean; label: string }> = [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' } + ]; constructor() { this.createrecurringDepositProductDetailsForm(); } ngOnInit() { - if (!(this.recurringDepositProductsTemplate === undefined) && this.recurringDepositProductsTemplate.id) { - this.recurringDepositProductDetailsForm.patchValue({ - name: this.recurringDepositProductsTemplate.name, - shortName: this.recurringDepositProductsTemplate.shortName, - description: this.recurringDepositProductsTemplate.description - }); + if (this.recurringDepositProductsTemplate) { + this.applyTemplate(this.recurringDepositProductsTemplate); + } + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.recurringDepositProductsTemplate?.currentValue) { + this.applyTemplate(changes.recurringDepositProductsTemplate.currentValue); } } @@ -53,14 +67,76 @@ export class RecurringDepositProductDetailsStepComponent implements OnInit { '', Validators.required ], - description: [ - '', + description: [''], + accountType: [ + null, Validators.required - ] + ], + customerSegment: [null], + glAccountId: [null], + effectiveTo: [''], + endDate: [''], + introducerRequired: [false], + shareLinkingEnabled: [false] + }); + } + + private getOptionNames(options: any[]): string[] { + if (!Array.isArray(options)) { + return []; + } + return options + .map((option) => option?.name ?? option?.value ?? option) + .filter((value) => typeof value === 'string' && value.trim().length > 0); + } + + private parseDate(dateValue: any): Date | null { + if (!dateValue) return null; + if (Array.isArray(dateValue)) { + return new Date(dateValue[0], dateValue[1] - 1, dateValue[2]); + } + if (dateValue instanceof Date) { + return dateValue; + } + return new Date(dateValue); + } + + compareById(o1: any, o2: any): boolean { + return o1 != null && o2 != null && Number(o1) === Number(o2); + } + + private applyTemplate(template: any) { + this.accountTypeData = this.getOptionNames(template.accountTypeOptions); + this.customerSegmentData = this.getOptionNames(template.customerSegmentOptions); + this.liabilityAccountOptions = template.accountingMappingOptions?.liabilityAccountOptions ?? []; + this.cdr.detectChanges(); + + const isEditing = !!template.id; + this.recurringDepositProductDetailsForm.patchValue({ + name: isEditing ? template.name : template.name || '', + shortName: isEditing ? template.shortName : template.shortName || '', + description: template.description || '', + accountType: template.accountType ?? this.accountTypeData[0] ?? null, + customerSegment: template.customerSegment ?? this.customerSegmentData[0] ?? null, + glAccountId: template.glAccountId ?? null, + effectiveTo: this.parseDate(template.effectiveTo), + endDate: this.parseDate(template.endDate), + introducerRequired: template.introducerRequired === true || template.introducerRequired === 'Yes', + shareLinkingEnabled: !!template.shareLinkingEnabled }); } get recurringDepositProductDetails() { - return this.recurringDepositProductDetailsForm.value; + const formValue = { ...this.recurringDepositProductDetailsForm.value }; + const dateFormat = this.settingsService.dateFormat; + + if (formValue.effectiveTo instanceof Date) { + formValue.effectiveTo = this.dateUtils.formatDate(formValue.effectiveTo, dateFormat) || ''; + } + if (formValue.endDate instanceof Date) { + formValue.endDate = this.dateUtils.formatDate(formValue.endDate, dateFormat) || ''; + } + + return formValue; } } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.html index 1f75f427d..7b799e488 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.html @@ -6,303 +6,104 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
-
-
- -

{{ 'labels.heading.Interest Rate Charts' | translate }}

- - -
+ +
+
+ +

Bank Interest Rate Chart  Reference / Editable

+ + +
-
- @for (chart of charts.controls; track chart; let chartIndex = $index) { -
-
-
- -
- -
- - {{ 'labels.inputs.Name' | translate }} - - @if (chart.get('name')?.touched && chart.get('name')?.hasError('required')) { - - {{ 'labels.inputs.Name' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - - - - {{ 'labels.inputs.Description' | translate }} - - @if (chart.get('description')?.touched && chart.get('description')?.hasError('required')) { - - {{ 'labels.inputs.Description' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - - - - {{ 'labels.inputs.From Date' | translate }} - - - - - {{ 'labels.inputs.From Date' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.End Date' | translate }} - - - - - -
- - {{ 'labels.inputs.Is primary grouping by amount?' | translate }} - -
-
- -
- -
- - @if (chart.value.chartSlabs.length === 0) { -
-

{{ 'labels.heading.It is required to add at least one Slab' | translate }}

-
- } - - @if (chart.value.chartSlabs.length !== 0) { -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Period' | translate }} - {{ - chartSlab.fromPeriod + - ' - ' + - chartSlab.toPeriod + - ' ' + - (chartSlab.periodType | find: periodTypeData : 'id' : 'value') - }} - {{ 'labels.inputs.Amount Range' | translate }} - {{ chartSlab.amountRangeFrom + ' - ' + chartSlab.amountRangeTo }} - {{ 'labels.inputs.Interest' | translate }} - {{ chartSlab.annualInterestRate }} - {{ 'labels.inputs.Description' | translate }} - {{ chartSlab.description }} - {{ 'labels.inputs.Actions' | translate }} - - - - -
- -

- {{ 'labels.heading.Incentives' | translate }} -

-
- -
- @if (chartSlab.incentives.length) { -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Entity Type' | translate }} - {{ incentive.entityType | find: entityTypeData : 'id' : 'value' }} - {{ 'labels.inputs.Attribute Name' | translate }} - {{ incentive.attributeName | find: attributeNameData : 'id' : 'value' }} - {{ 'labels.inputs.Condition Type' | translate }} - {{ incentive.conditionType | find: conditionTypeData : 'id' : 'value' }} - {{ 'labels.inputs.Attribute Value' | translate }} - @switch (incentive.attributeName) { - @case (2) { - {{ incentive.attributeValue | find: genderData : 'id' : 'name' }} - } - @case (3) { - {{ incentive.attributeValue }} - } - @case (4) { - {{ incentive.attributeValue | find: clientTypeData : 'id' : 'name' }} - } - @case (5) { - {{ - incentive.attributeValue | find: clientClassificationData : 'id' : 'name' - }} - } - } - {{ 'labels.inputs.Incentive Type' | translate }} - {{ incentive.incentiveType | find: incentiveTypeData : 'id' : 'value' }} - {{ 'labels.inputs.Interest' | translate }} - {{ incentive.amount }} - {{ 'labels.inputs.Actions' | translate }} - - -
-
- } -
-
-
-
- } -
-
+ @if (_referenceVersionLabel) { +
+ Version: {{ _referenceVersionLabel }} + @if (_referenceRemarks) { +  | {{ _referenceRemarks }} }
-
- + } + + @if (_editableSlabs.length === 0) { +
+

No interest rate slabs defined. Click Add Slab to add one.

+
+ } + + @if (_editableSlabs.length > 0) { +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Sr.{{ slab.srNo }}Particular + + From + + To + + Period{{ slab.periodTypeLabel }}Rate (%) + +
+
+ + + } +
diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.scss b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.scss index e72e64e10..21f95779c 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.scss +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.scss @@ -148,3 +148,68 @@ grid-template-columns: 1fr; } } + +/* ── Editable reference-rate table ───────────────────────────── */ +.rdpi-ref-section { + margin-top: 24px; + border-color: #bfdbfe; + background: #f0f7ff; + + .rdpi-section__header { + background: #dbeafe; + color: #1e3a5f; + } +} + +.rdpi-ref-badge { + font-size: 11px; + font-weight: 500; + background: #3b82f6; + color: #fff; + border-radius: 10px; + padding: 2px 8px; + vertical-align: middle; +} + +.rdpi-ref-meta { + padding: 8px 16px; + font-size: 13px; + color: #555; + border-bottom: 1px solid #bfdbfe; +} + +.rdpi-ref-table { + width: 100%; +} + +.rdpi-inline-input { + border: 1px solid #cbd5e1; + border-radius: 4px; + padding: 3px 6px; + font-size: 13px; + width: 100%; + background: #fff; + + &:focus { + outline: none; + border-color: #3b82f6; + } + + &--num { + width: 70px; + } + + &--rate { + width: 80px; + } +} + +.rdpi-ref-footer { + padding: 10px 16px; +} + +.rdpi-ref-hint { + font-size: 12px; + color: #64748b; + margin: 0; +} diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.ts b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.ts index 8f6778ce0..ddb15f739 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.ts +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-interest-rate-chart-step/recurring-deposit-product-interest-rate-chart-step.component.ts @@ -7,7 +7,7 @@ */ /** Angular Imports */ -import { Component, OnInit, Input, inject } from '@angular/core'; +import { Component, OnInit, OnChanges, Input, inject } from '@angular/core'; import { UntypedFormGroup, UntypedFormBuilder, @@ -21,7 +21,6 @@ import { animate, state, style, transition, trigger } from '@angular/animations' /** Custom Components */ import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component'; import { DepositProductIncentiveFormDialogComponent } from 'app/products/deposit-product-incentive-form-dialog/deposit-product-incentive-form-dialog.component'; -import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; /** Dialog Components */ import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base'; @@ -31,11 +30,10 @@ import { InputBase } from 'app/shared/form-dialog/formfield/model/input-base'; import { SettingsService } from 'app/settings/settings.service'; import { Dates } from 'app/core/utils/dates'; import { TranslateService } from '@ngx-translate/core'; -import { MatButton, MatIconButton } from '@angular/material/button'; +import { MatButton } from '@angular/material/button'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatDivider } from '@angular/material/divider'; -import { MatTooltip } from '@angular/material/tooltip'; import { MatCheckbox } from '@angular/material/checkbox'; import { MatTable, @@ -68,8 +66,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; ...STANDALONE_SHARED_IMPORTS, FaIconComponent, MatDivider, - MatIconButton, - MatTooltip, + MatButton, MatCheckbox, MatTable, MatColumnDef, @@ -81,8 +78,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; MatHeaderRow, MatRowDef, MatRow, - MatStepperPrevious, - MatStepperNext, FindPipe ] }) @@ -94,6 +89,190 @@ export class RecurringDepositProductInterestRateChartStepComponent implements On private translateService = inject(TranslateService); @Input() recurringDepositProductsTemplate: any; + /** Reference rate chart fetched from m_rd_interest_rate_effective / m_rd_interest_rate_slab_history */ + @Input() set rdInterestRates(rates: any[]) { + if (rates && rates.length > 0) { + // Use the first active version's slabs as the editable working copy + const activeVersion = rates.find((v: any) => v.isActive) ?? rates[0]; + this._referenceVersionLabel = activeVersion.versionLabel; + this._referenceRemarks = activeVersion.remarks; + // Deep-clone slabs so the original is not mutated + this._editableSlabs = (activeVersion.slabs ?? []).map((s: any) => ({ ...s })); + this.ensureSlabRangeRules(this._editableSlabs); + } + } + + /** Slabs the user is editing in the reference-rate table */ + _editableSlabs: any[] = []; + _referenceVersionLabel = ''; + _referenceRemarks = ''; + + /** Columns for the editable reference-rate table */ + readonly refSlabColumns = [ + 'srNo', + 'description', + 'fromPeriod', + 'toPeriod', + 'periodTypeLabel', + 'annualInterestRate' + ]; + + /** Returns the payload to save as a new rate version (called by parent after product save) */ + get modifiedRdInterestRatePayload(): any { + return { + versionLabel: this._referenceVersionLabel || 'Rate Version ' + new Date().toISOString().slice(0, 10), + remarks: this._referenceRemarks, + slabs: this._editableSlabs.map((s: any, idx: number) => ({ + srNo: s.srNo ?? idx + 1, + description: s.description, + periodTypeEnum: s.periodTypeEnum ?? 1, + fromPeriod: s.fromPeriod, + toPeriod: s.toPeriod ?? null, + annualInterestRate: s.annualInterestRate + })) + }; + } + + editSlab(index: number, field: string, value: any) { + const slabs = this._editableSlabs.map((slab: any) => ({ ...slab })); + const current = slabs[index]; + if (!current) { + return; + } + + const nextValue = this.normalizeSlabValue(value); + const oldFrom = this.normalizeSlabValue(current.fromPeriod); + const oldTo = this.normalizeSlabValue(current.toPeriod); + + current[field] = nextValue; + + // Keep the first slab anchored at 0 so the chart always starts from zero. + if (index === 0 && field === 'fromPeriod') { + current.fromPeriod = 0; + } + + if (field === 'fromPeriod' && nextValue === null) { + current.fromPeriod = oldFrom; + } + + if (field === 'fromPeriod') { + const effectiveFrom = this.normalizeSlabValue(current.fromPeriod); + const delta = effectiveFrom - oldFrom; + + if (current.toPeriod !== null && current.toPeriod !== undefined) { + current.toPeriod = this.normalizeSlabValue(current.toPeriod) + delta; + } + + this.shiftFollowingSlabs(slabs, index, delta); + } + + if (field === 'toPeriod') { + if (index < slabs.length - 1 && nextValue === null) { + // Non-final slabs should always have a closing boundary so later slabs can stay contiguous. + current.toPeriod = oldTo; + } else { + const delta = (this.normalizeSlabValue(current.toPeriod) ?? 0) - (oldTo ?? 0); + this.shiftFollowingSlabs(slabs, index, delta); + } + } + + this.ensureSlabRangeRules(slabs); + this._editableSlabs = slabs; + } + + addSlab() { + const slabs = this._editableSlabs.map((slab: any) => ({ ...slab })); + const nextSr = slabs.length + 1; + const lastSlab = slabs[slabs.length - 1]; + const lastFrom = this.normalizeSlabValue(lastSlab?.fromPeriod) ?? 0; + + if (lastSlab && this.normalizeSlabValue(lastSlab.toPeriod) === null) { + // Split the open-ended tail so the new slab can become the new last row. + // This keeps the sequence contiguous while preserving the existing tail start. + lastSlab.toPeriod = lastFrom; + } + + const previousSlab = slabs[slabs.length - 1]; + this._editableSlabs = [ + ...slabs, + { + srNo: nextSr, + description: '', + periodTypeEnum: 1, + periodTypeLabel: 'Days', + fromPeriod: slabs.length === 0 ? 0 : this.nextFromPeriod(previousSlab), + toPeriod: null, + annualInterestRate: null + } + ]; + this.ensureSlabRangeRules(this._editableSlabs); + } + + private normalizeSlabValue(value: any): number | null { + if (value === null || value === undefined || value === '') { + return null; + } + + const parsed = Number(value); + return Number.isNaN(parsed) ? null : parsed; + } + + private nextFromPeriod(previousSlab: any): number { + const previousTo = this.normalizeSlabValue(previousSlab?.toPeriod); + const previousFrom = this.normalizeSlabValue(previousSlab?.fromPeriod) ?? 0; + return previousTo === null ? previousFrom + 1 : previousTo + 1; + } + + private shiftFollowingSlabs(slabs: any[], index: number, delta: number): void { + if (!delta) { + return; + } + + for (let i = index + 1; i < slabs.length; i++) { + const slab = slabs[i]; + const currentFrom = this.normalizeSlabValue(slab.fromPeriod); + const currentTo = this.normalizeSlabValue(slab.toPeriod); + + if (currentFrom !== null) { + slab.fromPeriod = currentFrom + delta; + } + + if (currentTo !== null) { + slab.toPeriod = currentTo + delta; + } + } + } + + private ensureSlabRangeRules(slabs: any[]): void { + if (slabs.length === 0) { + return; + } + + slabs[0].fromPeriod = 0; + + for (let i = 1; i < slabs.length; i++) { + const previous = slabs[i - 1]; + const previousTo = this.normalizeSlabValue(previous.toPeriod); + if (previousTo === null) { + slabs[i].fromPeriod = this.normalizeSlabValue(slabs[i].fromPeriod); + continue; + } + + slabs[i].fromPeriod = previousTo + 1; + } + + // Keep the last slab open-ended so the chart ends with null. + slabs[slabs.length - 1].toPeriod = null; + + // Prevent inverted ranges on intermediate rows. + for (let i = 0; i < slabs.length - 1; i++) { + const fromPeriod = this.normalizeSlabValue(slabs[i].fromPeriod); + const toPeriod = this.normalizeSlabValue(slabs[i].toPeriod); + if (fromPeriod !== null && toPeriod !== null && toPeriod < fromPeriod) { + slabs[i].toPeriod = fromPeriod; + } + } + } recurringDepositProductInterestRateChartForm: UntypedFormGroup; @@ -146,9 +325,9 @@ export class RecurringDepositProductInterestRateChartStepComponent implements On this.clientClassificationData = this.recurringDepositProductsTemplate.chartTemplate.clientClassificationOptions; this.incentiveTypeData = this.recurringDepositProductsTemplate.chartTemplate.incentiveTypeOptions; - if (!(this.recurringDepositProductsTemplate === undefined)) { - this.assignFormData(); - } + // assignFormData() intentionally not called — old chart form is removed from UI. + // The recurringDepositProductInterestRateChartForm is kept only for stepControl + // validity; it remains empty (valid) so the preview tab becomes available. } // Assign form the values for edit recurring Depoosit @@ -429,17 +608,6 @@ export class RecurringDepositProductInterestRateChartStepComponent implements On }); } - delete(formArray: UntypedFormArray, index: number) { - const dialogRef = this.dialog.open(DeleteDialogComponent, { - data: { deleteContext: `this` } - }); - dialogRef.afterClosed().subscribe((response: any) => { - if (response.delete) { - formArray.removeAt(index); - } - }); - } - getData(formType: string, values?: any) { switch (formType) { case 'Slab': @@ -541,6 +709,9 @@ export class RecurringDepositProductInterestRateChartStepComponent implements On delete chart.id; } } - return recurringDepositProductInterestRateChart; + return { + ...recurringDepositProductInterestRateChart, + rdSlabs: this._editableSlabs + }; } } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.html index b8d5f706e..a946f557e 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.html @@ -1,4 +1,4 @@ - - -
-
- -

{{ 'labels.inputs.Details' | translate }}

-
-
-

{{ recurringDepositProduct.name }}

-
- {{ 'labels.inputs.Short Name' | translate }}: - {{ recurringDepositProduct.shortName }} -
- @if (recurringDepositProduct.description) { -
- {{ 'labels.inputs.Description' | translate }}: - {{ recurringDepositProduct.description }} -
- } -
-
+
+ +

{{ recurringDepositProduct.name }}

- -
-
- -

{{ 'labels.inputs.Currency' | translate }}

-
-
-
- {{ 'labels.inputs.Currency' | translate }}: - {{ - recurringDepositProduct.currencyCode | find: recurringDepositProductsTemplate.currencyOptions : 'code' : 'name' - }} -
-
- {{ 'labels.inputs.Decimal Places' | translate }}: - {{ - recurringDepositProduct.digitsAfterDecimal ?? recurringDepositProduct.currency?.decimalPlaces - }} -
- @if (recurringDepositProduct.inMultiplesOf) { -
- {{ 'labels.inputs.Currency in multiples of' | translate }}: - {{ recurringDepositProduct.inMultiplesOf }} -
- } -
-
+ + +

{{ 'labels.inputs.Details' | translate }}

+ - -
-
- -

{{ 'labels.heading.Terms' | translate }}

+
+ {{ 'labels.inputs.Short Name' | translate }}: + {{ recurringDepositProduct.shortName }}
-
-
- {{ 'labels.inputs.Deposit Amount' | translate }}: - {{ - recurringDepositProduct.depositAmount + - ' (Min: ' + - (recurringDepositProduct.minDepositAmount - ? recurringDepositProduct.minDepositAmount - : recurringDepositProduct.depositAmount) + - ', Max: ' + - (recurringDepositProduct.maxDepositAmount - ? recurringDepositProduct.maxDepositAmount - : recurringDepositProduct.depositAmount) + - ')' - }} -
-
- {{ 'labels.inputs.Interest Compounding Period' | translate }}: - {{ - recurringDepositProduct.interestCompoundingPeriodType - | find: recurringDepositProductsTemplate.interestCompoundingPeriodTypeOptions : 'id' : 'value' - }} -
-
- {{ 'labels.inputs.Interest Posting Period' | translate }}: - {{ - recurringDepositProduct.interestPostingPeriodType - | find: recurringDepositProductsTemplate.interestPostingPeriodTypeOptions : 'id' : 'value' - }} -
-
- {{ 'labels.inputs.Interest Calculated using' | translate }}: - {{ - recurringDepositProduct.interestCalculationType - | find: recurringDepositProductsTemplate.interestCalculationTypeOptions : 'id' : 'value' - }} -
-
- {{ 'labels.inputs.Days in Year' | translate }}: - {{ - recurringDepositProduct.interestCalculationDaysInYearType - | find: recurringDepositProductsTemplate.interestCalculationDaysInYearTypeOptions : 'id' : 'value' - }} -
-
-
- -
-
- -

{{ 'labels.heading.Settings' | translate }}

-
-
- @if (recurringDepositProduct.lockinPeriodFrequency) { -
- {{ 'labels.inputs.Lock-in Period' | translate }}: - {{ - recurringDepositProduct.lockinPeriodFrequency + - ' ' + - (recurringDepositProduct.lockinPeriodFrequencyType - | find: recurringDepositProductsTemplate.lockinPeriodFrequencyTypeOptions : 'id' : 'value') + @if (recurringDepositProduct.description) { +
+ {{ 'labels.inputs.Description' | translate }}: + {{ recurringDepositProduct.description }} +
+ } + + + @if ( + recurringDepositProduct.accountType || + recurringDepositProduct.customerSegment || + recurringDepositProduct.glAccountId || + recurringDepositProduct.effectiveTo || + recurringDepositProduct.introducerRequired !== undefined || + recurringDepositProduct.shareLinkingEnabled !== undefined + ) { +

Product Classification

+ + + @if (recurringDepositProduct.accountType) { +
+ Account Type: + {{ recurringDepositProduct.accountType }} +
+ } + + @if (recurringDepositProduct.customerSegment) { +
+ Customer Segment: + {{ recurringDepositProduct.customerSegment }} +
+ } + + @if (recurringDepositProduct.glAccountId) { +
+ GL Account (Liability): + {{ getGlAccountName(recurringDepositProduct.glAccountId) }} +
+ } + + @if (recurringDepositProduct.effectiveTo) { +
+ Effective To: + {{ recurringDepositProduct.effectiveTo | dateFormat }} +
+ } + + @if (recurringDepositProduct.endDate) { +
+ End Date: + {{ recurringDepositProduct.endDate | dateFormat }} +
+ } + + @if (recurringDepositProduct.introducerRequired !== undefined) { +
+ Introducer Required: + {{ + recurringDepositProduct.introducerRequired === true || recurringDepositProduct.introducerRequired === 'Yes' + ? 'Yes' + : 'No' }}
} -
- {{ 'labels.inputs.Minimum Deposit Term' | translate }}: - {{ - recurringDepositProduct.minDepositTerm + - ' ' + - (recurringDepositProduct.minDepositTermTypeId - | find: recurringDepositProductsTemplate.periodFrequencyTypeOptions : 'id' : 'value') + + @if (recurringDepositProduct.shareLinkingEnabled !== undefined) { +
+ Deposit Account Link: + {{ recurringDepositProduct.shareLinkingEnabled | yesNo }} +
+ } + } + + +

{{ 'labels.inputs.Currency' | translate }}

+ + +
+ {{ 'labels.inputs.Currency' | translate }}: + {{ + recurringDepositProduct.currencyCode | find: recurringDepositProductsTemplate.currencyOptions : 'code' : 'name' + }} +
+ +
+ {{ 'labels.inputs.Decimal Places' | translate }}: + {{ recurringDepositProduct.digitsAfterDecimal }} +
+ + @if (recurringDepositProduct.dormancyAlert !== undefined) { +
+ Dormancy Alert: + {{ recurringDepositProduct.dormancyAlert | yesNo }} +
+ } + + @if (recurringDepositProduct.balanceSwapOption !== undefined) { +
+ Balance Swap Option: + {{ recurringDepositProduct.balanceSwapOption | yesNo }} +
+ } + + @if (recurringDepositProduct.msme !== undefined) { +
+ MSME: + {{ recurringDepositProduct.msme | yesNo }} +
+ } + + @if (recurringDepositProduct.nominationFacility !== undefined) { +
+ Nomination Facility: + {{ recurringDepositProduct.nominationFacility | yesNo }} +
+ } + + @if (recurringDepositProduct.autoDebitFacility !== undefined) { +
+ Auto Debit Facility: + {{ recurringDepositProduct.autoDebitFacility | yesNo }} +
+ } + + +

{{ 'labels.inputs.Terms' | translate }}

+ + +
+ {{ 'labels.inputs.Nominal Annual Interest Rate' | translate }} (%): + {{ recurringDepositProduct.nominalAnnualInterestRate | formatNumber }} % +
+ +
+ {{ 'labels.inputs.Interest Compounding Period' | translate }}: + {{ + recurringDepositProduct.interestCompoundingPeriodType + | find: recurringDepositProductsTemplate.interestCompoundingPeriodTypeOptions : 'id' : 'value' + }} +
+ +
+ {{ 'labels.inputs.Interest Posting Period' | translate }}: + {{ + recurringDepositProduct.interestPostingPeriodType + | find: recurringDepositProductsTemplate.interestPostingPeriodTypeOptions : 'id' : 'value' + }} +
+ + @if (recurringDepositProduct.interestCalculateSelector) { +
+ {{ 'labels.inputs.Interest Calculation Type' | translate }}: + {{ + recurringDepositProduct.interestCalculateSelector + | find: recurringDepositProductsTemplate.interestCalculationTypeOption : 'id' : 'value' }}
- @if (recurringDepositProduct.inMultiplesOfDepositTerm) { -
- {{ 'labels.inputs.And thereafter, in Multiples of' | translate }}: - {{ - recurringDepositProduct.inMultiplesOfDepositTerm + - ' ' + - (recurringDepositProduct.inMultiplesOfDepositTermTypeId - | find: recurringDepositProductsTemplate.periodFrequencyTypeOptions : 'id' : 'value') - }} -
- } - @if (recurringDepositProduct.maxDepositTerm) { -
- {{ 'labels.inputs.Maximum Deposit Term' | translate }}: - {{ - recurringDepositProduct.maxDepositTerm + - ' ' + - (recurringDepositProduct.maxDepositTermTypeId - | find: recurringDepositProductsTemplate.periodFrequencyTypeOptions : 'id' : 'value') - }} -
- } -
- {{ 'labels.inputs.Apply Penal Interest (less)' | translate }}: - {{ recurringDepositProduct.preClosurePenalApplicable | yesNo }} -
- @if (recurringDepositProduct.preClosurePenalApplicable) { -
- {{ 'labels.inputs.Penal Interest' | translate }} (%): - {{ - recurringDepositProduct.preClosurePenalInterest + - '% (' + - (recurringDepositProduct.preClosurePenalInterestOnTypeId - | find: recurringDepositProductsTemplate.preClosurePenalInterestOnTypeOptions : 'id' : 'value') + - ')' - }} -
- } - @if (recurringDepositProduct.isMandatoryDeposit !== undefined) { -
- {{ 'labels.inputs.Recurring Deposit Type' | translate }}: - @if (recurringDepositProduct.isMandatoryDeposit) { - {{ 'labels.text.Mandatory' | translate }} - } - @if (!recurringDepositProduct.isMandatoryDeposit) { - {{ 'labels.text.Voluntary' | translate }} - } -
- } -
- {{ 'labels.inputs.Withhold Tax is Applicable' | translate }}: - {{ recurringDepositProduct.withHoldTax | yesNo }} -
- @if (recurringDepositProduct.withHoldTax) { -
- {{ 'labels.inputs.Tax Group' | translate }}: - {{ - recurringDepositProduct.taxGroupId | find: recurringDepositProductsTemplate.taxGroupOptions : 'id' : 'name' - }} -
- } -
-
+ } - -@if (recurringDepositProduct.charts.length !== 0) { -
-
- -

{{ 'labels.heading.Interest Rate Charts' | translate }}

+
+ {{ 'labels.inputs.Interest Calculated using' | translate }}: + {{ + recurringDepositProduct.interestCalculationType + | find: recurringDepositProductsTemplate.interestCalculationTypeOptions : 'id' : 'value' + | translateKey: 'catalogs' + }} +
+ +
+ {{ 'labels.inputs.Days in Year' | translate }}: + {{ + recurringDepositProduct.interestCalculationDaysInYearType + | find: recurringDepositProductsTemplate.interestCalculationDaysInYearTypeOptions : 'id' : 'value' + }} +
+ + @if (recurringDepositProduct.effectiveDate) { +
+ Effective Date: + {{ recurringDepositProduct.effectiveDate | dateFormat }}
-
- @for (chart of recurringDepositProduct.charts; track chart; let chartIndex = $index) { -
- @if (chart.name) { -
- {{ 'labels.inputs.Name' | translate }}: - {{ chart.name }} -
- } -
- {{ 'labels.inputs.From Date' | translate }}: - {{ chart.fromDate | dateFormat }} -
- @if (chart.endDate) { -
- {{ 'labels.inputs.End Date' | translate }}: - {{ chart.endDate | dateFormat }} -
- } - @if (chart.description) { -
- {{ 'labels.inputs.Description' | translate }}: - {{ chart.description }} -
- } -
- {{ 'labels.inputs.Primary Grouping by Amount' | translate }}: - {{ recurringDepositProduct.isPrimaryGroupingByAmount | yesNo }} -
- @if (chart.chartSlabs.length !== 0) { -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Period' | translate }} - {{ - chartSlab.fromPeriod + - ' - ' + - chartSlab.toPeriod + - ' ' + - (chartSlab.periodType - | find: recurringDepositProductsTemplate.chartTemplate.periodTypes : 'id' : 'value') - }} - {{ 'labels.inputs.Amount Range' | translate }} - {{ chartSlab.amountRangeFrom + ' - ' + chartSlab.amountRangeTo }} - {{ 'labels.inputs.Interest' | translate }}{{ chartSlab.annualInterestRate | formatNumber }} %{{ 'labels.inputs.Description' | translate }}{{ chartSlab.description }}{{ 'labels.inputs.Actions' | translate }} - - -
- -

{{ 'labels.heading.Incentives' | translate }}

-
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Entity Type' | translate }} - {{ - incentive.entityType - | find - : recurringDepositProductsTemplate.chartTemplate.entityTypeOptions - : 'id' - : 'value' - }} - {{ 'labels.inputs.Attribute Name' | translate }} - {{ - incentive.attributeName - | find - : recurringDepositProductsTemplate.chartTemplate.attributeNameOptions - : 'id' - : 'value' - }} - {{ 'labels.inputs.Condition Type' | translate }} - {{ - incentive.conditionType - | find - : recurringDepositProductsTemplate.chartTemplate.conditionTypeOptions - : 'id' - : 'value' - }} - {{ 'labels.inputs.Attribute Value' | translate }} - @switch (incentive.attributeName) { - @case (2) { - {{ - incentive.attributeValue - | find: recurringDepositProductsTemplate.chartTemplate.genderOptions : 'id' : 'name' - }} - } - @case (3) { - {{ incentive.attributeValue }} - } - @case (4) { - {{ - incentive.attributeValue - | find - : recurringDepositProductsTemplate.chartTemplate.clientTypeOptions - : 'id' - : 'name' - }} - } - @case (5) { - {{ - incentive.attributeValue - | find - : recurringDepositProductsTemplate.chartTemplate.clientClassificationOptions - : 'id' - : 'name' - }} - } - } - {{ 'labels.inputs.Incentive Type' | translate }} - {{ - incentive.incentiveType - | find - : recurringDepositProductsTemplate.chartTemplate.incentiveTypeOptions - : 'id' - : 'value' - }} - {{ 'labels.inputs.Amount' | translate }} - {{ incentive.amount | formatNumber }} -
-
-
-
-
-
- } + } + + @if (recurringDepositProduct.averagePeriod) { +
+ Average Period: + {{ recurringDepositProduct.averagePeriod }} +
+ } + +
+ Auto Interest Post: + {{ recurringDepositProduct.autoPost | yesNo }} +
+ + @if ( + recurringDepositProduct.frequencyFixDay !== undefined && + recurringDepositProduct.frequencyFixDay !== null && + recurringDepositProduct.frequencyFixDay !== '' + ) { +
+ Frequency (Fix Day): + {{ recurringDepositProduct.frequencyFixDay }} +
+ } + + @if (recurringDepositProduct.lastInterestDate) { +
+ Last Interest Date: + {{ recurringDepositProduct.lastInterestDate | dateFormat }} +
+ } + + +

{{ 'labels.heading.Settings' | translate }}

+ + +
+ Loan Facility: + {{ recurringDepositProduct.loanFacility | yesNo }} +
+ +
+ Premature Withdrawal: + {{ recurringDepositProduct.prematureWithdrawalOption | yesNo }} +
+ +
+ Tenure Flexibility: + {{ recurringDepositProduct.tenureFlexibility | yesNo }} +
+ +
+ Lien Marked: + {{ recurringDepositProduct.lienMarked | yesNo }} +
+ +
+ {{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}: + {{ recurringDepositProduct.withdrawalFeeForTransfers | yesNo }} +
+ +
+ Enable Lock-in Period: + {{ recurringDepositProduct.enableLockinPeriod | yesNo }} +
+ + @if (recurringDepositProduct.enableLockinPeriod && recurringDepositProduct.lockinPeriodFrequency) { +
+ Lock-in Period: + + {{ recurringDepositProduct.lockinPeriodFrequency }} + {{ getLockinPeriodFrequencyTypeLabel(recurringDepositProduct.lockinPeriodFrequencyType) }} + +
+ } + +
+ Maturity Instructions: + {{ recurringDepositProduct.maturityInstructions | yesNo }} +
+ +
+ Credit Maturity Proceeds: + {{ recurringDepositProduct.creditMaturityProceeds | yesNo }} +
+ +
+ Penalty Grace Period (Missed Installments): + {{ recurringDepositProduct.penaltyGracePeriodMissedInstallments | yesNo }} +
+ +
+ Panel Request Applicable: + {{ recurringDepositProduct.panelRequestApplicable | yesNo }} +
+ +
+ No Penalty (Nominee / Heir Closure): + {{ recurringDepositProduct.noPenaltyNomineeHeirClosure | yesNo }} +
+ +
+ Deceased Settlement: + {{ recurringDepositProduct.deceasedSettlement | yesNo }} +
+ +
+ Allow Debit Txn After Due Date: + {{ recurringDepositProduct.allowDebitTxnAfterDueDate | yesNo }} +
+ +
+ Account Closed with Zero Balance in Day Closing: + {{ recurringDepositProduct.accountClosedZeroBalanceInDayClosing | yesNo }} +
+ + @if (recurringDepositProduct.minBalanceForInterestCalculation) { +
+ Min Balance for Interest Calculation: + {{ recurringDepositProduct.minBalanceForInterestCalculation | formatNumber }} +
+ } + + + @if (recurringDepositProduct.rdSlabs?.length) { +

{{ 'labels.heading.Interest Rate Charts' | translate }}

+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
Sr.{{ slab.srNo }}Particular{{ slab.description }}From{{ slab.fromPeriod }}To{{ slab.toPeriod ?? '—' }}Period{{ slab.periodTypeLabel }}Rate (%) + {{ slab.annualInterestRate }} +
+
+ } + + + @if (recurringDepositProduct.charges?.length) { +
+

{{ 'labels.inputs.Charges' | translate }}

+ + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.Name' | translate }}{{ charge.name + ', ' + charge.currency.displaySymbol }}{{ 'labels.inputs.Type' | translate }}{{ charge.chargeCalculationType.value }}{{ 'labels.inputs.Amount' | translate }}{{ charge.amount | formatNumber }}{{ 'labels.inputs.Collected On' | translate }}{{ charge.chargeTimeType.value }}
+
+ } + + +

{{ 'labels.heading.Accounting' | translate }}

+ + + @if (isCashOrAccrualAccounting()) { +
+
+

GL Mapping

+
+ +
+
Assets
+
+ Savings Reference Account + {{ + formatGlAccountById( + recurringDepositProduct.savingsReferenceAccountId, + recurringDepositProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }}
+ @if (isAccrualAccounting()) { +
+ Fees Receivable Account + {{ + formatGlAccountById( + recurringDepositProduct.feesReceivableAccountId, + recurringDepositProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }} +
+
+ Penalties Receivable Account + {{ + formatGlAccountById( + recurringDepositProduct.penaltiesReceivableAccountId, + recurringDepositProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }} +
+ } +
+ +
+
Liabilities
+
+ Savings Control Account + {{ + formatGlAccountById( + recurringDepositProduct.savingsControlAccountId, + recurringDepositProductsTemplate?.accountingMappingOptions.liabilityAccountOptions + ) + }} +
+
+ Transfers In Suspense Account + {{ + formatGlAccountById( + recurringDepositProduct.transfersInSuspenseAccountId, + recurringDepositProductsTemplate?.accountingMappingOptions.liabilityAccountOptions + ) + }} +
+ @if (isAccrualAccounting()) { +
+ Interest Payable Account + {{ + formatGlAccountById( + recurringDepositProduct.interestPayableAccountId, + recurringDepositProductsTemplate?.accountingMappingOptions.liabilityAccountOptions + ) + }} +
+ } +
+ +
+
Expenses
+
+ Interest on Savings Account + {{ + formatGlAccountById( + recurringDepositProduct.interestOnSavingsAccountId, + recurringDepositProductsTemplate?.accountingMappingOptions.expenseAccountOptions + ) + }} +
+
+ +
+
Income
+
+ Income from Fees + {{ + formatGlAccountById( + recurringDepositProduct.incomeFromFeeAccountId, + recurringDepositProductsTemplate?.accountingMappingOptions.incomeAccountOptions + ) + }} +
+
+ Income from Penalties + {{ + formatGlAccountById( + recurringDepositProduct.incomeFromPenaltyAccountId, + recurringDepositProductsTemplate?.accountingMappingOptions.incomeAccountOptions + ) + }} +
+
+
+ } + + + @if (recurringDepositProduct.otherGlMappings) { +
+
+

Other GL Mappings

+
+ + @if (recurringDepositProduct.otherGlMappings.assets?.length) { + + + + + + + + + + + +
Asset Account{{ row.accountName }}GL Account + {{ + formatGlAccountByCode( + row.glCode, + recurringDepositProductsTemplate?.accountingMappingOptions.assetAccountOptions + ) + }} +
+ } + + @if (recurringDepositProduct.otherGlMappings.liabilities?.length) { + + + + + + + + + + + +
Liability Account{{ row.accountName }}GL Account + {{ + formatGlAccountByCode( + row.glCode, + recurringDepositProductsTemplate?.accountingMappingOptions.liabilityAccountOptions + ) + }} +
+ } + + @if (recurringDepositProduct.otherGlMappings.expenses?.length) { + + + + + + + + + + + +
Expense Account{{ row.accountName }}GL Account + {{ + formatGlAccountByCode( + row.glCode, + recurringDepositProductsTemplate?.accountingMappingOptions.expenseAccountOptions + ) + }} +
+ } + + @if (recurringDepositProduct.otherGlMappings.income?.length) { + + + + + + + + + + + +
Income Account{{ row.accountName }}GL Account + {{ + formatGlAccountByCode( + row.glCode, + recurringDepositProductsTemplate?.accountingMappingOptions.incomeAccountOptions + ) + }} +
}
-
-} + } - -@if (recurringDepositProduct.charges.length) { -
-
- -

{{ 'labels.heading.Charges' | translate }}

-
-
-
- - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Name' | translate }} - {{ charge.name + ', ' + charge.currency.displaySymbol }} - {{ 'labels.inputs.Type' | translate }} - {{ charge.chargeCalculationType.value }} - {{ 'labels.inputs.Amount' | translate }} - {{ charge.amount }} - {{ 'labels.inputs.Collected On' | translate }} - {{ charge.chargeTimeType.value }} -
-
-
-
-} - - -
-
- -

{{ 'labels.heading.Accounting' | translate }}

-
-
- @if (!isNoneAccounting()) { - - - } - - @if ( - recurringDepositProduct.advancedAccountingRules && - (recurringDepositProduct.paymentChannelToFundSourceMappings?.length || - recurringDepositProduct.feeToIncomeAccountMappings?.length || - recurringDepositProduct.penaltyToIncomeAccountMappings?.length) - ) { -

{{ 'labels.heading.Advanced Accounting Rules' | translate }}

+ + @if ( + recurringDepositProduct.advancedAccountingRules && + (recurringDepositProduct.paymentChannelToFundSourceMappings?.length || + recurringDepositProduct.feeToIncomeAccountMappings?.length || + recurringDepositProduct.penaltyToIncomeAccountMappings?.length) + ) { +
+

{{ 'labels.heading.Advanced Accounting Rules' | translate }}

+ @if (recurringDepositProduct.paymentChannelToFundSourceMappings?.length) { -
{{ 'labels.heading.Fund Sources for Payment Channels' | translate }}
-
- +
+

{{ 'labels.heading.Fund Sources for Payment Channels' | translate }}

+
- - - + [queryParams]="{ type: row.reportType, id: row.id }" + >
{{ 'labels.inputs.Payment Type' | translate }} - {{ - paymentFundSource.paymentTypeId - | find: recurringDepositProductsTemplate.paymentTypeOptions : 'id' : 'name' - }} + + {{ item.paymentTypeId | find: recurringDepositProductsTemplate.paymentTypeOptions : 'id' : 'name' }} {{ 'labels.inputs.Fund Source' | translate }} + {{ - paymentFundSource.fundSourceAccountId + item.fundSourceAccountId | find - : recurringDepositProductsTemplate.accountingMappingOptions.assetAccountOptions + : recurringDepositProductsTemplate?.accountingMappingOptions.assetAccountOptions : 'id' : 'name' }} @@ -512,22 +661,26 @@ } @if (recurringDepositProduct.feeToIncomeAccountMappings?.length) { -
{{ 'labels.heading.Fees to Specific Income Accounts' | translate }}
-
- +
+

{{ 'labels.heading.Fees to Specific Income Accounts' | translate }}

+
- - + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.Fees' | translate }} - {{ feesIncome.chargeId | find: recurringDepositProductsTemplate.chargeOptions : 'id' : 'name' }} + + {{ item.chargeId | find: recurringDepositProductsTemplate.chargeOptions : 'id' : 'name' }} {{ 'labels.inputs.Income Account' | translate }} + {{ - feesIncome.incomeAccountId + item.incomeAccountId | find - : recurringDepositProductsTemplate.accountingMappingOptions.incomeAccountOptions + : recurringDepositProductsTemplate?.accountingMappingOptions.incomeAccountOptions : 'id' : 'name' }} @@ -540,22 +693,26 @@ } @if (recurringDepositProduct.penaltyToIncomeAccountMappings?.length) { -
{{ 'labels.heading.Penalties to Specific Income Accounts' | translate }}
-
- +
+

{{ 'labels.heading.Penalties to Specific Income Accounts' | translate }}

+
- -
{{ 'labels.inputs.Penalty' | translate }} - {{ penaltyIncome.chargeId | find: recurringDepositProductsTemplate.penaltyOptions : 'id' : 'name' }} + + {{ item.chargeId | find: recurringDepositProductsTemplate.penaltyOptions : 'id' : 'name' }} {{ 'labels.inputs.Income Account' | translate }} + {{ - penaltyIncome.incomeAccountId + item.incomeAccountId | find - : recurringDepositProductsTemplate.accountingMappingOptions.incomeAccountOptions + : recurringDepositProductsTemplate?.accountingMappingOptions.incomeAccountOptions : 'id' : 'name' }} @@ -566,6 +723,6 @@
} - } - + + } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.scss b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.scss index 375a82861..33a0e1852 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.scss +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.scss @@ -6,108 +6,126 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -.rdpp-section { - border: 1px solid #e2e8f0; - border-radius: 10px; - overflow: hidden; - background: #fff; +@use 'assets/styles/colours' as *; - & + & { margin-top: 16px; } - - &__header { - display: flex; - align-items: center; - gap: 10px; - padding: 12px 24px; - background: #f8f9fa; - border-bottom: 1px solid #e8eaed; - - i { font-size: 14px; color: #0ea5e9; } - h3 { margin: 0; font-size: 14px; font-weight: 600; color: #202124; } - } - - &__body { padding: 16px 24px; } +.mat-h3 { + margin-top: 10px !important; + font-weight: 300; + color: $status-approved; } -.rdpp-product-name { - margin: 0 0 12px; - font-size: 18px; - font-weight: 600; - color: #202124; +.mat-elevation-z1 { + margin: 1em 0 1.5em; } -.rdpp-row { - display: flex; - padding: 6px 0; - border-bottom: 1px solid #f1f3f4; - - &:last-child { border-bottom: none; } -} - -.rdpp-label { - flex: 0 0 40%; - font-size: 13px; +h2, +h3, +h4 { + margin: 0; font-weight: 500; - color: #5f6368; -} - -.rdpp-value { - flex: 1; - font-size: 13px; - color: #202124; -} - -.rdpp-table-wrapper { - border: 1px solid #e2e8f0; - border-radius: 8px; - overflow: hidden; - margin-top: 8px; - - table { width: 100%; } - - & + & { margin-top: 12px; } -} - -.rdpp-sub-heading { - margin: 16px 0 8px; - font-size: 14px; - font-weight: 600; - color: #0ea5e9; -} - -.rdpp-sub-heading-sm { - margin: 12px 0 4px; - font-size: 13px; - font-weight: 600; - color: #202124; -} - -.rdpp-chart-block { - & + & { - margin-top: 16px; - padding-top: 16px; - border-top: 1px solid #e2e8f0; - } -} - -.rdpp-incentives { - overflow: hidden; - - &__card { - border-radius: 0; - padding: 12px 16px; - - h4 { - margin: 0 0 8px; - font-size: 13px; - font-weight: 600; - } - } } table { - tr.incentives-row { - height: 0; - > td { padding: 0; } + width: 100%; +} + +span { + margin: 0.5em 0; +} + +mat-divider { + margin: 0 0 0.5em; +} + +/* ── GL mapping summary (same as savings product preview) ── */ +.spa-gl-summary, +.spa-other-gl-summary { + display: flex; + flex-direction: column; + gap: 0.9rem; + margin-top: 1.25rem; +} + +.spa-gl-summary__header { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.25rem 0 0.5rem; +} + +.spa-gl-section { + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 1rem 1.15rem; + border: 1px solid #e3e7f2; + border-radius: 12px; + background: #fff; + + h5 { + font-size: 0.9rem; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #91a0bc; + } +} + +.spa-gl-row { + display: grid; + grid-template-columns: minmax(220px, 40%) 1fr; + gap: 1rem; + align-items: baseline; + padding: 0.2rem 0; +} + +.spa-gl-row__label { + color: #1f2f4a; + font-weight: 500; +} + +.spa-gl-row__value { + color: #101828; + font-weight: 600; + word-break: break-word; +} + +.spa-other-gl-table { + margin-top: 0.5rem; + margin-bottom: 0.75rem; + table-layout: fixed; + width: 100%; + + &:first-of-type { + margin-top: 0.25rem; + } + + th.mat-mdc-header-cell, + td.mat-mdc-cell { + padding-left: 1.25rem; + padding-right: 1.25rem; + } + + th.mat-mdc-header-cell:first-child, + td.mat-mdc-cell:first-child { + width: 38%; + } + + th.mat-mdc-header-cell:last-child, + td.mat-mdc-cell:last-child { + width: 62%; + word-break: break-word; + } +} + +.spa-other-gl-summary .mat-elevation-z1 { + border-radius: 10px; + overflow: hidden; +} + +@media (width <= 768px) { + .spa-gl-row { + grid-template-columns: 1fr; + gap: 0.35rem; } } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.ts b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.ts index d259e2e75..9aa3f0276 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.ts +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-preview-step/recurring-deposit-product-preview-step.component.ts @@ -7,9 +7,9 @@ */ import { Component, OnInit, Input, Output, EventEmitter, OnChanges, SimpleChanges, inject } from '@angular/core'; -import { trigger, state, transition, animate, style } from '@angular/animations'; -import { OptionData } from 'app/shared/models/option-data.model'; import { Accounting } from 'app/core/utils/accounting'; +import { OptionData } from 'app/shared/models/option-data.model'; +import { MatDivider } from '@angular/material/divider'; import { MatTable, MatColumnDef, @@ -23,7 +23,6 @@ import { MatRow } from '@angular/material/table'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; -import { ViewSavingsAccountingDetailsComponent } from '../../../../shared/accounting/view-savings-accounting-details/view-savings-accounting-details.component'; import { FindPipe } from '../../../../pipes/find.pipe'; import { DateFormatPipe } from '../../../../pipes/date-format.pipe'; import { FormatNumberPipe } from '../../../../pipes/format-number.pipe'; @@ -34,15 +33,9 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; selector: 'mifosx-recurring-deposit-product-preview-step', templateUrl: './recurring-deposit-product-preview-step.component.html', styleUrls: ['./recurring-deposit-product-preview-step.component.scss'], - animations: [ - trigger('expandChartSlab', [ - state('collapsed', style({ height: '0px', minHeight: '0' })), - state('expanded', style({ height: '*' })), - transition('expanded <=> collapsed', animate('225ms cubic-bezier(0.4, 0.0, 0.2, 1)')) - ]) - ], imports: [ ...STANDALONE_SHARED_IMPORTS, + MatDivider, MatTable, MatColumnDef, MatHeaderCellDef, @@ -54,7 +47,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; MatHeaderRow, MatRowDef, MatRow, - ViewSavingsAccountingDetailsComponent, FindPipe, DateFormatPipe, FormatNumberPipe, @@ -65,20 +57,11 @@ export class RecurringDepositProductPreviewStepComponent implements OnInit, OnCh private accounting = inject(Accounting); @Input() recurringDepositProductsTemplate: any; - @Input() chartSlabsDisplayedColumns: any[]; + @Input() chartSlabsDisplayedColumns: any[] = []; @Input() accountingRuleData: any; @Input() recurringDepositProduct: any; @Output() submitEvent = new EventEmitter(); - chartSlabsIncentivesDisplayedColumns: string[] = ['incentives']; - incentivesDisplayedColumns: string[] = [ - 'entityType', - 'attributeName', - 'conditionType', - 'attributeValue', - 'incentiveType', - 'amount' - ]; chargesDisplayedColumns: string[] = [ 'name', 'chargeCalculationType', @@ -94,72 +77,28 @@ export class RecurringDepositProductPreviewStepComponent implements OnInit, OnCh 'incomeAccountId' ]; - expandChartSlabIndex: number[] = []; + readonly rdSlabColumns = [ + 'srNo', + 'description', + 'fromPeriod', + 'toPeriod', + 'periodTypeLabel', + 'annualInterestRate' + ]; - accountingMappings: any = {}; - accountingRule: OptionData; + accountingRule: OptionData | undefined; ngOnInit() { this.setCurrentValues(); } - ngOnChanges(changes: SimpleChanges): void { + ngOnChanges(_changes: SimpleChanges): void { this.setCurrentValues(); } setCurrentValues(): void { - if (this.isCashOrAccrualAccounting()) { + if (this.recurringDepositProduct) { this.accountingRule = this.accounting.getAccountingRuleFrom(this.recurringDepositProduct.accountingRule); - - const assetAccountData = this.recurringDepositProductsTemplate.accountingMappingOptions.assetAccountOptions || []; - const incomeAccountData = - this.recurringDepositProductsTemplate.accountingMappingOptions.incomeAccountOptions || []; - const expenseAccountData = - this.recurringDepositProductsTemplate.accountingMappingOptions.expenseAccountOptions || []; - const liabilityAccountData = - this.recurringDepositProductsTemplate.accountingMappingOptions.liabilityAccountOptions || []; - - this.accountingMappings = { - savingsReferenceAccount: this.accounting.glAccountLookUp( - this.recurringDepositProduct.savingsReferenceAccountId, - assetAccountData - ), - savingsControlAccount: this.accounting.glAccountLookUp( - this.recurringDepositProduct.savingsControlAccountId, - liabilityAccountData - ), - transfersInSuspenseAccount: this.accounting.glAccountLookUp( - this.recurringDepositProduct.transfersInSuspenseAccountId, - liabilityAccountData - ), - interestOnSavingsAccount: this.accounting.glAccountLookUp( - this.recurringDepositProduct.interestOnSavingsAccountId, - expenseAccountData - ), - incomeFromFeeAccount: this.accounting.glAccountLookUp( - this.recurringDepositProduct.incomeFromFeeAccountId, - incomeAccountData - ), - incomeFromPenaltyAccount: this.accounting.glAccountLookUp( - this.recurringDepositProduct.incomeFromPenaltyAccountId, - incomeAccountData - ) - }; - - if (this.isAccrualAccounting()) { - this.accountingMappings['feeReceivableAccount'] = this.accounting.glAccountLookUp( - this.recurringDepositProduct.feesReceivableAccountId, - assetAccountData - ); - this.accountingMappings['penaltyReceivableAccount'] = this.accounting.glAccountLookUp( - this.recurringDepositProduct.penaltiesReceivableAccountId, - assetAccountData - ); - this.accountingMappings['interestPayableAccount'] = this.accounting.glAccountLookUp( - this.recurringDepositProduct.interestPayableAccountId, - liabilityAccountData - ); - } } } @@ -174,4 +113,30 @@ export class RecurringDepositProductPreviewStepComponent implements OnInit, OnCh isAccrualAccounting(): boolean { return this.accounting.isAccrualAccountingRuleId(this.recurringDepositProduct.accountingRule); } + + getLockinPeriodFrequencyTypeLabel(value: any): string { + if (!value && value !== 0) return ''; + const options = this.recurringDepositProductsTemplate?.lockinPeriodFrequencyTypeOptions || []; + const found = options.find((o: any) => o.id === value); + return found?.value || String(value); + } + + getGlAccountName(id: any): string { + if (!id) return ''; + const accounts = this.recurringDepositProductsTemplate?.accountingMappingOptions?.liabilityAccountOptions ?? []; + const found = accounts.find((a: any) => a.id === id || a.id === +id); + return found ? `${found.name} (${found.glCode})` : String(id); + } + + formatGlAccountById(glAccountId: number, options: any[]): string { + if (!glAccountId || !Array.isArray(options)) return '-'; + const account = this.accounting.glAccountLookUp(glAccountId, options as any); + return account ? `(${account.glCode}) ${account.name}` : '-'; + } + + formatGlAccountByCode(glCode: string, options: any[]): string { + if (!glCode || !Array.isArray(options)) return glCode || '-'; + const account = options.find((o: any) => o?.glCode === glCode); + return account ? `(${account.glCode}) ${account.name}` : glCode; + } } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.html index ff0f2d5d5..ca817dbd2 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.html @@ -7,215 +7,117 @@ -->
+
- -

{{ 'labels.inputs.Deposit Options' | translate }}

+ + Premature option
-
-
- - {{ 'labels.inputs.Is Mandatory Deposit' | translate }} - -
- -
- - {{ 'labels.inputs.Adjust advance payments toward future installments' | translate }} - -
- -
- - {{ 'labels.inputs.Allow Withdrawals' | translate }} - +
+
+ Loan Facility + Premature Withdrawal option + Tenure Flexibility + Lien Marked + Apply Withdrawal Fee for Transfers
+
- -

{{ 'labels.inputs.Lock-in Period' | translate }}

+ + {{ 'labels.inputs.Lock-in Period' | translate }}
-
- - {{ 'labels.inputs.Frequency' | translate }} - - - - - {{ 'labels.inputs.Type' | translate }} - - @for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) { - - {{ lockinPeriodFrequencyType.value }} - - } - - -
-
- -
-
- -

{{ 'labels.inputs.Minimum Deposit Term' | translate }}

-
-
- - {{ 'labels.inputs.Frequency' | translate }} - - - {{ 'labels.inputs.Minimum Deposit Term Frequency' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.Frequency Type' | translate }} - - @for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) { - - {{ periodFrequencyType.value }} - - } - - - {{ 'labels.inputs.Minimum Deposit Term Frequency Type' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - -
-
- -
-
- -

{{ 'labels.heading.And thereafter, in Multiples of' | translate }}

-
-
- - {{ 'labels.inputs.Frequency' | translate }} - - - - - {{ 'labels.inputs.Type' | translate }} - - @for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) { - - {{ periodFrequencyType.value }} - - } - - -
-
- -
-
- -

{{ 'labels.inputs.Maximum Deposit Term' | translate }}

-
-
- - {{ 'labels.inputs.Frequency' | translate }} - - - - - {{ 'labels.inputs.Type' | translate }} - - @for (periodFrequencyType of periodFrequencyTypeData; track periodFrequencyType) { - - {{ periodFrequencyType.value }} - - } - - -
-
- -
-
- -

{{ 'labels.heading.For Pre-mature closure' | translate }}

-
-
-
- - {{ 'labels.inputs.Apply Penal Interest (less)' | translate }} - +
+
+ Enable Lock-in Period +
- - {{ 'labels.inputs.Penal Interest' | translate }} (%) - - + @if (recurringDepositProductSettingsForm.value.enableLockinPeriod) { +
+ + Frequency* + + - - {{ 'labels.inputs.Period' | translate }} - - @for (preClosurePenalInterestOnType of preClosurePenalInterestOnTypeData; track preClosurePenalInterestOnType) { - - {{ preClosurePenalInterestOnType.value }} - - } - - -
-
- -
-
- -

{{ 'labels.inputs.Tax' | translate }}

-
-
-
- - {{ 'labels.inputs.Is Withhold Tax Applicable' | translate }} - -
- - @if (recurringDepositProductSettingsForm.value.withHoldTax) { - - {{ 'labels.inputs.Tax Group' | translate }} - - @for (taxGroup of taxGroupData; track taxGroup) { - - {{ taxGroup.name }} - - } - - - {{ 'labels.inputs.Tax Group' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - + + Type* + + @for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) { + + {{ lockinPeriodFrequencyType.value | translateKey: 'catalogs' }} + + } + + +
}
+ + +
+
+ + Maturity Instructions +
+
+
+ Maturity Instructions + +
+
+ Option to credit maturity proceeds directly to the linked account or reinvest in another product (like + FD). + +
+
+ Penalty / Grace Period for Missed Installments + +
+
+ Panel Request Applicable + +
+
+ No penalty for nominee / legal heir closure + +
+
+ Deceased Settlement + +
+
+ Allow Debit Txn after due date + +
+
+
+ + +
+
+ + Additional Rules +
+
+
+
+ + Account Closed with Zero balance in Day Closing + +
+ + + Balance Required for Interest Calculation + + +
+
+
diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.scss b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.scss index c4670dd8f..53e82e31e 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.scss +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.scss @@ -6,8 +6,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +$accent: #0ea5e9; +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-900: #202124; + .rdps-section { - border: 1px solid #e2e8f0; + border: 1px solid $border; border-radius: 10px; overflow: hidden; background: #fff; @@ -21,28 +27,37 @@ align-items: center; gap: 10px; padding: 12px 24px; - background: #f8f9fa; - border-bottom: 1px solid #e8eaed; + background: $gray-50; + border-bottom: 1px solid $gray-200; i { font-size: 14px; - color: #0ea5e9; + color: $accent; } - h3 { + span:not(.rdps-section__badge) { margin: 0; font-size: 14px; font-weight: 600; - color: #202124; + color: $gray-900; } } } -.rdps-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 4px 16px; - padding: 16px 24px; +.rdps-section__badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: #eff6ff; + font-size: 14px; +} + +.rdps-body { + background: #fff; + padding: 18px 24px 24px; } .rdps-field { @@ -53,14 +68,98 @@ } } -.rdps-checkbox-row { +.rdps-premature-row { display: flex; align-items: center; - padding: 4px 0; + gap: 24px; + flex-wrap: wrap; + + mat-checkbox { + font-size: 14px; + color: $gray-900; + } } -@media (width <= 600px) { - .rdps-grid { +.rdps-layout { + display: grid; + gap: 16px; +} + +.rdps-layout--additional { + grid-template-columns: repeat(2, minmax(0, 1fr)); + + @media (width <= 1100px) { grid-template-columns: 1fr; } } + +.rdps-toggle { + display: flex; + align-items: center; + min-height: 40px; + + mat-checkbox { + font-size: 14px; + color: $gray-900; + } + + &--full { + min-height: auto; + margin-bottom: 16px; + } +} + +.rdps-body--list { + padding: 0 24px; +} + +.rdps-list-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 14px 0; + border-bottom: 1px solid $gray-200; + font-size: 14px; + color: $gray-900; + + &:last-child { + border-bottom: none; + } + + span { + max-width: 85%; + line-height: 1.4; + } +} + +.rdps-lockin-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 16px; + padding-bottom: 16px; + border-bottom: 1px solid $gray-200; + font-size: 14px; + color: $gray-900; +} + +.rdps-lockin-fields { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + + &--single { + grid-template-columns: minmax(0, 1fr); + max-width: 50%; + } + + @media (width <= 640px) { + grid-template-columns: 1fr; + + &--single { + max-width: 100%; + } + } +} diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.ts b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.ts index a9b559218..74c8ca844 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.ts +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-settings-step/recurring-deposit-product-settings-step.component.ts @@ -7,15 +7,8 @@ */ import { Component, OnInit, Input, inject } from '@angular/core'; -import { - UntypedFormGroup, - UntypedFormBuilder, - UntypedFormControl, - Validators, - ReactiveFormsModule -} from '@angular/forms'; +import { UntypedFormGroup, UntypedFormBuilder, ReactiveFormsModule } from '@angular/forms'; import { MatCheckbox } from '@angular/material/checkbox'; -import { MatTooltip } from '@angular/material/tooltip'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ @@ -24,8 +17,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; styleUrls: ['./recurring-deposit-product-settings-step.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, - MatCheckbox, - MatTooltip + MatCheckbox ] }) export class RecurringDepositProductSettingsStepComponent implements OnInit { @@ -36,106 +28,68 @@ export class RecurringDepositProductSettingsStepComponent implements OnInit { recurringDepositProductSettingsForm: UntypedFormGroup; lockinPeriodFrequencyTypeData: any; - periodFrequencyTypeData: any; - preClosurePenalInterestOnTypeData: any; - taxGroupData: any; constructor() { this.createrecurringDepositProductSettingsForm(); - this.setConditionalControls(); } ngOnInit() { this.lockinPeriodFrequencyTypeData = this.recurringDepositProductsTemplate.lockinPeriodFrequencyTypeOptions; - this.periodFrequencyTypeData = this.recurringDepositProductsTemplate.periodFrequencyTypeOptions.slice(0, -1); - this.preClosurePenalInterestOnTypeData = this.recurringDepositProductsTemplate.preClosurePenalInterestOnTypeOptions; - this.taxGroupData = this.recurringDepositProductsTemplate.taxGroupOptions; - if (!(this.recurringDepositProductsTemplate === undefined) && this.recurringDepositProductsTemplate.id) { + if (this.recurringDepositProductsTemplate?.id) { this.recurringDepositProductSettingsForm.patchValue({ - isMandatoryDeposit: this.recurringDepositProductsTemplate.isMandatoryDeposit, - adjustAdvanceTowardsFuturePayments: this.recurringDepositProductsTemplate.adjustAdvanceTowardsFuturePayments, - allowWithdrawal: this.recurringDepositProductsTemplate.allowWithdrawal, - lockinPeriodFrequency: this.recurringDepositProductsTemplate.lockinPeriodFrequency, - lockinPeriodFrequencyType: this.recurringDepositProductsTemplate.lockinPeriodFrequencyType - ? this.recurringDepositProductsTemplate.lockinPeriodFrequencyType.id - : '', - minDepositTerm: this.recurringDepositProductsTemplate.minDepositTerm, - minDepositTermTypeId: this.recurringDepositProductsTemplate.minDepositTermType - ? this.recurringDepositProductsTemplate.minDepositTermType.id - : '', - inMultiplesOfDepositTerm: this.recurringDepositProductsTemplate.inMultiplesOfDepositTerm, - inMultiplesOfDepositTermTypeId: this.recurringDepositProductsTemplate.inMultiplesOfDepositTermType - ? this.recurringDepositProductsTemplate.inMultiplesOfDepositTerm.id - : '', - maxDepositTerm: this.recurringDepositProductsTemplate.maxDepositTerm, - maxDepositTermTypeId: this.recurringDepositProductsTemplate.maxDepositTermType - ? this.recurringDepositProductsTemplate.minDepositTermType.id - : '', - preClosurePenalApplicable: this.recurringDepositProductsTemplate.preClosurePenalApplicable, - preClosurePenalInterest: this.recurringDepositProductsTemplate.preClosurePenalInterest, - preClosurePenalInterestOnTypeId: this.recurringDepositProductsTemplate.preClosurePenalInterestOnType - ? this.recurringDepositProductsTemplate.preClosurePenalInterestOnType.id - : '', - withHoldTax: this.recurringDepositProductsTemplate.withHoldTax - }); - } - - if (this.recurringDepositProductsTemplate.withHoldTax) { - this.recurringDepositProductSettingsForm.patchValue({ - taxGroupId: this.recurringDepositProductsTemplate.taxGroup - ? this.recurringDepositProductsTemplate.taxGroup.id - : '' + loanFacility: this.recurringDepositProductsTemplate.loanFacility ?? false, + prematureWithdrawalOption: this.recurringDepositProductsTemplate.prematureWithdrawalOption ?? false, + tenureFlexibility: this.recurringDepositProductsTemplate.tenureFlexibility ?? false, + lienMarked: this.recurringDepositProductsTemplate.lienMarked ?? false, + withdrawalFeeForTransfers: this.recurringDepositProductsTemplate.withdrawalFeeForTransfers ?? false, + enableLockinPeriod: this.recurringDepositProductsTemplate.enableLockinPeriod ?? false, + lockinPeriodFrequency: this.recurringDepositProductsTemplate.lockinPeriodFrequency ?? '', + lockinPeriodFrequencyType: this.recurringDepositProductsTemplate.lockinPeriodFrequencyType?.id ?? '', + maturityInstructions: this.recurringDepositProductsTemplate.maturityInstructions ?? false, + creditMaturityProceeds: this.recurringDepositProductsTemplate.creditMaturityProceeds ?? false, + penaltyGracePeriodMissedInstallments: + this.recurringDepositProductsTemplate.penaltyGracePeriodMissedInstallments ?? false, + panelRequestApplicable: this.recurringDepositProductsTemplate.panelRequestApplicable ?? false, + noPenaltyNomineeHeirClosure: this.recurringDepositProductsTemplate.noPenaltyNomineeHeirClosure ?? false, + deceasedSettlement: this.recurringDepositProductsTemplate.deceasedSettlement ?? false, + allowDebitTxnAfterDueDate: this.recurringDepositProductsTemplate.allowDebitTxnAfterDueDate ?? false, + accountClosedZeroBalanceInDayClosing: + this.recurringDepositProductsTemplate.accountClosedZeroBalanceInDayClosing ?? false, + minBalanceForInterestCalculation: this.recurringDepositProductsTemplate.minBalanceForInterestCalculation ?? '' }); } } createrecurringDepositProductSettingsForm() { this.recurringDepositProductSettingsForm = this.formBuilder.group({ - isMandatoryDeposit: [false], - adjustAdvanceTowardsFuturePayments: [false], - allowWithdrawal: [false], + loanFacility: [false], + prematureWithdrawalOption: [false], + tenureFlexibility: [false], + lienMarked: [false], + withdrawalFeeForTransfers: [false], + enableLockinPeriod: [false], lockinPeriodFrequency: [''], lockinPeriodFrequencyType: [''], - minDepositTerm: [ - '', - Validators.required - ], - minDepositTermTypeId: [ - '', - Validators.required - ], - inMultiplesOfDepositTerm: [''], - inMultiplesOfDepositTermTypeId: [''], - maxDepositTerm: [''], - maxDepositTermTypeId: [''], - preClosurePenalApplicable: [false], - preClosurePenalInterest: [''], - preClosurePenalInterestOnTypeId: [''], - withHoldTax: [false] - }); - } - - setConditionalControls() { - this.recurringDepositProductSettingsForm.get('withHoldTax').valueChanges.subscribe((withHoldTax: any) => { - if (withHoldTax) { - this.recurringDepositProductSettingsForm.addControl( - 'taxGroupId', - new UntypedFormControl('', Validators.required) - ); - } else { - this.recurringDepositProductSettingsForm.removeControl('taxGroupId'); - } + maturityInstructions: [false], + creditMaturityProceeds: [false], + penaltyGracePeriodMissedInstallments: [false], + panelRequestApplicable: [false], + noPenaltyNomineeHeirClosure: [false], + deceasedSettlement: [false], + allowDebitTxnAfterDueDate: [false], + accountClosedZeroBalanceInDayClosing: [false], + minBalanceForInterestCalculation: [''] }); } get recurringDepositProductSettings() { - const recurringDepositProductSettings = this.recurringDepositProductSettingsForm.value; - for (const key in recurringDepositProductSettings) { - if (recurringDepositProductSettings[key] === '') { - delete recurringDepositProductSettings[key]; + const settings = { ...this.recurringDepositProductSettingsForm.value }; + for (const key in settings) { + if (settings[key] === '') { + delete settings[key]; } } - return recurringDepositProductSettings; + return settings; } } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.html index b5a55f78e..df3d7e0f7 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.html @@ -9,99 +9,145 @@
- -

{{ 'labels.inputs.Deposit Amount' | translate }}

+ + Interest Rate & Posting Terms
-
- - {{ 'labels.inputs.Minimum' | translate }} - - +
+
+ ℹ️ + + Recurring Deposit base rate: + {{ recurringDepositProductTermsForm.get('nominalAnnualInterestRate').value }}% + +
- - {{ 'labels.inputs.Default' | translate }} - - - {{ 'labels.inputs.Default' | translate }} {{ 'labels.inputs.Deposit Amount' | translate }} - {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.Maximum' | translate }} - - -
-
- -
-
- -

{{ 'labels.inputs.Interest Compounding Period' | translate }}

-
-
- - {{ 'labels.inputs.Interest Compounding Period' | translate }} - - @for (interestCompoundingPeriodType of interestCompoundingPeriodTypeData; track interestCompoundingPeriodType) { - - {{ interestCompoundingPeriodType.value }} - +
+ + {{ 'labels.inputs.Nominal Annual Interest' | translate }} + + @if (recurringDepositProductTermsForm.get('nominalAnnualInterestRate').hasError('required')) { + + {{ 'labels.inputs.Nominal Annual Interest' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + } - - - {{ 'labels.inputs.Interest Compounding Period' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.Interest Posting Period' | translate }} - - @for (interestPostingPeriodType of interestPostingPeriodTypeData; track interestPostingPeriodType) { - - {{ interestPostingPeriodType.value }} - + @if (recurringDepositProductTermsForm.get('nominalAnnualInterestRate').hasError('min')) { + {{ 'labels.inputs.Nominal Annual Interest' | translate }} must be zero or greater } - - - {{ 'labels.inputs.Interest Posting Period' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - + - - {{ 'labels.inputs.Interest Calculated using' | translate }} - - @for (interestCalculationType of interestCalculationTypeData; track interestCalculationType) { - - {{ interestCalculationType.value }} - - } - - - {{ 'labels.inputs.Interest Calculated using' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - + + Effective Date + + + + - - {{ 'labels.inputs.Days in Year' | translate }} - - @for ( - interestCalculationDaysInYearType of interestCalculationDaysInYearTypeData; - track interestCalculationDaysInYearType - ) { - - {{ interestCalculationDaysInYearType.value }} - - } - - - {{ 'labels.inputs.Days in Year' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - + + {{ 'labels.inputs.Interest Calculation Type' | translate }} + + @for (opt of interestCalculationTypeOptionData; track opt.id) { + {{ opt.value }} + } + + +
+ +
+ + Interest Calculate + + @for (mode of interestCalculationTypeData; track mode.id) { + {{ mode.value | translateKey: 'catalogs' }} + } + + + {{ 'labels.inputs.Interest Calculation Type' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + + + @if (recurringDepositProductTermsForm.value.interestCalculationType === 2) { + + Average Period + + @for (period of periodFrequencyData; track period.id) { + {{ period.value }} + } + + + } + + + {{ 'labels.inputs.Interest Posting Period' | translate }} + + @for (interestPostingPeriodType of interestPostingPeriodTypeData; track interestPostingPeriodType) { + + {{ interestPostingPeriodType.value | translateKey: 'catalogs' }} + + } + + + {{ 'labels.inputs.Interest Posting Period' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + +
+ +
diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.scss b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.scss index 4fcbd4861..e5e2608bc 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.scss +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.scss @@ -6,8 +6,14 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ +$accent: #0ea5e9; +$border: #e2e8f0; +$gray-50: #f8f9fa; +$gray-200: #e8eaed; +$gray-900: #202124; + .rdpt-section { - border: 1px solid #e2e8f0; + border: 1px solid $border; border-radius: 10px; overflow: hidden; background: #fff; @@ -21,23 +27,39 @@ align-items: center; gap: 10px; padding: 12px 24px; - background: #f8f9fa; - border-bottom: 1px solid #e8eaed; + background: $gray-50; + border-bottom: 1px solid $gray-200; i { font-size: 14px; - color: #0ea5e9; + color: $accent; } - h3 { + h3, + span:not(.rdpt-section__badge) { margin: 0; font-size: 14px; font-weight: 600; - color: #202124; + color: $gray-900; } } } +.rdpt-section__badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: #eff6ff; + font-size: 14px; +} + +.rdpt-body { + background: #fff; +} + .rdpt-grid { display: grid; grid-template-columns: repeat(2, 1fr); @@ -47,6 +69,14 @@ &--three { grid-template-columns: repeat(3, 1fr); } + + @media (width <= 600px) { + grid-template-columns: 1fr; + + &--three { + grid-template-columns: 1fr; + } + } } .rdpt-field { @@ -57,12 +87,66 @@ } } -@media (width <= 600px) { - .rdpt-grid { - grid-template-columns: 1fr; +.rdpt-banner { + display: flex; + align-items: center; + gap: 10px; + margin: 16px 24px 0; + padding: 12px 14px; + background: #eff6ff; + border: 1px solid #dbeafe; + border-radius: 8px; + color: #1d4ed8; + font-size: 13px; - &--three { - grid-template-columns: 1fr; - } + &__icon { + font-size: 14px; } } + +.rdpt-footer { + padding: 0 24px 24px; + border-top: 1px solid #f1f5f9; +} + +.rdpt-footer__grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + padding-top: 18px; + + @media (width <= 900px) { + grid-template-columns: 1fr; + } +} + +.rdpt-toggle-card { + padding: 12px 0; + + h4 { + margin: 0 0 8px; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + + &__choices { + display: flex; + align-items: center; + gap: 16px; + flex-wrap: wrap; + } +} + +.rdpt-note { + display: flex; + align-items: center; + gap: 10px; + margin-top: 18px; + padding: 12px 14px; + border: 1px solid #fde68a; + background: #fffbeb; + color: #b45309; + border-radius: 8px; + font-size: 12px; +} diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.ts b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.ts index 81b8af313..838f3e42e 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.ts +++ b/src/app/products/recurring-deposit-products/recurring-deposit-product-stepper/recurring-deposit-product-terms-step/recurring-deposit-product-terms-step.component.ts @@ -7,7 +7,10 @@ */ import { Component, OnInit, Input, inject } from '@angular/core'; -import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; +import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; +import { MatTooltip } from '@angular/material/tooltip'; +import { Dates } from 'app/core/utils/dates'; +import { SettingsService } from 'app/settings/settings.service'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ @@ -15,11 +18,14 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; templateUrl: './recurring-deposit-product-terms-step.component.html', styleUrls: ['./recurring-deposit-product-terms-step.component.scss'], imports: [ - ...STANDALONE_SHARED_IMPORTS + ...STANDALONE_SHARED_IMPORTS, + MatTooltip ] }) export class RecurringDepositProductTermsStepComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); + private dateUtils = inject(Dates); + private settingsService = inject(SettingsService); @Input() recurringDepositProductsTemplate: any; @@ -28,7 +34,13 @@ export class RecurringDepositProductTermsStepComponent implements OnInit { interestCompoundingPeriodTypeData: any; interestPostingPeriodTypeData: any; interestCalculationTypeData: any; + interestCalculationTypeOptionData: any; interestCalculationDaysInYearTypeData: any; + periodFrequencyData: any[] = []; + yesNoOptions: Array<{ value: boolean; label: string }> = [ + { value: true, label: 'Yes' }, + { value: false, label: 'No' } + ]; constructor() { this.createrecurringDepositProductTermsForm(); @@ -37,41 +49,78 @@ export class RecurringDepositProductTermsStepComponent implements OnInit { ngOnInit() { this.interestCompoundingPeriodTypeData = this.recurringDepositProductsTemplate.interestCompoundingPeriodTypeOptions; this.interestPostingPeriodTypeData = this.recurringDepositProductsTemplate.interestPostingPeriodTypeOptions; + // Standard enum (1=Daily, 2=Average) → "Interest Calculate" dropdown → interestCalculationType form control this.interestCalculationTypeData = this.recurringDepositProductsTemplate.interestCalculationTypeOptions; + // CBS code values → "Interest Calculation Type" dropdown → interestCalculateSelector form control + this.interestCalculationTypeOptionData = this.recurringDepositProductsTemplate.interestCalculationTypeOption || []; this.interestCalculationDaysInYearTypeData = this.recurringDepositProductsTemplate.interestCalculationDaysInYearTypeOptions; - if (!(this.recurringDepositProductsTemplate === undefined) && this.recurringDepositProductsTemplate.id) { - this.recurringDepositProductTermsForm.patchValue({ - minDepositAmount: this.recurringDepositProductsTemplate.minDepositAmount, - depositAmount: this.recurringDepositProductsTemplate.depositAmount, - maxDepositAmount: this.recurringDepositProductsTemplate.maxDepositAmount - }); - } + this.periodFrequencyData = this.recurringDepositProductsTemplate.periodFrequencyOptions || []; + this.setYesNoOptions(this.recurringDepositProductsTemplate?.yesNoOptions); + + const isEditing = !!this.recurringDepositProductsTemplate.id; + const defaultPeriod = this.periodFrequencyData[0]?.value || 'Monthly'; + const savedEnumTypeId = + this.recurringDepositProductsTemplate.interestCalculationType?.id || + this.interestCalculationTypeData?.[0]?.id || + 1; + const savedSelectorId = isEditing + ? (this.recurringDepositProductsTemplate.interestCalculateSelector ?? + this.interestCalculationTypeOptionData?.[0]?.id ?? + null) + : (this.interestCalculationTypeOptionData?.[0]?.id ?? null); + this.recurringDepositProductTermsForm.patchValue({ - interestCompoundingPeriodType: this.recurringDepositProductsTemplate.interestCompoundingPeriodType.id, - interestPostingPeriodType: this.recurringDepositProductsTemplate.interestPostingPeriodType.id, - interestCalculationType: this.recurringDepositProductsTemplate.interestCalculationType.id, - interestCalculationDaysInYearType: this.recurringDepositProductsTemplate.interestCalculationDaysInYearType.id + nominalAnnualInterestRate: this.recurringDepositProductsTemplate.nominalAnnualInterestRate ?? 0, + interestCompoundingPeriodType: this.recurringDepositProductsTemplate.interestCompoundingPeriodType?.id, + interestPostingPeriodType: this.recurringDepositProductsTemplate.interestPostingPeriodType?.id, + interestCalculationType: savedEnumTypeId, + interestCalculationDaysInYearType: this.recurringDepositProductsTemplate.interestCalculationDaysInYearType?.id, + effectiveDate: this.recurringDepositProductsTemplate.effectiveDate + ? this.parseDate(this.recurringDepositProductsTemplate.effectiveDate) + : '', + interestCalculateSelector: savedSelectorId, + averagePeriod: isEditing ? this.recurringDepositProductsTemplate.averagePeriod || defaultPeriod : defaultPeriod, + autoPost: isEditing ? (this.recurringDepositProductsTemplate.autoPost ?? true) : true, + frequencyFixDay: isEditing ? (this.recurringDepositProductsTemplate.frequencyFixDay ?? 30) : 30, + lastInterestDate: this.recurringDepositProductsTemplate.lastInterestDate + ? this.parseDate(this.recurringDepositProductsTemplate.lastInterestDate) + : '' }); } + private parseDate(dateValue: any): Date | null { + if (!dateValue) return null; + if (Array.isArray(dateValue)) { + return new Date(dateValue[0], dateValue[1] - 1, dateValue[2]); + } + if (dateValue instanceof Date) return dateValue; + return new Date(dateValue); + } + + private setYesNoOptions(options: any[]) { + if (!Array.isArray(options)) return; + const mapped = options + .map((opt: any) => { + const raw = opt?.name ?? opt?.value ?? opt; + const norm = typeof raw === 'string' ? raw.trim().toUpperCase() : raw; + if (norm === 'YES') return { value: true, label: 'Yes' }; + if (norm === 'NO') return { value: false, label: 'No' }; + return null; + }) + .filter(Boolean); + if (mapped.length) this.yesNoOptions = mapped; + } + createrecurringDepositProductTermsForm() { this.recurringDepositProductTermsForm = this.formBuilder.group({ - minDepositAmount: [ - '', - Validators.min(0) - ], - depositAmount: [ + nominalAnnualInterestRate: [ '', [ Validators.required, Validators.min(0) ] ], - maxDepositAmount: [ - '', - Validators.min(0) - ], interestCompoundingPeriodType: [ '', Validators.required @@ -87,17 +136,34 @@ export class RecurringDepositProductTermsStepComponent implements OnInit { interestCalculationDaysInYearType: [ '', Validators.required - ] + ], + effectiveDate: [''], + interestCalculateSelector: [1], + averagePeriod: ['Monthly'], + autoPost: [true], + frequencyFixDay: [ + 30, + [Validators.min(0)] + ], + lastInterestDate: [''] }); } get recurringDepositProductTerms() { - const recurringDepositProductTerms = this.recurringDepositProductTermsForm.value; - for (const key in recurringDepositProductTerms) { - if (recurringDepositProductTerms[key] === '') { - delete recurringDepositProductTerms[key]; + const formValue = { ...this.recurringDepositProductTermsForm.value }; + const dateFormat = this.settingsService.dateFormat; + + if (formValue.effectiveDate instanceof Date) { + formValue.effectiveDate = this.dateUtils.formatDate(formValue.effectiveDate, dateFormat) || ''; + } + if (formValue.lastInterestDate instanceof Date) { + formValue.lastInterestDate = this.dateUtils.formatDate(formValue.lastInterestDate, dateFormat) || ''; + } + for (const key in formValue) { + if (formValue[key] === '') { + delete formValue[key]; } } - return recurringDepositProductTerms; + return formValue; } } diff --git a/src/app/products/recurring-deposit-products/recurring-deposit-products.component.html b/src/app/products/recurring-deposit-products/recurring-deposit-products.component.html index 5523e15cc..a2e463e62 100644 --- a/src/app/products/recurring-deposit-products/recurring-deposit-products.component.html +++ b/src/app/products/recurring-deposit-products/recurring-deposit-products.component.html @@ -20,6 +20,18 @@ {{ 'labels.buttons.Create Recurring Deposit Product' | translate }}
+
+ +
diff --git a/src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates-tab.component.html b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates-tab.component.html new file mode 100644 index 000000000..2295a36d2 --- /dev/null +++ b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates-tab.component.html @@ -0,0 +1,68 @@ + + +
+ @if (!rdInterestRates || rdInterestRates.length === 0) { +

No CBS base interest rate versions defined.

+ } + + + @for (version of rdInterestRates; track version.id) { + + + + + {{ version.versionLabel }} + + {{ version.isActive ? 'Active' : 'Inactive' }} + + Effective from: {{ version.effectiveFromDate }} + + + + + @if (version.remarks) { +

+ {{ version.remarks }} +

+ } + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Sr.{{ slab.srNo }}Particular{{ slab.description }}From{{ slab.fromPeriod }}To{{ slab.toPeriod ?? '—' }}Period Type{{ slab.periodTypeLabel }}Annual Rate (%) + {{ slab.annualInterestRate | number: '1.2-2' }} +
+
+ } +
+
diff --git a/src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates-tab.component.scss b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates-tab.component.scss new file mode 100644 index 000000000..1728940c9 --- /dev/null +++ b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates-tab.component.scss @@ -0,0 +1,40 @@ +/** + * 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/. + */ + +.rd-version-header { + display: flex; + align-items: center; + gap: 12px; +} + +.rd-active-badge { + background: #4caf50; + color: #fff; + border-radius: 4px; + padding: 2px 8px; + font-size: 12px; +} + +.rd-inactive-badge { + background: #9e9e9e; + color: #fff; + border-radius: 4px; + padding: 2px 8px; + font-size: 12px; +} + +.rd-slab-table { + width: 100%; + margin-top: 8px; +} + +.no-rates { + padding: 24px; + color: #757575; + text-align: center; +} diff --git a/src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates-tab.component.ts b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates-tab.component.ts new file mode 100644 index 000000000..79c7ac552 --- /dev/null +++ b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates-tab.component.ts @@ -0,0 +1,66 @@ +/** + * 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 { ActivatedRoute } from '@angular/router'; +import { DecimalPipe } from '@angular/common'; +import { + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow +} from '@angular/material/table'; +import { MatExpansionModule } from '@angular/material/expansion'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +@Component({ + selector: 'mifosx-rd-interest-rates-tab', + templateUrl: './rd-interest-rates-tab.component.html', + styleUrls: ['./rd-interest-rates-tab.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + MatTable, + MatColumnDef, + MatHeaderCellDef, + MatHeaderCell, + MatCellDef, + MatCell, + MatHeaderRowDef, + MatHeaderRow, + MatRowDef, + MatRow, + MatExpansionModule, + DecimalPipe + ] +}) +export class RdInterestRatesTabComponent { + private route = inject(ActivatedRoute); + + rdInterestRates: any[] = []; + + readonly slabColumns = [ + 'srNo', + 'description', + 'fromPeriod', + 'toPeriod', + 'periodTypeLabel', + 'annualInterestRate' + ]; + + constructor() { + this.route.data.subscribe((data: { rdInterestRates: any[] }) => { + this.rdInterestRates = data.rdInterestRates || []; + }); + } +} diff --git a/src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates.resolver.ts b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates.resolver.ts new file mode 100644 index 000000000..68e82a666 --- /dev/null +++ b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/rd-interest-rates-tab/rd-interest-rates.resolver.ts @@ -0,0 +1,16 @@ +/** + * 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 { inject } from '@angular/core'; +import { ResolveFn } from '@angular/router'; +import { ProductsService } from '../../../products.service'; + +export const rdInterestRatesResolver: ResolveFn = (route) => { + const productsService = inject(ProductsService); + return productsService.getRdBaseInterestRates(); +}; diff --git a/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.html b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.html index 004fb3daa..1105b7f68 100644 --- a/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.html +++ b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.html @@ -6,546 +6,629 @@ file, You can obtain one at http://mozilla.org/MPL/2.0/. --> -
-
-
- - -
-

{{ recurringDepositProduct.name }}

+
+

{{ recurringDepositProduct.name }}

- + + +

{{ 'labels.inputs.Details' | translate }}

+ -

{{ 'labels.inputs.Details' | translate }}

+
+ {{ 'labels.inputs.Short Name' | translate }}: + {{ recurringDepositProduct.shortName }} +
- + @if (recurringDepositProduct.description) { +
+ {{ 'labels.inputs.Description' | translate }}: + {{ recurringDepositProduct.description }} +
+ } + +

Product Classification

+ + + @if (recurringDepositProduct.accountType) { +
+ Account Type: + {{ recurringDepositProduct.accountType }} +
+ } + @if (recurringDepositProduct.customerSegment) { +
+ Customer Segment: + {{ recurringDepositProduct.customerSegment }} +
+ } + @if (recurringDepositProduct.glAccountId) { +
+ GL Account (Liability): + {{ recurringDepositProduct.glAccountName || recurringDepositProduct.glAccountId }} +
+ } + @if (recurringDepositProduct.effectiveTo) { +
+ Effective To: + {{ recurringDepositProduct.effectiveTo | dateFormat }} +
+ } + @if (recurringDepositProduct.endDate) { +
+ End Date: + {{ recurringDepositProduct.endDate | dateFormat }} +
+ } +
+ Introducer Required: + {{ recurringDepositProduct.introducerRequired | yesNo }} +
+
+ Deposit Account Link: + {{ recurringDepositProduct.shareLinkingEnabled | yesNo }} +
+ + +

{{ 'labels.inputs.Currency' | translate }}

+ + +
+ {{ 'labels.inputs.Currency' | translate }}: + ({{ recurringDepositProduct.currency.code }}) {{ recurringDepositProduct.currency.name }} +
+
+ {{ 'labels.inputs.Decimal Places' | translate }}: + {{ recurringDepositProduct.currency.decimalPlaces }} +
+
+ Dormancy Alert: + {{ recurringDepositProduct.dormancyAlert | yesNo }} +
+
+ Balance Swap Option: + {{ recurringDepositProduct.balanceSwapOption | yesNo }} +
+
+ MSME: + {{ recurringDepositProduct.msme | yesNo }} +
+
+ Nomination Facility: + {{ recurringDepositProduct.nominationFacility | yesNo }} +
+
+ Auto Debit Facility: + {{ recurringDepositProduct.autoDebitFacility | yesNo }} +
+ + +

{{ 'labels.inputs.Terms' | translate }}

+ + +
+ {{ 'labels.inputs.Deposit Amount' | translate }}: + {{ + recurringDepositProduct.depositAmount + + ' (Min: ' + + (recurringDepositProduct.minDepositAmount || recurringDepositProduct.depositAmount) + + ', Max: ' + + (recurringDepositProduct.maxDepositAmount || recurringDepositProduct.depositAmount) + + ')' + }} +
+
+ {{ 'labels.inputs.Nominal Annual Interest' | translate }}: + {{ recurringDepositProduct.nominalAnnualInterestRate | formatNumber }} % +
+ @if (recurringDepositProduct.effectiveDate) { +
+ Effective Date: + {{ recurringDepositProduct.effectiveDate | dateFormat }} +
+ } + @if (recurringDepositProduct.interestCalculationTypeName) { +
+ {{ 'labels.inputs.Interest Calculation Type' | translate }}: + {{ recurringDepositProduct.interestCalculationTypeName }} +
+ } +
+ Interest Calculate: + {{ recurringDepositProduct.interestCalculationType?.value }} +
+ @if (recurringDepositProduct.averagePeriod) { +
+ Average Period: + {{ recurringDepositProduct.averagePeriod }} +
+ } +
+ {{ 'labels.inputs.Interest Posting Period' | translate }}: + {{ recurringDepositProduct.interestPostingPeriodType.value }} +
+
+ {{ 'labels.inputs.Interest Compounding Period' | translate }}: + {{ recurringDepositProduct.interestCompoundingPeriodType.value }} +
+
+ {{ 'labels.inputs.Days in Year' | translate }}: + {{ recurringDepositProduct.interestCalculationDaysInYearType.value }} +
+
+ Auto Post: + {{ recurringDepositProduct.autoPost | yesNo }} +
+ @if (recurringDepositProduct.frequencyFixDay) { +
+ Frequency (Fix Day): + {{ recurringDepositProduct.frequencyFixDay }} +
+ } + @if (recurringDepositProduct.lastInterestDate) { +
+ Last Interest Date: + {{ recurringDepositProduct.lastInterestDate | dateFormat }} +
+ } + + +

{{ 'labels.inputs.Settings' | translate }}

+ + + +

Premature Option

+
+ Loan Facility: + {{ recurringDepositProduct.loanFacility | yesNo }} +
+
+ Premature Withdrawal Option: + {{ recurringDepositProduct.prematureWithdrawalOption | yesNo }} +
+
+ Tenure Flexibility: + {{ recurringDepositProduct.tenureFlexibility | yesNo }} +
+
+ Lien Marked: + {{ recurringDepositProduct.lienMarked | yesNo }} +
+
+ {{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }}: + {{ recurringDepositProduct.withdrawalFeeForTransfers | yesNo }} +
+ + +

{{ 'labels.inputs.Lock-in Period' | translate }}

+
+ Enable Lock-in Period: + {{ recurringDepositProduct.enableLockinPeriod | yesNo }} +
+ @if (recurringDepositProduct.lockinPeriodFrequency) { +
+ {{ 'labels.inputs.Lock-in Period' | translate }}: + {{ recurringDepositProduct.lockinPeriodFrequency }} + {{ recurringDepositProduct.lockinPeriodFrequencyType?.value }} +
+ } + + +

Maturity Instructions

+
+ Maturity Instructions: + {{ recurringDepositProduct.maturityInstructions | yesNo }} +
+
+ Credit Maturity Proceeds: + {{ recurringDepositProduct.creditMaturityProceeds | yesNo }} +
+
+ Penalty Grace Period (Missed Installments): + {{ recurringDepositProduct.penaltyGracePeriodMissedInstallments | yesNo }} +
+
+ Panel Request Applicable: + {{ recurringDepositProduct.panelRequestApplicable | yesNo }} +
+
+ No Penalty (Nominee / Heir Closure): + {{ recurringDepositProduct.noPenaltyNomineeHeirClosure | yesNo }} +
+
+ Deceased Settlement: + {{ recurringDepositProduct.deceasedSettlement | yesNo }} +
+
+ Allow Debit Txn After Due Date: + {{ recurringDepositProduct.allowDebitTxnAfterDueDate | yesNo }} +
+ + +

Additional Rules

+
+ Account Closed Zero Balance in Day Closing: + {{ recurringDepositProduct.accountClosedZeroBalanceInDayClosing | yesNo }} +
+ @if (recurringDepositProduct.minBalanceForInterestCalculation) { +
+ {{ 'labels.inputs.Balance Required for Interest Calculation' | translate }}: + {{ recurringDepositProduct.minBalanceForInterestCalculation | formatNumber }} +
+ } + + @if (recurringDepositProduct.minDepositTerm) { +
+ {{ 'labels.inputs.Minimum Deposit Term' | translate }}: + {{ recurringDepositProduct.minDepositTerm }} {{ recurringDepositProduct.minDepositTermType?.value }} +
+ } + @if (recurringDepositProduct.maxDepositTerm) { +
+ {{ 'labels.inputs.Maximum Deposit Term' | translate }}: + {{ recurringDepositProduct.maxDepositTerm }} {{ recurringDepositProduct.maxDepositTermType?.value }} +
+ } + @if (recurringDepositProduct.inMultipleOfDepositTerm) { +
+ {{ 'labels.inputs.Deposit Term In multiple of' | translate }}: + {{ recurringDepositProduct.inMultipleOfDepositTerm }} - + {{ recurringDepositProduct.inMultipleOfDepositTermType?.value }} +
+ } + @if (recurringDepositProduct.isMandatoryDeposit !== undefined) { +
+ {{ 'labels.inputs.Recurring Deposit Type' | translate }}: + {{ + recurringDepositProduct.isMandatoryDeposit + ? ('labels.text.Mandatory' | translate) + : ('labels.text.Voluntary' | translate) + }} +
+ } + @if (recurringDepositProduct.preClosurePenalInterest) { +
+ {{ 'labels.inputs.Apply penal interest (less)' | translate }}: + {{ recurringDepositProduct.preClosurePenalInterest }}% On + {{ recurringDepositProduct.preClosurePenalInterestOnType?.value }} +
+ } +
+ {{ 'labels.inputs.Withhold Tax is Applicable' | translate }}: + {{ recurringDepositProduct.withHoldTax | yesNo }} +
+ @if (recurringDepositProduct.withHoldTax) { +
+ {{ 'labels.inputs.Tax Group' | translate }}: + {{ recurringDepositProduct.taxGroup?.name }} +
+ } + + + @if (recurringDepositProduct.activeChart) { +
+

{{ 'labels.heading.Interest Rate Charts' | translate }}

+ +
+ @if (recurringDepositProduct.activeChart.name) { +
+ {{ 'labels.inputs.Name' | translate }}: + {{ recurringDepositProduct.activeChart.name }} +
+ }
- {{ 'labels.inputs.Short Name' | translate }}: - {{ recurringDepositProduct.shortName }} + {{ 'labels.inputs.Valid from Date' | translate }}: + {{ recurringDepositProduct.activeChart.fromDate | dateFormat }}
- - @if (recurringDepositProduct.description) { + @if (recurringDepositProduct.activeChart.endDate) { +
+ {{ 'labels.inputs.End Date' | translate }}: + {{ recurringDepositProduct.activeChart.endDate | dateFormat }} +
+ } + @if (recurringDepositProduct.activeChart.description) {
{{ 'labels.inputs.Description' | translate }}: - {{ recurringDepositProduct.description }} + {{ recurringDepositProduct.activeChart.description }}
} - -

{{ 'labels.inputs.Currency' | translate }}

- - -
- {{ 'labels.heading.Currency' | translate }}: - ({{ recurringDepositProduct.currency.code }}) {{ recurringDepositProduct.currency.name }} + {{ 'labels.inputs.Primary Grouping by Amount' | translate }}: + {{ recurringDepositProduct.activeChart.isPrimaryGroupingByAmount | yesNo }}
- -
- {{ 'labels.inputs.Decimal Places' | translate }}: - {{ recurringDepositProduct.currency.decimalPlaces }} -
- -
- {{ 'labels.inputs.Currency in multiples of' | translate }}: - {{ recurringDepositProduct.currency.inMultiplesOf | formatNumber }} -
- -

{{ 'labels.heading.Terms' | translate }}

- - - -
- {{ 'labels.inputs.Deposit Amount' | translate }}: - {{ - recurringDepositProduct.depositAmount + - ' (Min: ' + - (recurringDepositProduct.minDepositAmount - ? recurringDepositProduct.minDepositAmount - : recurringDepositProduct.depositAmount) + - ', Max: ' + - (recurringDepositProduct.maxDepositAmount - ? recurringDepositProduct.maxDepositAmount - : recurringDepositProduct.depositAmount) + - ')' - }} -
- -
- {{ 'labels.inputs.Interest Compounding Period' | translate }}: - {{ recurringDepositProduct.interestCompoundingPeriodType.value }} -
- -
- {{ 'labels.inputs.Interest Posting Period' | translate }}: - {{ recurringDepositProduct.interestPostingPeriodType.value }} -
- -
- {{ 'labels.inputs.Interest Calculated using' | translate }}: - {{ recurringDepositProduct.interestCalculationType.value }} -
- -
- {{ 'labels.inputs.Days in Year' | translate }}: - {{ recurringDepositProduct.interestCalculationDaysInYearType.value }} -
- -

{{ 'labels.heading.Settings' | translate }}

- - - - @if (recurringDepositProduct.lockinPeriodFrequency) { -
- {{ 'labels.inputs.Lock-in Period' | translate }}: - @if (recurringDepositProduct.lockinPeriodFrequency !== undefined) { - - {{ recurringDepositProduct.lockinPeriodFrequency }} - {{ recurringDepositProduct.lockinPeriodFrequencyType.value }} - } -
- } - - @if (recurringDepositProduct.minDepositTerm) { -
- {{ 'labels.inputs.Minimum Deposit Term' | translate }}: - @if (recurringDepositProduct.minDepositTerm !== undefined) { - {{ recurringDepositProduct.minDepositTerm }} -   - {{ recurringDepositProduct.minDepositTermType.value }} - } -
- } - - @if (recurringDepositProduct.maxDepositTerm) { -
- {{ 'labels.inputs.Maximum Deposit Term' | translate }}: - @if (recurringDepositProduct.maxDepositTerm !== undefined) { - {{ recurringDepositProduct.maxDepositTerm }} -   - {{ recurringDepositProduct.maxDepositTermType.value }} - } -
- } - - @if (recurringDepositProduct.inMultipleOfDepositTerm) { -
- {{ 'labels.inputs.Deposit Term In multiple of' | translate }}: - @if (recurringDepositProduct.inMultipleOfDepositTerm !== undefined) { - {{ recurringDepositProduct.inMultipleOfDepositTerm }} - - - {{ recurringDepositProduct.inMultipleOfDepositTermType.value }} - } -
- } - - @if (recurringDepositProduct.isMandatoryDeposit !== undefined) { -
- {{ 'labels.inputs.Recurring Deposit Type' | translate }}: - @if (recurringDepositProduct.isMandatoryDeposit) { - - {{ 'labels.text.Mandatory' | translate }} - - } - @if (!recurringDepositProduct.isMandatoryDeposit) { - - {{ 'labels.text.Voluntary' | translate }} - - } -
- } - - @if (recurringDepositProduct.preClosurePenalInterest) { -
- {{ 'labels.inputs.Apply penal interest (less)' | translate }}: - {{ recurringDepositProduct.preClosurePenalInterest }}%On - {{ recurringDepositProduct.preClosurePenalInterestOnType.value }} - -
- } - -
- {{ 'labels.inputs.Withhold Tax is Applicable' | translate }}: - {{ recurringDepositProduct.withHoldTax | yesNo }} -
- - @if (recurringDepositProduct.withHoldTax) { -
- {{ 'labels.inputs.Withhold Tax group' | translate }}: - {{ recurringDepositProduct.taxGroup.name }} -
- } - - @if (recurringDepositProduct.activeChart) { -
-

{{ 'labels.heading.Interest Rate Charts' | translate }}

- -
- @if (recurringDepositProduct.activeChart.name) { -
- {{ 'labels.inputs.Name' | translate }}: - {{ recurringDepositProduct.activeChart.name }} -
- } -
- {{ 'labels.inputs.Valid from Date' | translate }}: - {{ recurringDepositProduct.activeChart.fromDate | dateFormat }} -
- @if (recurringDepositProduct.activeChart.endDate) { -
- {{ 'labels.inputs.End Date' | translate }}: - {{ recurringDepositProduct.activeChart.endDate | dateFormat }} -
- } - @if (recurringDepositProduct.activeChart.description) { -
- {{ 'labels.inputs.Description' | translate }}: - {{ recurringDepositProduct.activeChart.description }} -
- } -
- {{ 'labels.inputs.Primary Grouping by Amount' | translate }}: - {{ recurringDepositProduct.activeChart.isPrimaryGroupingByAmount | yesNo }} -
- - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Period' | translate }} - @if (chartSlab.fromPeriod !== undefined) { -
{{ chartSlab.fromPeriod }} - {{ chartSlab.toPeriod }} {{ chartSlab.periodType.value }}
- } - @if (chartSlab.fromPeriod === undefined) { -
 
- } -
{{ 'labels.inputs.Amount Range' | translate }} - @if (chartSlab.amountRangeFrom !== undefined) { -
{{ chartSlab.amountRangeFrom }} - {{ chartSlab.amountRangeTo }}
- } - @if (chartSlab.amountRangeFrom === undefined) { -
 
- } -
{{ 'labels.inputs.Interest' | translate }} - {{ chartSlab.annualInterestRate }} - {{ 'labels.inputs.Description' | translate }} - {{ chartSlab.description }} - {{ 'labels.inputs.Actions' | translate }} - - -
- -

- {{ 'labels.heading.Incentives' | translate }} -

- - - - - - - - - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Entity Type' | translate }} - {{ - incentive.entityType - | find - : recurringDepositProductTemplate.chartTemplate.entityTypeOptions - : 'id' - : 'value' - }} - {{ 'labels.inputs.Attribute Name' | translate }} - {{ - incentive.attributeName - | find - : recurringDepositProductTemplate.chartTemplate.attributeNameOptions - : 'id' - : 'value' - }} - {{ 'labels.inputs.Condition Type' | translate }} - {{ - incentive.conditionType - | find - : recurringDepositProductTemplate.chartTemplate.conditionTypeOptions - : 'id' - : 'value' - }} - {{ 'labels.inputs.Attribute Value' | translate }} - @switch (incentive.attributeName) { - @case (2) { - {{ - incentive.attributeValue - | find - : recurringDepositProductTemplate.chartTemplate.genderOptions - : 'id' - : 'name' - }} - } - @case (3) { - {{ incentive.attributeValue }} - } - @case (4) { - {{ - incentive.attributeValue - | find - : recurringDepositProductTemplate.chartTemplate.clientTypeOptions - : 'id' - : 'name' - }} - } - @case (5) { - {{ - incentive.attributeValue - | find - : recurringDepositProductTemplate.chartTemplate.clientClassificationOptions - : 'id' - : 'name' - }} - } - } - {{ 'labels.inputs.Incentive Type' | translate }} - {{ - incentive.incentiveType - | find - : recurringDepositProductTemplate.chartTemplate.incentiveTypeOptions - : 'id' - : 'value' - }} - {{ 'labels.inputs.Amount' | translate }} - {{ incentive.amount }} -
-
-
-
- -
-
- } - - @if (recurringDepositProduct.charges.length) { -
-

{{ 'labels.heading.Charges' | translate }}

- - - - - - - - - - - - - - - - - - - - -
{{ 'labels.inputs.Name' | translate }} - {{ charge.name }} - {{ 'labels.inputs.Type' | translate }} - {{ charge.chargeCalculationType.value }} - {{ 'labels.inputs.Amount' | translate }} - {{ charge.amount | formatNumber }} - {{ 'labels.inputs.Collected On' | translate }} - {{ charge.chargeTimeType.value }} -
-
- } - -

{{ 'labels.heading.Accounting' | translate }}

- - - - - - - @if ( - recurringDepositProduct.paymentChannelToFundSourceMappings?.length || - recurringDepositProduct.feeToIncomeAccountMappings?.length || - recurringDepositProduct.penaltyToIncomeAccountMappings?.length - ) { -
-

{{ 'labels.inputs.Advanced Accounting Rules' | translate }}

- - @if (recurringDepositProduct.paymentChannelToFundSourceMappings?.length) { -
-

- {{ 'labels.inputs.Configure Fund Sources for Payment Channels' | translate }} -

- - - - - - - - - - - -
{{ 'labels.inputs.Payment Type' | translate }} - {{ paymentFundSource.paymentType.name }} - {{ 'labels.inputs.Fund Source' | translate }} - {{ paymentFundSource.fundSourceAccount.name }} -
+ +
{{ 'labels.inputs.Period' | translate }} + @if (chartSlab.fromPeriod !== undefined) { +
{{ chartSlab.fromPeriod }} - {{ chartSlab.toPeriod }} {{ chartSlab.periodType.value }}
+ } + @if (chartSlab.fromPeriod === undefined) { +
 
+ } +
{{ 'labels.inputs.Amount Range' | translate }} + @if (chartSlab.amountRangeFrom !== undefined) { +
{{ chartSlab.amountRangeFrom }} - {{ chartSlab.amountRangeTo }}
+ } + @if (chartSlab.amountRangeFrom === undefined) { +
 
+ } +
{{ 'labels.inputs.Interest' | translate }}{{ chartSlab.annualInterestRate }}{{ 'labels.inputs.Description' | translate }}{{ chartSlab.description }}{{ 'labels.inputs.Actions' | translate }} + + +
+ +

{{ 'labels.heading.Incentives' | translate }}

+ + + + + + + + + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.Entity Type' | translate }} + {{ + incentive.entityType + | find: recurringDepositProductTemplate.chartTemplate.entityTypeOptions : 'id' : 'value' + }} + {{ 'labels.inputs.Attribute Name' | translate }} + {{ + incentive.attributeName + | find: recurringDepositProductTemplate.chartTemplate.attributeNameOptions : 'id' : 'value' + }} + {{ 'labels.inputs.Condition Type' | translate }} + {{ + incentive.conditionType + | find: recurringDepositProductTemplate.chartTemplate.conditionTypeOptions : 'id' : 'value' + }} + {{ 'labels.inputs.Attribute Value' | translate }} + @switch (incentive.attributeName) { + @case (2) { + {{ + incentive.attributeValue + | find: recurringDepositProductTemplate.chartTemplate.genderOptions : 'id' : 'name' + }} + } + @case (3) { + {{ incentive.attributeValue }} + } + @case (4) { + {{ + incentive.attributeValue + | find: recurringDepositProductTemplate.chartTemplate.clientTypeOptions : 'id' : 'name' + }} + } + @case (5) { + {{ + incentive.attributeValue + | find + : recurringDepositProductTemplate.chartTemplate.clientClassificationOptions + : 'id' + : 'name' + }} + } + } + {{ 'labels.inputs.Incentive Type' | translate }} + {{ + incentive.incentiveType + | find: recurringDepositProductTemplate.chartTemplate.incentiveTypeOptions : 'id' : 'value' + }} + {{ 'labels.inputs.Amount' | translate }}{{ incentive.amount }}
+
- } - @if (recurringDepositProduct.feeToIncomeAccountMappings?.length) { -
-

{{ 'labels.inputs.Fees to Specific Income Accounts' | translate }}

- - - - - - - - - - - -
{{ 'labels.inputs.Fees' | translate }} - {{ feesIncome.charge.name }} - {{ 'labels.inputs.Income Account' | translate }} - {{ feesIncome.incomeAccount.name }} -
-
- } - @if (recurringDepositProduct.penaltyToIncomeAccountMappings?.length) { -
-

{{ 'labels.inputs.Penalties to Specific Income Accounts' | translate }}

- - - - - - - - - - - -
{{ 'labels.inputs.Penalty' | translate }} - {{ penaltyIncome.charge.name }} - {{ 'labels.inputs.Income Account' | translate }} - {{ penaltyIncome.incomeAccount.name }} -
-
- } - - } +
+
- - + + } + + + @if (recurringDepositProduct.charges.length) { +
+

{{ 'labels.heading.Charges' | translate }}

+ + + + + + + + + + + + + + + + + + + + +
{{ 'labels.inputs.Name' | translate }}{{ charge.name }}{{ 'labels.inputs.Type' | translate }}{{ charge.chargeCalculationType.value }}{{ 'labels.inputs.Amount' | translate }}{{ charge.amount | formatNumber }}{{ 'labels.inputs.Collected On' | translate }}{{ charge.chargeTimeType.value }}
+
+ } + + +

{{ 'labels.heading.Accounting' | translate }}

+ + + + + + @if ( + recurringDepositProduct.paymentChannelToFundSourceMappings?.length || + recurringDepositProduct.feeToIncomeAccountMappings?.length || + recurringDepositProduct.penaltyToIncomeAccountMappings?.length + ) { +
+

{{ 'labels.inputs.Advanced Accounting Rules' | translate }}

+ + @if (recurringDepositProduct.paymentChannelToFundSourceMappings?.length) { +
+

{{ 'labels.inputs.Fund Sources for Payment Channels' | translate }}

+ + + + + + + + + + + +
{{ 'labels.inputs.Payment Type' | translate }}{{ paymentFundSource.paymentType.name }}{{ 'labels.inputs.Fund Source' | translate }}{{ paymentFundSource.fundSourceAccount.name }}
+
+ } + @if (recurringDepositProduct.feeToIncomeAccountMappings?.length) { +
+

{{ 'labels.inputs.Fees to Specific Income Accounts' | translate }}

+ + + + + + + + + + + +
{{ 'labels.inputs.Fee/Charge' | translate }}{{ feesIncome.charge.name }}{{ 'labels.inputs.Income Account' | translate }}{{ feesIncome.incomeAccount.name }}
+
+ } + @if (recurringDepositProduct.penaltyToIncomeAccountMappings?.length) { +
+

{{ 'labels.inputs.Penalties to Specific Income Accounts' | translate }}

+ + + + + + + + + + + +
{{ 'labels.inputs.Penalty' | translate }}{{ penaltyIncome.charge.name }}{{ 'labels.inputs.Income Account' | translate }}{{ penaltyIncome.incomeAccount.name }}
+
+ } +
+ } diff --git a/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.ts b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.ts index 88331fcaf..4ba8ac022 100644 --- a/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.ts +++ b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/recurring-deposit-general-tab/recurring-deposit-general-tab.component.ts @@ -68,6 +68,7 @@ export class RecurringDepositGeneralTabComponent { recurringDepositProduct: any; recurringDepositProductTemplate: any; + expandChartSlabIndex: number | null = null; chartSlabsIncentivesDisplayedColumns: string[] = ['incentives']; chartSlabsDisplayedColumns: string[] = [ diff --git a/src/app/products/recurring-deposit-products/view-recurring-deposit-product/view-recurring-deposit-product.component.html b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/view-recurring-deposit-product.component.html index 041cff962..4bde6da1a 100644 --- a/src/app/products/recurring-deposit-products/view-recurring-deposit-product/view-recurring-deposit-product.component.html +++ b/src/app/products/recurring-deposit-products/view-recurring-deposit-product/view-recurring-deposit-product.component.html @@ -18,6 +18,15 @@ > {{ 'labels.heading.General' | translate }} + + CBS Base Chart + @for (recurringDepositDatatable of recurringDepositDatatables; track recurringDepositDatatable) { {{ 'labels.heading.Create Savings Product' | translate }} - + {{ 'labels.inputs.DETAILS' | translate }}
- +
@@ -72,6 +79,18 @@ + + + + Digital + +
+ +
+
+ @@ -114,19 +133,19 @@
@if (selectedTabIndex > 0) { } @if (selectedTabIndex < totalTabs - 1) { } @if (savingProductFormValid && selectedTabIndex === totalTabs - 1) { } diff --git a/src/app/products/saving-products/create-saving-product/create-saving-product.component.ts b/src/app/products/saving-products/create-saving-product/create-saving-product.component.ts index 2f8d66171..c7bab6636 100644 --- a/src/app/products/saving-products/create-saving-product/create-saving-product.component.ts +++ b/src/app/products/saving-products/create-saving-product/create-saving-product.component.ts @@ -1,4 +1,4 @@ -/** +/** * Copyright since 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public @@ -16,6 +16,7 @@ import { SavingProductCurrencyStepComponent } from '../saving-product-stepper/sa import { SavingProductTermsStepComponent } from '../saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component'; import { SavingProductSettingsStepComponent } from '../saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component'; import { SavingProductChargesStepComponent } from '../saving-product-stepper/saving-product-charges-step/saving-product-charges-step.component'; +import { SavingProductLimitsStepComponent } from '../saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component'; import { SavingProductAccountingStepComponent } from '../saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component'; /** Custom Services */ @@ -38,6 +39,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; SavingProductTermsStepComponent, SavingProductSettingsStepComponent, SavingProductChargesStepComponent, + SavingProductLimitsStepComponent, SavingProductAccountingStepComponent, SavingProductPreviewStepComponent ] @@ -58,6 +60,8 @@ export class CreateSavingProductComponent { savingProductSettingsStep: SavingProductSettingsStepComponent; @ViewChild(SavingProductChargesStepComponent, { static: true }) savingProductChargesStep: SavingProductChargesStepComponent; + @ViewChild(SavingProductLimitsStepComponent, { static: true }) + savingProductLimitsStep: SavingProductLimitsStepComponent; @ViewChild(SavingProductAccountingStepComponent, { static: true }) savingProductAccountingStep: SavingProductAccountingStepComponent; @@ -100,7 +104,7 @@ export class CreateSavingProductComponent { } get totalTabs(): number { - return this.savingProductFormValid ? 7 : 6; + return this.savingProductFormValid ? 8 : 7; } get savingProductFormValid() { @@ -120,18 +124,44 @@ export class CreateSavingProductComponent { ...this.savingProductTermsStep.savingProductTerms, ...this.savingProductSettingsStep.savingProductSettings, ...this.savingProductChargesStep.savingProductCharges, + ...this.savingProductLimitsStep.savingProductLimits, ...this.savingProductAccountingStep.savingProductAccounting }; } - submit() { - // TODO: Update once language and date settings are setup + private prepareSavingProductPayload() { const savingProduct = { ...this.savingProduct, charges: this.savingProduct.charges.map((charge: any) => ({ id: charge.id })), - locale: this.settingsService.language.code // locale required for nominalAnnualInterestRate + locale: this.settingsService.language.code, + dateFormat: 'dd MMMM yyyy' }; delete savingProduct.advancedAccountingRules; + + // Convert lockinPeriodFrequencyType from string to integer + // Backend expects: 0=DAYS, 1=WEEKS, 2=MONTHS, 3=YEARS + if (savingProduct.lockinPeriodFrequencyType) { + const frequencyTypeMapping: { [key: string]: number } = { + DAYS: 0, + WEEKS: 1, + MONTHS: 2, + YEARS: 3 + }; + savingProduct.lockinPeriodFrequencyType = frequencyTypeMapping[savingProduct.lockinPeriodFrequencyType]; + } + + Object.keys(savingProduct).forEach((key) => { + if (savingProduct[key] === '' || savingProduct[key] === null || savingProduct[key] === undefined) { + delete savingProduct[key]; + } + }); + + return savingProduct; + } + + submit() { + const savingProduct = this.prepareSavingProductPayload(); + console.log('Saving product submit payload:', savingProduct); this.productsService.createSavingProduct(savingProduct).subscribe((response: any) => { this.router.navigate( [ diff --git a/src/app/products/saving-products/edit-saving-product/edit-saving-product.component.html b/src/app/products/saving-products/edit-saving-product/edit-saving-product.component.html index dfc39c64b..708c73f55 100644 --- a/src/app/products/saving-products/edit-saving-product/edit-saving-product.component.html +++ b/src/app/products/saving-products/edit-saving-product/edit-saving-product.component.html @@ -12,7 +12,12 @@

{{ 'labels.heading.Edit Savings Product' | translate }}

- + @@ -74,6 +79,18 @@ + + + + Digital + +
+ +
+
+ @@ -116,19 +133,19 @@
@if (selectedTabIndex > 0) { } @if (selectedTabIndex < totalTabs - 1) { } @if (savingProductFormValidAndNotPristine && selectedTabIndex === totalTabs - 1) { } diff --git a/src/app/products/saving-products/edit-saving-product/edit-saving-product.component.ts b/src/app/products/saving-products/edit-saving-product/edit-saving-product.component.ts index 673113c16..1ad5e4b11 100644 --- a/src/app/products/saving-products/edit-saving-product/edit-saving-product.component.ts +++ b/src/app/products/saving-products/edit-saving-product/edit-saving-product.component.ts @@ -1,4 +1,4 @@ -/** +/** * Copyright since 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public @@ -16,6 +16,7 @@ import { SavingProductCurrencyStepComponent } from '../saving-product-stepper/sa import { SavingProductTermsStepComponent } from '../saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component'; import { SavingProductSettingsStepComponent } from '../saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component'; import { SavingProductChargesStepComponent } from '../saving-product-stepper/saving-product-charges-step/saving-product-charges-step.component'; +import { SavingProductLimitsStepComponent } from '../saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component'; import { SavingProductAccountingStepComponent } from '../saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component'; /** Custom Services */ @@ -38,6 +39,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; SavingProductTermsStepComponent, SavingProductSettingsStepComponent, SavingProductChargesStepComponent, + SavingProductLimitsStepComponent, SavingProductAccountingStepComponent, SavingProductPreviewStepComponent ] @@ -58,6 +60,8 @@ export class EditSavingProductComponent { savingProductSettingsStep: SavingProductSettingsStepComponent; @ViewChild(SavingProductChargesStepComponent, { static: true }) savingProductChargesStep: SavingProductChargesStepComponent; + @ViewChild(SavingProductLimitsStepComponent, { static: true }) + savingProductLimitsStep: SavingProductLimitsStepComponent; @ViewChild(SavingProductAccountingStepComponent, { static: true }) savingProductAccountingStep: SavingProductAccountingStepComponent; @@ -100,7 +104,7 @@ export class EditSavingProductComponent { } get totalTabs(): number { - return this.savingProductFormValidAndNotPristine ? 7 : 6; + return this.savingProductFormValidAndNotPristine ? 8 : 7; } get savingProductFormValidAndNotPristine() { @@ -115,6 +119,7 @@ export class EditSavingProductComponent { !this.savingProductTermsForm.pristine || !this.savingProductSettingsForm.pristine || !this.savingProductChargesStep.pristine || + !this.savingProductLimitsStep.pristine || !this.savingProductAccountingForm.pristine) ); } @@ -126,18 +131,44 @@ export class EditSavingProductComponent { ...this.savingProductTermsStep.savingProductTerms, ...this.savingProductSettingsStep.savingProductSettings, ...this.savingProductChargesStep.savingProductCharges, + ...this.savingProductLimitsStep.savingProductLimits, ...this.savingProductAccountingStep.savingProductAccounting }; } - submit() { - // TODO: Update once language and date settings are setup + private prepareSavingProductPayload() { const savingProduct = { ...this.savingProduct, charges: this.savingProduct.charges.map((charge: any) => ({ id: charge.id })), - locale: this.settingsService.language.code // locale required for nominalAnnualInterestRate + locale: this.settingsService.language.code, + dateFormat: 'dd MMMM yyyy' }; delete savingProduct.advancedAccountingRules; + + // Convert lockinPeriodFrequencyType from string to integer + // Backend expects: 0=DAYS, 1=WEEKS, 2=MONTHS, 3=YEARS + if (savingProduct.lockinPeriodFrequencyType) { + const frequencyTypeMapping: { [key: string]: number } = { + DAYS: 0, + WEEKS: 1, + MONTHS: 2, + YEARS: 3 + }; + savingProduct.lockinPeriodFrequencyType = frequencyTypeMapping[savingProduct.lockinPeriodFrequencyType]; + } + + Object.keys(savingProduct).forEach((key) => { + if (savingProduct[key] === '' || savingProduct[key] === null || savingProduct[key] === undefined) { + delete savingProduct[key]; + } + }); + + return savingProduct; + } + + submit() { + const savingProduct = this.prepareSavingProductPayload(); + console.log('Saving product update payload:', savingProduct); this.productsService .updateSavingProduct(this.savingProductAndTemplate.id, savingProduct) .subscribe((response: any) => { diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.html b/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.html index 6214db809..d2db28c86 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.html +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.html @@ -1,4 +1,4 @@ -
-
- -

{{ 'labels.inputs.Accounting Rule' | translate }}

-
-
- - @for (accountingRule of accountingRuleData; track accountingRule; let i = $index) { - - {{ 'labels.accounting.' + accountingRule | translate }} - - } - -
-
- - @if (isCashOrAccrualAccounting()) { - -
-
- -

{{ 'labels.heading.Assets' | translate }}

-
-
- - - - - @if (isAccrualAccounting()) { - - - - } - - @if (isAccrualAccounting() && allowOverdraft.value) { - - } -
+ + + +

Accounting

- -
-
- -

{{ 'labels.heading.Liabilities' | translate }}

-
-
- - - - - @if (isAccrualAccounting()) { - - } - - @if (isDormancyTrackingActive.value) { - - } -
-
- - -
-
- -

{{ 'labels.heading.Expenses' | translate }}

-
-
- - - -
-
- - -
-
- -

{{ 'labels.heading.Income' | translate }}

-
-
- - - - - -
-
- - -
-
- -

{{ 'labels.heading.Advanced Accounting Rules' | translate }}

-
-
-
- - {{ 'labels.heading.Advanced Accounting Rules' | translate }} - +
+
+
+

Accounting Rule

+
+ Cash +
+
- @if (savingProductAccountingForm.value.advancedAccountingRules) { - -
-
-

{{ 'labels.heading.Configure Fund Sources for Payment Channels' | translate }}

-
- - @if (paymentChannelToFundSourceMappings.value.length !== 0) { -
- - - - - - - - - - - - - - - -
{{ 'labels.inputs.Payment Type' | translate }} - {{ paymentFundSource.paymentTypeId | find: paymentTypeData : 'id' : 'name' }} - {{ 'labels.inputs.Fund Source' | translate }} - {{ paymentFundSource.fundSourceAccountId | find: combinedAccountData : 'id' : 'name' }} - {{ 'labels.inputs.Actions' | translate }} - - -
-
- } + {{ section.pillLabel }} + + {{ row.label }} +
+ +
+ } + } +
+ +
+ @for ( + otherRow of getOtherGlMappingsArray(section.otherMappingsControlName).controls; + track $index; + let i = $index + ) { +
+
+
+ + {{ section.pillLabel }} + + +
+ +
+ + GL Code + + @for (glAccount of section.otherAccountOptions; track glAccount.id) { + + {{ '(' + glAccount.glCode + ') ' + glAccount.name }} + + } + + +
+
+ +
+ } +
- - - @if (existCharges()) { -
-
-

{{ 'labels.heading.Map Fees to Specific Income Accounts' | translate }}

- -
- - @if (feeToIncomeAccountMappings.value.length !== 0) { -
- - - - - - - - - - - - - - - -
{{ 'labels.inputs.Fees' | translate }} - {{ feesIncome.chargeId | find: chargeData : 'id' : 'name' }} - {{ 'labels.inputs.Income Account' | translate }} - {{ feesIncome.incomeAccountId | find: incomeAccountData : 'id' : 'name' }} - {{ 'labels.inputs.Actions' | translate }} - - -
-
- } -
- - -
-
-

{{ 'labels.heading.Map Penalties to Specific Income Accounts' | translate }}

- -
- - @if (penaltyToIncomeAccountMappings.value.length !== 0) { -
- - - - - - - - - - - - - - - -
{{ 'labels.inputs.Penalty' | translate }} - {{ penaltyIncome.chargeId | find: penaltyData : 'id' : 'name' }} - {{ 'labels.inputs.Income Account' | translate }} - {{ penaltyIncome.incomeAccountId | find: incomeAccountData : 'id' : 'name' }} - {{ 'labels.inputs.Actions' | translate }} - - -
-
- } -
- } } -
+ }
- } + diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.scss b/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.scss index 02908020d..30125e801 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.scss +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.scss @@ -6,124 +6,309 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -$primary: #1353a0; -$accent: #0ea5e9; $border: #e2e8f0; $gray-50: #f8f9fa; $gray-200: #e8eaed; -$gray-500: #9aa0a6; -$gray-700: #5f6368; +$gray-500: #6b7280; +$gray-700: #334155; $gray-900: #202124; +$primary: #1353a0; .spa-section { border: 1px solid $border; border-radius: 10px; overflow: hidden; background: #fff; - - & + & { - margin-top: 16px; - } - - &__header { - display: flex; - align-items: center; - gap: 10px; - padding: 12px 24px; - background: $gray-50; - border-bottom: 1px solid $gray-200; - - i { - font-size: 14px; - color: $accent; - } - - h3 { - margin: 0; - font-size: 14px; - font-weight: 600; - color: $gray-900; - } - } } -.spa-body { - padding: 16px 24px; -} - -.spa-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 4px 16px; - padding: 16px 24px; - - @media (width <= 600px) { - grid-template-columns: 1fr; - } -} - -.spa-field { - width: 100%; - - &--full { - grid-column: 1 / -1; - } -} - -.spa-radio-group { - display: flex; - gap: 24px; - flex-wrap: wrap; - - @media (width <= 600px) { - flex-direction: column; - gap: 12px; - } -} - -.spa-checkbox-row { +.spa-section__header { display: flex; align-items: center; - padding: 4px 0; - margin-bottom: 12px; + gap: 10px; + padding: 12px 24px; + background: $gray-50; + border-bottom: 1px solid $gray-200; + + h3 { + margin: 0; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } } -.spa-subsection { - margin-top: 16px; - padding-top: 16px; - border-top: 1px solid $gray-200; +.spa-section__icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: #eff6ff; + color: #2563eb; + font-size: 13px; +} + +.spa-section__body { + padding: 16px 24px 20px; +} + +.spa-group { + & + & { + margin-top: 20px; + } &__header { display: flex; align-items: center; justify-content: space-between; - gap: 12px; - margin-bottom: 12px; + gap: 16px; + margin: 0 0 12px; + } - h4 { - margin: 0; - font-size: 13px; - font-weight: 600; - color: $gray-700; - } + h4 { + margin: 0 0 12px; + font-size: 12px; + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + color: #94a3b8; } } .spa-add-btn { - height: 32px !important; - border-radius: 8px !important; - font-size: 12px !important; - font-weight: 600 !important; - box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - white-space: nowrap; + border: 0; + background: transparent; + color: #2563eb; + font-size: 12px; + font-weight: 700; + padding: 0; + cursor: pointer; } -.spa-table-wrap { - border: 1px solid $border; +.spa-list { + display: flex; + flex-direction: column; + gap: 10px; +} + +.spa-list + .spa-list { + margin-top: 12px; +} + +.spa-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + padding: 10px 12px; + border: 1px solid #e2e8f0; border-radius: 8px; - overflow: hidden; + background: #fff; + + @media (width <= 900px) { + flex-direction: column; + align-items: stretch; + } } -.spa-table { +.spa-row:has(mifosx-gl-account-selector) { + align-items: stretch; +} + +.spa-row--other { + border-style: dashed; + border-color: #cbd5e1; + background: #f8fafc; + display: grid; + grid-template-columns: minmax(0, 0.95fr) minmax(0, 1.05fr); + align-items: center; + padding: 12px 16px; + + @media (width <= 1100px) { + grid-template-columns: 1fr; + } +} + +.spa-other-row-wrap { + display: grid; + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + gap: 12px; + + @media (width <= 1100px) { + grid-template-columns: 1fr; + gap: 8px; + } +} + +.spa-row__meta { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; + flex: 1 1 auto; +} + +.spa-row__meta--other { + min-width: 0; + flex: 1 1 auto; + align-self: stretch; +} + +.spa-row__label { + font-size: 14px; + font-weight: 600; + color: $gray-700; +} + +.spa-row__input { + flex: 0 0 22rem; + max-width: 22rem; + min-width: 18rem; + + @media (width <= 900px) { + width: 100%; + max-width: 100%; + min-width: 0; + } +} + +.spa-row__input--other { + flex: 1 1 auto; + max-width: 100%; + min-width: 0; +} + +.spa-row__input :is(mifosx-gl-account-selector) { + width: 100%; + display: block; +} + +.spa-row__input :is(mifosx-gl-account-selector) :is(.layout-column) { + width: 100%; + margin-right: 0 !important; +} + +.spa-row__input :is(mifosx-gl-account-selector) :is(mat-form-field) { width: 100%; } + +.spa-row__actions { + display: flex; + align-items: center; + gap: 10px; + min-width: 0; +} + +.spa-row__actions--other { + justify-content: flex-end; + align-self: stretch; + width: 100%; + + @media (width <= 1100px) { + justify-content: flex-start; + } +} + +.spa-row__name { + width: 100%; + min-width: 0; + max-width: 24rem; + height: 40px; + border: 1px solid #cbd5e1; + border-radius: 6px; + padding: 0 12px; + font-size: 14px; + background: #fff; + + @media (width <= 900px) { + max-width: 100%; + } +} + +.spa-row__name--code { + max-width: 20rem; +} + +.spa-row__name--other-account { + max-width: 100%; + flex: 1 1 auto; + height: 48px; +} + +.spa-row__other-field { + width: 100%; + min-width: 0; +} + +.spa-row__other-field :is(mat-form-field) { + width: 100%; +} + +.spa-row__other-field :is(.mat-mdc-form-field-required-marker) { + display: none; +} + +.spa-remove-btn { + border: 0; + background: transparent; + color: #ef4444; + font-size: 18px; + line-height: 1; + cursor: pointer; + padding: 0 4px; +} + +.spa-remove-btn--side { + place-self: center end; + margin-right: 4px; + + @media (width <= 1100px) { + justify-self: start; + margin-right: 0; + } +} + +.spa-pill { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 2px 8px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + white-space: nowrap; + + &--green { + background: #dcfce7; + color: #16a34a; + } + + &--amber { + background: #fef3c7; + color: #d97706; + } + + &--blue { + background: #dbeafe; + color: #2563eb; + } +} + +datalist { + display: none; +} + +@media (width <= 900px) { + .spa-row__meta { + margin-bottom: 4px; + } + + .spa-row__input { + flex: 1 1 auto; + max-width: 100%; + } +} diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.ts b/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.ts index 6a1d22664..88e88a51d 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.ts +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-accounting-step/saving-product-accounting-step.component.ts @@ -1,4 +1,4 @@ -/** +/** * Copyright since 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public @@ -6,489 +6,490 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, OnInit, Input, inject } from '@angular/core'; +import { Component, Input, OnInit, inject } from '@angular/core'; import { - UntypedFormGroup, - UntypedFormBuilder, + FormsModule, + ReactiveFormsModule, UntypedFormArray, - Validators, + UntypedFormBuilder, UntypedFormControl, - ReactiveFormsModule + UntypedFormGroup, + Validators } from '@angular/forms'; -import { MatDialog } from '@angular/material/dialog'; - -import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component'; -import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; - -import { FormfieldBase } from 'app/shared/form-dialog/formfield/model/formfield-base'; -import { SelectBase } from 'app/shared/form-dialog/formfield/model/select-base'; -import { TranslateService } from '@ngx-translate/core'; -import { MatRadioGroup, MatRadioButton } from '@angular/material/radio'; -import { GlAccountSelectorComponent } from '../../../../shared/accounting/gl-account-selector/gl-account-selector.component'; -import { MatCheckbox } from '@angular/material/checkbox'; -import { MatButton, MatIconButton } from '@angular/material/button'; -import { FaIconComponent } from '@fortawesome/angular-fontawesome'; -import { - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow -} from '@angular/material/table'; -import { FindPipe } from '../../../../pipes/find.pipe'; +import { MatRadioButton, MatRadioGroup } from '@angular/material/radio'; +import { GlAccountSelectorComponent } from 'app/shared/accounting/gl-account-selector/gl-account-selector.component'; +import { Accounting } from 'app/core/utils/accounting'; +import { GLAccount } from 'app/shared/models/general.model'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +type AccountingSectionKey = 'assets' | 'liabilities' | 'expenses' | 'income'; + +type AccountingRowConfig = { + key: string; + label: string; + inputLabel: string; + controlName: string; + options: GLAccount[]; + required?: boolean; + accrualOnly?: boolean; + dormancyOnly?: boolean; +}; + +type AccountingSectionConfig = { + key: AccountingSectionKey; + title: string; + pillLabel: string; + pillClass: 'green' | 'amber' | 'blue'; + rows: AccountingRowConfig[]; + otherMappingsControlName: string; + otherAccountOptions: GLAccount[]; +}; + +type OtherGlMappingValue = { + accountName: string; + glAccountId: number | null; +}; + @Component({ selector: 'mifosx-saving-product-accounting-step', templateUrl: './saving-product-accounting-step.component.html', styleUrls: ['./saving-product-accounting-step.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, + FormsModule, + ReactiveFormsModule, MatRadioGroup, MatRadioButton, - GlAccountSelectorComponent, - MatCheckbox, - FaIconComponent, - MatTable, - MatColumnDef, - MatHeaderCellDef, - MatHeaderCell, - MatCellDef, - MatCell, - MatIconButton, - MatHeaderRowDef, - MatHeaderRow, - MatRowDef, - MatRow, - FindPipe + GlAccountSelectorComponent ] }) export class SavingProductAccountingStepComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); - private dialog = inject(MatDialog); - private translateService = inject(TranslateService); + private accounting = inject(Accounting); @Input() savingProductsTemplate: any; @Input() accountingRuleData: any; - @Input() isDormancyTrackingActive: UntypedFormControl; + @Input() isDormancyTrackingActive: any; @Input() savingProductFormValid: boolean; - @Input() allowOverdraft: UntypedFormControl; + @Input() allowOverdraft: any; savingProductAccountingForm: UntypedFormGroup; - - chargeData: any; - penaltyData: any; - paymentTypeData: any; - assetAccountData: any; - incomeAccountData: any; - expenseAccountData: any; - liabilityAccountData: any; - incomeAndLiabilityAccountData: any; - combinedAccountData: any[]; - - paymentFundSourceDisplayedColumns: string[] = [ - 'paymentTypeId', - 'fundSourceAccountId', - 'actions' - ]; - feesPenaltyIncomeDisplayedColumns: string[] = [ - 'chargeId', - 'incomeAccountId', - 'actions' - ]; + liabilityAccountOptions: GLAccount[] = []; + expenseAccountOptions: GLAccount[] = []; + incomeAccountOptions: GLAccount[] = []; + assetAccountOptions: GLAccount[] = []; + accountingSectionConfigs: AccountingSectionConfig[] = []; constructor() { - this.createsavingProductAccountingForm(); + this.createSavingProductAccountingForm(); this.setConditionalControls(); } ngOnInit() { - this.chargeData = this.savingProductsTemplate.chargeOptions || []; - this.penaltyData = this.savingProductsTemplate.penaltyOptions || []; - this.paymentTypeData = this.savingProductsTemplate.paymentTypeOptions || []; - this.assetAccountData = this.savingProductsTemplate.accountingMappingOptions.assetAccountOptions || []; - this.incomeAccountData = this.savingProductsTemplate.accountingMappingOptions.incomeAccountOptions || []; - this.expenseAccountData = this.savingProductsTemplate.accountingMappingOptions.expenseAccountOptions || []; - this.liabilityAccountData = this.savingProductsTemplate.accountingMappingOptions.liabilityAccountOptions || []; - this.combinedAccountData = [ - ...this.assetAccountData, - ...this.incomeAccountData, - ...this.expenseAccountData, - ...this.liabilityAccountData - ]; - this.combinedAccountData.sort((a, b) => { - const valueA = a.name.toLowerCase(); - const valueB = b.name.toLowerCase(); - if (valueA < valueB) { - return -1; - } - if (valueA > valueB) { - return 1; - } - return 0; + const accountingMappings = this.savingProductsTemplate?.accountingMappings || {}; + // otherGlMappingsMap is at the root of the template, not inside accountingMappings + const mappings = { + ...accountingMappings, + otherGlMappingsMap: this.savingProductsTemplate?.otherGlMappingsMap + }; + const accountingMappingOptions = this.savingProductsTemplate?.accountingMappingOptions || {}; + const accountingRuleId = 2; // Always Cash + this.assetAccountOptions = accountingMappingOptions.assetAccountOptions || []; + this.liabilityAccountOptions = accountingMappingOptions.liabilityAccountOptions || []; + this.expenseAccountOptions = accountingMappingOptions.expenseAccountOptions || []; + this.incomeAccountOptions = accountingMappingOptions.incomeAccountOptions || []; + this.buildAccountingSectionConfigs(); + this.savingProductAccountingForm.patchValue({ + accountingRule: accountingRuleId }); + this.syncAccountingControls(accountingRuleId); + this.patchAccountingValues(mappings); + this.setOverdraftControl(); + this.setDormancyControl(); + } + + createSavingProductAccountingForm() { + this.savingProductAccountingForm = this.formBuilder.group({ + accountingRule: [2], + assetsOtherGlMappings: this.formBuilder.array([]), + liabilitiesOtherGlMappings: this.formBuilder.array([]), + expensesOtherGlMappings: this.formBuilder.array([]), + incomeOtherGlMappings: this.formBuilder.array([]) + }); + } + + private setConditionalControls() { + this.savingProductAccountingForm.get('accountingRule')?.valueChanges.subscribe((accountingRule: number) => { + this.syncAccountingControls(accountingRule); + }); + } + + private patchAccountingValues(mappings: any) { this.savingProductAccountingForm.patchValue({ - accountingRule: this.savingProductsTemplate.accountingRule.id + savingsReferenceAccountId: mappings.savingsReferenceAccount?.id || '', + savingsControlAccountId: mappings.savingsControlAccount?.id || '', + transfersInSuspenseAccountId: mappings.transfersInSuspenseAccount?.id || '', + interestOnSavingsAccountId: mappings.interestOnSavingsAccount?.id || '', + incomeFromFeeAccountId: mappings.incomeFromFeeAccount?.id || '', + incomeFromPenaltyAccountId: mappings.incomeFromPenaltyAccount?.id || '', + incomeFromInterestId: mappings.incomeFromInterest?.id || '', + overdraftPortfolioControlId: mappings.overdraftPortfolioControl?.id || '', + writeOffAccountId: mappings.writeOffAccount?.id || '', + feesReceivableAccountId: mappings.feeReceivableAccount?.id || '', + penaltiesReceivableAccountId: mappings.penaltyReceivableAccount?.id || '', + interestPayableAccountId: mappings.interestPayableAccount?.id || '', + interestReceivableAccountId: mappings.interestReceivableAccount?.id || '', + escheatLiabilityId: mappings.escheatLiabilityAccount?.id || '' }); if (this.isCashOrAccrualAccounting()) { - this.savingProductAccountingForm.patchValue({ - savingsReferenceAccountId: this.savingProductsTemplate.accountingMappings.savingsReferenceAccount.id, - overdraftPortfolioControlId: this.savingProductsTemplate.accountingMappings.overdraftPortfolioControl.id, - savingsControlAccountId: this.savingProductsTemplate.accountingMappings.savingsControlAccount.id, - transfersInSuspenseAccountId: this.savingProductsTemplate.accountingMappings.transfersInSuspenseAccount.id, - interestOnSavingsAccountId: this.savingProductsTemplate.accountingMappings.interestOnSavingsAccount.id, - writeOffAccountId: this.savingProductsTemplate.accountingMappings.writeOffAccount.id, - incomeFromFeeAccountId: this.savingProductsTemplate.accountingMappings.incomeFromFeeAccount.id, - incomeFromPenaltyAccountId: this.savingProductsTemplate.accountingMappings.incomeFromPenaltyAccount.id, - incomeFromInterestId: this.savingProductsTemplate.accountingMappings.incomeFromInterest.id, - advancedAccountingRules: - this.savingProductsTemplate.paymentChannelToFundSourceMappings || - this.savingProductsTemplate.feeToIncomeAccountMappings || - this.savingProductsTemplate.penaltyToIncomeAccountMappings || - this.savingProductsTemplate.accrualCharges - ? true - : false - }); - - if (this.isAccrualAccounting()) { - this.savingProductAccountingForm.patchValue({ - feesReceivableAccountId: this.savingProductsTemplate.accountingMappings.feeReceivableAccount.id, - penaltiesReceivableAccountId: this.savingProductsTemplate.accountingMappings.penaltyReceivableAccount.id, - interestReceivableAccountId: - this.savingProductsTemplate.accountingMappings.interestReceivableAccount == null || undefined - ? '' - : this.savingProductsTemplate.accountingMappings.interestReceivableAccount.id, - interestPayableAccountId: this.savingProductsTemplate.accountingMappings.interestPayableAccount.id - }); - } - - if (this.isDormancyTrackingActive.value) { - this.savingProductAccountingForm.patchValue({ - escheatLiabilityId: this.savingProductsTemplate.accountingMappings.escheatLiabilityAccount.id - }); - } - this.savingProductAccountingForm.setControl( - 'paymentChannelToFundSourceMappings', - this.formBuilder.array( - (this.savingProductsTemplate.paymentChannelToFundSourceMappings || []).map((paymentFundSource: any) => ({ - paymentTypeId: paymentFundSource.paymentType.id, - fundSourceAccountId: paymentFundSource.fundSourceAccount.id - })) - ) - ); - this.savingProductAccountingForm.setControl( - 'feeToIncomeAccountMappings', - this.formBuilder.array( - (this.savingProductsTemplate.feeToIncomeAccountMappings || []).map((feesIncome: any) => ({ - chargeId: feesIncome.charge.id, - incomeAccountId: feesIncome.incomeAccount.id - })) - ) - ); - this.savingProductAccountingForm.setControl( - 'penaltyToIncomeAccountMappings', - this.formBuilder.array( - (this.savingProductsTemplate.penaltyToIncomeAccountMappings || []).map((penaltyIncome: any) => ({ - chargeId: penaltyIncome.charge.id, - incomeAccountId: penaltyIncome.incomeAccount.id - })) - ) + // Backend serializes as 'otherGlMappingsMap'; fall back to 'otherGlMappings' for compatibility + const glMappings = mappings.otherGlMappingsMap || mappings.otherGlMappings; + this.setOtherGlMappings('assetsOtherGlMappings', glMappings?.assets || [], this.assetAccountOptions); + this.setOtherGlMappings( + 'liabilitiesOtherGlMappings', + glMappings?.liabilities || [], + this.liabilityAccountOptions ); + this.setOtherGlMappings('expensesOtherGlMappings', glMappings?.expenses || [], this.expenseAccountOptions); + this.setOtherGlMappings('incomeOtherGlMappings', glMappings?.income || [], this.incomeAccountOptions); } } - createsavingProductAccountingForm() { - this.savingProductAccountingForm = this.formBuilder.group({ - accountingRule: [1] + private createOtherGlMappingGroup(otherGlMapping: Partial = {}) { + return this.formBuilder.group({ + accountName: [ + otherGlMapping.accountName || '', + Validators.required + ], + glAccountId: [ + otherGlMapping.glAccountId || '', + Validators.required + ] }); } - setConditionalControls() { - this.savingProductAccountingForm.get('accountingRule').valueChanges.subscribe((accountingRule: any) => { - if (accountingRule === 2 || accountingRule === 3) { - this.savingProductAccountingForm.addControl( - 'savingsReferenceAccountId', - new UntypedFormControl('', Validators.required) - ); - this.savingProductAccountingForm.addControl( - 'overdraftPortfolioControlId', - new UntypedFormControl('', Validators.required) - ); - this.savingProductAccountingForm.addControl( - 'savingsControlAccountId', - new UntypedFormControl('', Validators.required) - ); - this.savingProductAccountingForm.addControl( - 'transfersInSuspenseAccountId', - new UntypedFormControl('', Validators.required) - ); - this.savingProductAccountingForm.addControl( - 'interestOnSavingsAccountId', - new UntypedFormControl('', Validators.required) - ); - this.savingProductAccountingForm.addControl( - 'writeOffAccountId', - new UntypedFormControl('', Validators.required) - ); - this.savingProductAccountingForm.addControl( - 'incomeFromFeeAccountId', - new UntypedFormControl('', Validators.required) - ); - this.savingProductAccountingForm.addControl( - 'incomeFromPenaltyAccountId', - new UntypedFormControl('', Validators.required) - ); - this.savingProductAccountingForm.addControl( - 'incomeFromInterestId', - new UntypedFormControl('', Validators.required) - ); - this.savingProductAccountingForm.addControl('advancedAccountingRules', new UntypedFormControl(false)); + getOtherGlMappingsArray(controlName: string): UntypedFormArray { + return this.savingProductAccountingForm.get(controlName) as UntypedFormArray; + } - if (accountingRule === 3) { - this.savingProductAccountingForm.addControl( - 'feesReceivableAccountId', - new UntypedFormControl('', Validators.required) - ); - this.savingProductAccountingForm.addControl( - 'penaltiesReceivableAccountId', - new UntypedFormControl('', Validators.required) - ); - if (this.allowOverdraft.value) { - this.savingProductAccountingForm.addControl('interestReceivableAccountId', new UntypedFormControl('')); - } - this.allowOverdraft.valueChanges.subscribe((allowOverdraft: boolean) => { - if (allowOverdraft) { - this.savingProductAccountingForm.addControl('interestReceivableAccountId', new UntypedFormControl('')); - } else { - this.savingProductAccountingForm.removeControl('interestReceivableAccountId'); - } - }); - this.savingProductAccountingForm.addControl( - 'interestPayableAccountId', - new UntypedFormControl('', Validators.required) - ); - } - if (accountingRule === 2) { - this.savingProductAccountingForm.removeControl('feesReceivableAccountId'); - this.savingProductAccountingForm.removeControl('penaltiesReceivableAccountId'); - this.savingProductAccountingForm.removeControl('interestPayableAccountId'); - } + private setOtherGlMappings(controlName: string, otherGlMappings: any[], options: GLAccount[]) { + const otherGlMappingsArray = this.getOtherGlMappingsArray(controlName); + otherGlMappingsArray.clear(); + (otherGlMappings || []).forEach((otherGlMapping) => { + otherGlMappingsArray.push( + this.createOtherGlMappingGroup({ + accountName: otherGlMapping.accountName, + glAccountId: + otherGlMapping.glAccountId || options.find((option) => option.glCode === otherGlMapping.glCode)?.id || null + }) + ); + }); + } - if (this.isDormancyTrackingActive.value) { - this.savingProductAccountingForm.addControl( - 'escheatLiabilityId', - new UntypedFormControl('', Validators.required) - ); - } + addOtherGlMapping(controlName: string) { + this.getOtherGlMappingsArray(controlName).push(this.createOtherGlMappingGroup()); + } - this.isDormancyTrackingActive.valueChanges.subscribe((isDormancyTrackingActive: boolean) => { - if (isDormancyTrackingActive) { - this.savingProductAccountingForm.addControl( - 'escheatLiabilityId', - new UntypedFormControl('', Validators.required) - ); - } else { - this.savingProductAccountingForm.removeControl('escheatLiabilityId'); - } - }); + removeOtherGlMapping(controlName: string, index: number) { + this.getOtherGlMappingsArray(controlName).removeAt(index); + } - this.savingProductAccountingForm - .get('advancedAccountingRules') - .valueChanges.subscribe((advancedAccountingRules: boolean) => { - if (advancedAccountingRules) { - this.savingProductAccountingForm.addControl( - 'paymentChannelToFundSourceMappings', - this.formBuilder.array([]) - ); - this.savingProductAccountingForm.addControl('feeToIncomeAccountMappings', this.formBuilder.array([])); - this.savingProductAccountingForm.addControl('penaltyToIncomeAccountMappings', this.formBuilder.array([])); - } else { - this.savingProductAccountingForm.removeControl('paymentChannelToFundSourceMappings'); - this.savingProductAccountingForm.removeControl('feeToIncomeAccountMappings'); - this.savingProductAccountingForm.removeControl('penaltyToIncomeAccountMappings'); - } - }); + getOtherGlMappingControl(controlName: string, index: number, fieldName: string): UntypedFormControl { + return this.getOtherGlMappingsArray(controlName).at(index).get(fieldName) as UntypedFormControl; + } + + private clearOtherGlMappings() { + [ + 'assetsOtherGlMappings', + 'liabilitiesOtherGlMappings', + 'expensesOtherGlMappings', + 'incomeOtherGlMappings' + ].forEach((controlName) => this.getOtherGlMappingsArray(controlName).clear()); + } + + private setOverdraftControl() { + this.allowOverdraft?.valueChanges?.subscribe(() => { + this.syncAccountingControls(this.savingProductAccountingForm.value.accountingRule); + }); + } + + private setDormancyControl() { + this.isDormancyTrackingActive?.valueChanges?.subscribe(() => { + this.syncAccountingControls(this.savingProductAccountingForm.value.accountingRule); + }); + } + + private getOtherGlMappings() { + const mapOtherGlMappings = (controlName: string, options: GLAccount[]) => + this.getOtherGlMappingsArray(controlName) + .value.filter((item: OtherGlMappingValue) => !!item.accountName || !!item.glAccountId) + .map((item: OtherGlMappingValue) => ({ + accountName: item.accountName, + glCode: options.find((option) => option.id === item.glAccountId)?.glCode || '' + })) + .filter((item: { accountName: string; glCode: string }) => !!item.accountName || !!item.glCode); + + const otherGlMappings = { + assets: mapOtherGlMappings('assetsOtherGlMappings', this.assetAccountOptions), + liabilities: mapOtherGlMappings('liabilitiesOtherGlMappings', this.liabilityAccountOptions), + expenses: mapOtherGlMappings('expensesOtherGlMappings', this.expenseAccountOptions), + income: mapOtherGlMappings('incomeOtherGlMappings', this.incomeAccountOptions) + }; + + return Object.values(otherGlMappings).some((value) => value.length) ? otherGlMappings : null; + } + + isDormancyTrackingEnabled(): boolean { + return this.isDormancyTrackingActive?.value ?? this.isDormancyTrackingActive ?? false; + } + + private syncAccountingControls(accountingRule: number) { + const isDormancyActive = this.isDormancyTrackingEnabled(); + + const addControl = (controlName: string, required = false) => { + if (!this.savingProductAccountingForm.contains(controlName)) { + this.savingProductAccountingForm.addControl( + controlName, + new UntypedFormControl('', required ? Validators.required : undefined) + ); + } else if (required) { + this.savingProductAccountingForm.get(controlName)?.setValidators(Validators.required); + this.savingProductAccountingForm.get(controlName)?.updateValueAndValidity({ emitEvent: false }); + } + }; + + const removeControl = (controlName: string) => { + if (this.savingProductAccountingForm.contains(controlName)) { + this.savingProductAccountingForm.removeControl(controlName); + } + }; + + if (this.accounting.isCashOrAccrualAccountingRuleId(accountingRule)) { + addControl('savingsReferenceAccountId', true); + addControl('savingsControlAccountId', true); + addControl('transfersInSuspenseAccountId', true); + addControl('interestOnSavingsAccountId', true); + addControl('incomeFromFeeAccountId', true); + addControl('incomeFromPenaltyAccountId', true); + addControl('incomeFromInterestId', true); + addControl('writeOffAccountId', true); + addControl('overdraftPortfolioControlId', false); + + if (this.accounting.isAccrualAccountingRuleId(accountingRule)) { + addControl('feesReceivableAccountId', true); + addControl('penaltiesReceivableAccountId', true); + addControl('interestPayableAccountId', true); + addControl('interestReceivableAccountId', false); } else { - this.savingProductAccountingForm.removeControl('savingsReferenceAccountId'); - this.savingProductAccountingForm.removeControl('overdraftPortfolioControlId'); - this.savingProductAccountingForm.removeControl('savingsControlAccountId'); - this.savingProductAccountingForm.removeControl('transfersInSuspenseAccountId'); - this.savingProductAccountingForm.removeControl('interestOnSavingsAccountId'); - this.savingProductAccountingForm.removeControl('writeOffAccountId'); - this.savingProductAccountingForm.removeControl('incomeFromFeeAccountId'); - this.savingProductAccountingForm.removeControl('incomeFromPenaltyAccountId'); - this.savingProductAccountingForm.removeControl('incomeFromInterestId'); - this.savingProductAccountingForm.removeControl('advancedAccountingRules'); - this.savingProductAccountingForm.removeControl('escheatLiabilityId'); - this.savingProductAccountingForm.removeControl('feesReceivableAccountId'); - this.savingProductAccountingForm.removeControl('penaltiesReceivableAccountId'); - this.savingProductAccountingForm.removeControl('interestReceivableAccountId'); - this.savingProductAccountingForm.removeControl('interestPayableAccountId'); + removeControl('feesReceivableAccountId'); + removeControl('penaltiesReceivableAccountId'); + removeControl('interestPayableAccountId'); + removeControl('interestReceivableAccountId'); } - }); - } - get paymentChannelToFundSourceMappings(): UntypedFormArray { - return this.savingProductAccountingForm.get('paymentChannelToFundSourceMappings') as UntypedFormArray; - } - - get feeToIncomeAccountMappings(): UntypedFormArray { - return this.savingProductAccountingForm.get('feeToIncomeAccountMappings') as UntypedFormArray; - } - - get penaltyToIncomeAccountMappings(): UntypedFormArray { - return this.savingProductAccountingForm.get('penaltyToIncomeAccountMappings') as UntypedFormArray; - } - - setSavingProductAccountingFormDirty() { - if (this.savingProductAccountingForm.pristine) { - this.savingProductAccountingForm.markAsDirty(); + if (isDormancyActive) { + addControl('escheatLiabilityId', false); + } else { + removeControl('escheatLiabilityId'); + } + } else { + removeControl('savingsReferenceAccountId'); + removeControl('savingsControlAccountId'); + removeControl('transfersInSuspenseAccountId'); + removeControl('interestOnSavingsAccountId'); + removeControl('incomeFromFeeAccountId'); + removeControl('incomeFromPenaltyAccountId'); + removeControl('incomeFromInterestId'); + removeControl('writeOffAccountId'); + removeControl('overdraftPortfolioControlId'); + removeControl('feesReceivableAccountId'); + removeControl('penaltiesReceivableAccountId'); + removeControl('interestPayableAccountId'); + removeControl('interestReceivableAccountId'); + removeControl('escheatLiabilityId'); + this.clearOtherGlMappings(); } } - existCharges(): boolean { - return this.chargeData.length > 0; - } - - add(formType: string, formArray: UntypedFormArray) { - const data = { ...this.getData(formType), pristine: false }; - const dialogRef = this.dialog.open(FormDialogComponent, { data, width: '20rem' }); - dialogRef.afterClosed().subscribe((response: any) => { - if (response.data) { - formArray.push(response.data); - this.setSavingProductAccountingFormDirty(); - } - }); - } - - edit(formType: string, formArray: UntypedFormArray, index: number) { - const data = { ...this.getData(formType, formArray.at(index).value), layout: { addButtonText: 'Edit' } }; - const dialogRef = this.dialog.open(FormDialogComponent, { data }); - dialogRef.afterClosed().subscribe((response: any) => { - if (response.data) { - formArray.at(index).patchValue(response.data.value); - this.setSavingProductAccountingFormDirty(); - } - }); - } - - delete(formArray: UntypedFormArray, index: number) { - const dialogRef = this.dialog.open(DeleteDialogComponent, { - data: { deleteContext: `this` } - }); - dialogRef.afterClosed().subscribe((response: any) => { - if (response.delete) { - formArray.removeAt(index); - this.setSavingProductAccountingFormDirty(); - } - }); - } - - getData(formType: string, values?: any) { - switch (formType) { - case 'PaymentFundSource': - return { - title: this.translateService.instant('labels.heading.Configure Fund Sources for Payment Channels'), - formfields: this.getPaymentFundSourceFormfields(values) - }; - case 'FeesIncome': - return { - title: this.translateService.instant('labels.heading.Map Fees to Specific Income Accounts'), - formfields: this.getFeesIncomeFormfields(values) - }; - case 'PenaltyIncome': - return { - title: this.translateService.instant('labels.heading.Map Penalties to Specific Income Accounts'), - formfields: this.getPenaltyIncomeFormfields(values) - }; - } - } - - getPaymentFundSourceFormfields(values?: any) { - const formfields: FormfieldBase[] = [ - new SelectBase({ - controlName: 'paymentTypeId', - label: this.translateService.instant('labels.inputs.Payment Type'), - value: values ? values.paymentTypeId : this.paymentTypeData[0].id, - options: { label: 'name', value: 'id', data: this.paymentTypeData }, - required: true, - order: 1 - }), - new SelectBase({ - controlName: 'fundSourceAccountId', - label: this.translateService.instant('labels.inputs.Fund Source'), - value: values ? values.fundSourceAccountId : this.combinedAccountData[0].id, - options: { label: 'name', value: 'id', data: this.combinedAccountData }, - required: true, - order: 2 - }) - ]; - return formfields; - } - - getFeesIncomeFormfields(values?: any) { - const formfields: FormfieldBase[] = [ - new SelectBase({ - controlName: 'chargeId', - label: this.translateService.instant('labels.inputs.Fees'), - value: values ? values.chargeId : this.chargeData[0].id, - options: { label: 'name', value: 'id', data: this.chargeData }, - required: true, - order: 1 - }), - new SelectBase({ - controlName: 'incomeAccountId', - label: this.translateService.instant('labels.inputs.Income Account'), - value: values ? values.incomeAccountId : this.incomeAccountData[0].id, - options: { label: 'name', value: 'id', data: this.incomeAccountData }, - required: true, - order: 2 - }) - ]; - return formfields; - } - - getPenaltyIncomeFormfields(values?: any) { - const formfields: FormfieldBase[] = [ - new SelectBase({ - controlName: 'chargeId', - label: this.translateService.instant('labels.inputs.Penalty'), - value: values ? values.chargeId : this.penaltyData[0].id, - options: { label: 'name', value: 'id', data: this.penaltyData }, - required: true, - order: 1 - }), - new SelectBase({ - controlName: 'incomeAccountId', - label: this.translateService.instant('labels.inputs.Income Account'), - value: values ? values.incomeAccountId : this.incomeAccountData[0].id, - options: { label: 'name', value: 'id', data: this.incomeAccountData }, - required: true, - order: 2 - }) - ]; - return formfields; - } - get savingProductAccounting() { - return this.savingProductAccountingForm.value; + const { + assetsOtherGlMappings, + liabilitiesOtherGlMappings, + expensesOtherGlMappings, + incomeOtherGlMappings, + ...savingProductAccounting + } = this.savingProductAccountingForm.value; + const otherGlMappings = this.getOtherGlMappings(); + + return { + ...savingProductAccounting, + ...(otherGlMappings ? { otherGlMappings } : {}) + }; } isCashOrAccrualAccounting(): boolean { - return ( - this.savingProductAccountingForm.value.accountingRule === 2 || - this.savingProductAccountingForm.value.accountingRule === 3 - ); + return this.accounting.isCashOrAccrualAccountingRuleId(this.savingProductAccountingForm.value.accountingRule); } isAccrualAccounting(): boolean { - return this.savingProductAccountingForm.value.accountingRule === 3; + return this.accounting.isAccrualAccountingRuleId(this.savingProductAccountingForm.value.accountingRule); + } + + buildAccountingSectionConfigs() { + this.accountingSectionConfigs = [ + { + key: 'assets', + title: 'Assets', + pillLabel: 'Asset', + pillClass: 'green', + rows: [ + { + key: 'savingsReferenceAccountId', + label: 'Savings Reference Account', + inputLabel: 'Savings reference account', + controlName: 'savingsReferenceAccountId', + options: this.assetAccountOptions, + required: true + }, + { + key: 'overdraftPortfolioControlId', + label: 'Overdraft Portfolio Control', + inputLabel: 'Overdraft portfolio control', + controlName: 'overdraftPortfolioControlId', + options: this.assetAccountOptions + }, + { + key: 'interestReceivableAccountId', + label: 'Interest Receivable Account', + inputLabel: 'Interest receivable account', + controlName: 'interestReceivableAccountId', + options: this.assetAccountOptions, + accrualOnly: true + }, + { + key: 'feesReceivableAccountId', + label: 'Fees Receivable Account', + inputLabel: 'Fees receivable account', + controlName: 'feesReceivableAccountId', + options: this.assetAccountOptions, + accrualOnly: true + }, + { + key: 'penaltiesReceivableAccountId', + label: 'Penalties Receivable Account', + inputLabel: 'Penalties receivable account', + controlName: 'penaltiesReceivableAccountId', + options: this.assetAccountOptions, + accrualOnly: true + } + ], + otherMappingsControlName: 'assetsOtherGlMappings', + otherAccountOptions: this.assetAccountOptions + }, + { + key: 'liabilities', + title: 'Liabilities', + pillLabel: 'Liability', + pillClass: 'green', + rows: [ + { + key: 'savingsControlAccountId', + label: 'Savings Control Account', + inputLabel: 'Savings control account', + controlName: 'savingsControlAccountId', + options: this.liabilityAccountOptions, + required: true + }, + { + key: 'transfersInSuspenseAccountId', + label: 'Transfers In Suspense Account', + inputLabel: 'Transfers in suspense account', + controlName: 'transfersInSuspenseAccountId', + options: this.liabilityAccountOptions, + required: true + }, + { + key: 'interestPayableAccountId', + label: 'Interest Payable Account', + inputLabel: 'Interest payable account', + controlName: 'interestPayableAccountId', + options: this.liabilityAccountOptions, + accrualOnly: true + }, + { + key: 'escheatLiabilityId', + label: 'Escheat Liability Account', + inputLabel: 'Escheat liability account', + controlName: 'escheatLiabilityId', + options: this.liabilityAccountOptions, + dormancyOnly: true + } + ], + otherMappingsControlName: 'liabilitiesOtherGlMappings', + otherAccountOptions: this.liabilityAccountOptions + }, + { + key: 'expenses', + title: 'Expenses', + pillLabel: 'Expense', + pillClass: 'amber', + rows: [ + { + key: 'interestOnSavingsAccountId', + label: 'Interest On Savings Account', + inputLabel: 'Interest on savings account', + controlName: 'interestOnSavingsAccountId', + options: this.expenseAccountOptions, + required: true + }, + { + key: 'writeOffAccountId', + label: 'Write Off Account', + inputLabel: 'Write off account', + controlName: 'writeOffAccountId', + options: this.expenseAccountOptions, + required: true + } + ], + otherMappingsControlName: 'expensesOtherGlMappings', + otherAccountOptions: this.expenseAccountOptions + }, + { + key: 'income', + title: 'Income', + pillLabel: 'Income', + pillClass: 'blue', + rows: [ + { + key: 'incomeFromFeeAccountId', + label: 'Income From Fees', + inputLabel: 'Income from fees', + controlName: 'incomeFromFeeAccountId', + options: this.incomeAccountOptions, + required: true + }, + { + key: 'incomeFromPenaltyAccountId', + label: 'Income From Penalties', + inputLabel: 'Income from penalties', + controlName: 'incomeFromPenaltyAccountId', + options: this.incomeAccountOptions, + required: true + }, + { + key: 'incomeFromInterestId', + label: 'Income From Interest', + inputLabel: 'Income from interest', + controlName: 'incomeFromInterestId', + options: this.incomeAccountOptions, + required: true + } + ], + otherMappingsControlName: 'incomeOtherGlMappings', + otherAccountOptions: this.incomeAccountOptions + } + ]; } } diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.html b/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.html index ef48d062a..799064f6a 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.html +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.html @@ -10,74 +10,69 @@
-

{{ 'labels.inputs.Currency' | translate }}

+ Currency & Account Status Configuration
-
- - {{ 'labels.inputs.Currency' | translate }} - - @for (currency of currencyData; track currency) { - - {{ currency.name }} - - } - - - {{ 'labels.inputs.Currency' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.Decimal Places' | translate }} - - - {{ 'labels.inputs.Decimal Places' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - -
-
- -
-
- -

{{ 'labels.inputs.Multiples' | translate }}

-
-
-
- - {{ 'labels.inputs.Set the saving installment in multiples of' | translate }} - -
- - @if (savingProductCurrencyForm.value.setMultiples) { +
+
- {{ 'labels.inputs.Currency in multiples of' | translate }} - + {{ 'labels.inputs.Currency' | translate }} + + @for (currency of currencyData; track currency) { + + {{ currency.name }} + + } + + Default: Indian Rupee - auto-set for domestic products - {{ 'labels.inputs.Currency in multiples of' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.inputs.Currency' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} - } + + + {{ 'labels.inputs.Decimal Places' | translate }} + + @for (places of decimalPlacesData; track places) { + {{ places }} + } + + + {{ 'labels.inputs.Decimal Places' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + +
+ +
+
+

Dormancy Alert

+

Enable automated notifications when an account becomes inactive based on product rules.

+
+ + yes + no + +
+ +
+
+

Nomination Facility

+

Allow account holders to designate individuals to receive funds in the event of account holder's death.

+
+ + yes + no + +
diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.scss b/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.scss index eaf1489cd..a287d691e 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.scss +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.scss @@ -44,6 +44,17 @@ $gray-900: #202124; } } +.spc-section__badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: #eff6ff; + font-size: 14px; +} + .spc-grid { display: grid; grid-template-columns: repeat(2, 1fr); @@ -55,16 +66,49 @@ $gray-900: #202124; } } +.spc-body { + background: #fff; +} + .spc-field { width: 100%; - - &--full { - grid-column: 1 / -1; - } } -.spc-checkbox-row { +.spc-alert { display: flex; align-items: center; - padding: 4px 0; + justify-content: space-between; + gap: 24px; + padding: 18px 24px 24px; + border-top: 1px solid $border; + background: #f8fafc; + + @media (width <= 760px) { + flex-direction: column; + align-items: flex-start; + } + + &__content { + h4 { + margin: 0 0 6px; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + + p { + margin: 0; + font-size: 12px; + line-height: 1.5; + color: #64748b; + max-width: 540px; + } + } + + &__choices { + display: flex; + align-items: center; + gap: 18px; + flex-wrap: wrap; + } } diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.ts b/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.ts index c8f2da5f8..63ae1b3db 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.ts +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-currency-step/saving-product-currency-step.component.ts @@ -6,11 +6,10 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, OnInit, Input, inject, DestroyRef } from '@angular/core'; -import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { Component, OnInit, Input, inject } from '@angular/core'; import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; import { MatTooltip } from '@angular/material/tooltip'; -import { MatCheckbox } from '@angular/material/checkbox'; +import { MatRadioGroup, MatRadioButton } from '@angular/material/radio'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @@ -21,19 +20,24 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; imports: [ ...STANDALONE_SHARED_IMPORTS, MatTooltip, - FaIconComponent, - MatCheckbox + MatRadioGroup, + MatRadioButton, + FaIconComponent ] }) export class SavingProductCurrencyStepComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); - private destroyRef = inject(DestroyRef); @Input() savingProductsTemplate: any; savingProductCurrencyForm: UntypedFormGroup; currencyData: any; + decimalPlacesData = [ + 2, + 0, + 3 + ]; constructor() { this.createSavingProductCurrencyForm(); @@ -43,13 +47,11 @@ export class SavingProductCurrencyStepComponent implements OnInit { this.currencyData = this.savingProductsTemplate.currencyOptions; this.savingProductCurrencyForm.patchValue({ - currencyCode: this.savingProductsTemplate.currency.code || this.currencyData[0].code, - digitsAfterDecimal: this.savingProductsTemplate.digitsAfterDecimal ?? '', - setMultiples: !!this.savingProductsTemplate.inMultiplesOf, - inMultiplesOf: this.savingProductsTemplate.inMultiplesOf ?? '' + currencyCode: this.savingProductsTemplate.currency?.code || this.currencyData?.[0]?.code || '', + digitsAfterDecimal: this.savingProductsTemplate.digitsAfterDecimal ?? 2, + dormancyAlert: this.savingProductsTemplate.isDormancyTrackingActive ?? false, + nominationFacility: this.savingProductsTemplate.nominationFacility ?? false }); - - this.setupConditionalValidation(); } createSavingProductCurrencyForm() { @@ -59,39 +61,14 @@ export class SavingProductCurrencyStepComponent implements OnInit { Validators.required ], digitsAfterDecimal: [ - '', + 2, [ Validators.required, Validators.min(0) ] ], - setMultiples: [false], - inMultiplesOf: [''] - }); - } - - setupConditionalValidation() { - const inMultiplesOfControl = this.savingProductCurrencyForm.get('inMultiplesOf'); - const setMultiplesControl = this.savingProductCurrencyForm.get('setMultiples'); - if (setMultiplesControl?.value) { - inMultiplesOfControl?.setValidators([ - Validators.required, - Validators.min(1) - ]); - inMultiplesOfControl?.updateValueAndValidity(); - } - - setMultiplesControl?.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((checked) => { - if (checked) { - inMultiplesOfControl?.setValidators([ - Validators.required, - Validators.min(1) - ]); - } else { - inMultiplesOfControl?.clearValidators(); - inMultiplesOfControl?.setValue(''); - } - inMultiplesOfControl?.updateValueAndValidity(); + dormancyAlert: [false], + nominationFacility: [false] }); } @@ -99,13 +76,12 @@ export class SavingProductCurrencyStepComponent implements OnInit { const formValue = this.savingProductCurrencyForm.value; const result: any = { currencyCode: formValue.currencyCode, - digitsAfterDecimal: formValue.digitsAfterDecimal + digitsAfterDecimal: formValue.digitsAfterDecimal, + dormancyAlert: formValue.dormancyAlert, + isDormancyTrackingActive: formValue.dormancyAlert, + nominationFacility: formValue.nominationFacility }; - if (formValue.inMultiplesOf !== '' && formValue.inMultiplesOf !== null && formValue.inMultiplesOf !== undefined) { - result.inMultiplesOf = formValue.inMultiplesOf; - } - return result; } } diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.html b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.html index 30f8f0d7d..7a8c5d3dc 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.html +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.html @@ -54,4 +54,68 @@
+ +
+
+ +

Product Classification

+
+ +
+ + Account Type + + @for (accountType of accountTypeData; track accountType) { + {{ accountType }} + } + + Account Type is required + + + + Customer Segment + + @for (customerSegment of customerSegmentData; track customerSegment) { + {{ customerSegment }} + } + + + + + GL Account (Liability) + + @for (account of liabilityAccountOptions; track account.id) { + {{ account.name }} ({{ account.glCode }}) + } + + + + + Effective To + + + + + + + End Date + + + + + + + Introducer Required + + @for (option of yesNoOptions; track option.value) { + {{ option.label }} + } + + + +
+ Share Linking Enabled +
+
+
diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.scss b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.scss index 713bdab40..7b5c920ef 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.scss +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.scss @@ -51,6 +51,10 @@ $gray-900: #202124; } } +.spd-grid--classification { + grid-template-columns: repeat(3, 1fr); +} + .spd-field { width: 100%; @@ -58,3 +62,9 @@ $gray-900: #202124; grid-column: 1 / -1; } } + +.spd-checkbox-row { + display: flex; + align-items: center; + min-height: 56px; +} diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.ts b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.ts index febc0c9dd..c9d2ba566 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.ts +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-details-step/saving-product-details-step.component.ts @@ -6,11 +6,13 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ -import { Component, OnInit, Input, inject } from '@angular/core'; +import { Component, OnInit, OnChanges, Input, SimpleChanges, inject, ChangeDetectorRef } from '@angular/core'; import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; import { MatTooltip } from '@angular/material/tooltip'; import { CdkTextareaAutosize } from '@angular/cdk/text-field'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { Dates } from 'app/core/utils/dates'; +import { SettingsService } from 'app/settings/settings.service'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ @@ -24,12 +26,31 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; FaIconComponent ] }) -export class SavingProductDetailsStepComponent implements OnInit { +export class SavingProductDetailsStepComponent implements OnInit, OnChanges { private formBuilder = inject(UntypedFormBuilder); + private dateUtils = inject(Dates); + private settingsService = inject(SettingsService); + private cdr = inject(ChangeDetectorRef); @Input() savingProductsTemplate: any; savingProductDetailsForm: UntypedFormGroup; + accountTypeData: string[] = []; + customerSegmentData: string[] = []; + liabilityAccountOptions: any[] = []; + yesNoOptions: Array<{ + value: boolean; + label: string; + }> = [ + { + value: true, + label: 'Yes' + }, + { + value: false, + label: 'No' + } + ]; constructor() { this.createSavingProductDetailsForm(); @@ -37,11 +58,13 @@ export class SavingProductDetailsStepComponent implements OnInit { ngOnInit() { if (this.savingProductsTemplate) { - this.savingProductDetailsForm.patchValue({ - name: this.savingProductsTemplate.name, - shortName: this.savingProductsTemplate.shortName, - description: this.savingProductsTemplate.description - }); + this.applyTemplate(this.savingProductsTemplate); + } + } + + ngOnChanges(changes: SimpleChanges) { + if (changes.savingProductsTemplate?.currentValue) { + this.applyTemplate(changes.savingProductsTemplate.currentValue); } } @@ -55,11 +78,117 @@ export class SavingProductDetailsStepComponent implements OnInit { '', Validators.required ], - description: [''] + description: [''], + accountType: [ + null, + Validators.required + ], + customerSegment: [null], + glAccountId: [null], + effectiveDate: [''], + effectiveTo: [''], + endDate: [''], + introducerRequired: [false], + shareLinkingEnabled: [false], + nominationFacility: [false], + dormancyAlert: [false] }); } + private getOptionNames(options: any[]): string[] { + if (!Array.isArray(options)) { + return []; + } + + return options + .map((option) => option?.name ?? option?.value ?? option) + .filter((value) => typeof value === 'string' && value.trim().length > 0); + } + + private applyTemplate(template: any) { + const accountTypeOptions = this.getOptionNames(template.accountTypeOptions); + const customerSegmentOptions = this.getOptionNames(template.customerSegmentOptions); + const yesNoOptions = this.getYesNoOptions(template.yesNoOptions); + + this.accountTypeData = accountTypeOptions; + this.customerSegmentData = customerSegmentOptions; + this.liabilityAccountOptions = template.accountingMappingOptions?.liabilityAccountOptions ?? []; + this.cdr.detectChanges(); + if (yesNoOptions.length) { + this.yesNoOptions = yesNoOptions; + } + + // Use existing product data if available (when editing), otherwise use template defaults + const isEditing = !!template.id; + this.savingProductDetailsForm.patchValue({ + name: isEditing ? template.name : template.name || '', + shortName: isEditing ? template.shortName : template.shortName || '', + description: template.description, + accountType: template.accountType ?? this.accountTypeData[0] ?? null, + customerSegment: template.customerSegment ?? this.customerSegmentData[0] ?? null, + glAccountId: template.glAccountId ?? null, + effectiveDate: template.effectiveDate ? new Date(template.effectiveDate) : '', + effectiveTo: template.effectiveTo ? new Date(template.effectiveTo) : '', + endDate: template.endDate ? new Date(template.endDate) : '', + introducerRequired: template.introducerRequired === true || template.introducerRequired === 'Yes', + shareLinkingEnabled: !!template.shareLinkingEnabled, + nominationFacility: !!template.nominationFacility, + dormancyAlert: !!template.dormancyAlert + }); + } + + private getYesNoOptions(options: any[]): Array<{ value: boolean; label: string }> { + if (!Array.isArray(options)) { + return []; + } + + return options + .map((option) => { + const rawValue = option?.name ?? option?.value ?? option; + const normalized = typeof rawValue === 'string' ? rawValue.trim().toUpperCase() : rawValue; + + if (normalized === 'YES') { + return { + value: true, + label: 'Yes' + }; + } + + if (normalized === 'NO') { + return { + value: false, + label: 'No' + }; + } + + return null; + }) + .filter((value) => !!value); + } + + compareById(o1: any, o2: any): boolean { + return o1 != null && o2 != null && Number(o1) === Number(o2); + } + get savingProductDetails() { - return this.savingProductDetailsForm.value; + const formValue = { ...this.savingProductDetailsForm.value }; + const dateFormat = this.settingsService.dateFormat; + const prevEffectiveDate: Date = this.savingProductDetailsForm.value.effectiveDate; + const prevEffectiveTo: Date = this.savingProductDetailsForm.value.effectiveTo; + const prevEndDate: Date = this.savingProductDetailsForm.value.endDate; + + if (formValue.effectiveDate instanceof Date) { + formValue.effectiveDate = this.dateUtils.formatDate(prevEffectiveDate, dateFormat) || ''; + } + + if (formValue.effectiveTo instanceof Date) { + formValue.effectiveTo = this.dateUtils.formatDate(prevEffectiveTo, dateFormat) || ''; + } + + if (formValue.endDate instanceof Date) { + formValue.endDate = this.dateUtils.formatDate(prevEndDate, dateFormat) || ''; + } + + return formValue; } } diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component.html b/src/app/products/saving-products/saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component.html new file mode 100644 index 000000000..4224ebeaa --- /dev/null +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component.html @@ -0,0 +1,351 @@ + +
+
+ + + +
Transaction & Product Limits
+
R&D BASED DEFAULTS
+
+ +
+
+
Account Features (as per RBI guidelines)
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ +
+ + + + + + + + + + + + + + + + + @for (row of accountFacilityLimits; track row.limitType) { + + + + + + + } + +
Limit TypeGeneral Limit (Daily)Minor Account LimitSmall Saving Limit
{{ row.limitType }} + + + + + +
+
+ +
+

Deposit Limit (KYC)

+ + + + + + + + + + + + + + + + + + + + + + + + + +
Account TypeKYC TypeLimit
General + + + +
Minor + + + +
Small Saving + + + +
+
+ +
+ Note: + Small Saving Account limits are strictly capped at Rs.1,00,000 maximum balance and Rs.10,000 monthly withdrawal + per RBI Master Circular. Minor accounts are restricted from RTGS and high-risk digital products (BEPG/BBPS) by + default. +
+
+
diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component.scss b/src/app/products/saving-products/saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component.scss new file mode 100644 index 000000000..f0f7cad32 --- /dev/null +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component.scss @@ -0,0 +1,257 @@ +/** + * 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/. + */ + +$border: #d9e2ec; +$border-strong: #c7d2de; +$text: #24364b; +$muted: #64748b; +$header-bg: #f8fafc; +$highlight-bg: #eff6ff; + +.spl-section { + border: 1px solid $border; + border-radius: 12px; + overflow: hidden; + background: #fff; +} + +.spl-section__header { + display: flex; + align-items: center; + gap: 12px; + padding: 16px 20px; + background: $header-bg; + border-bottom: 1px solid $border; +} + +.spl-section__icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 28px; + height: 28px; + border-radius: 8px; + background: #eff6ff; + color: #2563eb; + font-size: 14px; + flex: 0 0 auto; +} + +.spl-section__title { + flex: 1 1 auto; + font-size: 16px; + font-weight: 600; + color: $text; +} + +.spl-section__badge { + flex: 0 0 auto; + padding: 5px 10px; + border-radius: 999px; + background: #dbeafe; + color: #1d4ed8; + font-size: 10px; + font-weight: 700; + letter-spacing: 0.05em; + white-space: nowrap; +} + +.spl-section__body { + padding: 18px 20px 20px; +} + +.spl-subsection { + border: 1px solid $border; + border-radius: 12px; + overflow: hidden; + background: #fff; + + & + & { + margin-top: 16px; + } +} + +.spl-subsection__header { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; + background: $header-bg; + border-bottom: 1px solid $border; + color: $text; + font-size: 14px; + font-weight: 600; +} + +.spl-subsection__icon { + display: inline-flex; + align-items: center; + justify-content: center; + width: 26px; + height: 26px; + border-radius: 8px; + background: #eff6ff; + color: #2563eb; + font-size: 13px; + flex: 0 0 auto; +} + +.spl-subsection__body { + padding: 16px 16px 18px; +} + +.spl-table-wrap { + overflow-x: auto; +} + +.spl-table { + width: 100%; + min-width: 0; + table-layout: fixed; + border-collapse: collapse; + border: 1px solid $border; + + col.spl-table__col--label { + width: 22%; + } + + col.spl-table__col--value { + width: 26%; + } + + th, + td { + border: 1px solid $border; + padding: 6px 8px; + vertical-align: middle; + } + + th { + background: #f8fafc; + color: #64748b; + text-align: center; + font-size: 11px; + font-weight: 700; + line-height: 1.3; + white-space: normal; + word-break: break-word; + } + + td:first-child { + background: #f8fafc; + font-weight: 600; + color: $text; + white-space: nowrap; + } + + td { + font-size: 13px; + overflow: hidden; + } +} + +.spl-table__highlight td:first-child { + color: #1d4ed8; +} + +.spl-input, +.spl-select { + width: 100%; + box-sizing: border-box; + font: inherit; +} + +.spl-input { + height: 30px; + border: 1px solid $border-strong; + border-radius: 2px; + padding: 0 8px; + background: #fff; +} + +.spl-input:disabled, +.spl-select:disabled { + background: #f8fafc; + color: $muted; +} + +.spl-select { + height: 30px; + border: 1px solid $border-strong; + border-radius: 2px; + padding: 0 6px; + background: #fff; +} + +.spl-input--stacked { + margin-top: 6px; +} + +.spl-input--disabled { + background: #f8fafc; + color: $muted; +} + +.spl-table__muted { + font-size: 11px; + color: #94a3b8; + letter-spacing: 0.03em; +} + +.spl-note { + margin-top: 16px; + padding: 12px 14px; + border: 1px solid $border; + border-radius: 8px; + background: #f8fafc; + color: $muted; + font-size: 11px; + line-height: 1.6; +} + +.spl-features { + margin-bottom: 16px; +} + +.spl-features__grid { + display: grid; + grid-template-columns: repeat(3, minmax(0, 1fr)); + gap: 12px 16px; + + @media (width <= 900px) { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + + @media (width <= 640px) { + grid-template-columns: 1fr; + } +} + +.spl-feature { + display: flex; + flex-direction: column; + gap: 6px; + font-size: 13px; + color: $text; +} + +.spl-feature__label { + display: inline-flex; + align-items: center; + gap: 8px; + min-height: 18px; + font-weight: 600; + color: #334155; + + i { + color: #2563eb; + font-size: 12px; + width: 14px; + text-align: center; + } +} diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component.ts b/src/app/products/saving-products/saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component.ts new file mode 100644 index 000000000..0f057264f --- /dev/null +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-limits-step/saving-product-limits-step.component.ts @@ -0,0 +1,250 @@ +/** + * 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, OnChanges, SimpleChanges } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + +type BooleanFeatureKey = + | 'atmFacility' + | 'atmWithdrawalFacility' + | 'upiFacility' + | 'posTransactionsFacility' + | 'chequeBook' + | 'impsFacility' + | 'smsAlerts' + | 'bbpsFacility' + | 'insuranceCover' + | 'bepgFacility' + | 'neftFacility' + | 'rtgsFacility' + | 'interBankTotalCapFacility' + | 'withdrawalFacility' + | 'depositFacilityKyc' + | 'virtualDebitCard'; + +interface FacilityLimitRow { + limitType: string; + generalLimit: string | number | null; + minorAccountLimit: string | number | null; + smallSavingLimit: string | number | null; +} + +@Component({ + selector: 'mifosx-saving-product-limits-step', + templateUrl: './saving-product-limits-step.component.html', + styleUrls: ['./saving-product-limits-step.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FormsModule + ] +}) +export class SavingProductLimitsStepComponent implements OnChanges { + @Input() savingProductsTemplate: any; + pristine = true; + + yesNoOptions: Array<{ + value: boolean; + label: string; + }> = [ + { + value: true, + label: 'Yes' + }, + { + value: false, + label: 'No' + } + ]; + + accountFacilityConfig: Record = { + atmFacility: true, + atmWithdrawalFacility: true, + upiFacility: true, + posTransactionsFacility: true, + chequeBook: true, + impsFacility: true, + smsAlerts: true, + bbpsFacility: true, + insuranceCover: false, + bepgFacility: true, + neftFacility: true, + rtgsFacility: true, + interBankTotalCapFacility: true, + withdrawalFacility: true, + depositFacilityKyc: true, + virtualDebitCard: false + }; + + accountFacilityLimits: FacilityLimitRow[] = [ + { + limitType: 'ATM Withdrawal', + generalLimit: 40000, + minorAccountLimit: 5000, + smallSavingLimit: 10000 + }, + { + limitType: 'POS Transactions', + generalLimit: 100000, + minorAccountLimit: 2000, + smallSavingLimit: 10000 + }, + { + limitType: 'UPI Limit', + generalLimit: 50000, + minorAccountLimit: 1000, + smallSavingLimit: 5000 + }, + { + limitType: 'IMPS Limit', + generalLimit: 50000, + minorAccountLimit: 100, + smallSavingLimit: 5000 + }, + { + limitType: 'BBPS', + generalLimit: 25000, + minorAccountLimit: 0, + smallSavingLimit: 2000 + }, + { + limitType: 'BEPG', + generalLimit: 25000, + minorAccountLimit: 0, + smallSavingLimit: 2000 + }, + { + limitType: 'NEFT Limit', + generalLimit: 1000000, + minorAccountLimit: 25000, + smallSavingLimit: 10000 + }, + { + limitType: 'RTGS Limit (Min 2L)', + generalLimit: 5000000, + minorAccountLimit: 'N/A', + smallSavingLimit: 'N/A' + }, + { + limitType: 'Inter-Bank Total Cap', + generalLimit: 5000000, + minorAccountLimit: 25000, + smallSavingLimit: 10000 + }, + { + limitType: 'Withdrawal Limit', + generalLimit: 'UNLIMITED', + minorAccountLimit: 10000, + smallSavingLimit: 10000 + } + ]; + + depositLimitKyc = { + general: { + kycType: 'Full KYC', + limit: 'No Limit' + }, + minor: { + kycType: 'Guardian KYC', + limit: 100000 + }, + smallSaving: { + kycType: 'Min KYC', + limit: 50000 + } + }; + + ngOnChanges(changes: SimpleChanges) { + if (changes.savingProductsTemplate?.currentValue) { + const template = changes.savingProductsTemplate.currentValue; + this.setYesNoOptions(template); + this.populateFromTemplate(template); + } + } + + markAsDirty() { + this.pristine = false; + } + + private populateFromTemplate(template: any) { + const isEditing = !!template.id; + + // Populate accountFacilityConfig from existing product + if (isEditing && template.accountFacilityConfig) { + this.accountFacilityConfig = { ...this.accountFacilityConfig, ...template.accountFacilityConfig }; + } + + // Populate accountFacilityLimits from existing product + if (isEditing && template.accountFacilityLimits && template.accountFacilityLimits.length > 0) { + this.accountFacilityLimits = template.accountFacilityLimits.map((limit: any) => ({ + limitType: limit.limitType || '', + generalLimit: limit.generalLimit ?? null, + minorAccountLimit: limit.minorAccountLimit ?? null, + smallSavingLimit: limit.smallSavingLimit ?? null + })); + } + + // Populate depositLimitKyc from existing product + if (isEditing && template.depositLimitKyc) { + this.depositLimitKyc = { + general: { + kycType: template.depositLimitKyc.general?.kycType || 'Full KYC', + limit: template.depositLimitKyc.general?.limit || 'No Limit' + }, + minor: { + kycType: template.depositLimitKyc.minor?.kycType || 'Guardian KYC', + limit: template.depositLimitKyc.minor?.limit ?? 100000 + }, + smallSaving: { + kycType: template.depositLimitKyc.smallSaving?.kycType || 'Min KYC', + limit: template.depositLimitKyc.smallSaving?.limit ?? 50000 + } + }; + } + } + + private setYesNoOptions(template: any) { + const options = Array.isArray(template?.yesNoOptions) ? template.yesNoOptions : []; + const mappedOptions = options + .map((option: any) => { + const rawValue = option?.name ?? option?.value ?? option; + const normalized = typeof rawValue === 'string' ? rawValue.trim().toUpperCase() : rawValue; + + if (normalized === 'YES') { + return { + value: true, + label: 'Yes' + }; + } + + if (normalized === 'NO') { + return { + value: false, + label: 'No' + }; + } + + return null; + }) + .filter((value: any) => !!value); + + if (mappedOptions.length) { + this.yesNoOptions = mappedOptions; + } + } + + get savingProductLimits() { + return { + accountFacilityConfig: { ...this.accountFacilityConfig }, + accountFacilityLimits: this.accountFacilityLimits.map((row) => ({ ...row })), + depositLimitKyc: { + ...this.depositLimitKyc + } + }; + } +} diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.html b/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.html index ec338e603..8710a144e 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.html +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-preview-step/saving-product-preview-step.component.html @@ -1,4 +1,4 @@ -
-
- -

{{ 'labels.inputs.Opening Balance' | translate }}

+ + Balance & Minimum Balance
-
- - {{ 'labels.inputs.Minimum Opening Balance' | translate }} - - @if (savingProductSettingsForm.get('minRequiredOpeningBalance').hasError('min')) { - - {{ 'labels.inputs.Minimum Opening Balance' | translate }} {{ 'labels.commons.must be' | translate }} - {{ 'labels.commons.a positive number' | translate }} - - } - +
+
+
+ + Minimum Opening Balance + + + +
+ + Enforce Minimum Balance + +
+ + + Minimum Balance (₹) + + Penal charge applies on breach + +
+ +
+ + Balance Frequency + + @for (balanceFrequency of periodFrequencyData; track balanceFrequency.id) { + {{ balanceFrequency.value }} + } + + + +
+ + Apply Withdrawal Fee for Transfers + +
+
+
-
- -

{{ 'labels.inputs.Lock-in Period' | translate }}

+ + Lock-in Period
-
-
- - {{ 'labels.inputs.Enable Lock-in Period' | translate }} - +
+
+ Enable Lock-in Period +
@if (savingProductSettingsForm.value.enableLockinPeriod) { - - {{ 'labels.inputs.Frequency' | translate }} - - @if (savingProductSettingsForm.get('lockinPeriodFrequency')?.hasError('required')) { - - {{ 'labels.inputs.Frequency' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - @if (savingProductSettingsForm.get('lockinPeriodFrequency')?.hasError('min')) { - {{ 'tooltips.Frequency must be greater than zero' | translate }} - } - @if (savingProductSettingsForm.get('lockinPeriodFrequency')?.hasError('pattern')) { - {{ 'tooltips.Frequency must be a positive integer' | translate }} - } - +
+ + Frequency* + + - - {{ 'labels.inputs.Type' | translate }} - - @for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) { - - {{ lockinPeriodFrequencyType.value | translateKey: 'catalogs' }} - - } - - @if (savingProductSettingsForm.get('lockinPeriodFrequencyType')?.hasError('required')) { - - {{ 'labels.inputs.Type' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - } - + + Type* + + @for (lockinPeriodFrequencyType of lockinPeriodFrequencyTypeData; track lockinPeriodFrequencyType) { + + {{ lockinPeriodFrequencyType.value | translateKey: 'catalogs' }} + + } + + +
}
-
- -

{{ 'labels.inputs.Balance' | translate }} & {{ 'labels.inputs.Fees' | translate }}

+ + Additional Rules
-
-
- - {{ 'labels.inputs.Apply Withdrawal Fee for Transfers' | translate }} - +
+
+
+ + Account Closed with Zero balance in Day Closing + +
+ + + Balance Required for Interest Calculation + +
- - - {{ 'labels.inputs.Balance Required for Interest Calculation' | translate }} - - @if (savingProductSettingsForm.get('minBalanceForInterestCalculation').hasError('min')) { - - {{ 'labels.inputs.Balance Required for Interest Calculation' | translate }} - {{ 'labels.commons.must be' | translate }} - {{ 'labels.commons.a positive number' | translate }} - - } - - -
- - {{ 'labels.inputs.Enforce Minimum Balance' | translate }} - -
- - - {{ 'labels.inputs.Minimum Balance' | translate }} - - @if (savingProductSettingsForm.get('minRequiredBalance').hasError('min')) { - - {{ 'labels.inputs.Minimum Balance' | translate }} {{ 'labels.commons.must be' | translate }} - {{ 'labels.commons.a positive number' | translate }} - - } - -
-
- - -
-
- -

{{ 'labels.inputs.Tax' | translate }}

-
-
-
- - {{ 'labels.inputs.Is Withhold Tax Applicable' | translate }} - -
- - @if (savingProductSettingsForm.value.withHoldTax) { - - {{ 'labels.inputs.Tax Group' | translate }} - - @for (taxGroup of taxGroupData; track taxGroup) { - - {{ taxGroup.name }} - - } - - - {{ 'labels.inputs.Tax Group' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - } -
-
- - -
-
- -

{{ 'labels.inputs.Overdraft' | translate }}

-
-
-
- - {{ 'labels.inputs.Is Overdraft Allowed' | translate }} - -
- - @if (savingProductSettingsForm.value.allowOverdraft) { - - {{ 'labels.inputs.Minimum Overdraft Required for Interest Calculation' | translate }} - - - - - {{ 'labels.inputs.Nominal Annual Interest for Overdraft' | translate }} - - - - - {{ 'labels.inputs.Maximum Overdraft Amount Limit' | translate }} - - - } -
-
- - -
-
- -

{{ 'labels.inputs.Dormancy Tracking' | translate }}

-
-
-
- - {{ 'labels.inputs.Enable Dormancy Tracking' | translate }} - -
- - @if (savingProductSettingsForm.value.isDormancyTrackingActive) { - - {{ 'labels.inputs.Number of Days to Inactive sub-status' | translate }} - - - {{ 'labels.inputs.Number of Days to Inactive sub-status' | translate }} - {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.Number of Days to Dormant sub-status' | translate }} - - - {{ 'labels.inputs.Number of Days to Dormant sub-status' | translate }} - {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.Number of Days to Escheat' | translate }} - - - {{ 'labels.inputs.Number of Days to Escheat' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - }
diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component.scss b/src/app/products/saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component.scss index d41d873e1..191facb3f 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component.scss +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component.scss @@ -44,15 +44,20 @@ $gray-900: #202124; } } -.sps-grid { - display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 4px 16px; - padding: 16px 24px; +.sps-section__badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: #eff6ff; + font-size: 14px; +} - @media (width <= 600px) { - grid-template-columns: 1fr; - } +.sps-body { + background: #fff; + padding: 18px 24px 24px; } .sps-field { @@ -61,10 +66,112 @@ $gray-900: #202124; &--full { grid-column: 1 / -1; } + + &--compact { + width: 100%; + } } -.sps-checkbox-row { +.sps-layout { + display: grid; + gap: 16px; +} + +.sps-layout--balance { + gap: 22px; +} + +.sps-layout--lockin { + grid-template-columns: repeat(2, minmax(0, 1fr)); + + @media (width <= 640px) { + grid-template-columns: 1fr; + } +} + +.sps-layout--additional { + grid-template-columns: repeat(2, minmax(0, 1fr)); + + @media (width <= 1100px) { + grid-template-columns: 1fr; + } +} + +.sps-row { + display: grid; + gap: 18px; + align-items: start; +} + +.sps-row--three { + grid-template-columns: minmax(0, 1fr) auto minmax(0, 1fr); + align-items: center; + + .sps-field--compact:first-child { + min-width: 0; + } + + .sps-toggle--inline { + align-self: center; + min-height: 56px; + } +} + +.sps-row--two { + grid-template-columns: minmax(0, 1fr) auto; + align-items: center; + + .sps-field--compact { + min-width: 0; + } +} + +.sps-toggle { display: flex; align-items: center; - padding: 4px 0; + min-height: 56px; + + mat-checkbox { + font-size: 14px; + color: $gray-900; + } + + &--full { + min-height: auto; + margin-bottom: 8px; + } + + &--inline { + padding-top: 0; + } + + &--right { + justify-content: flex-start; + } +} + +.sps-lockin-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 16px; + margin-bottom: 16px; + padding-bottom: 16px; + border-bottom: 1px solid $gray-200; + font-size: 14px; + color: $gray-900; +} + +.sps-lockin-row mat-checkbox { + margin-right: 2px; +} + +.sps-lockin-fields { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 16px; + + @media (width <= 640px) { + grid-template-columns: 1fr; + } } diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component.ts b/src/app/products/saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component.ts index cc01b4769..f62a8327f 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component.ts +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-settings-step/saving-product-settings-step.component.ts @@ -1,4 +1,4 @@ -/** +/** * Copyright since 2025 Mifos Initiative * * This Source Code Form is subject to the terms of the Mozilla Public @@ -7,16 +7,8 @@ */ import { Component, OnInit, Input, inject } from '@angular/core'; -import { - UntypedFormGroup, - UntypedFormBuilder, - UntypedFormControl, - Validators, - ReactiveFormsModule -} from '@angular/forms'; -import { MatTooltip } from '@angular/material/tooltip'; +import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; import { MatCheckbox } from '@angular/material/checkbox'; -import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ @@ -25,9 +17,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; styleUrls: ['./saving-product-settings-step.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, - MatTooltip, - MatCheckbox, - FaIconComponent + MatCheckbox ] }) export class SavingProductSettingsStepComponent implements OnInit { @@ -37,8 +27,26 @@ export class SavingProductSettingsStepComponent implements OnInit { savingProductSettingsForm: UntypedFormGroup; - lockinPeriodFrequencyTypeData: any; - taxGroupData: any; + periodFrequencyData: any[] = []; + + lockinPeriodFrequencyTypeData = [ + { + id: 'DAYS', + value: 'value.days' + }, + { + id: 'WEEKS', + value: 'value.weeks' + }, + { + id: 'MONTHS', + value: 'value.months' + }, + { + id: 'YEARS', + value: 'value.years' + } + ]; constructor() { this.createSavingProductSettingsForm(); @@ -46,37 +54,43 @@ export class SavingProductSettingsStepComponent implements OnInit { } ngOnInit() { - this.lockinPeriodFrequencyTypeData = this.savingProductsTemplate.lockinPeriodFrequencyTypeOptions; - this.taxGroupData = this.savingProductsTemplate.taxGroupOptions; - - const hasLockinPeriod = - this.savingProductsTemplate.lockinPeriodFrequency && this.savingProductsTemplate.lockinPeriodFrequency > 0; + this.periodFrequencyData = this.savingProductsTemplate.periodFrequencyOptions || []; + const defaultPeriod = this.periodFrequencyData[0]?.value || 'Monthly'; + const isEditing = !!this.savingProductsTemplate.id; this.savingProductSettingsForm.patchValue({ minRequiredOpeningBalance: this.savingProductsTemplate.minRequiredOpeningBalance, - enableLockinPeriod: hasLockinPeriod, withdrawalFeeForTransfers: this.savingProductsTemplate.withdrawalFeeForTransfers, minBalanceForInterestCalculation: this.savingProductsTemplate.minBalanceForInterestCalculation, enforceMinRequiredBalance: this.savingProductsTemplate.enforceMinRequiredBalance, minRequiredBalance: this.savingProductsTemplate.minRequiredBalance, - allowOverdraft: this.savingProductsTemplate.allowOverdraft, - minOverdraftForInterestCalculation: this.savingProductsTemplate.minOverdraftForInterestCalculation, - nominalAnnualInterestRateOverdraft: this.savingProductsTemplate.nominalAnnualInterestRateOverdraft, - overdraftLimit: this.savingProductsTemplate.overdraftLimit, - withHoldTax: this.savingProductsTemplate.withHoldTax, - taxGroupId: this.savingProductsTemplate.taxGroup && this.savingProductsTemplate.taxGroup.id, - isDormancyTrackingActive: this.savingProductsTemplate.isDormancyTrackingActive, - daysToInactive: this.savingProductsTemplate.daysToInactive, - daysToDormancy: this.savingProductsTemplate.daysToDormancy, - daysToEscheat: this.savingProductsTemplate.daysToEscheat + balanceFrequency: isEditing ? this.savingProductsTemplate.balanceFrequency || defaultPeriod : defaultPeriod, + enableLockinPeriod: isEditing + ? this.savingProductsTemplate.lockinPeriodFrequency && this.savingProductsTemplate.lockinPeriodFrequency > 0 + : this.savingProductsTemplate.lockinPeriodFrequency && this.savingProductsTemplate.lockinPeriodFrequency > 0, + accountClosedZeroBalanceInDayClosing: isEditing + ? (this.savingProductsTemplate.accountClosedZeroBalanceInDayClosing ?? false) + : false, + isDormancyTrackingActive: isEditing ? (this.savingProductsTemplate.isDormancyTrackingActive ?? true) : true, + daysToInactive: isEditing ? this.savingProductsTemplate.daysToInactive || 30 : 30, + daysToDormancy: isEditing ? this.savingProductsTemplate.daysToDormancy || 60 : 60, + daysToEscheat: isEditing ? this.savingProductsTemplate.daysToEscheat || 90 : 90 }); - if (hasLockinPeriod) { + if ( + isEditing && + this.savingProductsTemplate.lockinPeriodFrequency && + this.savingProductsTemplate.lockinPeriodFrequency > 0 + ) { + // Backend returns lockinPeriodFrequencyType.id as integer (0=DAYS,1=WEEKS,2=MONTHS,3=YEARS) + // but the form dropdown uses string IDs, so reverse-map the number back to string + const reverseFrequencyTypeMap: { [key: number]: string } = { 0: 'DAYS', 1: 'WEEKS', 2: 'MONTHS', 3: 'YEARS' }; + const freqTypeId = this.savingProductsTemplate.lockinPeriodFrequencyType?.id; + const freqTypeString = reverseFrequencyTypeMap[freqTypeId] ?? ''; + this.savingProductSettingsForm.patchValue({ lockinPeriodFrequency: this.savingProductsTemplate.lockinPeriodFrequency, - lockinPeriodFrequencyType: - this.savingProductsTemplate.lockinPeriodFrequencyType && - this.savingProductsTemplate.lockinPeriodFrequencyType.id + lockinPeriodFrequencyType: freqTypeString }); } } @@ -87,7 +101,6 @@ export class SavingProductSettingsStepComponent implements OnInit { '', [Validators.min(0)] ], - enableLockinPeriod: [false], withdrawalFeeForTransfers: [false], minBalanceForInterestCalculation: [ '', @@ -98,95 +111,42 @@ export class SavingProductSettingsStepComponent implements OnInit { '', [Validators.min(0)] ], - allowOverdraft: [false], - withHoldTax: [false], - isDormancyTrackingActive: [false] + balanceFrequency: ['Monthly'], + enableLockinPeriod: [false], + lockinPeriodFrequency: [''], + lockinPeriodFrequencyType: [''], + accountClosedZeroBalanceInDayClosing: [false], + // Dormancy tracking fields (required when isDormancyTrackingActive is true) + isDormancyTrackingActive: [true], + daysToInactive: [30], + daysToDormancy: [60], + daysToEscheat: [90] }); } setConditionalControls() { this.savingProductSettingsForm.get('enableLockinPeriod').valueChanges.subscribe((enableLockinPeriod: any) => { if (enableLockinPeriod) { - this.savingProductSettingsForm.addControl( - 'lockinPeriodFrequency', - new UntypedFormControl('', [ - Validators.required, - Validators.min(1), - Validators.pattern('^[1-9]\\d*$') - ]) - ); - this.savingProductSettingsForm.addControl( - 'lockinPeriodFrequencyType', - new UntypedFormControl('', Validators.required) - ); + this.savingProductSettingsForm.get('lockinPeriodFrequency').setValidators([ + Validators.required, + Validators.min(1), + Validators.pattern('^[1-9]\\d*$') + ]); + this.savingProductSettingsForm.get('lockinPeriodFrequencyType').setValidators([ + Validators.required + ]); } else { - this.savingProductSettingsForm.removeControl('lockinPeriodFrequency'); - this.savingProductSettingsForm.removeControl('lockinPeriodFrequencyType'); + this.savingProductSettingsForm.get('lockinPeriodFrequency').setValue(''); + this.savingProductSettingsForm.get('lockinPeriodFrequencyType').setValue(''); + this.savingProductSettingsForm.get('lockinPeriodFrequency').clearValidators(); + this.savingProductSettingsForm.get('lockinPeriodFrequencyType').clearValidators(); } + this.savingProductSettingsForm.get('lockinPeriodFrequency').updateValueAndValidity(); + this.savingProductSettingsForm.get('lockinPeriodFrequencyType').updateValueAndValidity(); }); - - this.savingProductSettingsForm.get('allowOverdraft').valueChanges.subscribe((allowOverdraft: any) => { - if (allowOverdraft) { - this.savingProductSettingsForm.addControl( - 'minOverdraftForInterestCalculation', - new UntypedFormControl('', [Validators.min(0)]) - ); - this.savingProductSettingsForm.addControl( - 'nominalAnnualInterestRateOverdraft', - new UntypedFormControl('', [Validators.min(0)]) - ); - this.savingProductSettingsForm.addControl('overdraftLimit', new UntypedFormControl('', [Validators.min(0)])); - } else { - this.savingProductSettingsForm.removeControl('minOverdraftForInterestCalculation'); - this.savingProductSettingsForm.removeControl('nominalAnnualInterestRateOverdraft'); - this.savingProductSettingsForm.removeControl('overdraftLimit'); - } - }); - - this.savingProductSettingsForm.get('withHoldTax').valueChanges.subscribe((withHoldTax: any) => { - if (withHoldTax) { - this.savingProductSettingsForm.addControl('taxGroupId', new UntypedFormControl('', Validators.required)); - } else { - this.savingProductSettingsForm.removeControl('taxGroupId'); - } - }); - - this.savingProductSettingsForm - .get('isDormancyTrackingActive') - .valueChanges.subscribe((isDormancyTrackingActive: any) => { - if (isDormancyTrackingActive) { - this.savingProductSettingsForm.addControl( - 'daysToInactive', - new UntypedFormControl('', [ - Validators.required, - Validators.min(0) - ]) - ); - this.savingProductSettingsForm.addControl( - 'daysToDormancy', - new UntypedFormControl('', [ - Validators.required, - Validators.min(0) - ]) - ); - this.savingProductSettingsForm.addControl( - 'daysToEscheat', - new UntypedFormControl('', [ - Validators.required, - Validators.min(0) - ]) - ); - } else { - this.savingProductSettingsForm.removeControl('daysToInactive'); - this.savingProductSettingsForm.removeControl('daysToDormancy'); - this.savingProductSettingsForm.removeControl('daysToEscheat'); - } - }); } get savingProductSettings() { - const formValue = { ...this.savingProductSettingsForm.value }; - delete formValue.enableLockinPeriod; - return formValue; + return { ...this.savingProductSettingsForm.value }; } } diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.html b/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.html index 311ad5d01..d949f7ab4 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.html +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.html @@ -9,120 +9,145 @@
- -

{{ 'labels.inputs.Interest Rate' | translate }}

+ + Interest Rate & Posting Terms
-
- - {{ 'labels.inputs.Nominal Annual Interest' | translate }} - - @if (savingProductTermsForm.get('nominalAnnualInterestRate').hasError('required')) { +
+
+ ℹ️ + + Savings Account base rate: + {{ savingProductTermsForm.get('nominalAnnualInterestRate').value }}% + +
+ +
+ + {{ 'labels.inputs.Nominal Annual Interest' | translate }} + + @if (savingProductTermsForm.get('nominalAnnualInterestRate').hasError('required')) { + + {{ 'labels.inputs.Nominal Annual Interest' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + + } + @if (savingProductTermsForm.get('nominalAnnualInterestRate').hasError('min')) { + {{ 'labels.inputs.Nominal Annual Interest' | translate }} must be zero or greater + } + + + + Effective Date + + + + + + + {{ 'labels.inputs.Interest Calculation Type' | translate }} + + @for (opt of interestCalculationTypeOptionData; track opt.id) { + {{ opt.value }} + } + + +
+ +
+ + Interest Calculate + + @for (mode of interestCalculationTypeData; track mode.id) { + {{ mode.value | translateKey: 'catalogs' }} + } + - {{ 'labels.inputs.Nominal Annual Interest' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.inputs.Interest Calculation Type' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} + + + @if (savingProductTermsForm.value.interestCalculationType === 2) { + + Average Period + + @for (period of periodFrequencyData; track period.id) { + {{ period.value }} + } + + } - @if (savingProductTermsForm.get('nominalAnnualInterestRate').hasError('min')) { + + + {{ 'labels.inputs.Interest Posting Period' | translate }} + + @for (interestPostingPeriodType of interestPostingPeriodTypeData; track interestPostingPeriodType) { + + {{ interestPostingPeriodType.value | translateKey: 'catalogs' }} + + } + - {{ 'labels.inputs.Nominal Annual Interest' | translate }} must be zero or greater + {{ 'labels.inputs.Interest Posting Period' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} + +
+ + -
- -
-
- -

{{ 'labels.inputs.Interest Compounding' | translate }}

-
-
- - {{ 'labels.inputs.Interest Compounding Period' | translate }} - - @for (interestCompoundingPeriodType of interestCompoundingPeriodTypeData; track interestCompoundingPeriodType) { - - {{ interestCompoundingPeriodType.value | translateKey: 'catalogs' }} - - } - - - {{ 'labels.inputs.Interest Compounding Period' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.Interest Posting Period' | translate }} - - @for (interestPostingPeriodType of interestPostingPeriodTypeData; track interestPostingPeriodType) { - - {{ interestPostingPeriodType.value | translateKey: 'catalogs' }} - - } - - - {{ 'labels.inputs.Interest Posting Period' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.Interest Calculated using' | translate }} - - @for (interestCalculationType of interestCalculationTypeData; track interestCalculationType) { - - {{ interestCalculationType.value | translateKey: 'catalogs' }} - - } - - - {{ 'labels.inputs.Interest Calculated using' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - - - - {{ 'labels.inputs.Days in Year' | translate }} - - @for ( - interestCalculationDaysInYearType of interestCalculationDaysInYearTypeData; - track interestCalculationDaysInYearType - ) { - - {{ interestCalculationDaysInYearType.value | translateKey: 'catalogs' }} - - } - - - {{ 'labels.inputs.Days in Year' | translate }} {{ 'labels.commons.is' | translate }} - {{ 'labels.commons.required' | translate }} - - +
diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.scss b/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.scss index 9f31e6a64..33445c26e 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.scss +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.scss @@ -44,6 +44,21 @@ $gray-900: #202124; } } +.spt-section__badge { + display: inline-flex; + align-items: center; + justify-content: center; + width: 24px; + height: 24px; + border-radius: 6px; + background: #eff6ff; + font-size: 14px; +} + +.spt-body { + background: #fff; +} + .spt-grid { display: grid; grid-template-columns: repeat(2, 1fr); @@ -55,6 +70,10 @@ $gray-900: #202124; } } +.spt-grid--three { + grid-template-columns: repeat(3, 1fr); +} + .spt-field { width: 100%; @@ -62,3 +81,67 @@ $gray-900: #202124; grid-column: 1 / -1; } } + +.spt-banner { + display: flex; + align-items: center; + gap: 10px; + margin: 16px 24px 0; + padding: 12px 14px; + background: #eff6ff; + border: 1px solid #dbeafe; + border-radius: 8px; + color: #1d4ed8; + font-size: 13px; + + &__icon { + font-size: 14px; + } +} + +.spt-footer { + padding: 0 24px 24px; + border-top: 1px solid #f1f5f9; +} + +.spt-footer__grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 16px; + padding-top: 18px; + + @media (width <= 900px) { + grid-template-columns: 1fr; + } +} + +.spt-toggle-card { + padding: 12px 0; + + h4 { + margin: 0 0 8px; + font-size: 14px; + font-weight: 600; + color: $gray-900; + } + + &__choices { + display: flex; + align-items: center; + gap: 16px; + flex-wrap: wrap; + } +} + +.spt-note { + display: flex; + align-items: center; + gap: 10px; + margin-top: 18px; + padding: 12px 14px; + border: 1px solid #fde68a; + background: #fffbeb; + color: #b45309; + border-radius: 8px; + font-size: 12px; +} diff --git a/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.ts b/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.ts index 15a488aa2..cd8ff964c 100644 --- a/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.ts +++ b/src/app/products/saving-products/saving-product-stepper/saving-product-terms-step/saving-product-terms-step.component.ts @@ -10,6 +10,8 @@ import { Component, OnInit, Input, inject } from '@angular/core'; import { UntypedFormGroup, UntypedFormBuilder, Validators, ReactiveFormsModule } from '@angular/forms'; import { MatTooltip } from '@angular/material/tooltip'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; +import { Dates } from 'app/core/utils/dates'; +import { SettingsService } from 'app/settings/settings.service'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; @Component({ @@ -24,6 +26,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; }) export class SavingProductTermsStepComponent implements OnInit { private formBuilder = inject(UntypedFormBuilder); + private dateUtils = inject(Dates); + private settingsService = inject(SettingsService); @Input() savingProductsTemplate: any; @@ -32,8 +36,22 @@ export class SavingProductTermsStepComponent implements OnInit { interestCompoundingPeriodTypeData: any; interestPostingPeriodTypeData: any; interestCalculationTypeData: any; + interestCalculationTypeOptionData: any; interestCalculationDaysInYearTypeData: any; - + periodFrequencyData: any[] = []; + yesNoOptions: Array<{ + value: boolean; + label: string; + }> = [ + { + value: true, + label: 'Yes' + }, + { + value: false, + label: 'No' + } + ]; constructor() { this.createSavingProductTermsForm(); } @@ -41,18 +59,78 @@ export class SavingProductTermsStepComponent implements OnInit { ngOnInit() { this.interestCompoundingPeriodTypeData = this.savingProductsTemplate.interestCompoundingPeriodTypeOptions; this.interestPostingPeriodTypeData = this.savingProductsTemplate.interestPostingPeriodTypeOptions; + // interestCalculationTypeData = standard enum (1=Daily, 2=Average) → "Interest Calculate" dropdown → interestCalculationType form control this.interestCalculationTypeData = this.savingProductsTemplate.interestCalculationTypeOptions; + // interestCalculationTypeOptionData = CBS code values from DB → "Interest Calculation Type" dropdown → interestCalculateSelector form control + this.interestCalculationTypeOptionData = this.savingProductsTemplate.interestCalculationTypeOption || []; this.interestCalculationDaysInYearTypeData = this.savingProductsTemplate.interestCalculationDaysInYearTypeOptions; + this.periodFrequencyData = this.savingProductsTemplate.periodFrequencyOptions || []; + this.setYesNoOptions(this.savingProductsTemplate?.yesNoOptions); + + const isEditing = !!this.savingProductsTemplate.id; + const defaultPeriod = this.periodFrequencyData[0]?.value || 'Monthly'; + // interestCalculationType = standard enum id (1=Daily Balance, 2=Average Daily Balance) + const savedEnumTypeId = + this.savingProductsTemplate.interestCalculationType?.id || this.interestCalculationTypeData?.[0]?.id || 1; + // interestCalculateSelector = CBS code value id from interestCalculationTypeOption + const savedSelectorId = isEditing + ? (this.savingProductsTemplate.interestCalculateSelector ?? + this.interestCalculationTypeOptionData?.[0]?.id ?? + null) + : (this.interestCalculationTypeOptionData?.[0]?.id ?? null); this.savingProductTermsForm.patchValue({ nominalAnnualInterestRate: this.savingProductsTemplate.nominalAnnualInterestRate, - interestCompoundingPeriodType: this.savingProductsTemplate.interestCompoundingPeriodType.id, - interestPostingPeriodType: this.savingProductsTemplate.interestPostingPeriodType.id, - interestCalculationType: this.savingProductsTemplate.interestCalculationType.id, - interestCalculationDaysInYearType: this.savingProductsTemplate.interestCalculationDaysInYearType.id + interestCompoundingPeriodType: this.savingProductsTemplate.interestCompoundingPeriodType?.id, + interestPostingPeriodType: this.savingProductsTemplate.interestPostingPeriodType?.id, + interestCalculationType: savedEnumTypeId, + interestCalculationDaysInYearType: this.savingProductsTemplate.interestCalculationDaysInYearType?.id, + effectiveDate: this.savingProductsTemplate.effectiveDate + ? new Date(this.savingProductsTemplate.effectiveDate) + : '', + interestCalculateSelector: savedSelectorId, + averagePeriod: isEditing ? this.savingProductsTemplate.averagePeriod || defaultPeriod : defaultPeriod, + autoPost: isEditing ? (this.savingProductsTemplate.autoPost ?? true) : true, + frequencyFixDay: isEditing ? (this.savingProductsTemplate.frequencyFixDay ?? 30) : 30, + lastInterestDate: this.savingProductsTemplate.lastInterestDate + ? new Date(this.savingProductsTemplate.lastInterestDate) + : '' }); } + private setYesNoOptions(options: any[]) { + if (!Array.isArray(options)) { + return; + } + + const mappedOptions = options + .map((option: any) => { + const rawValue = option?.name ?? option?.value ?? option; + const normalized = typeof rawValue === 'string' ? rawValue.trim().toUpperCase() : rawValue; + + if (normalized === 'YES') { + return { + value: true, + label: 'Yes' + }; + } + + if (normalized === 'NO') { + return { + value: false, + label: 'No' + }; + } + + return null; + }) + .filter((value: any) => !!value); + + if (mappedOptions.length) { + this.yesNoOptions = mappedOptions; + } + } + createSavingProductTermsForm() { this.savingProductTermsForm = this.formBuilder.group({ nominalAnnualInterestRate: [ @@ -77,11 +155,35 @@ export class SavingProductTermsStepComponent implements OnInit { interestCalculationDaysInYearType: [ '', Validators.required - ] + ], + effectiveDate: [''], + interestCalculateSelector: [1], + averagePeriod: ['Monthly'], + autoPost: [true], + frequencyFixDay: [ + 30, + [Validators.min(0)] + ], + lastInterestDate: [''] }); } get savingProductTerms() { - return this.savingProductTermsForm.value; + const formValue = { ...this.savingProductTermsForm.value }; + const dateFormat = this.settingsService.dateFormat; + const prevEffectiveDate: Date = this.savingProductTermsForm.value.effectiveDate; + const prevLastInterestDate: Date = this.savingProductTermsForm.value.lastInterestDate; + + if (formValue.effectiveDate instanceof Date) { + formValue.effectiveDate = this.dateUtils.formatDate(prevEffectiveDate, dateFormat) || ''; + } + + if (formValue.lastInterestDate instanceof Date) { + formValue.lastInterestDate = this.dateUtils.formatDate(prevLastInterestDate, dateFormat) || ''; + } + + return { + ...formValue + }; } } diff --git a/src/app/products/saving-products/saving-product.resolver.ts b/src/app/products/saving-products/saving-product.resolver.ts index cf6f858dc..1a3926298 100644 --- a/src/app/products/saving-products/saving-product.resolver.ts +++ b/src/app/products/saving-products/saving-product.resolver.ts @@ -24,7 +24,7 @@ export class SavingProductResolver { private productsService = inject(ProductsService); /** - * Returns the saving product data. + * Returns the saving product data with template options for dropdown lookups. * @returns {Observable} */ resolve(route: ActivatedRouteSnapshot): Observable { diff --git a/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.html b/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.html index 211eb5268..a36665ab5 100644 --- a/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.html +++ b/src/app/products/saving-products/view-saving-product/saving-product-general-tab/saving-product-general-tab.component.html @@ -1,4 +1,4 @@ - @@ -327,7 +345,11 @@
Priority - + {{ opSelectedWorkPackage.priority }}
@@ -354,7 +376,10 @@
Progress
- + {{ opSelectedWorkPackage.percentageDone }}%
diff --git a/src/app/rbi-circulars/rbi-circulars.component.scss b/src/app/rbi-circulars/rbi-circulars.component.scss index c38517fb5..2086f9871 100644 --- a/src/app/rbi-circulars/rbi-circulars.component.scss +++ b/src/app/rbi-circulars/rbi-circulars.component.scss @@ -423,7 +423,9 @@ border-radius: 10px; padding: 16px; cursor: pointer; - transition: box-shadow 0.2s, border-color 0.2s; + transition: + box-shadow 0.2s, + border-color 0.2s; &:hover { box-shadow: 0 4px 12px rgb(0 0 0 / 10%); diff --git a/src/app/rbi-circulars/rbi-circulars.component.ts b/src/app/rbi-circulars/rbi-circulars.component.ts index dec51c67e..10981e107 100644 --- a/src/app/rbi-circulars/rbi-circulars.component.ts +++ b/src/app/rbi-circulars/rbi-circulars.component.ts @@ -280,7 +280,10 @@ export class RbiCircularsComponent implements OnInit, AfterViewInit { } const q = query.toLowerCase(); this.opFilteredWorkPackages = this.opWorkPackages.filter( - (wp) => wp.subject?.toLowerCase().includes(q) || wp.status?.toLowerCase().includes(q) || wp.type?.toLowerCase().includes(q) + (wp) => + wp.subject?.toLowerCase().includes(q) || + wp.status?.toLowerCase().includes(q) || + wp.type?.toLowerCase().includes(q) ); } @@ -303,7 +306,9 @@ export class RbiCircularsComponent implements OnInit, AfterViewInit { formatOpDate(dateStr: string): string { if (!dateStr) return '—'; const d = new Date(dateStr); - return isNaN(d.getTime()) ? '—' : d.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' }); + return isNaN(d.getTime()) + ? '—' + : d.toLocaleDateString('en-IN', { day: '2-digit', month: 'short', year: 'numeric' }); } private buildTabs() { diff --git a/src/app/reports/reports.component.html b/src/app/reports/reports.component.html index f63edec37..5025998dc 100644 --- a/src/app/reports/reports.component.html +++ b/src/app/reports/reports.component.html @@ -50,7 +50,11 @@
- @@ -65,7 +69,11 @@
- +
@@ -124,9 +132,13 @@
diff --git a/src/app/reports/reports.component.scss b/src/app/reports/reports.component.scss index d88df72ce..686fd2d24 100644 --- a/src/app/reports/reports.component.scss +++ b/src/app/reports/reports.component.scss @@ -60,10 +60,21 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; span { border-radius: 2px; - &:nth-child(1) { background: $brand; } - &:nth-child(2) { background: $accent; } - &:nth-child(3) { background: #10b981; } - &:nth-child(4) { background: #cbd5e1; } + &:nth-child(1) { + background: $brand; + } + + &:nth-child(2) { + background: $accent; + } + + &:nth-child(3) { + background: #10b981; + } + + &:nth-child(4) { + background: #cbd5e1; + } } } @@ -83,21 +94,33 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; } ::ng-deep { - .mat-mdc-form-field-flex { height: 38px; } + .mat-mdc-form-field-flex { + height: 38px; + } .mat-mdc-form-field-infix { padding: 6px 0; min-height: 38px; } - .mat-mdc-form-field-subscript-wrapper { height: 0; min-height: 0; } - - .mdc-notched-outline { - .mdc-notched-outline__leading { border-radius: 8px 0 0 8px; } - .mdc-notched-outline__trailing { border-radius: 0 8px 8px 0; } + .mat-mdc-form-field-subscript-wrapper { + height: 0; + min-height: 0; } - input { font-size: 13px; } + .mdc-notched-outline { + .mdc-notched-outline__leading { + border-radius: 8px 0 0 8px; + } + + .mdc-notched-outline__trailing { + border-radius: 0 8px 8px 0; + } + } + + input { + font-size: 13px; + } } } @@ -107,9 +130,13 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; grid-template-columns: repeat(3, 1fr); gap: 12px; - @media (width <= 900px) { grid-template-columns: repeat(2, 1fr); } + @media (width <= 900px) { + grid-template-columns: repeat(2, 1fr); + } - @media (width <= 500px) { grid-template-columns: 1fr; } + @media (width <= 500px) { + grid-template-columns: 1fr; + } } .rpt-tile { @@ -121,7 +148,9 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; border: 1px solid rgb(0 0 0 / 6%); border-radius: 8px; cursor: pointer; - transition: box-shadow 0.15s, transform 0.15s; + transition: + box-shadow 0.15s, + transform 0.15s; &:hover { box-shadow: 0 4px 16px rgb(0 0 0 / 8%); @@ -138,12 +167,35 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; 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 { @@ -172,13 +224,17 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; transition: color 0.12s; } - &:hover &__arrow { color: $brand; } + &:hover &__arrow { + color: $brand; + } } .rpt-tiles--sub { grid-template-columns: repeat(2, 1fr); - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } // Detail view @@ -204,9 +260,14 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; border-color: $border !important; color: $text-muted !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $brand !important; border-color: $brand !important; } + &:hover { + color: $brand !important; + border-color: $brand !important; + } } // Table @@ -246,8 +307,13 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; height: 38px; border-bottom: 1px solid #f1f5f9; - &:nth-child(even) { background: #fafbfc; } - &:last-child { border-bottom: none; } + &:nth-child(even) { + background: #fafbfc; + } + + &:last-child { + border-bottom: none; + } } .mat-mdc-cell { @@ -298,8 +364,19 @@ $mono: 'Roboto Mono', 'DM Mono', monospace; border-color: rgb(255 255 255 / 6%); } - .rpt-tile__name { color: #e0e0e0; } - .rpt-tile__arrow { color: #444; } - .rpt-header__title { color: #e0e0e0; } - .rpt-detail__title { color: #e0e0e0; } + .rpt-tile__name { + color: #e0e0e0; + } + + .rpt-tile__arrow { + color: #444; + } + + .rpt-header__title { + color: #e0e0e0; + } + + .rpt-detail__title { + color: #e0e0e0; + } } diff --git a/src/app/reports/reports.component.ts b/src/app/reports/reports.component.ts index 3fe1a64a3..fe22c0815 100644 --- a/src/app/reports/reports.component.ts +++ b/src/app/reports/reports.component.ts @@ -45,10 +45,36 @@ const DEFAULT_CONFIG = { icon: 'fa-chart-bar', color: 'blue' }; /** Sub-category rules for large categories (keyword → sub-category name) */ const SUB_CATEGORY_RULES: Record = { Loan: [ - { name: 'Disbursal & Approval', keywords: ['disbursal', 'pending approval', 'awaiting'], icon: 'fa-check-circle', color: 'green' }, - { name: 'Active & Portfolio', keywords: ['active loan', 'portfolio', 'outstanding', 'balance', 'summary'], icon: 'fa-chart-line', color: 'blue' }, - { name: 'Arrears & Aging', keywords: ['arrear', 'aging', 'overdue', 'delinquen', 'written off', 'write-off', 'obligation'], icon: 'fa-exclamation-triangle', color: 'red' }, - { name: 'Repayment & Collection', keywords: ['repayment', 'collection', 'demand', 'due', 'paid', 'payment', 'transaction'], icon: 'fa-money-bill', color: 'teal' }, + { name: 'Disbursal & Approval', keywords: [ + 'disbursal', + 'pending approval', + 'awaiting' + ], icon: 'fa-check-circle', color: 'green' }, + { name: 'Active & Portfolio', keywords: [ + 'active loan', + 'portfolio', + 'outstanding', + 'balance', + 'summary' + ], icon: 'fa-chart-line', color: 'blue' }, + { name: 'Arrears & Aging', keywords: [ + 'arrear', + 'aging', + 'overdue', + 'delinquen', + 'written off', + 'write-off', + 'obligation' + ], icon: 'fa-exclamation-triangle', color: 'red' }, + { name: 'Repayment & Collection', keywords: [ + 'repayment', + 'collection', + 'demand', + 'due', + 'paid', + 'payment', + 'transaction' + ], icon: 'fa-money-bill', color: 'teal' }, { name: 'Other Loan Reports', keywords: [], icon: 'fa-file-alt', color: 'purple' } ] }; @@ -89,7 +115,11 @@ export class ReportsComponent implements OnInit { reportsData: any[] = []; filter: string; - displayedColumns: string[] = ['reportName', 'reportType', 'reportCategory']; + displayedColumns: string[] = [ + 'reportName', + 'reportType', + 'reportCategory' + ]; dataSource = new MatTableDataSource(); /** Category dashboard */ @@ -136,10 +166,15 @@ export class ReportsComponent implements OnInit { } this.categories = Array.from(catMap.entries()) .sort((a, b) => b[1] - a[1]) - .map(([name, count]) => { - const cfg = CATEGORY_CONFIG[name] || DEFAULT_CONFIG; - return { name, count, icon: cfg.icon, color: cfg.color }; - }); + .map( + ([ + name, + count + ]) => { + const cfg = CATEGORY_CONFIG[name] || DEFAULT_CONFIG; + return { name, count, icon: cfg.icon, color: cfg.color }; + } + ); } selectCategory(category: string) { @@ -173,19 +208,31 @@ export class ReportsComponent implements OnInit { const matched = this.filteredReports.filter((r: any, idx: number) => { if (assigned.has(idx)) return false; const name = (r.reportName || '').toLowerCase(); - return rule.keywords.some(kw => name.includes(kw)); + return rule.keywords.some((kw) => name.includes(kw)); }); - matched.forEach(r => assigned.add(this.filteredReports.indexOf(r))); + matched.forEach((r) => assigned.add(this.filteredReports.indexOf(r))); if (matched.length > 0) { - this.subCategories.push({ name: rule.name, count: matched.length, icon: rule.icon, color: rule.color, reports: matched }); + this.subCategories.push({ + name: rule.name, + count: matched.length, + icon: rule.icon, + color: rule.color, + reports: matched + }); } } // "Other" bucket — remaining unmatched - const otherRule = rules.find(r => r.keywords.length === 0); + const otherRule = rules.find((r) => r.keywords.length === 0); const remaining = this.filteredReports.filter((_, idx) => !assigned.has(idx)); if (remaining.length > 0 && otherRule) { - this.subCategories.push({ name: otherRule.name, count: remaining.length, icon: otherRule.icon, color: otherRule.color, reports: remaining }); + this.subCategories.push({ + name: otherRule.name, + count: remaining.length, + icon: otherRule.icon, + color: otherRule.color, + reports: remaining + }); } } @@ -219,7 +266,7 @@ export class ReportsComponent implements OnInit { this.isSearching = true; this.showSubCategories = false; if (this.selectedSubCategory) { - const subCat = this.subCategories.find(sc => sc.name === this.selectedSubCategory); + const subCat = this.subCategories.find((sc) => sc.name === this.selectedSubCategory); this.setTableData(subCat ? subCat.reports : this.filteredReports); } else if (this.selectedCategory) { this.setTableData(this.filteredReports); @@ -230,7 +277,11 @@ export class ReportsComponent implements OnInit { } else { if (this.selectedSubCategory) { // keep showing sub-cat table - } else if (this.selectedCategory && SUB_CATEGORY_RULES[this.selectedCategory] && this.filteredReports.length > 15) { + } else if ( + this.selectedCategory && + SUB_CATEGORY_RULES[this.selectedCategory] && + this.filteredReports.length > 15 + ) { this.isSearching = false; this.showSubCategories = true; } else { diff --git a/src/app/reports/sanctions-logs/sanctions-logs.component.html b/src/app/reports/sanctions-logs/sanctions-logs.component.html index 8581ef8e0..6d90a0da8 100644 --- a/src/app/reports/sanctions-logs/sanctions-logs.component.html +++ b/src/app/reports/sanctions-logs/sanctions-logs.component.html @@ -1,3 +1,11 @@ + +
diff --git a/src/app/shared/footer/footer.component.scss b/src/app/shared/footer/footer.component.scss index a48a8cb09..fb4fc8a96 100644 --- a/src/app/shared/footer/footer.component.scss +++ b/src/app/shared/footer/footer.component.scss @@ -260,8 +260,11 @@ $accent-2: #10b981; position: absolute; inset: 0; background: repeating-linear-gradient( - 108deg, transparent 0, transparent 80px, - rgb(255 255 255 / 2.2%) 80px, rgb(255 255 255 / 2.2%) 160px + 108deg, + transparent 0, + transparent 80px, + rgb(255 255 255 / 2.2%) 80px, + rgb(255 255 255 / 2.2%) 160px ); pointer-events: none; } @@ -299,10 +302,21 @@ $accent-2: #10b981; span { border-radius: 1.5px; - &:nth-child(1) { background: #fff; } - &:nth-child(2) { background: $accent; } - &:nth-child(3) { background: $accent-2; } - &:nth-child(4) { background: rgb(255 255 255 / 30%); } + &:nth-child(1) { + background: #fff; + } + + &:nth-child(2) { + background: $accent; + } + + &:nth-child(3) { + background: $accent-2; + } + + &:nth-child(4) { + background: rgb(255 255 255 / 30%); + } } } @@ -642,7 +656,9 @@ $accent-2: #10b981; text-decoration: none; padding: 2px 6px; border-radius: 3px; - transition: color 0.15s, background 0.15s; + transition: + color 0.15s, + background 0.15s; &:hover { color: #5ec9f8; @@ -727,7 +743,6 @@ $accent-2: #10b981; padding: 12px 20px; } - .footer-divider { margin: 0 20px; } @@ -758,7 +773,6 @@ $accent-2: #10b981; gap: 16px; } - .sec-item { font-size: 10px; } diff --git a/src/app/shared/introducer-search/introducer-search.component.html b/src/app/shared/introducer-search/introducer-search.component.html new file mode 100644 index 000000000..d7663ef85 --- /dev/null +++ b/src/app/shared/introducer-search/introducer-search.component.html @@ -0,0 +1,130 @@ + + +
+
+ + Introducer + Required +
+ +
+ @if (!selectedClient) { + +
+ + Search by Name or Client ID + + search + + + @if (searching) { +
+
+ + Searching... +
+
+ } + + @if (!searching && searchResults.length > 0) { +
+ @for (r of searchResults; track r.id) { +
+ person +
+ {{ r.displayName }} + + ID: {{ r.id }} + @if (r.officeName) { + · {{ r.officeName }} + } + @if (r.mobileNo) { + · {{ r.mobileNo }} + } + +
+
+ } +
+ } + + @if (!searching && searchText.length >= 2 && searchResults.length === 0) { +
No clients found
+ } +
+ } + + @if (selectedClient) { + +
+
+ @if (linkedProfileImage) { + Profile + } @else { + person + } +
+
+ {{ selectedClient.displayName }} + + Client ID: {{ selectedClient.id }} + @if (selectedClient.accountNumber) { + · A/C: {{ selectedClient.accountNumber }} + } + +
+ +
+ + @if (fetchingLinkedData) { +
+ + Fetching client details... +
+ } + + @if (!fetchingLinkedData) { +
+
+ FULL NAME + {{ selectedClient.displayName }} +
+
+ DATE OF BIRTH + {{ clientDob || '—' }} +
+ @if (clientAadhaar) { +
+ AADHAAR + + {{ clientAadhaar }} + @if (linkedKycStatus?.aadhaarVerified) { + verified Verified + } + +
+ } + @if (clientPan) { +
+ PAN + + {{ clientPan }} + @if (linkedKycStatus?.panVerified) { + verified Verified + } + +
+ } +
+ } + } +
+
diff --git a/src/app/shared/introducer-search/introducer-search.component.scss b/src/app/shared/introducer-search/introducer-search.component.scss new file mode 100644 index 000000000..4b882d3cd --- /dev/null +++ b/src/app/shared/introducer-search/introducer-search.component.scss @@ -0,0 +1,316 @@ +/** + * 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/. + */ + +$blue: #2563eb; +$blue-light: #eff6ff; +$green: #16a34a; +$green-light: #dcfce7; +$border: #e2e8f0; +$text: #1e293b; +$muted: #64748b; +$gray-bg: #f8fafc; + +:host { + overflow: visible; +} + +.intro-section { + border: 1px solid $border; + border-radius: 12px; + overflow: visible; + background: #fff; + margin-top: 16px; + position: relative; + z-index: 10; + + &__header { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 20px; + background: $gray-bg; + border-bottom: 1px solid $border; + border-radius: 12px 12px 0 0; + + i { + color: $blue; + font-size: 14px; + } + + span:nth-child(2) { + font-size: 14px; + font-weight: 600; + color: $text; + flex: 1; + } + } +} + +.intro-badge { + display: inline-flex; + align-items: center; + padding: 2px 10px; + border-radius: 999px; + font-size: 11px; + font-weight: 600; + + &--required { + background: #fef3c7; + color: #92400e; + border: 1px solid #fde68a; + } +} + +.intro-body { + padding: 16px 20px; + overflow: visible; + position: relative; + z-index: 1000; +} + +// Search +.intro-search-wrap { + position: relative; + z-index: 1000; + overflow: visible !important; +} + +.intro-search-field { + width: 100%; +} + +.intro-results-inline { + margin-top: 4px; + border: 1px solid $border; + border-radius: 8px; + background: #fff; + box-shadow: 0 4px 16px rgb(0 0 0 / 8%); + overflow: hidden; + max-height: 260px; + overflow-y: auto; +} + +.intro-results { + position: absolute !important; + z-index: 99999 !important; + top: calc(100% + 4px); + left: 0; + right: 0; + background: #fff; + border: 1px solid $border; + border-radius: 8px; + box-shadow: 0 4px 16px rgb(0 0 0 / 10%); + max-height: 260px; + overflow: hidden auto; + + &__loading { + display: flex; + align-items: center; + gap: 10px; + padding: 14px 16px; + font-size: 13px; + color: $muted; + } +} + +.intro-result-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + cursor: pointer; + transition: background 0.15s; + border-bottom: 1px solid $border; + min-width: 0; + + &:last-child { + border-bottom: none; + } + + &:hover { + background: $blue-light; + } + + &__icon { + color: $muted; + font-size: 20px; + width: 20px; + height: 20px; + flex: 0 0 auto; + } + + &__info { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + flex: 1; + } + + &__name { + font-size: 13px; + font-weight: 600; + color: $text; + white-space: normal; + word-break: break-word; + overflow-wrap: break-word; + } + + &__meta { + font-size: 11px; + color: $muted; + white-space: normal; + word-break: break-word; + overflow-wrap: break-word; + } +} + +.intro-no-results { + padding: 10px 0; + font-size: 13px; + color: $muted; + text-align: center; +} + +// Selected client card +.intro-card { + display: flex; + align-items: center; + gap: 14px; + padding: 14px 16px; + border: 1px solid $border; + border-radius: 10px; + background: $gray-bg; + margin-bottom: 14px; + min-width: 0; + + &__avatar { + width: 44px; + height: 44px; + border-radius: 50%; + background: $blue-light; + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + overflow: hidden; + } + + &__photo { + width: 100%; + height: 100%; + object-fit: cover; + } + + &__avatar-icon { + color: $blue; + font-size: 24px; + width: 24px; + height: 24px; + } + + &__info { + flex: 1; + display: flex; + flex-direction: column; + gap: 3px; + min-width: 0; + } + + &__name { + font-size: 14px; + font-weight: 600; + color: $text; + white-space: normal; + word-break: break-word; + overflow-wrap: break-word; + } + + &__meta { + font-size: 12px; + color: $muted; + white-space: normal; + word-break: break-word; + overflow-wrap: break-word; + } + + &__remove { + flex: 0 0 auto; + color: $muted; + } +} + +.intro-fetching { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: $muted; + padding: 6px 0; +} + +// Details grid +.intro-details-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 14px 24px; + padding: 14px 16px; + border: 1px solid $border; + border-radius: 10px; + background: #fff; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.intro-detail { + display: flex; + flex-direction: column; + gap: 4px; + min-width: 0; + + &__label { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.06em; + color: $muted; + text-transform: uppercase; + } + + &__value { + font-size: 13px; + font-weight: 600; + color: $text; + display: flex; + align-items: center; + gap: 6px; + white-space: normal; + word-break: break-word; + overflow-wrap: break-word; + flex-wrap: wrap; + } +} + +.intro-verified { + display: inline-flex; + align-items: center; + gap: 3px; + font-size: 11px; + font-weight: 600; + color: $green; + background: $green-light; + border-radius: 999px; + padding: 2px 8px; + + mat-icon { + font-size: 12px; + width: 12px; + height: 12px; + } +} diff --git a/src/app/shared/introducer-search/introducer-search.component.ts b/src/app/shared/introducer-search/introducer-search.component.ts new file mode 100644 index 000000000..adf14cc46 --- /dev/null +++ b/src/app/shared/introducer-search/introducer-search.component.ts @@ -0,0 +1,142 @@ +/** + * 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, Output, EventEmitter, inject } from '@angular/core'; +import { DomSanitizer } from '@angular/platform-browser'; +import { ClientsService } from 'app/clients/clients.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { MatIconButton } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; +import { FormsModule } from '@angular/forms'; + +@Component({ + selector: 'mifosx-introducer-search', + templateUrl: './introducer-search.component.html', + styleUrls: ['./introducer-search.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FormsModule, + MatProgressSpinner, + MatIconButton, + MatIcon + ] +}) +export class IntroducerSearchComponent { + private clientsService = inject(ClientsService); + private sanitizer = inject(DomSanitizer); + + @Output() introducerChange = new EventEmitter<{ clientId: number; displayName: string; cifNumber?: string } | null>(); + + entryMode: 'linked' = 'linked'; + searchText = ''; + searching = false; + searchResults: any[] = []; + selectedClient: any = null; + fetchingLinkedData = false; + linkedProfileImage: any = null; + linkedKycStatus: any = null; + + // Fetched client details + clientDob = ''; + clientAadhaar = ''; + clientPan = ''; + + private searchTimer: any; + + onSearch(text: string): void { + this.searchText = text; + clearTimeout(this.searchTimer); + if (!text || text.length < 2) { + this.searchResults = []; + return; + } + this.searching = true; + this.searchTimer = setTimeout(() => { + this.clientsService.searchByText(text, 0, 8).subscribe({ + next: (data: any) => { + this.searchResults = data?.content || []; + this.searching = false; + }, + error: () => { + this.searchResults = []; + this.searching = false; + } + }); + }, 400); + } + + selectClient(client: any): void { + this.selectedClient = client; + this.searchResults = []; + this.searchText = ''; + this.clientDob = ''; + this.clientAadhaar = ''; + this.clientPan = ''; + this.linkedProfileImage = null; + this.linkedKycStatus = null; + this.fetchLinkedClientData(client.id); + this.introducerChange.emit({ + clientId: client.id, + displayName: client.displayName, + cifNumber: client.cifNumber || client.cifNo || '' + }); + } + + removeClient(): void { + this.selectedClient = null; + this.linkedProfileImage = null; + this.linkedKycStatus = null; + this.clientDob = ''; + this.clientAadhaar = ''; + this.clientPan = ''; + this.introducerChange.emit(null); + } + + private fetchLinkedClientData(clientId: string): void { + this.fetchingLinkedData = true; + + this.clientsService.getClientData(clientId).subscribe({ + next: (data: any) => { + if (data.dateOfBirth) { + const d = Array.isArray(data.dateOfBirth) + ? new Date(data.dateOfBirth[0], data.dateOfBirth[1] - 1, data.dateOfBirth[2]) + : new Date(data.dateOfBirth); + this.clientDob = d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }); + } + this.fetchingLinkedData = false; + }, + error: () => { + this.fetchingLinkedData = false; + } + }); + + this.clientsService.getClientKyc(clientId).subscribe({ + next: (kyc: any) => { + if (kyc) { + this.clientAadhaar = kyc.aadhaarNumberMasked || ''; + this.clientPan = kyc.panNumber || ''; + this.linkedKycStatus = { + panVerified: kyc.panVerified || false, + aadhaarVerified: kyc.aadhaarVerified || false + }; + } + }, + error: () => {} + }); + + this.clientsService.getClientProfileImage(clientId).subscribe({ + next: (imageData: any) => { + if (imageData) { + this.linkedProfileImage = this.sanitizer.bypassSecurityTrustResourceUrl(imageData); + } + }, + error: () => {} + }); + } +} diff --git a/src/app/shared/nominee-form/nominee-form.component.html b/src/app/shared/nominee-form/nominee-form.component.html new file mode 100644 index 000000000..49bd32d8a --- /dev/null +++ b/src/app/shared/nominee-form/nominee-form.component.html @@ -0,0 +1,282 @@ + + +
+ +
+
+ + Nomination Facility +
+
+ + +
+
+ +
+ + @if (mode === 'manual') { +
+ +
+ + Full Name * + + Full name is required + + + + Relationship * + + @for (rel of relationships; track rel) { + {{ rel }} + } + + Relationship is required + +
+ +
+ + Date of Birth + + + + + + + Nationality + + Indian + NRI + Foreign National + + +
+ + + + +
+ + Aadhaar Number + + + + + PAN Number + + +
+ +
+ + Mobile Number + + + + + Nominee Share (%) + + +
+ + + + +
+ + Guardian Name + + + + + Guardian Relationship + + @for (rel of relationships; track rel) { + {{ rel }} + } + + +
+ + +
+ Is Alive + Is Dependent +
+ + + + Remarks + + +
+ } + + + @if (mode === 'linked') { + @if (!selectedClient) { + +
+ + Search by Name or Client ID + + search + + + @if (searching) { +
+
+ + Searching... +
+
+ } + + @if (!searching && searchResults.length > 0) { +
+ @for (r of searchResults; track r.id) { +
+ person +
+ {{ r.displayName }} + + ID: {{ r.id }} + @if (r.officeName) { + · {{ r.officeName }} + } + @if (r.mobileNo) { + · {{ r.mobileNo }} + } + +
+
+ } +
+ } + + @if (!searching && searchText.length >= 2 && searchResults.length === 0) { +
No clients found
+ } +
+ } + + @if (selectedClient) { + +
+
+ @if (linkedProfileImage) { + Profile + } @else { + person + } +
+
+ {{ selectedClient.displayName }} + + Client ID: {{ selectedClient.id }} + @if (selectedClient.accountNumber) { + · A/C: {{ selectedClient.accountNumber }} + } + +
+ +
+ + @if (fetchingLinkedData) { +
+ + Fetching nominee details... +
+ } + + @if (!fetchingLinkedData) { +
+
+ Full Name + {{ selectedClient.displayName }} +
+
+ Date of Birth + {{ clientDob || '—' }} +
+ @if (clientAadhaar) { +
+ Aadhaar + + {{ clientAadhaar }} + @if (linkedKycStatus?.aadhaarVerified) { + verified Verified + } + +
+ } + @if (clientPan) { +
+ PAN + + {{ clientPan }} + @if (linkedKycStatus?.panVerified) { + verified Verified + } + +
+ } +
+ + +
+ + Relationship * + + @for (rel of relationships; track rel) { + {{ rel }} + } + + + + + Nominee Share (%) + + +
+ } + } + } +
+
diff --git a/src/app/shared/nominee-form/nominee-form.component.scss b/src/app/shared/nominee-form/nominee-form.component.scss new file mode 100644 index 000000000..edfa9a96f --- /dev/null +++ b/src/app/shared/nominee-form/nominee-form.component.scss @@ -0,0 +1,387 @@ +/** + * 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/. + */ + +@use 'sass:color'; + +$blue: #2563eb; +$blue-light: #eff6ff; +$green: #16a34a; +$green-light: #dcfce7; +$border: #e2e8f0; +$text: #1e293b; +$muted: #64748b; +$gray-bg: #f8fafc; +$purple: #7c3aed; +$purple-light: #f5f3ff; + +:host { + display: block; +} + +// ─── Wrapper ─────────────────────────────────────────────────── +.nom-section { + border: 1px solid $border; + border-radius: 14px; + background: #fff; + margin-top: 20px; + overflow: visible; + position: relative; + z-index: 10; +} + +// ─── Header ──────────────────────────────────────────────────── +.nom-header { + display: flex; + align-items: center; + justify-content: space-between; + flex-wrap: wrap; + gap: 12px; + padding: 14px 20px; + background: $gray-bg; + border-bottom: 1px solid $border; + border-radius: 14px 14px 0 0; + + &__left { + display: flex; + align-items: center; + gap: 10px; + + i { + color: $purple; + font-size: 15px; + } + } + + &__title { + font-size: 14px; + font-weight: 700; + color: $text; + } +} + +// ─── Mode toggle tabs ────────────────────────────────────────── +.nom-mode-tabs { + display: flex; + gap: 8px; +} + +.nom-tab { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 6px 14px; + border-radius: 8px; + font-size: 12px; + font-weight: 600; + cursor: pointer; + border: 1.5px solid $border; + background: #fff; + color: $muted; + transition: all 0.18s; + + i { + font-size: 11px; + } + + &:hover { + border-color: $blue; + color: $blue; + background: $blue-light; + } + + &--active { + border-color: $blue; + background: $blue; + color: #fff; + + &:hover { + background: color.adjust(#2563eb, $lightness: -5%); + } + } +} + +// ─── Body ────────────────────────────────────────────────────── +.nom-body { + padding: 20px; + overflow: visible; + position: relative; + z-index: 10; +} + +// ─── Manual form grid ────────────────────────────────────────── +.nom-form { + display: flex; + flex-direction: column; + gap: 0; +} + +.nom-row { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.nom-field { + width: 100%; + + &--full { + width: 100%; + display: block; + } +} + +.nom-section-label { + display: flex; + align-items: center; + gap: 8px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.07em; + text-transform: uppercase; + color: $blue; + margin: 8px 0 4px; + + i { + font-size: 12px; + } +} + +.nom-hint { + font-size: 10px; + font-weight: 500; + text-transform: none; + letter-spacing: 0; + color: $muted; + margin-left: 2px; +} + +.nom-flags { + display: flex; + align-items: center; + gap: 24px; + margin: 8px 0 12px; +} + +// ─── Search (link mode) ──────────────────────────────────────── +.nom-search-wrap { + position: relative; + z-index: 1000; + overflow: visible; +} + +.nom-results { + margin-top: 4px; + border: 1px solid $border; + border-radius: 8px; + background: #fff; + box-shadow: 0 4px 20px rgb(0 0 0 / 10%); + overflow: hidden; + max-height: 260px; + overflow-y: auto; + + &__loading { + display: flex; + align-items: center; + gap: 10px; + padding: 14px 16px; + font-size: 13px; + color: $muted; + } +} + +.nom-result-item { + display: flex; + align-items: center; + gap: 12px; + padding: 12px 16px; + cursor: pointer; + transition: background 0.15s; + border-bottom: 1px solid $border; + + &:last-child { + border-bottom: none; + } + + &:hover { + background: $blue-light; + } + + &__icon { + color: $muted; + font-size: 20px; + width: 20px; + height: 20px; + flex: 0 0 auto; + } + + &__info { + display: flex; + flex-direction: column; + gap: 2px; + } + + &__name { + font-size: 13px; + font-weight: 600; + color: $text; + } + + &__meta { + font-size: 11px; + color: $muted; + } +} + +.nom-no-results { + padding: 12px 0; + font-size: 13px; + color: $muted; + text-align: center; +} + +// ─── Selected client card ────────────────────────────────────── +.nom-client-card { + display: flex; + align-items: center; + gap: 14px; + padding: 14px 16px; + border: 1px solid $border; + border-radius: 10px; + background: $gray-bg; + margin-bottom: 14px; + + &__avatar { + width: 46px; + height: 46px; + border-radius: 50%; + background: $blue-light; + display: flex; + align-items: center; + justify-content: center; + flex: 0 0 auto; + overflow: hidden; + } + + &__photo { + width: 100%; + height: 100%; + object-fit: cover; + } + + &__avatar-icon { + color: $blue; + font-size: 26px; + width: 26px; + height: 26px; + } + + &__info { + flex: 1; + display: flex; + flex-direction: column; + gap: 3px; + } + + &__name { + font-size: 14px; + font-weight: 700; + color: $text; + } + + &__meta { + font-size: 12px; + color: $muted; + } + + &__remove { + flex: 0 0 auto; + color: $muted; + } +} + +.nom-fetching { + display: flex; + align-items: center; + gap: 8px; + font-size: 12px; + color: $muted; + padding: 6px 0; +} + +// ─── Linked client details grid ──────────────────────────────── +.nom-details-grid { + display: grid; + grid-template-columns: repeat(2, 1fr); + gap: 14px 24px; + padding: 14px 16px; + border: 1px solid $border; + border-radius: 10px; + background: #fff; + margin-bottom: 14px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} + +.nom-detail { + display: flex; + flex-direction: column; + gap: 4px; + + &__label { + font-size: 10px; + font-weight: 700; + letter-spacing: 0.06em; + color: $muted; + text-transform: uppercase; + } + + &__value { + font-size: 13px; + font-weight: 600; + color: $text; + display: flex; + align-items: center; + gap: 6px; + } +} + +.nom-verified { + display: inline-flex; + align-items: center; + gap: 3px; + font-size: 11px; + font-weight: 600; + color: $green; + background: $green-light; + border-radius: 999px; + padding: 2px 8px; + + mat-icon { + font-size: 12px; + width: 12px; + height: 12px; + } +} + +// ─── Extras for linked (relationship + share) ────────────────── +.nom-linked-extras { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 16px; + margin-top: 4px; + + @media (width <= 600px) { + grid-template-columns: 1fr; + } +} diff --git a/src/app/shared/nominee-form/nominee-form.component.ts b/src/app/shared/nominee-form/nominee-form.component.ts new file mode 100644 index 000000000..8d0e05652 --- /dev/null +++ b/src/app/shared/nominee-form/nominee-form.component.ts @@ -0,0 +1,263 @@ +/** + * 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, Output, EventEmitter, inject, OnInit } from '@angular/core'; +import { UntypedFormBuilder, UntypedFormGroup, Validators, FormsModule } from '@angular/forms'; +import { DomSanitizer } from '@angular/platform-browser'; +import { ClientsService } from 'app/clients/clients.service'; +import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; +import { MatProgressSpinner } from '@angular/material/progress-spinner'; +import { MatIconButton } from '@angular/material/button'; +import { MatIcon } from '@angular/material/icon'; +import { MatCheckbox } from '@angular/material/checkbox'; +import { MatDatepickerModule } from '@angular/material/datepicker'; + +export interface NomineeData { + mode: 'manual' | 'linked'; + // Manual fields + fullName?: string; + relationship?: string; + dateOfBirth?: string; + nationality?: string; + aadhaarNumber?: string; + panNumber?: string; + mobileNo?: string; + nomineeShare?: number; + isAlive?: boolean; + isDependent?: boolean; + guardianName?: string; + guardianRelationship?: string; + remarks?: string; + // Linked client fields + clientId?: number; + displayName?: string; + cifNumber?: string; +} + +const RELATIONSHIPS = [ + 'Spouse', + 'Son', + 'Daughter', + 'Father', + 'Mother', + 'Brother', + 'Sister', + 'Grandfather', + 'Grandmother', + 'Grandson', + 'Granddaughter', + 'Uncle', + 'Aunt', + 'Nephew', + 'Niece', + 'Friend', + 'Other' +]; + +@Component({ + selector: 'mifosx-nominee-form', + templateUrl: './nominee-form.component.html', + styleUrls: ['./nominee-form.component.scss'], + imports: [ + ...STANDALONE_SHARED_IMPORTS, + FormsModule, + MatProgressSpinner, + MatIconButton, + MatIcon, + MatCheckbox, + MatDatepickerModule + ] +}) +export class NomineeFormComponent implements OnInit { + private fb = inject(UntypedFormBuilder); + private clientsService = inject(ClientsService); + private sanitizer = inject(DomSanitizer); + + @Output() nomineeChange = new EventEmitter(); + + mode: 'manual' | 'linked' = 'manual'; + relationships = RELATIONSHIPS; + + // Manual form + manualForm!: UntypedFormGroup; + minDate = new Date(1900, 0, 1); + maxDate = new Date(); + + // Link mode + searchText = ''; + searching = false; + searchResults: any[] = []; + selectedClient: any = null; + fetchingLinkedData = false; + linkedProfileImage: any = null; + linkedKycStatus: any = null; + clientDob = ''; + clientAadhaar = ''; + clientPan = ''; + clientCifNumber = ''; + linkedRelationship = ''; + linkedShare = 100; + + private searchTimer: any; + + ngOnInit(): void { + this.manualForm = this.fb.group({ + fullName: [ + '', + Validators.required + ], + relationship: [ + '', + Validators.required + ], + dateOfBirth: [''], + nationality: ['Indian'], + aadhaarNumber: [''], + panNumber: [''], + mobileNo: [''], + nomineeShare: [100], + isAlive: [true], + isDependent: [false], + guardianName: [''], + guardianRelationship: [''], + remarks: [''] + }); + + this.manualForm.valueChanges.subscribe(() => { + if (this.mode === 'manual' && this.manualForm.valid) { + this.emitManual(); + } + }); + } + + switchMode(m: 'manual' | 'linked'): void { + this.mode = m; + if (m === 'manual') { + this.clearLinked(); + } else { + this.manualForm.reset({ nationality: 'Indian', isAlive: true, isDependent: false, nomineeShare: 100 }); + this.nomineeChange.emit(null); + } + } + + // ─── Manual ────────────────────────────────────── + private emitManual(): void { + const v = this.manualForm.value; + this.nomineeChange.emit({ mode: 'manual', ...v }); + } + + // ─── Link existing client ───────────────────────── + onSearch(text: string): void { + this.searchText = text; + clearTimeout(this.searchTimer); + if (!text || text.length < 2) { + this.searchResults = []; + return; + } + this.searching = true; + this.searchTimer = setTimeout(() => { + this.clientsService.searchByText(text, 0, 8).subscribe({ + next: (data: any) => { + this.searchResults = data?.content || []; + this.searching = false; + }, + error: () => { + this.searchResults = []; + this.searching = false; + } + }); + }, 400); + } + + selectClient(client: any): void { + this.selectedClient = client; + this.searchResults = []; + this.searchText = ''; + this.fetchLinkedClientData(client.id); + this.nomineeChange.emit({ + mode: 'linked', + clientId: client.id, + displayName: client.displayName, + cifNumber: client.cifNumber || client.cifNo || '' + }); + } + + removeLinked(): void { + this.clearLinked(); + this.nomineeChange.emit(null); + } + + onLinkedRelationshipChange(rel: string): void { + if (this.selectedClient) { + this.nomineeChange.emit({ + mode: 'linked', + clientId: this.selectedClient.id, + displayName: this.selectedClient.displayName, + cifNumber: this.selectedClient.accountNo || this.selectedClient.cifNumber || '', + relationship: rel, + nomineeShare: this.linkedShare + }); + } + } + + private clearLinked(): void { + this.selectedClient = null; + this.linkedProfileImage = null; + this.linkedKycStatus = null; + this.clientDob = ''; + this.clientAadhaar = ''; + this.clientPan = ''; + this.clientCifNumber = ''; + this.searchText = ''; + this.searchResults = []; + this.linkedRelationship = ''; + this.linkedShare = 100; + } + + private fetchLinkedClientData(clientId: string): void { + this.fetchingLinkedData = true; + + this.clientsService.getClientData(clientId).subscribe({ + next: (data: any) => { + if (data.dateOfBirth) { + const d = Array.isArray(data.dateOfBirth) + ? new Date(data.dateOfBirth[0], data.dateOfBirth[1] - 1, data.dateOfBirth[2]) + : new Date(data.dateOfBirth); + this.clientDob = d.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }); + } + this.fetchingLinkedData = false; + }, + error: () => { + this.fetchingLinkedData = false; + } + }); + + this.clientsService.getClientKyc(clientId).subscribe({ + next: (kyc: any) => { + if (kyc) { + this.clientAadhaar = kyc.aadhaarNumberMasked || ''; + this.clientPan = kyc.panNumber || ''; + this.linkedKycStatus = { + panVerified: kyc.panVerified || false, + aadhaarVerified: kyc.aadhaarVerified || false + }; + } + }, + error: () => {} + }); + + this.clientsService.getClientProfileImage(clientId).subscribe({ + next: (imageData: any) => { + if (imageData) { + this.linkedProfileImage = this.sanitizer.bypassSecurityTrustResourceUrl(imageData); + } + }, + error: () => {} + }); + } +} diff --git a/src/app/system/audit-trails/audit-trails.component.html b/src/app/system/audit-trails/audit-trails.component.html index f3da5e48b..9d7b0696c 100644 --- a/src/app/system/audit-trails/audit-trails.component.html +++ b/src/app/system/audit-trails/audit-trails.component.html @@ -45,7 +45,10 @@ Status - + @for (processingResult of auditTrailSearchTemplateData.processingResults; track processingResult) { {{ processingResult.processingResult }} } @@ -54,7 +57,12 @@ User - +
@@ -63,17 +71,32 @@
Action - + Entity - + Checker - +
@@ -84,7 +107,13 @@
From Date - + @@ -115,7 +144,13 @@
From Date - + @@ -128,7 +163,13 @@
To Date - + @@ -186,7 +227,12 @@
{{ 'labels.inputs.Status' | translate }} - + {{ auditTrail.processingResult }} {{ 'labels.inputs.Entity' | translate }}{{ auditTrail.entityName | translateKey: 'auditTrail.entities' }} + {{ auditTrail.entityName | translateKey: 'auditTrail.entities' }} + {{ 'labels.inputs.Office' | translate }} {{ 'labels.inputs.Checked Date' | translate }}{{ auditTrail.checkedOnDate | datetimeFormat }} + {{ auditTrail.checkedOnDate | datetimeFormat }} + {{ 'labels.inputs.Client Ip' | translate }} {{ 'labels.inputs.Account Number' | translate }} + {{ element.accountNo }} {{ 'labels.inputs.Staff' | translate }} + {{ element.staffName }}
} diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.scss b/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.scss index 1626355c9..80e3ad1f3 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.scss +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/client-approval/client-approval.component.scss @@ -31,7 +31,9 @@ $text-muted: #64748b; max-width: 320px; ::ng-deep { - .mat-mdc-form-field-flex { height: 32px; } + .mat-mdc-form-field-flex { + height: 32px; + } .mat-mdc-form-field-infix { padding: 4px 0; @@ -44,8 +46,13 @@ $text-muted: #64748b; } .mdc-notched-outline { - .mdc-notched-outline__leading { border-radius: 8px 0 0 8px; } - .mdc-notched-outline__trailing { border-radius: 0 8px 8px 0; } + .mdc-notched-outline__leading { + border-radius: 8px 0 0 8px; + } + + .mdc-notched-outline__trailing { + border-radius: 0 8px 8px 0; + } } .mat-mdc-form-field-icon-prefix { @@ -53,7 +60,9 @@ $text-muted: #64748b; color: $text-muted; } - input { font-size: 13px; } + input { + font-size: 13px; + } } } diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.html b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.html index 2aa3ade2e..414ab0f40 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.html +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.html @@ -84,7 +84,12 @@
{{ loan.loanPurposeName }}
} diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.scss b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.scss index 1626355c9..80e3ad1f3 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.scss +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-approval/loan-approval.component.scss @@ -31,7 +31,9 @@ $text-muted: #64748b; max-width: 320px; ::ng-deep { - .mat-mdc-form-field-flex { height: 32px; } + .mat-mdc-form-field-flex { + height: 32px; + } .mat-mdc-form-field-infix { padding: 4px 0; @@ -44,8 +46,13 @@ $text-muted: #64748b; } .mdc-notched-outline { - .mdc-notched-outline__leading { border-radius: 8px 0 0 8px; } - .mdc-notched-outline__trailing { border-radius: 0 8px 8px 0; } + .mdc-notched-outline__leading { + border-radius: 8px 0 0 8px; + } + + .mdc-notched-outline__trailing { + border-radius: 0 8px 8px 0; + } } .mat-mdc-form-field-icon-prefix { @@ -53,7 +60,9 @@ $text-muted: #64748b; color: $text-muted; } - input { font-size: 13px; } + input { + font-size: 13px; + } } } diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.html b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.html index 6462e9623..f115e81b6 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.html +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.html @@ -68,7 +68,7 @@ mat-cell *matCellDef="let loan" class="cell-mono" - style="cursor: pointer;" + style="cursor: pointer" [routerLink]="['../../clients', loan.clientId, 'loans-accounts', loan.id, 'general']" > {{ loan.accountNo }} @@ -80,7 +80,7 @@ mat-cell *matCellDef="let loan" [routerLink]="['../../clients', loan.clientId, 'loans-accounts', loan.id, 'general']" - style="cursor: pointer;" + style="cursor: pointer" > {{ loan.loanProductName }}
{{ loan.principal | formatNumber }}
diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.scss b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.scss index 5b0c37a8d..0b9f62ce1 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.scss +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/loan-disbursal/loan-disbursal.component.scss @@ -31,7 +31,9 @@ $text-muted: #64748b; max-width: 320px; ::ng-deep { - .mat-mdc-form-field-flex { height: 32px; } + .mat-mdc-form-field-flex { + height: 32px; + } .mat-mdc-form-field-infix { padding: 4px 0; @@ -44,8 +46,13 @@ $text-muted: #64748b; } .mdc-notched-outline { - .mdc-notched-outline__leading { border-radius: 8px 0 0 8px; } - .mdc-notched-outline__trailing { border-radius: 0 8px 8px 0; } + .mdc-notched-outline__leading { + border-radius: 8px 0 0 8px; + } + + .mdc-notched-outline__trailing { + border-radius: 0 8px 8px 0; + } } .mat-mdc-form-field-icon-prefix { @@ -53,7 +60,9 @@ $text-muted: #64748b; color: $text-muted; } - input { font-size: 13px; } + input { + font-size: 13px; + } } } diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.html b/src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.html index 2bb7fd20c..62ad3f06c 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.html +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.html @@ -80,7 +80,7 @@ mat-cell *matCellDef="let loan" class="cell-mono" - style="cursor: pointer;" + style="cursor: pointer" [routerLink]="['../../clients', loan.clientId, 'loans', loan.loanId, 'general']" > {{ loan.loanAccountNumber }} @@ -95,7 +95,12 @@
{{ loan.rescheduleReasonCodeValue.name }}
diff --git a/src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.scss b/src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.scss index 1fc26e613..7f73f40b8 100644 --- a/src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.scss +++ b/src/app/tasks/checker-inbox-and-tasks-tabs/reschedule-loan/reschedule-loan.component.scss @@ -31,7 +31,9 @@ $text-muted: #64748b; max-width: 320px; ::ng-deep { - .mat-mdc-form-field-flex { height: 32px; } + .mat-mdc-form-field-flex { + height: 32px; + } .mat-mdc-form-field-infix { padding: 4px 0; @@ -44,8 +46,13 @@ $text-muted: #64748b; } .mdc-notched-outline { - .mdc-notched-outline__leading { border-radius: 8px 0 0 8px; } - .mdc-notched-outline__trailing { border-radius: 0 8px 8px 0; } + .mdc-notched-outline__leading { + border-radius: 8px 0 0 8px; + } + + .mdc-notched-outline__trailing { + border-radius: 0 8px 8px 0; + } } .mat-mdc-form-field-icon-prefix { @@ -53,7 +60,9 @@ $text-muted: #64748b; color: $text-muted; } - input { font-size: 13px; } + input { + font-size: 13px; + } } } diff --git a/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.html b/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.html index 71f132790..b13591041 100644 --- a/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.html +++ b/src/app/tasks/checker-inbox-and-tasks/checker-inbox-and-tasks.component.html @@ -17,19 +17,54 @@ diff --git a/src/app/users/create-user/create-user.component.html b/src/app/users/create-user/create-user.component.html index 463f3a38e..bfa0a7d55 100644 --- a/src/app/users/create-user/create-user.component.html +++ b/src/app/users/create-user/create-user.component.html @@ -29,7 +29,10 @@ {{ 'labels.inputs.Username' | translate }} @if (userForm.controls.username.hasError('required')) { - {{ 'labels.inputs.Username' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} + {{ 'labels.inputs.Username' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} } @@ -37,10 +40,16 @@ {{ 'labels.inputs.Email' | translate }} @if (userForm.controls.email.hasError('email')) { - {{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.invalid' | translate }} + {{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.invalid' | translate }} } @if (userForm.controls.email.hasError('required')) { - {{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} + {{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} } @@ -48,10 +57,18 @@ {{ 'labels.inputs.First Name' | translate }} @if (userForm.controls.firstname.hasError('pattern')) { - {{ 'labels.inputs.First Name' | translate }} {{ 'labels.commons.cannot begin with a special character or number' | translate }} + {{ 'labels.inputs.First Name' | translate }} + {{ + 'labels.commons.cannot begin with a special character or number' | translate + }} } @if (userForm.controls.firstname.hasError('required')) { - {{ 'labels.inputs.First Name' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} + {{ 'labels.inputs.First Name' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} } @@ -59,10 +76,18 @@ {{ 'labels.inputs.Last Name' | translate }} @if (userForm.controls.lastname.hasError('pattern')) { - {{ 'labels.inputs.Last Name' | translate }} {{ 'labels.commons.cannot begin with a special character or number' | translate }} + {{ 'labels.inputs.Last Name' | translate }} + {{ + 'labels.commons.cannot begin with a special character or number' | translate + }} } @if (userForm.controls.lastname.hasError('required')) { - {{ 'labels.inputs.Last Name' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} + {{ 'labels.inputs.Last Name' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} } @@ -90,16 +115,33 @@ {{ 'labels.inputs.Password' | translate }} @if (userForm.controls.password.hasError('required')) { - {{ 'labels.inputs.Password' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} + {{ 'labels.inputs.Password' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} } @if (userForm.controls.password.hasError('minlength')) { - {{ 'labels.commons.Password should be at least' | translate }} {{ 'labels.commons.12 characters long' | translate }} + {{ 'labels.commons.Password should be at least' | translate }} + {{ 'labels.commons.12 characters long' | translate }} } @if (userForm.controls.password.hasError('maxlength')) { - {{ 'labels.commons.Password should not be more than' | translate }} {{ 'labels.commons.50 characters long' | translate }} + {{ 'labels.commons.Password should not be more than' | translate }} + {{ 'labels.commons.50 characters long' | translate }} } - @if (userForm.controls.password.hasError('pattern') && !(userForm.controls.password.hasError('minlength') || userForm.controls.password.hasError('maxlength'))) { - {{ 'labels.commons.Password should include a' | translate }} {{ 'labels.commons.numeral' | translate }} {{ 'labels.commons.and at' | translate }} {{ 'labels.commons.least one uppercase' | translate }}, {{ 'labels.commons.lowercase and special character' | translate }} + @if ( + userForm.controls.password.hasError('pattern') && + !(userForm.controls.password.hasError('minlength') || userForm.controls.password.hasError('maxlength')) + ) { + {{ 'labels.commons.Password should include a' | translate }} + {{ 'labels.commons.numeral' | translate }} {{ 'labels.commons.and at' | translate }} + {{ 'labels.commons.least one uppercase' | translate }}, {{ 'labels.commons.lowercase and special character' | translate }} } @@ -107,13 +149,23 @@ {{ 'labels.inputs.Repeat Password' | translate }} @if (userForm.controls.repeatPassword.hasError('required')) { - {{ 'labels.inputs.Password' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} + {{ 'labels.inputs.Password' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} } } - @if (userForm.errors?.passwordsDoNotMatch && !userForm.controls.password.hasError('required') && !userForm.controls.repeatPassword.hasError('required')) { -

{{ 'labels.inputs.Passwords' | translate }} {{ 'labels.commons.do not match' | translate }}

+ @if ( + userForm.errors?.passwordsDoNotMatch && + !userForm.controls.password.hasError('required') && + !userForm.controls.repeatPassword.hasError('required') + ) { +

+ {{ 'labels.inputs.Passwords' | translate }} + {{ 'labels.commons.do not match' | translate }} +

} @@ -133,7 +185,10 @@ } @if (userForm.controls.officeId.hasError('required')) { - {{ 'labels.inputs.Office' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} + {{ 'labels.inputs.Office' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} } @@ -154,7 +209,10 @@ } @if (userForm.controls.roles.hasError('required')) { - {{ 'labels.commons.At least one role' | translate }} {{ 'labels.commons.must be selected' | translate }} + {{ 'labels.commons.At least one role' | translate }} + {{ 'labels.commons.must be selected' | translate }} } @@ -176,11 +234,24 @@

{{ 'labels.heading.Create User' | translate }}

-

{{ 'labels.text.Filling Details' | translate }}: {{ 'labels.inputs.Users' | translate }}

+

+ {{ 'labels.text.Filling Details' | translate }}: + {{ + 'labels.inputs.Users' | translate + }} +

- - - - + + + +
diff --git a/src/app/users/create-user/create-user.component.scss b/src/app/users/create-user/create-user.component.scss index 00bcd0378..f1efbba06 100644 --- a/src/app/users/create-user/create-user.component.scss +++ b/src/app/users/create-user/create-user.component.scss @@ -44,9 +44,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -92,7 +97,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .cu-field { @@ -148,6 +155,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/users/edit-user/edit-user.component.html b/src/app/users/edit-user/edit-user.component.html index 5320a7681..a5db4c43c 100644 --- a/src/app/users/edit-user/edit-user.component.html +++ b/src/app/users/edit-user/edit-user.component.html @@ -29,7 +29,10 @@ {{ 'labels.inputs.Username' | translate }} @if (editUserForm.controls.username.hasError('required')) { - {{ 'labels.inputs.Username' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} + {{ 'labels.inputs.Username' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} } @@ -37,10 +40,16 @@ {{ 'labels.inputs.Email' | translate }} @if (editUserForm.controls.email.hasError('email')) { - {{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.invalid' | translate }} + {{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.invalid' | translate }} } @if (editUserForm.controls.email.hasError('required')) { - {{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} + {{ 'labels.inputs.Email' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} } @@ -48,10 +57,18 @@ {{ 'labels.inputs.First Name' | translate }} @if (editUserForm.controls.firstname.hasError('pattern')) { - {{ 'labels.inputs.First Name' | translate }} {{ 'labels.commons.cannot begin with a special character or number' | translate }} + {{ 'labels.inputs.First Name' | translate }} + {{ + 'labels.commons.cannot begin with a special character or number' | translate + }} } @if (editUserForm.controls.firstname.hasError('required')) { - {{ 'labels.inputs.First Name' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} + {{ 'labels.inputs.First Name' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} } @@ -59,10 +76,18 @@ {{ 'labels.inputs.Last Name' | translate }} @if (editUserForm.controls.lastname.hasError('pattern')) { - {{ 'labels.inputs.Last Name' | translate }} {{ 'labels.commons.cannot begin with a special character or number' | translate }} + {{ 'labels.inputs.Last Name' | translate }} + {{ + 'labels.commons.cannot begin with a special character or number' | translate + }} } @if (editUserForm.controls.lastname.hasError('required')) { - {{ 'labels.inputs.Last Name' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} + {{ 'labels.inputs.Last Name' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} } @@ -98,7 +123,10 @@ } @if (editUserForm.controls.officeId.hasError('required')) { - {{ 'labels.inputs.Office' | translate }} {{ 'labels.commons.is' | translate }} {{ 'labels.commons.required' | translate }} + {{ 'labels.inputs.Office' | translate }} {{ 'labels.commons.is' | translate }} + {{ 'labels.commons.required' | translate }} } @@ -119,7 +147,10 @@ } @if (editUserForm.controls.roles.hasError('required')) { - {{ 'labels.commons.At least one role' | translate }} {{ 'labels.commons.must be selected' | translate }} + {{ 'labels.commons.At least one role' | translate }} + {{ 'labels.commons.must be selected' | translate }} } @@ -130,7 +161,13 @@ - diff --git a/src/app/users/edit-user/edit-user.component.scss b/src/app/users/edit-user/edit-user.component.scss index cf07ad55f..c19dcd5c4 100644 --- a/src/app/users/edit-user/edit-user.component.scss +++ b/src/app/users/edit-user/edit-user.component.scss @@ -44,9 +44,14 @@ $gray-900: #202124; border-color: $border !important; color: $gray-500 !important; - i { margin-right: 6px; } + i { + margin-right: 6px; + } - &:hover { color: $primary !important; border-color: $primary !important; } + &:hover { + color: $primary !important; + border-color: $primary !important; + } } // Card @@ -92,7 +97,9 @@ $gray-900: #202124; gap: 4px 16px; padding: 16px 24px; - @media (width <= 600px) { grid-template-columns: 1fr; } + @media (width <= 600px) { + grid-template-columns: 1fr; + } } .eu-field { @@ -140,6 +147,9 @@ $gray-900: #202124; height: 36px !important; box-shadow: 0 1px 3px rgba($primary, 0.3) !important; - i { margin-right: 6px; font-size: 12px; } + i { + margin-right: 6px; + font-size: 12px; + } } } diff --git a/src/app/users/users.component.html b/src/app/users/users.component.html index 3a235a289..443599f73 100644 --- a/src/app/users/users.component.html +++ b/src/app/users/users.component.html @@ -20,7 +20,12 @@
- +
@@ -63,7 +68,11 @@
{{ 'labels.buttons.Is Self Service' | translate }} - + {{ user.isSelfServiceUser ? ('labels.buttons.Yes' | translate) : ('labels.buttons.No' | translate) }}