WEB-623 refactor: remove hard-coded back-routing for component reload (#3044)

This commit is contained in:
Shubham Kumar
2026-01-23 23:45:19 -06:00
committed by GitHub
parent 8d4f6636d0
commit 47951f241d
2 changed files with 146 additions and 56 deletions
@@ -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<void>();
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;
});
}
}
@@ -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<string, ReplaySubject<void>>();
/**
* 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<void> {
if (!this.reloadSubjects.has(context)) {
this.reloadSubjects.set(context, new ReplaySubject<void>(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<void>(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);
}
}
}