fix(clients): address-proof date fields use calendar picker, Pension Payment Order gets one too

Both the address-proof upload dialog and the permanent-address step used a
native `type="date"` input for the issue/statement date, so no calendar
popup was available. Switched both to Angular Material's datepicker, kept
the client-india-address-step round-trip (payload -> saved description ->
re-parsed on edit) working with the Dates util now that the control holds
a Date object instead of a raw string.

Pension Payment Orders was also the only "Financial & Other" address-proof
type missing requiresDate, so selecting it never showed a date field at
all — added the flag and moved Monthly/Yearly Turnover under the "Other
Income" heading alongside Agriculture/Non-Agriculture Income.
This commit is contained in:
2026-07-30 10:30:00 +05:30
parent 56f13ade67
commit 5dc284bd1c
6 changed files with 72 additions and 68 deletions
@@ -1232,38 +1232,6 @@
} }
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline" class="form-field">
<mat-label>Monthly Turnover</mat-label>
<span matPrefix class="rupee-prefix">&#8377;</span>
<input matInput type="number" formControlName="monthlyTurnover" min="0" />
@if (
createClientForm.get('monthlyTurnover')?.hasError('pattern') &&
createClientForm.get('monthlyTurnover')?.touched
) {
<mat-error>Enter a valid amount</mat-error>
} @else if (
createClientForm.get('monthlyTurnover')?.hasError('min') && createClientForm.get('monthlyTurnover')?.touched
) {
<mat-error>Amount cannot be negative</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="form-field">
<mat-label>Yearly Turnover</mat-label>
<span matPrefix class="rupee-prefix">&#8377;</span>
<input matInput type="number" formControlName="annualTurnover" min="0" />
@if (
createClientForm.get('annualTurnover')?.hasError('pattern') &&
createClientForm.get('annualTurnover')?.touched
) {
<mat-error>Enter a valid amount</mat-error>
} @else if (
createClientForm.get('annualTurnover')?.hasError('min') && createClientForm.get('annualTurnover')?.touched
) {
<mat-error>Amount cannot be negative</mat-error>
}
</mat-form-field>
<div class="full-width subsection-title"> <div class="full-width subsection-title">
<fa-icon icon="coins" class="subsection-icon"></fa-icon> <fa-icon icon="coins" class="subsection-icon"></fa-icon>
Other Income Other Income
@@ -1303,6 +1271,38 @@
} }
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline" class="form-field">
<mat-label>Monthly Turnover</mat-label>
<span matPrefix class="rupee-prefix">&#8377;</span>
<input matInput type="number" formControlName="monthlyTurnover" min="0" />
@if (
createClientForm.get('monthlyTurnover')?.hasError('pattern') &&
createClientForm.get('monthlyTurnover')?.touched
) {
<mat-error>Enter a valid amount</mat-error>
} @else if (
createClientForm.get('monthlyTurnover')?.hasError('min') && createClientForm.get('monthlyTurnover')?.touched
) {
<mat-error>Amount cannot be negative</mat-error>
}
</mat-form-field>
<mat-form-field appearance="outline" class="form-field">
<mat-label>Yearly Turnover</mat-label>
<span matPrefix class="rupee-prefix">&#8377;</span>
<input matInput type="number" formControlName="annualTurnover" min="0" />
@if (
createClientForm.get('annualTurnover')?.hasError('pattern') &&
createClientForm.get('annualTurnover')?.touched
) {
<mat-error>Enter a valid amount</mat-error>
} @else if (
createClientForm.get('annualTurnover')?.hasError('min') && createClientForm.get('annualTurnover')?.touched
) {
<mat-error>Amount cannot be negative</mat-error>
}
</mat-form-field>
<div class="full-width subsection-title"> <div class="full-width subsection-title">
<fa-icon icon="user-shield" class="subsection-icon"></fa-icon> <fa-icon icon="user-shield" class="subsection-icon"></fa-icon>
Senior Citizen / Person with Disability Senior Citizen / Person with Disability
@@ -173,8 +173,9 @@
@if (selectedAddrProof.requiresDate) { @if (selectedAddrProof.requiresDate) {
<mat-form-field appearance="outline" class="addr-ff"> <mat-form-field appearance="outline" class="addr-ff">
<mat-label>Issue / bill date</mat-label> <mat-label>Issue / bill date</mat-label>
<span matPrefix class="addr-prefix"><mat-icon>event</mat-icon></span> <input matInput [matDatepicker]="addrIssueDatePicker" formControlName="issueDate" />
<input matInput type="date" formControlName="issueDate" /> <mat-datepicker-toggle matSuffix [for]="addrIssueDatePicker"></mat-datepicker-toggle>
<mat-datepicker #addrIssueDatePicker></mat-datepicker>
</mat-form-field> </mat-form-field>
} }
</div> </div>
@@ -20,6 +20,7 @@ import { ClientsService } from '../../clients.service';
import { ProofType, normalizeProofType, groupProofsByCategory } from '../../shared/kyc-proof-types'; import { ProofType, normalizeProofType, groupProofsByCategory } from '../../shared/kyc-proof-types';
import { KycUploadHelperService } from '../../shared/kyc-upload-helper.service'; import { KycUploadHelperService } from '../../shared/kyc-upload-helper.service';
import { DocumentPreviewDialogComponent } from '../../shared/document-preview-dialog/document-preview-dialog.component'; import { DocumentPreviewDialogComponent } from '../../shared/document-preview-dialog/document-preview-dialog.component';
import { Dates } from 'app/core/utils/dates';
@Component({ @Component({
selector: 'mifosx-client-india-address-step', selector: 'mifosx-client-india-address-step',
@@ -41,6 +42,7 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
private clientsService = inject(ClientsService); private clientsService = inject(ClientsService);
private kycUpload = inject(KycUploadHelperService); private kycUpload = inject(KycUploadHelperService);
private dialog = inject(MatDialog); private dialog = inject(MatDialog);
private dates = inject(Dates);
/** Client ID — when set, loads existing addresses for edit mode */ /** Client ID — when set, loads existing addresses for edit mode */
@Input() clientId: string = ''; @Input() clientId: string = '';
@@ -413,11 +415,12 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
files.push({ slot: 'Back', file: this.addrFiles.back }); files.push({ slot: 'Back', file: this.addrFiles.back });
} }
} }
const rawIssueDate = this.permanentForm.get('issueDate')?.value;
return { return {
proof: this.selectedAddrProof, proof: this.selectedAddrProof,
proofId: this.permanentForm.get('proofId')?.value || '', proofId: this.permanentForm.get('proofId')?.value || '',
documentKey: this.permanentForm.get('documentKey')?.value || '', documentKey: this.permanentForm.get('documentKey')?.value || '',
issueDate: this.permanentForm.get('issueDate')?.value || '', issueDate: rawIssueDate ? this.dates.getDate(rawIssueDate) : '',
files files
}; };
} }
@@ -560,13 +563,13 @@ export class ClientIndiaAddressStepComponent implements OnInit, OnChanges {
proofId: matchedProof.id, proofId: matchedProof.id,
documentKey: identifier.documentKey || '' documentKey: identifier.documentKey || ''
}); });
// The description carries `Issue date: dd/mm/yyyy` when set on create (see // The description carries `Issue date: yyyy-MM-dd` when set on create (see
// create-client.saveAddressProof) — pull it out so the form's issueDate field // create-client.saveAddressProof) — pull it out so the form's issueDate field
// shows what was previously entered. // shows what was previously entered.
const desc: string = identifier.description || ''; const desc: string = identifier.description || '';
const issueMatch = desc.match(/Issue date:\s*(.+)$/i); const issueMatch = desc.match(/Issue date:\s*(.+)$/i);
if (issueMatch) { if (issueMatch) {
this.permanentForm.patchValue({ issueDate: issueMatch[1].trim() }); this.permanentForm.patchValue({ issueDate: this.dates.parseDate(issueMatch[1].trim()) });
} }
} }
@@ -347,9 +347,18 @@
<i class="fa fa-calendar"></i> Issue / statement date <i class="fa fa-calendar"></i> Issue / statement date
<span class="kyc-opt">(not older than 3 months)</span> <span class="kyc-opt">(not older than 3 months)</span>
</label> </label>
<div class="kyc-input-wrap kyc-input-wrap--date"> <div class="kyc-input-wrap kyc-input-wrap--date" (click)="uploadIssueDatePicker.open()">
<i class="fa fa-calendar-o kyc-input-wrap__icon"></i> <input
<input type="date" class="kyc-input kyc-input--date" formControlName="issueDate" /> class="kyc-input kyc-input--date"
[matDatepicker]="uploadIssueDatePicker"
formControlName="issueDate"
readonly
/>
<mat-datepicker-toggle
class="kyc-input-wrap__calendar-toggle"
[for]="uploadIssueDatePicker"
></mat-datepicker-toggle>
<mat-datepicker #uploadIssueDatePicker></mat-datepicker>
</div> </div>
</div> </div>
} }
@@ -388,42 +388,32 @@ $red-900: #993c1d;
padding-left: 38px; padding-left: 38px;
} }
/* Date input — match styling of other kyc inputs */ /* Date input — opens the Material calendar picker; styled to match other kyc inputs */
.kyc-input--date { .kyc-input--date {
font-family: inherit; font-family: inherit;
color: $gray-900; color: $gray-900;
cursor: pointer; cursor: pointer;
min-height: 40px; min-height: 40px;
padding-right: 14px !important; padding-left: 12px !important;
padding-right: 40px !important;
}
&::-webkit-calendar-picker-indicator { .kyc-input-wrap--date .kyc-input-wrap__calendar-toggle {
opacity: 0.55; position: absolute;
cursor: pointer; right: 2px;
filter: grayscale(1); top: 50%;
transition: opacity 0.15s; transform: translateY(-50%);
color: $gray-500;
::ng-deep .mat-mdc-icon-button {
width: 32px;
height: 32px;
padding: 4px;
} }
&:hover::-webkit-calendar-picker-indicator { ::ng-deep .mat-mdc-icon-button svg {
opacity: 0.9; width: 18px;
} height: 18px;
&::-webkit-datetime-edit {
color: $gray-900;
padding-top: 1px;
}
&::-webkit-datetime-edit-fields-wrapper {
color: $gray-900;
}
&::-webkit-datetime-edit-text {
color: $gray-500;
padding: 0 2px;
}
&:invalid::-webkit-datetime-edit,
&:not(:focus):placeholder-shown::-webkit-datetime-edit {
color: $gray-500;
} }
} }
@@ -145,6 +145,7 @@ export const PROOF_META: Record<string, Partial<ProofType>> = {
PENSION_PAYMENT_ORDER: { PENSION_PAYMENT_ORDER: {
sides: 1, sides: 1,
category: 'Financial & other', category: 'Financial & other',
requiresDate: true,
info: 'Pension payment order — upload full document issued by pension authority.' info: 'Pension payment order — upload full document issued by pension authority.'
}, },
CERT_INCORPORATION: { CERT_INCORPORATION: {