Client-stepper: pre-fill existing KYC/address proofs in edit, fix identifier saves
Edit-mode now shows the previously selected KYC document type + uploaded file thumbnail
(loads from /clients/{id}/identifiers + /client_identifiers/{id}/documents). Same for
address proof on the India address step. Closes the gap where editing a client showed
an empty file picker even when proof had been uploaded on create.
Identifier-save hardened across all three callsites (create-client, edit-client,
identities-tab.createIdentifierWithDocs) - the KYC chip picker proofId is a CODE
string ('AADHAAR'), but backend ClientIdentifierRequest.documentTypeId is Long, which
Spring rejects with HttpMessageNotReadableException -> 400. resolveDocumentTypeId
helper looks up the code against /kyc/proof-types?type=id and substitutes the numeric
CodeValue id before posting. Skips silently for constitution-specific non-individual
docs that have no CodeValue mapping.
KYC payload normalises panNumber / enterprisePan / gstinNumber / cinNumber via
toUpperCase + whitespace strip so user-typed lowercase or trailing spaces don't trip
the backend regex (validation.msg.CLIENTKYC.panNumber.does.not.match.regexp).
State name match in client-address save now normalises both sides (lowercase, collapse
underscores + spaces) so legacy MADHYA_PRADESH resolves to 'Madhya Pradesh' returned
by api.postalpincode.in. Backed by tenant migration 0297.
Identities-tab resolver flattened - was returning identifiers before the per-row
forkJoin(getClientIdentificationDocuments) completed, so the view rendered empty
documents arrays. Now uses switchMap + forkJoin so the resolver awaits completion.
Also bundles concurrent work-in-progress on letter-mgmt, organization, users,
bank-calendar, shell, nominee/notes/identifier dialogs, and theming.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
325228c514
commit
83ea57ccf0
+10
-2
@@ -20,6 +20,7 @@ import {
|
||||
CertificatePreviewDialogData
|
||||
} from '../certificate-preview-dialog/certificate-preview-dialog.component';
|
||||
import { downloadBalanceCertificatePdf } from '../certificate-pdf';
|
||||
import { DscSignService } from '../../dsc-sign-dialog/dsc-sign.service';
|
||||
|
||||
export type BalanceAccountType = 'SAVINGS' | 'CURRENT' | 'FIXED_DEPOSIT' | 'RD' | 'NRE' | 'NRO';
|
||||
|
||||
@@ -60,6 +61,7 @@ export interface BalanceCertificateRecord {
|
||||
export class BalanceCertificateComponent implements OnInit {
|
||||
private dialog = inject(MatDialog);
|
||||
private snack = inject(MatSnackBar);
|
||||
private dscSign = inject(DscSignService);
|
||||
|
||||
// ── Filters ──
|
||||
search = '';
|
||||
@@ -126,8 +128,14 @@ export class BalanceCertificateComponent implements OnInit {
|
||||
}
|
||||
|
||||
downloadCertificate(r: BalanceCertificateRecord): void {
|
||||
downloadBalanceCertificatePdf(r, this.asOnDate);
|
||||
this.snack.open(`Balance Certificate (${r.accountNo}) downloaded.`, 'OK', { duration: 2400 });
|
||||
this.dscSign.promptThenDownload(
|
||||
{
|
||||
title: 'Balance Certificate',
|
||||
reference: `${r.accountNo} · ${r.customerName} · as on ${this.asOnDate}`,
|
||||
filename: `Balance-Certificate-${r.accountNo}-${this.asOnDate}.pdf`
|
||||
},
|
||||
(dsc) => downloadBalanceCertificatePdf(r, this.asOnDate, dsc)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
|
||||
import { jsPDF } from 'jspdf';
|
||||
|
||||
import { BankUserDsc } from 'app/users/dsc/dsc.model';
|
||||
|
||||
import { FyInterestBreakup, LoanInterestRecord } from './loan-interest/loan-interest.component';
|
||||
import { ShareCertificateRecord } from './share-certificate/share-certificate.component';
|
||||
import { BalanceCertificateRecord } from './balance-certificate/balance-certificate.component';
|
||||
@@ -394,6 +396,73 @@ function newCtx(accent: [number, number, number]): PdfCtx {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Paint a "Digitally Signed" stamp + auditor footer line on the final page of
|
||||
* the given doc. Stamp = green-bordered rounded box on the bottom-right with
|
||||
* a ✓ icon, the signer's common name, masked DSC serial, and signing time.
|
||||
* Audit line = thin teal strip under the existing footer with the same info
|
||||
* in monospace, so the certificate also reads cleanly when printed in B/W.
|
||||
*
|
||||
* Call this just before `doc.save(...)` — it doesn't add a new page.
|
||||
*/
|
||||
export function paintDscSignatureStamp(doc: jsPDF, dsc: BankUserDsc): void {
|
||||
const pageW = doc.internal.pageSize.getWidth();
|
||||
const pageH = doc.internal.pageSize.getHeight();
|
||||
const margin = 20;
|
||||
const signedAt = new Date();
|
||||
const pad = (n: number) => String(n).padStart(2, '0');
|
||||
const ts = `${signedAt.getFullYear()}-${pad(signedAt.getMonth() + 1)}-${pad(signedAt.getDate())} ${pad(signedAt.getHours())}:${pad(signedAt.getMinutes())}:${pad(signedAt.getSeconds())}`;
|
||||
|
||||
// ── Bottom-right rounded green stamp ──
|
||||
const sW = 68;
|
||||
const sH = 22;
|
||||
const sX = pageW - margin - sW;
|
||||
const sY = pageH - margin - sH - 4;
|
||||
|
||||
// Tint background + green border (slight rounding gives it a "stamped" feel)
|
||||
doc.setFillColor(220, 252, 231); // green-100
|
||||
doc.setDrawColor(21, 128, 61); // green-700
|
||||
doc.setLineWidth(0.6);
|
||||
doc.roundedRect(sX, sY, sW, sH, 2, 2, 'FD');
|
||||
|
||||
// ✓ icon + heading
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setFontSize(9);
|
||||
doc.setTextColor(21, 128, 61);
|
||||
doc.text('✓ DIGITALLY SIGNED', sX + 3, sY + 5);
|
||||
|
||||
// Signer name (Common Name)
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setFontSize(8.5);
|
||||
doc.setTextColor(15, 23, 42);
|
||||
doc.text(dsc.commonName, sX + 3, sY + 10);
|
||||
|
||||
// DSC serial (masked) + type
|
||||
doc.setFont('courier', 'normal');
|
||||
doc.setFontSize(7);
|
||||
doc.setTextColor(71, 85, 105);
|
||||
doc.text(`DSC ${dsc.dscSerialNoMasked}`, sX + 3, sY + 14);
|
||||
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setFontSize(6.5);
|
||||
doc.setTextColor(100, 116, 139);
|
||||
doc.text(dsc.dscType, sX + 3, sY + 17.5);
|
||||
doc.text(`Signed at ${ts}`, sX + 3, sY + 20.5);
|
||||
|
||||
// ── Audit footer line (thin teal strip just below the existing footer) ──
|
||||
const fY = pageH - margin + 2;
|
||||
doc.setFillColor(204, 251, 241); // teal-100
|
||||
doc.rect(margin, fY, pageW - margin * 2, 4, 'F');
|
||||
doc.setFont('courier', 'normal');
|
||||
doc.setFontSize(7);
|
||||
doc.setTextColor(13, 148, 136); // teal-600
|
||||
doc.text(
|
||||
`[SIGNED] ${dsc.commonName} · DSC ${dsc.dscSerialNoMasked} · Issued by ${dsc.issuedBy.slice(0, 38)} · ${ts}`,
|
||||
margin + 2,
|
||||
fY + 2.8
|
||||
);
|
||||
}
|
||||
|
||||
// ───────────────────── Loan Interest Certificate ─────────────────────
|
||||
export function buildLoanInterestPdf(record: LoanInterestRecord, fy: FyInterestBreakup): jsPDF {
|
||||
const c = newCtx(ACCENT_BLUE);
|
||||
@@ -467,8 +536,9 @@ export function buildLoanInterestPdf(record: LoanInterestRecord, fy: FyInterestB
|
||||
return c.doc;
|
||||
}
|
||||
|
||||
export function downloadLoanInterestPdf(record: LoanInterestRecord, fy: FyInterestBreakup): void {
|
||||
export function downloadLoanInterestPdf(record: LoanInterestRecord, fy: FyInterestBreakup, dsc?: BankUserDsc): void {
|
||||
const doc = buildLoanInterestPdf(record, fy);
|
||||
if (dsc) paintDscSignatureStamp(doc, dsc);
|
||||
doc.save(`Interest-Certificate-${record.accountNo}-${fy.fy.replace(/\s+/g, '')}.pdf`);
|
||||
}
|
||||
|
||||
@@ -552,8 +622,9 @@ export function buildShareCertificatePdf(r: ShareCertificateRecord): jsPDF {
|
||||
return c.doc;
|
||||
}
|
||||
|
||||
export function downloadShareCertificatePdf(r: ShareCertificateRecord): void {
|
||||
export function downloadShareCertificatePdf(r: ShareCertificateRecord, dsc?: BankUserDsc): void {
|
||||
const doc = buildShareCertificatePdf(r);
|
||||
if (dsc) paintDscSignatureStamp(doc, dsc);
|
||||
doc.save(`Share-Certificate-${r.certificateNo}.pdf`);
|
||||
}
|
||||
|
||||
@@ -645,8 +716,9 @@ export function buildBalanceCertificatePdf(r: BalanceCertificateRecord, asOnDate
|
||||
return c.doc;
|
||||
}
|
||||
|
||||
export function downloadBalanceCertificatePdf(r: BalanceCertificateRecord, asOnDate: string): void {
|
||||
export function downloadBalanceCertificatePdf(r: BalanceCertificateRecord, asOnDate: string, dsc?: BankUserDsc): void {
|
||||
const doc = buildBalanceCertificatePdf(r, asOnDate);
|
||||
if (dsc) paintDscSignatureStamp(doc, dsc);
|
||||
doc.save(`Balance-Certificate-${r.accountNo}-${asOnDate}.pdf`);
|
||||
}
|
||||
|
||||
@@ -736,8 +808,9 @@ export function buildForm60Pdf(r: Form60Record): jsPDF {
|
||||
return c.doc;
|
||||
}
|
||||
|
||||
export function downloadForm60Pdf(r: Form60Record): void {
|
||||
export function downloadForm60Pdf(r: Form60Record, dsc?: BankUserDsc): void {
|
||||
const doc = buildForm60Pdf(r);
|
||||
if (dsc) paintDscSignatureStamp(doc, dsc);
|
||||
doc.save(`Form-60-${r.customerId}.pdf`);
|
||||
}
|
||||
|
||||
@@ -830,8 +903,9 @@ export function buildForm15ghPdf(r: Form15ghRecord, kind: Form15Kind): jsPDF {
|
||||
return c.doc;
|
||||
}
|
||||
|
||||
export function downloadForm15ghPdf(r: Form15ghRecord, kind: Form15Kind): void {
|
||||
export function downloadForm15ghPdf(r: Form15ghRecord, kind: Form15Kind, dsc?: BankUserDsc): void {
|
||||
const doc = buildForm15ghPdf(r, kind);
|
||||
if (dsc) paintDscSignatureStamp(doc, dsc);
|
||||
doc.save(`Form-${kind}-${r.customerId}-${r.fy.replace(/\s+/g, '')}.pdf`);
|
||||
}
|
||||
|
||||
@@ -922,8 +996,9 @@ export function buildHypothecationPdf(r: HypothecationRecord): jsPDF {
|
||||
return c.doc;
|
||||
}
|
||||
|
||||
export function downloadHypothecationPdf(r: HypothecationRecord): void {
|
||||
export function downloadHypothecationPdf(r: HypothecationRecord, dsc?: BankUserDsc): void {
|
||||
const doc = buildHypothecationPdf(r);
|
||||
if (dsc) paintDscSignatureStamp(doc, dsc);
|
||||
doc.save(`Hypothecation-${r.certificateNo}.pdf`);
|
||||
}
|
||||
|
||||
@@ -1052,7 +1127,8 @@ export function buildNocPdf(r: NocRecord): jsPDF {
|
||||
return c.doc;
|
||||
}
|
||||
|
||||
export function downloadNocPdf(r: NocRecord): void {
|
||||
export function downloadNocPdf(r: NocRecord, dsc?: BankUserDsc): void {
|
||||
const doc = buildNocPdf(r);
|
||||
if (dsc) paintDscSignatureStamp(doc, dsc);
|
||||
doc.save(`NOC-${r.nocNo}.pdf`);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
CertificatePreviewDialogData
|
||||
} from '../certificate-preview-dialog/certificate-preview-dialog.component';
|
||||
import { downloadForm15ghPdf } from '../certificate-pdf';
|
||||
import { DscSignService } from '../../dsc-sign-dialog/dsc-sign.service';
|
||||
|
||||
export type Form15Kind = '15G' | '15H';
|
||||
export type Form15Status = 'FILED' | 'PENDING';
|
||||
@@ -60,6 +61,7 @@ export interface Form15ghRecord {
|
||||
export class Form15ghComponent implements OnInit {
|
||||
private dialog = inject(MatDialog);
|
||||
private snack = inject(MatSnackBar);
|
||||
private dscSign = inject(DscSignService);
|
||||
|
||||
// ── Filters ──
|
||||
search = '';
|
||||
@@ -141,8 +143,15 @@ export class Form15ghComponent implements OnInit {
|
||||
}
|
||||
|
||||
downloadCertificate(r: Form15ghRecord): void {
|
||||
downloadForm15ghPdf(r, this.formKind(r));
|
||||
this.snack.open(`Form ${this.formKind(r)} for ${r.customerName} downloaded.`, 'OK', { duration: 2400 });
|
||||
const kind = this.formKind(r);
|
||||
this.dscSign.promptThenDownload(
|
||||
{
|
||||
title: `Form ${kind} Declaration`,
|
||||
reference: `${r.customerId} · ${r.customerName} · ${r.fy}`,
|
||||
filename: `Form-${kind}-${r.customerId}-${r.fy.replace(/\s+/g, '')}.pdf`
|
||||
},
|
||||
(dsc) => downloadForm15ghPdf(r, kind, dsc)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
CertificatePreviewDialogData
|
||||
} from '../certificate-preview-dialog/certificate-preview-dialog.component';
|
||||
import { downloadForm60Pdf } from '../certificate-pdf';
|
||||
import { DscSignService } from '../../dsc-sign-dialog/dsc-sign.service';
|
||||
|
||||
export type Form60IdProofType = 'AADHAAR' | 'VOTER_ID' | 'DRIVING_LICENSE' | 'PASSPORT' | 'RATION';
|
||||
|
||||
@@ -59,6 +60,7 @@ export interface Form60Record {
|
||||
export class Form60Component implements OnInit {
|
||||
private dialog = inject(MatDialog);
|
||||
private snack = inject(MatSnackBar);
|
||||
private dscSign = inject(DscSignService);
|
||||
|
||||
// ── Filters ──
|
||||
search = '';
|
||||
@@ -142,8 +144,14 @@ export class Form60Component implements OnInit {
|
||||
});
|
||||
return;
|
||||
}
|
||||
downloadForm60Pdf(r);
|
||||
this.snack.open(`Form 60 for ${r.customerName} downloaded.`, 'OK', { duration: 2400 });
|
||||
this.dscSign.promptThenDownload(
|
||||
{
|
||||
title: 'Form 60 Declaration',
|
||||
reference: `${r.customerId} · ${r.customerName}`,
|
||||
filename: `Form60-${r.customerId}.pdf`
|
||||
},
|
||||
(dsc) => downloadForm60Pdf(r, dsc)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
CertificatePreviewDialogData
|
||||
} from '../certificate-preview-dialog/certificate-preview-dialog.component';
|
||||
import { downloadHypothecationPdf } from '../certificate-pdf';
|
||||
import { DscSignService } from '../../dsc-sign-dialog/dsc-sign.service';
|
||||
|
||||
export type HypoAssetType = 'VEHICLE' | 'PLANT_MACHINERY' | 'STOCK_DEBTORS' | 'GOLD' | 'EQUIPMENT';
|
||||
export type HypoStatus = 'ACTIVE' | 'CLOSED' | 'NOC_ISSUED';
|
||||
@@ -63,6 +64,7 @@ export interface HypothecationRecord {
|
||||
export class HypothecationCertificateComponent implements OnInit {
|
||||
private dialog = inject(MatDialog);
|
||||
private snack = inject(MatSnackBar);
|
||||
private dscSign = inject(DscSignService);
|
||||
|
||||
search = '';
|
||||
assetFilter: '' | HypoAssetType = '';
|
||||
@@ -133,10 +135,14 @@ export class HypothecationCertificateComponent implements OnInit {
|
||||
}
|
||||
|
||||
downloadCertificate(r: HypothecationRecord): void {
|
||||
downloadHypothecationPdf(r);
|
||||
this.snack.open(`Hypothecation Certificate ${r.certificateNo} downloaded.`, 'OK', {
|
||||
duration: 2400
|
||||
});
|
||||
this.dscSign.promptThenDownload(
|
||||
{
|
||||
title: 'Hypothecation Certificate',
|
||||
reference: `${r.certificateNo} · ${r.accountNo} · ${r.customerName}`,
|
||||
filename: `Hypothecation-Certificate-${r.certificateNo}.pdf`
|
||||
},
|
||||
(dsc) => downloadHypothecationPdf(r, dsc)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
CertificatePreviewDialogData
|
||||
} from '../certificate-preview-dialog/certificate-preview-dialog.component';
|
||||
import { downloadLoanInterestPdf } from '../certificate-pdf';
|
||||
import { DscSignService } from '../../dsc-sign-dialog/dsc-sign.service';
|
||||
|
||||
export type LoanProductType = 'HOUSING' | 'PERSONAL' | 'AUTO' | 'EDUCATION' | 'GOLD' | 'BUSINESS';
|
||||
|
||||
@@ -62,6 +63,7 @@ export interface LoanInterestRecord {
|
||||
export class LoanInterestCertificateComponent implements OnInit {
|
||||
private dialog = inject(MatDialog);
|
||||
private snack = inject(MatSnackBar);
|
||||
private dscSign = inject(DscSignService);
|
||||
|
||||
// ── Filters ──
|
||||
search = '';
|
||||
@@ -161,8 +163,14 @@ export class LoanInterestCertificateComponent implements OnInit {
|
||||
this.snack.open('No interest data on file for the selected financial year.', 'OK', { duration: 2500 });
|
||||
return;
|
||||
}
|
||||
downloadLoanInterestPdf(l, fy);
|
||||
this.snack.open(`Interest Certificate (${fy.fy}) downloaded.`, 'OK', { duration: 2400 });
|
||||
this.dscSign.promptThenDownload(
|
||||
{
|
||||
title: 'Interest Certificate',
|
||||
reference: `${l.accountNo} · ${l.customerName} · ${fy.fy}`,
|
||||
filename: `Interest-Certificate-${l.accountNo}-${fy.fy.replace(/\s+/g, '')}.pdf`
|
||||
},
|
||||
(dsc) => downloadLoanInterestPdf(l, fy, dsc)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
CertificatePreviewDialogData
|
||||
} from '../certificate-preview-dialog/certificate-preview-dialog.component';
|
||||
import { downloadNocPdf } from '../certificate-pdf';
|
||||
import { DscSignService } from '../../dsc-sign-dialog/dsc-sign.service';
|
||||
|
||||
export type NocLoanType = 'AUTO' | 'HOUSING' | 'PERSONAL' | 'EDUCATION' | 'GOLD' | 'BUSINESS' | 'CC' | 'OD';
|
||||
|
||||
@@ -67,6 +68,7 @@ export interface NocRecord {
|
||||
export class NocCertificateComponent implements OnInit {
|
||||
private dialog = inject(MatDialog);
|
||||
private snack = inject(MatSnackBar);
|
||||
private dscSign = inject(DscSignService);
|
||||
|
||||
// ── Filters ──
|
||||
search = '';
|
||||
@@ -158,8 +160,14 @@ export class NocCertificateComponent implements OnInit {
|
||||
);
|
||||
return;
|
||||
}
|
||||
downloadNocPdf(r);
|
||||
this.snack.open(`NOC ${r.nocNo} downloaded.`, 'OK', { duration: 2400 });
|
||||
this.dscSign.promptThenDownload(
|
||||
{
|
||||
title: 'No-Objection Certificate (NOC)',
|
||||
reference: `${r.nocNo} · ${r.accountNo} · ${r.customerName}`,
|
||||
filename: `NOC-${r.nocNo}.pdf`
|
||||
},
|
||||
(dsc) => downloadNocPdf(r, dsc)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
CertificatePreviewDialogData
|
||||
} from '../certificate-preview-dialog/certificate-preview-dialog.component';
|
||||
import { downloadShareCertificatePdf } from '../certificate-pdf';
|
||||
import { DscSignService } from '../../dsc-sign-dialog/dsc-sign.service';
|
||||
|
||||
export type ShareClass = 'ORDINARY' | 'PREFERENCE';
|
||||
|
||||
@@ -58,6 +59,7 @@ export interface ShareCertificateRecord {
|
||||
export class ShareCertificateComponent implements OnInit {
|
||||
private dialog = inject(MatDialog);
|
||||
private snack = inject(MatSnackBar);
|
||||
private dscSign = inject(DscSignService);
|
||||
|
||||
// ── Filters ──
|
||||
search = '';
|
||||
@@ -132,8 +134,14 @@ export class ShareCertificateComponent implements OnInit {
|
||||
}
|
||||
|
||||
downloadCertificate(r: ShareCertificateRecord): void {
|
||||
downloadShareCertificatePdf(r);
|
||||
this.snack.open(`Share Certificate ${r.certificateNo} downloaded.`, 'OK', { duration: 2400 });
|
||||
this.dscSign.promptThenDownload(
|
||||
{
|
||||
title: 'Share Certificate',
|
||||
reference: `${r.certificateNo} · Folio ${r.folioNo} · ${r.memberName}`,
|
||||
filename: `Share-Certificate-${r.certificateNo}.pdf`
|
||||
},
|
||||
(dsc) => downloadShareCertificatePdf(r, dsc)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user