Files
cbsnexora/src/app/system/roles-and-permissions/view-role/view-role.component.ts
T

830 lines
24 KiB
TypeScript

/**
* 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 {
}
});
}
}