From 47951f241d9421fd3351546332662431e32ada87 Mon Sep 17 00:00:00 2001 From: Shubham Kumar Date: Sat, 24 Jan 2026 11:15:19 +0530 Subject: [PATCH] WEB-623 refactor: remove hard-coded back-routing for component reload (#3044) --- .../centers-view/centers-view.component.ts | 144 +++++++++++------- src/app/core/services/data-reload.service.ts | 58 +++++++ 2 files changed, 146 insertions(+), 56 deletions(-) create mode 100644 src/app/core/services/data-reload.service.ts diff --git a/src/app/centers/centers-view/centers-view.component.ts b/src/app/centers/centers-view/centers-view.component.ts index ebdc4fd52..853ecb795 100644 --- a/src/app/centers/centers-view/centers-view.component.ts +++ b/src/app/centers/centers-view/centers-view.component.ts @@ -7,25 +7,28 @@ */ /** Angular Imports */ -import { Component, OnInit, 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'; /** Dialog Imports */ import { ConfirmationDialogComponent } from 'app/shared/confirmation-dialog/confirmation-dialog.component'; - -/** Custom Services */ -import { CentersService } from '../centers.service'; import { DeleteDialogComponent } from 'app/shared/delete-dialog/delete-dialog.component'; + +/** Services */ +import { CentersService } from '../centers.service'; +import { DataReloadService } from 'app/core/services/data-reload.service'; import { TranslateService } from '@ngx-translate/core'; + +/** UI Imports */ import { - MatCard, MatCardHeader, MatCardTitleGroup, MatCardMdImage, MatCardTitle, - MatCardSubtitle, - MatCardContent + MatCardSubtitle } from '@angular/material/card'; import { MatTooltip } from '@angular/material/tooltip'; import { NgClass, LowerCasePipe } from '@angular/common'; @@ -38,8 +41,9 @@ import { MatTabNav, MatTabLink, MatTabNavPanel } from '@angular/material/tabs'; import { StatusLookupPipe } from '../../pipes/status-lookup.pipe'; import { DateFormatPipe } from '../../pipes/date-format.pipe'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; + /** - * Create Center View + * Centers View Component */ @Component({ selector: 'mifosx-centers-view', @@ -71,57 +75,63 @@ import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; DateFormatPipe ] }) -export class CentersViewComponent implements OnInit { +export class CentersViewComponent implements OnInit, OnDestroy { private route = inject(ActivatedRoute); private router = inject(Router); - dialog = inject(MatDialog); - centersService = inject(CentersService); + private dialog = inject(MatDialog); + private centersService = inject(CentersService); private translateService = inject(TranslateService); + private dataReloadService = inject(DataReloadService); - /** Stores Center View Data */ centerViewData: any; - /** Center datatable */ centerDatatables: any; - /** Meeting data */ - meetingData: boolean; + meetingData = false; - /** - * Retrieves the data for center - * @param route route Activated Route. - */ - constructor() { - this.route.data.subscribe((data: { centerViewData: any; centerDatatables: any }) => { + private reloadContext!: string; + private destroy$ = new Subject(); + + ngOnInit(): void { + this.route.data.pipe(takeUntil(this.destroy$)).subscribe((data: { centerViewData: any; centerDatatables: any }) => { this.centerViewData = data.centerViewData; this.centerDatatables = data.centerDatatables; + this.meetingData = !!this.centerViewData?.collectionMeetingCalendar; + this.reloadContext = `center-${this.centerViewData.id}`; + + // Subscribe to reload events after we have the center ID + this.dataReloadService + .getReloadObservable(this.reloadContext) + .pipe(takeUntil(this.destroy$)) + .subscribe(() => { + this.refreshData(); + }); }); } - ngOnInit() { - if (this.centerViewData.collectionMeetingCalendar) { - this.meetingData = true; - } else { - this.meetingData = false; + ngOnDestroy(): void { + this.destroy$.next(); + this.destroy$.complete(); + if (this.reloadContext) { + this.dataReloadService.cleanup(this.reloadContext); } } /** * Checks if meeting is editable. */ - get editMeeting() { - if (this.centerViewData.collectionMeetingCalendar) { - const entityType = this.centerViewData.collectionMeetingCalendar.entityType.value; - if (entityType === 'CENTERS' && this.centerViewData.hierarchy === '.' + this.centerViewData.id + '.') { - return true; - } + get editMeeting(): boolean { + if (!this.centerViewData?.collectionMeetingCalendar) { + return false; } - return false; + + const entityType = this.centerViewData.collectionMeetingCalendar.entityType.value; + return entityType === 'CENTERS' && this.centerViewData.hierarchy === `.${this.centerViewData.id}.`; } /** * Performs action button/option action. * @param {string} name action name. */ - doAction(name: string) { + doAction(name: string): void { switch (name) { case 'Activate': case 'Assign Staff': @@ -132,38 +142,46 @@ export class CentersViewComponent implements OnInit { case 'Staff Assignment History': this.router.navigate([`actions/${name}`], { relativeTo: this.route }); break; + case 'Edit Meeting': - const queryParams: any = { calendarId: this.centerViewData.collectionMeetingCalendar.id }; - this.router.navigate([`actions/${name}`], { relativeTo: this.route, queryParams: queryParams }); + this.router.navigate([`actions/${name}`], { + relativeTo: this.route, + queryParams: { + calendarId: this.centerViewData.collectionMeetingCalendar.id + } + }); break; + case 'Unassign Staff': - this.centersUnassignStaff(); + this.unassignStaff(); break; + case 'Delete': this.deleteCenter(); break; + case 'Edit': this.router.navigate(['edit'], { relativeTo: this.route }); + break; } } /** * Unassign's the centers's staff. */ - private centersUnassignStaff() { - const unAssignStaffDialogRef = this.dialog.open(ConfirmationDialogComponent, { + private unassignStaff(): void { + const dialogRef = this.dialog.open(ConfirmationDialogComponent, { data: { heading: this.translateService.instant('labels.heading.Unassign Staff'), dialogContext: this.translateService.instant('labels.dialogContext.Are you sure you want Unassign Staff') } }); - unAssignStaffDialogRef.afterClosed().subscribe((response: { confirm: any }) => { - if (response.confirm) { + + dialogRef.afterClosed().subscribe((response: { confirm: boolean }) => { + if (response?.confirm) { this.centersService .executeGroupActionCommand(this.centerViewData.id, 'unassignStaff', { staffId: this.centerViewData.staffId }) - .subscribe(() => { - this.reload(); - }); + .subscribe(() => this.reload()); } }); } @@ -171,25 +189,39 @@ export class CentersViewComponent implements OnInit { /** * Deletes the center */ - private deleteCenter() { - const deleteGroupDialogRef = this.dialog.open(DeleteDialogComponent, { - data: { deleteContext: `center with id: ${this.centerViewData.id}` } + private deleteCenter(): void { + const dialogRef = this.dialog.open(DeleteDialogComponent, { + data: { + deleteContext: `center with id: ${this.centerViewData.id}` + } }); - deleteGroupDialogRef.afterClosed().subscribe((response: any) => { - if (response.delete) { + + dialogRef.afterClosed().subscribe((response: any) => { + if (response?.delete) { this.centersService.deleteCenter(this.centerViewData.id).subscribe(() => { - this.router.navigate(['/centers'], { relativeTo: this.route }); + this.router.navigate(['/centers']); }); } }); } /** - * Refetches data for the component - * TODO: Replace by a custom reload component instead of hard-coded back-routing. + * Triggers a reload event for this center. */ - reload() { - const url: string = this.router.url; - this.router.navigateByUrl(`/centers`, { skipLocationChange: true }).then(() => this.router.navigate([url])); + reload(): void { + this.dataReloadService.triggerReload(this.reloadContext); + } + + /** + * Refreshes the center data when reload is triggered. + */ + private refreshData(): void { + this.centersService + .getCenterData(this.centerViewData.id) + .pipe(takeUntil(this.destroy$)) + .subscribe((data: any) => { + this.centerViewData = data; + this.meetingData = !!data?.collectionMeetingCalendar; + }); } } diff --git a/src/app/core/services/data-reload.service.ts b/src/app/core/services/data-reload.service.ts new file mode 100644 index 000000000..f0ec759b5 --- /dev/null +++ b/src/app/core/services/data-reload.service.ts @@ -0,0 +1,58 @@ +/** + * 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 { Injectable } from '@angular/core'; +import { ReplaySubject, Observable } from 'rxjs'; + +/** + * Service to handle component data reloading without navigation hacks. + * Provides a clean way to trigger data refreshes across the application. + * Uses ReplaySubject to handle late subscribers and race conditions. + */ +@Injectable({ + providedIn: 'root' +}) +export class DataReloadService { + private reloadSubjects = new Map>(); + + /** + * Gets or creates a reload observable for a specific component/context. + * @param context Unique identifier for the reload context (e.g., 'savings-account', 'loan-view') + * @returns Observable that emits when a reload is triggered + */ + getReloadObservable(context: string): Observable { + if (!this.reloadSubjects.has(context)) { + this.reloadSubjects.set(context, new ReplaySubject(1)); + } + return this.reloadSubjects.get(context)!.asObservable(); + } + + /** + * Triggers a reload for the specified context. + * All components subscribed to this context will be notified. + * Late subscribers will immediately receive the last reload notification. + * @param context Unique identifier for the reload context + */ + triggerReload(context: string): void { + if (!this.reloadSubjects.has(context)) { + this.reloadSubjects.set(context, new ReplaySubject(1)); + } + this.reloadSubjects.get(context)!.next(); + } + + /** + * Cleanup method to remove subjects when they're no longer needed. + * @param context Unique identifier for the reload context + */ + cleanup(context: string): void { + if (this.reloadSubjects.has(context)) { + this.reloadSubjects.get(context)!.complete(); + this.reloadSubjects.delete(context); + } + } +}