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
@@ -394,7 +394,12 @@ export class JointHolderDialogComponent implements OnInit {
this.kyc = kycArr[0] ?? null; this.kyc = kycArr[0] ?? null;
this.photo = photo ?? null; this.photo = photo ?? null;
const docs: any[] = Array.isArray(documents) ? documents : []; const docs: any[] = Array.isArray(documents) ? documents : [];
const sigDoc = docs.find((d: any) => d.name === 'clientSignature' || d.name?.toLowerCase().includes('signature')); // Multiple 'clientSignature' documents can exist for clients who re-cropped/replaced their
// signature before uploads became single-slot, so pick the highest id (latest) rather than
// the first unordered match.
const sigDoc = docs
.filter((d: any) => d.name === 'clientSignature' || d.name?.toLowerCase().includes('signature'))
.sort((a: any, b: any) => b.id - a.id)[0];
this.loading = false; this.loading = false;
if (sigDoc) { if (sigDoc) {
this.http this.http
@@ -1868,7 +1873,12 @@ export class TransactionEntryComponent implements OnInit, OnDestroy {
this.clientAddresses = Array.isArray(r.addresses) ? r.addresses : []; this.clientAddresses = Array.isArray(r.addresses) ? r.addresses : [];
this.clientPhotoUrl = r.photo ?? null; this.clientPhotoUrl = r.photo ?? null;
const docs: any[] = Array.isArray(r.documents) ? r.documents : []; const docs: any[] = Array.isArray(r.documents) ? r.documents : [];
const sigDoc = docs.find((d: any) => d.name === 'clientSignature' || d.name?.toLowerCase().includes('signature')); // Multiple 'clientSignature' documents can exist for clients who re-cropped/replaced their
// signature before uploads became single-slot, so pick the highest id (latest) rather than
// the first unordered match.
const sigDoc = docs
.filter((d: any) => d.name === 'clientSignature' || d.name?.toLowerCase().includes('signature'))
.sort((a: any, b: any) => b.id - a.id)[0];
if (sigDoc) { if (sigDoc) {
this.http this.http
.get(`/clients/${clientId}/documents/${sigDoc.id}/attachment`, { responseType: 'blob' }) .get(`/clients/${clientId}/documents/${sigDoc.id}/attachment`, { responseType: 'blob' })
@@ -446,8 +446,12 @@ export class RecordMasterComponent implements OnInit {
}; };
} }
// 2. Signature — client document named 'clientSignature' // 2. Signature — client document named 'clientSignature'. Multiple rows can exist for
const signatureDoc = (documents as any[])?.find((d: any) => d?.name === 'clientSignature'); // clients who re-cropped/replaced their signature before uploads became single-slot, so
// pick the highest id (latest) rather than the first unordered match.
const signatureDoc = (documents as any[])
?.filter((d: any) => d?.name === 'clientSignature')
?.sort((a: any, b: any) => b.id - a.id)?.[0];
if (signatureDoc) { if (signatureDoc) {
this.viewDetails.docs.signature = { this.viewDetails.docs.signature = {
present: true, present: true,
@@ -66,9 +66,12 @@ export class SignatoryService {
fetchClientSignature(clientId: number | string): Observable<string | null> { fetchClientSignature(clientId: number | string): Observable<string | null> {
return this.http.get<any[]>(`/clients/${clientId}/documents`).pipe( return this.http.get<any[]>(`/clients/${clientId}/documents`).pipe(
switchMap((docs: any[]) => { switchMap((docs: any[]) => {
const sigDoc = (docs || []).find( // Multiple 'clientSignature' documents can exist for clients who re-cropped/replaced
(d: any) => d.name === 'clientSignature' || d.name?.toLowerCase().includes('signature') // their signature before uploads became single-slot, so pick the highest id (latest)
); // rather than the first unordered match.
const sigDoc = (docs || [])
.filter((d: any) => d.name === 'clientSignature' || d.name?.toLowerCase().includes('signature'))
.sort((a: any, b: any) => b.id - a.id)[0];
if (!sigDoc) { if (!sigDoc) {
return of(null); return of(null);
} }
@@ -358,6 +358,9 @@
@if (isMinor) { @if (isMinor) {
<span class="minor-badge">Minor</span> <span class="minor-badge">Minor</span>
} }
@if (isSeniorCitizen) {
<span class="senior-citizen-badge">Senior Citizen</span>
}
</div> </div>
} }
</div> </div>
@@ -1217,7 +1220,38 @@
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline" class="form-field"> <mat-form-field appearance="outline" class="form-field">
<mat-label>Annual Turnover (Last FY)</mat-label> <mat-label>Annual Income</mat-label>
<span matPrefix class="rupee-prefix">&#8377;</span>
<input matInput type="number" formControlName="annualIncome" min="0" />
@if (
createClientForm.get('annualIncome')?.hasError('pattern') && createClientForm.get('annualIncome')?.touched
) {
<mat-error>Enter a valid amount</mat-error>
} @else if (
createClientForm.get('annualIncome')?.hasError('min') && createClientForm.get('annualIncome')?.touched
) {
<mat-error>Amount cannot be negative</mat-error>
}
</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> <span matPrefix class="rupee-prefix">&#8377;</span>
<input matInput type="number" formControlName="annualTurnover" min="0" /> <input matInput type="number" formControlName="annualTurnover" min="0" />
@if ( @if (
@@ -1232,6 +1266,11 @@
} }
</mat-form-field> </mat-form-field>
<div class="full-width subsection-title">
<fa-icon icon="coins" class="subsection-icon"></fa-icon>
Other Income
</div>
<mat-form-field appearance="outline" class="form-field"> <mat-form-field appearance="outline" class="form-field">
<mat-label>Agriculture Income</mat-label> <mat-label>Agriculture Income</mat-label>
<span matPrefix class="rupee-prefix">&#8377;</span> <span matPrefix class="rupee-prefix">&#8377;</span>
@@ -1265,6 +1304,76 @@
<mat-error>Amount cannot be negative</mat-error> <mat-error>Amount cannot be negative</mat-error>
} }
</mat-form-field> </mat-form-field>
<div class="full-width subsection-title">
<fa-icon icon="user-shield" class="subsection-icon"></fa-icon>
Senior Citizen / Person with Disability
</div>
@if (isSeniorCitizen) {
<div class="full-width senior-citizen-auto-note">
<fa-icon icon="check-circle"></fa-icon>
Senior Citizen (60+ years) — detected automatically from Date of Birth
</div>
<div class="full-width doc-upload-field">
<label class="doc-upload-label">
<input type="file" accept=".jpg,.jpeg,.png,.pdf" hidden (change)="onSeniorCitizenProofSelected($event)" />
<fa-icon icon="file-upload"></fa-icon>
@if (seniorCitizenProofFile) {
<span>{{ seniorCitizenProofFile.name }}</span>
} @else {
<span>Upload Senior Citizen Proof (Age / ID Proof) &mdash; optional</span>
}
</label>
@if (seniorCitizenProofFile) {
<button type="button" mat-icon-button (click)="removeSeniorCitizenProof()" matTooltip="Remove">
<mat-icon>close</mat-icon>
</button>
}
</div>
}
<div class="checkbox-field full-width">
<mat-checkbox formControlName="isPwd">Person with Disability (PWD)</mat-checkbox>
</div>
@if (createClientForm.get('isPwd')?.value) {
<mat-form-field appearance="outline" class="form-field">
<mat-label>Disability Type</mat-label>
<mat-select formControlName="pwdCategoryId">
@for (category of pwdCategoryOptions; track category.id) {
<mat-option [value]="category.id">{{ category.name }}</mat-option>
}
</mat-select>
@if (seniorPwdDocsTouched && !createClientForm.get('pwdCategoryId')?.value) {
<mat-error>Disability type is required</mat-error>
}
</mat-form-field>
<div class="full-width doc-upload-field">
<label
class="doc-upload-label"
[class.doc-upload-label--missing]="seniorPwdDocsTouched && !pwdCertificateFile"
>
<input type="file" accept=".jpg,.jpeg,.png,.pdf" hidden (change)="onPwdCertificateSelected($event)" />
<fa-icon icon="file-upload"></fa-icon>
@if (pwdCertificateFile) {
<span>{{ pwdCertificateFile.name }}</span>
} @else {
<span>Upload Disability Certificate <span class="doc-upload-req">*</span></span>
}
</label>
@if (pwdCertificateFile) {
<button type="button" mat-icon-button (click)="removePwdCertificate()" matTooltip="Remove">
<mat-icon>close</mat-icon>
</button>
}
@if (seniorPwdDocsTouched && !pwdCertificateFile) {
<span class="doc-upload-error">Required when PWD is checked</span>
}
</div>
}
</div> </div>
</div> </div>
} }
@@ -56,6 +56,78 @@ form {
grid-column: 1 / -1; grid-column: 1 / -1;
} }
.senior-citizen-auto-note {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: #1074b9;
background: #eff6ff;
border: 1px solid #bfdbfe;
border-radius: 8px;
padding: 8px 12px;
margin-bottom: 8px;
}
.doc-upload-field {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 8px;
}
.doc-upload-label {
display: flex;
align-items: center;
gap: 8px;
flex: 1;
padding: 10px 14px;
border: 1px dashed #cbd5e1;
border-radius: 8px;
font-size: 13px;
color: #475569;
cursor: pointer;
transition: border-color 0.15s;
&:hover {
border-color: #1074b9;
}
&.doc-upload-label--missing {
border-color: #dc2626;
color: #dc2626;
}
}
.doc-upload-req {
color: #dc2626;
font-weight: 700;
}
.doc-upload-error {
font-size: 11px;
color: #dc2626;
}
.subsection-title {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.04em;
color: #6b7280;
margin: 20px 0 4px;
padding-top: 16px;
border-top: 1px solid #e5e7eb;
.subsection-icon {
color: var(--md-sys-color-primary, #1074b9);
font-size: 12px;
}
}
.checkbox-field { .checkbox-field {
display: flex; display: flex;
align-items: center; align-items: center;
@@ -234,6 +306,18 @@ form {
letter-spacing: 0.5px; letter-spacing: 0.5px;
} }
.senior-citizen-badge {
font-size: 10px;
font-weight: 700;
padding: 2px 8px;
border-radius: 10px;
background: #eff6ff;
color: #1074b9;
margin-left: 6px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* ═══ Guardian Section (Redesigned) ═══ */ /* ═══ Guardian Section (Redesigned) ═══ */
.guardian-section { .guardian-section {
margin-top: 12px; margin-top: 12px;
@@ -93,6 +93,54 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
/** Expose enum to template */ /** Expose enum to template */
readonly LegalFormId = LegalFormId; 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. */ /** Minimum date allowed. */
minDate = new Date(2000, 0, 1); minDate = new Date(2000, 0, 1);
/** Maximum date allowed. */ /** Maximum date allowed. */
@@ -157,6 +205,8 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
calculatedAgeDetail: string | null = null; calculatedAgeDetail: string | null = null;
/** Whether client is a minor (age < 10) */ /** Whether client is a minor (age < 10) */
isMinor = false; isMinor = false;
/** Whether client is a senior citizen (age >= 60) — auto-detected from Date of Birth */
isSeniorCitizen = false;
/** Age breakdown for DB storage */ /** Age breakdown for DB storage */
ageBreakdown = { years: 0, months: 0, days: 0 }; ageBreakdown = { years: 0, months: 0, days: 0 };
@@ -525,7 +575,11 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
occupationCategoryId: data.occupationCategory?.id || data.occupationCategoryId || '', occupationCategoryId: data.occupationCategory?.id || data.occupationCategoryId || '',
occupationId: data.occupation?.id || data.occupationId || '', occupationId: data.occupation?.id || data.occupationId || '',
monthlyIncome: data.monthlyIncome || '', monthlyIncome: data.monthlyIncome || '',
annualIncome: data.annualIncome || '',
annualTurnover: data.annualTurnover || '', annualTurnover: data.annualTurnover || '',
monthlyTurnover: data.monthlyTurnover || '',
isPwd: data.isPwd || false,
pwdCategoryId: data.pwdCategory?.id || '',
agricultureIncome: data.agricultureIncome || '', agricultureIncome: data.agricultureIncome || '',
nonAgricultureIncome: data.nonAgricultureIncome || '', nonAgricultureIncome: data.nonAgricultureIncome || '',
submittedOnDate: data.timeline?.submittedOnDate submittedOnDate: data.timeline?.submittedOnDate
@@ -665,6 +719,13 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
Validators.min(0) Validators.min(0)
] ]
], ],
annualIncome: [
'',
[
Validators.pattern(/^\d+(\.\d{1,2})?$/),
Validators.min(0)
]
],
annualTurnover: [ annualTurnover: [
'', '',
[ [
@@ -672,6 +733,15 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
Validators.min(0) Validators.min(0)
] ]
], ],
monthlyTurnover: [
'',
[
Validators.pattern(/^\d+(\.\d{1,2})?$/),
Validators.min(0)
]
],
isPwd: [false],
pwdCategoryId: [''],
agricultureIncome: [ agricultureIncome: [
'', '',
[ [
@@ -709,6 +779,7 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
this.occupationCategoryOptions = this.clientTemplate.occupationCategoryOptions || []; this.occupationCategoryOptions = this.clientTemplate.occupationCategoryOptions || [];
this.occupationOptions = this.clientTemplate.occupationOptions || []; this.occupationOptions = this.clientTemplate.occupationOptions || [];
this.salutationOptions = this.clientTemplate.salutationOptions || []; 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 }; this.ageBreakdown = { years, months, days };
const wasMinor = this.isMinor; const wasMinor = this.isMinor;
this.isMinor = years < 10; this.isMinor = years < 10;
this.isSeniorCitizen = years >= 60;
// Add/remove guardian form group based on minor status // Add/remove guardian form group based on minor status
if (this.isMinor && !wasMinor) { if (this.isMinor && !wasMinor) {
this.createClientForm.addControl( this.createClientForm.addControl(
@@ -850,12 +922,14 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
} else { } else {
this.calculatedAgeDetail = null; this.calculatedAgeDetail = null;
this.isMinor = false; this.isMinor = false;
this.isSeniorCitizen = false;
this.ageBreakdown = { years: 0, months: 0, days: 0 }; this.ageBreakdown = { years: 0, months: 0, days: 0 };
this.createClientForm.removeControl('guardianDetails'); this.createClientForm.removeControl('guardianDetails');
} }
} else { } else {
this.calculatedAgeDetail = null; this.calculatedAgeDetail = null;
this.isMinor = false; this.isMinor = false;
this.isSeniorCitizen = false;
this.ageBreakdown = { years: 0, months: 0, days: 0 }; this.ageBreakdown = { years: 0, months: 0, days: 0 };
this.createClientForm.removeControl('guardianDetails'); 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('postgrad') || n === 'pg' || n.includes('master')) return 'postgraduate';
if (n.includes('undergrad') || n === 'ug' || n.includes('bachelor') || n.includes('graduate')) if (n.includes('undergrad') || n === 'ug' || n.includes('bachelor') || n.includes('graduate'))
return 'undergraduate'; 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 ( if (
n.includes('school') || n.includes('school') ||
n.includes('10th') || n.includes('10th') ||
@@ -1556,7 +1633,6 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
n.includes('hsc') || n.includes('hsc') ||
n.includes('matric') || n.includes('matric') ||
n.includes('intermediate') || n.includes('intermediate') ||
n.includes('diploma') ||
n.includes('iti') || n.includes('iti') ||
n.includes('below') n.includes('below')
) )
@@ -1634,8 +1710,12 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
} }
/** /**
* Degrees filtered by the currently selected Education Level. * Degrees filtered by the currently selected Education Level. Each DegreeQualification code
* Returns all degrees if no level is selected, or category can't be determined. * 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[] { get filteredDegreeQualificationOptions(): any[] {
const level = this.getSelectedEducationLevel(); const level = this.getSelectedEducationLevel();
@@ -1643,6 +1723,9 @@ export class ClientGeneralStepComponent implements OnInit, OnDestroy, OnChanges
const category = this.getEducationLevelCategory(level.name); const category = this.getEducationLevelCategory(level.name);
if (!category) return this.degreeQualificationOptions; if (!category) return this.degreeQualificationOptions;
const filtered = this.degreeQualificationOptions.filter((d: any) => { const filtered = this.degreeQualificationOptions.filter((d: any) => {
if (d.description) {
return this.getEducationLevelCategory(d.description) === category;
}
const c = this.getDegreeQualificationCategory(d.name); const c = this.getDegreeQualificationCategory(d.name);
return c === category || c === ''; 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) // Store extended fields separately (not yet supported by Fineract API)
// Add isMinor flag // Add isMinor flag
generalDetails.isMinor = this.isMinor; 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 // Add guardian clientId for minors
if (this.isMinor && generalDetails.guardianDetails) { if (this.isMinor && generalDetails.guardianDetails) {
@@ -845,7 +845,12 @@ export class ClientKycStepComponent implements OnInit, OnChanges, OnDestroy {
if (!this.clientId) return; if (!this.clientId) return;
this.clientsService.getClientDocuments(this.clientId).subscribe({ this.clientsService.getClientDocuments(this.clientId).subscribe({
next: (documents: any[]) => { next: (documents: any[]) => {
const sigDoc = documents?.find((d: any) => d.name === 'clientSignature'); // Multiple 'clientSignature' documents can exist for clients who re-cropped/replaced
// their signature before uploads became single-slot, so pick the highest id (latest)
// rather than the first unordered match.
const sigDoc = documents
?.filter((d: any) => d.name === 'clientSignature')
?.sort((a: any, b: any) => b.id - a.id)?.[0];
if (sigDoc?.id) { if (sigDoc?.id) {
this.existingSignatureDocId = sigDoc.id.toString(); this.existingSignatureDocId = sigDoc.id.toString();
this.clientsService.getClientSignatureImage(this.clientId, this.existingSignatureDocId).subscribe({ this.clientsService.getClientSignatureImage(this.clientId, this.existingSignatureDocId).subscribe({
@@ -234,8 +234,12 @@ export class CkycTabComponent implements OnInit, OnDestroy {
}; };
} }
// 2. Signature — client document with name='clientSignature' // 2. Signature — client document with name='clientSignature'. Multiple rows can exist
const signatureDoc = (documents as any[])?.find((d: any) => d?.name === 'clientSignature'); // for clients who re-cropped/replaced their signature before uploads became single-slot,
// so pick the highest id (latest) rather than the first unordered match.
const signatureDoc = (documents as any[])
?.filter((d: any) => d?.name === 'clientSignature')
?.sort((a: any, b: any) => b.id - a.id)?.[0];
if (signatureDoc) { if (signatureDoc) {
this.existing.CLIENT_SIGNATURE = { this.existing.CLIENT_SIGNATURE = {
present: true, present: true,
@@ -47,7 +47,13 @@ export class DeleteSignatureDialogComponent {
* @param {any} data Documents data * @param {any} data Documents data
*/ */
constructor() { constructor() {
const signature = this.data.find((document: any) => document.name === 'clientSignature') || {}; // Multiple 'clientSignature' documents can exist for clients who re-cropped/replaced their
// signature before uploads became single-slot, so pick the highest id (latest) rather than
// the first unordered match.
const signature =
this.data
.filter((document: any) => document.name === 'clientSignature')
.sort((a: any, b: any) => b.id - a.id)[0] || {};
this.signatureId = signature.id; this.signatureId = signature.id;
} }
} }
@@ -65,7 +65,13 @@ export class ViewSignatureDialogComponent implements OnInit {
* @param {any} data Documents data * @param {any} data Documents data
*/ */
constructor() { constructor() {
const signature = this.data.documents.find((document: any) => document.name === 'clientSignature') || {}; // Multiple 'clientSignature' documents can exist for clients who re-cropped/replaced their
// signature before uploads became single-slot, so pick the highest id (latest) rather than
// the first unordered match.
const signature =
this.data.documents
.filter((document: any) => document.name === 'clientSignature')
.sort((a: any, b: any) => b.id - a.id)[0] || {};
this.signatureId = signature.id; this.signatureId = signature.id;
this.clientId = this.data.id; this.clientId = this.data.id;
} }
@@ -331,6 +331,18 @@ export class CreateClientComponent {
return; return;
} }
// PWD gate — disability type + certificate are mandatory when PWD is checked
// (Senior Citizen proof upload is optional, not gated here)
if (!this.clientGeneralStep?.seniorPwdDocsComplete) {
this.clientGeneralStep?.markSeniorPwdDocsTouched();
this.snackBar.open(
'Select the disability type and upload the Disability Certificate before continuing.',
'Close',
{ duration: 5000 }
);
return;
}
// Sanctions verification gate — Verify Client must be clicked before save // Sanctions verification gate — Verify Client must be clicked before save
if (!this.clientGeneralStep?.sanctionsVerified) { if (!this.clientGeneralStep?.sanctionsVerified) {
this.dialog this.dialog
@@ -469,6 +481,29 @@ export class CreateClientComponent {
clearInterval(progressInterval); clearInterval(progressInterval);
this.saveProgress = 100; this.saveProgress = 100;
this.createdClientId = response.resourceId; this.createdClientId = response.resourceId;
// Senior Citizen / PWD supporting documents — uploaded after creation, same pattern as
// the KYC step's photo/signature uploads (the generic /clients/{id}/documents endpoint
// needs a real clientId, which only exists once createClient() has returned).
if (this.clientGeneralStep?.seniorCitizenProofFile) {
const seniorCitizenProofForm = new FormData();
seniorCitizenProofForm.append('file', this.clientGeneralStep.seniorCitizenProofFile);
seniorCitizenProofForm.append('name', 'seniorCitizenProof');
seniorCitizenProofForm.append('description', 'Senior Citizen Proof (Age/ID Proof)');
this.clientsService
.uploadClientDocument(this.createdClientId, seniorCitizenProofForm)
.subscribe({ error: () => {} });
}
if (this.clientGeneralStep?.pwdCertificateFile) {
const pwdCertificateForm = new FormData();
pwdCertificateForm.append('file', this.clientGeneralStep.pwdCertificateFile);
pwdCertificateForm.append('name', 'pwdCertificate');
pwdCertificateForm.append('description', 'Disability Certificate');
this.clientsService
.uploadClientDocument(this.createdClientId, pwdCertificateForm)
.subscribe({ error: () => {} });
}
// Brief delay to show 100% before transitioning // Brief delay to show 100% before transitioning
setTimeout(() => { setTimeout(() => {
this.savingClient = false; this.savingClient = false;