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
@@ -0,0 +1,49 @@
<!--
Copyright since 2025 Mifos Initiative
-->
<div class="crop-dialog">
<div class="crop-header">
<div class="crop-header__left">
<fa-icon icon="crop" class="crop-header__icon"></fa-icon>
<span class="crop-header__title">{{ data.title }}</span>
</div>
<button class="crop-header__close" (click)="dialogRef.close()">
<fa-icon icon="times"></fa-icon>
</button>
</div>
<mat-dialog-content class="crop-body">
@if (!imageLoaded) {
<div class="crop-loading">Loading image...</div>
}
<div class="crop-workspace" [class.hidden]="!imageLoaded">
<div class="crop-source">
<canvas #sourceCanvas
(mousedown)="onMouseDown($event)"
(mousemove)="onMouseMove($event)"
(mouseup)="onMouseUp()"
(mouseleave)="onMouseUp()">
</canvas>
<div class="crop-hint">Drag to move. Drag corner to resize.</div>
</div>
<div class="crop-preview-panel">
<span class="crop-preview-label">Preview</span>
<div class="crop-preview-box">
<canvas #previewCanvas></canvas>
</div>
<span class="crop-preview-size">{{ data.maxWidth }}x{{ data.maxHeight }}px</span>
</div>
</div>
</mat-dialog-content>
<mat-dialog-actions class="crop-actions">
<button mat-stroked-button (click)="dialogRef.close()">Cancel</button>
<button mat-flat-button color="primary" [disabled]="!imageLoaded" (click)="confirm()">
<fa-icon icon="check" class="crop-actions__icon"></fa-icon>
Crop &amp; Use
</button>
</mat-dialog-actions>
</div>
@@ -0,0 +1,176 @@
/**
* Copyright since 2025 Mifos Initiative
*/
$blue: #1353a0;
$navy: #0f172a;
.crop-dialog {
width: 620px;
max-width: 95vw;
}
.crop-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 24px 12px;
border-bottom: 1px solid #e2e8f0;
&__left {
display: flex;
align-items: center;
gap: 10px;
}
&__icon {
font-size: 16px;
color: $blue;
}
&__title {
font-size: 16px;
font-weight: 600;
color: $navy;
}
&__close {
width: 30px;
height: 30px;
border: none;
background: #f1f5f9;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
color: #64748b;
transition: all 0.15s;
&:hover {
background: #e2e8f0;
color: $navy;
}
}
}
.crop-body {
padding: 20px 24px !important;
overflow: visible !important;
max-height: 70vh !important;
}
.crop-loading {
text-align: center;
padding: 40px;
color: #94a3b8;
font-size: 14px;
}
.hidden {
display: none !important;
}
.crop-workspace {
display: flex;
gap: 20px;
align-items: flex-start;
}
.crop-source {
flex: 1;
min-width: 0;
canvas {
cursor: crosshair;
border: 1px solid #e2e8f0;
border-radius: 8px;
display: block;
max-width: 100%;
}
}
.crop-hint {
font-size: 11px;
color: #94a3b8;
margin-top: 6px;
text-align: center;
}
.crop-preview-panel {
width: 160px;
flex-shrink: 0;
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
}
.crop-preview-label {
font-size: 11px;
font-weight: 600;
color: #64748b;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.crop-preview-box {
width: 150px;
height: 150px;
border: 2px solid $blue;
border-radius: 8px;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
background: #f8fafc;
canvas {
display: block;
max-width: 100%;
max-height: 100%;
}
}
.crop-preview-size {
font-size: 10px;
color: #94a3b8;
font-family: 'Roboto Mono', monospace;
}
.crop-actions {
padding: 12px 24px 16px !important;
border-top: 1px solid #e2e8f0;
margin: 0 !important;
display: flex !important;
justify-content: flex-end !important;
gap: 10px !important;
button {
border-radius: 8px !important;
font-size: 13px !important;
font-weight: 600 !important;
}
&__icon {
margin-right: 6px;
font-size: 12px;
}
}
@media (max-width: 600px) {
.crop-workspace {
flex-direction: column;
}
.crop-preview-panel {
width: 100%;
flex-direction: row;
justify-content: center;
}
.crop-preview-box {
width: 100px;
height: 100px;
}
}
@@ -0,0 +1,244 @@
/**
* Copyright since 2025 Mifos Initiative
*/
import { Component, Inject, ViewChild, ElementRef, AfterViewInit } from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef, MatDialogContent, MatDialogActions } from '@angular/material/dialog';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
import { FaIconComponent } from '@fortawesome/angular-fontawesome';
export interface ImageCropData {
file: File;
title: string;
aspectRatio: number; // width/height, e.g. 1 for square, 1.5 for 3:2
maxWidth: number;
maxHeight: number;
}
@Component({
selector: 'mifosx-image-crop-dialog',
templateUrl: './image-crop-dialog.component.html',
styleUrls: ['./image-crop-dialog.component.scss'],
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
MatDialogContent,
MatDialogActions
]
})
export class ImageCropDialogComponent implements AfterViewInit {
@ViewChild('sourceCanvas') sourceCanvas!: ElementRef<HTMLCanvasElement>;
@ViewChild('previewCanvas') previewCanvas!: ElementRef<HTMLCanvasElement>;
imageLoaded = false;
private img = new Image();
private imgWidth = 0;
private imgHeight = 0;
// Crop box (relative to displayed image)
cropX = 0;
cropY = 0;
cropW = 200;
cropH = 200;
// Drag state
private dragging = false;
private dragStartX = 0;
private dragStartY = 0;
private dragStartCropX = 0;
private dragStartCropY = 0;
// Resize state
private resizing = false;
// Canvas display dimensions
private canvasDisplayW = 0;
private canvasDisplayH = 0;
private scaleX = 1;
private scaleY = 1;
zoom = 1;
constructor(
public dialogRef: MatDialogRef<ImageCropDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: ImageCropData
) {}
ngAfterViewInit() {
const reader = new FileReader();
reader.onload = (e: any) => {
this.img.onload = () => {
this.imageLoaded = true;
setTimeout(() => this.initCanvas());
};
this.img.src = e.target.result;
};
reader.readAsDataURL(this.data.file);
}
private initCanvas() {
const canvas = this.sourceCanvas.nativeElement;
const ctx = canvas.getContext('2d')!;
const container = canvas.parentElement!;
const maxW = container.clientWidth || 500;
const maxH = 400;
// Scale image to fit container
const ratio = Math.min(maxW / this.img.width, maxH / this.img.height, 1);
this.canvasDisplayW = Math.floor(this.img.width * ratio);
this.canvasDisplayH = Math.floor(this.img.height * ratio);
this.imgWidth = this.img.width;
this.imgHeight = this.img.height;
canvas.width = this.canvasDisplayW;
canvas.height = this.canvasDisplayH;
this.scaleX = this.img.width / this.canvasDisplayW;
this.scaleY = this.img.height / this.canvasDisplayH;
// Init crop box centered
const cropSize = Math.min(this.canvasDisplayW, this.canvasDisplayH) * 0.6;
if (this.data.aspectRatio >= 1) {
this.cropW = cropSize;
this.cropH = cropSize / this.data.aspectRatio;
} else {
this.cropH = cropSize;
this.cropW = cropSize * this.data.aspectRatio;
}
this.cropX = (this.canvasDisplayW - this.cropW) / 2;
this.cropY = (this.canvasDisplayH - this.cropH) / 2;
this.drawCanvas();
this.updatePreview();
}
private drawCanvas() {
const canvas = this.sourceCanvas.nativeElement;
const ctx = canvas.getContext('2d')!;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(this.img, 0, 0, canvas.width, canvas.height);
// Dark overlay outside crop
ctx.fillStyle = 'rgba(0,0,0,0.5)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
// Clear crop area
ctx.clearRect(this.cropX, this.cropY, this.cropW, this.cropH);
ctx.drawImage(
this.img,
this.cropX * this.scaleX, this.cropY * this.scaleY,
this.cropW * this.scaleX, this.cropH * this.scaleY,
this.cropX, this.cropY, this.cropW, this.cropH
);
// Crop border
ctx.strokeStyle = '#1353a0';
ctx.lineWidth = 2;
ctx.strokeRect(this.cropX, this.cropY, this.cropW, this.cropH);
// Corner handles
const hs = 8;
ctx.fillStyle = '#1353a0';
const corners = [
[this.cropX, this.cropY],
[this.cropX + this.cropW, this.cropY],
[this.cropX, this.cropY + this.cropH],
[this.cropX + this.cropW, this.cropY + this.cropH]
];
corners.forEach(([x, y]) => {
ctx.fillRect(x - hs / 2, y - hs / 2, hs, hs);
});
}
private updatePreview() {
if (!this.previewCanvas) return;
const pCanvas = this.previewCanvas.nativeElement;
const pCtx = pCanvas.getContext('2d')!;
const pw = this.data.maxWidth || 150;
const ph = this.data.maxHeight || 150;
pCanvas.width = pw;
pCanvas.height = ph;
pCtx.clearRect(0, 0, pw, ph);
pCtx.drawImage(
this.img,
this.cropX * this.scaleX, this.cropY * this.scaleY,
this.cropW * this.scaleX, this.cropH * this.scaleY,
0, 0, pw, ph
);
}
onMouseDown(event: MouseEvent) {
const rect = this.sourceCanvas.nativeElement.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
// Check if near bottom-right corner for resize
const cornerX = this.cropX + this.cropW;
const cornerY = this.cropY + this.cropH;
if (Math.abs(x - cornerX) < 12 && Math.abs(y - cornerY) < 12) {
this.resizing = true;
return;
}
// Check if inside crop box for drag
if (x >= this.cropX && x <= this.cropX + this.cropW && y >= this.cropY && y <= this.cropY + this.cropH) {
this.dragging = true;
this.dragStartX = x;
this.dragStartY = y;
this.dragStartCropX = this.cropX;
this.dragStartCropY = this.cropY;
}
}
onMouseMove(event: MouseEvent) {
if (!this.dragging && !this.resizing) return;
const rect = this.sourceCanvas.nativeElement.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
if (this.dragging) {
this.cropX = Math.max(0, Math.min(this.canvasDisplayW - this.cropW, this.dragStartCropX + (x - this.dragStartX)));
this.cropY = Math.max(0, Math.min(this.canvasDisplayH - this.cropH, this.dragStartCropY + (y - this.dragStartY)));
}
if (this.resizing) {
const newW = Math.max(40, Math.min(this.canvasDisplayW - this.cropX, x - this.cropX));
const newH = newW / this.data.aspectRatio;
if (this.cropY + newH <= this.canvasDisplayH) {
this.cropW = newW;
this.cropH = newH;
}
}
this.drawCanvas();
this.updatePreview();
}
onMouseUp() {
this.dragging = false;
this.resizing = false;
}
confirm() {
// Create cropped image as blob
const offscreen = document.createElement('canvas');
offscreen.width = this.data.maxWidth || 300;
offscreen.height = this.data.maxHeight || 300;
const ctx = offscreen.getContext('2d')!;
ctx.drawImage(
this.img,
this.cropX * this.scaleX, this.cropY * this.scaleY,
this.cropW * this.scaleX, this.cropH * this.scaleY,
0, 0, offscreen.width, offscreen.height
);
offscreen.toBlob((blob) => {
if (blob) {
const croppedFile = new File([blob], this.data.file.name, { type: 'image/png' });
this.dialogRef.close({ croppedFile, previewUrl: offscreen.toDataURL('image/png') });
}
}, 'image/png');
}
}