diff --git a/src/app/collections/individual-collection-sheet/individual-collection-sheet.component.ts b/src/app/collections/individual-collection-sheet/individual-collection-sheet.component.ts index 78e3ae025..0092a931d 100644 --- a/src/app/collections/individual-collection-sheet/individual-collection-sheet.component.ts +++ b/src/app/collections/individual-collection-sheet/individual-collection-sheet.component.ts @@ -7,7 +7,7 @@ */ /** Angular Imports */ -import { Component, OnInit, ViewChild, inject } from '@angular/core'; +import { Component, OnInit, OnDestroy, ViewChild, inject } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { MatPaginator } from '@angular/material/paginator'; import { MatSort, MatSortHeader } from '@angular/material/sort'; @@ -26,9 +26,12 @@ import { } from '@angular/material/table'; import { UntypedFormGroup, UntypedFormBuilder, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; +import { Subject } from 'rxjs'; +import { takeUntil, switchMap } from 'rxjs/operators'; /** Services Import */ import { CollectionsService } from '../collections.service'; +import { DataReloadService } from 'app/core/services/data-reload.service'; /** Custom Dialogs */ import { FormDialogComponent } from 'app/shared/form-dialog/form-dialog.component'; @@ -70,34 +73,30 @@ import { OrganizationService } from 'app/organization/organization.service'; MatPaginator ] }) -export class IndividualCollectionSheetComponent implements OnInit { +export class IndividualCollectionSheetComponent implements OnInit, OnDestroy { private formBuilder = inject(UntypedFormBuilder); private collectionsService = inject(CollectionsService); private organizationService = inject(OrganizationService); private route = inject(ActivatedRoute); private dateUtils = inject(Dates); - dialog = inject(MatDialog); + private dialog = inject(MatDialog); private router = inject(Router); private settingsService = inject(SettingsService); + private dataReloadService = inject(DataReloadService); - /** Offices Data */ officesData: any; - /** Loan Officer Data */ loanOfficerData: any = []; - /** Loans Data */ loansData: any = []; - /** Savings Data */ savingsData: any = []; - /** Minimum Date allowed. */ minDate = new Date(2000, 0, 1); - /** Maximum Date allowed. */ maxDate = new Date(); - /** Collection Sheet form. */ collectionSheetForm: UntypedFormGroup; - /** Toggles b/w form and table */ isCollapsed = false; - /** Collections Sheet Data */ collectionSheetData: any; + + private reloadContext = 'individual-collection-sheet'; + private destroy$ = new Subject(); + private buildDependencies$ = new Subject(); /** checks and stores the local storage values */ Success: boolean; /** Bulk Disbursement Transactions Data */ @@ -151,13 +150,19 @@ export class IndividualCollectionSheetComponent implements OnInit { * @param {Router} router Router for navigation. * @param {SettingsService} settingsService Settings Service */ - constructor() { - this.route.data.subscribe((data: { officesData: any }) => { + ngOnInit(): void { + this.route.data.pipe(takeUntil(this.destroy$)).subscribe((data: { officesData: any }) => { this.officesData = data.officesData; }); - } - ngOnInit() { + // Subscribe to reload events + this.dataReloadService + .getReloadObservable(this.reloadContext) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.refreshData(); + }); + this.maxDate = this.settingsService.businessDate; if (localStorage.getItem('Success')) { localStorage.removeItem('Success'); @@ -170,6 +175,16 @@ export class IndividualCollectionSheetComponent implements OnInit { this.buildDependencies(); } + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + this.buildDependencies$.next(); + this.buildDependencies$.complete(); + if (this.reloadContext) { + this.dataReloadService.cleanup(this.reloadContext); + } + } + /** * Creates the Individual Collection Sheet Form */ @@ -191,11 +206,19 @@ export class IndividualCollectionSheetComponent implements OnInit { * Checks for the office id value change */ buildDependencies() { - this.collectionSheetForm.get('officeId').valueChanges.subscribe((value: any) => { - this.organizationService.getStaffs(value).subscribe((response: any) => { + // Complete previous subscription before creating new one + this.buildDependencies$.next(); + + this.collectionSheetForm + .get('officeId') + .valueChanges.pipe( + takeUntil(this.buildDependencies$), + takeUntil(this.destroy$), + switchMap((value: any) => this.organizationService.getStaffs(value)) + ) + .subscribe((response: any) => { this.loanOfficerData = response; }); - }); } /** @@ -418,11 +441,30 @@ export class IndividualCollectionSheetComponent implements OnInit { } /** - * Refetches data for the component - * TODO: Replace by a custom reload component instead of hard-coded back-routing. + * Triggers a reload event for the collection sheet. */ - reload() { - const url: string = this.router.url; - this.router.navigateByUrl(`/collections`, { skipLocationChange: true }).then(() => this.router.navigate([url])); + reload(): void { + this.dataReloadService.triggerReload(this.reloadContext); + } + + /** + * Refreshes the collection sheet data when reload is triggered. + */ + private refreshData(): void { + // Re-create the form and rebuild dependencies + this.isCollapsed = false; + this.collectionSheetData = null; + + // Clear bulk transaction arrays to prevent stale data + this.bulkRepaymentTransactions = []; + this.bulkSavingsDueTransactions = []; + this.bulkDisbursementTransactionsData = {}; + + // Clear table data arrays + this.loansData = []; + this.savingsData = []; + + this.createCollectionSheetForm(); + this.buildDependencies(); } } diff --git a/src/app/groups/groups-view/groups-view.component.ts b/src/app/groups/groups-view/groups-view.component.ts index e9603e75d..776a53445 100644 --- a/src/app/groups/groups-view/groups-view.component.ts +++ b/src/app/groups/groups-view/groups-view.component.ts @@ -7,9 +7,11 @@ */ /** Angular Imports */ -import { Component, inject } from '@angular/core'; -import { ActivatedRoute, Router, RouterLink, RouterLinkActive, RouterOutlet } from '@angular/router'; +import { Component, OnInit, OnDestroy, inject } from '@angular/core'; +import { ActivatedRoute, Router, RouterLinkActive, RouterOutlet } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; /** Custom Dialogs */ import { UnassignStaffDialogComponent } from './custom-dialogs/unassign-staff-dialog/unassign-staff-dialog.component'; @@ -17,14 +19,13 @@ import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.co /** Custom Services */ import { GroupsService } from '../groups.service'; +import { DataReloadService } from 'app/core/services/data-reload.service'; import { - MatCard, MatCardHeader, MatCardTitleGroup, MatCardMdImage, MatCardTitle, - MatCardSubtitle, - MatCardContent + MatCardSubtitle } from '@angular/material/card'; import { NgClass, LowerCasePipe } from '@angular/common'; import { MatTooltip } from '@angular/material/tooltip'; @@ -69,31 +70,43 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; DateFormatPipe ] }) -export class GroupsViewComponent { +export class GroupsViewComponent implements OnInit, OnDestroy { private route = inject(ActivatedRoute); - private groupsService = inject(GroupsService); private router = inject(Router); - dialog = inject(MatDialog); + private dialog = inject(MatDialog); + private groupsService = inject(GroupsService); + private dataReloadService = inject(DataReloadService); - /** Group view data */ groupViewData: any; - /** Group datatables data */ groupDatatables: any; - /** - * Fetches group data from `resolve` - * @param {ActivatedRoute} route Activated Route - * @param {GroupsService} groupsService Groups Service - * @param {Router} router Router - * @param {MatDialog} dialog Dialog - */ - constructor() { - this.route.data.subscribe((data: { groupViewData: any; groupDatatables: any }) => { + private reloadContext!: string; + private destroy$ = new Subject(); + + ngOnInit(): void { + this.route.data.pipe(takeUntil(this.destroy$)).subscribe((data: { groupViewData: any; groupDatatables: any }) => { this.groupViewData = data.groupViewData; this.groupDatatables = data.groupDatatables; + this.reloadContext = `group-${this.groupViewData.id}`; + + // Subscribe to reload events after we have the group ID + this.dataReloadService + .getReloadObservable(this.reloadContext) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.refreshData(); + }); }); } + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + if (this.reloadContext) { + this.dataReloadService.cleanup(this.reloadContext); + } + } + /** * Performs action button/option action. * @param {string} name action name. @@ -116,8 +129,12 @@ export class GroupsViewComponent { } break; case 'Edit Meeting': - const queryParams: any = { calendarId: this.groupViewData.collectionMeetingCalendar.id }; - this.router.navigate([`actions/${name}`], { relativeTo: this.route, queryParams: queryParams }); + this.router.navigate([`actions/${name}`], { + relativeTo: this.route, + queryParams: { + calendarId: this.groupViewData.collectionMeetingCalendar.id + } + }); break; case 'Edit': this.router.navigate(['edit'], { relativeTo: this.route }); @@ -136,36 +153,43 @@ export class GroupsViewComponent { /** * Checks if meeting is editable. */ - get editMeeting() { - if (this.groupViewData.collectionMeetingCalendar) { - const entityType = this.groupViewData.collectionMeetingCalendar.entityType.value; - if (entityType === 'GROUPS' && this.groupViewData.hierarchy === '.' + this.groupViewData.id + '.') { - return true; - } + get editMeeting(): boolean { + if (!this.groupViewData?.collectionMeetingCalendar) { + return false; } - return false; + + const entityType = this.groupViewData.collectionMeetingCalendar.entityType.value; + return entityType === 'GROUPS' && this.groupViewData.hierarchy === `.${this.groupViewData.id}.`; } /** - * Refetches data for the component - * TODO: Replace by a custom reload component instead of hard-coded back-routing. + * Triggers a reload event for this group. */ - reload() { - const url: string = this.router.url; - this.router.navigateByUrl(`/groups`, { skipLocationChange: true }).then(() => this.router.navigate([url])); + reload(): void { + this.dataReloadService.triggerReload(this.reloadContext); + } + + /** + * Refreshes the group data when reload is triggered. + */ + private refreshData(): void { + this.groupsService + .getGroupData(this.groupViewData.id) + .pipe(takeUntil(this.destroy$)) + .subscribe((data: any) => { + this.groupViewData = data; + }); } /** * Unassign's the group's staff. */ - private unassignStaff() { - const unAssignStaffDialogRef = this.dialog.open(UnassignStaffDialogComponent); - unAssignStaffDialogRef.afterClosed().subscribe((response: { confirm: any }) => { - if (response.confirm) { + private unassignStaff(): void { + const dialogRef = this.dialog.open(UnassignStaffDialogComponent); + dialogRef.afterClosed().subscribe((response: { confirm: boolean }) => { + if (response?.confirm) { this.groupsService .executeGroupCommand(this.groupViewData.id, 'unassignStaff', { staffId: this.groupViewData.staffId }) - .subscribe(() => { - this.reload(); - }); + .subscribe(() => this.reload()); } }); } @@ -173,14 +197,17 @@ export class GroupsViewComponent { /** * Deletes the group */ - private deleteGroup() { - const deleteGroupDialogRef = this.dialog.open(DeleteDialogComponent, { - data: { deleteContext: `group with id: ${this.groupViewData.id}` } + private deleteGroup(): void { + const dialogRef = this.dialog.open(DeleteDialogComponent, { + data: { + deleteContext: `group with id: ${this.groupViewData.id}` + } }); - deleteGroupDialogRef.afterClosed().subscribe((response: any) => { - if (response.delete) { + + dialogRef.afterClosed().subscribe((response: any) => { + if (response?.delete) { this.groupsService.deleteGroup(this.groupViewData.id).subscribe(() => { - this.router.navigate(['/groups'], { relativeTo: this.route }); + this.router.navigate(['/groups']); }); } }); diff --git a/src/app/organization/sms-campaigns/view-campaign/view-campaign.component.ts b/src/app/organization/sms-campaigns/view-campaign/view-campaign.component.ts index 6a59d55af..f2f77e96a 100644 --- a/src/app/organization/sms-campaigns/view-campaign/view-campaign.component.ts +++ b/src/app/organization/sms-campaigns/view-campaign/view-campaign.component.ts @@ -7,9 +7,11 @@ */ /** Angular Imports */ -import { Component, OnInit, ViewChild, inject } from '@angular/core'; +import { Component, OnInit, OnDestroy, ViewChild, inject } from '@angular/core'; import { UntypedFormGroup, UntypedFormBuilder, ReactiveFormsModule } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; import { MatTableDataSource, MatTable, @@ -37,6 +39,7 @@ import { DatepickerBase } from 'app/shared/form-dialog/formfield/model/datepicke import { OrganizationService } from '../../organization.service'; import { SettingsService } from 'app/settings/settings.service'; import { Dates } from 'app/core/utils/dates'; +import { DataReloadService } from 'app/core/services/data-reload.service'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatTabGroup, MatTab } from '@angular/material/tabs'; import { MatList, MatListItem } from '@angular/material/list'; @@ -70,35 +73,32 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; DateFormatPipe ] }) -export class ViewCampaignComponent implements OnInit { +export class ViewCampaignComponent implements OnInit, OnDestroy { private router = inject(Router); private route = inject(ActivatedRoute); - dialog = inject(MatDialog); + private dialog = inject(MatDialog); private formBuilder = inject(UntypedFormBuilder); private dateUtils = inject(Dates); private organizationService = inject(OrganizationService); private settingsService = inject(SettingsService); + private dataReloadService = inject(DataReloadService); - /** Minimum date allowed. */ minDate = new Date(2000, 0, 1); - /** Maximum date allowed. */ maxDate = new Date(); - /** SMS form. */ smsForm: UntypedFormGroup; - /** SMS Campaign data. */ smsCampaignData: any; - /** Message Status */ status: any; - /** Data Table Columns */ displayedColumns: string[] = [ 'Message', 'Status', 'Mobile No.', 'Campaign Name' ]; - /** Data source for SMS campaigns table. */ dataSource = new MatTableDataSource(); + private reloadContext!: string; + private destroy$ = new Subject(); + /** Message Table Reference */ @ViewChild('messageTable') messageTableRef: MatTable; @@ -126,27 +126,32 @@ export class ViewCampaignComponent implements OnInit { } ]; - /** - * Retrieves the SMS Campaign data from `resolve - * @param {Router} router Router - * @param {ActivatedRoute} route Activated Route - * @param {MatDialog} dialog Mat Dialog - * @param {FormBuilder} formBuilder Form Builder - * @param {Dates} dateUtils Date Utils - * @param {OrganizationService} organizationService Organization Service - * @param {SettingsService} settingsService Setting Service - */ - constructor() { - this.route.data.subscribe((data: { smsCampaign: any }) => { + ngOnInit(): void { + this.route.data.pipe(takeUntil(this.destroy$)).subscribe((data: { smsCampaign: any }) => { this.smsCampaignData = data.smsCampaign; + this.reloadContext = `sms-campaign-${this.smsCampaignData.id}`; + + // Subscribe to reload events after we have the campaign ID + this.dataReloadService + .getReloadObservable(this.reloadContext) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.refreshData(); + }); }); - } - ngOnInit() { this.maxDate = this.settingsService.businessDate; this.createSMSForm(); } + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + if (this.reloadContext) { + this.dataReloadService.cleanup(this.reloadContext); + } + } + /** * Creates the SMS form. */ @@ -300,14 +305,22 @@ export class ViewCampaignComponent implements OnInit { } /** - * Refetches data fot the component - * TODO: Replace by a custom reload component instead of hard-coded back-routing. + * Triggers a reload event for this campaign. */ - private reload() { - const url: string = this.router.url; - this.router - .navigateByUrl(`/organization/sms-campaigns`, { skipLocationChange: true }) - .then(() => this.router.navigate([url])); + private reload(): void { + this.dataReloadService.triggerReload(this.reloadContext); + } + + /** + * Refreshes the campaign data when reload is triggered. + */ + private refreshData(): void { + this.organizationService + .getSmsCampaign(this.smsCampaignData.id) + .pipe(takeUntil(this.destroy$)) + .subscribe((data: any) => { + this.smsCampaignData = data; + }); } /**