feat: non-individual entity UI improvements — hide individual-only sections, fix legal form display, send extended fields to API

- Hide Family Members tab/section for non-individual entities in client view, create flow, and preview
- Hide Financial & Classification section for non-individual entities in personal data and preview
- Fix legal form column in clients list — fetch legalForm from client API for correct display
- Add person/business icon before client name in list rows
- Remove unsupportedNpd filter — send authorizedSignatory, additionalInfo, dateOfRegistration to API
- Add registration number format hint (mat-hint) based on constitution type
- Add spacing between verification row and constitution type dropdowns
- Reduce table cell padding and prevent action buttons from being clipped
- Add missing MPL-2.0 license headers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-04-04 15:10:51 +05:30
co-authored by Claude Opus 4.6
parent ab01111424
commit b890d24bb7
522 changed files with 51991 additions and 21885 deletions
@@ -6,16 +6,30 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<mat-card class="container">
<mat-card-content>
<div class="investor-filter-section">
<div class="filter-row">
<mat-form-field class="filter-field">
<div class="inv-container">
<!-- Header -->
<div class="inv-header">
<button mat-stroked-button [routerLink]="['../']" class="inv-back">
<i class="fas fa-arrow-left"></i> Back
</button>
<h2 class="inv-header__title">Investors</h2>
</div>
<!-- Search Card -->
<div class="inv-card">
<!-- Section: Search -->
<div class="inv-section">
<div class="inv-section__header">
<i class="fas fa-search"></i>
<h3>Search</h3>
</div>
<div class="inv-grid">
<mat-form-field appearance="outline" class="inv-field">
<mat-label>{{ 'labels.text.Search by Text' | translate }}</mat-label>
<input matInput [formControl]="searchText" />
</mat-form-field>
<mat-form-field class="filter-field" (click)="effectiveFromDatePicker.open()">
<mat-form-field appearance="outline" class="inv-field" (click)="effectiveFromDatePicker.open()">
<mat-label>{{ 'labels.text.Effective Date From' | translate }}</mat-label>
<input
matInput
@@ -28,7 +42,7 @@
<mat-datepicker #effectiveFromDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field class="filter-field" (click)="effectiveToDatePicker.open()">
<mat-form-field appearance="outline" class="inv-field" (click)="effectiveToDatePicker.open()">
<mat-label>{{ 'labels.text.Effective Date To' | translate }}</mat-label>
<input
matInput
@@ -40,10 +54,8 @@
<mat-datepicker-toggle matSuffix [for]="effectiveToDatePicker"></mat-datepicker-toggle>
<mat-datepicker #effectiveToDatePicker></mat-datepicker>
</mat-form-field>
</div>
<div class="filter-row">
<mat-form-field class="filter-field" (click)="settlementFromDatePicker.open()">
<mat-form-field appearance="outline" class="inv-field" (click)="settlementFromDatePicker.open()">
<mat-label>{{ 'labels.text.Settlement Date From' | translate }}</mat-label>
<input
matInput
@@ -56,7 +68,7 @@
<mat-datepicker #settlementFromDatePicker></mat-datepicker>
</mat-form-field>
<mat-form-field class="filter-field" (click)="settlementToDatePicker.open()">
<mat-form-field appearance="outline" class="inv-field" (click)="settlementToDatePicker.open()">
<mat-label>{{ 'labels.text.Settlement Date To' | translate }}</mat-label>
<input
matInput
@@ -69,185 +81,153 @@
<mat-datepicker #settlementToDatePicker></mat-datepicker>
</mat-form-field>
<div class="filter-button-container">
<button mat-raised-button color="primary" (click)="searchEAO()" class="search-button">
<fa-icon icon="search" class="m-r-10"></fa-icon> {{ 'labels.buttons.Search' | translate }}
<div class="inv-search-btn-wrap">
<button mat-flat-button color="primary" (click)="searchEAO()" class="inv-search-btn">
<i class="fas fa-search"></i> {{ 'labels.buttons.Search' | translate }}
</button>
</div>
</div>
</div>
<div class="mat-elevation-z8 container m-t-20">
@if (isLoading) {
<div class="loader">
<div class="loading"></div>
</div>
}
@if (existsDataToFilter) {
<mat-accordion>
@for (record of searchResults; track record; let i = $index) {
<mat-expansion-panel class="record">
<mat-expansion-panel-header>
<mat-panel-title>
<table>
<tr>
<td>
<div [ngClass]="itemStatus(record.status)" class="m-r-20">
<fa-icon icon="stop"></fa-icon>
<span class="m-l-10 status">{{ record.status }}</span>
</div>
</td>
<td class="field-name">{{ 'labels.inputs.Owner External Id' | translate }}</td>
<td>
<!-- Results Area -->
<div class="inv-section">
<div class="inv-section__header">
<i class="fas fa-list"></i>
<h3>Results</h3>
</div>
<div class="inv-results">
@if (isLoading) {
<div class="inv-loader">
<div class="inv-loader__spinner"></div>
</div>
}
@if (existsDataToFilter) {
<mat-accordion class="inv-accordion">
@for (record of searchResults; track record; let i = $index) {
<mat-expansion-panel class="inv-panel">
<mat-expansion-panel-header>
<mat-panel-title>
<div class="inv-panel-summary">
<div [ngClass]="itemStatus(record.status)" class="inv-status-badge">
<fa-icon icon="stop"></fa-icon>
<span class="inv-status-text">{{ record.status }}</span>
</div>
<div class="inv-panel-meta">
<span class="inv-panel-label">{{ 'labels.inputs.Owner External Id' | translate }}</span>
<mifosx-external-identifier
externalId="{{ record.owner.externalId }}"
></mifosx-external-identifier>
</td>
<td class="field-name">{{ 'labels.inputs.Transfer External Id' | translate }}</td>
<td>
</div>
<div class="inv-panel-meta">
<span class="inv-panel-label">{{ 'labels.inputs.Transfer External Id' | translate }}</span>
<mifosx-external-identifier
externalId="{{ record.transferExternalId }}"
></mifosx-external-identifier>
</td>
<td class="field-name">{{ 'labels.inputs.Effective From' | translate }}</td>
<td>{{ record.effectiveFrom | dateFormat }}</td>
</tr>
</table>
</mat-panel-title>
</mat-expansion-panel-header>
<div class="external-asset-transfer">
<table>
<tbody>
<tr>
<td class="m-l-5 flex-25">
<b>{{ 'labels.inputs.Status' | translate }} :</b>
</td>
<td class="flex-25 m-l-5 left">
<div [ngClass]="itemStatus(record.status)">
<fa-icon icon="stop"></fa-icon>
<span class="m-l-5 status">{{ record.status }}</span>
</div>
<div class="inv-panel-meta">
<span class="inv-panel-label">{{ 'labels.inputs.Effective From' | translate }}</span>
<span>{{ record.effectiveFrom | dateFormat }}</span>
</div>
</div>
</mat-panel-title>
</mat-expansion-panel-header>
<!-- Expanded Content -->
<div class="inv-detail">
<div class="inv-detail__grid">
<div class="inv-detail__item">
<span class="inv-detail__label">{{ 'labels.inputs.Status' | translate }}</span>
<div [ngClass]="itemStatus(record.status)" class="inv-status-badge">
<fa-icon icon="stop"></fa-icon>
<span class="inv-status-text">{{ record.status }}</span>
</div>
</div>
<div class="inv-detail__item">
<span class="inv-detail__label">{{ 'labels.inputs.Owner External Id' | translate }}</span>
<mifosx-external-identifier
externalId="{{ record.owner.externalId }}"
completed="true"
></mifosx-external-identifier>
</div>
<div class="inv-detail__item">
<span class="inv-detail__label">{{ 'labels.inputs.Settlement Date' | translate }}</span>
<span class="inv-detail__value">{{ record.settlementDate | dateFormat }}</span>
</div>
<div class="inv-detail__item">
<span class="inv-detail__label">{{ 'labels.inputs.Effective Date' | translate }}</span>
<span class="inv-detail__value">{{ record.effectiveFrom | dateFormat }}</span>
</div>
</div>
@if (record.details) {
<div class="inv-financials">
<h4 class="inv-financials__title">{{ 'labels.inputs.Details' | translate }}</h4>
<div class="inv-financials__grid">
<div class="inv-financials__row">
<span class="inv-financials__label">{{ 'labels.inputs.Principal Outstanding' | translate }}</span>
<span class="inv-financials__amount">{{ record.details.totalPrincipalOutstanding | formatNumber }}</span>
</div>
</td>
<td class="m-l-5 flex-25">
<b>{{ 'labels.inputs.Owner External Id' | translate }} :</b>
</td>
<td class="flex-25 m-l-5 left">
<mifosx-external-identifier
externalId="{{ record.owner.externalId }}"
completed="true"
></mifosx-external-identifier>
</td>
</tr>
<tr>
<td class="m-l-5 flex-25">
<b>{{ 'labels.inputs.Settlement Date' | translate }} :</b>
</td>
<td class="m-l-5 flex-25">
{{ record.settlementDate | dateFormat }}
</td>
<td class="m-l-5 flex-25">
<b>{{ 'labels.inputs.Effective Date' | translate }} :</b>
</td>
<td class="m-l-5 flex-25">
{{ record.effectiveFrom | dateFormat }}
</td>
</tr>
@if (record.details) {
<tr>
<td class="m-l-5 flex-25">
<b>{{ 'labels.inputs.Details' | translate }} :</b>
</td>
<td class="flex-75 left">
<table>
<tbody>
<tr>
<td class="flex-50 m-l-5">
<b>{{ 'labels.inputs.Principal Outstanding' | translate }} :</b>
</td>
<td class="flex-50 r-amount">
{{ record.details.totalPrincipalOutstanding | formatNumber }}
</td>
</tr>
<tr>
<td class="flex-50 m-l-5">
<b>{{ 'labels.inputs.Interest Outstanding' | translate }} :</b>
</td>
<td class="flex-50 r-amount">
{{ record.details.totalInterestOutstanding | formatNumber }}
</td>
</tr>
<tr>
<td class="flex-50 m-l-5">
<b>{{ 'labels.inputs.Fees Outstanding' | translate }} :</b>
</td>
<td class="flex-50 r-amount">
{{ record.details.totalFeeChargesOutstanding | formatNumber }}
</td>
</tr>
<tr>
<td class="flex-50 m-l-5">
<b>{{ 'labels.inputs.Penalties Outstanding' | translate }} :</b>
</td>
<td class="flex-50 r-amount">
{{ record.details.totalPenaltyChargesOutstanding | formatNumber }}
</td>
</tr>
<tr>
<td class="flex-50 m-l-5">
<b>{{ 'labels.inputs.Outstanding' | translate }} :</b>
</td>
<td class="flex-50 r-amount">{{ record.details.totalOutstanding | formatNumber }}</td>
</tr>
<tr>
<td class="flex-50 m-l-5">
<b>{{ 'labels.inputs.Overpaid' | translate }} :</b>
</td>
<td class="flex-50 r-amount">{{ record.details.totalOverpaid | formatNumber }}</td>
</tr>
</tbody>
</table>
</td>
</tr>
}
</tbody>
</table>
</div>
<mat-divider [inset]="true"></mat-divider>
@if (canBeCancelled(record)) {
<div class="record-actions layout-row align-flex-end">
<button
mat-button
color="warn"
(click)="cancelPendingSale(record)"
matTooltip="{{ 'labels.text.Cancel Pending Sale' | translate }}"
>
<fa-icon icon="trash"></fa-icon>
</button>
<div class="inv-financials__row">
<span class="inv-financials__label">{{ 'labels.inputs.Interest Outstanding' | translate }}</span>
<span class="inv-financials__amount">{{ record.details.totalInterestOutstanding | formatNumber }}</span>
</div>
<div class="inv-financials__row">
<span class="inv-financials__label">{{ 'labels.inputs.Fees Outstanding' | translate }}</span>
<span class="inv-financials__amount">{{ record.details.totalFeeChargesOutstanding | formatNumber }}</span>
</div>
<div class="inv-financials__row">
<span class="inv-financials__label">{{ 'labels.inputs.Penalties Outstanding' | translate }}</span>
<span class="inv-financials__amount">{{ record.details.totalPenaltyChargesOutstanding | formatNumber }}</span>
</div>
<div class="inv-financials__row inv-financials__row--total">
<span class="inv-financials__label">{{ 'labels.inputs.Outstanding' | translate }}</span>
<span class="inv-financials__amount">{{ record.details.totalOutstanding | formatNumber }}</span>
</div>
<div class="inv-financials__row">
<span class="inv-financials__label">{{ 'labels.inputs.Overpaid' | translate }}</span>
<span class="inv-financials__amount">{{ record.details.totalOverpaid | formatNumber }}</span>
</div>
</div>
</div>
}
</div>
}
</mat-expansion-panel>
}
</mat-accordion>
}
@if (existsDataToFilter) {
<mat-paginator
[pageSize]="pageSize"
[pageSizeOptions]="[50, 100, 200]"
[length]="totalRows"
(page)="pageChanged($event)"
showFirstLastButtons
></mat-paginator>
}
</div>
<mat-divider [inset]="true"></mat-divider>
@if (canBeCancelled(record)) {
<div class="inv-panel-actions">
<button
mat-button
color="warn"
(click)="cancelPendingSale(record)"
matTooltip="{{ 'labels.text.Cancel Pending Sale' | translate }}"
>
<fa-icon icon="trash"></fa-icon>
</button>
</div>
}
</mat-expansion-panel>
}
</mat-accordion>
}
@if (!existsDataToFilter) {
<div class="alert">
<div class="message">
<i class="fa fa-exclamation-circle alert-check"></i>
{{ 'labels.text.No data found' | translate }}
</div>
@if (existsDataToFilter) {
<mat-paginator
[pageSize]="pageSize"
[pageSizeOptions]="[50, 100, 200]"
[length]="totalRows"
(page)="pageChanged($event)"
showFirstLastButtons
></mat-paginator>
}
@if (!existsDataToFilter) {
<div class="inv-empty">
<i class="fas fa-inbox inv-empty__icon"></i>
<p class="inv-empty__text">{{ 'labels.text.No data found' | translate }}</p>
</div>
}
</div>
}
</mat-card-content>
</mat-card>
</div>
</div>
</div>
@@ -6,198 +6,303 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
// Filter Section Styling
.investor-filter-section {
margin-bottom: 24px;
$primary: #1353a0;
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-500: #9aa0a6;
$gray-700: #5f6368;
$gray-900: #202124;
.filter-row {
.inv-container {
max-width: 960px;
margin: 0 auto;
padding: 20px 16px;
}
// Header
.inv-header {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 20px;
&__title {
font-size: 18px;
font-weight: 600;
color: $gray-900;
margin: 0;
}
}
.inv-back {
border-radius: 8px !important;
font-size: 12px !important;
font-weight: 600 !important;
height: 32px !important;
border-color: $border !important;
color: $gray-500 !important;
i { margin-right: 6px; }
&:hover { color: $primary !important; border-color: $primary !important; }
}
// Card
.inv-card {
background: #fff;
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
}
// Section
.inv-section {
&__header {
display: flex;
align-items: flex-start;
gap: 16px;
margin-bottom: 12px;
flex-wrap: wrap;
align-items: center;
gap: 10px;
padding: 12px 24px;
background: $gray-50;
border-bottom: 1px solid $gray-200;
&:last-child {
margin-bottom: 0;
i {
font-size: 14px;
color: $accent;
}
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
}
.filter-field {
flex: 1 1 calc(33.333% - 11px);
min-width: 200px;
max-width: 100%;
// Ensure consistent height across all fields
::ng-deep .mat-mdc-form-field-infix {
min-height: 56px;
}
// Align datepicker icons consistently
::ng-deep .mat-datepicker-toggle {
display: flex;
align-items: center;
}
& + & {
border-top: 1px solid $gray-200;
}
}
.filter-button-container {
flex: 1 1 calc(33.333% - 11px);
min-width: 200px;
display: flex;
align-items: flex-end;
padding-bottom: 4px;
// Grid
.inv-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 4px 16px;
padding: 16px 24px;
.search-button {
width: 100%;
height: 48px;
font-size: 15px;
font-weight: 500;
letter-spacing: 0.5px;
@media (max-width: 768px) { grid-template-columns: 1fr; }
@media (max-width: 1024px) and (min-width: 769px) { grid-template-columns: repeat(2, 1fr); }
}
fa-icon {
margin-right: 8px;
.inv-field {
width: 100%;
}
.inv-search-btn-wrap {
display: flex;
align-items: center;
padding-top: 4px;
@media (max-width: 768px) { padding-top: 0; }
}
.inv-search-btn {
border-radius: 8px !important;
font-size: 13px !important;
font-weight: 600 !important;
height: 44px !important;
width: 100%;
box-shadow: 0 1px 3px rgba($primary, 0.3) !important;
i { margin-right: 6px; font-size: 12px; }
}
// Results area
.inv-results {
padding: 16px 24px;
}
// Loader
.inv-loader {
display: flex;
justify-content: center;
padding: 40px 0;
&__spinner {
width: 36px;
height: 36px;
border: 3px solid $gray-200;
border-top-color: $accent;
border-radius: 50%;
animation: inv-spin 0.7s linear infinite;
}
}
@keyframes inv-spin {
to { transform: rotate(360deg); }
}
// Accordion & Panels
.inv-accordion {
::ng-deep .mat-expansion-panel {
border: 1px solid $border;
border-radius: 8px !important;
margin-bottom: 10px;
box-shadow: none !important;
&:not(.mat-expanded) {
&:hover {
border-color: #c5cdd8;
}
}
}
// Responsive adjustments for tablets
@media screen and (width <= 1024px) {
.filter-field,
.filter-button-container {
flex: 1 1 calc(50% - 8px);
min-width: 180px;
}
}
// Responsive adjustments for mobile
@media screen and (width <= 768px) {
.filter-row {
flex-direction: column;
gap: 12px;
}
.filter-field,
.filter-button-container {
flex: 1 1 100%;
width: 100%;
max-width: 100%;
min-width: 100%;
}
.filter-button-container {
padding-bottom: 0;
margin-top: 8px;
&.mat-expanded {
border-color: $accent;
}
}
}
.status {
color: black;
.inv-panel-summary {
display: flex;
align-items: center;
gap: 20px;
flex-wrap: wrap;
width: 100%;
}
.field-name {
font-weight: 800;
.inv-panel-meta {
display: flex;
flex-direction: column;
gap: 2px;
font-size: 12px;
}
.record-actions {
margin-top: 1%;
.inv-panel-label {
font-size: 11px;
font-weight: 600;
color: $gray-500;
text-transform: uppercase;
letter-spacing: 0.3px;
}
button {
margin-right: 1%;
// Status badge
.inv-status-badge {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 600;
}
.inv-status-text {
color: $gray-900;
}
// Detail (expanded)
.inv-detail {
padding: 8px 0 16px;
&__grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
margin-bottom: 16px;
@media (max-width: 600px) { grid-template-columns: 1fr; }
}
&__item {
display: flex;
flex-direction: column;
gap: 4px;
}
&__label {
font-size: 11px;
font-weight: 600;
color: $gray-500;
text-transform: uppercase;
letter-spacing: 0.3px;
}
&__value {
font-size: 13px;
color: $gray-900;
}
}
.external-asset-transfer {
margin-top: 10px;
min-height: 160px;
}
// Financials
.inv-financials {
background: $gray-50;
border: 1px solid $gray-200;
border-radius: 8px;
padding: 14px 18px;
.record {
.record-actions {
margin-top: 1%;
&__title {
margin: 0 0 12px;
font-size: 13px;
font-weight: 600;
color: $gray-900;
}
button {
margin-right: 1%;
&__grid {
display: flex;
flex-direction: column;
gap: 8px;
}
&__row {
display: flex;
justify-content: space-between;
align-items: center;
padding: 4px 0;
&--total {
border-top: 1px solid $gray-200;
padding-top: 8px;
margin-top: 4px;
font-weight: 700;
}
}
h3 {
margin: 1% auto;
&__label {
font-size: 13px;
color: $gray-700;
}
p {
line-height: 30px;
margin-left: 2%;
&__amount {
font-size: 13px;
font-weight: 600;
color: $gray-900;
font-variant-numeric: tabular-nums;
}
}
$bar: 140px;
$foo: $bar * 8;
$color: #e2e4ec;
.loader {
margin-bottom: 39px;
// Panel actions
.inv-panel-actions {
display: flex;
justify-content: flex-end;
padding: 8px 0 0;
}
.sidenav {
overflow-x: hidden;
}
// Empty state
.inv-empty {
display: flex;
flex-direction: column;
align-items: center;
padding: 48px 0;
color: $gray-500;
@keyframes move {
0% {
left: 0;
&__icon {
font-size: 36px;
margin-bottom: 12px;
opacity: 0.5;
}
16.7% {
left: 0;
}
33.3% {
left: calc($foo / 2);
}
50% {
left: $foo - $bar;
}
66.7% {
left: calc($foo / 2);
}
83.3% {
left: 0;
}
100% {
left: 0;
}
}
@keyframes grow {
0% {
width: $bar;
}
16.7% {
width: calc($foo / 2);
}
33.3% {
width: calc($foo / 2);
}
50% {
width: $bar;
}
66.7% {
width: calc($foo / 2);
}
83.3% {
width: calc($foo / 2);
}
100% {
width: $bar;
&__text {
font-size: 14px;
margin: 0;
}
}