feat: redesign client view header — dark gradient card with modern layout

- Dark gradient background (#0f1923 → #1c2d3f) replacing old blue card
- Rounded 72px avatar with compact upload/capture/delete buttons
- Name row: bold white text + green status badge + menu button
- ID chips: monospace font, semi-transparent background
- 4-column details grid with cell borders inside rounded container
- All labels 9px uppercase, values 12.5px white
- Responsive: 3-col → 2-col → 1-col on smaller screens
- Also fix: 2-column grid specificity for dialog file upload

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-03-21 00:40:38 +05:30
co-authored by Claude Opus 4.6
parent 693ffed397
commit 54ab436563
4 changed files with 379 additions and 246 deletions
@@ -6,165 +6,158 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<mat-card class="account-card">
<mat-card-header class="header layout-column">
<mat-card-title-group class="header-title-group">
<div class="profile-image-container">
<div>
<img
mat-card-md-image
class="profile-image"
[src]="clientImage ? clientImage : 'assets/images/user_placeholder.png'"
/>
</div>
<div class="m-b-5 flex-center">
<button
class="client-image-button"
mat-raised-button
(click)="doAction('Upload Image')"
matTooltip="{{ 'tooltips.Upload Image' | translate }}"
*mifosxHasPermission="'CREATE_CLIENTIMAGE'"
>
<i class="fa fa-upload"></i>
</button>
<button
class="client-image-button"
mat-raised-button
(click)="doAction('Capture Image')"
matTooltip="{{ 'tooltips.Capture Image' | translate }}"
*mifosxHasPermission="'CREATE_CLIENTIMAGE'"
>
<i class="fa fa-camera"></i>
</button>
<button
class="client-image-button"
mat-raised-button
(click)="doAction('Delete Image')"
matTooltip="{{ 'tooltips.Delete Image' | translate }}"
*mifosxHasPermission="'DELETE_CLIENTIMAGE'"
>
<i class="fa fa-trash"></i>
</button>
<br />
</div>
<p (click)="doAction('View Signature')" class="signature">{{ 'labels.inputs.View Signature' | translate }}</p>
<div class="cv-card">
<div class="cv-top">
<!-- Avatar -->
<div class="cv-avatar">
<img class="cv-avatar__img"
[src]="clientImage ? clientImage : 'assets/images/user_placeholder.png'" />
<div class="cv-avatar__actions">
<button class="cv-avatar__btn"
(click)="doAction('Upload Image')"
matTooltip="{{ 'tooltips.Upload Image' | translate }}"
*mifosxHasPermission="'CREATE_CLIENTIMAGE'">
<i class="fa fa-upload"></i>
</button>
<button class="cv-avatar__btn"
(click)="doAction('Capture Image')"
matTooltip="{{ 'tooltips.Capture Image' | translate }}"
*mifosxHasPermission="'CREATE_CLIENTIMAGE'">
<i class="fa fa-camera"></i>
</button>
<button class="cv-avatar__btn cv-avatar__btn--danger"
(click)="doAction('Delete Image')"
matTooltip="{{ 'tooltips.Delete Image' | translate }}"
*mifosxHasPermission="'DELETE_CLIENTIMAGE'">
<i class="fa fa-trash"></i>
</button>
</div>
<span class="cv-avatar__sig" (click)="doAction('View Signature')">{{ 'labels.inputs.View Signature' | translate }}</span>
</div>
<div class="mat-typography account-card-title">
<!-- 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>
<!-- Info -->
<div class="cv-info">
<!-- Name + Status + Menu -->
<div class="cv-name-row">
<div class="cv-name">
<i class="fa fa-stop cv-name__dot" [ngClass]="clientViewData.status.code | statusLookup"
matTooltip="{{ 'labels.status.' + clientViewData.status.value | translate }}"></i>
<span class="cv-name__text">
<mifosx-entity-name
[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>
<span class="cv-name__status">
{{ 'labels.status.' + clientViewData.status.value | translate }}
</span>
</div>
<button mat-icon-button [matMenuTriggerFor]="clientMenu" class="cv-menu-btn" aria-label="Client actions">
<mat-icon matListIcon class="actions-menu"><fa-icon icon="bars" size="sm"></fa-icon></mat-icon>
</button>
</div>
<!-- ID Chips -->
<div class="cv-ids">
<div class="cv-id">
<span class="cv-id__label">{{ 'labels.inputs.Account No' | translate }}</span>
<span class="cv-id__val">
<mifosx-account-number accountNo="{{ clientViewData.accountNo }}"></mifosx-account-number>
</span>
</div>
@if (clientViewData.cifNumber) {
<div class="cv-id">
<span class="cv-id__label">{{ 'labels.inputs.CIF Number' | translate }}</span>
<span class="cv-id__val">
<mifosx-account-number accountNo="{{ clientViewData.cifNumber }}"></mifosx-account-number>
</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>
}
@if (clientViewData.externalId) {
<div class="cv-id">
<span class="cv-id__label">{{ 'labels.inputs.External Id' | translate }}</span>
<span class="cv-id__val">
<mifosx-external-identifier
[externalId]="complianceHideClientData ? maskValue(clientViewData.externalId) : clientViewData.externalId"
[completed]="true" [hideCopy]="complianceHideClientData"></mifosx-external-identifier>
</div>
}
</div>
<!-- Details Grid -->
<mat-card-subtitle>
<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="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>
}
</span>
</div>
</mat-card-subtitle>
}
</div>
<!-- Details Grid -->
<div class="cv-grid">
<div class="cv-cell">
<div class="cv-cell__label">{{ 'labels.inputs.Office' | translate }}</div>
<div class="cv-cell__value">{{ clientViewData.officeName }}</div>
</div>
<div class="cv-cell">
<div class="cv-cell__label">{{ 'labels.inputs.Staff' | translate }}</div>
<div class="cv-cell__value">{{ clientViewData.staffName || ('labels.inputs.Unassigned' | translate) }}</div>
</div>
@if (clientViewData.activationDate) {
<div class="cv-cell">
<div class="cv-cell__label">{{ 'labels.inputs.Activation Date' | translate }}</div>
<div class="cv-cell__value">{{ clientViewData.activationDate | dateFormat }}</div>
</div>
}
@if (clientViewData.clientType) {
<div class="cv-cell">
<div class="cv-cell__label">{{ 'labels.inputs.Client Type' | translate }}</div>
<div class="cv-cell__value">{{ clientViewData.clientType.name }}</div>
</div>
}
@if (clientViewData.clientClassification) {
<div class="cv-cell">
<div class="cv-cell__label">{{ 'labels.inputs.Client Classification' | translate }}</div>
<div class="cv-cell__value">{{ clientViewData.clientClassification.name }}</div>
</div>
}
@if (clientViewData.mobileNo) {
<div class="cv-cell">
<div class="cv-cell__label">{{ 'labels.inputs.Mobile Number' | translate }}</div>
<div class="cv-cell__value">
<mifosx-external-identifier
[externalId]="complianceHideClientData ? maskValue(clientViewData.mobileNo) : clientViewData.mobileNo"
[completed]="true" [hideCopy]="complianceHideClientData"></mifosx-external-identifier>
</div>
</div>
}
@if (clientViewData.emailAddress) {
<div class="cv-cell">
<div class="cv-cell__label">{{ 'labels.inputs.Email' | translate }}</div>
<div class="cv-cell__value">
<mifosx-external-identifier
[externalId]="complianceHideClientData ? maskEmail(clientViewData.emailAddress) : clientViewData.emailAddress"
[completed]="true" [hideCopy]="complianceHideClientData"></mifosx-external-identifier>
</div>
</div>
}
@if (clientViewData.occupationCategory) {
<div class="cv-cell">
<div class="cv-cell__label">{{ 'labels.inputs.Occupation Category' | translate }}</div>
<div class="cv-cell__value">{{ clientViewData.occupationCategory.name }}</div>
</div>
}
@if (clientViewData.occupation) {
<div class="cv-cell">
<div class="cv-cell__label">{{ 'labels.inputs.Occupation' | translate }}</div>
<div class="cv-cell__value">{{ clientViewData.occupation.name }}</div>
</div>
}
@if (clientViewData.groups?.length > 0) {
<div class="cv-cell">
<div class="cv-cell__label">{{ 'labels.inputs.Member Of' | translate }}</div>
<div class="cv-cell__value">
@for (group of clientViewData.groups; track group) {
<span>{{ group.name }} </span>
}
</div>
</div>
}
</div>
</div>
</div>
<mat-menu #clientMenu="matMenu">
<button mat-menu-item [routerLink]="['edit']" *mifosxHasPermission="'UPDATE_CLIENT'">
<mat-icon matListIcon>
@@ -362,10 +355,9 @@
</span>
}
</mat-menu>
</mat-card-title-group>
</mat-card-header>
</mat-menu>
<mat-card-content>
<div>
<nav mat-tab-nav-bar class="navigation-tabs" [tabPanel]="tabPanel">
<a
mat-tab-link
@@ -464,5 +456,5 @@
<mat-tab-nav-panel #tabPanel>
<router-outlet></router-outlet>
</mat-tab-nav-panel>
</mat-card-content>
</mat-card>
</div>
</div>