fix(clients): stale signature bug, monthly turnover/annual income fields, senior citizen/PWD classification

- Fix signature-document reads picking the oldest upload instead of the latest across 7 call
  sites (CKYC tab, view/delete-signature dialogs, edit-wizard preview, cashier transaction entry,
  signatory service, record-master) by sorting on document id instead of an unsorted find().
- Add Monthly Turnover and Annual Income fields alongside the renamed Yearly Turnover field.
- Add Senior Citizen (auto-detected from DOB) and Person with Disability classification, with a
  required disability-certificate upload gate before client creation can proceed.
- Add Graduate and Diploma as distinct Education Levels, with diploma-specific degree options.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-28 10:49:28 +05:30
co-authored by Claude Sonnet 5
parent a9c60083bf
commit cb4260b7a3
11 changed files with 367 additions and 16 deletions
@@ -93,6 +93,54 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
/** Expose enum to template */
readonly LegalFormId = LegalFormId;
/** Senior Citizen proof is optional; the PWD certificate is mandatory (see seniorPwdDocsComplete). Both, when
* attached, are uploaded after client creation. */
seniorCitizenProofFile: File | null = null;
pwdCertificateFile: File | null = null;
/** Disability categories (per the Rights of Persons with Disabilities Act, 2016) — fetched from
* m_code_value (code "PwdCategory") via the client template, same as occupationCategoryOptions.
* Admins can add/reorder/deactivate entries from System -> Manage Codes without a code change. */
pwdCategoryOptions: any[] = [];
/** Set true after a save attempt so the UI can highlight a missing required document */
seniorPwdDocsTouched = false;
/** Senior Citizen proof is optional. The PWD certificate + disability type are mandatory
* whenever the PWD checkbox is set. */
get seniorPwdDocsComplete(): boolean {
const pwdOk =
!this.createClientForm.get('isPwd')?.value ||
(!!this.pwdCertificateFile && !!this.createClientForm.get('pwdCategoryId')?.value);
return pwdOk;
}
markSeniorPwdDocsTouched(): void {
this.seniorPwdDocsTouched = true;
}
onSeniorCitizenProofSelected(event: any): void {
const file = event.target?.files?.[0];
if (file) {
this.seniorCitizenProofFile = file;
}
}
removeSeniorCitizenProof(): void {
this.seniorCitizenProofFile = null;
}
onPwdCertificateSelected(event: any): void {
const file = event.target?.files?.[0];
if (file) {
this.pwdCertificateFile = file;
}
}
removePwdCertificate(): void {
this.pwdCertificateFile = null;
}
/** Minimum date allowed. */
minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */
@@ -157,6 +205,8 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
calculatedAgeDetail: string | null = null;
/** Whether client is a minor (age < 10) */
isMinor = false;
/** Whether client is a senior citizen (age >= 60) — auto-detected from Date of Birth */
isSeniorCitizen = false;
/** Age breakdown for DB storage */
ageBreakdown = { years: 0, months: 0, days: 0 };
@@ -525,7 +575,11 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
occupationCategoryId: data.occupationCategory?.id || data.occupationCategoryId || '',
occupationId: data.occupation?.id || data.occupationId || '',
monthlyIncome: data.monthlyIncome || '',
annualIncome: data.annualIncome || '',
annualTurnover: data.annualTurnover || '',
monthlyTurnover: data.monthlyTurnover || '',
isPwd: data.isPwd || false,
pwdCategoryId: data.pwdCategory?.id || '',
agricultureIncome: data.agricultureIncome || '',
nonAgricultureIncome: data.nonAgricultureIncome || '',
submittedOnDate: data.timeline?.submittedOnDate
@@ -665,6 +719,13 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
Validators.min(0)
]
],
annualIncome: [
'',
[
Validators.pattern(/^\d+(\.\d{1,2})?$/),
Validators.min(0)
]
],
annualTurnover: [
'',
[
@@ -672,6 +733,15 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
Validators.min(0)
]
],
monthlyTurnover: [
'',
[
Validators.pattern(/^\d+(\.\d{1,2})?$/),
Validators.min(0)
]
],
isPwd: [false],
pwdCategoryId: [''],
agricultureIncome: [
'',
[
@@ -709,6 +779,7 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
this.occupationCategoryOptions = this.clientTemplate.occupationCategoryOptions || [];
this.occupationOptions = this.clientTemplate.occupationOptions || [];
this.salutationOptions = this.clientTemplate.salutationOptions || [];
this.pwdCategoryOptions = this.clientTemplate.pwdCategoryOptions || [];
}
/**
@@ -825,6 +896,7 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
this.ageBreakdown = { years, months, days };
const wasMinor = this.isMinor;
this.isMinor = years < 10;
this.isSeniorCitizen = years >= 60;
// Add/remove guardian form group based on minor status
if (this.isMinor && !wasMinor) {
this.createClientForm.addControl(
@@ -850,12 +922,14 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
} else {
this.calculatedAgeDetail = null;
this.isMinor = false;
this.isSeniorCitizen = false;
this.ageBreakdown = { years: 0, months: 0, days: 0 };
this.createClientForm.removeControl('guardianDetails');
}
} else {
this.calculatedAgeDetail = null;
this.isMinor = false;
this.isSeniorCitizen = false;
this.ageBreakdown = { years: 0, months: 0, days: 0 };
this.createClientForm.removeControl('guardianDetails');
}
@@ -1548,6 +1622,9 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
if (n.includes('postgrad') || n === 'pg' || n.includes('master')) return 'postgraduate';
if (n.includes('undergrad') || n === 'ug' || n.includes('bachelor') || n.includes('graduate'))
return 'undergraduate';
// Diploma is its own Education Level, distinct from School — checked before the school
// keywords below so it doesn't get swept into that bucket.
if (n.includes('diploma') || n.includes('polytechnic')) return 'diploma';
if (
n.includes('school') ||
n.includes('10th') ||
@@ -1556,7 +1633,6 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
n.includes('hsc') ||
n.includes('matric') ||
n.includes('intermediate') ||
n.includes('diploma') ||
n.includes('iti') ||
n.includes('below')
)
@@ -1634,8 +1710,12 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
}
/**
* Degrees filtered by the currently selected Education Level.
* Returns all degrees if no level is selected, or category can't be determined.
* Degrees filtered by the currently selected Education Level. Each DegreeQualification code
* value is tagged with its owning level in `description` (e.g. "Undergraduate") when seeded —
* match on that authoritative field first. Only fall back to the name-keyword heuristic
* (getDegreeQualificationCategory) for older/untagged rows that predate this tagging, so
* degrees never silently disappear because an admin added one without a description.
* Returns all degrees if no level is selected.
*/
get filteredDegreeQualificationOptions(): any[] {
const level = this.getSelectedEducationLevel();
@@ -1643,6 +1723,9 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
const category = this.getEducationLevelCategory(level.name);
if (!category) return this.degreeQualificationOptions;
const filtered = this.degreeQualificationOptions.filter((d: any) => {
if (d.description) {
return this.getEducationLevelCategory(d.description) === category;
}
const c = this.getDegreeQualificationCategory(d.name);
return c === category || c === '';
});
@@ -1805,6 +1888,8 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
// Store extended fields separately (not yet supported by Fineract API)
// Add isMinor flag
generalDetails.isMinor = this.isMinor;
// isSeniorCitizen is auto-detected from DOB (not a form control) — add it explicitly
generalDetails.isSeniorCitizen = this.isSeniorCitizen;
// Add guardian clientId for minors
if (this.isMinor && generalDetails.guardianDetails) {