/** * 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/. */ /** Angular Imports */ import { Component, OnInit, inject } from '@angular/core'; import { UntypedFormBuilder, UntypedFormGroup, Validators, FormArray, ReactiveFormsModule } from '@angular/forms'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import * as _ from 'lodash'; import { SystemService } from '../../system.service'; /** Custom Components */ import { TranslateService } from '@ngx-translate/core'; import { DeleteDialogComponent } from '../../../shared/delete-dialog/delete-dialog.component'; import { DisableDialogComponent } from '../../../shared/disable-dialog/disable-dialog.component'; import { EnableDialogComponent } from '../../../shared/enable-dialog/enable-dialog.component'; import { FaIconComponent } from '@fortawesome/angular-fontawesome'; import { MatCheckbox } from '@angular/material/checkbox'; import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module'; import { AlertService } from 'app/core/alert/alert.service'; /** Custom Service Zitadel */ import { environment } from '../../../../environments/environment'; import { AuthService } from 'app/zitadel/auth.service'; /** * View Role and Permissions Component */ @Component({ selector: 'mifosx-view-role', templateUrl: './view-role.component.html', styleUrls: ['./view-role.component.scss'], imports: [ ...STANDALONE_SHARED_IMPORTS, FaIconComponent, MatCheckbox ] }) export class ViewRoleComponent implements OnInit { private route = inject(ActivatedRoute); private systemService = inject(SystemService); private router = inject(Router); private formBuilder = inject(UntypedFormBuilder); private translateService = inject(TranslateService); dialog = inject(MatDialog); private authService = inject(AuthService); private alertService = inject(AlertService); /** Role Permissions Data */ rolePermissionService: any; /** Stores the current grouping */ currentGrouping: string; /** Stores the previous grouping */ previousGrouping = ''; /** Stores Grouping Data */ groupings: string[] = []; /** Stores the selected role */ selectedItem = ''; /** Grouped categories for sidebar */ roleGroups: { name: string; icon: string; categories: string[]; expanded: boolean }[] = []; /** Mapping of category groupings to role groups */ private categoryToGroupMap: { [key: string]: string } = { // Special & Authorisation special: 'Special & Authorisation', authorisation: 'Special & Authorisation', // Configuration & Admin configuration: 'Configuration & Admin', organisation: 'Configuration & Admin', datatable: 'Configuration & Admin', jobs: 'Configuration & Admin', // Portfolio — Client, Loan, Savings, FD, RD, Guarantor etc. portfolio: 'Portfolio', portfolio_group: 'Portfolio — Groups & Centers', portfolio_center: 'Portfolio — Groups & Centers', // Transactions transaction_loan: 'Transactions — Loan', transaction_savings: 'Transactions — Savings', transaction_shareaccount: 'Transactions — Share Account', // Accounting & Finance accounting: 'Accounting & Finance', loan_provisioning: 'Accounting & Finance', loan_reschedule: 'Accounting & Finance', // Cash Management cash_mgmt: 'Cash Management', // Reporting report: 'Reporting & Analytics', // Share Account shareaccount: 'Share Account', // Heading Menu (Top Navigation Bar) heading_menu: 'Heading Menu', // Left Panel Menu (Sidebar) sidebar_menu: 'Left Panel Menu' }; private groupIcons: { [key: string]: string } = { 'Special & Authorisation': 'fa-star', 'Configuration & Admin': 'fa-cog', Portfolio: 'fa-briefcase', 'Portfolio — Groups & Centers': 'fa-users', 'Transactions — Loan': 'fa-exchange-alt', 'Transactions — Savings': 'fa-piggy-bank', 'Transactions — Share Account': 'fa-share-alt', 'Accounting & Finance': 'fa-calculator', 'Cash Management': 'fa-cash-register', 'Reporting & Analytics': 'fa-chart-bar', 'Share Account': 'fa-chart-pie', 'Heading Menu': 'fa-th-list', 'Left Panel Menu': 'fa-bars' }; /** Checks if its disabled */ isDisabled: Boolean = true; /** Checks if there is any change in data */ checkboxesChanged: Boolean = false; /** Stores backup values */ bValuesOnly: string[] = []; /** Role ID */ roleId: any; /** Creates permission form */ formGroup: UntypedFormGroup; /** Creates Backup form */ backupform: UntypedFormGroup; /** Temporarily stores Permission data */ tempPermissionUIData: { [key: string]: { permissions: { code: string; id: number; selected?: boolean }[]; }; } = {}; /** Stores permissions */ permissions: { permissions: { code: string; id: number }[]; } = { permissions: [] }; /** Sub-grouped permissions for large categories */ permissionSubGroups: { entity: string; makerPerms: { code: string; id: number }[]; checkerPerms: { code: string; id: number }[]; expanded: boolean; }[] = []; /** Flat maker/checker split for small categories */ flatMakerPerms: { code: string; id: number }[] = []; flatCheckerPerms: { code: string; id: number }[] = []; /** Threshold for sub-grouping */ subGroupThreshold = 30; /** Add role zitadel */ /** * Retrieves the roledetails data from `resolve`. * @param {ActivatedRoute} route Activated Route. * @param {SystemService} systemService System Service. * @param {Router} router Router for navigation. * @param {FormBuilder} formBuilder Form Builder. * @param {MatDialog} dialog Shared Dialog Boxes. * @param {TranslateService} translateService Translate Service. */ constructor() { this.route.data.subscribe((data: { roledetails: any }) => { this.rolePermissionService = data.roledetails; }); } /** * Groups all the data on init */ ngOnInit() { this.permissions = { permissions: [] }; this.createForm(); this.groupRules(); this.buildRoleGroups(); this.selectedItem = 'special'; this.showPermissions('special'); this.route.params.subscribe((routeParams: any) => { this.roleId = routeParams.id; }); } /** * creates the form to display and edit permissions */ createForm() { this.formGroup = this.formBuilder.group({ roster: this.formBuilder.array( this.rolePermissionService.permissionUsageData.map((elem: any) => this.createMemberGroup(elem)) ) }); } createMemberGroup(permission: any): UntypedFormGroup { return this.formBuilder.group({ ...permission, ...{ code: [ permission.code, Validators.required ], selected: [ { value: permission.selected, disabled: true }, Validators.required ] } }); } /** * Groups the permissions based on rules */ groupRules() { this.tempPermissionUIData = {}; for (const i in this.rolePermissionService.permissionUsageData) { if (this.rolePermissionService.permissionUsageData[i]) { if (this.rolePermissionService.permissionUsageData[i].grouping !== this.currentGrouping) { this.currentGrouping = this.rolePermissionService.permissionUsageData[i].grouping; this.groupings.push(this.currentGrouping); this.tempPermissionUIData[this.currentGrouping] = { permissions: [] }; } const temp = { code: this.rolePermissionService.permissionUsageData[i].code, id: +i, selected: this.rolePermissionService.permissionUsageData[i].selected }; this.tempPermissionUIData[this.currentGrouping].permissions.push(temp); } } } /** * Builds role groups from flat groupings */ buildRoleGroups() { const groupMap: { [key: string]: string[] } = {}; for (const g of this.groupings) { const groupName = this.categoryToGroupMap[g] || 'Other'; if (!groupMap[groupName]) { groupMap[groupName] = []; } groupMap[groupName].push(g); } const order = [ 'Special & Authorisation', 'Configuration & Admin', 'Portfolio', 'Portfolio — Groups & Centers', 'Transactions — Loan', 'Transactions — Savings', 'Transactions — Share Account', 'Accounting & Finance', 'Cash Management', 'Reporting & Analytics', 'Share Account', 'Heading Menu', 'Left Panel Menu', 'Other' ]; this.roleGroups = order .filter((name) => groupMap[name]) .map((name) => ({ name, icon: this.groupIcons[name] || 'fa-folder', categories: groupMap[name], expanded: name === 'Special & Authorisation' })); } toggleGroup(group: any) { group.expanded = !group.expanded; } selectAllGroup(group: any) { const roster = this.formGroup.get('roster') as FormArray; for (const cat of group.categories) { const perms = this.tempPermissionUIData[cat]?.permissions || []; for (const p of perms) { roster.at(p.id).patchValue({ selected: true }); } } } deselectAllGroup(group: any) { const roster = this.formGroup.get('roster') as FormArray; for (const cat of group.categories) { const perms = this.tempPermissionUIData[cat]?.permissions || []; for (const p of perms) { roster.at(p.id).patchValue({ selected: false }); } } } selectAllCategory(cat: string) { const roster = this.formGroup.get('roster') as FormArray; const perms = this.tempPermissionUIData[cat]?.permissions || []; for (const p of perms) { roster.at(p.id).patchValue({ selected: true }); } } deselectAllCategory(cat: string) { const roster = this.formGroup.get('roster') as FormArray; const perms = this.tempPermissionUIData[cat]?.permissions || []; for (const p of perms) { roster.at(p.id).patchValue({ selected: false }); } } getGroupPermCount(group: any): number { let count = 0; for (const cat of group.categories) { count += this.tempPermissionUIData[cat]?.permissions?.length || 0; } return count; } getCatPermCount(cat: string): number { return this.tempPermissionUIData[cat]?.permissions?.length || 0; } /** Entity display names */ private entityNames: { [key: string]: string } = { CLIENT: 'Client', CLIENTIMAGE: 'Client Image', CLIENTIDENTIFIER: 'Client Identifier', CLIENTNOTE: 'Client Note', CLIENTCHARGE: 'Client Charge', LOAN: 'Loan', LOANNOTE: 'Loan Note', LOANCHARGE: 'Loan Charge', LOANTRANSACTIONNOTE: 'Loan Transaction Note', SAVINGSACCOUNT: 'Savings Account', SAVINGSACCOUNTCHARGE: 'Savings Account Charge', SAVINGNOTE: 'Savings Note', FIXEDDEPOSITACCOUNT: 'Fixed Deposit Account', RECURRINGDEPOSITACCOUNT: 'Recurring Deposit Account', SHAREACCOUNT: 'Share Account', GROUP: 'Group', GROUPNOTE: 'Group Note', CENTER: 'Center', GUARANTOR: 'Guarantor', COLLATERAL: 'Collateral', DOCUMENT: 'Document', ACCOUNTTRANSFER: 'Account Transfer', STANDINGINSTRUCTION: 'Standing Instruction', RESCHEDULELOAN: 'Reschedule Loan', JOURNALENTRY: 'Journal Entry', TELLER: 'Teller', CASHIER: 'Cashier', ROLE: 'Role', USER: 'User', PERMISSION: 'Permission', CODE: 'Code', CODEVALUE: 'Code Value', OFFICE: 'Office', OFFICETRANSACTION: 'Office Transaction', STAFF: 'Staff', STAFFIMAGE: 'Staff Image', CHARGE: 'Charge', FUND: 'Fund', LOANPRODUCT: 'Loan Product', SAVINGSPRODUCT: 'Savings Product', FIXEDDEPOSITPRODUCT: 'Fixed Deposit Product', RECURRINGDEPOSITPRODUCT: 'Recurring Deposit Product', SHAREPRODUCT: 'Share Product', CURRENCY: 'Currency', DATATABLE: 'Data Table', CALENDAR: 'Calendar', CONFIGURATION: 'Configuration', AUDIT: 'Audit', REPORT: 'Report', HOOK: 'Hook', TEMPLATE: 'Template', PAYMENTTYPE: 'Payment Type', HOLIDAY: 'Holiday', WORKINGDAYS: 'Working Days', GLACCOUNT: 'GL Account', GLCLOSURE: 'GL Closure', FINANCIALACTIVITYACCOUNT: 'Financial Activity Account', TAXCOMPONENT: 'Tax Component', TAXGROUP: 'Tax Group', PRODUCTMIX: 'Product Mix', EXTERNALSERVICES: 'External Services', SCHEDULER: 'Scheduler', MEETING: 'Meeting', INTERESTRATECHART: 'Interest Rate Chart', FLOATINGRATE: 'Floating Rate', ACCOUNTNUMBERFORMAT: 'Account Number Format', ACCOUNTINGRULE: 'Accounting Rule', ENTITYMAPPING: 'Entity Mapping', PROVISION: 'Provisioning', REPORTMAILINGJOB: 'Report Mailing Job', PASSWORD_PREFERENCES: 'Password Preferences', ADDRESS: 'Address', CACHE: 'Cache' }; /** * Extracts entity name from permission code (e.g., CREATE_CLIENT -> CLIENT) */ private extractEntity(code: string): string { // Common action prefixes to strip const actions = [ 'CREATE', 'READ', 'UPDATE', 'DELETE', 'APPROVE', 'REJECT', 'ACTIVATE', 'DEACTIVATE', 'CLOSE', 'DEPOSIT', 'WITHDRAWAL', 'WITHDRAW', 'DISBURSE', 'DISBURSETOSAVINGS', 'DISBURSALUNDO', 'DISBURSALLASTUNDO', 'REPAYMENT', 'ADJUST', 'WAIVE', 'WAIVEINTERESTPORTION', 'WRITEOFF', 'UNDOWRITEOFF', 'UNDOAPPROVAL', 'UNDOTRANSACTION', 'UNDO', 'CLOSEASRESCHEDULED', 'FORECLOSURE', 'REOPEN', 'REACTIVATE', 'POSTINTEREST', 'CALCULATEINTEREST', 'APPLYANNUALFEE', 'PREMATURECLOSE', 'UPDATEDEPOSITAMOUNT', 'WITHDRAWWITHHELD', 'UPDATELOANOFFICER', 'REMOVELOANOFFICER', 'BULKREASSIGN', 'REFUNDBYCASH', 'REFUNDBYTRANSFER', 'RECOVERGUARANTEES', 'RECOVERYPAYMENT', 'CREATESCHEDULEEXCEPTIONS', 'DELETESCHEDULEEXCEPTIONS', 'UPDATERUNNINGBALANCE', 'DEFINEOPENINGBALANCE', 'REVERSE', 'REGISTER', 'DEREGISTER', 'ENABLE', 'DISABLE', 'PERMISSIONS', 'INACTIVATE', 'PAY', 'PROPOSETRANSFER', 'ACCEPTTRANSFER', 'REJECTTRANSFER', 'WITHDRAWTRANSFER', 'ASSIGNSTAFF', 'UNASSIGNSTAFF', 'ASSIGNROLE', 'UPDATEROLE', 'DISASSOCIATECLIENTS', 'ASSOCIATECLIENTS', 'TRANSFERCLIENTS', 'ASSOCIATEGROUPS', 'DISASSOCIATEGROUPS', 'ALLOCATECASHIER', 'ALLOCATECASHTOCASHIER', 'SETTLECASHFROMCASHIER', 'EXECUTEJOB', 'EXECUTE', 'DIVIDEND', 'APPLYADDITIONALSHARES', 'APPROVEADDITIONALSHARES', 'REJECTADDITIONALSHARES', 'REDEEMSHARES' ]; // Try to match action_entity pattern for (const action of actions) { if (code.startsWith(action + '_')) { const entity = code.substring(action.length + 1); // Remove _CHECKER suffix return entity.replace(/_CHECKER$/, ''); } } // Fallback: return the code itself return code.replace(/_CHECKER$/, ''); } private isChecker(code: string): boolean { return code.endsWith('_CHECKER'); } /** * Splits permissions into maker/checker for flat display */ buildFlatMakerChecker() { const perms = this.permissions.permissions; this.flatMakerPerms = perms.filter((p) => !this.isChecker(p.code)); this.flatCheckerPerms = perms.filter((p) => this.isChecker(p.code)); } /** * Builds sub-groups for permissions in the current category */ buildSubGroups() { const perms = this.permissions.permissions; this.buildFlatMakerChecker(); if (perms.length <= this.subGroupThreshold) { this.permissionSubGroups = []; return; } const groups: { [entity: string]: { maker: { code: string; id: number }[]; checker: { code: string; id: number }[] }; } = {}; for (const p of perms) { const entity = this.extractEntity(p.code); if (!groups[entity]) { groups[entity] = { maker: [], checker: [] }; } if (this.isChecker(p.code)) { groups[entity].checker.push(p); } else { groups[entity].maker.push(p); } } this.permissionSubGroups = Object.keys(groups).map((entity) => ({ entity, makerPerms: groups[entity].maker, checkerPerms: groups[entity].checker, expanded: false })); } getEntityDisplayName(entity: string): string { return this.entityNames[entity] || entity.replace(/_/g, ' '); } toggleSubGroup(subGroup: any) { subGroup.expanded = !subGroup.expanded; } selectAllSubGroup(subGroup: any) { const roster = this.formGroup.get('roster') as FormArray; for (const p of [ ...subGroup.makerPerms, ...subGroup.checkerPerms ]) { roster.at(p.id).patchValue({ selected: true }); } } deselectAllSubGroup(subGroup: any) { const roster = this.formGroup.get('roster') as FormArray; for (const p of [ ...subGroup.makerPerms, ...subGroup.checkerPerms ]) { roster.at(p.id).patchValue({ selected: false }); } } getSubGroupCount(subGroup: any): number { return subGroup.makerPerms.length + subGroup.checkerPerms.length; } selectAllMaker(perms: { code: string; id: number }[]) { const roster = this.formGroup.get('roster') as FormArray; for (const p of perms) { roster.at(p.id).patchValue({ selected: true }); } } deselectAllMaker(perms: { code: string; id: number }[]) { const roster = this.formGroup.get('roster') as FormArray; for (const p of perms) { roster.at(p.id).patchValue({ selected: false }); } } selectAllChecker(perms: { code: string; id: number }[]) { const dialogRef = this.dialog.open(EnableDialogComponent, { data: { enableContext: this.translateService.instant( 'labels.text.all Checker permissions for this section. This grants approval authority' ) } }); dialogRef.afterClosed().subscribe((response: any) => { if (response?.enable) { const roster = this.formGroup.get('roster') as FormArray; for (const p of perms) { roster.at(p.id).patchValue({ selected: true }); } } }); } deselectAllChecker(perms: { code: string; id: number }[]) { const roster = this.formGroup.get('roster') as FormArray; for (const p of perms) { roster.at(p.id).patchValue({ selected: false }); } } /** * Displays the permission for selected role * @param grouping Selected Role */ showPermissions(grouping: string) { this.permissions = this.tempPermissionUIData[grouping]; this.selectedItem = grouping; this.previousGrouping = grouping; this.buildSubGroups(); } /** * Formats the Role Name * @param string String */ formatName(string: string) { if (!string) { return string; } // Try to translate first const translationKey = `labels.catalogs.${string}`; const translated = this.translateService.instant(translationKey); // If translation exists (and is different from key), use it if (translated && translated !== translationKey) { return translated; } // Otherwise, format the original string if (string.indexOf('portfolio_') > -1) { string = string.replace('portfolio_', ''); } if (string.indexOf('transaction_') > -1) { const temp = string.split('_'); string = temp[1] + ' ' + temp[0].charAt(0).toUpperCase() + temp[0].slice(1) + 's'; } string = string.charAt(0).toUpperCase() + string.slice(1); return string; } /** * Formats the permission from permission code * @param name String */ permissionName(name: any) { name = name || ''; // replace '_' with ' ' name = name.replace(/_/g, ' '); // for reports replace read with view if (this.previousGrouping === 'report') { name = name.replace(/READ/g, 'View'); } return name; } /** * Backups the values */ backupCheckValues() { this.backupform = _.cloneDeep(this.formGroup) as UntypedFormGroup; } /** * Restores the checkboxes to previous data on clicking cancel */ restoreCheckboxes() { this.formGroup = _.cloneDeep(this.backupform) as UntypedFormGroup; } isRoleEnable(value: any) { return value; } editRoles() { this.isDisabled = false; this.formGroup.controls.roster.enable(); } /** * Cancel the changes */ cancel() { this.isDisabled = true; this.formGroup.controls.roster.disable(); } /** * Submits the modified permissions */ submit() { const value = this.formGroup.get('roster').value; const data: { [key: string]: boolean } = {}; const permissionData = { permissions: {} }; for (let i = 0; i < value.length; i++) { data[value[i].code] = value[i].selected; } permissionData.permissions = data; this.formGroup.controls.roster.disable(); this.checkboxesChanged = false; this.isDisabled = true; this.systemService.updateRolePermission(this.roleId, permissionData).subscribe((response: any) => { if (response?.rollbackTransaction) { this.alertService.alert({ type: 'Success', message: 'Role permissions update request submitted successfully for approval.' }); } }); } /** * Selects all the permission of a particular role */ selectAll() { const roster = this.formGroup.get('roster') as FormArray; for (let i = 0; i < this.permissions.permissions.length; i++) { roster.at(this.permissions.permissions[i].id).patchValue({ selected: true }); } } /** * Deselects all the permissions of a particular role */ deselectAll() { const roster = this.formGroup.get('roster') as FormArray; for (let i = 0; i < this.permissions.permissions.length; i++) { roster.at(this.permissions.permissions[i].id).patchValue({ selected: false }); } } /** * Deletes the Role and redirects to Roles and Permissions. */ deleteRole() { const deleteRoleDialogRef = this.dialog.open(DeleteDialogComponent, { data: { deleteContext: this.translateService.instant('labels.inputs.Role') + ' ' + this.roleId } }); deleteRoleDialogRef.afterClosed().subscribe((response: any) => { if (response.delete) { this.systemService.deleteRole(this.roleId).subscribe((res: any) => { if (res?.rollbackTransaction) { this.alertService.alert({ type: 'Success', message: 'Role deletion request submitted successfully for approval.' }); } else if (environment.OIDC.oidcServerEnabled) { this.authService.deleteRole(this.roleId); } this.router.navigate(['/system/roles-and-permissions']); }); } else { } }); } /** * Enables the Role and redirects to Roles and Permissions. */ enableRolesConfirmation() { const enableRoleDialogRef = this.dialog.open(EnableDialogComponent, { data: { enableContext: this.translateService.instant('labels.inputs.Role') + ' ' + this.roleId } }); enableRoleDialogRef.afterClosed().subscribe((response: any) => { if (response.enable) { this.systemService.enableRole(this.roleId).subscribe((res: any) => { if (res?.rollbackTransaction) { this.alertService.alert({ type: 'Success', message: 'Role enablement request submitted successfully for approval.' }); } this.router.navigate(['/system/roles-and-permissions']); }); } else { } }); } /** * Disables the Role and redirects to Roles and Permissions. */ disableRolesConfirmation() { const deleteRoleDialogRef = this.dialog.open(DisableDialogComponent, { data: { disableContext: this.translateService.instant('labels.inputs.Role') + ' ' + this.roleId } }); deleteRoleDialogRef.afterClosed().subscribe((response: any) => { if (response.disable) { this.systemService.disableRole(this.roleId).subscribe((res: any) => { if (res?.rollbackTransaction) { this.alertService.alert({ type: 'Success', message: 'Role disablement request submitted successfully for approval.' }); } this.router.navigate(['/system/roles-and-permissions']); }); } else { } }); } }