diff --git a/src/app/clients/clients-view/clients-view.component.html b/src/app/clients/clients-view/clients-view.component.html
index a4cd5f25d..2bc58ab72 100644
--- a/src/app/clients/clients-view/clients-view.component.html
+++ b/src/app/clients/clients-view/clients-view.component.html
@@ -410,6 +410,9 @@
+
diff --git a/src/app/clients/clients-view/clients-view.component.ts b/src/app/clients/clients-view/clients-view.component.ts
index 0c8bf3bf5..0d914d5d5 100644
--- a/src/app/clients/clients-view/clients-view.component.ts
+++ b/src/app/clients/clients-view/clients-view.component.ts
@@ -22,6 +22,7 @@ import { DrawSignatureDialogComponent } from './custom-dialogs/draw-signature-di
import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component';
import { UploadImageDialogComponent } from './custom-dialogs/upload-image-dialog/upload-image-dialog.component';
import { CaptureImageDialogComponent } from './custom-dialogs/capture-image-dialog/capture-image-dialog.component';
+import { WelcomeKitDialogComponent } from './custom-dialogs/welcome-kit-dialog/welcome-kit-dialog.component';
/** Custom Services */
import { ClientsService } from '../clients.service';
@@ -255,6 +256,9 @@ export class ClientsViewComponent implements OnInit {
case 'Delete Image':
this.deleteProfileImage();
break;
+ case 'Welcome Kit':
+ this.dispatchWelcomeKit();
+ break;
case 'Create Standing Instructions':
const createStandingInstructionsQueryParams: any = {
officeId: this.clientViewData.officeId,
@@ -455,4 +459,20 @@ export class ClientsViewComponent implements OnInit {
}
});
}
+
+ /**
+ * Opens the Welcome Kit Dispatch dialog.
+ */
+ private dispatchWelcomeKit() {
+ const welcomeKitDialogRef = this.dialog.open(WelcomeKitDialogComponent, {
+ data: { clientId: this.clientViewData.id },
+ width: '600px'
+ });
+ welcomeKitDialogRef.afterClosed().subscribe((response: any) => {
+ if (response) {
+ // the form was successfully submitted
+ this.reload();
+ }
+ });
+ }
}
diff --git a/src/app/clients/clients-view/custom-dialogs/welcome-kit-dialog/welcome-kit-dialog.component.html b/src/app/clients/clients-view/custom-dialogs/welcome-kit-dialog/welcome-kit-dialog.component.html
new file mode 100644
index 000000000..d7f979973
--- /dev/null
+++ b/src/app/clients/clients-view/custom-dialogs/welcome-kit-dialog/welcome-kit-dialog.component.html
@@ -0,0 +1,94 @@
+
+
Welcome Kit Dispatch
+
+
+
+
+
+
+
+
+
+
+
{{ i === 0 ? 'Current' : 'Previous' }} Welcome Kit Status
+
+
+
Delivery Address: {{ getDisplayAddress(dispatch) }}
+
Tracking ID: {{ dispatch.trackingId }}
+
+ Delivery Mode: {{ dispatch.deliveryMode }}
+ ({{ dispatch.deliveryModeOtherText }})
+
+
Dispatched On: {{ dispatch.dispatchedOnDate | date: 'mediumDate' }}
+
+ Status:
+ {{ dispatch.status }}
+
+
+
+
0 && welcomeKitsData?.canDispatchNew" />
+
+
+
+
+
+
+
+
+
+
diff --git a/src/app/clients/clients-view/custom-dialogs/welcome-kit-dialog/welcome-kit-dialog.component.scss b/src/app/clients/clients-view/custom-dialogs/welcome-kit-dialog/welcome-kit-dialog.component.scss
new file mode 100644
index 000000000..4eea9cdf5
--- /dev/null
+++ b/src/app/clients/clients-view/custom-dialogs/welcome-kit-dialog/welcome-kit-dialog.component.scss
@@ -0,0 +1,59 @@
+/**
+ * Copyright since 2025 Mifos Initiative
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+.dispatch-tracking-panel {
+ background-color: #f5f5f5;
+ border-radius: 8px;
+ padding: 16px;
+ margin-bottom: 16px;
+
+ h3 {
+ margin-top: 0;
+ margin-bottom: 12px;
+ font-size: 1.1rem;
+ }
+
+ p {
+ margin: 6px 0;
+ }
+
+ .status-badge {
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 0.85rem;
+ font-weight: 500;
+ color: white;
+
+ &.status-dispatched {
+ background-color: #1976d2;
+ }
+
+ &.status-in_transit {
+ background-color: #f57c00;
+ }
+
+ &.status-delivered {
+ background-color: #388e3c;
+ }
+
+ &.status-returned {
+ background-color: #d32f2f;
+ }
+ }
+}
+
+.dispatch-create-form {
+ h3 {
+ margin-top: 0;
+ margin-bottom: 12px;
+ font-size: 1.1rem;
+ }
+}
+
+.full-width {
+ width: 100%;
+}
diff --git a/src/app/clients/clients-view/custom-dialogs/welcome-kit-dialog/welcome-kit-dialog.component.ts b/src/app/clients/clients-view/custom-dialogs/welcome-kit-dialog/welcome-kit-dialog.component.ts
new file mode 100644
index 000000000..b3d72602b
--- /dev/null
+++ b/src/app/clients/clients-view/custom-dialogs/welcome-kit-dialog/welcome-kit-dialog.component.ts
@@ -0,0 +1,347 @@
+/**
+ * Copyright since 2025 Mifos Initiative
+ *
+ * This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+import { Component, OnInit, inject } from '@angular/core';
+import { FormBuilder, FormGroup, Validators } from '@angular/forms';
+import {
+ MAT_DIALOG_DATA,
+ MatDialogRef,
+ MatDialogTitle,
+ MatDialogContent,
+ MatDialogActions,
+ MatDialogClose
+} from '@angular/material/dialog';
+import { MatSnackBar } from '@angular/material/snack-bar';
+import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
+import { ClientsService } from '../../../clients.service';
+import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
+import { DatePipe } from '@angular/common';
+import { jsPDF } from 'jspdf';
+import autoTable from 'jspdf-autotable';
+
+@Component({
+ selector: 'mifosx-welcome-kit-dialog',
+ templateUrl: './welcome-kit-dialog.component.html',
+ styleUrls: ['./welcome-kit-dialog.component.scss'],
+ imports: [
+ ...STANDALONE_SHARED_IMPORTS,
+ MatDialogTitle,
+ MatDialogContent,
+ MatDialogActions,
+ MatDialogClose,
+ MatProgressSpinnerModule
+ ],
+ providers: [DatePipe]
+})
+export class WelcomeKitDialogComponent implements OnInit {
+ private dialogRef = inject(MatDialogRef);
+ data: { clientId: string } = inject(MAT_DIALOG_DATA);
+ private clientsService = inject(ClientsService);
+ private formBuilder = inject(FormBuilder);
+ private snackBar = inject(MatSnackBar);
+ private datePipe = inject(DatePipe);
+
+ isLoading = true;
+ welcomeKitsData: any = null;
+ dispatchForm: FormGroup = this.formBuilder.group({
+ deliveryAddress: [
+ '',
+ Validators.required
+ ],
+ deliveryMode: [
+ 'POSTMAN',
+ Validators.required
+ ],
+ deliveryModeOtherText: ['']
+ });
+ deliveryModes = [
+ 'POSTMAN',
+ 'COURIER',
+ 'OTHER'
+ ];
+
+ constructor() {
+ this.dispatchForm.get('deliveryMode')?.valueChanges.subscribe((mode) => {
+ const otherControl = this.dispatchForm.get('deliveryModeOtherText');
+ if (mode === 'OTHER') {
+ otherControl?.setValidators([Validators.required]);
+ } else {
+ otherControl?.clearValidators();
+ }
+ otherControl?.updateValueAndValidity();
+ });
+ }
+
+ ngOnInit(): void {
+ this.fetchData();
+ }
+
+ fetchData() {
+ this.isLoading = true;
+ this.clientsService.getWelcomeKits(this.data.clientId).subscribe({
+ next: (response: any) => {
+ this.welcomeKitsData = response;
+ if (this.welcomeKitsData && this.welcomeKitsData.canDispatchNew) {
+ this.fetchPrimaryAddress();
+ } else {
+ this.isLoading = false;
+ }
+ },
+ error: () => {
+ this.isLoading = false;
+ this.snackBar.open('Failed to load welcome kits data', 'Close', { duration: 3000 });
+ }
+ });
+ }
+
+ 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);
+
+ const clientNameStr = this.welcomeKitsData?.clientName ? this.welcomeKitsData.clientName + ' — ' : '';
+ this.dispatchForm.patchValue({
+ deliveryAddress: clientNameStr + parts.join(', ')
+ });
+ }
+ },
+ error: () => {
+ this.isLoading = false;
+ }
+ });
+ }
+
+ get activeDispatches() {
+ if (this.welcomeKitsData && this.welcomeKitsData.dispatches && this.welcomeKitsData.dispatches.length > 0) {
+ return this.welcomeKitsData.dispatches;
+ }
+ return [];
+ }
+
+ submit() {
+ if (this.dispatchForm.invalid) {
+ this.dispatchForm.markAllAsTouched();
+ return;
+ }
+
+ this.isLoading = true;
+ const formValue = this.dispatchForm.value;
+
+ const today = new Date();
+ const formattedDate = this.datePipe.transform(today, 'dd MMMM yyyy');
+
+ const payload = {
+ deliveryAddress: formValue.deliveryAddress,
+ deliveryMode: formValue.deliveryMode,
+ deliveryModeOtherText: formValue.deliveryMode === 'OTHER' ? formValue.deliveryModeOtherText : null,
+ dispatchedOnDate: formattedDate,
+ locale: 'en',
+ dateFormat: 'dd MMMM yyyy'
+ };
+
+ this.clientsService.dispatchWelcomeKit(this.data.clientId, payload).subscribe({
+ next: () => {
+ this.snackBar.open('Welcome Kit Dispatched successfully', 'Close', { duration: 3000 });
+ this.fetchData();
+ },
+ error: () => {
+ this.isLoading = false;
+ this.snackBar.open('Failed to dispatch welcome kit', 'Close', { duration: 3000 });
+ }
+ });
+ }
+
+ getDisplayAddress(dispatch: any): string {
+ const address = dispatch.deliveryAddress || '';
+ const name = this.welcomeKitsData?.clientName || '';
+ if (name && !address.startsWith(name)) {
+ return `${name} — ${address}`;
+ }
+ return address;
+ }
+
+ downloadPdf(dispatch: any) {
+ const doc = new jsPDF();
+ const clientName = this.welcomeKitsData?.clientName || dispatch.clientName || 'N/A';
+ const address = this.getDisplayAddress(dispatch);
+ const mode =
+ dispatch.deliveryMode === 'OTHER'
+ ? `${dispatch.deliveryMode} (${dispatch.deliveryModeOtherText || ''})`
+ : dispatch.deliveryMode;
+ const trackingId = dispatch.trackingId || 'N/A';
+ const status = dispatch.status || 'N/A';
+ const dispatchedOn = this.datePipe.transform(dispatch.dispatchedOnDate, 'mediumDate') || 'N/A';
+
+ const primaryColor: [number, number, number] = [
+ 31,
+ 73,
+ 125
+ ]; // Navy Blue
+ const grayColor: [number, number, number] = [
+ 100,
+ 100,
+ 100
+ ];
+ const lightGray: [number, number, number] = [
+ 230,
+ 230,
+ 230
+ ];
+ const pageWidth = doc.internal.pageSize.getWidth();
+ const pageHeight = doc.internal.pageSize.getHeight();
+
+ // --- 1. Header Banner ---
+ doc.setFillColor(...primaryColor);
+ doc.rect(0, 0, pageWidth, 40, 'F');
+
+ // Decorative circles
+ doc.setFillColor(255, 255, 255);
+ doc.circle(pageWidth - 20, 15, 3, 'F');
+ doc.circle(pageWidth - 30, 25, 2, 'F');
+ doc.circle(pageWidth - 10, 30, 1.5, 'F');
+
+ doc.setFontSize(28);
+ doc.setFont('helvetica', 'bold');
+ doc.setTextColor(255, 255, 255);
+ doc.text('CBSNexora', 20, 22);
+
+ doc.setFontSize(11);
+ doc.setFont('helvetica', 'normal');
+ doc.text('Core Banking Solution', 22, 30);
+
+ // --- 2. Title ---
+ let currentY = 55;
+ doc.setFontSize(16);
+ doc.setFont('helvetica', 'bold');
+ doc.setTextColor(...primaryColor);
+ doc.text('Welcome Kit Dispatch Confirmation', pageWidth / 2, currentY, { align: 'center' });
+ currentY += 12;
+
+ // --- 3. Date Line ---
+ doc.setFontSize(11);
+ doc.setFont('helvetica', 'normal');
+ doc.setTextColor(0, 0, 0);
+ const today = this.datePipe.transform(new Date(), 'longDate');
+ doc.text(`Date: ${today}`, pageWidth - 20, currentY, { align: 'right' });
+ currentY += 15;
+
+ // --- 4. Greeting ---
+ doc.text(`Dear ${clientName},`, 20, currentY);
+ currentY += 10;
+
+ // --- 5. Opening welcome paragraph ---
+ const p1 =
+ 'Welcome to the CBSNexora family! We are delighted to have you on board as a valued client. As part of our onboarding process, we have dispatched your Welcome Kit to your registered address, which includes your account documentation and essential banking materials to help you get started with us.';
+ const splitP1 = doc.splitTextToSize(p1, pageWidth - 40);
+ doc.text(splitP1, 20, currentY);
+ currentY += splitP1.length * 6 + 8;
+
+ // --- 6. Short paragraph about the company ---
+ const p2 =
+ 'CBSNexora is committed to providing secure, reliable, and customer-first banking services. Our team is always available to assist you with any banking needs, from account management to digital services, ensuring a seamless banking experience.';
+ const splitP2 = doc.splitTextToSize(p2, pageWidth - 40);
+ doc.text(splitP2, 20, currentY);
+ currentY += splitP2.length * 6 + 8;
+
+ // --- 7. Transition line before details ---
+ doc.text('Please find your Welcome Kit dispatch details below for your reference:', 20, currentY);
+ currentY += 8;
+
+ // --- 8. Details Section ---
+ autoTable(doc, {
+ startY: currentY,
+ theme: 'plain',
+ margin: { left: 20, right: 20 },
+ styles: { fontSize: 11, cellPadding: 6, textColor: [
+ 0,
+ 0,
+ 0
+ ] },
+ columnStyles: {
+ 0: { fontStyle: 'bold', cellWidth: 50 },
+ 1: { cellWidth: 'auto' }
+ },
+ didDrawCell: (data) => {
+ if (data.row.index >= 0) {
+ doc.setDrawColor(...lightGray);
+ doc.setLineWidth(0.2);
+ doc.line(
+ data.cell.x,
+ data.cell.y + data.cell.height,
+ data.cell.x + data.cell.width,
+ data.cell.y + data.cell.height
+ );
+ }
+ },
+ body: [
+ [
+ 'Delivery Address',
+ address
+ ],
+ [
+ 'Delivery Mode',
+ mode
+ ],
+ [
+ 'Tracking ID',
+ trackingId
+ ],
+ [
+ 'Status',
+ status
+ ],
+ [
+ 'Dispatched On',
+ dispatchedOn
+ ]
+ ]
+ });
+
+ // --- 9. Closing paragraph ---
+ currentY = (doc as any).lastAutoTable.finalY + 12;
+ const p3 =
+ 'Should you have any questions about your Welcome Kit or need any assistance, please do not hesitate to reach out to our support team. We look forward to serving you and building a long-term relationship with you.';
+ const splitP3 = doc.splitTextToSize(p3, pageWidth - 40);
+ doc.text(splitP3, 20, currentY);
+ currentY += splitP3.length * 6 + 12;
+
+ // --- 10. Sign-off ---
+ doc.text('Warm regards,', 20, currentY);
+ currentY += 7;
+ doc.setFont('helvetica', 'bold');
+ doc.text('CBSNexora Operations Team', 20, currentY);
+
+ // --- 11. Footer ---
+ // Ensure footer doesn't float too high on short pages, but pushes down if content goes long
+ let footerY = currentY + 30;
+ if (footerY < pageHeight - 30) {
+ footerY = pageHeight - 30;
+ }
+
+ doc.setDrawColor(...primaryColor);
+ doc.setLineWidth(0.3);
+ doc.line(20, footerY - 5, pageWidth - 20, footerY - 5);
+
+ doc.setFontSize(9);
+ doc.setFont('helvetica', 'italic');
+ doc.setTextColor(...grayColor);
+ const timestamp = this.datePipe.transform(new Date(), 'medium');
+ doc.text(`This is a system-generated document. Generated on: ${timestamp}`, 20, footerY + 2);
+
+ doc.save(`WelcomeKit_Dispatch_${trackingId !== 'N/A' ? trackingId : new Date().getTime()}.pdf`);
+ }
+}
diff --git a/src/app/clients/clients.service.ts b/src/app/clients/clients.service.ts
index cbb893260..14a2efc49 100644
--- a/src/app/clients/clients.service.ts
+++ b/src/app/clients/clients.service.ts
@@ -950,4 +950,12 @@ export class ClientsService {
submitClientForm60(clientId: string, data: any): Observable {
return this.http.post(`/clients/${clientId}/form60`, data);
}
+
+ getWelcomeKits(clientId: string | number): Observable {
+ return this.http.get(`/clients/${clientId}/welcomekit`);
+ }
+
+ dispatchWelcomeKit(clientId: string | number, data: any): Observable {
+ return this.http.post(`/clients/${clientId}/welcomekit`, data);
+ }
}
diff --git a/src/app/core/shell/shell.component.html b/src/app/core/shell/shell.component.html
index bfb5c140c..4dd81b25f 100644
--- a/src/app/core/shell/shell.component.html
+++ b/src/app/core/shell/shell.component.html
@@ -1121,7 +1121,7 @@
-