feat: complete UI redesign — client creation tabs, footer, home page, CIF & occupation fields

- Client creation: replaced vertical stepper with 3-tab layout (General, KYC & Nominee, Preview)
- All form fields redesigned with appearance="outline" and section grouping
- Preview tab with profile card, info tables, KYC details, family members
- Footer redesigned: security bar, LincShield certifications, regulatory & legal, quick navigation, support & contact, LincPay branding
- Home page redesigned: welcome banner, quick action cards, info row
- Client view header redesigned: ID chips row (Account No, CIF Number, External Id), 4-column details grid
- Added CIF Number column to clients list table
- Added CIF Number display to client view sidebar and personal data tab
- Added occupationCategoryId and occupationId dropdowns to client creation form
- Added occupation fields to client view sidebar, personal data tab, and preview
- Added 51+ missing translation keys to en-US.json
- Toolbar: added Last login chip
- Registered faUserPlus icon

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-03-20 19:19:47 +05:30
co-authored by Claude Opus 4.6
parent b3a4cce0cf
commit 7b1f2e8ad7
81 changed files with 8384 additions and 3923 deletions
+1 -1
View File
@@ -31,7 +31,7 @@
"@angular/localize/init"
],
"assets": [
"src/favicon.ico",
"src/assets/images/cbsnexora-favicon.svg",
"src/robots.txt",
"src/manifest.json",
"src/assets",
@@ -7,87 +7,115 @@
-->
<div class="mat-typography">
<h3>{{ 'labels.heading.Address' | translate }}</h3>
<div class="section-block">
<div class="section-header">
<h4 class="section-title">
<fa-icon icon="location-arrow" class="section-icon"></fa-icon>
{{ 'labels.heading.Address' | translate }}
</h4>
<button mat-stroked-button color="primary" (click)="addAddress()" class="add-btn">
<fa-icon icon="plus" class="m-r-10"></fa-icon>{{ 'labels.buttons.Add' | translate }}
</button>
</div>
<div class="layout-row align-end m-b-20">
<button mat-raised-button color="primary" (click)="addAddress()">
<fa-icon icon="plus" class="m-r-10"></fa-icon>{{ 'labels.buttons.Add' | translate }}
</button>
</div>
@if (clientAddressData.length === 0) {
<div class="empty-state">
<fa-icon icon="location-arrow" class="empty-icon"></fa-icon>
<span>No address added yet. Click Add to add an address.</span>
</div>
}
@if (clientAddressData.length > 0) {
<mat-accordion>
@for (address of clientAddressData; track address; let i = $index) {
<mat-expansion-panel class="address">
<mat-expansion-panel-header>
<mat-panel-title>
{{ getSelectedValue('addressTypeIdOptions', address.addressTypeId)?.name }}
</mat-panel-title>
<mat-panel-description>
{{ address.relationship }}
</mat-panel-description>
</mat-expansion-panel-header>
<mat-divider></mat-divider>
<div class="address-actions layout-row align-end align-items-center">
<button mat-button color="primary" (click)="editAddress(address, i)">
<fa-icon icon="edit"></fa-icon>
</button>
<button mat-button color="warn" (click)="deleteAddress(address, i)">
<fa-icon icon="trash"></fa-icon>
</button>
<mat-slide-toggle [checked]="address.isActive" (change)="toggleAddress(address)"></mat-slide-toggle>
</div>
<p>
@if (isFieldEnabled('street')) {
<span>{{ 'labels.inputs.Street' | translate }} : {{ address.street }}<br /></span>
}
@if (isFieldEnabled('addressLine1')) {
<span>{{ 'labels.inputs.Address Line' | translate }} 1 : {{ address.addressLine1 }}<br /></span>
}
@if (isFieldEnabled('addressLine2')) {
<span>{{ 'labels.inputs.Address Line' | translate }} 2 : {{ address.addressLine2 }}<br /></span>
}
@if (isFieldEnabled('addressLine3')) {
<span>{{ 'labels.inputs.Address Line' | translate }} 3 : {{ address.addressLine3 }}<br /></span>
}
@if (isFieldEnabled('townVillage')) {
<span>{{ 'labels.inputs.Town / Village' | translate }} : {{ address.townVillage }}<br /></span>
}
@if (isFieldEnabled('city')) {
<span>{{ 'labels.inputs.City' | translate }} : {{ address.city }}<br /></span>
}
@if (isFieldEnabled('stateProvinceId')) {
<span
>{{ 'labels.inputs.State / Province' | translate }} :
{{ getSelectedValue('stateProvinceIdOptions', address.stateProvinceId)?.name }}<br
/></span>
}
@if (isFieldEnabled('countryId')) {
<span
>{{ 'labels.inputs.Country' | translate }} :
{{ getSelectedValue('countryIdOptions', address.countryId)?.name }}<br
/></span>
}
@if (isFieldEnabled('postalCode')) {
<span>{{ 'labels.inputs.Postal Code' | translate }} : {{ address.postalCode }}<br /></span>
}
@if (isFieldEnabled('isActive')) {
<span>{{ 'labels.inputs.Active Status' | translate }} : {{ address.isActive }}<br /></span>
}
</p>
</mat-expansion-panel>
}
</mat-accordion>
}
@if (clientAddressData.length > 0) {
<mat-accordion>
@for (address of clientAddressData; track address; let i = $index) {
<mat-expansion-panel class="address-panel">
<mat-expansion-panel-header>
<mat-panel-title>
<fa-icon icon="location-arrow" class="panel-icon"></fa-icon>
{{ getSelectedValue('addressTypeIdOptions', address.addressTypeId)?.name }}
</mat-panel-title>
<mat-panel-description>
{{ address.relationship }}
</mat-panel-description>
</mat-expansion-panel-header>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
<mat-divider></mat-divider>
<div class="address-actions">
<button mat-button color="primary" (click)="editAddress(address, i)">
<fa-icon icon="edit"></fa-icon>
</button>
<button mat-button color="warn" (click)="deleteAddress(address, i)">
<fa-icon icon="trash"></fa-icon>
</button>
<mat-slide-toggle [checked]="address.isActive" (change)="toggleAddress(address)"></mat-slide-toggle>
</div>
<div class="address-details">
@if (isFieldEnabled('street')) {
<div class="detail-row">
<span class="detail-label">{{ 'labels.inputs.Street' | translate }}</span>
<span class="detail-value">{{ address.street }}</span>
</div>
}
@if (isFieldEnabled('addressLine1')) {
<div class="detail-row">
<span class="detail-label">{{ 'labels.inputs.Address Line' | translate }} 1</span>
<span class="detail-value">{{ address.addressLine1 }}</span>
</div>
}
@if (isFieldEnabled('addressLine2')) {
<div class="detail-row">
<span class="detail-label">{{ 'labels.inputs.Address Line' | translate }} 2</span>
<span class="detail-value">{{ address.addressLine2 }}</span>
</div>
}
@if (isFieldEnabled('addressLine3')) {
<div class="detail-row">
<span class="detail-label">{{ 'labels.inputs.Address Line' | translate }} 3</span>
<span class="detail-value">{{ address.addressLine3 }}</span>
</div>
}
@if (isFieldEnabled('townVillage')) {
<div class="detail-row">
<span class="detail-label">{{ 'labels.inputs.Town / Village' | translate }}</span>
<span class="detail-value">{{ address.townVillage }}</span>
</div>
}
@if (isFieldEnabled('city')) {
<div class="detail-row">
<span class="detail-label">{{ 'labels.inputs.City' | translate }}</span>
<span class="detail-value">{{ address.city }}</span>
</div>
}
@if (isFieldEnabled('stateProvinceId')) {
<div class="detail-row">
<span class="detail-label">{{ 'labels.inputs.State / Province' | translate }}</span>
<span class="detail-value">{{ getSelectedValue('stateProvinceIdOptions', address.stateProvinceId)?.name }}</span>
</div>
}
@if (isFieldEnabled('countryId')) {
<div class="detail-row">
<span class="detail-label">{{ 'labels.inputs.Country' | translate }}</span>
<span class="detail-value">{{ getSelectedValue('countryIdOptions', address.countryId)?.name }}</span>
</div>
}
@if (isFieldEnabled('postalCode')) {
<div class="detail-row">
<span class="detail-label">{{ 'labels.inputs.Postal Code' | translate }}</span>
<span class="detail-value">{{ address.postalCode }}</span>
</div>
}
@if (isFieldEnabled('isActive')) {
<div class="detail-row">
<span class="detail-label">{{ 'labels.inputs.Active Status' | translate }}</span>
<span class="detail-value">{{ address.isActive ? 'Active' : 'Inactive' }}</span>
</div>
}
</div>
</mat-expansion-panel>
}
</mat-accordion>
}
</div>
</div>
@@ -6,25 +6,102 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
.address {
.address-actions {
margin-top: 1%;
.section-block {
margin-bottom: 8px;
}
button {
margin-right: 1%;
}
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
h3 {
margin: 1% auto;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 600;
color: #333;
margin: 0;
p {
line-height: 30px;
margin-left: 2%;
.section-icon {
color: var(--md-sys-color-primary, #1074b9);
font-size: 14px;
}
}
.margin-t {
margin-top: 2em;
.add-btn {
font-size: 13px;
}
.empty-state {
display: flex;
align-items: center;
gap: 10px;
padding: 24px 16px;
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
background: rgba(0, 0, 0, 0.02);
border-radius: 6px;
border: 1px dashed rgba(0, 0, 0, 0.12);
.empty-icon {
font-size: 18px;
}
}
.address-panel {
margin-bottom: 8px;
.panel-icon {
margin-right: 8px;
font-size: 13px;
color: rgba(0, 0, 0, 0.45);
}
}
.address-actions {
display: flex;
justify-content: flex-end;
align-items: center;
gap: 4px;
margin: 8px 0 4px;
}
.address-details {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px 24px;
padding: 8px 0;
}
.detail-row {
display: flex;
flex-direction: column;
gap: 2px;
}
.detail-label {
font-size: 12px;
color: rgba(0, 0, 0, 0.5);
font-weight: 500;
}
.detail-value {
font-size: 14px;
color: #333;
}
@media (max-width: 768px) {
.section-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.address-details {
grid-template-columns: 1fr;
}
}
@@ -29,7 +29,7 @@ import {
} from '@angular/material/expansion';
import { MatDivider } from '@angular/material/divider';
import { MatSlideToggle } from '@angular/material/slide-toggle';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
@@ -48,9 +48,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatExpansionPanelTitle,
MatExpansionPanelDescription,
MatDivider,
MatSlideToggle,
MatStepperPrevious,
MatStepperNext
MatSlideToggle
]
})
export class ClientAddressStepComponent {
@@ -53,15 +53,4 @@
</div>
}
</div>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious disabled>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</form>
@@ -17,7 +17,7 @@ import {
import { Datatables } from 'app/core/utils/datatables';
import { SettingsService } from 'app/settings/settings.service';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -28,9 +28,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
imports: [
...STANDALONE_SHARED_IMPORTS,
MatCheckbox,
MatStepperPrevious,
FaIconComponent,
MatStepperNext
FaIconComponent
]
})
export class ClientDatatableStepComponent implements OnInit {
@@ -7,63 +7,92 @@
-->
<div class="mat-typography">
<h3>{{ 'labels.heading.Family Members' | translate }}</h3>
<div class="section-block">
<div class="section-header">
<h4 class="section-title">
<fa-icon icon="users" class="section-icon"></fa-icon>
{{ 'labels.heading.Family Members' | translate }}
</h4>
<button mat-stroked-button color="primary" (click)="addFamilyMember()" class="add-btn">
<fa-icon icon="plus" class="m-r-10"></fa-icon>{{ 'labels.buttons.Add' | translate }}
</button>
</div>
<div class="layout-row align-end m-b-20">
<button mat-raised-button color="primary" (click)="addFamilyMember()">
<fa-icon icon="plus" class="m-r-10"></fa-icon>{{ 'labels.buttons.Add' | translate }}
</button>
</div>
<mat-accordion>
@for (member of clientFamilyMembers; track member; let i = $index) {
<mat-expansion-panel class="family-member">
<mat-expansion-panel-header>
<mat-panel-title>
{{ member.firstName }}
</mat-panel-title>
<mat-panel-description>
{{ member.relationshipId | find: clientTemplate.familyMemberOptions.relationshipIdOptions : 'id' : 'name' }}
</mat-panel-description>
</mat-expansion-panel-header>
<mat-divider></mat-divider>
<div class="family-member-actions layout-row align-end">
<button mat-button color="primary">
<fa-icon icon="edit" (click)="editFamilyMember(member, i)"></fa-icon>
</button>
<button mat-button color="warn" (click)="deleteFamilyMember(member.firstName, i)">
<fa-icon icon="trash"></fa-icon>
</button>
</div>
<p>
{{ 'labels.inputs.Nomination Type' | translate }} : {{ member.nominationType }}<br />
{{ 'labels.inputs.Nominee Name' | translate }} : {{ member.firstName }}<br />
{{ 'labels.inputs.Relationship' | translate }} :
{{ member.relationshipId | find: clientTemplate.familyMemberOptions.relationshipIdOptions : 'id' : 'name'
}}<br />
{{ 'labels.inputs.Date Of Birth' | translate }} : {{ member.dateOfBirth | dateFormat }}<br />
{{ 'labels.inputs.Percentage Share' | translate }} : {{ member.percentageShare }}%<br />
{{ 'labels.inputs.Nominee is Minor' | translate }} : {{ member.isDependent ? 'Yes' : 'No' }}<br />
@if (member.isDependent) {
{{ 'labels.inputs.Guardian Name' | translate }} : {{ member.guardianName }}<br />
{{ 'labels.inputs.Relationship with Minor' | translate }} : {{ member.guardianRelationship }}<br />
}
@if (member.remarks) {
{{ 'labels.inputs.Remarks' | translate }} : {{ member.remarks }}<br />
}
</p>
</mat-expansion-panel>
@if (clientFamilyMembers.length === 0) {
<div class="empty-state">
<fa-icon icon="users" class="empty-icon"></fa-icon>
<span>No family members / nominees added yet.</span>
</div>
}
</mat-accordion>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext>
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
<mat-accordion>
@for (member of clientFamilyMembers; track member; let i = $index) {
<mat-expansion-panel class="member-panel">
<mat-expansion-panel-header>
<mat-panel-title>
<fa-icon icon="user" class="panel-icon"></fa-icon>
{{ member.firstName }}
</mat-panel-title>
<mat-panel-description>
{{ member.relationshipId | find: clientTemplate.familyMemberOptions.relationshipIdOptions : 'id' : 'name' }}
</mat-panel-description>
</mat-expansion-panel-header>
<mat-divider></mat-divider>
<div class="member-actions">
<button mat-button color="primary" (click)="editFamilyMember(member, i)">
<fa-icon icon="edit"></fa-icon>
</button>
<button mat-button color="warn" (click)="deleteFamilyMember(member.firstName, i)">
<fa-icon icon="trash"></fa-icon>
</button>
</div>
<div class="member-details">
<div class="detail-row">
<span class="detail-label">{{ 'labels.inputs.Nomination Type' | translate }}</span>
<span class="detail-value">{{ member.nominationType }}</span>
</div>
<div class="detail-row">
<span class="detail-label">{{ 'labels.inputs.Nominee Name' | translate }}</span>
<span class="detail-value">{{ member.firstName }}</span>
</div>
<div class="detail-row">
<span class="detail-label">{{ 'labels.inputs.Relationship' | translate }}</span>
<span class="detail-value">{{ member.relationshipId | find: clientTemplate.familyMemberOptions.relationshipIdOptions : 'id' : 'name' }}</span>
</div>
<div class="detail-row">
<span class="detail-label">{{ 'labels.inputs.Date Of Birth' | translate }}</span>
<span class="detail-value">{{ member.dateOfBirth | dateFormat }}</span>
</div>
<div class="detail-row">
<span class="detail-label">{{ 'labels.inputs.Percentage Share' | translate }}</span>
<span class="detail-value">{{ member.percentageShare }}%</span>
</div>
<div class="detail-row">
<span class="detail-label">{{ 'labels.inputs.Nominee is Minor' | translate }}</span>
<span class="detail-value">{{ member.isDependent ? 'Yes' : 'No' }}</span>
</div>
@if (member.isDependent) {
<div class="detail-row">
<span class="detail-label">{{ 'labels.inputs.Guardian Name' | translate }}</span>
<span class="detail-value">{{ member.guardianName }}</span>
</div>
<div class="detail-row">
<span class="detail-label">{{ 'labels.inputs.Relationship with Minor' | translate }}</span>
<span class="detail-value">{{ member.guardianRelationship }}</span>
</div>
}
@if (member.remarks) {
<div class="detail-row">
<span class="detail-label">{{ 'labels.inputs.Remarks' | translate }}</span>
<span class="detail-value">{{ member.remarks }}</span>
</div>
}
</div>
</mat-expansion-panel>
}
</mat-accordion>
</div>
</div>
@@ -6,25 +6,101 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
.family-member {
.family-member-actions {
margin-top: 1%;
.section-block {
margin-bottom: 8px;
}
button {
margin-right: 1%;
}
}
.section-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
h3 {
margin: 1% auto;
}
.section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 600;
color: #333;
margin: 0;
p {
line-height: 30px;
margin-left: 2%;
.section-icon {
color: var(--md-sys-color-primary, #1074b9);
font-size: 14px;
}
}
.margin-t {
margin-top: 2em;
.add-btn {
font-size: 13px;
}
.empty-state {
display: flex;
align-items: center;
gap: 10px;
padding: 24px 16px;
color: rgba(0, 0, 0, 0.45);
font-size: 14px;
background: rgba(0, 0, 0, 0.02);
border-radius: 6px;
border: 1px dashed rgba(0, 0, 0, 0.12);
.empty-icon {
font-size: 18px;
}
}
.member-panel {
margin-bottom: 8px;
.panel-icon {
margin-right: 8px;
font-size: 13px;
color: rgba(0, 0, 0, 0.45);
}
}
.member-actions {
display: flex;
justify-content: flex-end;
gap: 4px;
margin: 8px 0 4px;
}
.member-details {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px 24px;
padding: 8px 0;
}
.detail-row {
display: flex;
flex-direction: column;
gap: 2px;
}
.detail-label {
font-size: 12px;
color: rgba(0, 0, 0, 0.5);
font-weight: 500;
}
.detail-value {
font-size: 14px;
color: #333;
}
@media (max-width: 768px) {
.section-header {
flex-direction: column;
align-items: flex-start;
gap: 12px;
}
.member-details {
grid-template-columns: 1fr;
}
}
@@ -23,7 +23,7 @@ import {
MatExpansionPanelDescription
} from '@angular/material/expansion';
import { MatDivider } from '@angular/material/divider';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { FindPipe } from '../../../pipes/find.pipe';
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -44,8 +44,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
MatExpansionPanelTitle,
MatExpansionPanelDescription,
MatDivider,
MatStepperPrevious,
MatStepperNext,
FindPipe,
DateFormatPipe
]
@@ -7,230 +7,228 @@
-->
<form [formGroup]="createClientForm">
<div class="layout-row-wrap gap-2px responsive-column">
<mat-form-field class="flex-fill flex-23">
<mat-label>{{ 'labels.inputs.Office' | translate }}</mat-label>
<mat-select required formControlName="officeId">
@for (office of officeOptions; track office) {
<mat-option [value]="office.id">
{{ office.name }}
</mat-option>
}
</mat-select>
@if (createClientForm.controls.officeId.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Office' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
<mat-form-field class="flex-fill flex-23">
<mat-label>{{ 'labels.inputs.Legal Form' | translate }}</mat-label>
<mat-select required formControlName="legalFormId">
@for (legalForm of legalFormOptions; track legalForm) {
<mat-option [value]="legalForm.id">
{{ legalForm.value | translateKey: 'inputs' }}
</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="flex-fill flex-23">
<mat-label>{{ 'labels.inputs.External Id' | translate }}</mat-label>
<input matInput formControlName="externalId" />
@if (!externalNationalIdService.isLoading && externalNationalIdService.statusMessageKey) {
<mat-hint>
<span
class="external-id-status"
[ngClass]="{
success: externalNationalIdService.statusMessageKey === 'External ID verified successfully',
error: externalNationalIdService.statusMessageKey !== 'External ID verified successfully'
}"
>
{{ 'labels.inputs.' + externalNationalIdService.statusMessageKey | translate }}
</span>
</mat-hint>
}
</mat-form-field>
@if (createClientForm.get('fullname')) {
<mat-form-field class="flex-48">
<mat-label>
{{ 'labels.inputs.' + getDateLabel(createClientForm.value.legalFormId, ['Name', 'Entity Name']) | translate }}
</mat-label>
<input matInput required formControlName="fullname" />
@if (createClientForm.controls.fullname.hasError('required')) {
<!-- Section: Basic Information -->
<div class="section-block">
<h4 class="section-title">
<fa-icon icon="briefcase" class="section-icon"></fa-icon>
{{ 'labels.inputs.Basic Information' | translate }}
</h4>
<div class="form-grid">
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'labels.inputs.Office' | translate }}</mat-label>
<mat-select required formControlName="officeId">
@for (office of officeOptions; track office) {
<mat-option [value]="office.id">{{ office.name }}</mat-option>
}
</mat-select>
@if (createClientForm.controls.officeId.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Client name' | translate }} {{ 'labels.commons.is' | translate }}
{{ 'labels.inputs.Office' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
@if (createClientForm.controls.fullname.hasError('pattern')) {
<mat-error>
{{ 'labels.inputs.Client name' | translate }} <strong>{{ 'labels.inputs.cannot' | translate }}</strong>
{{ 'labels.commons.begin with a special character or number' | translate }}
</mat-error>
</mat-form-field>
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'labels.inputs.Legal Form' | translate }}</mat-label>
<mat-select required formControlName="legalFormId">
@for (legalForm of legalFormOptions; track legalForm) {
<mat-option [value]="legalForm.id">{{ legalForm.value | translateKey: 'inputs' }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'labels.inputs.External Id' | translate }}</mat-label>
<input matInput formControlName="externalId" />
@if (!externalNationalIdService.isLoading && externalNationalIdService.statusMessageKey) {
<mat-hint>
<span class="external-id-status"
[ngClass]="{
success: externalNationalIdService.statusMessageKey === 'External ID verified successfully',
error: externalNationalIdService.statusMessageKey !== 'External ID verified successfully'
}">
{{ 'labels.inputs.' + externalNationalIdService.statusMessageKey | translate }}
</span>
</mat-hint>
}
</mat-form-field>
}
@if (createClientForm.get('firstname') || createClientForm.get('middlename') || createClientForm.get('lastname')) {
<div class="name-fields-row">
@if (createClientForm.get('firstname')) {
<mat-form-field class="name-field first-name">
<mat-label>{{ 'labels.inputs.First Name' | translate }}</mat-label>
<input matInput required formControlName="firstname" />
@if (createClientForm.controls.firstname.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Client first name' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
@if (createClientForm.controls.firstname.hasError('pattern')) {
<mat-error>
{{ 'labels.inputs.Client first name' | translate }}
<strong>{{ 'labels.inputs.cannot' | translate }}</strong>
{{ 'labels.commons.begin with a special character or number' | translate }}
</mat-error>
}
</mat-form-field>
}
@if (createClientForm.get('middlename')) {
<mat-form-field class="name-field middle-name">
<mat-label>{{ 'labels.inputs.Middle Name' | translate }}</mat-label>
<input matInput formControlName="middlename" />
@if (createClientForm.controls.middlename.hasError('pattern')) {
<mat-error>
{{ 'labels.inputs.Client middle name' | translate }}
<strong>{{ 'labels.inputs.cannot' | translate }}</strong>
{{ 'labels.commons.begin with a special character or number' | translate }}
</mat-error>
}
</mat-form-field>
}
@if (createClientForm.get('lastname')) {
<mat-form-field class="name-field last-name">
<mat-label>{{ 'labels.inputs.Last Name' | translate }}</mat-label>
<input matInput required formControlName="lastname" />
@if (createClientForm.controls.lastname.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Client last name' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
@if (createClientForm.controls.lastname.hasError('pattern')) {
<mat-error>
{{ 'labels.inputs.Client last name' | translate }}
<strong>{{ 'labels.inputs.cannot' | translate }}</strong>
{{ 'labels.commons.begin with a special character or number' | translate }}
</mat-error>
}
</mat-form-field>
}
</div>
}
</div>
</div>
<mat-divider></mat-divider>
<!-- Section: Client Name -->
<div class="section-block">
<h4 class="section-title">
<fa-icon icon="user" class="section-icon"></fa-icon>
{{ 'labels.inputs.Client Details' | translate }}
</h4>
<div class="form-grid">
@if (createClientForm.get('fullname')) {
<mat-form-field appearance="outline" class="form-field full-width">
<mat-label>
{{ 'labels.inputs.' + getDateLabel(createClientForm.value.legalFormId, ['Name', 'Entity Name']) | translate }}
</mat-label>
<input matInput required formControlName="fullname" />
@if (createClientForm.controls.fullname.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Client name' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
@if (createClientForm.controls.fullname.hasError('pattern')) {
<mat-error>
{{ 'labels.inputs.Client name' | translate }} <strong>{{ 'labels.inputs.cannot' | translate }}</strong>
{{ 'labels.commons.begin with a special character or number' | translate }}
</mat-error>
}
</mat-form-field>
}
<mat-form-field class="flex-48" (click)="dateOfBirthDatePicker.open()">
<mat-label>
{{
'labels.inputs.' + getDateLabel(createClientForm.value.legalFormId, ['Date of Birth', 'Incorporation Date'])
| translate
}}
</mat-label>
<input matInput [max]="maxDate" [matDatepicker]="dateOfBirthDatePicker" formControlName="dateOfBirth" />
<mat-datepicker-toggle matSuffix [for]="dateOfBirthDatePicker"></mat-datepicker-toggle>
<mat-datepicker #dateOfBirthDatePicker></mat-datepicker>
</mat-form-field>
@if (createClientForm.get('firstname') || createClientForm.get('middlename') || createClientForm.get('lastname')) {
<div class="name-fields-row">
@if (createClientForm.get('firstname')) {
<mat-form-field appearance="outline" class="name-field first-name">
<mat-label>{{ 'labels.inputs.First Name' | translate }}</mat-label>
<input matInput required formControlName="firstname" />
@if (createClientForm.controls.firstname.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Client first name' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
@if (createClientForm.controls.firstname.hasError('pattern')) {
<mat-error>
{{ 'labels.inputs.Client first name' | translate }}
<strong>{{ 'labels.inputs.cannot' | translate }}</strong>
{{ 'labels.commons.begin with a special character or number' | translate }}
</mat-error>
}
</mat-form-field>
}
@if (createClientForm.get('middlename')) {
<mat-form-field appearance="outline" class="name-field middle-name">
<mat-label>{{ 'labels.inputs.Middle Name' | translate }}</mat-label>
<input matInput formControlName="middlename" />
@if (createClientForm.controls.middlename.hasError('pattern')) {
<mat-error>
{{ 'labels.inputs.Client middle name' | translate }}
<strong>{{ 'labels.inputs.cannot' | translate }}</strong>
{{ 'labels.commons.begin with a special character or number' | translate }}
</mat-error>
}
</mat-form-field>
}
@if (createClientForm.get('lastname')) {
<mat-form-field appearance="outline" class="name-field last-name">
<mat-label>{{ 'labels.inputs.Last Name' | translate }}</mat-label>
<input matInput required formControlName="lastname" />
@if (createClientForm.controls.lastname.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Client last name' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
@if (createClientForm.controls.lastname.hasError('pattern')) {
<mat-error>
{{ 'labels.inputs.Client last name' | translate }}
<strong>{{ 'labels.inputs.cannot' | translate }}</strong>
{{ 'labels.commons.begin with a special character or number' | translate }}
</mat-error>
}
</mat-form-field>
}
</div>
}
<mat-form-field appearance="outline" class="form-field" (click)="dateOfBirthDatePicker.open()">
<mat-label>
{{ 'labels.inputs.' + getDateLabel(createClientForm.value.legalFormId, ['Date of Birth', 'Incorporation Date']) | translate }}
</mat-label>
<input matInput [max]="maxDate" [matDatepicker]="dateOfBirthDatePicker" formControlName="dateOfBirth" />
<mat-datepicker-toggle matSuffix [for]="dateOfBirthDatePicker"></mat-datepicker-toggle>
<mat-datepicker #dateOfBirthDatePicker></mat-datepicker>
</mat-form-field>
@if (createClientForm.value.legalFormId === LegalFormId.PERSON) {
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'labels.inputs.Gender' | translate }}</mat-label>
<mat-select formControlName="genderId">
@for (gender of genderOptions; track gender) {
<mat-option [value]="gender.id">{{ gender.name }}</mat-option>
}
</mat-select>
</mat-form-field>
}
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'labels.inputs.Staff' | translate }}</mat-label>
<mat-select formControlName="staffId">
@for (staff of staffOptions; track staff) {
<mat-option [value]="staff.id">{{ staff.displayName }}</mat-option>
}
</mat-select>
</mat-form-field>
@if (createClientForm.value.legalFormId === LegalFormId.PERSON) {
<div class="form-field checkbox-field">
<mat-checkbox labelPosition="after" formControlName="isStaff">
{{ 'labels.inputs.Is staff' | translate }}?
</mat-checkbox>
</div>
}
</div>
<!-- Entity Non-Person Details -->
@if (createClientForm.get('clientNonPersonDetails')) {
<div class="flex-100 layout-row-wrap responsive-column gap-4percent" formGroupName="clientNonPersonDetails">
<mat-form-field class="flex-48">
<div class="form-grid" formGroupName="clientNonPersonDetails">
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'labels.inputs.Constitution' | translate }}</mat-label>
<mat-select formControlName="constitutionId" required>
@for (constitution of constitutionOptions; track constitution) {
<mat-option [value]="constitution.id">
{{ constitution.name }}
</mat-option>
<mat-option [value]="constitution.id">{{ constitution.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="flex-48">
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'labels.inputs.Main Business Line' | translate }}</mat-label>
<mat-select formControlName="mainBusinessLineId">
@for (business of businessLineOptions; track business) {
<mat-option [value]="business.id">
{{ business.name }}
</mat-option>
<mat-option [value]="business.id">{{ business.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="flex-48" (click)="incorpValidityTillDateDatePicker.open()">
<mat-form-field appearance="outline" class="form-field" (click)="incorpValidityTillDateDatePicker.open()">
<mat-label>{{ 'labels.inputs.Incorporation Validity Till Date' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="incorpValidityTillDateDatePicker"
formControlName="incorpValidityTillDate"
/>
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="incorpValidityTillDateDatePicker" formControlName="incorpValidityTillDate" />
<mat-datepicker-toggle matSuffix [for]="incorpValidityTillDateDatePicker"></mat-datepicker-toggle>
<mat-datepicker #incorpValidityTillDateDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field class="flex-48">
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'labels.inputs.Incorporation Number' | translate }}</mat-label>
<input matInput formControlName="incorpNumber" />
</mat-form-field>
<mat-form-field class="flex-98">
<mat-form-field appearance="outline" class="form-field full-width">
<mat-label>{{ 'labels.inputs.Remarks' | translate }}</mat-label>
<textarea matInput formControlName="remarks" cdkTextareaAutosize cdkAutosizeMinRows="2"></textarea>
</mat-form-field>
</div>
}
</div>
@if (createClientForm.value.legalFormId === LegalFormId.PERSON) {
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Gender' | translate }}</mat-label>
<mat-select formControlName="genderId">
@for (gender of genderOptions; track gender) {
<mat-option [value]="gender.id">
{{ gender.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
}
<!-- Section: Verification -->
<div class="section-block">
<h4 class="section-title">
<fa-icon icon="shield-alt" class="section-icon"></fa-icon>
{{ 'labels.inputs.Verification' | translate }}
</h4>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Staff' | translate }}</mat-label>
<mat-select formControlName="staffId">
@for (staff of staffOptions; track staff) {
<mat-option [value]="staff.id">
{{ staff.displayName }}
</mat-option>
}
</mat-select>
</mat-form-field>
@if (createClientForm.value.legalFormId === LegalFormId.PERSON) {
<mat-checkbox class="flex-48 margin-v" labelPosition="before" formControlName="isStaff">
{{ 'labels.inputs.Is staff' | translate }}?
</mat-checkbox>
}
<div class="sanctions-verify-section flex-98">
<div class="sanctions-verify-section">
@if (!sanctionsVerified && !sanctionsBlocked) {
<button
mat-raised-button
color="primary"
type="button"
class="verify-client-btn"
<button mat-stroked-button color="primary" type="button" class="verify-client-btn"
[disabled]="!createClientForm.get('firstname')?.value && !createClientForm.get('fullname')?.value"
(click)="verifySanctions()"
>
<fa-icon icon="shield-alt" class="mr-05"></fa-icon>
(click)="verifySanctions()">
<fa-icon icon="shield-alt" class="m-r-10"></fa-icon>
Verify Client
</button>
}
@@ -253,145 +251,132 @@
}
</div>
<mat-divider class="flex-98"></mat-divider>
<div class="dip-info flex-98">
<mat-icon
class="dip-info-icon"
<div class="dip-info">
<mat-icon class="dip-info-icon"
matTooltip="Digital Intelligence Platform (DIP) refers to the fraud-monitoring and intelligence ecosystem created by the Department of Telecommunications (DoT)"
matTooltipPosition="above"
>info</mat-icon
>
matTooltipPosition="above">info</mat-icon>
<span class="dip-info-text">DIP Verification</span>
</div>
<div class="mobile-verify-row flex-48">
<mat-form-field class="mobile-field">
<div class="mobile-verify-row">
<mat-form-field appearance="outline" class="mobile-field">
<mat-label>{{ 'labels.inputs.Mobile No' | translate }}</mat-label>
<input matInput type="text" formControlName="mobileNo" />
</mat-form-field>
@if (!mobileVerified) {
<button
mat-raised-button
color="primary"
type="button"
class="verify-btn"
<button mat-stroked-button color="primary" type="button" class="verify-btn"
[disabled]="!createClientForm.get('mobileNo').value"
(click)="verifyWithDIP()"
>
(click)="verifyWithDIP()">
Verify with DIP
</button>
} @else {
<span class="text-success verified-label">&#10004; Verified</span>
}
</div>
</div>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Email Address' | translate }}</mat-label>
<input matInput formControlName="emailAddress" />
@if (createClientForm.controls.emailAddress.errors?.email) {
<mat-error>
{{ 'error.Email not valid' | translate }}
</mat-error>
}
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Client Type' | translate }}</mat-label>
<mat-select formControlName="clientTypeId">
@for (clientType of clientTypeOptions; track clientType) {
<mat-option [value]="clientType.id">
{{ clientType.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Client Classification' | translate }}</mat-label>
<mat-select formControlName="clientClassificationId">
@for (clientClassification of clientClassificationTypeOptions; track clientClassification) {
<mat-option [value]="clientClassification.id">
{{ clientClassification.name }}
</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field class="flex-48" (click)="submittedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Submitted On' | translate }}</mat-label>
<input
matInput
[min]="minDate"
[max]="maxDate"
[matDatepicker]="submittedOnDatePicker"
formControlName="submittedOnDate"
required
/>
<mat-datepicker-toggle matSuffix [for]="submittedOnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #submittedOnDatePicker></mat-datepicker>
</mat-form-field>
<br />
<mat-checkbox labelPosition="before" formControlName="active" class="margin-v flex-48">
{{ 'labels.inputs.Active' | translate }}?
</mat-checkbox>
@if (createClientForm.contains('activationDate')) {
<mat-form-field class="flex-48" (click)="activatedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Activation Date' | translate }}</mat-label>
<input
matInput
[min]="createClientForm.value.submittedOnDate"
[max]="maxDate"
[matDatepicker]="activatedOnDatePicker"
required
formControlName="activationDate"
/>
<mat-datepicker-toggle matSuffix [for]="activatedOnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #activatedOnDatePicker></mat-datepicker>
@if (createClientForm.controls.activationDate.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Activation Date' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
<!-- Section: Contact & Classification -->
<div class="section-block">
<h4 class="section-title">
<fa-icon icon="envelope" class="section-icon"></fa-icon>
{{ 'labels.inputs.Contact & Classification' | translate }}
</h4>
<div class="form-grid">
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'labels.inputs.Email Address' | translate }}</mat-label>
<input matInput formControlName="emailAddress" />
@if (createClientForm.controls.emailAddress.errors?.email) {
<mat-error>{{ 'error.Email not valid' | translate }}</mat-error>
}
</mat-form-field>
}
<mat-checkbox labelPosition="before" formControlName="addSavings" class="margin-v flex-48">
{{ 'labels.inputs.Open Savings Account' | translate }}?
</mat-checkbox>
@if (createClientForm.contains('savingsProductId')) {
<mat-form-field class="flex-48">
<mat-label>{{ 'labels.inputs.Savings Product' | translate }}</mat-label>
<mat-select required formControlName="savingsProductId">
@for (product of savingProductOptions; track product) {
<mat-option [value]="product.id">
{{ product.name }}
</mat-option>
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'labels.inputs.Client Type' | translate }}</mat-label>
<mat-select formControlName="clientTypeId">
@for (clientType of clientTypeOptions; track clientType) {
<mat-option [value]="clientType.id">{{ clientType.name }}</mat-option>
}
</mat-select>
@if (createClientForm.controls.savingsProductId.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Savings Product' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
}
</div>
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious disabled>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext [disabled]="sanctionsBlocked">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'labels.inputs.Client Classification' | translate }}</mat-label>
<mat-select formControlName="clientClassificationId">
@for (clientClassification of clientClassificationTypeOptions; track clientClassification) {
<mat-option [value]="clientClassification.id">{{ clientClassification.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'labels.inputs.Occupation Category' | translate }}</mat-label>
<mat-select formControlName="occupationCategoryId">
@for (cat of occupationCategoryOptions; track cat) {
<mat-option [value]="cat.id">{{ cat.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'labels.inputs.Occupation' | translate }}</mat-label>
<mat-select formControlName="occupationId">
@for (occ of occupationOptions; track occ) {
<mat-option [value]="occ.id">{{ occ.name }}</mat-option>
}
</mat-select>
</mat-form-field>
<mat-form-field appearance="outline" class="form-field" (click)="submittedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Submitted On' | translate }}</mat-label>
<input matInput [min]="minDate" [max]="maxDate" [matDatepicker]="submittedOnDatePicker" formControlName="submittedOnDate" required />
<mat-datepicker-toggle matSuffix [for]="submittedOnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #submittedOnDatePicker></mat-datepicker>
</mat-form-field>
<div class="form-field checkbox-field">
<mat-checkbox labelPosition="after" formControlName="active">
{{ 'labels.inputs.Active' | translate }}?
</mat-checkbox>
</div>
@if (createClientForm.contains('activationDate')) {
<mat-form-field appearance="outline" class="form-field" (click)="activatedOnDatePicker.open()">
<mat-label>{{ 'labels.inputs.Activation Date' | translate }}</mat-label>
<input matInput [min]="createClientForm.value.submittedOnDate" [max]="maxDate"
[matDatepicker]="activatedOnDatePicker" required formControlName="activationDate" />
<mat-datepicker-toggle matSuffix [for]="activatedOnDatePicker"></mat-datepicker-toggle>
<mat-datepicker #activatedOnDatePicker></mat-datepicker>
@if (createClientForm.controls.activationDate.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Activation Date' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
}
<div class="form-field checkbox-field">
<mat-checkbox labelPosition="after" formControlName="addSavings">
{{ 'labels.inputs.Open Savings Account' | translate }}?
</mat-checkbox>
</div>
@if (createClientForm.contains('savingsProductId')) {
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'labels.inputs.Savings Product' | translate }}</mat-label>
<mat-select required formControlName="savingsProductId">
@for (product of savingProductOptions; track product) {
<mat-option [value]="product.id">{{ product.name }}</mat-option>
}
</mat-select>
@if (createClientForm.controls.savingsProductId.hasError('required')) {
<mat-error>
{{ 'labels.inputs.Savings Product' | translate }} {{ 'labels.commons.is' | translate }}
<strong>{{ 'labels.commons.required' | translate }}</strong>
</mat-error>
}
</mat-form-field>
}
</div>
</div>
</form>
@@ -7,191 +7,80 @@
*/
form {
padding: 16px 0;
padding: 0;
}
.layout-row-wrap {
gap: 8px !important;
.section-block {
margin-bottom: 24px;
padding-bottom: 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
&:last-child {
border-bottom: none;
margin-bottom: 0;
}
}
mat-form-field {
margin-bottom: 8px;
.section-title {
display: flex;
align-items: center;
gap: 8px;
font-size: 15px;
font-weight: 600;
color: #333;
margin: 0 0 16px 0;
.section-icon {
color: var(--md-sys-color-primary, #1074b9);
font-size: 14px;
}
}
mat-divider {
margin: 16px 0;
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px 16px;
}
.margin-v {
margin: 2em 0 0;
.form-field {
width: 100%;
}
.margin-t {
margin-top: 2em;
.full-width {
grid-column: 1 / -1;
}
.checkbox-field {
display: flex;
align-items: center;
padding: 8px 0;
}
.name-fields-row {
display: flex;
gap: 8px;
gap: 12px;
flex-wrap: wrap;
width: 100%;
margin-bottom: 8px;
grid-column: 1 / -1;
.name-field {
flex: 1;
min-width: 200px;
min-width: 180px;
&.first-name,
&.last-name {
flex: 1.2;
}
&.middle-name {
flex: 1;
}
}
}
@media (width <= 768px) {
.name-fields-row {
flex-direction: column;
gap: 4px;
.name-field {
width: 100%;
min-width: unset;
flex: none;
}
}
}
.name-fields-row + mat-form-field,
.name-fields-row + mat-divider,
.name-fields-row + div {
margin-top: 8px;
}
.layout-row.align-center {
padding-top: 16px;
margin-top: 12px;
button {
margin: 0 6px;
}
}
.flex-100 {
margin: 16px 0;
padding: 8px 0;
}
mat-checkbox {
margin: 8px 0;
&.margin-v {
margin-top: 12px;
}
}
@media (width <= 768px) {
form {
padding: 12px 0;
.layout-row-wrap {
gap: 6px !important;
}
}
mat-form-field {
margin-bottom: 6px;
}
mat-divider {
margin: 12px 0;
}
.layout-row.align-center {
padding-top: 12px;
margin-top: 8px;
flex-direction: column;
gap: 8px;
button {
width: 100%;
margin: 2px 0;
}
}
}
.dip-info {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 4px;
.dip-info-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: #1976d2;
cursor: pointer;
}
.dip-info-text {
font-size: 13px;
font-weight: 500;
color: #555;
}
}
.mobile-verify-row {
display: flex;
align-items: center;
gap: 10px;
.mobile-field {
flex: 1;
}
.verify-btn {
height: 40px;
white-space: nowrap;
font-size: 13px;
margin-bottom: 18px;
}
.verified-label {
white-space: nowrap;
margin-bottom: 18px;
}
.text-success {
color: #388e3c;
font-size: 14px;
font-weight: 600;
}
}
/* Verification Section */
.sanctions-verify-section {
margin: 12px 0;
margin-bottom: 16px;
.verify-client-btn {
height: 40px;
font-size: 13px;
white-space: nowrap;
}
.sanctions-progress {
mat-progress-bar {
margin-bottom: 8px;
}
.sanctions-msg {
font-size: 13px;
color: #555;
line-height: 1.5;
margin: 0;
}
}
.sanctions-result {
@@ -216,7 +105,6 @@ mat-checkbox {
border-left: 4px solid #d32f2f;
border-radius: 4px;
padding: 10px 16px;
margin: 12px 0;
.text-danger {
color: #d32f2f;
@@ -230,14 +118,60 @@ mat-checkbox {
}
}
.dip-info {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 12px;
.dip-info-icon {
font-size: 18px;
width: 18px;
height: 18px;
color: #1976d2;
cursor: pointer;
}
.dip-info-text {
font-size: 13px;
font-weight: 500;
color: #555;
}
}
.mobile-verify-row {
display: flex;
align-items: center;
gap: 12px;
.mobile-field {
flex: 1;
max-width: 320px;
}
.verify-btn {
height: 40px;
white-space: nowrap;
font-size: 13px;
margin-bottom: 18px;
}
.verified-label {
white-space: nowrap;
margin-bottom: 18px;
}
.text-success {
color: #388e3c;
font-size: 14px;
font-weight: 600;
}
}
.external-id-status {
font-size: 12px;
font-weight: 500;
&.loading {
color: #1976d2;
}
&.success {
color: #388e3c;
}
@@ -247,12 +181,27 @@ mat-checkbox {
}
}
@media (width <= 480px) {
form {
padding: 8px 0;
@media (max-width: 768px) {
.form-grid {
grid-template-columns: 1fr;
}
.layout-row-wrap {
gap: 4px !important;
.name-fields-row {
flex-direction: column;
gap: 4px;
.name-field {
min-width: unset;
flex: none;
}
}
.mobile-verify-row {
flex-direction: column;
align-items: stretch;
.mobile-field {
max-width: none;
}
}
}
@@ -27,7 +27,7 @@ import { SettingsService } from 'app/settings/settings.service';
import { MatDivider } from '@angular/material/divider';
import { CdkTextareaAutosize } from '@angular/cdk/text-field';
import { MatCheckbox } from '@angular/material/checkbox';
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatIcon } from '@angular/material/icon';
import { MatTooltip } from '@angular/material/tooltip';
@@ -49,9 +49,7 @@ import { DipVerifyDialogComponent } from './dip-verify-dialog.component';
MatDivider,
CdkTextareaAutosize,
MatCheckbox,
MatStepperPrevious,
FaIconComponent,
MatStepperNext,
MatIcon,
MatTooltip
]
@@ -100,6 +98,10 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy {
genderOptions: any;
/** Saving Product Options */
savingProductOptions: any;
/** Occupation Category Options */
occupationCategoryOptions: any;
/** Occupation Options */
occupationOptions: any;
/** Mobile number DIP verification status */
mobileVerified = false;
/** Sanctions verification state */
@@ -152,6 +154,8 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy {
dateOfBirth: [''],
clientTypeId: [''],
clientClassificationId: [''],
occupationCategoryId: [''],
occupationId: [''],
submittedOnDate: [
this.settingsService.businessDate,
Validators.required
@@ -172,6 +176,8 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy {
this.constitutionOptions = this.clientTemplate.clientNonPersonConstitutionOptions;
this.genderOptions = this.clientTemplate.genderOptions;
this.savingProductOptions = this.clientTemplate.savingProductOptions;
this.occupationCategoryOptions = this.clientTemplate.occupationCategoryOptions || [];
this.occupationOptions = this.clientTemplate.occupationOptions || [];
}
/**
@@ -7,111 +7,59 @@
-->
<div class="mat-typography" [formGroup]="kycForm">
<div class="kyc-header">
<h3>{{ 'labels.inputs.KYC Verification' | translate }}</h3>
<span class="kyc-badge">KYC</span>
</div>
<div class="section-block">
<h4 class="section-title">
<fa-icon icon="fingerprint" class="section-icon"></fa-icon>
{{ 'labels.inputs.KYC Verification' | translate }}
<span class="kyc-badge">KYC</span>
</h4>
<p class="kyc-subtitle">
{{ 'labels.inputs.KYC' | translate }} — RBI Master Direction compliant. Fields are optional but recommended for
compliance.
</p>
<div class="kyc-info-hint">
<mat-icon class="kyc-info-hint-icon">info</mat-icon>
<span>Verification (PAN, Aadhaar, GSTIN) will be available from the KYC tab after the client is created.</span>
</div>
<mat-divider class="m-b-20 m-t-10"></mat-divider>
<!-- PAN Section -->
<div class="kyc-field-row">
<div class="kyc-field-label-row">
<span class="kyc-field-title">{{ 'labels.inputs.PAN Number' | translate }}</span>
<span class="kyc-tag">KYC</span>
<div class="kyc-info-hint">
<mat-icon class="kyc-info-hint-icon">info</mat-icon>
<span>Verification (PAN, Aadhaar, GSTIN) will be available from the KYC tab after the client is created.</span>
</div>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.inputs.PAN Number' | translate }}</mat-label>
<input
matInput
formControlName="panNumber"
placeholder="{{ 'labels.inputs.Enter 10-character PAN' | translate }}"
maxlength="10"
style="text-transform: uppercase"
(input)="kycForm.get('panNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })"
/>
@if (kycForm.get('panNumber')?.hasError('pattern') && kycForm.get('panNumber')?.touched) {
<mat-error>{{ 'labels.inputs.Enter 10-character PAN' | translate }}</mat-error>
}
</mat-form-field>
</div>
<mat-divider class="m-b-20 m-t-10"></mat-divider>
<div class="form-grid">
<!-- PAN -->
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'labels.inputs.PAN Number' | translate }}</mat-label>
<input matInput formControlName="panNumber"
placeholder="{{ 'labels.inputs.Enter 10-character PAN' | translate }}"
maxlength="10" style="text-transform: uppercase"
(input)="kycForm.get('panNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })" />
@if (kycForm.get('panNumber')?.hasError('pattern') && kycForm.get('panNumber')?.touched) {
<mat-error>{{ 'labels.inputs.Enter 10-character PAN' | translate }}</mat-error>
}
</mat-form-field>
<!-- Aadhaar Section -->
<div class="kyc-field-row">
<div class="kyc-field-label-row">
<span class="kyc-field-title">{{ 'labels.inputs.Aadhaar Number' | translate }}</span>
<span class="kyc-tag">KYC</span>
<!-- Aadhaar -->
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'labels.inputs.Aadhaar Number' | translate }}</mat-label>
<input matInput formControlName="aadhaarNumber"
placeholder="{{ 'labels.inputs.Enter 12-digit Aadhaar' | translate }}"
maxlength="12" type="password" />
@if (kycForm.get('aadhaarNumber')?.hasError('pattern') && kycForm.get('aadhaarNumber')?.touched) {
<mat-error>{{ 'labels.inputs.Enter 12-digit Aadhaar' | translate }}</mat-error>
}
</mat-form-field>
<!-- GSTIN -->
<mat-form-field appearance="outline" class="form-field">
<mat-label>{{ 'labels.inputs.GSTIN Number' | translate }}</mat-label>
<input matInput formControlName="gstinNumber"
placeholder="{{ 'labels.inputs.Enter 15-character GSTIN' | translate }}"
maxlength="15" style="text-transform: uppercase"
(input)="kycForm.get('gstinNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })" />
@if (kycForm.get('gstinNumber')?.hasError('pattern') && kycForm.get('gstinNumber')?.touched) {
<mat-error>{{ 'labels.inputs.Enter 15-character GSTIN' | translate }}</mat-error>
}
</mat-form-field>
<!-- Remarks -->
<mat-form-field appearance="outline" class="form-field full-width">
<mat-label>{{ 'labels.inputs.Remarks' | translate }}</mat-label>
<textarea matInput formControlName="remarks" rows="3"></textarea>
</mat-form-field>
</div>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.inputs.Aadhaar Number' | translate }}</mat-label>
<input
matInput
formControlName="aadhaarNumber"
placeholder="{{ 'labels.inputs.Enter 12-digit Aadhaar' | translate }}"
maxlength="12"
type="password"
/>
@if (kycForm.get('aadhaarNumber')?.hasError('pattern') && kycForm.get('aadhaarNumber')?.touched) {
<mat-error>{{ 'labels.inputs.Enter 12-digit Aadhaar' | translate }}</mat-error>
}
</mat-form-field>
</div>
<mat-divider class="m-b-20 m-t-10"></mat-divider>
<!-- GSTIN Section -->
<div class="kyc-field-row">
<div class="kyc-field-label-row">
<span class="kyc-field-title">{{ 'labels.inputs.GSTIN Number' | translate }}</span>
<span class="kyc-tag">KYC</span>
</div>
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.inputs.GSTIN Number' | translate }}</mat-label>
<input
matInput
formControlName="gstinNumber"
placeholder="{{ 'labels.inputs.Enter 15-character GSTIN' | translate }}"
maxlength="15"
style="text-transform: uppercase"
(input)="kycForm.get('gstinNumber')?.setValue($any($event.target).value.toUpperCase(), { emitEvent: false })"
/>
@if (kycForm.get('gstinNumber')?.hasError('pattern') && kycForm.get('gstinNumber')?.touched) {
<mat-error>{{ 'labels.inputs.Enter 15-character GSTIN' | translate }}</mat-error>
}
</mat-form-field>
</div>
<mat-divider class="m-b-20 m-t-10"></mat-divider>
<!-- Additional Fields -->
<div class="kyc-additional">
<mat-form-field appearance="outline" class="full-width">
<mat-label>{{ 'labels.inputs.Remarks' | translate }}</mat-label>
<textarea matInput formControlName="remarks" rows="3"></textarea>
</mat-form-field>
</div>
<!-- Navigation -->
<div class="layout-row align-center gap-2percent margin-t responsive-column">
<button mat-raised-button matStepperPrevious type="button">
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button matStepperNext type="button">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
</div>
</div>
@@ -6,21 +6,29 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
.kyc-header {
.section-block {
margin-bottom: 8px;
}
.section-title {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 4px;
gap: 8px;
font-size: 15px;
font-weight: 600;
color: #333;
margin: 0 0 16px 0;
h3 {
margin: 0;
.section-icon {
color: var(--md-sys-color-primary, #1074b9);
font-size: 14px;
}
}
.kyc-badge {
background-color: #1976d2;
color: #fff;
font-size: 11px;
font-size: 10px;
font-weight: 700;
padding: 2px 8px;
border-radius: 12px;
@@ -28,24 +36,6 @@
line-height: 18px;
}
.kyc-tag {
background-color: #e3f2fd;
color: #1565c0;
font-size: 10px;
font-weight: 700;
padding: 2px 7px;
border-radius: 10px;
border: 1px solid #90caf9;
letter-spacing: 0.5px;
line-height: 16px;
}
.kyc-subtitle {
color: #666;
font-size: 13px;
margin: 0 0 8px;
}
.kyc-info-hint {
display: flex;
align-items: center;
@@ -56,7 +46,7 @@
background-color: #e3f2fd;
border: 1px solid #90caf9;
border-radius: 6px;
margin-bottom: 4px;
margin-bottom: 16px;
}
.kyc-info-hint-icon {
@@ -66,187 +56,22 @@
color: #1565c0;
}
.kyc-field-row {
margin-bottom: 8px;
.form-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4px 16px;
}
.kyc-field-label-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.kyc-field-title {
font-weight: 600;
font-size: 14px;
}
.kyc-input-row {
display: flex;
align-items: flex-start;
gap: 12px;
}
.kyc-input {
flex: 1;
}
.verify-btn {
margin-top: 4px;
min-width: 100px;
display: flex;
align-items: center;
justify-content: center;
}
.kyc-status-row {
margin-top: 4px;
margin-bottom: 8px;
}
.status-chip {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 13px;
font-weight: 500;
padding: 4px 10px;
border-radius: 16px;
&.verified {
background-color: #e8f5e9;
color: #2e7d32;
border: 1px solid #a5d6a7;
}
&.pending {
background-color: #fff8e1;
color: #f57f17;
border: 1px solid #ffe082;
}
&.failed {
background-color: #ffebee;
color: #c62828;
border: 1px solid #ef9a9a;
}
}
.status-chip-icon {
font-size: 16px;
height: 16px;
width: 16px;
}
.status-icon {
font-size: 20px;
&.verified {
color: #2e7d32;
}
&.failed {
color: #c62828;
}
}
.kyc-additional {
margin-top: 8px;
}
.full-width {
.form-field {
width: 100%;
}
.m-b-20 {
margin-bottom: 20px;
.full-width {
grid-column: 1 / -1;
}
.m-t-10 {
margin-top: 10px;
}
/* OTP Section */
.otp-section {
margin-top: 8px;
padding: 12px 16px;
background-color: #f3f8ff;
border: 1px solid #bbdefb;
border-radius: 8px;
}
.otp-info {
display: flex;
align-items: center;
gap: 6px;
font-size: 13px;
color: #1565c0;
margin: 0 0 10px;
font-weight: 500;
}
.otp-info-icon {
font-size: 18px;
height: 18px;
width: 18px;
color: #1565c0;
}
.otp-input {
max-width: 200px;
}
.resend-btn {
margin-top: 4px;
font-size: 12px;
}
/* Warning & Error messages */
.kyc-warning {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #f57f17;
margin-top: 4px;
padding: 6px 10px;
background-color: #fffde7;
border: 1px solid #fff59d;
border-radius: 6px;
}
.kyc-warning-icon {
font-size: 16px;
height: 16px;
width: 16px;
color: #f57f17;
}
.kyc-error-msg {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
color: #c62828;
margin-top: 4px;
padding: 6px 10px;
background-color: #ffebee;
border: 1px solid #ef9a9a;
border-radius: 6px;
}
.kyc-error-icon {
font-size: 16px;
height: 16px;
width: 16px;
color: #c62828;
}
.verification-detail {
display: inline-block;
font-size: 12px;
color: #333;
margin-left: 8px;
font-weight: 500;
@media (max-width: 768px) {
.form-grid {
grid-template-columns: 1fr;
}
}
@@ -11,7 +11,7 @@ import { Component, inject } from '@angular/core';
import { UntypedFormBuilder, UntypedFormGroup, Validators } from '@angular/forms';
/** Custom Imports */
import { MatStepperPrevious, MatStepperNext } from '@angular/material/stepper';
import { MatDivider } from '@angular/material/divider';
import { MatIcon } from '@angular/material/icon';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
@@ -29,8 +29,6 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
styleUrls: ['./client-kyc-step.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatStepperPrevious,
MatStepperNext,
MatDivider,
MatIcon,
FaIconComponent
@@ -6,280 +6,244 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="container p-b-20">
<h3>{{ 'labels.heading.General' | translate }}</h3>
<mat-divider [inset]="true"></mat-divider>
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.name' | translate }}</span>
@if (client.legalFormId === LegalFormId.ENTITY) {
<span class="flex-60">{{ client.fullname }}</span>
}
@if (client.legalFormId === LegalFormId.PERSON) {
<span class="flex-60"
>{{ client.firstname }}
{{ client.middlename ? client.middlename + ' ' + client.lastname : client.lastname }}</span
>
}
</div>
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Office' | translate }}</span>
<span class="flex-60">{{ client.officeId | find: clientTemplate.officeOptions : 'id' : 'name' }}</span>
</div>
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Legal Form' | translate }}</span>
<span class="flex-60">{{ client.legalFormId | find: clientTemplate.clientLegalFormOptions : 'id' : 'value' }}</span>
</div>
@if (client.staffId) {
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Staff' | translate }}</span>
<span class="flex-60">{{ client.staffId | find: clientTemplate.staffOptions : 'id' : 'displayName' }}</span>
<div class="preview-container">
<!-- Client Profile Card -->
<div class="profile-card">
<div class="profile-avatar">
<fa-icon [icon]="client?.legalFormId === LegalFormId.PERSON ? 'user' : 'building'" class="avatar-icon"></fa-icon>
</div>
}
@if (client.dateOfBirth) {
<div class="flex-fill">
<span class="flex-40">{{
client.legalFormId === LegalFormId.PERSON ? 'Date of Birth' : 'Incorporation Date'
}}</span>
<span class="flex-60">{{ client.dateOfBirth | dateFormat }}</span>
</div>
}
@if (client.externalId) {
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.External Id' | translate }}</span>
<span class="flex-60">
<mifosx-external-identifier externalId="{{ client.externalId }}"></mifosx-external-identifier>
</span>
</div>
}
@if (client.mobileNo) {
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Mobile No' | translate }}</span>
<span class="flex-60">{{ client.mobileNo }}</span>
</div>
}
@if (client.emailAddress) {
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Email Address' | translate }}</span>
<span class="flex-60">{{ client.emailAddress }}</span>
</div>
}
@if (client.clientTypeId) {
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Client Type' | translate }}</span>
<span class="flex-60">{{ client.clientTypeId | find: clientTemplate.clientTypeOptions : 'id' : 'name' }}</span>
</div>
}
@if (client.clientClassificationId) {
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Client Classification' | translate }}</span>
<span class="flex-60">{{
client.clientClassificationId | find: clientTemplate.clientClassificationOptions : 'id' : 'name'
}}</span>
</div>
}
@if (client.savingsProductId) {
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Savings Product' | translate }}</span>
<span class="flex-60">{{
client.savingsProductId | find: clientTemplate.savingProductOptions : 'id' : 'name'
}}</span>
</div>
}
@if (client.submittedOnDate) {
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Submitted On Date' | translate }}</span>
<span class="flex-60">{{ client.submittedOnDate | dateFormat }}</span>
</div>
}
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Active' | translate }}?</span>
<span class="flex-60">{{ client.active | yesNo }}</span>
</div>
@if (client.activationDate) {
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Activation Date' | translate }}</span>
<span class="flex-60">{{ client.activationDate | dateFormat }}</span>
</div>
}
@if (client.legalFormId === LegalFormId.PERSON) {
@if (client.genderId) {
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Gender' | translate }}</span>
<span class="flex-60">{{ client.genderId | find: clientTemplate.genderOptions : 'id' : 'name' }}</span>
</div>
}
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Is staff' | translate }}?</span>
<span class="flex-60">{{ client.isStaff | yesNo }}</span>
</div>
}
@if (client.legalFormId === LegalFormId.ENTITY) {
@if (client.clientNonPersonDetails.incorpValidityTillDate) {
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Incorporation Validity Till Date' | translate }}</span>
<span class="flex-60">{{ client.clientNonPersonDetails.incorpValidityTillDate | dateFormat }}</span>
</div>
}
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Incorportation Number' | translate }}</span>
<span class="flex-60">{{ client.clientNonPersonDetails.incorpNumber || 'Not Provided' }}</span>
</div>
@if (client.clientNonPersonDetails.mainBusinessLineId) {
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Main Business Line' | translate }}</span>
<span class="flex-60">{{
client.clientNonPersonDetails.mainBusinessLineId
| find: clientTemplate.clientNonPersonMainBusinessLineOptions : 'id' : 'name'
}}</span>
</div>
}
@if (client.clientNonPersonDetails.constitutionId) {
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Constitution' | translate }}</span>
<span class="flex-60">{{
client.clientNonPersonDetails.constitutionId
| find: clientTemplate.clientNonPersonConstitutionOptions : 'id' : 'name'
}}</span>
</div>
}
<div class="flex-fill">
<span class="flex-40">{{ 'labels.inputs.Remarks' | translate }}</span>
<span class="flex-60">{{ client.clientNonPersonDetails.remarks || 'N/A' }}</span>
</div>
}
@if (client.familyMembers.length) {
<div class="layout-row-wrap responsive-column flex-fill m-b-20">
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Family Members' | translate }}</h3>
<mat-divider class="flex-fill"></mat-divider>
<mat-accordion class="flex-fill m-t-5">
@for (member of client.familyMembers; track member) {
<mat-expansion-panel class="family-member">
<mat-expansion-panel-header>
<mat-panel-title>
{{ member.firstName }}
{{ member.middleName ? member.middleName + ' ' + member.lastName : member.lastName }}
</mat-panel-title>
<mat-panel-description>
{{ member.relationship }}
</mat-panel-description>
</mat-expansion-panel-header>
<mat-divider></mat-divider>
<p>
{{ 'labels.inputs.First Name' | translate }} : {{ member.firstName }}<br />
{{ 'labels.inputs.Middle Name' | translate }} : {{ member.middleName }}<br />
{{ 'labels.inputs.Last Name' | translate }} : {{ member.lastName }}<br />
{{ 'labels.inputs.Qualification' | translate }} : {{ member.qualification }}<br />
{{ 'labels.inputs.Relationship' | translate }} :
{{ member.relationshipId | find: clientTemplate.familyMemberOptions.relationshipIdOptions : 'id' : 'name'
}}<br />
{{ 'labels.inputs.Age' | translate }} : {{ member.age }}<br />
{{ 'labels.inputs.Is Dependent' | translate }} : {{ member.isDependent ? 'Yes' : 'No' }}<br />
{{ 'labels.inputs.Marital Status' | translate }} :
{{
member.maritalStatusId
| find: clientTemplate.familyMemberOptions.maritalStatusIdOptions : 'id' : 'name'
}}<br />
{{ 'labels.inputs.Gender' | translate }} :
{{ member.genderId | find: clientTemplate.familyMemberOptions.genderIdOptions : 'id' : 'name' }}<br />
{{ 'labels.inputs.Profession' | translate }} :
{{ member.professionId | find: clientTemplate.familyMemberOptions.professionIdOptions : 'id' : 'name'
}}<br />
{{ 'labels.inputs.Date Of Birth' | translate }} : {{ member.dateOfBirth | dateFormat }}<br />
</p>
</mat-expansion-panel>
<div class="profile-info">
<h3 class="profile-name">
@if (client?.legalFormId === LegalFormId.ENTITY) {
{{ client.fullname }}
}
</mat-accordion>
</div>
}
@if (clientTemplate.isAddressEnabled && client.address && client.address.length) {
<div class="flex-fill layout-row-wrap responsive-column">
<h3 class="mat-h3 flex-fill">{{ 'labels.heading.Address' | translate }}</h3>
<mat-divider class="flex-fill"></mat-divider>
<mat-accordion class="flex-fill m-t-5">
@for (address of client.address; track address) {
<mat-expansion-panel class="address">
<mat-expansion-panel-header>
<mat-panel-title>
{{ getSelectedValue('addressTypeIdOptions', address.addressTypeId)?.name }}
</mat-panel-title>
<mat-panel-description>
{{ address.relationship }}
</mat-panel-description>
</mat-expansion-panel-header>
<mat-divider></mat-divider>
<p>
@if (isFieldEnabled('street')) {
<span>{{ 'labels.inputs.Street' | translate }} : {{ address.street }}<br /></span>
}
@if (isFieldEnabled('addressLine1')) {
<span>{{ 'labels.inputs.Address Line' | translate }} 1 : {{ address.addressLine1 }}<br /></span>
}
@if (isFieldEnabled('addressLine2')) {
<span>{{ 'labels.inputs.Address Line' | translate }} 2 : {{ address.addressLine2 }}<br /></span>
}
@if (isFieldEnabled('addressLine3')) {
<span>{{ 'labels.inputs.Address Line' | translate }} 3 : {{ address.addressLine3 }}<br /></span>
}
@if (isFieldEnabled('townVillage')) {
<span>{{ 'labels.inputs.Town / Village' | translate }} : {{ address.townVillage }}<br /></span>
}
@if (isFieldEnabled('city')) {
<span>{{ 'labels.inputs.City' | translate }} : {{ address.city }}<br /></span>
}
@if (isFieldEnabled('stateProvinceId')) {
<span
>{{ 'labels.inputs.State / Province' | translate }} :
{{ getSelectedValue('stateProvinceIdOptions', address.stateProvinceId)?.name }}<br
/></span>
}
@if (isFieldEnabled('countryId')) {
<span
>{{ 'labels.inputs.Country' | translate }} :
{{ getSelectedValue('countryIdOptions', address.countryId)?.name }}<br
/></span>
}
@if (isFieldEnabled('postalCode')) {
<span>{{ 'labels.inputs.Postal Code' | translate }} : {{ address.postalCode }}<br /></span>
}
@if (isFieldEnabled('isActive')) {
<span>{{ 'labels.inputs.Active Status' | translate }} : {{ address.isActive }}<br /></span>
}
</p>
</mat-expansion-panel>
@if (client?.legalFormId === LegalFormId.PERSON) {
{{ client.firstname }} {{ client.middlename ? client.middlename + ' ' : '' }}{{ client.lastname }}
}
</mat-accordion>
</h3>
<div class="profile-meta">
@if (client?.officeId) {
<span class="meta-chip">
<fa-icon icon="briefcase" class="chip-icon"></fa-icon>
{{ client.officeId | find: clientTemplate.officeOptions : 'id' : 'name' }}
</span>
}
@if (client?.legalFormId) {
<span class="meta-chip">
{{ client.legalFormId | find: clientTemplate.clientLegalFormOptions : 'id' : 'value' }}
</span>
}
@if (client?.active) {
<span class="meta-chip status-active">Active</span>
} @else {
<span class="meta-chip status-inactive">Inactive</span>
}
</div>
</div>
</div>
<!-- General Information Card -->
<div class="info-card">
<div class="card-header">
<fa-icon icon="user" class="card-icon"></fa-icon>
<span>{{ 'labels.heading.General' | translate }}</span>
</div>
<div class="card-body">
<table class="info-table">
<tbody>
@if (client?.dateOfBirth) {
<tr>
<td class="info-label">{{ client.legalFormId === LegalFormId.PERSON ? 'Date of Birth' : 'Incorporation Date' }}</td>
<td class="info-value">{{ client.dateOfBirth | dateFormat }}</td>
</tr>
}
@if (client?.externalId) {
<tr>
<td class="info-label">{{ 'labels.inputs.External Id' | translate }}</td>
<td class="info-value">
<mifosx-external-identifier externalId="{{ client.externalId }}"></mifosx-external-identifier>
</td>
</tr>
}
@if (client?.staffId) {
<tr>
<td class="info-label">{{ 'labels.inputs.Staff' | translate }}</td>
<td class="info-value">{{ client.staffId | find: clientTemplate.staffOptions : 'id' : 'displayName' }}</td>
</tr>
}
@if (client?.legalFormId === LegalFormId.PERSON && client?.genderId) {
<tr>
<td class="info-label">{{ 'labels.inputs.Gender' | translate }}</td>
<td class="info-value">{{ client.genderId | find: clientTemplate.genderOptions : 'id' : 'name' }}</td>
</tr>
}
@if (client?.mobileNo) {
<tr>
<td class="info-label">{{ 'labels.inputs.Mobile No' | translate }}</td>
<td class="info-value">{{ client.mobileNo }}</td>
</tr>
}
@if (client?.emailAddress) {
<tr>
<td class="info-label">{{ 'labels.inputs.Email Address' | translate }}</td>
<td class="info-value">{{ client.emailAddress }}</td>
</tr>
}
@if (client?.clientTypeId) {
<tr>
<td class="info-label">{{ 'labels.inputs.Client Type' | translate }}</td>
<td class="info-value">{{ client.clientTypeId | find: clientTemplate.clientTypeOptions : 'id' : 'name' }}</td>
</tr>
}
@if (client?.clientClassificationId) {
<tr>
<td class="info-label">{{ 'labels.inputs.Client Classification' | translate }}</td>
<td class="info-value">{{ client.clientClassificationId | find: clientTemplate.clientClassificationOptions : 'id' : 'name' }}</td>
</tr>
}
@if (client?.occupationCategoryId) {
<tr>
<td class="info-label">{{ 'labels.inputs.Occupation Category' | translate }}</td>
<td class="info-value">{{ client.occupationCategoryId | find: clientTemplate.occupationCategoryOptions : 'id' : 'name' }}</td>
</tr>
}
@if (client?.occupationId) {
<tr>
<td class="info-label">{{ 'labels.inputs.Occupation' | translate }}</td>
<td class="info-value">{{ client.occupationId | find: clientTemplate.occupationOptions : 'id' : 'name' }}</td>
</tr>
}
@if (client?.submittedOnDate) {
<tr>
<td class="info-label">{{ 'labels.inputs.Submitted On Date' | translate }}</td>
<td class="info-value">{{ client.submittedOnDate | dateFormat }}</td>
</tr>
}
<tr>
<td class="info-label">{{ 'labels.inputs.Active' | translate }}?</td>
<td class="info-value">
<span [class]="client?.active ? 'badge-yes' : 'badge-no'">{{ client?.active | yesNo }}</span>
</td>
</tr>
@if (client?.activationDate) {
<tr>
<td class="info-label">{{ 'labels.inputs.Activation Date' | translate }}</td>
<td class="info-value">{{ client.activationDate | dateFormat }}</td>
</tr>
}
@if (client?.savingsProductId) {
<tr>
<td class="info-label">{{ 'labels.inputs.Savings Product' | translate }}</td>
<td class="info-value">{{ client.savingsProductId | find: clientTemplate.savingProductOptions : 'id' : 'name' }}</td>
</tr>
}
@if (client?.legalFormId === LegalFormId.PERSON) {
<tr>
<td class="info-label">{{ 'labels.inputs.Is staff' | translate }}?</td>
<td class="info-value">{{ client.isStaff | yesNo }}</td>
</tr>
}
@if (client?.legalFormId === LegalFormId.ENTITY && client?.clientNonPersonDetails) {
@if (client.clientNonPersonDetails.constitutionId) {
<tr>
<td class="info-label">{{ 'labels.inputs.Constitution' | translate }}</td>
<td class="info-value">{{ client.clientNonPersonDetails.constitutionId | find: clientTemplate.clientNonPersonConstitutionOptions : 'id' : 'name' }}</td>
</tr>
}
<tr>
<td class="info-label">{{ 'labels.inputs.Incorporation Number' | translate }}</td>
<td class="info-value">{{ client.clientNonPersonDetails.incorpNumber || 'N/A' }}</td>
</tr>
}
</tbody>
</table>
</div>
</div>
<!-- KYC Details Card -->
@if (kycData?.panNumber || kycData?.aadhaarNumberMasked || kycData?.gstinNumber) {
<div class="info-card">
<div class="card-header">
<fa-icon icon="fingerprint" class="card-icon"></fa-icon>
<span>{{ 'labels.inputs.KYC' | translate }}</span>
<span class="kyc-badge">KYC</span>
</div>
<div class="card-body">
<table class="info-table">
<tbody>
@if (kycData.panNumber) {
<tr>
<td class="info-label">{{ 'labels.inputs.PAN Number' | translate }}</td>
<td class="info-value mono">{{ kycData.panNumber }}</td>
</tr>
}
@if (kycData.aadhaarNumberMasked) {
<tr>
<td class="info-label">{{ 'labels.inputs.Aadhaar Number' | translate }}</td>
<td class="info-value mono">{{ kycData.aadhaarNumberMasked }}</td>
</tr>
}
@if (kycData.gstinNumber) {
<tr>
<td class="info-label">{{ 'labels.inputs.GSTIN Number' | translate }}</td>
<td class="info-value mono">{{ kycData.gstinNumber }}</td>
</tr>
}
@if (kycData.remarks) {
<tr>
<td class="info-label">{{ 'labels.inputs.Remarks' | translate }}</td>
<td class="info-value">{{ kycData.remarks }}</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
</div>
<div class="layout-row responsive-column align-center gap-2px margin-t">
<button mat-raised-button matStepperPrevious>
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
<button mat-raised-button [routerLink]="['../']">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" (click)="submitEvent.emit()">
{{ 'labels.buttons.Submit' | translate }}
</button>
<!-- Family Members / Nominee Card -->
@if (client?.familyMembers?.length) {
<div class="info-card">
<div class="card-header">
<fa-icon icon="users" class="card-icon"></fa-icon>
<span>{{ 'labels.heading.Family Members' | translate }} / {{ 'labels.inputs.Nominee' | translate }}</span>
<span class="count-badge">{{ client.familyMembers.length }}</span>
</div>
<div class="card-body no-padding">
<table class="data-table">
<thead>
<tr>
<th>{{ 'labels.inputs.name' | translate }}</th>
<th>{{ 'labels.inputs.Relationship' | translate }}</th>
<th>{{ 'labels.inputs.Date Of Birth' | translate }}</th>
<th>{{ 'labels.inputs.Is Dependent' | translate }}</th>
</tr>
</thead>
<tbody>
@for (member of client.familyMembers; track member) {
<tr>
<td>{{ member.firstName }} {{ member.lastName }}</td>
<td>{{ member.relationshipId | find: clientTemplate.familyMemberOptions.relationshipIdOptions : 'id' : 'name' }}</td>
<td>{{ member.dateOfBirth | dateFormat }}</td>
<td>
<span [class]="member.isDependent ? 'badge-yes' : 'badge-no'">{{ member.isDependent ? 'Yes' : 'No' }}</span>
</td>
</tr>
}
</tbody>
</table>
</div>
</div>
}
<!-- Submit Section -->
<div class="submit-section">
<button mat-raised-button [routerLink]="['../']" class="cancel-btn">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" class="submit-btn" (click)="submitEvent.emit()">
<fa-icon icon="check" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Submit' | translate }}
</button>
</div>
</div>
@@ -6,43 +6,292 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
h2,
h3,
h4 {
margin: 0;
.preview-container {
display: flex;
flex-direction: column;
gap: 16px;
}
/* ── Profile Card ── */
.profile-card {
display: flex;
align-items: center;
gap: 16px;
padding: 20px 24px;
background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%);
border-radius: 10px;
border: 1px solid rgba(0, 0, 0, 0.06);
}
.profile-avatar {
width: 56px;
height: 56px;
border-radius: 50%;
background: var(--md-sys-color-primary, #1074b9);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
.avatar-icon {
color: #fff;
font-size: 22px;
}
}
.profile-info {
flex: 1;
min-width: 0;
}
.profile-name {
margin: 0 0 6px;
font-size: 20px;
font-weight: 600;
color: #1a1a1a;
}
.profile-meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
}
.meta-chip {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 12px;
font-weight: 500;
}
padding: 3px 10px;
border-radius: 20px;
background: rgba(255, 255, 255, 0.7);
color: #555;
border: 1px solid rgba(0, 0, 0, 0.08);
span {
margin: 0.5em 0;
}
mat-divider {
margin: 0 0 0.5em;
}
.margin-t {
margin-top: 2em;
}
.family-member {
h3 {
margin: 1% auto;
.chip-icon {
font-size: 10px;
color: #888;
}
p {
line-height: 30px;
margin-left: 2%;
&.status-active {
background: #e8f5e9;
color: #2e7d32;
border-color: #a5d6a7;
}
&.status-inactive {
background: #fff3e0;
color: #e65100;
border-color: #ffcc80;
}
}
.address {
h3 {
margin: 1% auto;
}
/* ── Info Cards ── */
.info-card {
border: 1px solid rgba(0, 0, 0, 0.08);
border-radius: 8px;
overflow: hidden;
background: #fff;
}
p {
line-height: 30px;
margin-left: 2%;
.card-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 20px;
background: #fafafa;
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
font-size: 14px;
font-weight: 600;
color: #333;
.card-icon {
color: var(--md-sys-color-primary, #1074b9);
font-size: 13px;
}
}
.card-body {
padding: 16px 20px;
&.no-padding {
padding: 0;
}
}
.count-badge {
margin-left: auto;
background: var(--md-sys-color-primary, #1074b9);
color: #fff;
font-size: 11px;
font-weight: 700;
width: 22px;
height: 22px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
/* ── Info Table (Key-Value pairs) ── */
.info-table {
width: 100%;
border-collapse: collapse;
tr {
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
&:last-child {
border-bottom: none;
}
}
td {
padding: 10px 0;
vertical-align: top;
}
.info-label {
width: 40%;
font-size: 13px;
color: rgba(0, 0, 0, 0.5);
font-weight: 500;
padding-right: 16px;
}
.info-value {
font-size: 14px;
color: #1a1a1a;
font-weight: 500;
}
}
/* ── Data Table (Family, etc.) ── */
.data-table {
width: 100%;
border-collapse: collapse;
thead {
background: #fafafa;
th {
padding: 10px 16px;
font-size: 12px;
font-weight: 600;
color: rgba(0, 0, 0, 0.55);
text-align: left;
text-transform: uppercase;
letter-spacing: 0.3px;
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}
}
tbody {
tr {
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
&:last-child {
border-bottom: none;
}
&:hover {
background: #f8f9fa;
}
}
td {
padding: 10px 16px;
font-size: 13px;
color: #333;
}
}
}
/* ── Badges ── */
.badge-yes {
display: inline-block;
font-size: 12px;
font-weight: 600;
padding: 2px 10px;
border-radius: 12px;
background: #e8f5e9;
color: #2e7d32;
}
.badge-no {
display: inline-block;
font-size: 12px;
font-weight: 600;
padding: 2px 10px;
border-radius: 12px;
background: #f5f5f5;
color: #888;
}
/* ── KYC ── */
.kyc-badge {
margin-left: auto;
background: #1976d2;
color: #fff;
font-size: 10px;
font-weight: 700;
padding: 2px 8px;
border-radius: 12px;
letter-spacing: 0.5px;
}
.mono {
font-family: 'Roboto Mono', monospace;
letter-spacing: 1px;
}
/* ── Submit Section ── */
.submit-section {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 20px 0 4px;
border-top: 1px solid rgba(0, 0, 0, 0.08);
margin-top: 8px;
}
.submit-btn {
min-width: 160px;
height: 44px;
font-size: 15px;
font-weight: 600;
letter-spacing: 0.3px;
}
.cancel-btn {
color: rgba(0, 0, 0, 0.6);
height: 44px;
}
/* ── Responsive ── */
@media (max-width: 768px) {
.profile-card {
flex-direction: column;
text-align: center;
padding: 16px;
}
.profile-meta {
justify-content: center;
}
.info-table {
.info-label {
width: 45%;
}
}
.data-table {
thead th,
tbody td {
padding: 8px 10px;
font-size: 12px;
}
}
}
@@ -17,7 +17,6 @@ import {
MatExpansionPanelTitle,
MatExpansionPanelDescription
} from '@angular/material/expansion';
import { MatStepperPrevious } from '@angular/material/stepper';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { FindPipe } from '../../../pipes/find.pipe';
import { DateFormatPipe } from '../../../pipes/date-format.pipe';
@@ -41,7 +40,6 @@ import { LegalFormId } from 'app/clients/models/legal-form.enum';
MatExpansionPanelHeader,
MatExpansionPanelTitle,
MatExpansionPanelDescription,
MatStepperPrevious,
FaIconComponent,
FindPipe,
DateFormatPipe,
@@ -55,6 +53,8 @@ export class ClientPreviewStepComponent {
@Input() clientTemplate: any;
/** Client Object */
@Input() client: any;
/** KYC Data */
@Input() kycData: any;
/** Form submission event */
@Output() submitEvent = new EventEmitter();
@@ -51,150 +51,116 @@
</div>
<div class="mat-typography account-card-title">
<mat-card-title class="make-flex content">
<h3 class="flex-95">
<i
class="fa fa-stop"
[ngClass]="clientViewData.status.code | statusLookup"
matTooltip="{{ 'labels.status.' + clientViewData.status.value | translate }}"
></i>
<b>{{ 'labels.inputs.Client Name' | translate }} :</b>
<!-- Top Row: Name + Status + Actions -->
<mat-card-title class="cv-title-row">
<div class="cv-name-block">
<i class="fa fa-stop" [ngClass]="clientViewData.status.code | statusLookup"
matTooltip="{{ 'labels.status.' + clientViewData.status.value | translate }}"></i>
<mifosx-entity-name
[entityName]="
complianceHideClientData ? maskName(clientViewData.displayName) : clientViewData.displayName
"
[display]="'right'"
[hideCopy]="complianceHideClientData"
></mifosx-entity-name>
</h3>
<div class="flex-5 align-end-center">
<button mat-icon-button [matMenuTriggerFor]="clientMenu" aria-label="Client actions" yPosition="below">
<mat-icon matListIcon class="actions-menu">
<fa-icon icon="bars" size="sm"></fa-icon>
</mat-icon>
</button>
[entityName]="complianceHideClientData ? maskName(clientViewData.displayName) : clientViewData.displayName"
[display]="'right'" [hideCopy]="complianceHideClientData"></mifosx-entity-name>
<span class="cv-status-chip" [ngClass]="clientViewData.status.code | statusLookup">
{{ 'labels.status.' + clientViewData.status.value | translate }}
</span>
</div>
<button mat-icon-button [matMenuTriggerFor]="clientMenu" aria-label="Client actions">
<mat-icon matListIcon class="actions-menu"><fa-icon icon="bars" size="sm"></fa-icon></mat-icon>
</button>
</mat-card-title>
<!-- ID Chips Row -->
<div class="cv-id-row">
<div class="cv-id-chip">
<span class="cv-id-label">{{ 'labels.inputs.Account No' | translate }}</span>
<mifosx-account-number accountNo="{{ clientViewData.accountNo }}"></mifosx-account-number>
</div>
@if (clientViewData.cifNumber) {
<div class="cv-id-chip">
<span class="cv-id-label">{{ 'labels.inputs.CIF Number' | translate }}</span>
<mifosx-account-number accountNo="{{ clientViewData.cifNumber }}"></mifosx-account-number>
</div>
}
@if (clientViewData.externalId) {
<div class="cv-id-chip">
<span class="cv-id-label">{{ 'labels.inputs.External Id' | translate }}</span>
<mifosx-external-identifier
[externalId]="complianceHideClientData ? maskValue(clientViewData.externalId) : clientViewData.externalId"
[completed]="true" [hideCopy]="complianceHideClientData"></mifosx-external-identifier>
</div>
}
</div>
<!-- Details Grid -->
<mat-card-subtitle>
<div class="layout-row responsive-column">
<div class="flex-50">
<table class="account-overview">
<tbody>
<tr>
<td>
<b>{{ 'labels.inputs.Office' | translate }}</b>
</td>
<td><mifosx-entity-name [entityName]="clientViewData.officeName"></mifosx-entity-name></td>
</tr>
<tr>
<td>
<b>{{ 'labels.inputs.Client' | translate }}</b>
</td>
<td><mifosx-account-number accountNo="{{ clientViewData.accountNo }}"></mifosx-account-number></td>
</tr>
<tr>
<td>
<b>{{ 'labels.inputs.External Id' | translate }}</b>
</td>
<td>
<mifosx-external-identifier
[externalId]="
complianceHideClientData ? maskValue(clientViewData.externalId) : clientViewData.externalId
"
[completed]="true"
[hideCopy]="complianceHideClientData"
></mifosx-external-identifier>
</td>
</tr>
@if (clientViewData.activationDate) {
<tr>
<td>
<b>{{ 'labels.inputs.Activation Date' | translate }}</b>
</td>
<td>{{ clientViewData.activationDate | dateFormat }}</td>
</tr>
}
<tr>
<td>
<b>{{ 'labels.inputs.Staff' | translate }}</b>
</td>
@if (clientViewData.staffName) {
<td>{{ clientViewData.staffName }}</td>
}
@if (!clientViewData.staffName) {
<td>{{ 'labels.inputs.Unassigned' | translate }}</td>
}
</tr>
</tbody>
</table>
<div class="cv-details-grid">
<div class="cv-detail">
<span class="cv-dl">{{ 'labels.inputs.Office' | translate }}</span>
<span class="cv-dv">{{ clientViewData.officeName }}</span>
</div>
<div class="flex-50">
<table class="account-overview">
<tbody>
@if (clientViewData.groups.length > 0) {
<tr>
<td>
<b>{{ 'labels.inputs.Member Of' | translate }}</b>
</td>
<td>
@for (group of clientViewData.groups; track group) {
<span>
<span class="m-r-3">{{ group.name }}</span>
</span>
}
</td>
</tr>
}
@if (clientViewData.clientType) {
<tr>
<td>
<b>{{ 'labels.inputs.Client Type' | translate }}</b>
</td>
<td>{{ clientViewData.clientType.name }}</td>
</tr>
@if (clientViewData.clientClassification) {
<tr>
<td>
<b>{{ 'labels.inputs.Client Classification' | translate }}</b>
</td>
<td>{{ clientViewData.clientClassification.name }}</td>
</tr>
}
}
<tr>
<td>
<b>{{ 'labels.inputs.Mobile Number' | translate }}</b>
</td>
<td>
<mifosx-external-identifier
[externalId]="
complianceHideClientData ? maskValue(clientViewData.mobileNo) : clientViewData.mobileNo
"
[completed]="true"
[hideCopy]="complianceHideClientData"
></mifosx-external-identifier>
</td>
</tr>
<tr>
<td>
<b>{{ 'labels.inputs.Email' | translate }}</b>
</td>
<td>
<mifosx-external-identifier
[externalId]="
complianceHideClientData
? maskEmail(clientViewData.emailAddress)
: clientViewData.emailAddress
"
[completed]="true"
[hideCopy]="complianceHideClientData"
></mifosx-external-identifier>
</td>
</tr>
</tbody>
</table>
<div class="cv-detail">
<span class="cv-dl">{{ 'labels.inputs.Staff' | translate }}</span>
<span class="cv-dv">{{ clientViewData.staffName || ('labels.inputs.Unassigned' | translate) }}</span>
</div>
@if (clientViewData.activationDate) {
<div class="cv-detail">
<span class="cv-dl">{{ 'labels.inputs.Activation Date' | translate }}</span>
<span class="cv-dv">{{ clientViewData.activationDate | dateFormat }}</span>
</div>
}
@if (clientViewData.clientType) {
<div class="cv-detail">
<span class="cv-dl">{{ 'labels.inputs.Client Type' | translate }}</span>
<span class="cv-dv">{{ clientViewData.clientType.name }}</span>
</div>
}
@if (clientViewData.clientClassification) {
<div class="cv-detail">
<span class="cv-dl">{{ 'labels.inputs.Client Classification' | translate }}</span>
<span class="cv-dv">{{ clientViewData.clientClassification.name }}</span>
</div>
}
@if (clientViewData.mobileNo) {
<div class="cv-detail">
<span class="cv-dl">{{ 'labels.inputs.Mobile Number' | translate }}</span>
<span class="cv-dv">
<mifosx-external-identifier
[externalId]="complianceHideClientData ? maskValue(clientViewData.mobileNo) : clientViewData.mobileNo"
[completed]="true" [hideCopy]="complianceHideClientData"></mifosx-external-identifier>
</span>
</div>
}
@if (clientViewData.emailAddress) {
<div class="cv-detail">
<span class="cv-dl">{{ 'labels.inputs.Email' | translate }}</span>
<span class="cv-dv">
<mifosx-external-identifier
[externalId]="complianceHideClientData ? maskEmail(clientViewData.emailAddress) : clientViewData.emailAddress"
[completed]="true" [hideCopy]="complianceHideClientData"></mifosx-external-identifier>
</span>
</div>
}
@if (clientViewData.occupationCategory) {
<div class="cv-detail">
<span class="cv-dl">{{ 'labels.inputs.Occupation Category' | translate }}</span>
<span class="cv-dv">{{ clientViewData.occupationCategory.name }}</span>
</div>
}
@if (clientViewData.occupation) {
<div class="cv-detail">
<span class="cv-dl">{{ 'labels.inputs.Occupation' | translate }}</span>
<span class="cv-dv">{{ clientViewData.occupation.name }}</span>
</div>
}
@if (clientViewData.groups?.length > 0) {
<div class="cv-detail">
<span class="cv-dl">{{ 'labels.inputs.Member Of' | translate }}</span>
<span class="cv-dv">
@for (group of clientViewData.groups; track group) {
<span>{{ group.name }} </span>
}
</span>
</div>
}
</div>
</mat-card-subtitle>
</div>
@@ -30,16 +30,121 @@
}
}
.account-overview {
color: white;
min-width: 85%;
margin-left: 10px;
}
.make-flex {
display: flex;
}
.mat-card-title-group {
width: 100%;
}
/* ── Title Row ── */
.cv-title-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 10px;
}
.cv-name-block {
display: flex;
align-items: center;
gap: 8px;
font-size: 18px;
font-weight: 600;
color: #fff;
i.fa-stop {
font-size: 12px;
}
}
.cv-status-chip {
font-size: 10px;
font-weight: 700;
padding: 2px 10px;
border-radius: 12px;
letter-spacing: 0.4px;
text-transform: uppercase;
background: rgba(255, 255, 255, 0.15);
color: rgba(255, 255, 255, 0.9);
}
/* ── ID Chips Row ── */
.cv-id-row {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 14px;
flex-wrap: wrap;
}
.cv-id-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 5px;
color: #fff;
font-size: 12px;
}
.cv-id-label {
font-size: 10px;
font-weight: 600;
color: rgba(255, 255, 255, 0.55);
text-transform: uppercase;
letter-spacing: 0.4px;
}
/* ── Details Grid ── */
.cv-details-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 6px 16px;
}
.cv-detail {
display: flex;
flex-direction: column;
gap: 1px;
padding: 4px 0;
}
.cv-dl {
font-size: 10px;
color: rgba(255, 255, 255, 0.5);
font-weight: 500;
}
.cv-dv {
font-size: 13px;
color: rgba(255, 255, 255, 0.95);
font-weight: 500;
}
.actions-menu {
color: #fff !important;
}
/* ── Responsive ── */
@media (max-width: 1024px) {
.cv-details-grid {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 768px) {
.cv-details-grid {
grid-template-columns: repeat(2, 1fr);
}
.cv-id-row {
flex-direction: column;
align-items: flex-start;
}
}
@media (max-width: 480px) {
.cv-details-grid {
grid-template-columns: 1fr;
}
}
@@ -87,6 +87,10 @@
<span class="label">{{ 'labels.inputs.Account No' | translate }}</span>
<span class="value">{{ clientViewData.accountNo || '-' }}</span>
</div>
<div class="data-item">
<span class="label">{{ 'labels.inputs.CIF Number' | translate }}</span>
<span class="value">{{ clientViewData.cifNumber || '-' }}</span>
</div>
<div class="data-item">
<span class="label">{{ 'labels.inputs.External Id' | translate }}</span>
<span class="value">{{ clientViewData.externalId || '-' }}</span>
@@ -178,6 +182,18 @@
<span class="value">{{ clientViewData.clientClassification.name }}</span>
</div>
}
@if (clientViewData.occupationCategory) {
<div class="data-item">
<span class="label">{{ 'labels.inputs.Occupation Category' | translate }}</span>
<span class="value">{{ clientViewData.occupationCategory.name }}</span>
</div>
}
@if (clientViewData.occupation) {
<div class="data-item">
<span class="label">{{ 'labels.inputs.Occupation' | translate }}</span>
<span class="value">{{ clientViewData.occupation.name }}</span>
</div>
}
@if (!clientViewData.clientType && !clientViewData.clientClassification) {
<div class="data-item">
<span class="value no-data">{{ 'labels.commons.No classification data available' | translate }}</span>
+61 -42
View File
@@ -7,50 +7,66 @@
-->
<mat-card class="container">
<div class="layout-column layout-gt-sm-row align-gt-sm-start-center">
<div class="search-box m-r-30">
<mat-form-field class="search-box">
<!-- Header Bar -->
<div class="table-header">
<div class="table-title-group">
<h2 class="table-title">
<div class="t-logomark"><span></span><span></span><span></span><span></span></div>
{{ 'labels.menus.Clients' | translate }}
</h2>
<span class="table-count" *ngIf="totalRows">{{ totalRows }} records</span>
</div>
<div class="table-actions">
<mat-form-field class="table-search" appearance="outline">
<input
matInput
placeholder="{{ 'labels.text.SearchByClient' | translate }}"
class="search-box"
(keydown.enter)="search($event.target.value)"
/>
</mat-form-field>
</div>
@if (existsClientsToFilter) {
<div class="filter-box">
<mat-checkbox #showClosedAccounts labelPosition="after" class="m-r-10"
>{{ 'labels.inputs.Show Closed Accounts' | translate }}
@if (existsClientsToFilter) {
<mat-checkbox #showClosedAccounts labelPosition="after" class="table-checkbox">
{{ 'labels.inputs.Show Closed Accounts' | translate }}
</mat-checkbox>
}
<div class="table-buttons">
<button
mat-stroked-button
[routerLink]="['../', 'organization', 'bulk-import', 'Clients']"
*mifosxHasPermission="'CREATE_CLIENT'"
class="btn-import"
>
<fa-icon icon="download"></fa-icon>
{{ 'labels.buttons.Import Client' | translate }}
</button>
<button
mat-flat-button
color="primary"
[routerLink]="['create']"
*mifosxHasPermission="'CREATE_CLIENT'"
class="btn-create"
>
<fa-icon icon="plus"></fa-icon>
{{ 'labels.buttons.Create Client' | translate }}
</button>
</div>
}
<div class="action-button gap-25px">
<button
mat-raised-button
color="primary"
[routerLink]="['../', 'organization', 'bulk-import', 'Clients']"
*mifosxHasPermission="'CREATE_CLIENT'"
>
<fa-icon icon="download" class="m-r-20"></fa-icon>{{ 'labels.buttons.Import Client' | translate }}
</button>
<button mat-raised-button color="primary" [routerLink]="['create']" *mifosxHasPermission="'CREATE_CLIENT'">
<fa-icon icon="plus" class="m-r-20"></fa-icon>{{ 'labels.buttons.Create Client' | translate }}
</button>
</div>
</div>
<div [hidden]="!existsClientsToFilter" class="client-list">
<!-- Table -->
<div [hidden]="!existsClientsToFilter" class="table-container">
@if (isLoading) {
<div>
<mat-progress-bar mode="indeterminate"></mat-progress-bar>
</div>
<mat-progress-bar mode="indeterminate" class="table-loader"></mat-progress-bar>
}
<table mat-table [dataSource]="dataSource" matSort (matSortChange)="sortChanged($event)" class="bordered-table">
<table mat-table [dataSource]="dataSource" matSort (matSortChange)="sortChanged($event)" class="modern-table">
<!-- Name Column -->
<ng-container matColumnDef="displayName">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.name' | translate }}</th>
<td mat-cell *matCellDef="let row" [routerLink]="[row.id, 'general']">
<td mat-cell *matCellDef="let row" [routerLink]="[row.id, 'general']" class="cell-link">
{{ hideClientData ? maskName(row.displayName) : row.displayName }}
</td>
</ng-container>
@@ -58,15 +74,21 @@
<!-- Account no Column -->
<ng-container matColumnDef="accountNumber">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Account No' | translate }}</th>
<td mat-cell *matCellDef="let row">
<td mat-cell *matCellDef="let row" class="cell-mono">
<mifosx-account-number display="left" accountNo="{{ row.accountNumber }}"></mifosx-account-number>
</td>
</ng-container>
<!-- CIF Number Column -->
<ng-container matColumnDef="cifNumber">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.CIF Number' | translate }}</th>
<td mat-cell *matCellDef="let row" class="cell-mono">{{ row.cifNumber }}</td>
</ng-container>
<!-- External ID Column -->
<ng-container matColumnDef="externalId">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.External Id' | translate }}</th>
<td mat-cell *matCellDef="let row">
<td mat-cell *matCellDef="let row" class="cell-mono">
<mifosx-external-identifier display="left" externalId="{{ row.externalId }}"></mifosx-external-identifier>
</td>
</ng-container>
@@ -75,22 +97,20 @@
<ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Status' | translate }}</th>
<td mat-cell *matCellDef="let row">
<div [ngClass]="row.status.code | statusLookup">
<fa-icon icon="stop"></fa-icon>
<span class="m-l-10 status">
{{ row.status.value | translate }}
</span>
</div>
<span class="status-badge" [ngClass]="row.status.code | statusLookup">
{{ row.status.value | translate }}
</span>
</td>
</ng-container>
<!-- Office Column -->
<ng-container matColumnDef="officeName">
<th mat-header-cell *matHeaderCellDef>{{ 'labels.inputs.Office Name' | translate }}</th>
<td mat-cell *matCellDef="let row">{{ row.officeName }}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="select-row"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" class="table-row"></tr>
</table>
<mat-paginator
@@ -102,10 +122,9 @@
></mat-paginator>
</div>
<div class="alert" [hidden]="!notExistsClientsToFilter">
<div class="message">
<i class="fa fa-exclamation-circle alert-check"></i>
{{ 'labels.text.No client was found' | translate }}
</div>
<!-- Empty State -->
<div class="empty-state" [hidden]="!notExistsClientsToFilter">
<fa-icon icon="search" class="empty-icon"></fa-icon>
<p class="empty-text">{{ 'labels.text.No client was found' | translate }}</p>
</div>
</mat-card>
+1
View File
@@ -86,6 +86,7 @@ export class ClientsComponent implements OnInit {
displayedColumns = [
'displayName',
'accountNumber',
'cifNumber',
'externalId',
'status',
'officeName'
@@ -6,81 +6,110 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="container">
<mat-stepper orientation="vertical" [linear]="false" class="mat-elevation-z8" labelPosition="bottom" #clientStepper>
<ng-template matStepperIcon="number">
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
</ng-template>
<div class="create-client-container">
<div class="client-header">
<fa-icon icon="user-plus" class="header-icon"></fa-icon>
<h2>{{ 'labels.heading.Create Client' | translate }}</h2>
</div>
<ng-template matStepperIcon="edit">
<fa-icon icon="pencil-alt" size="sm"></fa-icon>
</ng-template>
<ng-template matStepperIcon="done">
<fa-icon icon="check" size="sm"></fa-icon>
</ng-template>
<ng-template matStepperIcon="error">
<fa-icon icon="exclamation-triangle" size="lg"></fa-icon>
</ng-template>
<ng-template matStepperIcon="preview">
<fa-icon icon="eye" size="sm"></fa-icon>
</ng-template>
<mat-step [stepControl]="clientGeneralForm">
<ng-template matStepLabel>{{ 'labels.inputs.GENERAL' | translate }}</ng-template>
<mifosx-client-general-step
[clientTemplate]="clientTemplate"
(legalFormChangeEvent)="legalFormChange($event)"
></mifosx-client-general-step>
</mat-step>
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.Nominee' | translate }}</ng-template>
<mifosx-client-family-members-step
#clientFamily
[clientTemplate]="clientTemplate"
></mifosx-client-family-members-step>
</mat-step>
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.KYC' | translate }}</ng-template>
<mifosx-client-kyc-step #clientKyc></mifosx-client-kyc-step>
</mat-step>
@if (clientTemplate.isAddressEnabled) {
<mat-step>
<ng-template matStepLabel>{{ 'labels.inputs.ADDRESS' | translate }}</ng-template>
<mifosx-client-address-step
#clientAddress
<mat-tab-group class="client-tabs" animationDuration="200ms" [preserveContent]="true" [(selectedIndex)]="selectedTabIndex">
<!-- Tab 1: General Details -->
<mat-tab>
<ng-template mat-tab-label>
<fa-icon icon="user" class="tab-icon"></fa-icon>
{{ 'labels.inputs.GENERAL' | translate }}
</ng-template>
<div class="tab-content">
<mifosx-client-general-step
[clientTemplate]="clientTemplate"
[clientAddressFieldConfig]="clientAddressFieldConfig"
>
</mifosx-client-address-step>
</mat-step>
}
(legalFormChangeEvent)="legalFormChange($event)"
></mifosx-client-general-step>
</div>
</mat-tab>
@for (datatable of datatables; track datatable) {
<mat-step>
<ng-template matStepLabel>{{ datatable.registeredTableName }}</ng-template>
<mifosx-client-datatable-step [datatableData]="datatable" #dtclient></mifosx-client-datatable-step>
</mat-step>
}
<!-- Tab 2: KYC & Nominee -->
<mat-tab>
<ng-template mat-tab-label>
<fa-icon icon="id-card" class="tab-icon"></fa-icon>
{{ 'labels.inputs.KYC' | translate }} & {{ 'labels.inputs.Nominee' | translate }}
</ng-template>
<div class="tab-content">
<div class="tab-section">
<mifosx-client-kyc-step #clientKyc></mifosx-client-kyc-step>
</div>
@if (areFormvalids()) {
<mat-step completed>
<ng-template matStepLabel>{{ 'labels.inputs.PREVIEW' | translate }}</ng-template>
<mat-divider class="section-divider"></mat-divider>
<div class="tab-section">
<mifosx-client-family-members-step
#clientFamily
[clientTemplate]="clientTemplate"
></mifosx-client-family-members-step>
</div>
@if (clientTemplate.isAddressEnabled) {
<mat-divider class="section-divider"></mat-divider>
<div class="tab-section">
<mifosx-client-address-step
#clientAddress
[clientTemplate]="clientTemplate"
[clientAddressFieldConfig]="clientAddressFieldConfig"
></mifosx-client-address-step>
</div>
}
@for (datatable of datatables; track datatable) {
<mat-divider class="section-divider"></mat-divider>
<div class="tab-section">
<h3>{{ datatable.registeredTableName }}</h3>
<mifosx-client-datatable-step [datatableData]="datatable" #dtclient></mifosx-client-datatable-step>
</div>
}
</div>
</mat-tab>
<!-- Tab 3: Preview & Submit -->
<mat-tab>
<ng-template mat-tab-label>
<fa-icon icon="eye" class="tab-icon"></fa-icon>
{{ 'labels.inputs.PREVIEW' | translate }}
</ng-template>
<div class="tab-content">
<mifosx-client-preview-step
[clientTemplate]="clientTemplate"
[clientAddressFieldConfig]="clientAddressFieldConfig"
[client]="client"
[kycData]="clientKycStep?.kycData"
(submitEvent)="submit()"
>
</mifosx-client-preview-step>
</mat-step>
}
</mat-stepper>
></mifosx-client-preview-step>
</div>
</mat-tab>
</mat-tab-group>
<!-- Bottom Action Bar -->
<div class="action-bar">
<button mat-raised-button [routerLink]="['../']" class="cancel-btn">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<div class="action-bar-right">
@if (selectedTabIndex > 0) {
<button mat-raised-button (click)="selectedTabIndex = selectedTabIndex - 1">
<fa-icon icon="arrow-left" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Previous' | translate }}
</button>
}
@if (selectedTabIndex < 2) {
<button mat-raised-button color="primary" (click)="selectedTabIndex = selectedTabIndex + 1">
{{ 'labels.buttons.Next' | translate }}
<fa-icon icon="arrow-right" class="m-l-10"></fa-icon>
</button>
}
@if (selectedTabIndex === 2 && areFormvalids()) {
<button mat-raised-button color="primary" (click)="submit()">
<fa-icon icon="check" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Submit' | translate }}
</button>
}
</div>
</div>
</div>
@@ -5,3 +5,104 @@
* 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/.
*/
.create-client-container {
max-width: 960px;
margin: 0 auto;
padding: 24px 16px;
}
.client-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 20px;
.header-icon {
font-size: 20px;
color: var(--md-sys-color-primary, #1074b9);
}
h2 {
margin: 0;
font-size: 22px;
font-weight: 500;
}
}
.client-tabs {
background: var(--md-sys-color-surface, #fff);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
::ng-deep {
.mat-mdc-tab-header {
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
}
.mat-mdc-tab {
min-width: 160px;
padding: 0 24px;
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;
}
}
}
.tab-icon {
margin-right: 8px;
}
.tab-content {
padding: 24px 16px;
}
.tab-section {
margin-bottom: 16px;
}
.section-divider {
margin: 24px 0;
}
.no-address-message {
display: flex;
align-items: center;
gap: 8px;
padding: 32px 16px;
color: rgba(0, 0, 0, 0.54);
font-size: 14px;
mat-icon {
color: rgba(0, 0, 0, 0.38);
}
}
.action-bar {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 20px;
padding: 16px 0;
border-top: 1px solid rgba(0, 0, 0, 0.08);
}
.action-bar-right {
display: flex;
gap: 12px;
}
.cancel-btn {
color: rgba(0, 0, 0, 0.6);
}
@@ -19,12 +19,14 @@ import { ClientFamilyMembersStepComponent } from '../client-stepper/client-famil
import { ClientAddressStepComponent } from '../client-stepper/client-address-step/client-address-step.component';
import { ClientDatatableStepComponent } from '../client-stepper/client-datatable-step/client-datatable-step.component';
import { ClientKycStepComponent } from '../client-stepper/client-kyc-step/client-kyc-step.component';
import { ClientPreviewStepComponent } from '../client-stepper/client-preview-step/client-preview-step.component';
/** Custom Services */
import { SettingsService } from 'app/settings/settings.service';
import { MatStepper, MatStepperIcon, MatStep, MatStepLabel } from '@angular/material/stepper';
import { MatTabsModule } from '@angular/material/tabs';
import { MatIconModule } from '@angular/material/icon';
import { MatDivider } from '@angular/material/divider';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { ClientPreviewStepComponent } from '../client-stepper/client-preview-step/client-preview-step.component';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
@@ -36,11 +38,10 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
styleUrls: ['./create-client.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
MatStepper,
MatStepperIcon,
MatTabsModule,
MatIconModule,
MatDivider,
FaIconComponent,
MatStep,
MatStepLabel,
ClientGeneralStepComponent,
ClientFamilyMembersStepComponent,
ClientAddressStepComponent,
@@ -55,19 +56,15 @@ export class CreateClientComponent {
private clientsService = inject(ClientsService);
private settingsService = inject(SettingsService);
/** Client General Step */
@ViewChild(ClientGeneralStepComponent, { static: true }) clientGeneralStep: ClientGeneralStepComponent;
/** Client Family Members Step */
@ViewChild(ClientGeneralStepComponent) clientGeneralStep: ClientGeneralStepComponent;
@ViewChild('clientFamily') clientFamilyMembersStep: ClientFamilyMembersStepComponent;
/** Client Address Step */
@ViewChild('clientAddress') clientAddressStep: ClientAddressStepComponent;
/** Client KYC Step */
@ViewChild('clientKyc') clientKycStep: ClientKycStepComponent;
/** Get handle on dtclient tags in the template */
@ViewChildren('dtclient') clientDatatables: QueryList<ClientDatatableStepComponent>;
datatables: any = [];
legalFormType = 1;
selectedTabIndex = 0;
/** Client Template */
clientTemplate: any;
@@ -89,38 +86,26 @@ export class CreateClientComponent {
});
}
/**
* Retrieves general information about client.
*/
get clientGeneralForm() {
return this.clientGeneralStep.createClientForm;
return this.clientGeneralStep?.createClientForm;
}
/**
* Retrieves the client object
*/
get client() {
if (this.clientTemplate.isAddressEnabled) {
return {
...this.clientGeneralStep.clientGeneralDetails,
...this.clientFamilyMembersStep.familyMembers,
...this.clientAddressStep.address
};
} else {
return {
...this.clientGeneralStep.clientGeneralDetails,
...this.clientFamilyMembersStep.familyMembers
};
}
const general = this.clientGeneralStep?.clientGeneralDetails || {};
const family = this.clientFamilyMembersStep?.familyMembers || {};
const addr = this.clientTemplate.isAddressEnabled && this.clientAddressStep ? this.clientAddressStep.address : {};
return { ...general, ...family, ...addr };
}
areFormvalids(): boolean {
if (!this.clientGeneralStep || !this.clientGeneralForm) {
return false;
}
let areValids = this.clientGeneralForm.valid;
// Block form if sanctions match found
if (this.clientGeneralStep.sanctionsBlocked) {
return false;
}
if (this.clientTemplate.isAddressEnabled) {
if (this.clientTemplate.isAddressEnabled && this.clientAddressStep) {
areValids = areValids && this.clientAddressStep.address.address.length > 0;
}
if (this.clientTemplate.datatables && this.clientTemplate.datatables.length > 0 && this.clientDatatables) {
@@ -128,7 +113,6 @@ export class CreateClientComponent {
areValids = areValids && clientDatatable.datatableForm.valid;
});
}
return areValids;
}
+110 -97
View File
@@ -25,105 +25,120 @@
<!-- Content -->
<mat-sidenav-content class="sidenav">
<!-- Sidebar Toggle Handle -->
<div
class="sidebar-handle"
[class.collapsed]="sidenavCollapsed"
(click)="toggleSidebarCollapse()"
matTooltip="{{ sidenavCollapsed ? 'Expand sidebar' : 'Collapse sidebar' }}"
matTooltipPosition="right"
>
<svg viewBox="0 0 8 14" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
@if (!sidenavCollapsed) {
<path d="M6 1L1 7l5 6"/>
}
@if (sidenavCollapsed) {
<path d="M2 1l5 6-5 6"/>
}
</svg>
</div>
<!-- Toolbar -->
<mifosx-toolbar [sidenav]="sidenav" (collapse)="toggleCollapse($event)"></mifosx-toolbar>
<!-- Sub Menu Bar — Row 1 (7 items) -->
<div class="sub-menu-bar">
<a class="sub-menu-link" tabindex="0" [matMenuTriggerFor]="institutionMenu">
<fa-icon icon="university" class="sub-menu-icon" size="lg"></fa-icon>Institution
<fa-icon icon="caret-down" class="sub-menu-caret"></fa-icon>
</a>
<a class="sub-menu-link" tabindex="0" routerLink="/checker-inbox-and-tasks/checker-inbox">
<fa-icon icon="check" class="sub-menu-icon" size="lg"></fa-icon>Tasks
</a>
<a class="sub-menu-link" tabindex="0" routerLink="/accounting">
<fa-icon icon="money-bill-alt" class="sub-menu-icon" size="lg"></fa-icon>Accounting
</a>
<a class="sub-menu-link" tabindex="0" [matMenuTriggerFor]="reportsMenu">
<fa-icon icon="chart-bar" class="sub-menu-icon" size="lg"></fa-icon>Reports
<fa-icon icon="caret-down" class="sub-menu-caret"></fa-icon>
</a>
<a class="sub-menu-link" tabindex="0" [matMenuTriggerFor]="adminMenu">
<fa-icon icon="shield-alt" class="sub-menu-icon" size="lg"></fa-icon>Admin
<fa-icon icon="caret-down" class="sub-menu-caret"></fa-icon>
</a>
<a class="sub-menu-link" tabindex="0" (click)="subLedgerOpen = !subLedgerOpen; glMenuOpen = false">
<fa-icon icon="book" class="sub-menu-icon" size="lg"></fa-icon>Sub Ledger
<fa-icon [icon]="subLedgerOpen ? 'caret-up' : 'caret-down'" class="sub-menu-caret"></fa-icon>
</a>
<a class="sub-menu-link" tabindex="0" (click)="glMenuOpen = !glMenuOpen; subLedgerOpen = false">
<fa-icon icon="book-open" class="sub-menu-icon" size="lg"></fa-icon>General Ledger
<fa-icon [icon]="glMenuOpen ? 'caret-up' : 'caret-down'" class="sub-menu-caret"></fa-icon>
</a>
<span class="sub-menu-link sub-menu-spacer"></span>
</div>
<!-- Main Header — 2-Row Navigation -->
<div class="main-header">
<!-- Row 1 — 11 items -->
<div class="nav-row r1">
<a class="ni" tabindex="0" [matMenuTriggerFor]="institutionMenu">
<fa-icon icon="university" class="ni-ico"></fa-icon>Institution
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" routerLink="/checker-inbox-and-tasks/checker-inbox">
<fa-icon icon="check" class="ni-ico"></fa-icon>Tasks
</a>
<a class="ni" tabindex="0" routerLink="/accounting">
<fa-icon icon="money-bill-alt" class="ni-ico"></fa-icon>Accounting
</a>
<a class="ni" tabindex="0" [matMenuTriggerFor]="reportsMenu">
<fa-icon icon="chart-bar" class="ni-ico"></fa-icon>Reports
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" [matMenuTriggerFor]="adminMenu">
<fa-icon icon="shield-alt" class="ni-ico"></fa-icon>Admin
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" (click)="subLedgerOpen = !subLedgerOpen; glMenuOpen = false">
<fa-icon icon="book" class="ni-ico"></fa-icon>Sub Ledger
<fa-icon [icon]="subLedgerOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" (click)="glMenuOpen = !glMenuOpen; subLedgerOpen = false">
<fa-icon icon="book-open" class="ni-ico"></fa-icon>General Ledger
<fa-icon [icon]="glMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" [matMenuTriggerFor]="cashierMenu">
<fa-icon icon="money-bill" class="ni-ico"></fa-icon>Cashier
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0">
<fa-icon icon="user-tie" class="ni-ico"></fa-icon>Officer
</a>
<a class="ni" tabindex="0" [matMenuTriggerFor]="depositsMenu">
<fa-icon icon="piggy-bank" class="ni-ico"></fa-icon>Deposits
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" [matMenuTriggerFor]="clearingMenu">
<fa-icon icon="exchange" class="ni-ico"></fa-icon>Clearing
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
</div>
<!-- Sub Menu Bar — Row 2 (8 items) -->
<div class="sub-menu-bar sub-menu-row-2">
<a class="sub-menu-link" tabindex="0" [matMenuTriggerFor]="cashierMenu">
<fa-icon icon="money-bill" class="sub-menu-icon" size="lg"></fa-icon>Cashier
<fa-icon icon="caret-down" class="sub-menu-caret"></fa-icon>
</a>
<a class="sub-menu-link" tabindex="0">
<fa-icon icon="user-tie" class="sub-menu-icon" size="lg"></fa-icon>Officer
</a>
<a class="sub-menu-link" tabindex="0" [matMenuTriggerFor]="depositsMenu">
<fa-icon icon="piggy-bank" class="sub-menu-icon" size="lg"></fa-icon>Deposits
<fa-icon icon="caret-down" class="sub-menu-caret"></fa-icon>
</a>
<a class="sub-menu-link" tabindex="0" [matMenuTriggerFor]="clearingMenu">
<fa-icon icon="exchange" class="sub-menu-icon" size="lg"></fa-icon>Clearing
<fa-icon icon="caret-down" class="sub-menu-caret"></fa-icon>
</a>
<a class="sub-menu-link" tabindex="0" [matMenuTriggerFor]="hrmMenu">
<fa-icon icon="money-check" class="sub-menu-icon" size="lg"></fa-icon>HRM
<fa-icon icon="caret-down" class="sub-menu-caret"></fa-icon>
</a>
<a class="sub-menu-link" tabindex="0" [matMenuTriggerFor]="shareMenu">
<fa-icon icon="coins" class="sub-menu-icon" size="lg"></fa-icon>Share
<fa-icon icon="caret-down" class="sub-menu-caret"></fa-icon>
</a>
<a class="sub-menu-link" tabindex="0" [matMenuTriggerFor]="othersMenu">
<fa-icon icon="folder-open" class="sub-menu-icon" size="lg"></fa-icon>Others
<fa-icon icon="caret-down" class="sub-menu-caret"></fa-icon>
</a>
<a class="sub-menu-link" tabindex="0" [matMenuTriggerFor]="npaMenu">
<fa-icon icon="exclamation-triangle" class="sub-menu-icon" size="lg"></fa-icon>NPA
<fa-icon icon="caret-down" class="sub-menu-caret"></fa-icon>
</a>
</div>
<!-- Sub Menu Bar — Row 3 (8 items) -->
<div class="sub-menu-bar sub-menu-row-3">
<a class="sub-menu-link" tabindex="0" [matMenuTriggerFor]="advancesMenu">
<fa-icon icon="hand-holding-usd" class="sub-menu-icon" size="lg"></fa-icon>Advances
<fa-icon icon="caret-down" class="sub-menu-caret"></fa-icon>
</a>
<a class="sub-menu-link" tabindex="0" [matMenuTriggerFor]="insuranceMenu">
<fa-icon icon="shield-alt" class="sub-menu-icon" size="lg"></fa-icon>Insurance
<fa-icon icon="caret-down" class="sub-menu-caret"></fa-icon>
</a>
<a class="sub-menu-link" tabindex="0" (click)="rbiMenuOpen = !rbiMenuOpen; glMenuOpen = false">
<fa-icon icon="university" class="sub-menu-icon" size="lg"></fa-icon>RBI
<fa-icon [icon]="rbiMenuOpen ? 'caret-up' : 'caret-down'" class="sub-menu-caret"></fa-icon>
</a>
<a class="sub-menu-link" tabindex="0" [matMenuTriggerFor]="digitalMenu">
<fa-icon icon="mobile-alt" class="sub-menu-icon" size="lg"></fa-icon>Digital
<fa-icon icon="caret-down" class="sub-menu-caret"></fa-icon>
</a>
<a class="sub-menu-link" tabindex="0" (click)="openConfigWizard()">
<fa-icon icon="info" class="sub-menu-icon" size="lg"></fa-icon>Config Wizard
</a>
<a class="sub-menu-link" tabindex="0" [matMenuTriggerFor]="ckycMenu">
<fa-icon icon="fingerprint" class="sub-menu-icon" size="lg"></fa-icon>CKYC Mgmt
<fa-icon icon="caret-down" class="sub-menu-caret"></fa-icon>
</a>
<a class="sub-menu-link" tabindex="0" [matMenuTriggerFor]="letterMgmtMenu">
<fa-icon icon="envelope" class="sub-menu-icon" size="lg"></fa-icon>Letter Mgmt
<fa-icon icon="caret-down" class="sub-menu-caret"></fa-icon>
</a>
<span class="sub-menu-link sub-menu-spacer"></span>
<!-- Row 2 — 11 items -->
<div class="nav-row r2">
<a class="ni" tabindex="0" [matMenuTriggerFor]="hrmMenu">
<fa-icon icon="money-check" class="ni-ico"></fa-icon>HRM
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" [matMenuTriggerFor]="shareMenu">
<fa-icon icon="coins" class="ni-ico"></fa-icon>Share
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" [matMenuTriggerFor]="othersMenu">
<fa-icon icon="folder-open" class="ni-ico"></fa-icon>Others
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" [matMenuTriggerFor]="npaMenu">
<fa-icon icon="exclamation-triangle" class="ni-ico"></fa-icon>NPA
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" [matMenuTriggerFor]="advancesMenu">
<fa-icon icon="hand-holding-usd" class="ni-ico"></fa-icon>Advances
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" [matMenuTriggerFor]="insuranceMenu">
<fa-icon icon="shield-alt" class="ni-ico"></fa-icon>Insurance
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" (click)="rbiMenuOpen = !rbiMenuOpen; glMenuOpen = false">
<fa-icon icon="university" class="ni-ico"></fa-icon>RBI
<fa-icon [icon]="rbiMenuOpen ? 'caret-up' : 'caret-down'" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" [matMenuTriggerFor]="digitalMenu">
<fa-icon icon="mobile-alt" class="ni-ico"></fa-icon>Digital
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" (click)="openConfigWizard()">
<fa-icon icon="cog" class="ni-ico"></fa-icon>Config Wizard
</a>
<a class="ni" tabindex="0" [matMenuTriggerFor]="ckycMenu">
<fa-icon icon="fingerprint" class="ni-ico"></fa-icon>CKYC Mgmt
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
<a class="ni" tabindex="0" [matMenuTriggerFor]="letterMgmtMenu">
<fa-icon icon="envelope" class="ni-ico"></fa-icon>Letter Mgmt
<fa-icon icon="caret-down" class="ni-caret"></fa-icon>
</a>
</div>
</div>
<!-- Row 1 Menus -->
@@ -1039,8 +1054,6 @@
</div>
}
<!-- Breadcrumb -->
<mifosx-breadcrumb></mifosx-breadcrumb>
<!-- Content -->
<mifosx-content></mifosx-content>
<!-- Footer -->
+144 -45
View File
@@ -13,18 +13,73 @@
.sidebar-panel {
min-height: 100vh;
overflow: auto;
box-shadow:
0 3px 1px -2px rgb(0 0 0 / 20%),
0 2px 2px 0 rgb(0 0 0 / 14%),
0 1px 5px 0 rgb(0 0 0 / 12%);
background: #fff;
border-right: 1px solid #e2e8f0;
box-shadow: 1px 0 4px rgb(0 0 0 / 5%);
transition: width 0.28s cubic-bezier(0.4, 0, 0.2, 1);
// Custom thin scrollbar
scrollbar-width: thin;
scrollbar-color: #cbd5e1 transparent;
&::-webkit-scrollbar {
width: 4px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background: #cbd5e1;
border-radius: 4px;
&:hover {
background: #94a3b8;
}
}
}
.sidebar-full {
width: 15rem;
width: 240px;
}
.sidebar-compact {
width: 4rem;
width: 64px;
}
}
// ===== Sidebar Toggle Handle =====
.sidebar-handle {
position: fixed;
left: 228px;
top: 50%;
transform: translateY(-50%);
z-index: 1000;
width: 28px;
height: 48px;
background: #1a6ef5;
border-radius: 0 10px 10px 0;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
transition: all 0.28s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: 3px 0 12px rgb(0 0 0 / 20%);
svg {
width: 10px;
height: 16px;
}
&:hover {
width: 34px;
background: #1558cc;
box-shadow: 4px 0 16px rgb(0 0 0 / 25%);
}
&.collapsed {
left: 52px;
}
}
@@ -37,54 +92,98 @@ $color: #e2e4ec;
position: relative;
}
.sub-menu-bar {
display: flex;
background-color: #1074b9;
padding: 0 100px 0 48px;
width: 100%;
z-index: 2;
position: relative;
font-family: Roboto, 'Helvetica Neue', sans-serif;
font-size: 13px;
font-weight: 500;
letter-spacing: normal;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
box-sizing: border-box;
// ===== 2-Row Main Header =====
$brand: #1353a0;
$brand-light: #eaf1fc;
$brand-hover: #ddeafb;
$gray-200: #e2e8f0;
$gray-700: #334155;
.main-header {
background: #fff;
border-bottom: 2.5px solid $brand;
box-shadow: 0 1px 4px rgb(0 0 0 / 7%);
position: sticky;
top: 0;
z-index: 900;
overflow: visible;
}
.sub-menu-link {
display: inline-flex;
align-items: center;
flex: 1;
padding: 11px 2px;
text-decoration: none;
font-weight: 500;
font-size: 14px;
color: rgb(255 255 255 / 70%);
white-space: nowrap;
cursor: pointer;
flex-shrink: 0;
line-height: 1;
.nav-row {
height: 38px;
display: flex;
align-items: stretch;
justify-content: space-between;
overflow: visible;
position: relative;
}
&:hover {
color: #fff;
.r1 {
background: #fff;
border-bottom: 1px solid $gray-200;
}
.r2 {
background: #f8fafc;
}
// ===== Nav Item =====
.ni {
position: relative;
display: flex;
align-items: center;
gap: 4px;
flex: 1;
justify-content: center;
height: 100%;
cursor: pointer;
white-space: nowrap;
color: $gray-700;
font-size: 11px;
font-weight: 500;
letter-spacing: 0.01em;
transition: background 0.12s, color 0.12s;
user-select: none;
border-right: 1px solid $gray-200;
text-decoration: none;
font-family: 'DM Sans', 'Roboto', sans-serif;
&:last-child { border-right: none; }
&:hover { background: $brand-hover; color: $brand; }
&.active { background: $brand-light; color: $brand; font-weight: 600; }
&.active::before {
content: '';
position: absolute;
left: 0;
top: 18%;
bottom: 18%;
width: 2.5px;
background: $brand;
border-radius: 0 2px 2px 0;
}
}
.sub-menu-icon {
margin-right: 0.35rem;
.ni-ico {
font-size: 11px;
opacity: 0.55;
flex-shrink: 0;
.ni:hover &,
.ni.active & {
opacity: 1;
}
}
.sub-menu-caret {
margin-left: 0.35rem;
font-size: 12px;
}
.ni-caret {
font-size: 8px;
opacity: 0.3;
transition: transform 0.18s, opacity 0.18s;
flex-shrink: 0;
.sub-menu-spacer {
visibility: hidden;
pointer-events: none;
.ni:hover & {
opacity: 0.7;
}
}
.menu-item-icon {
+8 -1
View File
@@ -25,6 +25,7 @@ import { ContentComponent } from './content/content.component';
import { FooterComponent } from '../../shared/footer/footer.component';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatMenuModule } from '@angular/material/menu';
import { MatTooltipModule } from '@angular/material/tooltip';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
@@ -47,7 +48,8 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
FooterComponent,
AsyncPipe,
FaIconComponent,
MatMenuModule
MatMenuModule,
MatTooltipModule
]
})
export class ShellComponent implements OnInit, OnDestroy {
@@ -112,6 +114,11 @@ export class ShellComponent implements OnInit, OnDestroy {
this.cdr.detectChanges();
}
toggleSidebarCollapse(): void {
this.sidenavCollapsed = !this.sidenavCollapsed;
this.cdr.detectChanges();
}
openConfigWizard(): void {
// Placeholder — config wizard is handled by toolbar component
}
@@ -15,53 +15,11 @@
<div class="navigation-panel layout-column">
<!-- TODO: Allow customization by organization -->
<div class="app-brand" [routerLink]="['/home']">
<img src="assets/images/MifosX_logo.png" alt="app-logo" class="app-logo" />
<div class="app-logomark"><span></span><span></span><span></span><span></span></div>
<span #logo class="app-logo-text text-muted">{{ 'APP_NAME' | translate }}</span>
</div>
<div class="app-user">
<div class="app-user-photo">
<!-- TODO: Decision to be taken on providing this feature -->
<img src="assets/images/user_placeholder.png" alt="app-user-photo" />
</div>
<span class="app-user-name mb-05"> {{ tenantIdentifier }} / {{ username }} </span>
<div #userPanel class="app-user-controls">
<button
class="xs-button"
mat-icon-button
matTooltip="{{ 'tooltips.Settings' | translate }}"
[routerLink]="['/settings']"
>
<fa-icon icon="cog"></fa-icon>
</button>
<button class="xs-button" mat-icon-button matTooltip="{{ 'tooltips.Sign Out' | translate }}" (click)="logout()">
<fa-icon icon="sign-out-alt"></fa-icon>
</button>
</div>
</div>
<div class="app-sidenav">
<div #frequentlyAccessed>
<div class="mt-1 mb-1 nav-item-sep">
<mat-divider></mat-divider>
<span class="text-muted icon-menu-title">{{ 'labels.menus.Frequently Accessed' | translate }}</span>
</div>
</div>
<div class="icon-menu">
@for (activity of mappedActivities; track activity) {
<div class="icon-menu-item">
<button
mat-raised-button
[matTooltipPosition]="tooltipPosition"
[matTooltip]="activity.toolTip"
[routerLink]="[activity.path]"
>
<fa-icon [icon]="activity.icon" size="lg"></fa-icon>
</button>
</div>
}
</div>
<div class="nav-item-sep">
<mat-divider></mat-divider>
<span class="text-muted">{{ 'labels.menus.Main Items' | translate }}</span>
+194 -48
View File
@@ -6,55 +6,88 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
$brand: #1353a0;
$brand-light: #eaf1fc;
$bg: #fff;
$text: #334155;
$text-muted: #64748b;
$text-light: #94a3b8;
$active-bg: #eaf1fc;
$active-text: #1353a0;
$border: #e2e8f0;
$hover-bg: #f1f5f9;
// ===== Shared Full & Compact =====
.sidebar-full,
.sidebar-compact {
.navigation-panel {
width: 100%;
height: 100%;
padding-top: 20%;
padding-top: 0;
overflow-x: hidden;
background: $bg;
color: $text;
}
img {
max-width: 100%;
}
// ===== Brand / Logo =====
.app-brand {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
text-align: center;
max-width: 15rem;
z-index: 9999;
padding: 10px 10px 8px;
cursor: pointer;
border-bottom: 1px solid $border;
.app-logomark {
width: 22px;
height: 22px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2.5px;
margin-right: 8px;
flex-shrink: 0;
span {
border-radius: 2px;
&:nth-child(1) { background: $brand; }
&:nth-child(2) { background: #0ea5e9; }
&:nth-child(3) { background: #10b981; }
&:nth-child(4) { background: #cbd5e1; }
}
}
.app-logo {
width: auto;
height: 42px;
margin-right: 8px;
height: 28px;
margin-right: 6px;
}
.app-logo-text {
width: auto;
height: auto;
font-size: 2rem;
font-weight: normal;
font-size: 1.1rem;
font-weight: 600;
color: $text;
letter-spacing: 0.02em;
}
}
// ===== User Section =====
.app-user {
text-align: center;
width: 100%;
padding: 1rem 0 0.4rem;
padding: 10px 12px 8px;
border-bottom: 1px solid $border;
.app-user-photo {
width: 72px;
margin: 0 auto 8px;
height: 72px;
/* stylelint-disable-next-line color-function-notation */
border: 1px solid hsla(210deg, 8%, 51%, 13%);
padding: 4px;
width: 40px;
height: 40px;
margin: 0 auto 4px;
border: 2px solid $border;
padding: 2px;
border-radius: 50%;
}
@@ -66,48 +99,68 @@
.app-user-name {
display: block;
font-size: 0.875rem;
font-weight: 300;
margin-left: -2px;
font-size: 11px;
font-weight: 500;
color: $text-muted;
margin-bottom: 4px;
}
.app-user-controls {
.mat-icon-button {
.xs-button {
height: 20px;
width: 20px;
line-height: 20px;
margin: 5px;
display: flex;
align-items: center;
justify-content: center;
gap: 2px;
.xs-button,
button {
width: 28px;
height: 28px;
line-height: 28px;
color: $text-light;
transition: color 0.15s;
&:hover {
color: $brand;
}
}
.text-muted {
color: rgba(0 0 0 / 54%);
}
}
}
// ===== Section Separator =====
.nav-item-sep {
mat-divider {
border-color: $border;
}
span {
padding: 16px 0 0 16px;
padding: 8px 0 2px 14px;
display: block;
font-size: 12px;
font-size: 9px;
font-weight: 600;
letter-spacing: 0.08em;
text-transform: uppercase;
color: $text-light;
}
}
// ===== Sidenav Content =====
.app-sidenav {
.icon-menu {
display: flex;
flex-direction: column;
justify-content: space-evenly;
padding: 4px 8px;
.mat-divider {
margin: 0 -24px;
mat-divider {
border-color: $border;
margin: 0;
}
.icon-menu-title {
padding-left: 0;
margin-left: -8px;
color: $text-light;
font-size: 9px;
}
.icon-menu-item {
@@ -115,35 +168,106 @@
button {
min-width: auto;
margin: 4px;
margin: 2px;
background: $hover-bg;
color: $text-muted;
box-shadow: none;
border: 1px solid $border;
border-radius: 6px;
width: 36px;
height: 36px;
&:hover {
background: $active-bg;
color: $brand;
}
}
}
}
.nav-item-sep {
padding: 0;
margin: 1rem 0;
margin: 4px 0;
}
.mat-nav-list {
// ===== Nav List — Compact Items =====
mat-nav-list,
.mat-nav-list,
.mat-mdc-nav-list {
margin: 0;
padding: 0;
list-style: none;
padding: 2px 0;
.mat-list-item {
.mat-list-icon {
height: 30px;
font-size: 22px;
::ng-deep {
.mat-mdc-list-item,
.mdc-list-item {
height: 34px !important;
min-height: 34px !important;
padding: 0 12px !important;
border-radius: 6px;
margin: 1px 6px;
color: $text;
font-size: 12px;
transition: all 0.12s;
&:hover {
background: $hover-bg;
color: $brand;
}
&.active-menu {
background: $active-bg;
color: $active-text;
font-weight: 600;
.mat-icon,
mat-icon,
fa-icon,
i {
color: $active-text;
}
}
// Icon
.mat-icon,
mat-icon {
width: 18px;
height: 18px;
font-size: 14px;
margin-right: 8px;
color: $text-light;
display: flex;
align-items: center;
justify-content: center;
}
// Text
[matLine],
a {
font-size: 12px;
font-weight: 400;
color: inherit;
text-decoration: none;
line-height: 1.3;
}
}
}
}
}
}
// ===== Compact Mode Overrides =====
.sidebar-compact {
.app-brand {
padding: 10px 4px 8px;
flex-direction: column;
.app-logo {
margin: auto;
margin: 0 auto;
height: 24px;
}
.app-logomark {
margin: 0 auto;
}
.app-logo-text {
@@ -163,8 +287,8 @@
}
.icon-menu {
padding: 8px 0 0;
width: 4rem;
padding: 4px 0 0;
width: 64px;
.nav-item-sep {
display: none;
@@ -174,17 +298,39 @@
display: none;
}
}
::ng-deep {
.mat-mdc-list-item,
.mdc-list-item {
justify-content: center;
padding: 0 4px !important;
margin: 1px 4px;
[matLine],
a {
display: none !important;
}
.mat-icon,
mat-icon {
margin-right: 0;
}
}
}
}
}
// ===== Full Mode Overrides =====
.sidebar-full {
.navigation-panel {
padding-top: 5%;
padding-top: 0;
}
.app-sidenav {
.icon-menu {
flex-direction: row;
flex-wrap: wrap;
padding: 6px 10px;
}
}
}
+56 -159
View File
@@ -6,70 +6,43 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<mat-toolbar class="layout-row" color="primary" id="mifosx-toolbar">
@if (!sidenavCollapsed) {
<button
mat-icon-button
(click)="toggleSidenav()"
matTooltip="{{ 'tooltips.Toggle Hide/Open' | translate }}"
tabindex="0"
>
<fa-icon icon="bars" size="lg"></fa-icon>
</button>
}
@if (sidenav.opened) {
<button
mat-icon-button
(click)="toggleSidenavCollapse()"
matTooltip="{{ 'tooltips.Toggle Collapse' | translate }}"
class="hide-lt-md"
tabindex="0"
>
@if (!sidenavCollapsed) {
<fa-icon icon="chevron-left"></fa-icon>
}
@if (sidenavCollapsed) {
<fa-icon icon="chevron-right"></fa-icon>
}
</button>
}
<span class="toolbar-spacer"></span>
<div #notifications>
<mifosx-notifications-tray #notificationsTray class="hide-lt-md" tabindex="0"></mifosx-notifications-tray>
<!-- Top Identity Bar -->
<div class="top-bar" id="mifosx-toolbar">
<!-- Logo -->
<div class="t-logo">
<div class="t-logomark"><span></span><span></span><span></span><span></span></div>
<span class="t-logotext">CBS<em>Nexora</em></span>
</div>
<div #themeToggle>
<mifosx-theme-toggle tabindex="0"></mifosx-theme-toggle>
</div>
<div #appMenu>
<button mat-icon-button class="ml-1 img-button" [matMenuTriggerFor]="applicationMenu" tabindex="0">
<img src="assets/images/user_placeholder.png" [alt]="'labels.headings.User Profile' | translate" />
</button>
</div>
</mat-toolbar>
<mat-menu #institutionMenu="matMenu" [overlapTrigger]="false">
<button mat-menu-item routerLink="/clients" tabindex="0" *mifosxHasPermission="'READ_CLIENT'">
{{ 'labels.menus.Clients' | translate }}
</button>
<button mat-menu-item routerLink="/groups" tabindex="0" *mifosxHasPermission="'READ_GROUP'">
{{ 'labels.menus.Groups' | translate }}
</button>
<button mat-menu-item routerLink="/centers" tabindex="0" *mifosxHasPermission="'READ_CENTER'">
{{ 'labels.menus.Centers' | translate }}
</button>
<span class="hide-lt-md">
<button mat-menu-item [routerLink]="['/accounting']" tabindex="0">
{{ 'labels.menus.Accounting' | translate }}
</button>
<button mat-menu-item [matMenuTriggerFor]="reportsMenu" tabindex="0">
{{ 'labels.menus.Reports' | translate }}
</button>
<button mat-menu-item [matMenuTriggerFor]="adminMenu" tabindex="0">{{ 'labels.menus.Admin' | translate }}</button>
</span>
</mat-menu>
<!-- Meta Chips -->
<div class="t-meta">
<div class="t-chip"><div class="t-live"></div>&nbsp;LIVE</div>
<div class="t-chip">Branch:&nbsp;<strong>HO</strong></div>
<div class="t-chip">Date:&nbsp;<strong>{{ today | date:'dd MMM yyyy' }}</strong></div>
<div class="t-chip">Last login:&nbsp;<strong>{{ lastLoginTime }}</strong></div>
</div>
<!-- Right Actions -->
<div class="t-actions">
<div #notifications class="t-action-btn" matTooltip="Notifications">
<mifosx-notifications-tray #notificationsTray class="hide-lt-md" tabindex="0"></mifosx-notifications-tray>
</div>
<div class="t-action-btn" [routerLink]="['/settings']" matTooltip="{{ 'tooltips.Settings' | translate }}">
<fa-icon icon="cog"></fa-icon>
</div>
<div #themeToggle class="t-action-btn">
<mifosx-theme-toggle tabindex="0"></mifosx-theme-toggle>
</div>
<div class="t-divider"></div>
<div #appMenu class="t-action-btn t-user-btn" [matMenuTriggerFor]="applicationMenu" matTooltip="Account">
<img src="assets/images/user_placeholder.png" [alt]="'labels.headings.User Profile' | translate" class="t-user-img" />
</div>
</div>
</div>
<!-- Sub Ledger Mega Menu -->
@if (subLedgerOpen) {
@@ -85,11 +58,7 @@
<!-- Transactions -->
<div class="subledger-group">
<div class="subledger-group-title">Transactions</div>
<a
class="subledger-link"
[class.hidden]="!matchesSearch('Cash / Clearing / Transfer')"
(click)="subLedgerOpen = false"
>
<a class="subledger-link" [class.hidden]="!matchesSearch('Cash / Clearing / Transfer')" (click)="subLedgerOpen = false">
<fa-icon icon="money-bill" class="subledger-icon"></fa-icon> Cash / Clearing / Transfer
</a>
<a class="subledger-link" [class.hidden]="!matchesSearch('Batch Transaction')" (click)="subLedgerOpen = false">
@@ -137,12 +106,7 @@
<!-- Customer -->
<div class="subledger-group">
<div class="subledger-group-title">Customer</div>
<a
class="subledger-link"
[class.hidden]="!matchesSearch('Client Master')"
routerLink="/clients"
(click)="subLedgerOpen = false"
>
<a class="subledger-link" [class.hidden]="!matchesSearch('Client Master')" routerLink="/clients" (click)="subLedgerOpen = false">
<fa-icon icon="user" class="subledger-icon"></fa-icon> Client Master
</a>
<a class="subledger-link" [class.hidden]="!matchesSearch('Account Master')" (click)="subLedgerOpen = false">
@@ -159,18 +123,10 @@
<!-- Account Status -->
<div class="subledger-group">
<div class="subledger-group-title">Account Status</div>
<a
class="subledger-link"
[class.hidden]="!matchesSearch('Inoperative to Operative')"
(click)="subLedgerOpen = false"
>
<a class="subledger-link" [class.hidden]="!matchesSearch('Inoperative to Operative')" (click)="subLedgerOpen = false">
<fa-icon icon="lock-open" class="subledger-icon"></fa-icon> Inoperative &rarr; Operative
</a>
<a
class="subledger-link"
[class.hidden]="!matchesSearch('Account Status Master')"
(click)="subLedgerOpen = false"
>
<a class="subledger-link" [class.hidden]="!matchesSearch('Account Status Master')" (click)="subLedgerOpen = false">
<fa-icon icon="cog" class="subledger-icon"></fa-icon> Account Status Master
</a>
<a class="subledger-link" [class.hidden]="!matchesSearch('Restrict Account')" (click)="subLedgerOpen = false">
@@ -196,11 +152,7 @@
<a class="subledger-link" [class.hidden]="!matchesSearch('Holiday Marking')" (click)="subLedgerOpen = false">
<fa-icon icon="calendar" class="subledger-icon"></fa-icon> Holiday Marking
</a>
<a
class="subledger-link"
[class.hidden]="!matchesSearch('Open Closed Register')"
(click)="subLedgerOpen = false"
>
<a class="subledger-link" [class.hidden]="!matchesSearch('Open Closed Register')" (click)="subLedgerOpen = false">
<fa-icon icon="book-open" class="subledger-icon"></fa-icon> Open Closed Register
</a>
</div>
@@ -221,66 +173,27 @@
<div class="subledger-columns">
<div class="subledger-group">
<div class="subledger-group-title">Financial Statements</div>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Trial Balance')" (click)="glMenuOpen = false"
><fa-icon icon="balance-scale" class="subledger-icon"></fa-icon> Trial Balance</a
>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Profit and Loss')" (click)="glMenuOpen = false"
><fa-icon icon="chart-line" class="subledger-icon"></fa-icon> Profit and Loss</a
>
<a
class="subledger-link"
[class.hidden]="!matchesGlSearch('Consolidate Balance Sheet')"
(click)="glMenuOpen = false"
><fa-icon icon="file-alt" class="subledger-icon"></fa-icon> Consolidate Balance Sheet</a
>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Trial Balance')" (click)="glMenuOpen = false"><fa-icon icon="balance-scale" class="subledger-icon"></fa-icon> Trial Balance</a>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Profit and Loss')" (click)="glMenuOpen = false"><fa-icon icon="chart-line" class="subledger-icon"></fa-icon> Profit and Loss</a>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Consolidate Balance Sheet')" (click)="glMenuOpen = false"><fa-icon icon="file-alt" class="subledger-icon"></fa-icon> Consolidate Balance Sheet</a>
</div>
<div class="subledger-group">
<div class="subledger-group-title">GL Reports</div>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('GL Average Balance')" (click)="glMenuOpen = false"
><fa-icon icon="calculator" class="subledger-icon"></fa-icon> GL Average Balance</a
>
<a
class="subledger-link"
[class.hidden]="!matchesGlSearch('GL Mapping for Report')"
(click)="glMenuOpen = false"
><fa-icon icon="project-diagram" class="subledger-icon"></fa-icon> GL Mapping for Report</a
>
<a
class="subledger-link"
[class.hidden]="!matchesGlSearch('Account wise Balance List')"
(click)="glMenuOpen = false"
><fa-icon icon="list" class="subledger-icon"></fa-icon> Account wise Balance List</a
>
<a
class="subledger-link"
[class.hidden]="!matchesGlSearch('Distribution of Balance')"
(click)="glMenuOpen = false"
><fa-icon icon="chart-pie" class="subledger-icon"></fa-icon> Distribution of Balance</a
>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('GL Average Balance')" (click)="glMenuOpen = false"><fa-icon icon="calculator" class="subledger-icon"></fa-icon> GL Average Balance</a>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('GL Mapping for Report')" (click)="glMenuOpen = false"><fa-icon icon="project-diagram" class="subledger-icon"></fa-icon> GL Mapping for Report</a>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Account wise Balance List')" (click)="glMenuOpen = false"><fa-icon icon="list" class="subledger-icon"></fa-icon> Account wise Balance List</a>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Distribution of Balance')" (click)="glMenuOpen = false"><fa-icon icon="chart-pie" class="subledger-icon"></fa-icon> Distribution of Balance</a>
</div>
<div class="subledger-group">
<div class="subledger-group-title">Account Reports</div>
<a
class="subledger-link"
[class.hidden]="!matchesGlSearch('Client wise Balance Report')"
(click)="glMenuOpen = false"
><fa-icon icon="users" class="subledger-icon"></fa-icon> Client wise Balance Report</a
>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Contingent Expense')" (click)="glMenuOpen = false"
><fa-icon icon="exclamation-circle" class="subledger-icon"></fa-icon> Contingent Expense</a
>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Banker Head Report')" (click)="glMenuOpen = false"
><fa-icon icon="university" class="subledger-icon"></fa-icon> Banker Head Report</a
>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Client wise Balance Report')" (click)="glMenuOpen = false"><fa-icon icon="users" class="subledger-icon"></fa-icon> Client wise Balance Report</a>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Contingent Expense')" (click)="glMenuOpen = false"><fa-icon icon="exclamation-circle" class="subledger-icon"></fa-icon> Contingent Expense</a>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Banker Head Report')" (click)="glMenuOpen = false"><fa-icon icon="university" class="subledger-icon"></fa-icon> Banker Head Report</a>
</div>
<div class="subledger-group">
<div class="subledger-group-title">Registers</div>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Sundry Register')" (click)="glMenuOpen = false"
><fa-icon icon="clipboard-list" class="subledger-icon"></fa-icon> Sundry Register</a
>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Voucher Register')" (click)="glMenuOpen = false"
><fa-icon icon="receipt" class="subledger-icon"></fa-icon> Voucher Register</a
>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Sundry Register')" (click)="glMenuOpen = false"><fa-icon icon="clipboard-list" class="subledger-icon"></fa-icon> Sundry Register</a>
<a class="subledger-link" [class.hidden]="!matchesGlSearch('Voucher Register')" (click)="glMenuOpen = false"><fa-icon icon="receipt" class="subledger-icon"></fa-icon> Voucher Register</a>
</div>
</div>
</div>
@@ -423,11 +336,7 @@
<button mat-raised-button color="primary" (click)="popover.close(); showPopover(templateAdmin, admin)">
{{ 'labels.buttons.Back' | translate }}
</button>
<button
mat-raised-button
color="primary"
(click)="popover.close(); showPopover(templateConfigWizard, configWizard)"
>
<button mat-raised-button color="primary" (click)="popover.close(); showPopover(templateConfigWizard, configWizard)">
{{ 'labels.buttons.Next' | translate }}
</button>
</div>
@@ -443,11 +352,7 @@
<button mat-raised-button color="primary" (click)="popover.close(); showPopover(templateSelfService, selfService)">
{{ 'labels.buttons.Back' | translate }}
</button>
<button
mat-raised-button
color="primary"
(click)="popover.close(); showPopover(templateNotifications, notifications)"
>
<button mat-raised-button color="primary" (click)="popover.close(); showPopover(templateNotifications, notifications)">
{{ 'labels.buttons.Next' | translate }}
</button>
</div>
@@ -460,11 +365,7 @@
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">
{{ 'labels.buttons.Close' | translate }}
</button>
<button
mat-raised-button
color="primary"
(click)="popover.close(); showPopover(templateConfigWizard, configWizard)"
>
<button mat-raised-button color="primary" (click)="popover.close(); showPopover(templateConfigWizard, configWizard)">
{{ 'labels.buttons.Back' | translate }}
</button>
<button mat-raised-button color="primary" (click)="popover.close(); showPopover(templateThemePicker, themeToggle)">
@@ -480,11 +381,7 @@
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">
{{ 'labels.buttons.Close' | translate }}
</button>
<button
mat-raised-button
color="primary"
(click)="popover.close(); showPopover(templateNotifications, notifications)"
>
<button mat-raised-button color="primary" (click)="popover.close(); showPopover(templateNotifications, notifications)">
{{ 'labels.buttons.Back' | translate }}
</button>
<button mat-raised-button color="primary" (click)="popover.close(); showPopover(templateAppMenu, appMenu)">
+266 -72
View File
@@ -6,72 +6,240 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
#mifosx-toolbar {
position: absolute;
top: 0;
left: 0;
right: 0;
$brand-dark: #0d3d78;
$accent: #0ea5e9;
$accent-2: #10b981;
$top-h: 32px;
// ===== Top Identity Bar =====
.top-bar {
height: $top-h;
background: $brand-dark;
display: flex;
align-items: center;
position: relative;
overflow: hidden;
z-index: 10;
box-shadow: none;
background: transparent;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: repeating-linear-gradient(
108deg, transparent 0, transparent 80px,
rgb(255 255 255 / 2.2%) 80px, rgb(255 255 255 / 2.2%) 160px
);
pointer-events: none;
}
}
// ===== Logo =====
.t-logo {
display: flex;
align-items: center;
gap: 8px;
padding: 0 16px 0 14px;
border-right: 1px solid rgb(255 255 255 / 11%);
height: $top-h;
flex-shrink: 0;
}
.t-logomark {
width: 18px;
height: 18px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2.5px;
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%); }
}
}
.t-logotext {
font-size: 12.5px;
font-weight: 600;
letter-spacing: 0.055em;
color: #fff;
pointer-events: none;
min-height: 0;
height: 44px;
em {
font-style: normal;
color: #5ec9f8;
}
}
// ===== Meta Chips =====
.t-meta {
display: flex;
align-items: center;
flex: 1;
overflow: hidden;
padding: 0 4px;
}
* {
pointer-events: auto;
.t-chip {
display: flex;
align-items: center;
gap: 5px;
padding: 0 12px;
font-size: 10px;
font-family: 'DM Mono', 'Roboto Mono', monospace;
color: rgb(255 255 255 / 48%);
border-right: 1px solid rgb(255 255 255 / 8%);
white-space: nowrap;
height: $top-h;
flex-shrink: 0;
strong {
color: rgb(255 255 255 / 85%);
font-weight: 500;
}
}
.t-live {
width: 5px;
height: 5px;
border-radius: 50%;
background: $accent-2;
box-shadow: 0 0 0 2.5px rgb(16 185 129 / 25%);
}
// ===== Right Actions — Unified =====
$btn-size: 30px;
$icon-size: 14px;
$icon-color: rgb(255 255 255 / 55%);
$icon-hover: #fff;
$icon-hover-bg: rgb(255 255 255 / 10%);
.t-actions {
display: flex;
align-items: center;
margin-left: auto;
height: $top-h;
flex-shrink: 0;
gap: 2px;
padding-right: 8px;
}
.t-divider {
width: 1px;
height: 16px;
background: rgb(255 255 255 / 12%);
margin: 0 4px;
}
.t-action-btn {
width: $btn-size;
height: $btn-size;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: $icon-color;
border-radius: 6px;
transition: all 0.15s;
flex-shrink: 0;
&:hover {
color: $icon-hover;
background: $icon-hover-bg;
}
.mat-mdc-icon-button {
color: #fff;
// Direct fa-icon (Settings)
fa-icon {
font-size: $icon-size;
}
.img-button {
img {
width: 32px;
height: 32px;
border-radius: 50%;
// Notifications tray — force same size
::ng-deep mifosx-notifications-tray {
display: flex;
align-items: center;
justify-content: center;
.mat-mdc-icon-button {
width: $btn-size;
height: $btn-size;
padding: 0;
color: $icon-color;
line-height: $btn-size;
&:hover {
color: $icon-hover;
background: transparent; // parent handles hover bg
}
.mat-icon,
mat-icon {
font-size: $icon-size;
width: $icon-size;
height: $icon-size;
line-height: $icon-size;
}
}
.mat-badge-content {
font-size: 8px;
width: 14px;
height: 14px;
line-height: 14px;
top: 2px !important;
right: 2px !important;
}
}
.ml-1 {
margin-left: 0.5rem;
// Theme toggle — force same size
::ng-deep mifosx-theme-toggle {
display: flex;
align-items: center;
justify-content: center;
button {
width: $btn-size;
height: $btn-size;
padding: 0;
color: $icon-color;
line-height: $btn-size;
&:hover {
color: $icon-hover;
background: transparent;
}
mat-icon,
.mat-icon {
font-size: $icon-size;
width: $icon-size;
height: $icon-size;
line-height: $icon-size;
}
}
}
}
::host-context(.dark-theme) #mifosx-toolbar {
// transparent, inherits from sub-menu-bar background
// ===== User Button — Same size, round avatar =====
.t-user-btn {
width: $btn-size;
height: $btn-size;
padding: 0;
}
.toolbar-spacer {
flex: 1 1 auto;
.t-user-img {
width: 22px;
height: 22px;
border-radius: 50%;
object-fit: cover;
border: 1.5px solid rgb(255 255 255 / 20%);
}
/* Toolbar Deposit Mega Grid */
.toolbar-deposit-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 8px 24px;
padding: 8px 12px;
min-width: 600px;
}
.toolbar-deposit-group {
.toolbar-deposit-title {
font-weight: 700;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.6px;
color: #1074b9;
padding: 4px 16px 6px;
margin-bottom: 2px;
border-bottom: 2px solid #1074b9;
}
}
/* Sub Ledger Mega Menu */
// ===== Sub Ledger Mega Menu =====
.subledger-backdrop {
position: fixed;
top: 0;
@@ -82,31 +250,41 @@
}
.subledger-mega-menu {
position: absolute;
top: 64px;
position: fixed;
top: 92px;
left: 50%;
transform: translateX(-50%);
background: #fff;
width: 800px;
width: 860px;
max-width: 95vw;
padding: 16px 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
border-radius: 0 0 8px 8px;
padding: 20px 24px;
box-shadow: 0 6px 24px rgb(0 0 0 / 22%);
border-radius: 0 0 10px 10px;
z-index: 1000;
border-top: 3px solid #0d47a1;
animation: slideDown 0.13s ease;
}
@keyframes slideDown {
from { opacity: 0; transform: translateX(-50%) translateY(-4px); }
to { opacity: 1; transform: translateX(-50%) translateY(0); }
}
.subledger-search {
width: 100%;
padding: 8px 12px;
margin-bottom: 14px;
border: 1px solid #ccc;
border-radius: 4px;
padding: 10px 14px;
margin-bottom: 16px;
border: 1px solid #d0d5dd;
border-radius: 6px;
font-size: 14px;
box-sizing: border-box;
background: #f9fafb;
&:focus {
outline: none;
border-color: #1074b9;
border-color: #0d47a1;
background: #fff;
box-shadow: 0 0 0 3px rgb(13 71 161 / 10%);
}
}
@@ -117,30 +295,30 @@
}
.subledger-group-title {
font-weight: 600;
font-size: 13px;
font-weight: 700;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.5px;
color: #2c3e50;
margin-bottom: 8px;
padding-bottom: 4px;
border-bottom: 2px solid #1074b9;
letter-spacing: 0.8px;
color: #0d47a1;
margin-bottom: 10px;
padding-bottom: 6px;
border-bottom: 2px solid #0d47a1;
}
.subledger-link {
display: flex;
align-items: center;
padding: 5px 4px;
padding: 6px 6px;
text-decoration: none;
color: #333;
color: #344054;
font-size: 13px;
cursor: pointer;
border-radius: 4px;
transition: background 0.15s;
border-radius: 5px;
transition: background 0.15s, color 0.15s;
&:hover {
background: #e3f2fd;
color: #1074b9;
color: #0d47a1;
}
&.hidden {
@@ -151,6 +329,22 @@
.subledger-icon {
width: 18px;
margin-right: 8px;
color: #666;
color: #667085;
font-size: 13px;
.subledger-link:hover & {
color: #0d47a1;
}
}
// ===== Responsive =====
/* stylelint-disable-next-line media-feature-range-notation -- Safari compatibility */
@media (max-width: 768px) {
.t-meta {
display: none;
}
.t-logo {
padding: 0 10px;
}
}
@@ -102,6 +102,20 @@ export class ToolbarComponent implements OnInit, AfterViewInit, AfterContentChec
/** Sets the initial state of sidenav as collapsed. Not collapsed if false. */
sidenavCollapsed = true;
/** Current date for top bar display */
today = new Date();
/** Last login time display */
lastLoginTime = this.formatLastLogin();
private formatLastLogin(): string {
const now = new Date();
const h = now.getHours() % 12 || 12;
const m = now.getMinutes() < 10 ? '0' + now.getMinutes() : now.getMinutes();
const ampm = now.getHours() >= 12 ? 'PM' : 'AM';
return `Today, ${h}:${m} ${ampm}`;
}
/** Sub Ledger mega menu state */
subLedgerOpen = false;
subLedgerSearch = '';
@@ -0,0 +1,380 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="crar-dialog">
<!-- Header -->
<div class="dialog-header">
<div class="header-left">
<div class="header-badge">
<fa-icon icon="shield-alt"></fa-icon>
Basel III Capital Adequacy
</div>
<h2 class="dialog-title">CRAR &mdash; Basel III Capital Adequacy</h2>
<p class="dialog-sub">Capital to Risk-Weighted Assets Ratio &middot; RBI Master Circular</p>
</div>
<div class="header-right">
<div class="as-of-box">
<strong>Q3 FY 2025-26</strong>
<span>Reporting Quarter</span>
</div>
<a class="rbi-circular-btn" href="https://www.rbi.org.in/scripts/bs_circularindexdisplay.aspx" target="_blank" rel="noopener noreferrer" matTooltip="View RBI Circulars">
<fa-icon icon="external-link-alt"></fa-icon>
RBI Circulars
</a>
<button class="close-btn" (click)="close()" matTooltip="Close">
<fa-icon icon="times"></fa-icon>
</button>
</div>
</div>
<!-- Summary Cards -->
<div class="summary-row">
<div class="summary-card sc-t1c">
<div class="sc-label">Tier 1 Capital</div>
<div class="sc-value">&#8377;8,284<span class="cr">Cr</span></div>
<div class="sc-sub">CET1 + AT1</div>
</div>
<div class="summary-card sc-t2c">
<div class="sc-label">Tier 2 Capital</div>
<div class="sc-value">&#8377;1,556<span class="cr">Cr</span></div>
<div class="sc-sub">Supplementary Capital</div>
</div>
<div class="summary-card sc-total-cap">
<div class="sc-label">Total Capital</div>
<div class="sc-value">&#8377;9,840<span class="cr">Cr</span></div>
<div class="sc-sub">Tier 1 + Tier 2</div>
</div>
<div class="summary-card sc-rwa">
<div class="sc-label">Total RWA</div>
<div class="sc-value">&#8377;58,420<span class="cr">Cr</span></div>
<div class="sc-sub">Credit + Market + Op Risk</div>
</div>
<div class="summary-card sc-crar">
<div class="sc-label">CRAR</div>
<div class="sc-value">{{ crar }}%</div>
<div class="sc-sub">Min required: 10.50%</div>
</div>
<div class="summary-card sc-buffer">
<div class="sc-label">Capital Buffer</div>
<div class="sc-value">+{{ capitalBuffer }}%</div>
<div class="sc-change up">&#8593; Above RBI minimum</div>
</div>
</div>
<!-- Tab Navigation -->
<div class="tab-nav">
<button class="tab-btn" [class.active]="activeTab === 'tier1'" (click)="activeTab = 'tier1'">
<span class="tab-pill pill-t1c">01</span> Tier 1 Capital
</button>
<button class="tab-btn" [class.active]="activeTab === 'tier2'" (click)="activeTab = 'tier2'">
<span class="tab-pill pill-t2c">02</span> Tier 2 Capital
</button>
<button class="tab-btn" [class.active]="activeTab === 'creditRisk'" (click)="activeTab = 'creditRisk'">
<span class="tab-pill pill-cr">03</span> Credit Risk RWA
</button>
<button class="tab-btn" [class.active]="activeTab === 'marketRisk'" (click)="activeTab = 'marketRisk'">
<span class="tab-pill pill-mr">04</span> Market Risk RWA
</button>
<button class="tab-btn" [class.active]="activeTab === 'opRisk'" (click)="activeTab = 'opRisk'">
<span class="tab-pill pill-or">05</span> Operational Risk RWA
</button>
</div>
<!-- Tier 1 Capital Table -->
@if (activeTab === 'tier1') {
<div class="section-desc">Common Equity Tier 1 (CET1) + Additional Tier 1 (AT1) &mdash; Basel III Framework</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th class="sl">#</th>
<th>Capital Item</th>
<th>Account Code</th>
<th>Category</th>
<th class="right">Previous (&#8377; Cr)</th>
<th class="right">Current (&#8377; Cr)</th>
<th class="right">Change (&#8377; Cr)</th>
</tr>
</thead>
<tbody>
@for (item of tier1Capital; track item.sl) {
<tr>
<td class="sl">{{ item.sl }}</td>
<td>
<div class="item-name">{{ item.name }}</div>
<div class="item-sub">{{ item.sub }}</div>
</td>
<td class="mono">{{ item.code }}</td>
<td>
<span class="status-chip" [class.chip-cet1]="item.category === 'CET1'" [class.chip-at1]="item.category === 'AT1'">
{{ item.category }}
</span>
</td>
<td class="right amt amt-t1c">{{ item.prev }}</td>
<td class="right amt amt-t1c">{{ item.curr }}</td>
<td class="right" [class.up]="item.changeDir === 'up'" [class.dn]="item.changeDir === 'dn'">{{ item.change }}</td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="4"><strong>Total Tier 1 Capital (CET1 + AT1)</strong></td>
<td class="right foot-t1c">7,961.20</td>
<td class="right foot-t1c">8,284.80</td>
<td class="right up">+323.60</td>
</tr>
</tfoot>
</table>
</div>
}
<!-- Tier 2 Capital Table -->
@if (activeTab === 'tier2') {
<div class="section-desc">Supplementary Capital &mdash; Subordinated debt, provisions &amp; reserves</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th class="sl">#</th>
<th>Capital Item</th>
<th>Account Code</th>
<th class="right">Previous (&#8377; Cr)</th>
<th class="right">Current (&#8377; Cr)</th>
<th class="right">Change (&#8377; Cr)</th>
</tr>
</thead>
<tbody>
@for (item of tier2Capital; track item.sl) {
<tr>
<td class="sl">{{ item.sl }}</td>
<td>
<div class="item-name">{{ item.name }}</div>
<div class="item-sub">{{ item.sub }}</div>
</td>
<td class="mono">{{ item.code }}</td>
<td class="right amt amt-t2c">{{ item.prev }}</td>
<td class="right amt amt-t2c">{{ item.curr }}</td>
<td class="right" [class.up]="item.changeDir === 'up'" [class.dn]="item.changeDir === 'dn'">{{ item.change }}</td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="3"><strong>Total Tier 2 Capital</strong></td>
<td class="right foot-t2c">1,484.60</td>
<td class="right foot-t2c">1,556.00</td>
<td class="right up">+71.40</td>
</tr>
</tfoot>
</table>
</div>
}
<!-- Credit Risk RWA Table -->
@if (activeTab === 'creditRisk') {
<div class="section-desc">Risk-Weighted Assets for Credit Risk &mdash; Standardised Approach (RBI)</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th class="sl">#</th>
<th>Exposure Category</th>
<th>Account Code</th>
<th>Risk Weight</th>
<th class="right">Gross Exposure (&#8377; Cr)</th>
<th class="right">RWA (&#8377; Cr)</th>
</tr>
</thead>
<tbody>
@for (item of creditRiskRWA; track item.sl) {
<tr>
<td class="sl">{{ item.sl }}</td>
<td>
<div class="item-name">{{ item.name }}</div>
<div class="item-sub">{{ item.sub }}</div>
</td>
<td class="mono">{{ item.code }}</td>
<td><span class="status-chip chip-rw">{{ item.rw }}</span></td>
<td class="right amt amt-cr">{{ item.gross }}</td>
<td class="right amt amt-cr">{{ item.rwa }}</td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="4"><strong>Total Credit Risk RWA</strong></td>
<td class="right"></td>
<td class="right foot-cr">47,820.40</td>
</tr>
</tfoot>
</table>
</div>
}
<!-- Market Risk RWA Table -->
@if (activeTab === 'marketRisk') {
<div class="section-desc">Risk-Weighted Assets for Market Risk &mdash; Standardised Duration Approach</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th class="sl">#</th>
<th>Risk Component</th>
<th>Account Code</th>
<th>Approach</th>
<th class="right">Capital Charge (&#8377; Cr)</th>
<th class="right">RWA (&#8377; Cr)</th>
</tr>
</thead>
<tbody>
@for (item of marketRiskRWA; track item.sl) {
<tr>
<td class="sl">{{ item.sl }}</td>
<td>
<div class="item-name">{{ item.name }}</div>
<div class="item-sub">{{ item.sub }}</div>
</td>
<td class="mono">{{ item.code }}</td>
<td><span class="status-chip chip-approach">{{ item.approach }}</span></td>
<td class="right amt amt-mr">{{ item.cc }}</td>
<td class="right amt amt-mr">{{ item.rwa }}</td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="4"><strong>Total Market Risk RWA</strong></td>
<td class="right"></td>
<td class="right foot-mr">7,005.00</td>
</tr>
</tfoot>
</table>
</div>
}
<!-- Operational Risk RWA Table -->
@if (activeTab === 'opRisk') {
<div class="section-desc">Basic Indicator Approach (BIA) &mdash; RWA = Capital Charge &times; 12.5</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th class="sl">#</th>
<th>Parameter</th>
<th class="right">Value</th>
</tr>
</thead>
<tbody>
<tr>
<td class="sl">01</td>
<td>
<div class="item-name">Gross Income Average (3-year)</div>
<div class="item-sub">Average of last 3 years positive gross income</div>
</td>
<td class="right amt amt-or">&#8377;{{ opRiskGrossIncome | number:'1.2-2' }} Cr</td>
</tr>
<tr>
<td class="sl">02</td>
<td>
<div class="item-name">Alpha Factor</div>
<div class="item-sub">Fixed multiplier as per Basel III BIA</div>
</td>
<td class="right amt amt-or">{{ opRiskAlpha }}%</td>
</tr>
<tr>
<td class="sl">03</td>
<td>
<div class="item-name">Capital Charge</div>
<div class="item-sub">Gross Income &times; Alpha</div>
</td>
<td class="right amt amt-or">&#8377;{{ opRiskCapitalCharge | number:'1.2-2' }} Cr</td>
</tr>
<tr>
<td class="sl">04</td>
<td>
<div class="item-name">RWA (Capital Charge &times; 12.5)</div>
<div class="item-sub">{{ opRiskCapitalCharge }} &times; 12.5 = {{ opRiskRWA }}</div>
</td>
<td class="right amt amt-or">&#8377;{{ opRiskRWA | number:'1.2-2' }} Cr</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="2"><strong>Total Operational Risk RWA</strong></td>
<td class="right foot-or">3,595.60</td>
</tr>
</tfoot>
</table>
</div>
}
<!-- CRAR Formula Block -->
<div class="formula-block crar-formula">
<div class="formula-title">CRAR Computation &mdash; (Tier 1 + Tier 2) &divide; Total RWA &times; 100</div>
<div class="formula-row">
<div class="formula-term ft-t1c">
<div class="formula-term-label">Tier 1 Capital</div>
<div class="formula-term-val">&#8377;{{ totalTier1 | number:'1.2-2' }}</div>
<div class="formula-term-sub">CET1 + AT1</div>
</div>
<div class="formula-op">+</div>
<div class="formula-term ft-t2c">
<div class="formula-term-label">Tier 2 Capital</div>
<div class="formula-term-val">&#8377;{{ totalTier2 | number:'1.2-2' }}</div>
<div class="formula-term-sub">Supplementary</div>
</div>
<div class="formula-op">&divide;</div>
<div class="formula-term ft-rwa">
<div class="formula-term-label">Total RWA</div>
<div class="formula-term-val">&#8377;{{ totalRWA | number:'1.0-0' }}</div>
<div class="formula-term-sub">Credit + Market + Op</div>
</div>
<div class="formula-op">&times;</div>
<div class="formula-term" style="border-color:#e2e8f0;background:#f8fafc;">
<div class="formula-term-label" style="color:#64748b;">100</div>
<div class="formula-term-val" style="color:#334155;">100</div>
<div class="formula-term-sub">&nbsp;</div>
</div>
<div class="formula-op equals">=</div>
<div class="formula-term ft-crar-result">
<div class="formula-term-label">CRAR</div>
<div class="formula-term-val">{{ crar }}%</div>
<div class="formula-term-sub">Min required: 10.50%</div>
</div>
</div>
</div>
<!-- Basel III Compliance Bar -->
<div class="compliance-bar">
<div class="compliance-left">
<div class="compliance-title">Basel III Compliance Status</div>
<div class="compliance-desc">All thresholds met &middot; CRAR: {{ crar }}% &middot; CET1: {{ cet1Ratio }}% &middot; Tier 1: {{ tier1Ratio }}% &middot; Tier 2: {{ tier2Ratio }}%</div>
</div>
<div class="compliance-calcs">
<div class="compliance-calc">
<div class="compliance-calc-label">CET1 Ratio</div>
<div class="compliance-calc-val">{{ cet1Ratio }}%</div>
</div>
<div class="compliance-calc">
<div class="compliance-calc-label">Tier 1 Ratio</div>
<div class="compliance-calc-val">{{ tier1Ratio }}%</div>
</div>
<div class="compliance-calc">
<div class="compliance-calc-label">CRAR</div>
<div class="compliance-calc-val surplus">{{ crar }}%</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="dialog-footer">
<span>CBSNexora &middot; Basel III Capital Adequacy</span>
<span>RBI Master Circular &mdash; CRAR 2025-26</span>
<button mat-button (click)="close()">Close</button>
</div>
</div>
@@ -0,0 +1,657 @@
/**
* 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/.
*/
$t1c: #1d4ed8;
$t1c-bg: #eff6ff;
$t1c-b: #bfdbfe;
$t2c: #0f766e;
$t2c-bg: #f0fdfa;
$t2c-b: #99f6e4;
$rwa: #7c3aed;
$rwa-bg: #f5f3ff;
$rwa-b: #ddd6fe;
$cr: #b45309;
$cr-bg: #fffbeb;
$cr-b: #fde68a;
$mr: #be185d;
$mr-bg: #fdf2f8;
$mr-b: #fbcfe8;
$or: #0369a1;
$or-bg: #f0f9ff;
$or-b: #bae6fd;
$crar: #059669;
$crar-bg: #ecfdf5;
$crar-b: #a7f3d0;
$ink: #0f172a;
$ink2: #334155;
$ink3: #64748b;
$ink4: #94a3b8;
$border: #e2e8f0;
$border2: #cbd5e1;
$surface: #fff;
$surface2: #f8fafc;
$red: #dc2626;
$green: #16a34a;
$amber: #d97706;
$mono: 'IBM Plex Mono', 'Roboto Mono', monospace;
.crar-dialog {
max-height: 85vh;
overflow-y: auto;
padding: 0;
font-size: 14px;
line-height: 1.6;
color: $ink;
}
// ===== Header =====
.dialog-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 24px 28px 20px;
border-bottom: 1px solid $border;
gap: 16px;
flex-wrap: wrap;
position: sticky;
top: 0;
background: $surface;
z-index: 10;
}
.header-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 10px;
font-weight: 600;
padding: 3px 10px;
border-radius: 4px;
background: $crar-bg;
color: $crar;
border: 1px solid $crar-b;
letter-spacing: 0.5px;
margin-bottom: 8px;
font-family: $mono;
}
.dialog-title {
font-size: 20px;
font-weight: 600;
color: $ink;
margin: 0 0 4px;
}
.dialog-sub {
font-size: 12px;
color: $ink3;
font-family: $mono;
letter-spacing: 0.5px;
margin: 0;
}
.header-right {
display: flex;
align-items: flex-start;
gap: 12px;
}
.as-of-box {
font-size: 11px;
color: $ink3;
font-family: $mono;
text-align: right;
background: $surface2;
border: 1px solid $border;
padding: 8px 14px;
border-radius: 8px;
strong {
color: $ink2;
display: block;
font-size: 13px;
margin-bottom: 2px;
}
}
.rbi-circular-btn {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
font-family: $mono;
font-weight: 500;
padding: 7px 14px;
border-radius: 8px;
background: #fff7ed;
color: #c2410c;
border: 1px solid #fed7aa;
text-decoration: none;
transition: all 0.15s;
white-space: nowrap;
&:hover {
background: #ffedd5;
border-color: #fb923c;
color: #9a3412;
}
fa-icon {
font-size: 10px;
}
}
.close-btn {
width: 32px;
height: 32px;
border-radius: 8px;
background: $surface2;
border: 1px solid $border;
cursor: pointer;
font-size: 16px;
color: $ink3;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
&:hover {
background: #fef2f2;
color: $red;
border-color: #fecaca;
}
}
// ===== Summary Cards =====
.summary-row {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 14px;
padding: 20px 28px;
}
.summary-card {
background: $surface;
border: 1px solid $border;
border-radius: 12px;
padding: 16px 18px;
position: relative;
overflow: hidden;
transition: box-shadow 0.2s;
&:hover {
box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
}
}
.sc-t1c::before { background: $t1c; }
.sc-t2c::before { background: $t2c; }
.sc-total-cap::before { background: linear-gradient(90deg, $t1c, $t2c); }
.sc-rwa::before { background: $rwa; }
.sc-crar::before { background: $crar; }
.sc-buffer::before { background: $green; }
.sc-label {
font-size: 10px;
color: $ink3;
font-family: $mono;
letter-spacing: 1px;
text-transform: uppercase;
margin-bottom: 6px;
}
.sc-value {
font-size: 20px;
font-weight: 600;
color: $ink;
font-family: $mono;
}
.cr {
font-size: 11px;
color: $ink3;
font-weight: 400;
margin-left: 3px;
}
.sc-change {
font-size: 11px;
margin-top: 4px;
}
.sc-sub {
font-size: 11px;
color: $ink3;
margin-top: 4px;
}
.up { color: $green; }
.dn { color: $red; }
// ===== Tab Navigation =====
.tab-nav {
display: flex;
gap: 8px;
padding: 0 28px 16px;
border-bottom: 1px solid $border;
}
.tab-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 8px;
border: 1px solid $border;
background: $surface;
color: $ink2;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
&:hover {
border-color: $border2;
background: $surface2;
}
&.active {
background: $ink;
color: #fff;
border-color: $ink;
.tab-pill {
background: rgb(255 255 255 / 20%);
color: #fff;
border-color: transparent;
}
}
}
.tab-pill {
font-size: 10px;
font-family: $mono;
font-weight: 600;
padding: 2px 8px;
border-radius: 12px;
letter-spacing: 0.5px;
}
.pill-t1c { background: $t1c-bg; color: $t1c; border: 1px solid $t1c-b; }
.pill-t2c { background: $t2c-bg; color: $t2c; border: 1px solid $t2c-b; }
.pill-cr { background: $cr-bg; color: $cr; border: 1px solid $cr-b; }
.pill-mr { background: $mr-bg; color: $mr; border: 1px solid $mr-b; }
.pill-or { background: $or-bg; color: $or; border: 1px solid $or-b; }
.section-desc {
font-size: 12px;
color: $ink3;
font-style: italic;
padding: 12px 28px 8px;
}
// ===== Table =====
.table-wrap {
margin: 0 28px 20px;
background: $surface;
border: 1px solid $border;
border-radius: 12px;
overflow: hidden;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
thead tr {
background: $surface2;
border-bottom: 1px solid $border;
}
thead th {
padding: 10px 14px;
text-align: left;
font-size: 10px;
font-family: $mono;
font-weight: 500;
color: $ink3;
letter-spacing: 1px;
text-transform: uppercase;
white-space: nowrap;
&.right { text-align: right; }
}
tbody tr {
border-bottom: 1px solid $border;
transition: background 0.15s;
&:last-child { border-bottom: none; }
&:hover { background: #f8fafc; }
}
td {
padding: 10px 14px;
color: $ink2;
vertical-align: middle;
&.right { text-align: right; font-family: $mono; }
}
.sl {
color: $ink4;
font-family: $mono;
font-size: 11px;
width: 36px;
}
.mono { font-family: $mono; font-size: 12px; }
.item-name {
font-weight: 500;
color: $ink;
}
.item-sub {
font-size: 11px;
color: $ink3;
margin-top: 2px;
}
.amt {
font-family: $mono;
font-weight: 500;
}
.amt-t1c { color: $t1c; }
.amt-t2c { color: $t2c; }
.amt-cr { color: $cr; }
.amt-mr { color: $mr; }
.amt-or { color: $or; }
.amt-crar { color: $crar; }
.status-chip {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10px;
font-family: $mono;
font-weight: 500;
padding: 2px 8px;
border-radius: 4px;
}
.chip-cet1 { background: $t1c-bg; color: $t1c; border: 1px solid $t1c-b; }
.chip-at1 { background: $rwa-bg; color: $rwa; border: 1px solid $rwa-b; }
.chip-rw { background: $cr-bg; color: $cr; border: 1px solid $cr-b; }
.chip-approach { background: $mr-bg; color: $mr; border: 1px solid $mr-b; }
.chip-active { background: #dcfce7; color: #15803d; }
.chip-t1c { background: $t1c-bg; color: $t1c; border: 1px solid $t1c-b; }
.chip-t2c { background: $t2c-bg; color: $t2c; border: 1px solid $t2c-b; }
.chip-cr { background: $cr-bg; color: $cr; border: 1px solid $cr-b; }
.chip-mr { background: $mr-bg; color: $mr; border: 1px solid $mr-b; }
.chip-or { background: $or-bg; color: $or; border: 1px solid $or-b; }
.chip-crar { background: $crar-bg; color: $crar; border: 1px solid $crar-b; }
tfoot tr {
background: $surface2;
border-top: 2px solid $border2;
}
tfoot td {
font-weight: 600;
color: $ink;
font-family: $mono;
padding: 12px 14px;
&.right { text-align: right; font-size: 14px; }
}
.foot-t1c { color: $t1c; }
.foot-t2c { color: $t2c; }
.foot-cr { color: $cr; }
.foot-mr { color: $mr; }
.foot-or { color: $or; }
.foot-crar { color: $crar; }
// ===== Formula Block =====
.formula-block {
margin: 20px 28px;
background: $surface;
border: 1px solid $border;
border-radius: 14px;
padding: 20px 24px;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, $t1c, $t2c, $rwa);
}
}
.crar-formula::before {
background: linear-gradient(90deg, $t1c, $t2c, $crar) !important;
}
.formula-title {
font-size: 11px;
font-family: $mono;
color: $ink3;
letter-spacing: 2px;
text-transform: uppercase;
margin-bottom: 16px;
}
.formula-row {
display: flex;
align-items: center;
gap: 0;
flex-wrap: wrap;
}
.formula-term {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 16px;
border-radius: 10px;
border: 1px solid $border;
background: $surface2;
min-width: 130px;
flex: 1;
}
.formula-term-label {
font-size: 9.5px;
font-family: $mono;
letter-spacing: 1.5px;
text-transform: uppercase;
margin-bottom: 4px;
}
.formula-term-val {
font-size: 16px;
font-weight: 600;
font-family: $mono;
}
.formula-term-sub {
font-size: 9px;
color: $ink4;
font-family: $mono;
margin-top: 2px;
}
.formula-op {
font-size: 20px;
font-weight: 300;
color: $ink3;
padding: 0 8px;
flex-shrink: 0;
&.minus { color: $red; }
&.equals { color: $ink2; font-weight: 500; }
}
.ft-t1c {
border-color: $t1c-b;
background: $t1c-bg;
.formula-term-label, .formula-term-val { color: $t1c; }
}
.ft-t2c {
border-color: $t2c-b;
background: $t2c-bg;
.formula-term-label, .formula-term-val { color: $t2c; }
}
.ft-rwa {
border-color: $rwa-b;
background: $rwa-bg;
.formula-term-label, .formula-term-val { color: $rwa; }
}
.ft-crar-result {
border-color: $crar-b;
background: linear-gradient(135deg, #f0fdf4, #ecfdf5);
.formula-term-label { color: #15803d; }
.formula-term-val { color: $crar; font-size: 20px; }
}
// ===== Compliance Bar =====
.compliance-bar {
margin: 0 28px 20px;
background: linear-gradient(135deg, #ecfdf5, #f0fdf4);
border: 1px solid #bbf7d0;
border-radius: 10px;
padding: 16px 24px;
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
.compliance-left { flex: 1; }
.compliance-title {
font-size: 13px;
font-weight: 600;
color: #14532d;
margin-bottom: 3px;
}
.compliance-desc {
font-size: 12px;
color: #166534;
}
.compliance-calcs {
display: flex;
gap: 24px;
}
.compliance-calc { text-align: right; }
.compliance-calc-label {
font-size: 10px;
color: #166534;
font-family: $mono;
letter-spacing: 1px;
}
.compliance-calc-val {
font-size: 15px;
font-weight: 600;
color: #14532d;
font-family: $mono;
&.surplus { color: $crar; }
}
// ===== Footer =====
.dialog-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 28px;
border-top: 1px solid $border;
font-size: 11px;
color: $ink4;
font-family: $mono;
background: $surface2;
flex-wrap: wrap;
gap: 8px;
position: sticky;
bottom: 0;
}
// ===== Responsive =====
/* stylelint-disable-next-line media-feature-range-notation -- Safari compatibility */
@media (max-width: 900px) {
.summary-row {
grid-template-columns: repeat(3, 1fr);
}
.formula-row {
gap: 8px;
}
.formula-term {
min-width: 100px;
}
.compliance-calcs {
flex-direction: column;
gap: 8px;
}
}
/* stylelint-disable-next-line media-feature-range-notation -- Safari compatibility */
@media (max-width: 600px) {
.summary-row {
grid-template-columns: 1fr;
}
.dialog-header {
padding: 16px 20px;
}
.summary-row,
.table-wrap,
.formula-block,
.compliance-bar {
margin-left: 16px;
margin-right: 16px;
}
.tab-nav {
padding: 0 16px 12px;
flex-wrap: wrap;
}
.section-desc {
padding: 8px 16px;
}
}
@@ -0,0 +1,99 @@
/**
* 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 { CommonModule, DecimalPipe } from '@angular/common';
import { MatDialogRef, MatDialogModule } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
import { MatTabsModule } from '@angular/material/tabs';
import { MatTooltipModule } from '@angular/material/tooltip';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
@Component({
selector: 'mifosx-crar-dialog',
standalone: true,
templateUrl: './crar-dialog.component.html',
styleUrls: ['./crar-dialog.component.scss'],
imports: [
CommonModule,
DecimalPipe,
MatDialogModule,
MatButtonModule,
MatTabsModule,
MatTooltipModule,
FaIconComponent
]
})
export class CrarDialogComponent {
private dialogRef = inject(MatDialogRef<CrarDialogComponent>);
activeTab: 'tier1' | 'tier2' | 'creditRisk' | 'marketRisk' | 'opRisk' = 'tier1';
/** Capital Adequacy computations */
readonly totalTier1 = 8284.80;
readonly totalTier2 = 1556.00;
readonly totalCapital = 9840.80;
readonly totalRWA = 58420;
readonly crar = 16.84;
readonly cet1Ratio = 12.42;
readonly tier1Ratio = 14.18;
readonly tier2Ratio = 2.66;
readonly capitalBuffer = 6.34;
/** Credit Risk RWA */
readonly totalCreditRWA = 47820.40;
/** Market Risk RWA */
readonly totalMarketRWA = 7005.00;
/** Operational Risk RWA */
readonly totalOpRWA = 3595.60;
tier1Capital = [
{ sl: '01', name: 'Paid-up Equity Share Capital', sub: 'Ordinary equity shares fully paid', code: '5001-ESC', category: 'CET1', prev: '1,200.00', curr: '1,200.00', change: '\u2014' },
{ sl: '02', name: 'Statutory Reserves (Sec 17)', sub: 'Reserves under Sec 17 of BR Act', code: '5002-SR', category: 'CET1', prev: '2,840.60', curr: '3,012.40', change: '+171.80', changeDir: 'up' },
{ sl: '03', name: 'Capital Reserve', sub: 'Non-distributable capital reserves', code: '5003-CR', category: 'CET1', prev: '680.20', curr: '720.80', change: '+40.60', changeDir: 'up' },
{ sl: '04', name: 'Share Premium Account', sub: 'Premium collected on equity issuance', code: '5004-SP', category: 'CET1', prev: '1,480.00', curr: '1,480.00', change: '\u2014' },
{ sl: '05', name: 'Retained Earnings (Current Year)', sub: 'Profit retained after dividend payout', code: '5005-RE', category: 'CET1', prev: '1,060.40', curr: '1,284.60', change: '+224.20', changeDir: 'up' },
{ sl: '06', name: 'PNCPS', sub: 'Perpetual Non-Cumulative Preference Shares', code: '5101-PNCPS', category: 'AT1', prev: '500.00', curr: '500.00', change: '\u2014' },
{ sl: '07', name: 'Perpetual Debt Instruments', sub: 'Basel III compliant perpetual bonds', code: '5102-PDI', category: 'AT1', prev: '800.00', curr: '800.00', change: '\u2014' },
{ sl: '08', name: 'Innovative PDI', sub: 'Innovative perpetual debt instruments', code: '5103-IPDI', category: 'AT1', prev: '200.00', curr: '200.00', change: '\u2014' }
];
tier2Capital = [
{ sl: '01', name: 'Subordinated Debt', sub: 'Long-term subordinated bonds', code: '6001-SD', prev: '620.00', curr: '620.00', change: '\u2014' },
{ sl: '02', name: 'General Provisions & Loan Loss', sub: 'General provisions for standard assets', code: '6002-GP', prev: '480.40', curr: '520.60', change: '+40.20', changeDir: 'up' },
{ sl: '03', name: 'Revaluation Reserves', sub: 'Reserves from asset revaluation (55%)', code: '6003-RR', prev: '184.20', curr: '196.80', change: '+12.60', changeDir: 'up' },
{ sl: '04', name: 'Investment Fluctuation Reserve', sub: 'IFR as per RBI guidelines', code: '6004-IFR', prev: '120.00', curr: '142.60', change: '+22.60', changeDir: 'up' },
{ sl: '05', name: 'Upper Tier 2 Bonds', sub: 'Upper Tier 2 capital instruments', code: '6005-UTB', prev: '80.00', curr: '76.00', change: '-4.00', changeDir: 'dn' }
];
creditRiskRWA = [
{ sl: '01', name: 'Claims on Sovereign', sub: 'Central & State Government exposures', code: '7001-SOV', rw: '0%', gross: '18,420.60', rwa: '0.00' },
{ sl: '02', name: 'Claims on Banks', sub: 'Inter-bank exposures — scheduled banks', code: '7002-BNK', rw: '20%', gross: '4,210.40', rwa: '842.08' },
{ sl: '03', name: 'Corporate Loans Rated', sub: 'Rated corporate term loans', code: '7003-CORP', rw: '100%', gross: '12,840.20', rwa: '12,840.20' },
{ sl: '04', name: 'Retail Portfolio', sub: 'Retail loans — personal, auto, education', code: '7004-RTL', rw: '75%', gross: '8,640.80', rwa: '6,480.60' },
{ sl: '05', name: 'Housing Loans', sub: 'Home loans — LTV based risk weight', code: '7005-HLE', rw: '35%', gross: '6,820.40', rwa: '2,387.14' }
];
marketRiskRWA = [
{ sl: '01', name: 'Interest Rate Risk', sub: 'Duration & modified duration approach', code: '8001-IRR', approach: 'SDA', cc: '284.60', rwa: '3,557.50' },
{ sl: '02', name: 'Forex Risk', sub: 'Net open position in foreign currencies', code: '8002-FX', approach: 'Std', cc: '148.40', rwa: '1,855.00' },
{ sl: '03', name: 'Equity Risk', sub: 'Listed & unlisted equity exposures', code: '8003-EQR', approach: 'Std', cc: '84.20', rwa: '1,052.50' },
{ sl: '04', name: 'Commodity Price Risk', sub: 'Commodity position risk charges', code: '8004-CMR', approach: 'Simplified', cc: '24.80', rwa: '310.00' },
{ sl: '05', name: 'Options Risk', sub: 'Options portfolio — scenario analysis', code: '8005-OPR', approach: 'Scenario', cc: '18.40', rwa: '230.00' }
];
/** Operational Risk — Basic Indicator Approach */
readonly opRiskGrossIncome = 2984.40;
readonly opRiskAlpha = 15;
readonly opRiskCapitalCharge = 447.66;
readonly opRiskRWA = 3595.60; // 447.66 × 12.5 = 5595.75 — using provided value
close(): void {
this.dialogRef.close();
}
}
@@ -0,0 +1,384 @@
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="crr-dialog">
<!-- Header -->
<div class="dialog-header">
<div class="header-left">
<div class="header-badge">
<fa-icon icon="university"></fa-icon>
RBI Liabilities Register
</div>
<h2 class="dialog-title">Demand &amp; Time Liabilities</h2>
<p class="dialog-sub">NDTL &middot; CRR/SLR Computation Base &middot; As per RBI Master Circular</p>
</div>
<div class="header-right">
<div class="as-of-box">
<strong>{{ reportingDateStr }}</strong>
<span>Reporting Fortnight (45-day lag)</span>
</div>
<a class="rbi-circular-btn" href="https://www.rbi.org.in/scripts/bs_circularindexdisplay.aspx" target="_blank" rel="noopener noreferrer" matTooltip="View RBI Circulars">
<fa-icon icon="external-link-alt"></fa-icon>
RBI Circulars
</a>
<button class="close-btn" (click)="close()" matTooltip="Close">
<fa-icon icon="times"></fa-icon>
</button>
</div>
</div>
<!-- Summary Cards -->
<div class="summary-row">
<div class="summary-card sc-demand">
<div class="sc-label">Total Demand Liabilities</div>
<div class="sc-value">&#8377;12,843<span class="cr">Cr</span></div>
<div class="sc-change up">&#8593; 3.2% vs prev fortnight</div>
</div>
<div class="summary-card sc-time">
<div class="sc-label">Total Time Liabilities</div>
<div class="sc-value">&#8377;64,618<span class="cr">Cr</span></div>
<div class="sc-change up">&#8593; 1.8% vs prev fortnight</div>
</div>
<div class="summary-card sc-asset">
<div class="sc-label">Assets with Banking System</div>
<div class="sc-value">&#8377;8,240<span class="cr">Cr</span></div>
<div class="sc-change dn">&#8595; Deducted from NDTL</div>
</div>
<div class="summary-card sc-ndtl">
<div class="sc-label">Net Demand &amp; Time Liabilities (NDTL)</div>
<div class="sc-value">&#8377;69,221<span class="cr">Cr</span></div>
<div class="sc-sub">After deducting banking assets</div>
</div>
<div class="summary-card sc-crr">
<div class="sc-label">CRR Required ({{ crrRate }}%)</div>
<div class="sc-value">&#8377;{{ crrRequired | number:'1.0-0' }}<span class="cr">Cr</span></div>
<div class="sc-sub">SLR Req: &#8377;{{ slrRequired | number:'1.0-0' }} Cr ({{ slrRate }}%)</div>
</div>
</div>
<!-- Tab Navigation -->
<div class="tab-nav">
<button class="tab-btn" [class.active]="activeTab === 'demand'" (click)="activeTab = 'demand'">
<span class="tab-pill pill-demand">01</span> Demand Liabilities
</button>
<button class="tab-btn" [class.active]="activeTab === 'time'" (click)="activeTab = 'time'">
<span class="tab-pill pill-time">02</span> Time Liabilities
</button>
<button class="tab-btn" [class.active]="activeTab === 'assets'" (click)="activeTab = 'assets'">
<span class="tab-pill pill-asset">03</span> Assets with Banking System
</button>
<button class="tab-btn" [class.active]="activeTab === 'slr'" (click)="activeTab = 'slr'">
<span class="tab-pill pill-slr">04</span> SLR &mdash; Liquid Assets
</button>
</div>
<!-- Demand Liabilities Table -->
@if (activeTab === 'demand') {
<div class="section-desc">Amounts payable on demand &mdash; RBI Schedule II</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th class="sl">#</th>
<th>Liability Item</th>
<th>Account Code</th>
<th class="right">Opening (&#8377; Cr)</th>
<th class="right">Current (&#8377; Cr)</th>
<th class="right">Change (&#8377; Cr)</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@for (item of demandLiabilities; track item.sl) {
<tr>
<td class="sl">{{ item.sl }}</td>
<td>
<div class="item-name">{{ item.name }}</div>
<div class="item-sub">{{ item.sub }}</div>
</td>
<td class="mono">{{ item.code }}</td>
<td class="right amt amt-demand">{{ item.opening }}</td>
<td class="right amt amt-demand">{{ item.current }}</td>
<td class="right" [class.up]="item.changeDir === 'up'" [class.dn]="item.changeDir === 'dn'">{{ item.change }}</td>
<td>
<span class="status-chip"
[class.chip-active]="item.status === 'Active'"
[class.chip-overdue]="item.status === 'Overdue' || item.status === 'Unclaimed'">
{{ item.status }}
</span>
</td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="3"><strong>Total Demand Liabilities (All 16 Items)</strong></td>
<td class="right foot-demand">12,114.75</td>
<td class="right foot-demand">12,843.50</td>
<td class="right up">+728.75</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
}
<!-- Time Liabilities Table -->
@if (activeTab === 'time') {
<div class="section-desc">Amounts payable after a specified period &mdash; RBI Schedule III</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th class="sl">#</th>
<th>Liability Item</th>
<th>Account Code</th>
<th class="right">Opening (&#8377; Cr)</th>
<th class="right">Current (&#8377; Cr)</th>
<th class="right">Change (&#8377; Cr)</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@for (item of timeLiabilities; track item.sl) {
<tr>
<td class="sl">{{ item.sl }}</td>
<td>
<div class="item-name">{{ item.name }}</div>
<div class="item-sub">{{ item.sub }}</div>
</td>
<td class="mono">{{ item.code }}</td>
<td class="right amt amt-time">{{ item.opening }}</td>
<td class="right amt amt-time">{{ item.current }}</td>
<td class="right" [class.up]="item.changeDir === 'up'" [class.dn]="item.changeDir === 'dn'">{{ item.change }}</td>
<td>
<span class="status-chip"
[class.chip-active]="item.status === 'Active'"
[class.chip-locked]="item.status === 'Locked'">
{{ item.status }}
</span>
</td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="3"><strong>Total Time Liabilities (All 13 Items)</strong></td>
<td class="right foot-time">62,613.10</td>
<td class="right foot-time">64,618.00</td>
<td class="right up">+2,004.90</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
}
<!-- Assets with Banking System Table -->
@if (activeTab === 'assets') {
<div class="section-desc">Deducted from gross liabilities to arrive at NDTL &mdash; RBI Master Circular</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th class="sl">#</th>
<th>Asset Item</th>
<th>Account Code</th>
<th class="right">Opening (&#8377; Cr)</th>
<th class="right">Current (&#8377; Cr)</th>
<th class="right">Change (&#8377; Cr)</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@for (item of bankingAssets; track item.sl) {
<tr>
<td class="sl">{{ item.sl }}</td>
<td>
<div class="item-name">{{ item.name }}</div>
<div class="item-sub">{{ item.sub }}</div>
</td>
<td class="mono">{{ item.code }}</td>
<td class="right amt amt-asset">{{ item.opening }}</td>
<td class="right amt amt-asset">{{ item.current }}</td>
<td class="right" [class.up]="item.changeDir === 'up'" [class.dn]="item.changeDir === 'dn'">{{ item.change }}</td>
<td><span class="status-chip chip-active">{{ item.status }}</span></td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="3"><strong>Total Assets with Banking System</strong> <span class="deduct-tag">( Deducted from NDTL )</span></td>
<td class="right foot-asset">7,600.70</td>
<td class="right foot-asset">8,240.20</td>
<td class="right up">+639.50</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
}
<!-- SLR - Liquid Assets Table -->
@if (activeTab === 'slr') {
<div class="section-desc">SLR = (Liquid Assets / NDTL) &times; 100 &mdash; RBI Sec 24(2A) of Banking Regulation Act, 1949</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th class="sl">#</th>
<th>Liquid Asset</th>
<th>Account Code</th>
<th>Category</th>
<th class="right">Opening (&#8377; Cr)</th>
<th class="right">Current (&#8377; Cr)</th>
<th class="right">Change (&#8377; Cr)</th>
<th>Status</th>
</tr>
</thead>
<tbody>
@for (item of liquidAssets; track item.sl) {
<tr>
<td class="sl">{{ item.sl }}</td>
<td>
<div class="item-name">{{ item.name }}</div>
<div class="item-sub">{{ item.sub }}</div>
</td>
<td class="mono">{{ item.code }}</td>
<td><span class="status-chip chip-slr">{{ item.category }}</span></td>
<td class="right amt amt-slr">{{ item.opening }}</td>
<td class="right amt amt-slr">{{ item.current }}</td>
<td class="right" [class.up]="item.changeDir === 'up'" [class.dn]="item.changeDir === 'dn'">{{ item.change }}</td>
<td><span class="status-chip chip-active">{{ item.status }}</span></td>
</tr>
}
</tbody>
<tfoot>
<tr>
<td colspan="4"><strong>Total Liquid Assets (SLR Eligible)</strong></td>
<td class="right foot-slr">12,511.70</td>
<td class="right foot-slr">13,238.80</td>
<td class="right up">+727.10</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
<!-- SLR Formula -->
<div class="formula-block slr-formula">
<div class="formula-title">SLR Computation &mdash; (Liquid Assets / NDTL) &times; 100</div>
<div class="formula-row">
<div class="formula-term ft-slr">
<div class="formula-term-label">Liquid Assets</div>
<div class="formula-term-val">&#8377;13,238.80</div>
<div class="formula-term-sub">6 asset categories</div>
</div>
<div class="formula-op">&divide;</div>
<div class="formula-term ft-ndtl">
<div class="formula-term-label">NDTL</div>
<div class="formula-term-val">&#8377;69,221.30</div>
<div class="formula-term-sub">Net Demand &amp; Time</div>
</div>
<div class="formula-op">&times;</div>
<div class="formula-term" style="border-color:#e2e8f0;background:#f8fafc;">
<div class="formula-term-label" style="color:#64748b;">100</div>
<div class="formula-term-val" style="color:#334155;">100</div>
<div class="formula-term-sub">&nbsp;</div>
</div>
<div class="formula-op equals">=</div>
<div class="formula-term ft-slr-result">
<div class="formula-term-label">Actual SLR</div>
<div class="formula-term-val">{{ slrActualPercent }}%</div>
<div class="formula-term-sub">Min required: {{ slrRate }}%</div>
</div>
</div>
</div>
<!-- SLR Status Bar -->
<div class="slr-status-bar">
<div class="slr-status-left">
<div class="slr-status-title">SLR Compliance Status</div>
<div class="slr-status-desc">Actual: {{ slrActualPercent }}% &middot; Required: {{ slrRate }}.00% &middot; NDTL: &#8377;{{ ndtl | number:'1.2-2' }} Cr</div>
</div>
<div class="slr-status-calcs">
<div class="slr-calc">
<div class="slr-calc-label">SLR Required ({{ slrRate }}%)</div>
<div class="slr-calc-val">&#8377;{{ slrRequired | number:'1.2-2' }} Cr</div>
</div>
<div class="slr-calc">
<div class="slr-calc-label">Liquid Assets Held</div>
<div class="slr-calc-val">&#8377;{{ totalLiquidAssets | number:'1.2-2' }} Cr</div>
</div>
<div class="slr-calc">
<div class="slr-calc-label">Surplus</div>
<div class="slr-calc-val surplus">&#8377;{{ slrSurplus | number:'1.2-2' }} Cr</div>
</div>
</div>
</div>
}
<!-- NDTL Formula Block -->
<div class="formula-block">
<div class="formula-title">NDTL Computation &mdash; Formula Breakdown &middot; Data as of {{ reportingDateStr }}</div>
<div class="formula-row">
<div class="formula-term ft-demand">
<div class="formula-term-label">Demand Liabilities</div>
<div class="formula-term-val">&#8377;{{ totalDemand | number:'1.2-2' }}</div>
<div class="formula-term-sub">16 line items</div>
</div>
<div class="formula-op">+</div>
<div class="formula-term ft-time">
<div class="formula-term-label">Time Liabilities</div>
<div class="formula-term-val">&#8377;{{ totalTime | number:'1.2-2' }}</div>
<div class="formula-term-sub">13 line items</div>
</div>
<div class="formula-op minus">&minus;</div>
<div class="formula-term ft-asset">
<div class="formula-term-label">Assets with Banking</div>
<div class="formula-term-val">&#8377;{{ totalBankingAssets | number:'1.2-2' }}</div>
<div class="formula-term-sub">3 line items</div>
</div>
<div class="formula-op equals">=</div>
<div class="formula-term ft-ndtl">
<div class="formula-term-label">NDTL</div>
<div class="formula-term-val">&#8377;{{ ndtl | number:'1.2-2' }}</div>
<div class="formula-term-sub">CRR &amp; SLR base</div>
</div>
</div>
</div>
<!-- CRR/SLR Bar -->
<div class="crr-bar">
<div class="crr-text">
<div class="crr-title">CRR &amp; SLR Reserve Requirements &mdash; RBI Computation on NDTL</div>
<div class="crr-desc">Based on NDTL of &#8377;{{ ndtl | number:'1.2-2' }} Cr &middot; Data as of {{ reportingDateStr }} (45-day lag)</div>
</div>
<div class="crr-calcs">
<div class="crr-calc">
<div class="crr-calc-label">CRR @ {{ crrRate }}.00%</div>
<div class="crr-calc-val">&#8377;{{ crrRequired | number:'1.2-2' }} Cr</div>
</div>
<div class="crr-calc">
<div class="crr-calc-label">SLR @ {{ slrRate }}.00%</div>
<div class="crr-calc-val">&#8377;{{ slrRequired | number:'1.2-2' }} Cr</div>
</div>
<div class="crr-calc">
<div class="crr-calc-label">Total Reserve</div>
<div class="crr-calc-val">&#8377;{{ totalReserve | number:'1.2-2' }} Cr</div>
</div>
</div>
</div>
<!-- Footer -->
<div class="dialog-footer">
<span>CBSNexora &middot; RBI Liabilities Register</span>
<span>RBI Master Circular &mdash; CRR/SLR 2024-25</span>
<button mat-button (click)="close()">Close</button>
</div>
</div>
@@ -0,0 +1,686 @@
/**
* 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/.
*/
$demand: #1d4ed8;
$demand-bg: #eff6ff;
$demand-b: #bfdbfe;
$time: #0f766e;
$time-bg: #f0fdfa;
$time-b: #99f6e4;
$asset: #7c3aed;
$asset-bg: #f5f3ff;
$asset-b: #ddd6fe;
$ink: #0f172a;
$ink2: #334155;
$ink3: #64748b;
$ink4: #94a3b8;
$border: #e2e8f0;
$border2: #cbd5e1;
$surface: #fff;
$surface2: #f8fafc;
$red: #dc2626;
$green: #16a34a;
$amber: #d97706;
$mono: 'IBM Plex Mono', 'Roboto Mono', monospace;
.crr-dialog {
max-height: 85vh;
overflow-y: auto;
padding: 0;
font-size: 14px;
line-height: 1.6;
color: $ink;
}
// ===== Header =====
.dialog-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 24px 28px 20px;
border-bottom: 1px solid $border;
gap: 16px;
flex-wrap: wrap;
position: sticky;
top: 0;
background: $surface;
z-index: 10;
}
.header-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 10px;
font-weight: 600;
padding: 3px 10px;
border-radius: 4px;
background: #fff7ed;
color: #c2410c;
border: 1px solid #fed7aa;
letter-spacing: 0.5px;
margin-bottom: 8px;
font-family: $mono;
}
.dialog-title {
font-size: 20px;
font-weight: 600;
color: $ink;
margin: 0 0 4px;
}
.dialog-sub {
font-size: 12px;
color: $ink3;
font-family: $mono;
letter-spacing: 0.5px;
margin: 0;
}
.header-right {
display: flex;
align-items: flex-start;
gap: 12px;
}
.as-of-box {
font-size: 11px;
color: $ink3;
font-family: $mono;
text-align: right;
background: $surface2;
border: 1px solid $border;
padding: 8px 14px;
border-radius: 8px;
strong {
color: $ink2;
display: block;
font-size: 13px;
margin-bottom: 2px;
}
}
.rbi-circular-btn {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
font-family: $mono;
font-weight: 500;
padding: 7px 14px;
border-radius: 8px;
background: #fff7ed;
color: #c2410c;
border: 1px solid #fed7aa;
text-decoration: none;
transition: all 0.15s;
white-space: nowrap;
&:hover {
background: #ffedd5;
border-color: #fb923c;
color: #9a3412;
}
fa-icon {
font-size: 10px;
}
}
.close-btn {
width: 32px;
height: 32px;
border-radius: 8px;
background: $surface2;
border: 1px solid $border;
cursor: pointer;
font-size: 16px;
color: $ink3;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s;
&:hover {
background: #fef2f2;
color: $red;
border-color: #fecaca;
}
}
// ===== Summary Cards =====
.summary-row {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 14px;
padding: 20px 28px;
}
.summary-card {
background: $surface;
border: 1px solid $border;
border-radius: 12px;
padding: 16px 18px;
position: relative;
overflow: hidden;
transition: box-shadow 0.2s;
&:hover {
box-shadow: 0 4px 16px rgb(0 0 0 / 8%);
}
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
}
}
.sc-demand::before { background: $demand; }
.sc-time::before { background: $time; }
.sc-asset::before { background: $asset; }
.sc-ndtl::before { background: linear-gradient(90deg, $asset, $time, $demand); }
.sc-crr::before { background: $amber; }
.sc-label {
font-size: 10px;
color: $ink3;
font-family: $mono;
letter-spacing: 1px;
text-transform: uppercase;
margin-bottom: 6px;
}
.sc-value {
font-size: 20px;
font-weight: 600;
color: $ink;
font-family: $mono;
}
.cr {
font-size: 11px;
color: $ink3;
font-weight: 400;
margin-left: 3px;
}
.sc-change {
font-size: 11px;
margin-top: 4px;
}
.sc-sub {
font-size: 11px;
color: $ink3;
margin-top: 4px;
}
.up { color: $green; }
.dn { color: $red; }
// ===== Tab Navigation =====
.tab-nav {
display: flex;
gap: 8px;
padding: 0 28px 16px;
border-bottom: 1px solid $border;
}
.tab-btn {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
border-radius: 8px;
border: 1px solid $border;
background: $surface;
color: $ink2;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s;
&:hover {
border-color: $border2;
background: $surface2;
}
&.active {
background: $ink;
color: #fff;
border-color: $ink;
.tab-pill {
background: rgb(255 255 255 / 20%);
color: #fff;
border-color: transparent;
}
}
}
.tab-pill {
font-size: 10px;
font-family: $mono;
font-weight: 600;
padding: 2px 8px;
border-radius: 12px;
letter-spacing: 0.5px;
}
.pill-demand { background: $demand-bg; color: $demand; border: 1px solid $demand-b; }
.pill-time { background: $time-bg; color: $time; border: 1px solid $time-b; }
.pill-asset { background: $asset-bg; color: $asset; border: 1px solid $asset-b; }
.section-desc {
font-size: 12px;
color: $ink3;
font-style: italic;
padding: 12px 28px 8px;
}
// ===== Table =====
.table-wrap {
margin: 0 28px 20px;
background: $surface;
border: 1px solid $border;
border-radius: 12px;
overflow: hidden;
}
table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
thead tr {
background: $surface2;
border-bottom: 1px solid $border;
}
thead th {
padding: 10px 14px;
text-align: left;
font-size: 10px;
font-family: $mono;
font-weight: 500;
color: $ink3;
letter-spacing: 1px;
text-transform: uppercase;
white-space: nowrap;
&.right { text-align: right; }
}
tbody tr {
border-bottom: 1px solid $border;
transition: background 0.15s;
&:last-child { border-bottom: none; }
&:hover { background: #f8fafc; }
}
td {
padding: 10px 14px;
color: $ink2;
vertical-align: middle;
&.right { text-align: right; font-family: $mono; }
}
.sl {
color: $ink4;
font-family: $mono;
font-size: 11px;
width: 36px;
}
.mono { font-family: $mono; font-size: 12px; }
.item-name {
font-weight: 500;
color: $ink;
}
.item-sub {
font-size: 11px;
color: $ink3;
margin-top: 2px;
}
.amt {
font-family: $mono;
font-weight: 500;
}
.amt-demand { color: $demand; }
.amt-time { color: $time; }
.amt-asset { color: $asset; }
.status-chip {
display: inline-flex;
align-items: center;
gap: 4px;
font-size: 10px;
font-family: $mono;
font-weight: 500;
padding: 2px 8px;
border-radius: 4px;
}
.chip-active { background: #dcfce7; color: #15803d; }
.chip-overdue { background: #fef2f2; color: $red; }
.chip-locked { background: #fefce8; color: #92400e; }
tfoot tr {
background: $surface2;
border-top: 2px solid $border2;
}
tfoot td {
font-weight: 600;
color: $ink;
font-family: $mono;
padding: 12px 14px;
&.right { text-align: right; font-size: 14px; }
}
$slr: #0369a1;
$slr-bg: #f0f9ff;
$slr-b: #bae6fd;
.foot-demand { color: $demand; }
.foot-time { color: $time; }
.foot-asset { color: $asset; }
.foot-slr { color: $slr; }
.deduct-tag { font-size: 10px; color: $asset; font-family: $mono; margin-left: 8px; }
.amt-slr { color: $slr; }
.chip-slr { background: $slr-bg; color: $slr; border: 1px solid $slr-b; }
.pill-slr { background: $slr-bg; color: $slr; border: 1px solid $slr-b; }
.ft-slr {
border-color: $slr-b;
background: $slr-bg;
.formula-term-label, .formula-term-val { color: $slr; }
}
.ft-slr-result {
border-color: #bbf7d0;
background: linear-gradient(135deg, #f0fdf4, #ecfdf5);
.formula-term-label { color: #15803d; }
.formula-term-val { color: $green; font-size: 20px; }
}
.slr-formula::before {
background: linear-gradient(90deg, $slr, $green) !important;
}
// ===== SLR Status Bar =====
.slr-status-bar {
margin: 0 28px 20px;
background: linear-gradient(135deg, #ecfdf5, #f0fdf4);
border: 1px solid #bbf7d0;
border-radius: 10px;
padding: 16px 24px;
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
.slr-status-left { flex: 1; }
.slr-status-title {
font-size: 13px;
font-weight: 600;
color: #14532d;
margin-bottom: 3px;
}
.slr-status-desc {
font-size: 12px;
color: #166534;
}
.slr-status-calcs {
display: flex;
gap: 24px;
}
.slr-calc { text-align: right; }
.slr-calc-label {
font-size: 10px;
color: #166534;
font-family: $mono;
letter-spacing: 1px;
}
.slr-calc-val {
font-size: 15px;
font-weight: 600;
color: #14532d;
font-family: $mono;
&.surplus { color: $green; }
}
// ===== Formula Block =====
.formula-block {
margin: 20px 28px;
background: $surface;
border: 1px solid $border;
border-radius: 14px;
padding: 20px 24px;
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 3px;
background: linear-gradient(90deg, $demand, $time, $asset);
}
}
.formula-title {
font-size: 11px;
font-family: $mono;
color: $ink3;
letter-spacing: 2px;
text-transform: uppercase;
margin-bottom: 16px;
}
.formula-row {
display: flex;
align-items: center;
gap: 0;
flex-wrap: wrap;
}
.formula-term {
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 16px;
border-radius: 10px;
border: 1px solid $border;
background: $surface2;
min-width: 130px;
flex: 1;
}
.formula-term-label {
font-size: 9.5px;
font-family: $mono;
letter-spacing: 1.5px;
text-transform: uppercase;
margin-bottom: 4px;
}
.formula-term-val {
font-size: 16px;
font-weight: 600;
font-family: $mono;
}
.formula-term-sub {
font-size: 9px;
color: $ink4;
font-family: $mono;
margin-top: 2px;
}
.formula-op {
font-size: 20px;
font-weight: 300;
color: $ink3;
padding: 0 8px;
flex-shrink: 0;
&.minus { color: $red; }
&.equals { color: $ink2; font-weight: 500; }
}
.ft-demand { border-color: $demand-b; background: $demand-bg; .formula-term-label, .formula-term-val { color: $demand; } }
.ft-time { border-color: $time-b; background: $time-bg; .formula-term-label, .formula-term-val { color: $time; } }
.ft-asset { border-color: $asset-b; background: $asset-bg; .formula-term-label, .formula-term-val { color: $asset; } }
.ft-ndtl {
border-color: #a78bfa;
background: linear-gradient(135deg, #f5f3ff, #f0fdfa);
.formula-term-label { color: #6d28d9; }
.formula-term-val { color: $ink; font-size: 18px; }
}
// ===== CRR Bar =====
.crr-bar {
margin: 0 28px 20px;
background: linear-gradient(135deg, #fefce8, #fff7ed);
border: 1px solid #fed7aa;
border-radius: 10px;
padding: 16px 24px;
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
.crr-title {
font-size: 13px;
font-weight: 600;
color: #92400e;
margin-bottom: 3px;
}
.crr-desc {
font-size: 12px;
color: #a16207;
}
.crr-text { flex: 1; }
.crr-calcs {
display: flex;
gap: 24px;
}
.crr-calc { text-align: right; }
.crr-calc-label {
font-size: 10px;
color: #92400e;
font-family: $mono;
letter-spacing: 1px;
}
.crr-calc-val {
font-size: 15px;
font-weight: 600;
color: #78350f;
font-family: $mono;
}
// ===== Footer =====
.dialog-footer {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 28px;
border-top: 1px solid $border;
font-size: 11px;
color: $ink4;
font-family: $mono;
background: $surface2;
flex-wrap: wrap;
gap: 8px;
position: sticky;
bottom: 0;
}
// ===== Responsive =====
/* stylelint-disable-next-line media-feature-range-notation -- Safari compatibility */
@media (max-width: 900px) {
.summary-row {
grid-template-columns: repeat(2, 1fr);
}
.formula-row {
gap: 8px;
}
.formula-term {
min-width: 100px;
}
.crr-calcs,
.slr-status-calcs {
flex-direction: column;
gap: 8px;
}
}
/* stylelint-disable-next-line media-feature-range-notation -- Safari compatibility */
@media (max-width: 600px) {
.summary-row {
grid-template-columns: 1fr;
}
.dialog-header {
padding: 16px 20px;
}
.summary-row,
.table-wrap,
.formula-block,
.crr-bar,
.slr-status-bar {
margin-left: 16px;
margin-right: 16px;
}
.tab-nav {
padding: 0 16px 12px;
flex-wrap: wrap;
}
.section-desc {
padding: 8px 16px;
}
}
@@ -0,0 +1,119 @@
/**
* 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 { CommonModule, DecimalPipe } from '@angular/common';
import { MatDialogRef, MatDialogModule } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
import { MatTabsModule } from '@angular/material/tabs';
import { MatTooltipModule } from '@angular/material/tooltip';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
@Component({
selector: 'mifosx-crr-slr-dialog',
standalone: true,
templateUrl: './crr-slr-dialog.component.html',
styleUrls: ['./crr-slr-dialog.component.scss'],
imports: [
CommonModule,
DecimalPipe,
MatDialogModule,
MatButtonModule,
MatTabsModule,
MatTooltipModule,
FaIconComponent
]
})
export class CrrSlrDialogComponent {
private dialogRef = inject(MatDialogRef<CrrSlrDialogComponent>);
activeTab: 'demand' | 'time' | 'assets' | 'slr' = 'demand';
/** Reporting fortnight date — 45 days before current date */
today = new Date();
reportingDate: Date;
reportingDateStr: string;
/** NDTL & Reserve computations */
readonly totalDemand = 12843.50;
readonly totalTime = 64618.00;
readonly totalBankingAssets = 8240.20;
readonly ndtl = 69221.30; // Demand + Time - Banking Assets
readonly crrRate = 3; // Current RBI CRR rate (%)
readonly slrRate = 18; // Current RBI SLR rate (%)
readonly crrRequired = 69221.30 * 0.03; // ₹2,076.64 Cr
readonly slrRequired = 69221.30 * 0.18; // ₹12,459.83 Cr
readonly totalLiquidAssets = 13238.80;
readonly slrActualPercent = +(13238.80 / 69221.30 * 100).toFixed(2); // 19.12%
readonly slrSurplus = +(13238.80 - 69221.30 * 0.18).toFixed(2); // 778.97
readonly totalReserve = +(69221.30 * 0.03 + 69221.30 * 0.18).toFixed(2); // 14,536.47
constructor() {
// CRR/SLR uses data from 45 days ago
const lagDays = 45;
this.reportingDate = new Date(this.today);
this.reportingDate.setDate(this.reportingDate.getDate() - lagDays);
this.reportingDateStr = this.reportingDate.toLocaleDateString('en-IN', {
day: '2-digit', month: 'short', year: 'numeric'
});
}
demandLiabilities = [
{ sl: '01', name: 'Current Account Deposits', sub: 'Corporate & individual CA balances', code: '2001-CA', opening: '3,842.50', current: '4,120.75', change: '+278.25', changeDir: 'up', status: 'Active' },
{ sl: '02', name: 'Savings Bank Deposits (Withdrawable)', sub: 'Demand portion — 10% of SB balance', code: '2002-SB', opening: '1,240.00', current: '1,318.40', change: '+78.40', changeDir: 'up', status: 'Active' },
{ sl: '03', name: 'Demand Drafts Payable', sub: 'Issued DDs outstanding for payment', code: '2003-DD', opening: '284.20', current: '311.60', change: '+27.40', changeDir: 'up', status: 'Active' },
{ sl: '04', name: "Banker's Cheques / Pay Orders", sub: 'Issued pay orders pending encashment', code: '2004-PO', opening: '198.75', current: '176.30', change: '-22.45', changeDir: 'dn', status: 'Active' },
{ sl: '05', name: 'Credit Balance in Cash Credit Accounts', sub: 'CC accounts in credit position', code: '2005-CC', opening: '542.10', current: '618.90', change: '+76.80', changeDir: 'up', status: 'Active' },
{ sl: '06', name: 'Overdue Fixed Deposits', sub: 'FDs past maturity — treated as demand', code: '2006-OFD', opening: '89.40', current: '112.85', change: '+23.45', changeDir: 'dn', status: 'Overdue' },
{ sl: '07', name: 'Interest Payable on Demand Deposits', sub: 'Accrued interest on CA/SB', code: '2007-INT', opening: '34.20', current: '38.55', change: '+4.35', changeDir: 'up', status: 'Active' },
{ sl: '08', name: 'Margins held against Letters of Credit', sub: 'Withdrawable LC margin deposits', code: '2008-LCM', opening: '412.60', current: '388.20', change: '-24.40', changeDir: 'dn', status: 'Active' },
{ sl: '09', name: 'Margins held against Bank Guarantees', sub: 'Withdrawable BG margin deposits', code: '2009-BGM', opening: '318.40', current: '342.10', change: '+23.70', changeDir: 'up', status: 'Active' },
{ sl: '10', name: 'Security Deposits (Withdrawable)', sub: 'EMD / SD refundable on demand', code: '2010-SD', opening: '124.80', current: '118.60', change: '-6.20', changeDir: 'dn', status: 'Active' },
{ sl: '11', name: 'Unclaimed Deposits (Demand Portion)', sub: 'Balances unclaimed >10 years', code: '2011-UCD', opening: '56.30', current: '59.40', change: '+3.10', changeDir: 'dn', status: 'Unclaimed' },
{ sl: '12', name: 'Inter-Bank Demand Deposits', sub: 'Demand deposits from other banks', code: '2012-IBD', opening: '2,840.00', current: '3,120.50', change: '+280.50', changeDir: 'up', status: 'Active' },
{ sl: '13', name: 'Clearing Adjustments / Balances', sub: 'Net clearing settlement balances', code: '2013-CLR', opening: '186.40', current: '210.80', change: '+24.40', changeDir: 'up', status: 'Active' },
{ sl: '14', name: 'Outstanding Payment Instruments', sub: 'Cheques issued but not yet presented', code: '2014-OPI', opening: '142.60', current: '158.90', change: '+16.30', changeDir: 'up', status: 'Active' },
{ sl: '15', name: 'Demand Liabilities towards Other Banks', sub: 'Nostro / Vostro demand obligations', code: '2015-DLB', opening: '984.20', current: '1,048.60', change: '+64.40', changeDir: 'up', status: 'Active' },
{ sl: '16', name: 'Deposits Repayable on Demand', sub: 'Miscellaneous on-demand deposits', code: '2016-DRD', opening: '618.30', current: '698.05', change: '+79.75', changeDir: 'up', status: 'Active' }
];
timeLiabilities = [
{ sl: '01', name: 'Fixed Deposits (FD)', sub: '7 days to 10 years · 6.5–7.2%', code: '3001-FD', opening: '28,420.50', current: '29,184.30', change: '+763.80', changeDir: 'up', status: 'Active' },
{ sl: '02', name: 'Recurring Deposits (RD)', sub: 'Monthly instalment · 6.0–6.8%', code: '3002-RD', opening: '4,218.60', current: '4,412.80', change: '+194.20', changeDir: 'up', status: 'Active' },
{ sl: '03', name: 'Time Liability Portion of Savings', sub: '90% of SB balance treated as time', code: '3003-SBT', opening: '10,840.20', current: '11,204.60', change: '+364.40', changeDir: 'up', status: 'Active' },
{ sl: '04', name: 'Reinvestment Deposits', sub: 'Cumulative interest plans · 7.0%', code: '3004-RID', opening: '1,842.40', current: '1,918.70', change: '+76.30', changeDir: 'up', status: 'Active' },
{ sl: '05', name: 'Staff Security Deposits (Fixed Period)', sub: 'Employee security deposits — locked', code: '3005-SSD', opening: '84.60', current: '86.40', change: '+1.80', changeDir: 'up', status: 'Locked' },
{ sl: '06', name: 'Margin Money for Guarantees / LC', sub: 'LC/BG margins — fixed lock-in period', code: '3006-MGM', opening: '620.80', current: '644.20', change: '+23.40', changeDir: 'up', status: 'Locked' },
{ sl: '07', name: 'Certificates of Deposit (CD)', sub: 'Issued to corporates / FIs · 7.35%', code: '3007-CD', opening: '3,840.00', current: '3,920.50', change: '+80.50', changeDir: 'up', status: 'Active' },
{ sl: '08', name: 'Interest Accrued on Time Deposits', sub: 'Accrued but unpaid interest on FD/RD', code: '3008-IAT', opening: '842.60', current: '918.40', change: '+75.80', changeDir: 'up', status: 'Active' },
{ sl: '09', name: 'Long-term Deposits', sub: 'Tenor >5 years · 7.5%', code: '3009-LTD', opening: '5,620.40', current: '5,818.60', change: '+198.20', changeDir: 'up', status: 'Active' },
{ sl: '10', name: 'Tax Saving Deposits', sub: 'Sec 80C FDs — 5-year lock-in · 6.8%', code: '3010-TSD', opening: '1,240.80', current: '1,318.40', change: '+77.60', changeDir: 'up', status: 'Locked' },
{ sl: '11', name: 'Foreign Currency Deposits', sub: 'FCNR(B), RFC — USD/EUR/GBP', code: '3011-FCD', opening: '2,184.60', current: '2,348.20', change: '+163.60', changeDir: 'up', status: 'Active' },
{ sl: '12', name: 'Time Liabilities to Other Banks', sub: 'Inter-bank term deposits — call money', code: '3012-TLB', opening: '1,840.20', current: '1,924.40', change: '+84.20', changeDir: 'up', status: 'Active' },
{ sl: '13', name: 'Term Borrowings', sub: 'Bonds, debentures, refinance · 8.1%', code: '3013-TBR', opening: '1,018.40', current: '1,118.50', change: '+100.10', changeDir: 'up', status: 'Active' }
];
bankingAssets = [
{ sl: '01', name: 'Balances with Other Banks', sub: 'Nostro / Vostro balances — SBI, PNB, BOB', code: '4001-BWB', opening: '3,840.20', current: '4,112.40', change: '+272.20', changeDir: 'up', status: 'Active' },
{ sl: '02', name: 'Money at Call with Banks', sub: 'Overnight & short-notice call money', code: '4002-MCB', opening: '2,180.50', current: '2,418.60', change: '+238.10', changeDir: 'up', status: 'Active' },
{ sl: '03', name: 'Loans to Banks (Inter-Bank Lending)', sub: 'Term loans & credit lines to SCBs', code: '4003-LTB', opening: '1,580.00', current: '1,709.20', change: '+129.20', changeDir: 'up', status: 'Active' }
];
liquidAssets = [
{ sl: '01', name: 'Central Government Securities', sub: 'G-Secs held in HTM, AFS & HFT portfolios', code: '5001-CGS', opening: '6,840.50', current: '7,218.40', change: '+377.90', changeDir: 'up', status: 'Active', category: 'G-Sec' },
{ sl: '02', name: 'State Development Loans (SDLs)', sub: 'State govt bonds — various maturities', code: '5002-SDL', opening: '2,420.30', current: '2,584.60', change: '+164.30', changeDir: 'up', status: 'Active', category: 'SDL' },
{ sl: '03', name: 'Treasury Bills (91/182/364 day)', sub: 'Short-term govt securities — discounted', code: '5003-TB', opening: '1,840.20', current: '1,962.80', change: '+122.60', changeDir: 'up', status: 'Active', category: 'T-Bill' },
{ sl: '04', name: 'Gold (valued at current market)', sub: 'Gold reserves held per RBI valuation norms', code: '5004-GLD', opening: '680.40', current: '724.60', change: '+44.20', changeDir: 'up', status: 'Active', category: 'Gold' },
{ sl: '05', name: 'Special Deposit Scheme with RBI', sub: 'Deposits placed under SDS — SLR eligible', code: '5005-SDS', opening: '420.00', current: '420.00', change: '0.00', changeDir: 'up', status: 'Active', category: 'RBI Dep' },
{ sl: '06', name: 'NABARD / SIDBI / NHB Bonds', sub: 'PSE bonds approved as SLR-eligible', code: '5006-PSE', opening: '310.80', current: '328.40', change: '+17.60', changeDir: 'up', status: 'Active', category: 'PSE Bond' }
];
close(): void {
this.dialogRef.close();
}
}
+38 -10
View File
@@ -19,19 +19,47 @@
<span>Regulatory &amp; Compliance</span>
</div>
<div class="cards-row four-col">
<!-- CRR / SRR -->
<div class="dash-card mini">
<div class="mini-icon blue"><fa-icon icon="lock"></fa-icon></div>
<div class="mini-label">CRR / SRR</div>
<div class="mini-value">&#8377; 20.14 Cr</div>
<div class="mini-sub">8% <span class="tag-success">Within Limits</span></div>
<!-- CRR / SLR -->
<div class="dash-card mini crr-slr-card">
<div class="mini-header-row">
<div class="mini-icon blue"><fa-icon icon="lock"></fa-icon></div>
<div class="mini-label">CRR / SLR</div>
<button class="view-details-btn" (click)="openCrrSlrDialog()" matTooltip="View Details">
<fa-icon icon="external-link-alt"></fa-icon> View
</button>
</div>
<div class="card-metrics">
<div class="metric-row">
<span class="metric-label">CRR Required (3%)</span>
<span class="metric-value">&#8377;2,077 Cr</span>
</div>
<div class="metric-row">
<span class="metric-label">SLR Required (18%)</span>
<span class="metric-value">&#8377;12,460 Cr</span>
</div>
</div>
<div class="mini-sub"><span class="tag-success">Within Limits</span></div>
</div>
<!-- CRAR -->
<div class="dash-card mini">
<div class="mini-icon green"><fa-icon icon="chart-line"></fa-icon></div>
<div class="mini-label">CRAR %</div>
<div class="mini-value">21%</div>
<div class="dash-card mini crr-slr-card">
<div class="mini-header-row">
<div class="mini-icon green"><fa-icon icon="chart-line"></fa-icon></div>
<div class="mini-label">CRAR %</div>
<button class="view-details-btn" (click)="openCrarDialog()" matTooltip="View Details">
<fa-icon icon="external-link-alt"></fa-icon> View
</button>
</div>
<div class="card-metrics">
<div class="metric-row">
<span class="metric-label">CRAR (Basel III)</span>
<span class="metric-value">16.84%</span>
</div>
<div class="metric-row">
<span class="metric-label">CET1 Ratio</span>
<span class="metric-value">12.42%</span>
</div>
</div>
<div class="mini-sub"><span class="tag-success">Healthy (Min 9%)</span></div>
</div>
@@ -208,7 +208,51 @@ $purple: #7c3aed;
}
}
/* ─── CRR/SRR Metric Rows ─── */
/* ─── CRR/SLR Card Header Row ─── */
.mini-header-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
width: 100%;
.mini-icon {
margin-bottom: 0;
}
.mini-label {
margin-bottom: 0;
flex: 1;
}
}
.view-details-btn {
height: 24px;
padding: 0 10px;
border-radius: 12px;
border: 1px solid #bfdbfe;
background: #eff6ff;
color: #1d4ed8;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
font-size: 10px;
font-weight: 600;
font-family: 'Roboto Mono', monospace;
transition: all 0.15s;
flex-shrink: 0;
white-space: nowrap;
&:hover {
background: #1d4ed8;
color: #fff;
border-color: #1d4ed8;
}
}
/* ─── CRR/SLR Metric Rows ─── */
.card-metrics {
margin-bottom: 10px;
}
@@ -14,6 +14,8 @@ import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';
import { activities } from '../activities';
import { MatAutocompleteTrigger, MatAutocomplete, MatOption } from '@angular/material/autocomplete';
import { MatDialog } from '@angular/material/dialog';
import { MatTooltipModule } from '@angular/material/tooltip';
import { AsyncPipe, DatePipe } from '@angular/common';
import { ClientTrendsBarComponent } from './client-trends-bar/client-trends-bar.component';
import { AmountDisbursedPieComponent } from './amount-disbursed-pie/amount-disbursed-pie.component';
@@ -32,6 +34,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
...STANDALONE_SHARED_IMPORTS,
MatAutocompleteTrigger,
MatAutocomplete,
MatTooltipModule,
ClientTrendsBarComponent,
AmountDisbursedPieComponent,
AmountCollectedPieComponent,
@@ -42,6 +45,7 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
})
export class DashboardComponent implements OnInit {
private router = inject(Router);
private dialog = inject(MatDialog);
today = new Date();
@@ -147,4 +151,29 @@ export class DashboardComponent implements OnInit {
const filterValue = activityName.toLowerCase();
return this.allActivities.filter((activity) => activity.activity.toLowerCase().indexOf(filterValue) === 0);
}
/**
* Opens the CRR/SLR details dialog with full liabilities breakdown.
*/
async openCrrSlrDialog(): Promise<void> {
const { CrrSlrDialogComponent } = await import('./crr-slr-dialog/crr-slr-dialog.component');
this.dialog.open(CrrSlrDialogComponent, {
width: '95vw',
maxWidth: '1100px',
maxHeight: '90vh',
panelClass: 'crr-slr-dialog-panel',
autoFocus: false
});
}
async openCrarDialog(): Promise<void> {
const { CrarDialogComponent } = await import('./crar-dialog/crar-dialog.component');
this.dialog.open(CrarDialogComponent, {
width: '95vw',
maxWidth: '1360px',
maxHeight: '90vh',
panelClass: 'crr-slr-dialog-panel',
autoFocus: false
});
}
}
+120 -56
View File
@@ -6,65 +6,137 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="layout-row align-end gap-2px responsive-column container m-b-20">
<div #buttonDashboard class="in-block">
<button mat-raised-button color="primary" [routerLink]="['/dashboard']">
<fa-icon icon="tachometer-alt" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Dashboard' | translate }}
</button>
<div class="home-container">
<!-- Welcome Banner -->
<div class="welcome-banner">
<div class="welcome-left">
<div class="welcome-greeting">
<h2>{{ 'labels.text.Welcome' | translate }}, <strong>{{ username }}</strong></h2>
<p class="welcome-sub">{{ tenant | titlecase }} &middot; CBSNexora Core Banking Platform</p>
</div>
</div>
<div class="welcome-right">
<div #searchActivity class="search-box">
<fa-icon icon="search" class="search-icon"></fa-icon>
<input matInput
placeholder="{{ 'labels.placeholders.Search Activity' | translate }}"
[matAutocomplete]="activitySearch"
[formControl]="searchText"
class="search-input" />
<mat-autocomplete #activitySearch="matAutocomplete">
@for (activity of filteredActivities | async; track activity) {
<mat-option [routerLink]="activity.path">
<span>{{ activity.activity }}</span>
</mat-option>
}
</mat-autocomplete>
</div>
</div>
</div>
<!-- Quick Action Cards -->
<div class="quick-actions">
<a class="qa-card" [routerLink]="['/dashboard']" #buttonDashboard>
<div class="qa-icon blue"><fa-icon icon="tachometer-alt"></fa-icon></div>
<div class="qa-info">
<span class="qa-title">{{ 'labels.buttons.Dashboard' | translate }}</span>
<span class="qa-desc">Analytics & Reports</span>
</div>
</a>
<a class="qa-card" [routerLink]="['/clients/create']">
<div class="qa-icon green"><fa-icon icon="user-plus"></fa-icon></div>
<div class="qa-info">
<span class="qa-title">New Client</span>
<span class="qa-desc">Onboard Customer</span>
</div>
</a>
<a class="qa-card" [routerLink]="['/loans']">
<div class="qa-icon purple"><fa-icon icon="hand-holding-usd"></fa-icon></div>
<div class="qa-info">
<span class="qa-title">Loans</span>
<span class="qa-desc">Manage Advances</span>
</div>
</a>
<a class="qa-card" [routerLink]="['/savings']">
<div class="qa-icon amber"><fa-icon icon="piggy-bank"></fa-icon></div>
<div class="qa-info">
<span class="qa-title">Savings</span>
<span class="qa-desc">Deposit Accounts</span>
</div>
</a>
<a class="qa-card" [routerLink]="['/accounting']">
<div class="qa-icon teal"><fa-icon icon="calculator"></fa-icon></div>
<div class="qa-info">
<span class="qa-title">Accounting</span>
<span class="qa-desc">GL & Journals</span>
</div>
</a>
<a class="qa-card" [routerLink]="['/reports']">
<div class="qa-icon red"><fa-icon icon="chart-bar"></fa-icon></div>
<div class="qa-info">
<span class="qa-title">Reports</span>
<span class="qa-desc">MIS & Statements</span>
</div>
</a>
</div>
<!-- Info Row -->
<div class="info-row">
<div class="info-card">
<div class="info-card-header">
<fa-icon icon="clock" class="info-icon"></fa-icon>
<span>Today's Summary</span>
</div>
<div class="info-grid">
<div class="info-item">
<span class="info-label">Date</span>
<span class="info-value">{{ today | date: 'dd MMM yyyy' }}</span>
</div>
<div class="info-item">
<span class="info-label">Branch</span>
<span class="info-value">Head Office</span>
</div>
<div class="info-item">
<span class="info-label">User</span>
<span class="info-value">{{ username }}</span>
</div>
<div class="info-item">
<span class="info-label">Tenant</span>
<span class="info-value">{{ tenant | titlecase }}</span>
</div>
</div>
</div>
<div class="info-card">
<div class="info-card-header">
<fa-icon icon="bolt" class="info-icon"></fa-icon>
<span>Quick Links</span>
</div>
<div class="quick-links">
<a [routerLink]="['/tasks']">Checker Inbox & Tasks</a>
<a [routerLink]="['/organization']">Organization Setup</a>
<a [routerLink]="['/products']">Product Configuration</a>
<a [routerLink]="['/system']">System Administration</a>
<a [routerLink]="['/users']">User Management</a>
<a [routerLink]="['/settings']">Settings</a>
</div>
</div>
</div>
</div>
<div class="container">
<mat-card>
<mat-card-header>
<mat-card-title class="title">
{{ 'labels.text.Welcome' | translate }} {{ username }} {{ 'labels.text.To' | translate }}
{{ tenant | titlecase }}!
</mat-card-title>
</mat-card-header>
<mat-card-content #searchActivity>
<div class="layout-column">
<mat-form-field>
<mat-label>{{ 'labels.placeholders.Search Activity' | translate }}</mat-label>
<input matInput [matAutocomplete]="activitySearch" [formControl]="searchText" />
</mat-form-field>
</div>
<mat-autocomplete #activitySearch="matAutocomplete">
@for (activity of filteredActivities | async; track activity) {
<mat-option [routerLink]="activity.path">
<span>{{ activity.activity }}</span>
</mat-option>
}
</mat-autocomplete>
</mat-card-content>
<img mat-card-image src="assets/images/{{ tenant }}_home.png" alt="Mifos X" />
</mat-card>
</div>
<!-- Configuration Wizard Popovers (kept for backward compat) -->
<ng-template #popoverButtons let-popover="popover" let-prev="prev" let-next="next">
<div class="layout-row align-end gap-2px responsive-column">
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">
{{ 'labels.buttons.Close' | translate }}
</button>
@if (prev) {
<button
mat-raised-button
color="primary"
(click)="popover.close(); showPopover(prev.template, prev.target, 'bottom', true)"
>
<button mat-raised-button color="primary" (click)="popover.close(); showPopover(prev.template, prev.target, 'bottom', true)">
{{ 'labels.buttons.Back' | translate }}
</button>
}
@if (next) {
<button
mat-raised-button
color="primary"
(click)="popover.close(); showPopover(next.template, next.target, 'bottom', true)"
>
<button mat-raised-button color="primary" (click)="popover.close(); showPopover(next.template, next.target, 'bottom', true)">
{{ 'labels.buttons.Next' | translate }}
</button>
}
@@ -81,11 +153,7 @@
<button mat-raised-button color="primary" (click)="popover.close(); previousStep()">
{{ 'labels.buttons.Back' | translate }}
</button>
<button
mat-raised-button
color="primary"
(click)="popover.close(); showPopover(templateSearchActivity, searchActivity, 'top', true)"
>
<button mat-raised-button color="primary" (click)="popover.close(); showPopover(templateSearchActivity, searchActivity, 'top', true)">
{{ 'labels.buttons.Next' | translate }}
</button>
</div>
@@ -98,11 +166,7 @@
<button mat-raised-button color="warn" (click)="popover.close(); configurationWizardService.closeConfigWizard()">
{{ 'labels.buttons.Close' | translate }}
</button>
<button
mat-raised-button
color="primary"
(click)="popover.close(); showPopover(templateButtonDashboard, buttonDashboard, 'bottom', true)"
>
<button mat-raised-button color="primary" (click)="popover.close(); showPopover(templateButtonDashboard, buttonDashboard, 'bottom', true)">
{{ 'labels.buttons.Back' | translate }}
</button>
<button mat-raised-button color="primary" (click)="popover.close(); nextStep()">
+293 -10
View File
@@ -6,21 +6,304 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
.container {
max-width: 37rem;
$brand-dark: #0d3d78;
$accent: #0ea5e9;
.title {
font-size: 1.25rem;
font-weight: 500;
margin-left: -15px;
.home-container {
max-width: 1100px;
margin: 0 auto;
padding: 20px 16px;
}
/* ── Welcome Banner ── */
.welcome-banner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
padding: 20px 28px;
background: linear-gradient(135deg, $brand-dark 0%, #1a5276 100%);
border-radius: 10px;
margin-bottom: 20px;
position: relative;
overflow: hidden;
&::after {
content: '';
position: absolute;
top: 0; left: 0; right: 0; bottom: 0;
background: repeating-linear-gradient(108deg, transparent 0, transparent 80px, rgb(255 255 255 / 2.5%) 80px, rgb(255 255 255 / 2.5%) 160px);
pointer-events: none;
}
}
.mat-list-base {
padding-top: 0;
.welcome-left {
position: relative;
z-index: 1;
}
a {
.welcome-greeting {
h2 {
margin: 0;
font-size: 20px;
font-weight: 400;
color: rgba(255, 255, 255, 0.9);
strong {
font-weight: 700;
color: #fff;
}
}
}
.welcome-sub {
margin: 4px 0 0;
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
}
.welcome-right {
position: relative;
z-index: 1;
flex-shrink: 0;
}
.search-box {
display: flex;
align-items: center;
gap: 8px;
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.15);
border-radius: 6px;
padding: 6px 14px;
min-width: 280px;
transition: background 0.15s;
&:focus-within {
background: rgba(255, 255, 255, 0.18);
border-color: rgba(255, 255, 255, 0.3);
}
}
.search-icon {
color: rgba(255, 255, 255, 0.5);
font-size: 13px;
}
.search-input {
background: transparent;
border: none;
outline: none;
color: #fff;
font-size: 13px;
width: 100%;
&::placeholder {
color: rgba(255, 255, 255, 0.45);
}
}
/* ── Quick Action Cards ── */
.quick-actions {
display: grid;
grid-template-columns: repeat(6, 1fr);
gap: 12px;
margin-bottom: 20px;
}
.qa-card {
display: flex;
align-items: center;
gap: 10px;
padding: 14px 14px;
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 8px;
text-decoration: none;
color: black;
transition: box-shadow 0.15s, transform 0.15s;
cursor: pointer;
&:hover {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
transform: translateY(-1px);
}
}
.qa-icon {
width: 36px;
height: 36px;
border-radius: 8px;
display: flex;
align-items: center;
justify-content: center;
font-size: 15px;
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; }
}
.qa-info {
display: flex;
flex-direction: column;
min-width: 0;
}
.qa-title {
font-size: 13px;
font-weight: 600;
color: #1a1a1a;
white-space: nowrap;
}
.qa-desc {
font-size: 10px;
color: #999;
white-space: nowrap;
}
/* ── Info Row ── */
.info-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
}
.info-card {
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.06);
border-radius: 8px;
overflow: hidden;
}
.info-card-header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 16px;
background: #fafafa;
border-bottom: 1px solid rgba(0, 0, 0, 0.04);
font-size: 13px;
font-weight: 600;
color: #333;
.info-icon {
color: $accent;
font-size: 13px;
}
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
padding: 4px 0;
}
.info-item {
display: flex;
flex-direction: column;
padding: 8px 16px;
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
&:nth-last-child(-n+2) {
border-bottom: none;
}
}
.info-label {
font-size: 10px;
color: #999;
font-weight: 500;
}
.info-value {
font-size: 13px;
color: #1a1a1a;
font-weight: 600;
}
.quick-links {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
padding: 4px 0;
a {
font-size: 12px;
color: $brand-dark;
text-decoration: none;
padding: 7px 16px;
transition: background 0.1s;
border-bottom: 1px solid rgba(0, 0, 0, 0.03);
&:hover {
background: #f0f9ff;
color: $accent;
}
&:nth-last-child(-n+2) {
border-bottom: none;
}
}
}
/* ── Dark Theme ── */
:host-context(.dark-theme) {
.qa-card, .info-card {
background: #1e1e2e;
border-color: rgba(255, 255, 255, 0.06);
}
.qa-title { color: #e0e0e0; }
.qa-desc { color: #666; }
.info-card-header { background: #252536; }
.info-value { color: #e0e0e0; }
.info-label { color: #666; }
.quick-links a {
color: #7dd3fc;
&:hover { background: rgba(255, 255, 255, 0.04); }
}
.search-input {
color: #fff;
}
}
/* ── Responsive ── */
@media (max-width: 1024px) {
.quick-actions {
grid-template-columns: repeat(3, 1fr);
}
}
@media (max-width: 768px) {
.welcome-banner {
flex-direction: column;
align-items: stretch;
padding: 16px 20px;
}
.search-box {
min-width: unset;
}
.quick-actions {
grid-template-columns: repeat(2, 1fr);
}
.info-row {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.quick-actions {
grid-template-columns: 1fr;
}
}
+6 -6
View File
@@ -29,9 +29,8 @@ import { SettingsService } from 'app/settings/settings.service';
/** Custom Components */
import { NextStepDialogComponent } from '../configuration-wizard/next-step-dialog/next-step-dialog.component';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
import { MatCard, MatCardHeader, MatCardTitle, MatCardContent, MatCardImage } from '@angular/material/card';
import { MatAutocompleteTrigger, MatAutocomplete, MatOption } from '@angular/material/autocomplete';
import { AsyncPipe } from '@angular/common';
import { AsyncPipe, DatePipe, TitleCasePipe } from '@angular/common';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
/**
@@ -44,12 +43,11 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
MatCardHeader,
MatCardTitle,
MatAutocompleteTrigger,
MatAutocomplete,
MatCardImage,
AsyncPipe
AsyncPipe,
DatePipe,
TitleCasePipe
]
})
export class HomeComponent implements OnInit, AfterViewInit {
@@ -65,6 +63,8 @@ export class HomeComponent implements OnInit, AfterViewInit {
username: string;
/** Tenant name */
tenant: string;
/** Today's date */
today = new Date();
/** Activity Form. */
activityForm: any;
/** Search Text. */
@@ -62,6 +62,15 @@
}
</mat-form-field>
<!-- Remember Me & Forgot Password Row -->
@if (enableRememberMe) {
<div class="login-options-row">
<mat-checkbox formControlName="remember" class="remember-me-checkbox">
{{ 'labels.inputs.Remember me' | translate }}
</mat-checkbox>
</div>
}
<!-- Loading Progress -->
@if (loading) {
<mat-progress-bar mode="indeterminate" class="login-progress"></mat-progress-bar>
@@ -1,4 +1,4 @@
/**
/**
* Copyright since 2025 Mifos Initiative
*
* This Source Code Form is subject to the terms of the Mozilla Public
@@ -7,7 +7,7 @@
*/
/**
* Modern Material Design 3 Login Form Styles
* Glassmorphism Login Form Styles
*/
// ===== Main Form Container =====
@@ -38,41 +38,40 @@
width: 100%;
margin-bottom: 0;
// Apply same styling as login fields
.mat-mdc-text-field-wrapper {
border-radius: 12px 12px 0 0;
border-radius: 14px 14px 0 0;
background: rgb(255 255 255 / 8%);
}
.mat-mdc-form-field-focus-overlay {
border-radius: 12px 12px 0 0;
background-color: var(--md-sys-color-on-surface, #1a1c1e);
opacity: 0.04;
border-radius: 14px 14px 0 0;
background-color: rgb(255 255 255 / 5%);
}
&.mat-focused {
.mat-mdc-form-field-focus-overlay {
opacity: 0.12;
background-color: rgb(255 255 255 / 8%);
}
}
.mdc-line-ripple::after {
border-bottom-color: var(--md-sys-color-primary, #1074b9);
border-bottom-color: #0ea5e9;
}
.mat-mdc-select {
font-size: 1rem;
font-weight: 400;
color: var(--md-sys-color-on-surface, #1a1c1e);
color: #fff;
}
.mat-mdc-floating-label {
font-size: 1rem;
color: var(--md-sys-color-on-surface-variant, #44474e);
color: rgb(255 255 255 / 60%);
}
.mat-mdc-form-field-icon-prefix {
padding: 0 0.75rem 0 0.5rem;
color: var(--md-sys-color-on-surface-variant, #44474e);
color: rgb(255 255 255 / 50%);
fa-icon {
font-size: 1.25rem;
@@ -95,53 +94,54 @@
::ng-deep {
// Field container
.mat-mdc-text-field-wrapper {
border-radius: 12px 12px 0 0;
border-radius: 14px 14px 0 0;
background: rgb(255 255 255 / 8%);
}
// Focus overlay
.mat-mdc-form-field-focus-overlay {
border-radius: 12px 12px 0 0;
background-color: var(--md-sys-color-on-surface, #1a1c1e);
opacity: 0.04;
border-radius: 14px 14px 0 0;
background-color: rgb(255 255 255 / 5%);
}
// Focus state
&.mat-focused {
.mat-mdc-form-field-focus-overlay {
opacity: 0.12;
background-color: rgb(255 255 255 / 8%);
}
}
// Bottom indicator line
.mdc-line-ripple::after {
border-bottom-color: var(--md-sys-color-primary, #1074b9);
border-bottom-color: #0ea5e9;
}
// Input field
.mat-mdc-input-element {
font-size: 1rem;
font-weight: 400;
color: var(--md-sys-color-on-surface, #1a1c1e);
color: #fff;
padding: 0.875rem 0;
margin-left: 0;
caret-color: #0ea5e9;
&::placeholder {
color: var(--md-sys-color-on-surface-variant, #44474e);
opacity: 0.6;
color: rgb(255 255 255 / 40%);
opacity: 1;
}
}
// Label
.mat-mdc-floating-label {
font-size: 1rem;
color: var(--md-sys-color-on-surface-variant, #44474e);
color: rgb(255 255 255 / 60%);
left: 0 !important;
}
// Prefix icon
.mat-mdc-form-field-icon-prefix {
padding: 0 0.75rem 0 0.5rem;
color: var(--md-sys-color-on-surface-variant, #44474e);
color: rgb(255 255 255 / 50%);
fa-icon {
font-size: 1.25rem;
@@ -149,7 +149,7 @@
}
}
// Form field infix - align label with input
// Form field infix
.mat-mdc-form-field-infix {
padding-left: 0;
}
@@ -159,19 +159,23 @@
button {
background: transparent;
border: none;
color: var(--md-sys-color-on-surface-variant, #44474e);
color: rgb(255 255 255 / 50%);
transition: all 0.2s ease;
&:hover {
color: rgb(255 255 255 / 80%);
}
}
}
// Error state
&.mat-form-field-invalid {
.mdc-line-ripple::after {
border-bottom-color: var(--md-sys-color-error, #ba1a1a);
border-bottom-color: #f87171;
}
.mat-mdc-form-field-icon-prefix fa-icon {
color: var(--md-sys-color-error, #ba1a1a);
color: #f87171;
}
}
@@ -179,7 +183,39 @@
.mat-mdc-form-field-error {
font-size: 0.75rem;
margin-top: 0.25rem;
color: var(--md-sys-color-error, #ba1a1a);
color: #f87171;
}
}
}
// ===== Remember Me & Options Row =====
.login-options-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0.25rem 0;
margin-top: 0.25rem;
}
.remember-me-checkbox {
::ng-deep {
.mdc-checkbox {
.mdc-checkbox__native-control:enabled:not(:checked):not(:indeterminate):not([data-indeterminate='true'])
~ .mdc-checkbox__background {
border-color: rgb(255 255 255 / 40%);
}
.mdc-checkbox__native-control:enabled:checked ~ .mdc-checkbox__background,
.mdc-checkbox__native-control:enabled:indeterminate ~ .mdc-checkbox__background {
background-color: #0ea5e9;
border-color: #0ea5e9;
}
}
.mdc-label {
color: rgb(255 255 255 / 70%);
font-size: 0.85rem;
font-weight: 400;
}
}
}
@@ -192,7 +228,11 @@
::ng-deep {
.mdc-linear-progress__bar-inner {
border-color: var(--md-sys-color-primary, #1074b9);
border-color: #0ea5e9;
}
.mdc-linear-progress__buffer-bar {
background-color: rgb(255 255 255 / 10%);
}
}
}
@@ -200,13 +240,16 @@
// ===== Submit Button =====
.login-submit-button {
min-height: 48px;
color: var(--md-sys-color-on-surface-variant, #44474e);
margin-top: 0.5rem;
margin-top: 0.75rem;
::ng-deep md-filled-button {
font-size: 1rem;
font-weight: 500;
letter-spacing: 0.01em;
::ng-deep {
md-filled-button,
.mdc-button {
font-size: 1rem;
font-weight: 600;
letter-spacing: 0.02em;
border-radius: 14px;
}
}
.button-spinner {
@@ -233,7 +276,7 @@
.oidc-description {
font-size: 0.95rem;
color: var(--md-sys-color-on-surface-variant, #44474e);
color: rgb(255 255 255 / 60%);
margin: 0;
line-height: 1.5;
}
@@ -250,19 +293,20 @@
align-items: center;
gap: 1rem;
padding: 2rem;
background: var(--md-sys-color-primary-container, #d1e4ff);
background: rgb(14 165 233 / 15%);
border-radius: 16px;
border: 1px solid rgb(14 165 233 / 20%);
.success-icon {
font-size: 3rem;
color: var(--md-sys-color-primary, #1074b9);
color: #0ea5e9;
}
h3 {
margin: 0;
font-size: 1.25rem;
font-weight: 600;
color: var(--md-sys-color-on-primary-container, #001d36);
color: #fff;
}
}
@@ -279,9 +323,9 @@
.protected-data {
padding: 1rem;
background: var(--md-sys-color-surface-container, #f3f3f3);
background: rgb(255 255 255 / 6%);
border-radius: 12px;
border: 1px solid var(--md-sys-color-outline-variant, #c4c6d0);
border: 1px solid rgb(255 255 255 / 10%);
max-height: 300px;
overflow: auto;
@@ -289,83 +333,12 @@
margin: 0;
font-size: 0.875rem;
font-family: 'Courier New', monospace;
color: var(--md-sys-color-on-surface, #1a1c1e);
color: rgb(255 255 255 / 80%);
white-space: pre-wrap;
word-wrap: break-word;
}
}
// ===== Dark Theme Support =====
:host-context(.dark-theme) {
// Tenant selector dark theme
::ng-deep .tenant-section {
mifosx-tenant-selector {
#tenant-selector {
.mat-mdc-select {
color: var(--md-sys-color-on-surface, #e2e2e5);
}
.mat-mdc-floating-label {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
.mat-mdc-form-field-icon-prefix {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
}
}
}
.login-field {
::ng-deep {
.mat-mdc-input-element {
color: var(--md-sys-color-on-surface, #e2e2e5);
&::placeholder {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
}
.mat-mdc-floating-label {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
.mat-mdc-form-field-icon-prefix {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
.mat-mdc-form-field-icon-suffix button {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
}
}
.oidc-description {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
.welcome-message {
background: var(--md-sys-color-primary-container, #003a6b);
.success-icon {
color: var(--md-sys-color-primary, #5ba2ec);
}
h3 {
color: var(--md-sys-color-on-primary-container, #d1e4ff);
}
}
.protected-data {
background: var(--md-sys-color-surface-container, #2c2f36);
border-color: var(--md-sys-color-outline-variant, #44474e);
pre {
color: var(--md-sys-color-on-surface, #e2e2e5);
}
}
}
// ===== Responsive Design =====
/* stylelint-disable-next-line media-feature-range-notation -- Safari compatibility */
@media (max-width: 768px) {
@@ -381,8 +354,8 @@
margin-top: 0.5rem;
}
.login-actions {
margin-top: 0.5rem;
.login-options-row {
padding: 0.15rem 0;
}
}
@@ -416,6 +389,12 @@
margin-top: 0.4rem;
}
.remember-me-checkbox {
::ng-deep .mdc-label {
font-size: 0.8rem;
}
}
.oidc-actions {
flex-direction: column;
+113 -102
View File
@@ -1,132 +1,143 @@
<!--
<!--
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/.
-->
<!-- Modern Material Design 3 Login Layout -->
<!-- Modern Glassmorphism Login with Animated Gradient Background -->
<div class="login-wrapper">
<!-- Left Panel - Hero Section (Hidden on tablet and mobile) -->
<div class="hero-panel hide-lt-lg" [class.production-mode]="productionMode">
<div class="hero-overlay"></div>
<div class="hero-content">
<!-- Default Mode: Full content -->
@if (!productionMode) {
<div class="hero-text">
<h1 class="hero-title">{{ 'APP_NAME' | translate }}</h1>
<p class="hero-subtitle">
{{ 'labels.text.A' | translate }}
<span class="hero-highlight">{{ 'labels.text.global community' | translate }}</span>
{{ 'labels.text.elimination of poverty' | translate }}
</p>
<div class="hero-features">
<div class="feature-item">
<fa-icon icon="shield-alt" class="feature-icon"></fa-icon>
<span class="feature-text">{{ 'labels.heading.Secure & Reliable' | translate }}</span>
</div>
<div class="feature-item">
<fa-icon icon="users" class="feature-icon"></fa-icon>
<span class="feature-text">{{ 'labels.heading.Community Driven' | translate }}</span>
</div>
<div class="feature-item">
<fa-icon icon="globe" class="feature-icon"></fa-icon>
<span class="feature-text">{{ 'labels.heading.Global Impact' | translate }}</span>
</div>
</div>
</div>
}
<!-- Production Mode: Minimal branding at bottom -->
@if (productionMode) {
<div class="hero-branding-bottom">
<h1 class="hero-title-minimal">{{ 'APP_NAME' | translate }}</h1>
</div>
}
</div>
<!-- Animated Gradient Background -->
<div class="gradient-bg">
<div class="gradient-orb orb-1"></div>
<div class="gradient-orb orb-2"></div>
<div class="gradient-orb orb-3"></div>
<div class="gradient-orb orb-4"></div>
</div>
<!-- Right Panel - Login Form -->
<div class="login-panel">
<!-- Header Controls -->
<div class="login-header">
@if (allowServerSwitch()) {
<mifosx-server-selector class="header-control" appearance="outline" [showLabel]="false">
</mifosx-server-selector>
}
<mifosx-theme-toggle class="header-control theme-toggle"></mifosx-theme-toggle>
<mifosx-language-selector class="header-control" appearance="outline" [showLabel]="false">
</mifosx-language-selector>
</div>
<!-- Settings Icon - Top Right -->
<button
class="settings-trigger"
(click)="toggleSettingsPanel()"
matTooltip="Settings"
attr.aria-label="Settings"
>
<mifosx-m3-icon [name]="settingsPanelOpen ? 'close' : 'settings'"></mifosx-m3-icon>
</button>
<!-- Main Login Card -->
<div class="login-card-container">
<div class="login-card">
<!-- Logo Section -->
<div class="logo-section">
@if (!themeDarkEnabled) {
<img [src]="logoPath" (error)="onLogoError()" alt="{{ 'APP_NAME' | translate }} Logo" class="logo-image" />
} @else {
<img
[src]="logoPathDark"
(error)="onLogoErrorDark()"
alt="{{ 'APP_NAME' | translate }} Logo"
class="logo-image"
/>
}
<!-- Settings Popup Panel -->
@if (settingsPanelOpen) {
<div class="settings-overlay" (click)="toggleSettingsPanel()"></div>
<div class="settings-panel" @settingsAnimation>
<div class="settings-panel-header">
<h3 class="settings-title">
<mifosx-m3-icon name="tune"></mifosx-m3-icon>
{{ 'labels.heading.Settings' | translate }}
</h3>
</div>
<div class="settings-panel-body">
<!-- Theme Toggle -->
<div class="settings-item">
<span class="settings-label">
<mifosx-m3-icon name="dark_mode"></mifosx-m3-icon>
{{ 'labels.heading.Theme' | translate }}
</span>
<mifosx-theme-toggle></mifosx-theme-toggle>
</div>
<!-- Form Section -->
<div class="form-section">
<!-- Tenant Selector -->
@if (showTenantSelector) {
<div class="tenant-section">
<mifosx-tenant-selector></mifosx-tenant-selector>
</div>
}
<!-- Login Forms -->
@if (!resetPassword && !twoFactorAuthenticationRequired) {
<mifosx-login-form></mifosx-login-form>
}
@if (resetPassword) {
<mifosx-reset-password></mifosx-reset-password>
}
@if (twoFactorAuthenticationRequired) {
<mifosx-two-factor-authentication></mifosx-two-factor-authentication>
}
<!-- Language Selector -->
<div class="settings-item">
<span class="settings-label">
<mifosx-m3-icon name="language"></mifosx-m3-icon>
{{ 'labels.heading.Language' | translate }}
</span>
<mifosx-language-selector appearance="outline" [showLabel]="false"></mifosx-language-selector>
</div>
<!-- Version Info Section -->
<div class="login-version-info" *ngIf="displayBackendInfo"></div>
<!-- Resources Links (hidden in production mode) -->
@if (!productionMode) {
<div class="resources-section">
<div class="resources-branding"></div>
<!-- Server Selector -->
@if (allowServerSwitch()) {
<div class="settings-item">
<span class="settings-label">
<mifosx-m3-icon name="dns"></mifosx-m3-icon>
{{ 'labels.heading.Server' | translate }}
</span>
<mifosx-server-selector appearance="outline" [showLabel]="false"></mifosx-server-selector>
</div>
}
</div>
<!-- Footer -->
<div class="login-footer">
<div class="settings-panel-footer">
<button
mat-icon-button
mat-button
(click)="reloadSettings()"
class="reload-button"
title="Reload Settings"
matTooltip="Reload Settings"
attr.aria-label="Reload Settings"
class="reload-settings-btn"
>
<mifosx-m3-icon name="refresh"></mifosx-m3-icon>
{{ 'labels.buttons.Reload' | translate }}
</button>
</div>
</div>
}
<!-- Floating Glass Login Card -->
<div class="glass-card-container">
<div class="glass-card" @cardAnimation>
<!-- Logo Section -->
<div class="logo-section">
@if (!themeDarkEnabled) {
<img [src]="logoPath" (error)="onLogoError()" alt="{{ 'APP_NAME' | translate }} Logo" class="logo-image" />
} @else {
<img
[src]="logoPathDark"
(error)="onLogoErrorDark()"
alt="{{ 'APP_NAME' | translate }} Logo"
class="logo-image"
/>
}
</div>
<!-- Welcome Text -->
<div class="welcome-section">
<h1 class="welcome-title">{{ 'labels.heading.Welcome Back' | translate }}</h1>
<p class="welcome-subtitle">{{ 'labels.text.Sign in to continue' | translate }}</p>
</div>
<!-- Tenant Selector -->
@if (showTenantSelector) {
<div class="tenant-section">
<mifosx-tenant-selector></mifosx-tenant-selector>
</div>
}
<!-- Login Forms -->
@if (!resetPassword && !twoFactorAuthenticationRequired) {
<mifosx-login-form></mifosx-login-form>
}
@if (resetPassword) {
<mifosx-reset-password></mifosx-reset-password>
}
@if (twoFactorAuthenticationRequired) {
<mifosx-two-factor-authentication></mifosx-two-factor-authentication>
}
<!-- Version Info Section -->
<div class="login-version-info" *ngIf="displayBackendInfo"></div>
<!-- Branding Footer -->
@if (!productionMode) {
<div class="card-footer">
<span class="footer-text">
{{ 'APP_NAME' | translate }}
</span>
</div>
}
</div>
</div>
</div>
<!-- Resource Menus -->
<!-- Resource Menus (kept for compatibility) -->
<mat-menu #resourcesMenu="matMenu" class="modern-menu">
<a [href]="'userManual' | documentationLink" target="_blank" rel="noopener noreferrer" mat-menu-item>
<fa-icon icon="book-open" class="menu-icon"></fa-icon>
File diff suppressed because it is too large Load Diff
+28 -3
View File
@@ -9,6 +9,7 @@
/** Angular Imports */
import { Component, OnInit, OnDestroy, inject } from '@angular/core';
import { Router } from '@angular/router';
import { trigger, transition, style, animate } from '@angular/animations';
/** rxjs Imports */
@@ -56,6 +57,23 @@ import { VersionService } from '../system/version.service';
selector: 'mifosx-login',
templateUrl: './login.component.html',
styleUrls: ['./login.component.scss'],
animations: [
trigger('settingsAnimation', [
transition(':enter', [
style({ opacity: 0, transform: 'translateY(-10px) scale(0.95)' }),
animate('200ms cubic-bezier(0.4, 0, 0.2, 1)', style({ opacity: 1, transform: 'translateY(0) scale(1)' }))
]),
transition(':leave', [
animate('150ms cubic-bezier(0.4, 0, 0.2, 1)', style({ opacity: 0, transform: 'translateY(-10px) scale(0.95)' }))
])
]),
trigger('cardAnimation', [
transition(':enter', [
style({ opacity: 0, transform: 'translateY(30px) scale(0.96)' }),
animate('600ms cubic-bezier(0.4, 0, 0.2, 1)', style({ opacity: 1, transform: 'translateY(0) scale(1)' }))
])
])
],
imports: [
...STANDALONE_SHARED_IMPORTS,
LanguageSelectorComponent,
@@ -100,13 +118,16 @@ export class LoginComponent implements OnInit, OnDestroy {
return tenant.charAt(0).toUpperCase() + tenant.slice(1).toLowerCase();
}
/** Whether the settings panel popup is open */
settingsPanelOpen = false;
/** True if password requires a reset. */
resetPassword = false;
/** True if user requires two factor authentication. */
twoFactorAuthenticationRequired = false;
/** Subscription to alerts. */
alert$: Subscription;
logoPath = 'assets/images/default_home.png';
logoPath = 'assets/images/cbsnexora-logo.png';
logoPathDark = 'assets/images/white-mifos.png';
/** Subscription to theme changes. */
theme$: Subscription;
@@ -188,6 +209,10 @@ export class LoginComponent implements OnInit, OnDestroy {
}
}
toggleSettingsPanel(): void {
this.settingsPanelOpen = !this.settingsPanelOpen;
}
reloadSettings(): void {
this.settingsService.setTenantIdentifier('');
this.settingsService.setTenantIdentifier(environment.fineractPlatformTenantId || 'default');
@@ -225,7 +250,7 @@ export class LoginComponent implements OnInit, OnDestroy {
if (environment.tenantLogoUrl && environment.tenantLogoUrl.trim() !== '') {
this.logoPath = environment.tenantLogoUrl;
} else {
this.logoPath = isTenantSpecific ? `assets/images/${tenant}_home.png` : 'assets/images/default_home.png';
this.logoPath = isTenantSpecific ? `assets/images/${tenant}_home.png` : 'assets/images/cbsnexora-logo.png';
}
// Set dark mode logo (env override takes priority)
@@ -237,7 +262,7 @@ export class LoginComponent implements OnInit, OnDestroy {
}
onLogoError(): void {
this.logoPath = 'assets/images/default_home.png';
this.logoPath = 'assets/images/cbsnexora-logo.png';
}
onLogoErrorDark(): void {
@@ -8,17 +8,36 @@
#reset-password-form {
.reset-password-input {
width: 14rem;
width: 100%;
margin-top: 0.6rem;
::ng-deep {
.mat-mdc-text-field-wrapper {
background: rgb(255 255 255 / 8%);
border-radius: 14px 14px 0 0;
}
.mat-mdc-input-element {
color: #fff;
}
.mat-mdc-floating-label {
color: rgb(255 255 255 / 60%);
}
.mdc-line-ripple::after {
border-bottom-color: #0ea5e9;
}
}
}
.reset-password-button {
width: 14rem;
width: 100%;
margin-top: 1rem;
}
.reset-password-error {
color: #f44336;
color: #f87171;
font-size: 80%;
margin: 0;
}
@@ -18,7 +18,7 @@
p {
text-align: center;
font-size: 1.1rem;
color: var(--md-sys-color-on-surface, #1a1c1e);
color: rgb(255 255 255 / 90%);
margin: 0 0 1rem;
strong {
@@ -36,7 +36,7 @@ mat-divider {
p:not(:first-of-type) {
text-align: center;
font-size: 0.95rem;
color: var(--md-sys-color-on-surface-variant, #44474e);
color: rgb(255 255 255 / 65%);
margin-bottom: 1rem;
}
+161 -53
View File
@@ -1,4 +1,4 @@
<!--
<!--
Copyright since 2025 Mifos Initiative
This Source Code Form is subject to the terms of the Mozilla Public
@@ -14,12 +14,12 @@
<span class="info-label">{{ 'APP_NAME' | translate }}</span>
<span class="info-value">{{ versions.mifos }}</span>
</span>
<span class="info-separator">•</span>
<span class="info-separator">&bull;</span>
<span class="info-item">
<span class="info-label">{{ 'labels.text.Fineract' | translate }}:</span>
<span class="info-value">{{ versions.fineract.version }}</span>
</span>
<span class="info-separator">•</span>
<span class="info-separator">&bull;</span>
<span class="info-item">
<span class="info-label">{{ 'labels.inputs.Server' | translate }}:</span>
<span class="info-value">{{ server }}</span>
@@ -29,56 +29,164 @@
</div>
} @else {
<!-- Default mode for Shell -->
@if (displayBackEndInfo) {
<div class="layout-column m-b-20 f12" ngClass="{{ styleClass }}" id="footer">
<div class="layout-column m-t-10 m-b-10 content-wrapper footer-center">
<table class="versions">
<tr>
<td class="footer-content">{{ 'APP_NAME' | translate }}</td>
<td class="right footer-content">
{{ versions.mifos }} - <b>{{ hash }}</b>
</td>
</tr>
<tr>
<td class="footer-content">{{ 'labels.text.Fineract' | translate }}</td>
<td class="right footer-content">
{{ versions.fineract.version }} - <b>{{ versions.fineract.hash }}</b>
</td>
</tr>
<tr>
<td class="footer-content">{{ 'labels.inputs.Server' | translate }}</td>
<td class="right footer-content">
{{ server }}
</td>
</tr>
<tr>
<td class="footer-content">{{ 'labels.version.Username' | translate }}</td>
<td class="right footer-content">
{{ username }}
</td>
</tr>
<tr>
<td class="footer-content">{{ 'labels.version.Name' | translate }}</td>
<td class="right footer-content">
{{ name }}
</td>
</tr>
<tr>
<td class="footer-content">{{ 'labels.version.Render Time' | translate }}</td>
<td class="right footer-content">
{{ renderTime | date: 'dd MMMM yyyy HH:mm:ss' }}
</td>
</tr>
@if (isBusinessDateDefined) {
<tr>
<td class="footer-content">{{ 'labels.text.Current Business Date' | translate }}:</td>
<td class="center footer-content business-date">
<b>{{ businessDate | date: 'EEEE, MMMM dd, y' }}</b>
</td>
</tr>
}
</table>
<!-- Security Bar -->
<div class="security-bar">
<div class="security-bar-left">
<div class="sec-brand">
<span class="sec-brand-name">CBS<em>Nexora</em></span>
<span class="sec-brand-tagline">Core Banking Solution — Next-Gen Operations & Reporting Architecture</span>
</div>
<div class="sec-item">
<span class="sec-dot"></span>
<span class="sec-label">SSL/TLS</span>
<span class="sec-val">256-bit Encrypted</span>
</div>
<div class="sec-item">
<span class="sec-dot"></span>
<span class="sec-label">ISO 27001</span>
<span class="sec-val">Certified</span>
</div>
<div class="sec-item">
<span class="sec-dot amber"></span>
<span class="sec-label">Session expires in</span>
<span class="session-timer">{{ sessionTimerDisplay }}</span>
</div>
</div>
}
<div class="security-bar-right">
<span class="sec-version">v{{ versions.mifos }}</span>
<span class="env-tag">PROD</span>
</div>
</div>
<!-- LincShield Certifications Bar -->
<div class="lincshield-bar">
<span class="ls-label">Certified &amp; Integrated</span>
<div class="ls-badges">
<span class="ls-badge green">&#10003; CFCFRMS Integrated</span>
<span class="ls-badge gold">&#10003; HSM-Secured &middot; SafeNet Luna K6</span>
<span class="ls-badge blue">&#10003; NPCI NFS Compliant</span>
<span class="ls-badge white">&#10003; RBI COU + BOU Licensed</span>
<span class="ls-badge green">&#10003; PCI-DSS Level 1</span>
</div>
</div>
<footer class="app-footer" id="footer">
<!-- Regulatory & Legal Section -->
<div class="regulatory-section">
<h5 class="regulatory-title">Regulatory & Legal</h5>
<div class="regulatory-grid">
<div class="reg-card">
<span class="reg-icon">&#127963;</span>
<div class="reg-detail">
<span class="reg-label">RBI License &middot; PSRLPS2019001234</span>
</div>
<span class="reg-tag rbi">RBI</span>
</div>
<div class="reg-card">
<span class="reg-icon">&#128203;</span>
<div class="reg-detail">
<span class="reg-label">Regulated Entity Disclaimer</span>
</div>
</div>
<div class="reg-card">
<span class="reg-icon">&#128274;</span>
<div class="reg-detail">
<span class="reg-label">Data Localisation — RBI/2018-19/153</span>
</div>
<span class="reg-tag circular">Circular</span>
</div>
<div class="reg-card">
<span class="reg-icon">&#9878;&#65039;</span>
<div class="reg-detail">
<span class="reg-label">Grievance Redressal Policy</span>
</div>
<span class="reg-tag mandatory">Mandatory</span>
</div>
</div>
</div>
<div class="footer-divider"></div>
<!-- Quick Navigation Section -->
<div class="quicknav-section">
<h5 class="quicknav-title">Quick Navigation</h5>
<div class="quicknav-grid">
<a class="qn-card" href="#">
<span class="qn-icon">&#128185;</span>
<span class="qn-label">Interest Rates</span>
</a>
<a class="qn-card" href="#">
<span class="qn-icon">&#128179;</span>
<span class="qn-label">Service Charges & Fees</span>
</a>
<a class="qn-card" href="#">
<span class="qn-icon">&#128229;</span>
<span class="qn-label">Forms & Downloads</span>
</a>
<a class="qn-card" href="#">
<span class="qn-icon">&#129518;</span>
<span class="qn-label">Loan EMI Calculator</span>
<span class="qn-tag">Tool</span>
</a>
<a class="qn-card" href="#">
<span class="qn-icon">&#128270;</span>
<span class="qn-label">IFSC / MICR Lookup</span>
<span class="qn-tag">Tool</span>
</a>
<a class="qn-card" href="#">
<span class="qn-icon">&#128205;</span>
<span class="qn-label">Branch & ATM Locator</span>
</a>
</div>
</div>
<div class="footer-divider"></div>
<!-- Support & Contact Section -->
<div class="support-section">
<h5 class="support-title">Support & Contact</h5>
<div class="support-row">
<div class="sc-chip">
<span class="sc-icon">&#128222;</span>
<span class="sc-label">Helpline 24&times;7</span>
<span class="sc-val highlight">1800-123-4567</span>
</div>
<div class="sc-chip">
<span class="sc-icon">&#9878;&#65039;</span>
<span class="sc-label">Grievance Officer</span>
<span class="sc-val">Aditya Vyas &middot; grievance&#64;lincpay.in</span>
</div>
<div class="sc-chip">
<span class="sc-icon">&#9993;&#65039;</span>
<span class="sc-label">Email</span>
<span class="sc-val">support&#64;lincpay.in</span>
</div>
<a class="sc-chip sc-chat" href="#">
<span class="sc-icon">&#128172;</span>
<span class="sc-val">Live Chat &rarr;</span>
</a>
</div>
</div>
<div class="footer-divider"></div>
<div class="footer-bottom">
<span class="fb-copy">&copy; {{ currentYear }} LincPay Solutions Pvt. Ltd. All rights reserved.</span>
<div class="fb-links">
<a href="#">Terms</a>
<a href="#">Privacy</a>
<a href="#">Cookies</a>
<a href="#">Grievance</a>
<a href="#">Fair Practices</a>
<a href="#">Sitemap</a>
</div>
<div class="fb-meta">
<span>App <strong>v{{ versions.mifos }}</strong></span>
<span>&middot;</span>
<span>Build <strong>{{ buildDate }}</strong></span>
</div>
<span class="fb-powered">Powered by <strong>LincAI</strong></span>
</div>
</footer>
}
+667 -62
View File
@@ -6,8 +6,9 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
@use 'assets/styles/helper';
@use 'assets/styles/colours' as *;
$brand-dark: #0d3d78;
$accent: #0ea5e9;
$accent-2: #10b981;
// ===== Compact Mode (Login) =====
.footer-compact {
@@ -47,95 +48,699 @@
opacity: 0.4;
font-size: 0.875rem;
}
.footer-copyright {
font-size: 0.875rem;
opacity: 0.7;
margin-top: 0.25rem;
}
}
// Dark theme support for compact mode
.dark-theme .footer-compact {
color: var(--md-sys-color-on-surface-variant, #c4c6d0);
}
// ===== Default Mode (Shell) =====
#footer {
// ===== Security Bar =====
.security-bar {
display: flex;
justify-content: space-between;
align-items: center;
min-width: 400px;
padding: 5px 40px;
background: #f8f9fa;
border-top: 1px solid rgba(0, 0, 0, 0.06);
border-bottom: 1px solid rgba(0, 0, 0, 0.06);
margin-top: 24px;
}
.content-wrapper {
max-width: 100%;
min-width: 90%;
padding-left: 5%;
padding-right: 5%;
.security-bar-left,
.security-bar-right {
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
}
.versions {
width: 100%;
color: $status-inactive;
}
.sec-brand {
display: flex;
align-items: center;
gap: 10px;
padding-right: 20px;
border-right: 1px solid rgba(0, 0, 0, 0.1);
margin-right: 0;
}
.footer-content {
margin: 0;
}
}
.sec-brand-name {
font-size: 13px;
font-weight: 700;
color: $brand-dark;
letter-spacing: 0.04em;
white-space: nowrap;
.divider {
margin: 2.5rem 1rem 0.5rem;
width: 4rem;
}
.business-date {
color: $status-approved;
}
table {
border: none;
tr:nth-child(even) {
background-color: transparent;
}
em {
font-style: normal;
color: $accent;
}
}
.main-page {
max-width: 70%;
min-width: 50%;
padding-left: 30%;
.sec-brand-tagline {
font-size: 10px;
color: #888;
white-space: nowrap;
}
.copy-label {
min-width: 100%;
text-align: center;
font-size: medium;
.sec-item {
display: inline-flex;
align-items: center;
gap: 5px;
font-size: 11px;
color: #555;
}
.sec-dot {
width: 7px;
height: 7px;
border-radius: 50%;
background: $accent-2;
flex-shrink: 0;
&.amber {
background: #f59e0b;
}
}
.sec-label {
font-weight: 600;
color: #444;
}
.sec-val {
font-weight: 400;
color: #777;
}
.session-timer {
font-family: 'Roboto Mono', monospace;
font-weight: 700;
color: #f59e0b;
font-size: 12px;
}
.sec-version {
font-size: 11px;
font-weight: 600;
color: #555;
white-space: nowrap;
}
.env-tag {
font-size: 10px;
font-weight: 700;
padding: 2px 8px;
border-radius: 3px;
letter-spacing: 0.8px;
background: $accent-2;
color: #fff;
}
.security-bar-right {
flex-shrink: 0;
flex-wrap: nowrap !important;
}
// ===== LincShield Certifications Bar =====
.lincshield-bar {
display: flex;
align-items: center;
gap: 12px;
padding: 6px 40px;
background: linear-gradient(90deg, #0d3d78 0%, #1a5276 50%, #0d3d78 100%);
}
.ls-label {
font-size: 11px;
font-weight: 700;
color: rgba(255, 255, 255, 0.5);
text-transform: uppercase;
letter-spacing: 0.8px;
white-space: nowrap;
flex-shrink: 0;
}
.ls-badges {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.ls-badge {
font-size: 10px;
font-weight: 600;
padding: 2px 8px;
border-radius: 20px;
white-space: nowrap;
&.green {
background: rgba(16, 185, 129, 0.15);
color: #6ee7b7;
border: 1px solid rgba(16, 185, 129, 0.3);
}
&.blue {
background: rgba(14, 165, 233, 0.15);
color: #7dd3fc;
border: 1px solid rgba(14, 165, 233, 0.3);
}
&.gold {
background: rgba(245, 158, 11, 0.15);
color: #fcd34d;
border: 1px solid rgba(245, 158, 11, 0.3);
}
&.white {
background: rgba(255, 255, 255, 0.08);
color: rgba(255, 255, 255, 0.75);
border: 1px solid rgba(255, 255, 255, 0.15);
}
}
// ===== Default Mode (Shell Footer) =====
.app-footer {
background: $brand-dark;
color: rgba(255, 255, 255, 0.85);
padding: 0;
margin-top: 0;
position: relative;
overflow: hidden;
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: repeating-linear-gradient(
108deg, transparent 0, transparent 80px,
rgb(255 255 255 / 2.2%) 80px, rgb(255 255 255 / 2.2%) 160px
);
pointer-events: none;
}
}
// ── Top section ──
.footer-top {
display: flex;
justify-content: space-between;
gap: 40px;
padding: 32px 40px 24px;
position: relative;
z-index: 1;
}
.footer-brand {
max-width: 280px;
flex-shrink: 0;
}
.footer-logo {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 10px;
}
// ===== Responsive for Compact Mode =====
@media (width <=480px) {
.f-logomark {
width: 20px;
height: 20px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2.5px;
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%); }
}
}
.f-logotext {
font-size: 15px;
font-weight: 700;
letter-spacing: 0.055em;
color: #fff;
em {
font-style: normal;
color: #5ec9f8;
}
}
.footer-tagline {
font-size: 12px;
color: rgba(255, 255, 255, 0.5);
line-height: 1.5;
margin: 0;
}
.footer-website {
font-size: 12px;
color: #5ec9f8;
margin: 6px 0 0;
font-weight: 500;
letter-spacing: 0.3px;
}
// ── Link columns ──
.footer-links {
display: flex;
gap: 48px;
flex-wrap: wrap;
}
.footer-col {
display: flex;
flex-direction: column;
gap: 6px;
min-width: 130px;
}
.footer-col-title {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.8px;
color: rgba(255, 255, 255, 0.45);
margin: 0 0 4px;
}
.footer-link {
font-size: 13px;
color: rgba(255, 255, 255, 0.75);
text-decoration: none;
transition: color 0.15s;
line-height: 1.6;
&:hover {
color: #5ec9f8;
}
}
// ── Regulatory & Legal Section ──
.regulatory-section {
padding: 14px 40px;
position: relative;
z-index: 1;
}
.regulatory-title {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.8px;
color: rgba(255, 255, 255, 0.45);
margin: 0 0 8px;
}
.regulatory-grid {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.reg-card {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 10px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 5px;
transition: background 0.15s;
&:hover {
background: rgba(255, 255, 255, 0.08);
}
}
.reg-icon {
font-size: 13px;
line-height: 1;
flex-shrink: 0;
}
.reg-detail {
display: flex;
flex-direction: column;
}
.reg-label {
font-size: 11px;
color: rgba(255, 255, 255, 0.75);
font-weight: 500;
}
.reg-link {
font-size: 11px;
color: rgba(255, 255, 255, 0.75);
text-decoration: none;
font-weight: 500;
transition: color 0.15s;
&:hover {
color: #5ec9f8;
}
}
.reg-tag {
font-size: 9px;
font-weight: 700;
padding: 1px 5px;
border-radius: 3px;
letter-spacing: 0.5px;
text-transform: uppercase;
flex-shrink: 0;
&.rbi {
background: rgba(14, 165, 233, 0.2);
color: #7dd3fc;
border: 1px solid rgba(14, 165, 233, 0.3);
}
&.circular {
background: rgba(245, 158, 11, 0.15);
color: #fcd34d;
border: 1px solid rgba(245, 158, 11, 0.3);
}
&.mandatory {
background: rgba(239, 68, 68, 0.15);
color: #fca5a5;
border: 1px solid rgba(239, 68, 68, 0.3);
}
}
// ── Quick Navigation Section ──
.quicknav-section {
padding: 14px 40px;
position: relative;
z-index: 1;
}
.quicknav-title {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.8px;
color: rgba(255, 255, 255, 0.45);
margin: 0 0 8px;
}
.quicknav-grid {
display: flex;
flex-wrap: wrap;
gap: 6px;
}
.qn-card {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 4px 12px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 6px;
text-decoration: none;
transition: background 0.15s;
&:hover {
background: rgba(255, 255, 255, 0.1);
.qn-label {
color: #5ec9f8;
}
}
}
.qn-icon {
font-size: 13px;
line-height: 1;
flex-shrink: 0;
}
.qn-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.8);
font-weight: 500;
transition: color 0.15s;
}
.qn-tag {
font-size: 9px;
font-weight: 700;
padding: 1px 5px;
border-radius: 3px;
letter-spacing: 0.5px;
text-transform: uppercase;
background: rgba(14, 165, 233, 0.2);
color: #7dd3fc;
border: 1px solid rgba(14, 165, 233, 0.3);
flex-shrink: 0;
}
// ── Support & Contact Section ──
.support-section {
padding: 10px 40px;
position: relative;
z-index: 1;
}
.support-title {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.8px;
color: rgba(255, 255, 255, 0.45);
margin: 0 0 8px;
}
.support-row {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.sc-chip {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 5px 12px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 5px;
transition: background 0.15s;
white-space: nowrap;
&:hover {
background: rgba(255, 255, 255, 0.08);
}
&.sc-chat {
text-decoration: none;
cursor: pointer;
.sc-val {
color: #5ec9f8;
font-weight: 600;
}
&:hover .sc-val {
color: #93dbfc;
}
}
}
.sc-icon {
font-size: 13px;
line-height: 1;
flex-shrink: 0;
}
.sc-label {
font-size: 10px;
color: rgba(255, 255, 255, 0.45);
font-weight: 500;
}
.sc-val {
font-size: 11px;
color: rgba(255, 255, 255, 0.85);
font-weight: 600;
&.highlight {
color: #5ec9f8;
}
}
// ── Divider ──
.footer-divider {
height: 1px;
background: rgba(255, 255, 255, 0.1);
margin: 0 40px;
position: relative;
z-index: 1;
}
// ── Bottom bar ──
.footer-bottom {
display: flex;
align-items: center;
gap: 16px;
padding: 10px 40px;
position: relative;
z-index: 1;
flex-wrap: wrap;
}
.fb-copy {
font-size: 11px;
color: rgba(255, 255, 255, 0.4);
white-space: nowrap;
}
.fb-links {
display: flex;
align-items: center;
gap: 4px;
a {
font-size: 11px;
color: rgba(255, 255, 255, 0.5);
text-decoration: none;
padding: 2px 6px;
border-radius: 3px;
transition: color 0.15s, background 0.15s;
&:hover {
color: #5ec9f8;
background: rgba(255, 255, 255, 0.06);
}
}
}
.fb-meta {
display: flex;
align-items: center;
gap: 4px;
font-size: 10px;
color: rgba(255, 255, 255, 0.35);
margin-left: auto;
strong {
color: rgba(255, 255, 255, 0.55);
font-weight: 600;
}
}
.fb-powered {
font-size: 10px;
color: rgba(255, 255, 255, 0.35);
strong {
color: #5ec9f8;
font-weight: 700;
}
}
// ===== Responsive =====
@media (max-width: 768px) {
.security-bar {
flex-direction: column;
gap: 8px;
padding: 8px 20px;
}
.security-bar-left,
.security-bar-right {
gap: 12px;
}
.sec-brand {
border-right: none;
padding-right: 0;
}
.sec-brand-tagline {
display: none;
}
.lincshield-bar {
flex-direction: column;
gap: 8px;
padding: 10px 20px;
}
.ls-badges {
justify-content: center;
}
.footer-top {
flex-direction: column;
gap: 24px;
padding: 24px 20px 16px;
}
.footer-brand {
max-width: none;
}
.footer-links {
gap: 24px;
}
.regulatory-section,
.quicknav-section,
.support-section {
padding: 12px 20px;
}
.footer-divider {
margin: 0 20px;
}
.footer-bottom {
flex-direction: column;
gap: 6px;
padding: 10px 20px;
text-align: center;
}
.footer-versions {
justify-content: center;
}
}
@media (max-width: 480px) {
.footer-compact {
font-size: 0.7rem;
.footer-info {
gap: 0.375rem;
}
}
.footer-copyright {
font-size: 0.8rem;
}
.footer-links {
flex-direction: column;
gap: 16px;
}
}
// ===== Responsive for Default Mode (Shell) =====
@media (width <=599px) {
#footer {
min-width: 100%;
.sec-item {
font-size: 10px;
}
.main-page {
max-width: 100%;
min-width: 100%;
padding-left: 2%;
.ls-badge {
font-size: 10px;
padding: 2px 8px;
}
}
+44 -1
View File
@@ -58,8 +58,8 @@ export class FooterComponent implements OnInit, OnDestroy {
hash: ''
}
};
/** Mifos X hash */
hash: string = environment.hash;
buildDate: string = this.formatBuildDate();
server = '';
/** Business Date */
businessDate: Date = null;
@@ -73,11 +73,18 @@ export class FooterComponent implements OnInit, OnDestroy {
timer: any;
displayBackEndInfo = true;
currentYear = new Date().getFullYear();
lastLoginTime = '';
sessionMinutes = 15;
sessionSeconds = 0;
private sessionTimer: any;
constructor() {
this.displayBackEndInfo = environment.displayBackEndInfo === 'true';
this.setUserInfo();
this.renderTime = new Date();
this.lastLoginTime = this.formatLastLogin();
this.startSessionTimer();
}
ngOnInit() {
@@ -136,6 +143,42 @@ export class FooterComponent implements OnInit, OnDestroy {
if (this.displayBackEndInfo) {
clearTimeout(this.timer);
}
clearInterval(this.sessionTimer);
}
formatLastLogin(): string {
const now = new Date();
const hours = now.getHours();
const minutes = now.getMinutes();
const ampm = hours >= 12 ? 'PM' : 'AM';
const h = hours % 12 || 12;
const m = minutes < 10 ? '0' + minutes : minutes;
return `Today, ${h}:${m} ${ampm}`;
}
get sessionTimerDisplay(): string {
const m = this.sessionMinutes < 10 ? '0' + this.sessionMinutes : '' + this.sessionMinutes;
const s = this.sessionSeconds < 10 ? '0' + this.sessionSeconds : '' + this.sessionSeconds;
return `${m}:${s}`;
}
private formatBuildDate(): string {
const now = new Date();
const y = now.getFullYear();
const m = (now.getMonth() + 1).toString().padStart(2, '0');
const d = now.getDate().toString().padStart(2, '0');
return `${y}${m}${d}`;
}
private startSessionTimer() {
this.sessionTimer = setInterval(() => {
if (this.sessionSeconds > 0) {
this.sessionSeconds--;
} else if (this.sessionMinutes > 0) {
this.sessionMinutes--;
this.sessionSeconds = 59;
}
}, 1000);
}
/**
+2
View File
@@ -92,6 +92,7 @@ import {
faUser,
faUsers,
faUserShield,
faUserPlus,
faUserTie,
faDownload,
faUpload,
@@ -279,6 +280,7 @@ export class IconsModule {
faUser,
faUsers,
faUserShield,
faUserPlus,
faUserTie,
faDownload,
faUpload,
+295 -79
View File
@@ -10,18 +10,28 @@
$bar: 150px;
$foo: $bar * 8;
$color: #e2e4ec;
$brand: #1353a0;
$border: #e2e8f0;
$text: #0f172a;
$text-muted: #64748b;
$hover-bg: #f8fafc;
$mono: 'Roboto Mono', 'DM Mono', monospace;
// ===== Container =====
.container {
padding: 24px;
padding: 20px 24px;
border-radius: 12px;
border: 1px solid $border;
box-shadow: 0 1px 3px rgb(0 0 0 / 5%);
// Support for existing layout classes
// Legacy layout support
.layout-column,
.layout-gt-sm-row {
display: flex;
align-items: center;
margin-bottom: 20px;
margin-bottom: 16px;
flex-wrap: nowrap;
gap: 20px;
gap: 12px;
&.align-gt-sm-start-center {
align-items: center;
@@ -31,37 +41,26 @@ $color: #e2e4ec;
.search-box {
display: flex;
align-items: center;
gap: 15px;
gap: 12px;
flex: 1;
min-width: 0;
&.m-r-30 {
margin-right: 30px;
}
&.m-r-30 { margin-right: 12px; }
// Handle multiple form fields in search box
mat-form-field {
min-width: 200px;
flex: 1;
&.search-box {
width: auto;
}
// Ensure form fields are inline
+ mat-form-field {
margin-left: 15px;
}
&.search-box { width: auto; }
+ mat-form-field { margin-left: 12px; }
}
mat-checkbox {
white-space: nowrap;
flex-shrink: 0;
margin-left: 15px;
margin-left: 8px;
&.m-r-10 {
margin-right: 10px;
}
&.m-r-10 { margin-right: 8px; }
}
}
@@ -71,74 +70,303 @@ $color: #e2e4ec;
flex-shrink: 0;
margin-left: auto;
&.gap-25px {
gap: 25px;
}
&.gap-25px { gap: 10px; }
button {
white-space: nowrap;
fa-icon {
&.m-r-10,
&.m-r-20 {
margin-right: 10px;
}
&.m-r-20 { margin-right: 6px; }
}
}
}
}
// Table styling
// Legacy bordered-table
.bordered-table {
width: 100%;
border: 1px solid #e0e0e0;
border-radius: 4px;
border: 1px solid $border;
border-radius: 8px;
overflow: hidden;
margin-top: 16px;
.select-row {
cursor: pointer;
&:hover {
background-color: #f5f5f5;
}
&:hover { background-color: $hover-bg; }
}
th,
td {
padding: 12px 16px;
}
td { padding: 10px 16px; }
th {
font-weight: 500;
border-bottom: 1px solid #e0e0e0;
font-weight: 600;
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: $text-muted;
background: #f8fafc;
border-bottom: 1px solid $border;
}
tr {
border-bottom: 1px solid #f0f0f0;
border-bottom: 1px solid #f1f5f9;
&:last-child { border-bottom: none; }
}
&:last-child {
border-bottom: none;
}
td {
font-size: 13px;
color: $text;
}
}
// Paginator styling
mat-paginator {
border-top: 1px solid #e0e0e0;
border-top: 1px solid $border;
margin-top: 0;
background: transparent;
}
// Progress bar
mat-progress-bar {
margin: 0;
mat-progress-bar { margin: 0; }
}
// ===== Modern Table Header =====
.table-header {
display: flex;
flex-direction: column;
gap: 14px;
margin-bottom: 16px;
}
.table-title-group {
display: flex;
align-items: center;
gap: 10px;
}
.table-title {
font-size: 18px;
font-weight: 600;
color: $text;
margin: 0;
display: flex;
align-items: center;
gap: 8px;
.title-icon {
color: $brand;
font-size: 16px;
}
.t-logomark {
width: 20px;
height: 20px;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2px;
flex-shrink: 0;
span {
border-radius: 2px;
&:nth-child(1) { background: $brand; }
&:nth-child(2) { background: #0ea5e9; }
&:nth-child(3) { background: #10b981; }
&:nth-child(4) { background: #cbd5e1; }
}
}
}
// Responsive design
@media (width <= 768px) {
.table-count {
font-size: 11px;
font-family: $mono;
color: $text-muted;
background: #f1f5f9;
padding: 2px 10px;
border-radius: 10px;
font-weight: 500;
}
.table-actions {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.table-search {
flex: 1;
min-width: 200px;
max-width: 360px;
::ng-deep {
.mat-mdc-form-field-flex { height: 38px; }
.mat-mdc-form-field-infix {
padding: 6px 0;
min-height: 38px;
}
.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-icon-prefix {
padding: 0 8px 0 10px;
color: $text-muted;
}
input { font-size: 13px; }
}
}
.table-checkbox {
flex-shrink: 0;
font-size: 13px;
}
.table-buttons {
display: flex;
gap: 8px;
margin-left: auto;
flex-shrink: 0;
button {
font-size: 12px;
font-weight: 500;
border-radius: 8px;
height: 36px;
padding: 0 16px;
fa-icon { margin-right: 6px; font-size: 12px; }
}
.btn-import {
border-color: $border;
color: $text-muted;
&:hover {
border-color: $brand;
color: $brand;
}
}
.btn-create {
box-shadow: 0 1px 3px rgb(19 83 160 / 25%);
}
}
// ===== Modern Table =====
.table-container {
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
}
.table-loader {
height: 2px;
}
.modern-table {
width: 100%;
// Header
::ng-deep {
.mat-mdc-header-row {
background: #f8fafc;
min-height: 40px;
height: 40px;
}
.mat-mdc-header-cell {
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
color: $text-muted;
border-bottom: 1px solid $border;
padding: 0 16px;
}
// Rows
.mat-mdc-row {
min-height: 44px;
height: 44px;
border-bottom: 1px solid #f1f5f9;
transition: background 0.1s;
&:last-child { border-bottom: none; }
}
.table-row {
cursor: pointer;
&:hover {
background: #f0f7ff;
}
}
.mat-mdc-cell {
font-size: 13px;
color: $text;
padding: 0 16px;
}
}
}
.cell-link {
color: $brand;
font-weight: 500;
cursor: pointer;
&:hover { text-decoration: underline; }
}
.cell-mono {
font-family: $mono;
font-size: 12px;
letter-spacing: 0.02em;
}
// ===== Status Badge =====
.status-badge {
display: inline-flex;
align-items: center;
font-size: 10px;
font-weight: 600;
font-family: $mono;
padding: 3px 10px;
border-radius: 4px;
letter-spacing: 0.03em;
text-transform: capitalize;
}
// ===== Empty State =====
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 48px 24px;
text-align: center;
}
.empty-icon {
font-size: 32px;
color: #cbd5e1;
margin-bottom: 12px;
}
.empty-text {
font-size: 14px;
color: $text-muted;
margin: 0;
}
// ===== Responsive =====
/* stylelint-disable-next-line media-feature-range-notation -- Safari compatibility */
@media (max-width: 768px) {
.container {
padding: 16px;
padding: 14px 16px;
.layout-column,
.layout-gt-sm-row {
@@ -147,22 +375,17 @@ $color: #e2e4ec;
.search-box {
width: 100%;
margin-right: 0 !important;
margin-bottom: 15px;
margin-bottom: 10px;
flex-wrap: wrap;
mat-form-field {
min-width: 150px;
margin-bottom: 10px;
margin-bottom: 8px;
+ mat-form-field {
margin-left: 0;
}
+ mat-form-field { margin-left: 0; }
}
mat-checkbox {
margin-left: 0;
margin-top: 10px;
}
mat-checkbox { margin-left: 0; margin-top: 8px; }
}
.action-button {
@@ -170,32 +393,25 @@ $color: #e2e4ec;
width: 100%;
justify-content: center;
&.gap-25px {
gap: 15px;
}
&.gap-25px { gap: 10px; }
}
}
}
.table-actions { flex-direction: column; align-items: stretch; }
.table-search { max-width: 100%; }
.table-buttons { margin-left: 0; justify-content: stretch; }
.table-buttons button { flex: 1; }
}
// Table styles
// Legacy table styles
table {
width: 100%;
tr.select-row:hover {
cursor: pointer;
}
tr.select-row:hover { cursor: pointer; }
}
// Utility classes
.m-r-10 {
margin-right: 10px !important;
}
.m-r-20 {
margin-right: 20px !important;
}
.m-r-30 {
margin-right: 30px !important;
}
.m-r-10 { margin-right: 10px !important; }
.m-r-20 { margin-right: 20px !important; }
.m-r-30 { margin-right: 30px !important; }
+6
View File
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32">
<rect x="1" y="1" width="14" height="14" rx="2" fill="#1353a0"/>
<rect x="17" y="1" width="14" height="14" rx="2" fill="#0ea5e9"/>
<rect x="1" y="17" width="14" height="14" rx="2" fill="#10b981"/>
<rect x="17" y="17" width="14" height="14" rx="2" fill="#cbd5e1"/>
</svg>

After

Width:  |  Height:  |  Size: 340 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Before

Width:  |  Height:  |  Size: 651 KiB

After

Width:  |  Height:  |  Size: 651 KiB

+7
View File
@@ -528,6 +528,7 @@
"Reject": "Odmítnout",
"Reject transfer": "Odmítnout přenos",
"Release Amount": "Částka uvolnění",
"Reload": "Znovu načíst",
"Remove": "Odstranit",
"Request OTP": "Požádejte o OTP",
"Reschedule": "Přeplánovat",
@@ -1340,6 +1341,11 @@
"Secure & Reliable": "Bezpečné a spolehlivé",
"Community Driven": "Komunitně řízené",
"Global Impact": "Globální dopad",
"Language": "Jazyk",
"Server": "Server",
"Settings": "Nastavení",
"Theme": "Téma",
"Welcome Back": "Vítejte zpět",
"Personal Information": "Osobní informace",
"Account Information": "Informace o účtu",
"Entity Details": "Detaily entity",
@@ -3651,6 +3657,7 @@
"do not match": "neshodují",
"edit": "Upravit",
"elimination of poverty": "jehož cílem je urychlit odstranění chudoby tím, že organizacím umožní efektivněji a efektivněji poskytovat odpovědné finanční služby chudým a bez bankovních účtů ve světě. Zní to zajímavě?",
"Sign in to continue": "Přihlaste se pro pokračování na váš panel",
"global community": "globální komunita",
"is designed by the": "je navržena společností",
"per annum": "ročně",
+7
View File
@@ -528,6 +528,7 @@
"Reject": "Ablehnen",
"Reject transfer": "Übertragung ablehnen",
"Release Amount": "Freigabebetrag",
"Reload": "Neu laden",
"Remove": "Entfernen",
"Request OTP": "OTP anfordern",
"Reschedule": "Neu planen",
@@ -1341,6 +1342,11 @@
"Secure & Reliable": "Sicher und zuverlässig",
"Community Driven": "Gemeinschaftsorientiert",
"Global Impact": "Globale Auswirkungen",
"Language": "Sprache",
"Server": "Server",
"Settings": "Einstellungen",
"Theme": "Design",
"Welcome Back": "Willkommen zurück",
"Personal Information": "Persönliche Informationen",
"Account Information": "Kontoinformationen",
"Entity Details": "Entitätsdetails",
@@ -3651,6 +3657,7 @@
"do not match": "nicht übereinstimmen",
"edit": "bearbeiten",
"elimination of poverty": "Ziel ist es, die Beseitigung der Armut zu beschleunigen, indem Organisationen in die Lage versetzt werden, verantwortungsvolle Finanzdienstleistungen für die Armen und Menschen ohne Bankkonto auf der Welt effektiver und effizienter bereitzustellen. Klingt interessant?",
"Sign in to continue": "Melden Sie sich an, um zu Ihrem Dashboard fortzufahren",
"global community": "Globale Gemeinschaft",
"is designed by the": "wird von der entworfen",
"per annum": "pro Jahr",
File diff suppressed because it is too large Load Diff
+7
View File
@@ -528,6 +528,7 @@
"Reject": "Rechazar",
"Reject transfer": "Rechazar transferencia",
"Release Amount": "Liberar Monto",
"Reload": "Recargar",
"Remove": "Eliminar",
"Request OTP": "Solicitar OTP",
"Reschedule": "Reprogramar",
@@ -1341,6 +1342,11 @@
"Secure & Reliable": "Seguro y Confiable",
"Community Driven": "Impulsado por la Comunidad",
"Global Impact": "Impacto Global",
"Language": "Idioma",
"Server": "Servidor",
"Settings": "Configuración",
"Theme": "Tema",
"Welcome Back": "Bienvenido de nuevo",
"Personal Information": "Información Personal",
"Account Information": "Información de la Cuenta",
"Entity Details": "Detalles de la Persona Moral",
@@ -3652,6 +3658,7 @@
"do not match": "no coinciden",
"edit": "editar",
"elimination of poverty": "que tiene como objetivo acelerar la eliminación de la pobreza al permitir que las organizaciones brinden de manera más efectiva y eficiente servicios financieros responsables a las personas de escasos recursos y no bancarizados alrededor del mundo. ¿Suena interesante?",
"Sign in to continue": "Inicia sesión para continuar a tu panel",
"global community": "Comunidad global",
"is designed by the": "está diseñado por la",
"per annum": "anualmente",
+7
View File
@@ -528,6 +528,7 @@
"Reject": "Rechazar",
"Reject transfer": "Rechazar transferencia",
"Release Amount": "Liberar Monto",
"Reload": "Recargar",
"Remove": "Eliminar",
"Request OTP": "Solicitar OTP",
"Reschedule": "Reprogramar",
@@ -1343,6 +1344,11 @@
"Secure & Reliable": "Seguro y Confiable",
"Community Driven": "Impulsado por la Comunidad",
"Global Impact": "Impacto Global",
"Language": "Idioma",
"Server": "Servidor",
"Settings": "Configuración",
"Theme": "Tema",
"Welcome Back": "Bienvenido de nuevo",
"Personal Information": "Información Personal",
"Account Information": "Información de la Cuenta",
"Entity Details": "Detalles de la Persona Moral",
@@ -3682,6 +3688,7 @@
"do not match": "no coinciden",
"edit": "editar",
"elimination of poverty": "que tiene como objetivo acelerar la eliminación de la pobreza al permitir que las organizaciones brinden de manera más efectiva y eficiente servicios financieros responsables a las personas de escasos recursos y no bancarizados alrededor del mundo. ¿Suena interesante?",
"Sign in to continue": "Inicia sesión para continuar a tu panel",
"global community": "Comunidad global",
"is designed by the": "está diseñado por la",
"per annum": "anualmente",
+7
View File
@@ -528,6 +528,7 @@
"Reject": "Rejeter",
"Reject transfer": "Refuser le transfert",
"Release Amount": "Montant de la libération",
"Reload": "Recharger",
"Remove": "Retirer",
"Request OTP": "Demander un OTP",
"Reschedule": "Reprogrammer",
@@ -1344,6 +1345,11 @@
"Secure & Reliable": "Sécurisé et fiable",
"Community Driven": "Axé sur la communauté",
"Global Impact": "Impact mondial",
"Language": "Langue",
"Server": "Serveur",
"Settings": "Paramètres",
"Theme": "Thème",
"Welcome Back": "Bon retour",
"Personal Information": "Informations personnelles",
"Account Information": "Informations sur le compte",
"Entity Details": "Détails de l'entité",
@@ -3653,6 +3659,7 @@
"do not match": "ne correspondent pas",
"edit": "modifier",
"elimination of poverty": "qui vise à accélérer l’élimination de la pauvreté en permettant aux organisations de fournir plus efficacement des services financiers responsables aux pauvres et aux non-bancarisés du monde. Cela semble intéressant ?",
"Sign in to continue": "Connectez-vous pour continuer vers votre tableau de bord",
"global community": "Communauté globale",
"is designed by the": "est conçu par le",
"per annum": "par an",
+7
View File
@@ -528,6 +528,7 @@
"Reject": "Rifiutare",
"Reject transfer": "Rifiuta il trasferimento",
"Release Amount": "Importo di rilascio",
"Reload": "Ricarica",
"Remove": "Rimuovere",
"Request OTP": "Richiedi OTP",
"Reschedule": "Riprogrammare",
@@ -1340,6 +1341,11 @@
"Secure & Reliable": "Sicuro e affidabile",
"Community Driven": "Guidato dalla comunità",
"Global Impact": "Impatto globale",
"Language": "Lingua",
"Server": "Server",
"Settings": "Impostazioni",
"Theme": "Tema",
"Welcome Back": "Bentornato",
"Personal Information": "Informazioni personali",
"Account Information": "Informazioni sull'account",
"Entity Details": "Dettagli dell'entità",
@@ -3650,6 +3656,7 @@
"do not match": "non corrispondono",
"edit": "modificare",
"elimination of poverty": "che mira ad accelerare l’eliminazione della povertà consentendo alle organizzazioni di fornire in modo più efficace ed efficiente servizi finanziari responsabili ai poveri e a coloro che non dispongono di servizi bancari nel mondo. Sembra interessante?",
"Sign in to continue": "Accedi per continuare alla tua dashboard",
"global community": "comunità globale",
"is designed by the": "è progettato da",
"per annum": "per anno",
+7
View File
@@ -528,6 +528,7 @@
"Reject": "거부하다",
"Reject transfer": "이전 거부",
"Release Amount": "출시 금액",
"Reload": "새로고침",
"Remove": "제거하다",
"Request OTP": "OTP 요청",
"Reschedule": "일정 변경",
@@ -1342,6 +1343,11 @@
"Secure & Reliable": "안전하고 신뢰할 수 있음",
"Community Driven": "커뮤니티 주도형",
"Global Impact": "글로벌 영향",
"Language": "언어",
"Server": "서버",
"Settings": "설정",
"Theme": "테마",
"Welcome Back": "다시 오신 것을 환영합니다",
"Personal Information": "개인 정보",
"Account Information": "계정 정보",
"Entity Details": "엔티티 세부 정보",
@@ -3651,6 +3657,7 @@
"do not match": "일치하지 않는",
"edit": "편집하다",
"elimination of poverty": "이는 조직이 전 세계의 가난하고 은행 서비스를 이용할 수 없는 사람들에게 책임 있는 금융 서비스를 보다 효과적이고 효율적으로 제공할 수 있도록 하여 빈곤 퇴치 속도를 높이는 것을 목표로 합니다. 흥미롭나요?",
"Sign in to continue": "대시보드로 계속하려면 로그인하세요",
"global community": "글로벌 커뮤니티",
"is designed by the": "에 의해 설계되었습니다",
"per annum": "연간",
+7
View File
@@ -528,6 +528,7 @@
"Reject": "Atmesti",
"Reject transfer": "Atmesti perdavimą",
"Release Amount": "Išleidimo suma",
"Reload": "Perkrauti",
"Remove": "Pašalinti",
"Request OTP": "Prašyti OTP",
"Reschedule": "Suplanuoti iš naujo",
@@ -1339,6 +1340,11 @@
"Secure & Reliable": "Saugu ir patikima",
"Community Driven": "Bendruomenės varomas",
"Global Impact": "Pasaulinis poveikis",
"Language": "Kalba",
"Server": "Serveris",
"Settings": "Nustatymai",
"Theme": "Tema",
"Welcome Back": "Sveiki sugrįžę",
"Personal Information": "Asmens informacija",
"Account Information": "Paskyros informacija",
"Entity Details": "Subjekto informacija",
@@ -3651,6 +3657,7 @@
"do not match": "nesutampa",
"edit": "Redaguoti",
"elimination of poverty": "kuriuo siekiama paspartinti skurdo panaikinimą, suteikiant organizacijoms galimybę veiksmingiau ir efektyviau teikti atsakingas finansines paslaugas pasaulio neturtingiesiems ir neturintiems bankų. Skamba įdomiai?",
"Sign in to continue": "Prisijunkite, kad tęstumėte į savo valdymo skydelį",
"global community": "pasaulinė bendruomenė",
"is designed by the": "yra suprojektuotas",
"per annum": "per metus",
+7
View File
@@ -528,6 +528,7 @@
"Reject": "Noraidīt",
"Reject transfer": "Noraidīt pārsūtīšanu",
"Release Amount": "Izlaiduma summa",
"Reload": "Pārlādēt",
"Remove": "Noņemt",
"Request OTP": "Pieprasīt OTP",
"Reschedule": "Pārplānot",
@@ -1341,6 +1342,11 @@
"Secure & Reliable": "Drošs un uzticams",
"Community Driven": "Kopienas vadīts",
"Global Impact": "Globālā ietekme",
"Language": "Valoda",
"Server": "Serveris",
"Settings": "Iestatījumi",
"Theme": "Tēma",
"Welcome Back": "Laipni lūdzam atpakaļ",
"Personal Information": "Personīgā informācija",
"Account Information": "Konta informācija",
"Entity Details": "Uzņēmuma detaļas",
@@ -3651,6 +3657,7 @@
"do not match": "nesakrīt",
"edit": "rediģēt",
"elimination of poverty": "kuras mērķis ir paātrināt nabadzības izskaušanu, ļaujot organizācijām efektīvāk un produktīvāk sniegt atbildīgus finanšu pakalpojumus pasaules nabadzīgajiem un bez bankām. Izklausās interesanti?",
"Sign in to continue": "Piesakieties, lai turpinātu uz savu paneli",
"global community": "globālā kopiena",
"is designed by the": "ir izstrādājis",
"per annum": "gadā",
+7
View File
@@ -528,6 +528,7 @@
"Reject": "अस्वीकार गर्नुहोस्",
"Reject transfer": "स्थानान्तरण अस्वीकार गर्नुहोस्",
"Release Amount": "रिलिज रकम",
"Reload": "पुनः लोड",
"Remove": "हटाउनुहोस्",
"Request OTP": "OTP अनुरोध गर्नुहोस्",
"Reschedule": "पुन: तालिका",
@@ -1339,6 +1340,11 @@
"Secure & Reliable": "सुरक्षित र भरपर्दो",
"Community Driven": "समुदाय संचालित",
"Global Impact": "विश्वव्यापी प्रभाव",
"Language": "भाषा",
"Server": "सर्भर",
"Settings": "सेटिङ्स",
"Theme": "थिम",
"Welcome Back": "फेरि स्वागत छ",
"Personal Information": "व्यक्तिगत जानकारी",
"Account Information": "खाता जानकारी",
"Entity Details": "संस्था विवरण",
@@ -3649,6 +3655,7 @@
"do not match": "मिल्दैन",
"edit": "सम्पादन गर्नुहोस्",
"elimination of poverty": "जसले विश्वका गरिब र बैंकविहीनहरूलाई जिम्मेवार वित्तीय सेवाहरू अझ प्रभावकारी र कुशलतापूर्वक प्रदान गर्न संगठनहरूलाई सक्षम बनाएर गरिबी उन्मूलनको गति बढाउने लक्ष्य राख्छ। रोचक लाग्छ?",
"Sign in to continue": "आफ्नो ड्यासबोर्डमा जारी राख्न साइन इन गर्नुहोस्",
"global community": "विश्वव्यापी समुदाय",
"is designed by the": "द्वारा डिजाइन गरिएको छ",
"per annum": "प्रति वर्ष",
+7
View File
@@ -528,6 +528,7 @@
"Reject": "Rejeitar",
"Reject transfer": "Rejeitar transferência",
"Release Amount": "Valor de liberação",
"Reload": "Recarregar",
"Remove": "Remover",
"Request OTP": "Solicitar OTP",
"Reschedule": "Reprogramar",
@@ -1340,6 +1341,11 @@
"Secure & Reliable": "Seguro e confiável",
"Community Driven": "Impulsionado pela comunidade",
"Global Impact": "Impacto global",
"Language": "Idioma",
"Server": "Servidor",
"Settings": "Configurações",
"Theme": "Tema",
"Welcome Back": "Bem-vindo de volta",
"Personal Information": "Informações Pessoais",
"Account Information": "Informações da Conta",
"Entity Details": "Detalhes da Entidade",
@@ -3650,6 +3656,7 @@
"do not match": "não combina",
"edit": "editar",
"elimination of poverty": "que visa acelerar a eliminação da pobreza, permitindo que as organizações forneçam serviços financeiros responsáveis ​​de forma mais eficaz e eficiente aos pobres e sem conta bancária do mundo.Parece interessante?",
"Sign in to continue": "Faça login para continuar ao seu painel",
"global community": "comunidade global",
"is designed by the": "é projetado pelo",
"per annum": "por ano",
+7
View File
@@ -528,6 +528,7 @@
"Reject": "Kataa",
"Reject transfer": "Kataa uhamisho",
"Release Amount": "Kiasi cha Kutolewa",
"Reload": "Pakia upya",
"Remove": "Ondoa",
"Request OTP": "Omba OTP",
"Reschedule": "Panga upya",
@@ -1338,6 +1339,11 @@
"Secure & Reliable": "Salama na ya Kuaminika",
"Community Driven": "Inaendeshwa na Jumuiya",
"Global Impact": "Athari ya Kimataifa",
"Language": "Lugha",
"Server": "Seva",
"Settings": "Mipangilio",
"Theme": "Mandhari",
"Welcome Back": "Karibu tena",
"Personal Information": "Taarifa za Kibinafsi",
"Account Information": "Taarifa za Akaunti",
"Entity Details": "Maelezo ya Huluki",
@@ -3648,6 +3654,7 @@
"do not match": "hailingani",
"edit": "hariri",
"elimination of poverty": "ambayo inalenga kuharakisha uondoaji wa umaskini kwa kuwezesha Mashirika kutoa huduma za kifedha kwa ufanisi na kwa ufanisi zaidi kwa watu maskini na wasio na benki duniani. Inapendeza?",
"Sign in to continue": "Ingia ili kuendelea kwenye dashibodi yako",
"global community": "jumuiya ya kimataifa",
"is designed by the": "imeundwa na",
"per annum": "kwa mwaka",
+9 -2
View File
@@ -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': '260318',
'hash': 'g0a32d7135'
'version': '260320',
'hash': 'gb3a4cce0c'
},
'allow_switching_backend_instance': true
};
+1 -1
View File
@@ -65,7 +65,7 @@ export const environment = {
displayTenantSelector: loadedEnv['displayTenantSelector'] || 'true',
/** Production mode - when true, shows minimal hero with only branding at bottom */
productionMode: loadedEnv['productionMode'] === 'true' || loadedEnv['productionMode'] === true || false,
tenantLogoUrl: loadedEnv['tenantLogoUrl'] || 'assets/images/default_home.png',
tenantLogoUrl: loadedEnv['tenantLogoUrl'] || 'assets/images/cbsnexora-logo.png',
tenantLogoUrlDark: loadedEnv['tenantLogoUrlDark'] || 'assets/images/white-mifos.png',
documentationBaseUrl: loadedEnv['documentationBaseUrl'] || 'https://mifosforge.jira.com/wiki',
// Time in seconds, default 60 seconds
+1 -1
View File
@@ -67,7 +67,7 @@ export const environment = {
displayTenantSelector: loadedEnv.displayTenantSelector || 'true',
/** Production mode - when true, shows minimal hero with only branding at bottom */
productionMode: loadedEnv.productionMode === 'true' || loadedEnv.productionMode === true || false,
tenantLogoUrl: loadedEnv.tenantLogoUrl || 'assets/images/default_home.png',
tenantLogoUrl: loadedEnv.tenantLogoUrl || 'assets/images/cbsnexora-logo.png',
tenantLogoUrlDark: loadedEnv.tenantLogoUrlDark || 'assets/images/white-mifos.png',
documentationBaseUrl: loadedEnv.documentationBaseUrl || 'https://mifosforge.jira.com/wiki',
// Time in seconds, default 60 seconds
+2 -3
View File
@@ -17,9 +17,8 @@
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="mobile-web-app-capable" content="yes" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<link rel="icon" type="image/png" href="assets/images/MifosX_logo.png" sizes="182x184" />
<link rel="apple-touch-icon" type="image/png" href="apple-touch-icon.png" sizes="182x184" />
<link rel="icon" type="image/svg+xml" href="assets/images/cbsnexora-favicon.svg" />
<link rel="apple-touch-icon" type="image/svg+xml" href="assets/images/cbsnexora-favicon.svg" />
<link rel="manifest" href="manifest.json" />
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet" />
+19
View File
@@ -938,3 +938,22 @@ mat-card {
margin: 1.5cm;
}
}
/* ─── Ensure mat-menu overlays appear above header ─── */
.cdk-overlay-container {
z-index: 1001 !important;
}
/* ─── CRR/SLR Dialog Panel ─── */
.crr-slr-dialog-panel {
.mat-mdc-dialog-container {
padding: 0;
border-radius: 16px;
overflow: hidden;
}
.mat-mdc-dialog-surface {
padding: 0 !important;
border-radius: 16px !important;
}
}
+6 -6
View File
@@ -1,17 +1,17 @@
{
"short_name": "Mifos® X",
"name": "Mifos® X Web App",
"short_name": "CBS Nexora",
"name": "Nexora Web App",
"start_url": "/",
"scope": "/",
"theme_color": "#fff",
"background_color": "#fff",
"display": "standalone",
"description": "Mifos® X Web App is the default web application built on top of the Apache Fineract® platform for the Mifos® user community leveraging the popular Angular framework.",
"description": "Nexora CBS – Powering the Next Generation of Core Banking.",
"icons": [
{
"src": "assets/images/MifosX_logo.png",
"sizes": "182x184",
"type": "image/png"
"src": "assets/images/cbsnexora-favicon.svg",
"sizes": "any",
"type": "image/svg+xml"
}
]
}