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 }} +

+
+ +
+ + +
+

Create New Dispatch

+
+ + Delivery Address + + + Delivery Address is required + + + + + Delivery Mode + + + {{ mode }} + + + + Delivery Mode is required + + + + + Specify Other Mode + + + Please specify the delivery mode + + +
+
+
+
+ + + + + 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 @@ - + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Client Name{{ element.clientName || 'N/A' }}Account No{{ element.accountNo || 'N/A' }}Share Product Name{{ element.productName || 'N/A' }}Approved Shares{{ element.totalApprovedShares || 0 }}Pending Shares{{ element.totalPendingForApprovalShares || 0 }}Currency{{ element.currency?.code || 'N/A' }}Activated On + {{ element.activatedDate ? (element.activatedDate | dateFormat) : 'Not Activated' }} +
+ + +
+ diff --git a/src/app/shares/share-holders/share-holders.component.scss b/src/app/shares/share-holders/share-holders.component.scss new file mode 100644 index 000000000..6d41b20a0 --- /dev/null +++ b/src/app/shares/share-holders/share-holders.component.scss @@ -0,0 +1,127 @@ +/** + * 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/. + */ +.container { + margin: 1.5rem; + padding: 0; + border-radius: var(--radius-xl); + background: var(--bg-card); + box-shadow: var(--shadow-sm); +} + +.table-header { + padding: 1.5rem; + border-bottom: 1px solid var(--border-light); + + .table-header-row { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1.5rem; + flex-wrap: wrap; + gap: 1rem; + } + + .table-title-group { + display: flex; + align-items: center; + gap: 1rem; + + .table-title { + margin: 0; + font-size: 1.25rem; + font-weight: 600; + color: var(--text-main); + display: flex; + align-items: center; + gap: 0.75rem; + letter-spacing: -0.01em; + + .t-logomark { + width: 24px; + height: 24px; + position: relative; + display: flex; + flex-wrap: wrap; + gap: 2px; + padding: 2px; + background: var(--bg-hover); + border-radius: 6px; + + span { + width: 8px; + height: 8px; + border-radius: 2px; + background: var(--primary-main); + opacity: 0.8; + + &:nth-child(2) { + opacity: 0.6; + } + + &:nth-child(3) { + opacity: 0.4; + } + + &:nth-child(4) { + opacity: 0.2; + } + } + } + } + + .table-count { + padding: 0.25rem 0.75rem; + background: var(--bg-hover); + color: var(--text-muted); + border-radius: var(--radius-full); + font-size: 0.75rem; + font-weight: 500; + letter-spacing: 0.02em; + } + } + + .table-filters { + display: flex; + gap: 1rem; + align-items: center; + flex-wrap: wrap; + + mat-form-field { + width: 240px; + margin-bottom: -1.25em; + } + } +} + +.table-container { + position: relative; + overflow: auto; + border-radius: 0 0 var(--radius-xl) var(--radius-xl); + + .table-loader { + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 10; + } +} + +.modern-table { + width: 100%; + background: transparent; + + tr.mat-row { + cursor: pointer; + transition: background-color 0.2s ease; + + &:hover { + background-color: var(--bg-hover); + } + } +} diff --git a/src/app/shares/share-holders/share-holders.component.ts b/src/app/shares/share-holders/share-holders.component.ts new file mode 100644 index 000000000..18d302ff5 --- /dev/null +++ b/src/app/shares/share-holders/share-holders.component.ts @@ -0,0 +1,199 @@ +/** + * 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, ViewChild, inject } from '@angular/core'; +import { MatTableDataSource, MatTableModule } from '@angular/material/table'; +import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; +import { MatSort, MatSortModule } from '@angular/material/sort'; +import { MatCardModule } from '@angular/material/card'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; +import { MatSelectModule } from '@angular/material/select'; +import { MatDatepickerModule } from '@angular/material/datepicker'; +import { MatNativeDateModule } from '@angular/material/core'; +import { MatIconModule } from '@angular/material/icon'; +import { MatButtonModule } from '@angular/material/button'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { FormsModule, ReactiveFormsModule, FormGroup, FormControl } from '@angular/forms'; +import { CommonModule } from '@angular/common'; +import { RouterModule } from '@angular/router'; +import { forkJoin, of } from 'rxjs'; +import { catchError, debounceTime, distinctUntilChanged, filter, switchMap, tap, startWith } from 'rxjs/operators'; +import { DateFormatPipe } from '../../pipes/date-format.pipe'; +import { SharesService } from '../shares.service'; + +@Component({ + selector: 'mifosx-share-holders', + templateUrl: './share-holders.component.html', + styleUrls: ['./share-holders.component.scss'], + standalone: true, + imports: [ + CommonModule, + RouterModule, + MatCardModule, + MatTableModule, + MatPaginatorModule, + MatSortModule, + MatProgressBarModule, + MatFormFieldModule, + MatInputModule, + MatSelectModule, + MatDatepickerModule, + MatNativeDateModule, + MatIconModule, + MatButtonModule, + MatTooltipModule, + FormsModule, + ReactiveFormsModule, + DateFormatPipe + ] +}) +export class ShareHoldersComponent implements OnInit { + displayedColumns: string[] = [ + 'clientName', + 'accountNo', + 'productName', + 'totalApprovedShares', + 'totalPendingForApprovalShares', + 'currency', + 'activatedDate' + ]; + dataSource = new MatTableDataSource(); + isLoading = true; + totalRows = 0; + + // Filter Data + offices: any[] = []; + products: any[] = []; + + // Filter State + searchValue = ''; + branchFilter = ''; + productFilter = ''; + statusFilter = ''; + dateRangeForm = new FormGroup({ + start: new FormControl(null), + end: new FormControl(null) + }); + + @ViewChild(MatPaginator, { static: true }) paginator!: MatPaginator; + @ViewChild(MatSort, { static: true }) sort!: MatSort; + + searchControl = new FormControl(''); + + private sharesService = inject(SharesService); + + ngOnInit(): void { + this.dataSource.paginator = this.paginator; + this.dataSource.sort = this.sort; + this.setupFilterPredicate(); + + this.searchControl.valueChanges + .pipe( + startWith(''), + debounceTime(400), + distinctUntilChanged(), + filter((term: string | null) => !term || term.trim().length === 0 || term.trim().length >= 2), + tap((term: string | null) => { + this.searchValue = term || ''; + this.isLoading = true; + }), + switchMap((term: string | null) => { + const search = term ? term.trim() : ''; + return forkJoin({ + accounts: this.sharesService.getShareAccountSummaries(search || undefined).pipe(catchError(() => of([]))), + offices: this.sharesService.getOffices().pipe(catchError(() => of([]))), + products: this.sharesService.getShareProducts().pipe(catchError(() => of([]))) + }); + }) + ) + .subscribe({ + next: (results: any) => { + this.offices = results.offices || []; + + const allShareAccounts = results.accounts || []; + + // Derive unique products from the actual active share accounts to ensure no garbage/dummy + // products (like NextGen Tech Pvt Ltd) show up in the dropdown. + const uniqueProductsMap = new Map(); + allShareAccounts.forEach((acc: any) => { + if (acc.productId && acc.productName) { + uniqueProductsMap.set(acc.productId, { id: acc.productId, name: acc.productName }); + } + }); + this.products = Array.from(uniqueProductsMap.values()); + + this.dataSource.data = allShareAccounts; + this.totalRows = allShareAccounts.length; + this.isLoading = false; + }, + error: () => { + this.isLoading = false; + } + }); + } + + setupFilterPredicate() { + this.dataSource.filterPredicate = (data: any, filter: string) => { + let branchMatch = true; + if (this.branchFilter) { + branchMatch = data.officeId?.toString() === this.branchFilter.toString(); + } + + let productMatch = true; + if (this.productFilter) { + productMatch = data.productId?.toString() === this.productFilter.toString(); + } + + let statusMatch = true; + if (this.statusFilter) { + statusMatch = + data.status?.value?.toLowerCase() === this.statusFilter.toLowerCase() || + data.status?.id?.toString() === this.statusFilter.toString(); + } + + let dateMatch = true; + if (this.dateRangeForm.value.start && data.activatedDate) { + const rowDate = new Date(data.activatedDate[0], data.activatedDate[1] - 1, data.activatedDate[2]); + const start = this.dateRangeForm.value.start; + const end = this.dateRangeForm.value.end || new Date(); // If no end date, use today + + // Reset hours for accurate comparison + rowDate.setHours(0, 0, 0, 0); + start.setHours(0, 0, 0, 0); + end.setHours(0, 0, 0, 0); + + dateMatch = rowDate >= start && rowDate <= end; + } + + return branchMatch && productMatch && statusMatch && dateMatch; + }; + } + + applyFilters() { + // We update the filter string to a random/unique value to trigger the filterPredicate + this.dataSource.filter = Math.random().toString(); + } + + search(filterValue: string) { + this.searchControl.setValue(filterValue); + } + + clearFilters() { + this.searchValue = ''; + this.branchFilter = ''; + this.productFilter = ''; + this.statusFilter = ''; + this.dateRangeForm.reset(); + + // Trigger server-side reload without search term + this.searchControl.setValue('', { emitEvent: true }); + // Trigger client-side filters update + this.applyFilters(); + } +} diff --git a/src/app/shares/shares-routing.module.ts b/src/app/shares/shares-routing.module.ts index b062886da..4c5193aef 100644 --- a/src/app/shares/shares-routing.module.ts +++ b/src/app/shares/shares-routing.module.ts @@ -18,7 +18,7 @@ import { DividendsTabComponent } from './shares-account-view/dividends-tab/divid import { CreateSharesAccountComponent } from './create-shares-account/create-shares-account.component'; import { EditSharesAccountComponent } from './edit-shares-account/edit-shares-account.component'; import { SharesAccountActionsComponent } from './shares-account-actions/shares-account-actions.component'; -import { EditShareAccountDetailsComponent } from './edit-share-account-details/edit-share-account-details.component'; +import { ShareHoldersComponent } from './share-holders/share-holders.component'; /** Custom Resolvers */ import { SharesAccountViewResolver } from './common-resolvers/share-account-view.resolver'; @@ -33,6 +33,11 @@ const routes: Routes = [ path: '', data: { title: 'Shares', breadcrumb: 'Shares', routeParamBreadcrumb: false, addBreadcrumbLink: false }, children: [ + { + path: 'share-holders', + data: { title: 'Share Holders', breadcrumb: 'Share Holders' }, + component: ShareHoldersComponent + }, { path: 'create', data: { title: 'Create Shares Account', breadcrumb: 'Create Shares Account' }, @@ -90,14 +95,6 @@ const routes: Routes = [ sharesAccountAndTemplate: SharesAccountAndTemplateResolver } }, - { - path: 'edit-details', - data: { title: 'Edit Share Account Details', breadcrumb: 'Edit Details', routeParamBreadcrumb: false }, - component: EditShareAccountDetailsComponent, - resolve: { - sharesAccountAndTemplate: SharesAccountAndTemplateResolver - } - }, { path: 'actions/:name', data: { title: 'Shares Account Actions', breadcrumb: 'Actions', routeParamBreadcrumb: 'name' }, diff --git a/src/app/shares/shares.module.ts b/src/app/shares/shares.module.ts index cf13126e5..6a88f04fc 100644 --- a/src/app/shares/shares.module.ts +++ b/src/app/shares/shares.module.ts @@ -37,6 +37,15 @@ import { ApproveShareDialogComponent } from './shares-account-actions/approve-sh import { RejectSharesComponent } from './shares-account-actions/reject-shares/reject-shares.component'; import { RejectShareDialogComponent } from './shares-account-actions/reject-shares/reject-share-dialog/reject-share-dialog.component'; import { GeneralTabComponent } from './shares-account-view/general-tab/general-tab.component'; +import { ShareHoldersComponent } from './share-holders/share-holders.component'; + +import { MatTableModule } from '@angular/material/table'; +import { MatPaginatorModule } from '@angular/material/paginator'; +import { MatSortModule } from '@angular/material/sort'; +import { MatCardModule } from '@angular/material/card'; +import { MatProgressBarModule } from '@angular/material/progress-bar'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; /** * Shares Module @@ -45,6 +54,15 @@ import { GeneralTabComponent } from './shares-account-view/general-tab/general-t */ @NgModule({ imports: [ + ShareHoldersComponent, + SharedModule, + MatTableModule, + MatPaginatorModule, + MatSortModule, + MatCardModule, + MatProgressBarModule, + MatFormFieldModule, + MatInputModule, SharedModule, PipesModule, DirectivesModule, diff --git a/src/app/shares/shares.service.ts b/src/app/shares/shares.service.ts index 1dc19b090..956093a20 100644 --- a/src/app/shares/shares.service.ts +++ b/src/app/shares/shares.service.ts @@ -22,6 +22,39 @@ import { Observable } from 'rxjs'; export class SharesService { private http = inject(HttpClient); + /** + * @param {any} params Optional HTTP params (like offset, limit, search) + * @returns {Observable} List of all share accounts + */ + getSharesAccounts(params?: any): Observable { + return this.http.get('/accounts/share', { params }); + } + + /** + * @returns {Observable} List of all share accounts with client details summary + */ + getShareAccountSummaries(searchTerm?: string): Observable { + let params = new HttpParams(); + if (searchTerm) { + params = params.set('searchTerm', searchTerm); + } + return this.http.get('/shareaccounts/summary', { params }); + } + + /** + * @returns {Observable} List of all offices + */ + getOffices(): Observable { + return this.http.get('/offices'); + } + + /** + * @returns {Observable} List of all share products + */ + getShareProducts(): Observable { + return this.http.get('/products/share'); + } + /** * @param {string} accountId Shares Account Id of account to get data for. * @param {boolean} template Shares account template required?. diff --git a/src/environments/.env.ts b/src/environments/.env.ts index f7521f4b2..f94dce0fc 100644 --- a/src/environments/.env.ts +++ b/src/environments/.env.ts @@ -9,8 +9,8 @@ /* tslint:disable */ export default { 'mifos_x': { - 'version': '260729', - 'hash': 'c1fc95d9' + 'version': '260731', + 'hash': 'ca85bae4' }, 'allow_switching_backend_instance': true };