feat: add database export, welcome kit dialog, and DIP/sanctions updates
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
+422
-32
@@ -12,12 +12,14 @@ import {
|
||||
MatDialogRef,
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogClose
|
||||
MatDialogActions
|
||||
} from '@angular/material/dialog';
|
||||
import { MatSnackBar } from '@angular/material/snack-bar';
|
||||
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { ClientsService } from '../../../clients.service';
|
||||
import { SettingsService } from 'app/settings/settings.service';
|
||||
import { Dates } from 'app/core/utils/dates';
|
||||
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
|
||||
import { DatePipe } from '@angular/common';
|
||||
import { jsPDF } from 'jspdf';
|
||||
@@ -32,32 +34,79 @@ import autoTable from 'jspdf-autotable';
|
||||
MatDialogTitle,
|
||||
MatDialogContent,
|
||||
MatDialogActions,
|
||||
MatDialogClose,
|
||||
MatProgressSpinnerModule
|
||||
MatProgressSpinnerModule,
|
||||
MatIcon
|
||||
],
|
||||
providers: [DatePipe]
|
||||
})
|
||||
export class WelcomeKitDialogComponent implements OnInit {
|
||||
private dialogRef = inject(MatDialogRef<WelcomeKitDialogComponent>);
|
||||
data: { clientId: string } = inject(MAT_DIALOG_DATA);
|
||||
data: {
|
||||
clientId: string;
|
||||
clientName?: string;
|
||||
accountNo?: string;
|
||||
cifNumber?: string;
|
||||
mobileNo?: string;
|
||||
emailAddress?: string;
|
||||
officeName?: string;
|
||||
} = inject(MAT_DIALOG_DATA);
|
||||
private clientsService = inject(ClientsService);
|
||||
private formBuilder = inject(FormBuilder);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private datePipe = inject(DatePipe);
|
||||
private settingsService = inject(SettingsService);
|
||||
private dateUtils = inject(Dates);
|
||||
|
||||
isLoading = true;
|
||||
/** Set once a dispatch or an outcome is saved, so the client view knows to reload on close. */
|
||||
private hasChanges = false;
|
||||
welcomeKitsData: any = null;
|
||||
/** Addresses captured for the client, each formatted for display and selection. */
|
||||
addressChoices: { id: any; type: string; isActive: boolean; lines: string[]; oneLine: string }[] = [];
|
||||
/** True once the address lookup has run, so the empty state only shows when there genuinely are none. */
|
||||
addressesLoaded = false;
|
||||
/** Dispatch whose outcome is being recorded, or null while the tracking list is shown. */
|
||||
outcomeFor: any = null;
|
||||
outcomeStatus: 'DELIVERED' | 'RETURNED' = 'DELIVERED';
|
||||
|
||||
maxDate = new Date();
|
||||
|
||||
dispatchForm: FormGroup = this.formBuilder.group({
|
||||
deliveryAddress: [
|
||||
'',
|
||||
// The kit goes to one of the addresses captured at client creation, never to a hand-typed one, so the
|
||||
// dispatch record always traces back to an address the client actually gave.
|
||||
addressId: [
|
||||
null,
|
||||
Validators.required
|
||||
],
|
||||
deliveryMode: [
|
||||
'POSTMAN',
|
||||
Validators.required
|
||||
],
|
||||
deliveryModeOtherText: ['']
|
||||
deliveryModeOtherText: [''],
|
||||
trackingId: [
|
||||
'',
|
||||
[
|
||||
Validators.required,
|
||||
Validators.maxLength(100)
|
||||
]
|
||||
],
|
||||
dispatchedOnDate: [
|
||||
new Date(),
|
||||
Validators.required
|
||||
]
|
||||
});
|
||||
|
||||
outcomeForm: FormGroup = this.formBuilder.group({
|
||||
outcomeDate: [
|
||||
new Date(),
|
||||
Validators.required
|
||||
],
|
||||
remarks: [
|
||||
'',
|
||||
Validators.maxLength(500)
|
||||
]
|
||||
});
|
||||
|
||||
deliveryModes = [
|
||||
'POSTMAN',
|
||||
'COURIER',
|
||||
@@ -76,7 +125,37 @@ export class WelcomeKitDialogComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
get clientName(): string {
|
||||
return this.welcomeKitsData?.clientName || this.data.clientName || '';
|
||||
}
|
||||
|
||||
/** The kit is a physical address check, so the outcome is phrased as verification rather than delivery. */
|
||||
verificationLabel(status: string): string {
|
||||
switch (status) {
|
||||
case 'DELIVERED':
|
||||
return 'Verified';
|
||||
case 'RETURNED':
|
||||
return 'Not Verified';
|
||||
default:
|
||||
return 'Awaiting Verification';
|
||||
}
|
||||
}
|
||||
|
||||
verificationHint(dispatch: any): string {
|
||||
switch (dispatch?.status) {
|
||||
case 'DELIVERED':
|
||||
return 'Kit delivered, so the address stands verified.';
|
||||
case 'RETURNED':
|
||||
return 'Kit came back undelivered, so the address could not be verified.';
|
||||
default:
|
||||
return 'Check this tracking ID on the delivery partner’s website or app, then record the outcome below.';
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.maxDate = this.settingsService.businessDate;
|
||||
this.dispatchForm.patchValue({ dispatchedOnDate: this.maxDate });
|
||||
this.outcomeForm.patchValue({ outcomeDate: this.maxDate });
|
||||
this.fetchData();
|
||||
}
|
||||
|
||||
@@ -98,33 +177,77 @@ export class WelcomeKitDialogComponent implements OnInit {
|
||||
});
|
||||
}
|
||||
|
||||
/** Order the address types are offered in: a kit is posted to the correspondence address when one exists. */
|
||||
private static readonly ADDRESS_TYPE_ORDER = [
|
||||
'Correspondence',
|
||||
'Current / Residential',
|
||||
'Permanent Address',
|
||||
'Office / Work'
|
||||
];
|
||||
|
||||
fetchPrimaryAddress() {
|
||||
this.clientsService.getClientAddressData(this.data.clientId).subscribe({
|
||||
next: (addresses: any) => {
|
||||
this.isLoading = false;
|
||||
if (addresses && addresses.length > 0) {
|
||||
// Attempt to build a readable address from the first address returned
|
||||
const primary = addresses[0];
|
||||
const parts = [];
|
||||
if (primary.addressLine1) parts.push(primary.addressLine1);
|
||||
if (primary.addressLine2) parts.push(primary.addressLine2);
|
||||
if (primary.city) parts.push(primary.city);
|
||||
if (primary.stateName) parts.push(primary.stateName);
|
||||
if (primary.postalCode) parts.push(primary.postalCode);
|
||||
if (primary.countryName) parts.push(primary.countryName);
|
||||
this.addressesLoaded = true;
|
||||
|
||||
const clientNameStr = this.welcomeKitsData?.clientName ? this.welcomeKitsData.clientName + ' — ' : '';
|
||||
this.dispatchForm.patchValue({
|
||||
deliveryAddress: clientNameStr + parts.join(', ')
|
||||
const usable = (addresses || []).filter((a: any) => a && (a.addressLine1 || a.postalCode || a.city));
|
||||
this.addressChoices = usable
|
||||
.map((a: any) => {
|
||||
const lines = [
|
||||
a.addressLine1,
|
||||
a.addressLine2,
|
||||
a.addressLine3,
|
||||
a.street,
|
||||
a.townVillage,
|
||||
a.city,
|
||||
a.countyDistrict,
|
||||
a.stateName || a.stateProvinceName,
|
||||
a.postalCode,
|
||||
a.countryName
|
||||
]
|
||||
.map((part: any) => (part == null ? '' : String(part).trim()))
|
||||
.filter((part: string) => part.length > 0);
|
||||
|
||||
return {
|
||||
id: a.addressId ?? a.id,
|
||||
type: a.addressType || 'Address',
|
||||
isActive: a.isActive !== false,
|
||||
lines,
|
||||
oneLine: lines.join(', ')
|
||||
};
|
||||
})
|
||||
.sort((x: any, y: any) => {
|
||||
// Active addresses first, then by how suitable the type is for posting.
|
||||
if (x.isActive !== y.isActive) return x.isActive ? -1 : 1;
|
||||
const order = WelcomeKitDialogComponent.ADDRESS_TYPE_ORDER;
|
||||
const xi = order.indexOf(x.type);
|
||||
const yi = order.indexOf(y.type);
|
||||
return (xi === -1 ? order.length : xi) - (yi === -1 ? order.length : yi);
|
||||
});
|
||||
|
||||
if (this.addressChoices.length > 0) {
|
||||
this.dispatchForm.patchValue({ addressId: this.addressChoices[0].id });
|
||||
}
|
||||
},
|
||||
error: () => {
|
||||
this.isLoading = false;
|
||||
this.addressesLoaded = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** The address the operator picked, or null before one is chosen. */
|
||||
get selectedAddress() {
|
||||
const id = this.dispatchForm.get('addressId')?.value;
|
||||
return this.addressChoices.find((a) => a.id === id) || null;
|
||||
}
|
||||
|
||||
/** Lines of the selected address, for the on-screen preview and the printed label. */
|
||||
get addressLines(): string[] {
|
||||
return this.selectedAddress?.lines || [];
|
||||
}
|
||||
|
||||
get activeDispatches() {
|
||||
if (this.welcomeKitsData && this.welcomeKitsData.dispatches && this.welcomeKitsData.dispatches.length > 0) {
|
||||
return this.welcomeKitsData.dispatches;
|
||||
@@ -138,36 +261,104 @@ export class WelcomeKitDialogComponent implements OnInit {
|
||||
return;
|
||||
}
|
||||
|
||||
const chosen = this.selectedAddress;
|
||||
if (!chosen) {
|
||||
this.snackBar.open('Select the address the kit was posted to', 'Close', { duration: 3000 });
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
const formValue = this.dispatchForm.value;
|
||||
|
||||
const today = new Date();
|
||||
const formattedDate = this.datePipe.transform(today, 'dd MMMM yyyy');
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
const locale = this.settingsService.language.code;
|
||||
|
||||
const payload = {
|
||||
deliveryAddress: formValue.deliveryAddress,
|
||||
// Stored as text so the dispatch keeps the address exactly as it read on the day it went out, even if
|
||||
// the client's address record is edited later.
|
||||
deliveryAddress: `${chosen.type}: ${chosen.oneLine}`,
|
||||
addressId: chosen.id,
|
||||
deliveryMode: formValue.deliveryMode,
|
||||
deliveryModeOtherText: formValue.deliveryMode === 'OTHER' ? formValue.deliveryModeOtherText : null,
|
||||
dispatchedOnDate: formattedDate,
|
||||
locale: 'en',
|
||||
dateFormat: 'dd MMMM yyyy'
|
||||
trackingId: (formValue.trackingId || '').trim(),
|
||||
dispatchedOnDate: this.dateUtils.formatDate(formValue.dispatchedOnDate, dateFormat),
|
||||
locale,
|
||||
dateFormat
|
||||
};
|
||||
|
||||
this.clientsService.dispatchWelcomeKit(this.data.clientId, payload).subscribe({
|
||||
next: () => {
|
||||
this.snackBar.open('Welcome Kit Dispatched successfully', 'Close', { duration: 3000 });
|
||||
this.hasChanges = true;
|
||||
this.snackBar.open('Welcome Kit dispatched successfully', 'Close', { duration: 3000 });
|
||||
this.fetchData();
|
||||
},
|
||||
error: () => {
|
||||
error: (err: any) => {
|
||||
this.isLoading = false;
|
||||
this.snackBar.open('Failed to dispatch welcome kit', 'Close', { duration: 3000 });
|
||||
this.snackBar.open(this.errorMessage(err, 'Failed to dispatch welcome kit'), 'Close', { duration: 5000 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Opens the outcome form for a dispatch, pre-set to the outcome the operator clicked. */
|
||||
startOutcome(dispatch: any, status: 'DELIVERED' | 'RETURNED') {
|
||||
this.outcomeFor = dispatch;
|
||||
this.outcomeStatus = status;
|
||||
this.outcomeForm.reset({ outcomeDate: this.maxDate, remarks: '' });
|
||||
}
|
||||
|
||||
cancelOutcome() {
|
||||
this.outcomeFor = null;
|
||||
}
|
||||
|
||||
/** Records what the operator found on the delivery partner's site: delivered verifies the address, returned does not. */
|
||||
submitOutcome() {
|
||||
if (this.outcomeForm.invalid || !this.outcomeFor) {
|
||||
this.outcomeForm.markAllAsTouched();
|
||||
return;
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
const formValue = this.outcomeForm.value;
|
||||
const dateFormat = this.settingsService.dateFormat;
|
||||
const locale = this.settingsService.language.code;
|
||||
|
||||
const payload = {
|
||||
status: this.outcomeStatus,
|
||||
outcomeDate: this.dateUtils.formatDate(formValue.outcomeDate, dateFormat),
|
||||
remarks: (formValue.remarks || '').trim() || null,
|
||||
locale,
|
||||
dateFormat
|
||||
};
|
||||
|
||||
this.clientsService.updateWelcomeKitStatus(this.outcomeFor.id, payload).subscribe({
|
||||
next: () => {
|
||||
this.hasChanges = true;
|
||||
this.outcomeFor = null;
|
||||
this.snackBar.open(
|
||||
this.outcomeStatus === 'DELIVERED' ? 'Address marked as verified' : 'Address marked as not verified',
|
||||
'Close',
|
||||
{ duration: 3000 }
|
||||
);
|
||||
this.fetchData();
|
||||
},
|
||||
error: (err: any) => {
|
||||
this.isLoading = false;
|
||||
this.snackBar.open(this.errorMessage(err, 'Failed to record the outcome'), 'Close', { duration: 5000 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
close() {
|
||||
this.dialogRef.close(this.hasChanges);
|
||||
}
|
||||
|
||||
/** Surfaces the backend's own message (duplicate tracking ID, inactive client) instead of a generic failure. */
|
||||
private errorMessage(err: any, fallback: string): string {
|
||||
return err?.error?.defaultUserMessage || err?.error?.errors?.[0]?.defaultUserMessage || fallback;
|
||||
}
|
||||
|
||||
getDisplayAddress(dispatch: any): string {
|
||||
const address = dispatch.deliveryAddress || '';
|
||||
const name = this.welcomeKitsData?.clientName || '';
|
||||
const name = this.clientName;
|
||||
if (name && !address.startsWith(name)) {
|
||||
return `${name} — ${address}`;
|
||||
}
|
||||
@@ -185,6 +376,9 @@ export class WelcomeKitDialogComponent implements OnInit {
|
||||
const trackingId = dispatch.trackingId || 'N/A';
|
||||
const status = dispatch.status || 'N/A';
|
||||
const dispatchedOn = this.datePipe.transform(dispatch.dispatchedOnDate, 'mediumDate') || 'N/A';
|
||||
const verification = this.verificationLabel(dispatch.status);
|
||||
const outcomeOn =
|
||||
this.datePipe.transform(dispatch.deliveredOnDate || dispatch.returnedOnDate, 'mediumDate') || 'Awaited';
|
||||
|
||||
const primaryColor: [number, number, number] = [
|
||||
31,
|
||||
@@ -307,6 +501,14 @@ export class WelcomeKitDialogComponent implements OnInit {
|
||||
[
|
||||
'Dispatched On',
|
||||
dispatchedOn
|
||||
],
|
||||
[
|
||||
'Address Verification',
|
||||
verification
|
||||
],
|
||||
[
|
||||
'Outcome Recorded On',
|
||||
outcomeOn
|
||||
]
|
||||
]
|
||||
});
|
||||
@@ -344,4 +546,192 @@ export class WelcomeKitDialogComponent implements OnInit {
|
||||
|
||||
doc.save(`WelcomeKit_Dispatch_${trackingId !== 'N/A' ? trackingId : new Date().getTime()}.pdf`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Address label for the envelope. It is printed before the kit is posted — off the live form for a new
|
||||
* dispatch, or off a saved record — so the operator can fold it into a window envelope and hand it over the
|
||||
* counter. Layout is a single A4 sheet: the addressee block sits inside the window cut-out zone, and the
|
||||
* welcome message sits below the fold where it stays hidden until the envelope is opened.
|
||||
*/
|
||||
downloadAddressLabel(dispatch?: any) {
|
||||
const doc = new jsPDF();
|
||||
const pageWidth = doc.internal.pageSize.getWidth();
|
||||
|
||||
const primaryColor: [number, number, number] = [
|
||||
31,
|
||||
73,
|
||||
125
|
||||
];
|
||||
const grayColor: [number, number, number] = [
|
||||
110,
|
||||
110,
|
||||
110
|
||||
];
|
||||
const lineColor: [number, number, number] = [
|
||||
205,
|
||||
212,
|
||||
222
|
||||
];
|
||||
|
||||
const name = this.clientName || 'Client';
|
||||
// A saved dispatch keeps the address it actually went out with; a new one uses the address currently
|
||||
// selected in the form. The saved value carries a "Type: " prefix that belongs on the record, not the envelope.
|
||||
const addressLines = dispatch
|
||||
? String(dispatch.deliveryAddress || '')
|
||||
.replace(/^[^:]+:\s*/, '')
|
||||
.split(',')
|
||||
.map((part: string) => part.trim())
|
||||
.filter((part: string) => part.length > 0)
|
||||
: this.addressLines;
|
||||
|
||||
// --- Sender block, top-left, as the return address ---
|
||||
doc.setFillColor(...primaryColor);
|
||||
doc.rect(0, 0, pageWidth, 26, 'F');
|
||||
doc.setFontSize(17);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(255, 255, 255);
|
||||
doc.text('CBSNexora', 18, 14);
|
||||
doc.setFontSize(9);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.text('Core Banking Solution', 18, 20);
|
||||
|
||||
doc.setFontSize(7.5);
|
||||
doc.setTextColor(...grayColor);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('FROM', 22, 37);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setFontSize(8.5);
|
||||
doc.setTextColor(95, 100, 110);
|
||||
let fromY = 42.5;
|
||||
const branch = this.data.officeName ? `CBSNexora — ${this.data.officeName}` : 'CBSNexora';
|
||||
[
|
||||
branch,
|
||||
'Customer Service Desk | support@cbsnexora.com | 1800-000-0000'
|
||||
].forEach((line) => {
|
||||
doc.text(line, 22, fromY);
|
||||
fromY += 4.6;
|
||||
});
|
||||
|
||||
// --- Addressee card: name, address and the client's reference details in one block, sized so the whole
|
||||
// card sits behind a standard window cut-out ---
|
||||
const cardX = 22;
|
||||
const cardY = 60;
|
||||
const cardW = pageWidth - 44;
|
||||
const padX = cardX + 9;
|
||||
|
||||
const refs: string[] = [];
|
||||
if (this.data.accountNo) refs.push(`Account No. ${this.data.accountNo}`);
|
||||
if (this.data.cifNumber) refs.push(`CIF ${this.data.cifNumber}`);
|
||||
if (this.data.mobileNo) refs.push(`Mobile ${this.data.mobileNo}`);
|
||||
if (this.data.emailAddress) refs.push(`Email ${this.data.emailAddress}`);
|
||||
if (dispatch?.trackingId) refs.push(`Tracking ID ${dispatch.trackingId}`);
|
||||
|
||||
// Height is measured off the same offsets the text below uses, so the card closes a fixed margin under the
|
||||
// last line instead of leaving a gap that grows with a short address.
|
||||
const addrStartY = 27;
|
||||
const addrEndY = addrStartY + (addressLines.length - 1) * 6.4;
|
||||
const cardH = refs.length > 0 ? addrEndY + 5 + refs.length * 5 + 4 : addrEndY + 9;
|
||||
|
||||
doc.setFillColor(252, 253, 255);
|
||||
doc.setDrawColor(...lineColor);
|
||||
doc.setLineWidth(0.3);
|
||||
doc.roundedRect(cardX, cardY, cardW, cardH, 2, 2, 'FD');
|
||||
|
||||
// Accent spine marks the addressee edge without boxing the text in.
|
||||
doc.setFillColor(...primaryColor);
|
||||
doc.rect(cardX, cardY + 1, 1.6, cardH - 2, 'F');
|
||||
|
||||
doc.setFontSize(7);
|
||||
doc.setTextColor(...grayColor);
|
||||
doc.text('ALIGN THIS BLOCK WITH THE ENVELOPE WINDOW', cardX, cardY - 3.5);
|
||||
|
||||
doc.setFontSize(7.5);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(...primaryColor);
|
||||
doc.text('TO', padX, cardY + 10);
|
||||
|
||||
doc.setFontSize(14);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(20, 20, 20);
|
||||
doc.text(name, padX, cardY + 19);
|
||||
|
||||
doc.setFontSize(11);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setTextColor(45, 45, 45);
|
||||
let addrY = cardY + addrStartY;
|
||||
addressLines.forEach((line: string) => {
|
||||
doc.text(line, padX, addrY);
|
||||
addrY += 6.4;
|
||||
});
|
||||
|
||||
// Reference details close the same card, separated by a hairline rather than a second frame.
|
||||
if (refs.length > 0) {
|
||||
const dividerY = cardY + addrEndY + 5;
|
||||
doc.setDrawColor(...lineColor);
|
||||
doc.setLineWidth(0.2);
|
||||
doc.line(padX, dividerY, cardX + cardW - 9, dividerY);
|
||||
|
||||
doc.setFontSize(8.5);
|
||||
doc.setTextColor(...grayColor);
|
||||
let refY = dividerY + 5;
|
||||
refs.forEach((line) => {
|
||||
doc.text(line, padX, refY);
|
||||
refY += 5;
|
||||
});
|
||||
}
|
||||
|
||||
// --- Fold line: hairline broken around a centred label, so it guides the fold without competing with
|
||||
// the address block above it ---
|
||||
const foldY = cardY + cardH + 22;
|
||||
const foldGap = 12;
|
||||
doc.setDrawColor(226, 230, 236);
|
||||
doc.setLineWidth(0.15);
|
||||
doc.setLineDashPattern(
|
||||
[
|
||||
1,
|
||||
2.5
|
||||
],
|
||||
0
|
||||
);
|
||||
doc.line(30, foldY, pageWidth / 2 - foldGap, foldY);
|
||||
doc.line(pageWidth / 2 + foldGap, foldY, pageWidth - 30, foldY);
|
||||
doc.setLineDashPattern([], 0);
|
||||
doc.setFontSize(6.5);
|
||||
doc.setTextColor(178, 184, 194);
|
||||
doc.text('FOLD', pageWidth / 2, foldY + 1.6, { align: 'center' });
|
||||
|
||||
// --- Welcome message, below the fold ---
|
||||
let msgY = foldY + 14;
|
||||
doc.setFontSize(14);
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.setTextColor(...primaryColor);
|
||||
doc.text('Welcome to CBSNexora', 22, msgY);
|
||||
msgY += 10;
|
||||
|
||||
doc.setFontSize(10.5);
|
||||
doc.setFont('helvetica', 'normal');
|
||||
doc.setTextColor(40, 40, 40);
|
||||
doc.text(`Dear ${name},`, 22, msgY);
|
||||
msgY += 8;
|
||||
|
||||
const body = [
|
||||
'Thank you for opening your account with us. Enclosed is your Welcome Kit containing your account documentation and the essential materials you need to start banking with us.',
|
||||
'Please keep your account number and CIF safe, and do not share your banking credentials with anyone. Our staff will never ask you for your PIN, password or OTP.',
|
||||
'If anything in this kit is missing or incorrect, contact your branch or call our customer service desk and we will put it right.'
|
||||
];
|
||||
body.forEach((paragraph) => {
|
||||
const lines = doc.splitTextToSize(paragraph, pageWidth - 44);
|
||||
doc.text(lines, 22, msgY);
|
||||
msgY += lines.length * 5.6 + 5;
|
||||
});
|
||||
|
||||
msgY += 4;
|
||||
doc.text('Warm regards,', 22, msgY);
|
||||
msgY += 6;
|
||||
doc.setFont('helvetica', 'bold');
|
||||
doc.text('CBSNexora Operations Team', 22, msgY);
|
||||
|
||||
const stamp = this.datePipe.transform(new Date(), 'yyyyMMdd-HHmmss');
|
||||
doc.save(`WelcomeKit_Label_${this.data.accountNo || this.data.clientId}_${stamp}.pdf`);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user