Add transaction permissions menu and component

Introduce a new Menu feature to manage transaction permissions: adds MenuComponent (template, spec and scss), registers a /home/menu route with ActivityGuard, and adds a side-nav entry. Extend PermissionNode with boolean flags for transaction types and add logic in the component to load default menu data, fetch user permissions, map endpoint URIs to booleans, and save selections by sending endpoint arrays. Add new URI enum keys and corresponding entries in assets/app.uri.json, insert a 'menu' node in sideMenu.json, and add i18n strings for English and Arabic. Also add rootDir to tsconfig and a small whitespace fix in reset-password component's GET subscription.
mazdak/UI-2896
Mazdak Gibran 1 month ago
parent 387002c494
commit dd649b51de

@ -139,7 +139,16 @@ export const routes: Routes = [
import('./user-management/change-password/change-password.component').then( import('./user-management/change-password/change-password.component').then(
m => m.ChangePasswordComponent m => m.ChangePasswordComponent
) )
} },
{
path: 'menu',
canActivate: [ActivityGuard],
loadComponent: () =>
import('./menu/menu.component').then(
m => m.MenuComponent
)
},
] ]
}, },
{ {

@ -0,0 +1,86 @@
<div class="page-content">
<div class="container-fluid">
<!-- User Selection Card -->
<div class="card shadow-sm mb-4">
<div class="card-body">
<form [formGroup]="permission" class="row align-items-center">
<label class="col-md-2 col-form-label fw-semibold">
{{ "userCode" | translate }}
</label>
<div class="col-md-6">
<ng-select class="form-select" formControlName="userCode" [items]="users" bindLabel="userName"
bindValue="userId" placeholder="{{ 'choose' | translate }}" (change)="onUserChange()"
[searchable]="true" [clearable]="false" [dropdownPosition]="'auto'" [virtualScroll]="true"
[bufferAmount]="20" appendTo="body">
<!-- Custom template for dropdown options -->
<ng-template ng-option-tmp let-item="item">
<div class="d-flex flex-column">
<span class="fw-medium">{{ item.userName }}</span>
<small class="text-muted">{{ item.userId }}</small>
</div>
</ng-template>
<!-- Optional: Custom template for selected item -->
<ng-template ng-label-tmp let-item="item">
<span>{{ item.userName }} ({{ item.userId }})</span>
</ng-template>
</ng-select>
</div>
</form>
</div>
</div>
<div class="card shadow-sm" *ngIf="showPermissions">
<div class="card-body">
<h4 class="card-title mb-3">{{ "menu" | translate }}</h4>
<div class="table-responsive scrollable-table">
<table class="table table-hover table-bordered table-sm permission-table">
<thead class="table-light">
<tr>
<th style="width: 50px">#</th>
<th>{{ "type" | translate }}</th>
<th style="width: 100px;" class="text-center">{{ "allow" | translate }}</th>
</tr>
</thead>
<tbody>
<ng-container *ngFor="let node of permissions">
<ng-container *ngIf="node.name === 'menu'">
<tr>
<td class="text-muted">{{ "one" | translate }}</td>
<td>{{ "ACCOUNT_TO_ACCOUNT" | translate }}</td>
<td class="text-center"><input type="checkbox"
[(ngModel)]="node.accountToAccount" /></td>
</tr>
<tr>
<td class="text-muted">{{ "two" | translate }}</td>
<td>{{ "GL_TO_GL" | translate }}</td>
<td class="text-center"><input type="checkbox" [(ngModel)]="node.glToGl" /></td>
</tr>
<tr>
<td class="text-muted">{{ "three" | translate }}</td>
<td>{{ "ACCOUNT_TO_GL" | translate }}</td>
<td class="text-center"><input type="checkbox" [(ngModel)]="node.accountToGl" />
</td>
</tr>
<tr>
<td class="text-muted">{{ "four" | translate }}</td>
<td>{{ "GL_TO_ACCOUNT" | translate }}</td>
<td class="text-center"><input type="checkbox" [(ngModel)]="node.glToAccount" />
</td>
</tr>
</ng-container>
</ng-container>
</tbody>
</table>
</div>
<div class="text-end mt-3">
<button class="btn btn-primary btn-sm px-4" (click)="savePermissions()">
{{ "save" | translate }}
</button>
</div>
</div>
</div>
</div>

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { MenuComponent } from './menu.component';
describe('MenuComponent', () => {
let component: MenuComponent;
let fixture: ComponentFixture<MenuComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [MenuComponent]
})
.compileComponents();
fixture = TestBed.createComponent(MenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

@ -0,0 +1,133 @@
import { Component } from '@angular/core';
import { FormBuilder, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { PermissionNode } from '../utils/app.interfaces';
import { CredentialService } from '../services/credential.service';
import { I18NService } from '../services/i18n.service';
import { HttpURIService } from '../app.http.uri.service';
import { TranslateModule } from '@ngx-translate/core';
import { URIKey } from '../utils/uri-enums';
import { HttpErrorResponse, HttpParams } from '@angular/common/http';
import { CommonModule } from '@angular/common';
import { NgSelectModule } from '@ng-select/ng-select';
import { Observable } from 'rxjs';
import { SuccessMessages } from '../utils/enums';
import { URIService } from '../app.uri';
@Component({
selector: 'app-menu',
imports: [TranslateModule, ReactiveFormsModule, CommonModule, TranslateModule, NgSelectModule, FormsModule],
templateUrl: './menu.component.html',
styleUrl: './menu.component.scss'
})
export class MenuComponent {
users: any[] = [];
permission: FormGroup;
showPermissions = false;
permissions: PermissionNode[] = [];
constructor(
private credentialService: CredentialService,
private fb: FormBuilder,
private httpService: HttpURIService,
private i18nService: I18NService,
private uriService: URIService
) {
this.permission = this.fb.group({
allocation: [''],
userCode: [null],
userRole: [''],
});
this.defaultPermissions().subscribe((data: PermissionNode[]) => {
this.permissions = data;
});
}
ngOnInit() {
this.getAllUsers();
}
defaultPermissions(): Observable<PermissionNode[]> {
return this.httpService.requestGET<PermissionNode[]>('assets/data/sideMenu.json');
}
getAllUsers() {
this.httpService.requestGET<any[]>(URIKey.GET_ALL_USER_URI).subscribe((response) => {
console.log(URIKey.GET_ALL_USER_URI);
if (!(response instanceof HttpErrorResponse)) {
this.users = response.map(item => ({
userName: item.userFullname,
userId: item.userId,
}));
}
});
}
onUserChange() {
this.showPermissions = true;
this.defaultPermissions().subscribe((data: PermissionNode[]) => {
this.permissions = data;
const params = new HttpParams().set('userId', this.permission.get('userCode')?.value);
this.httpService.requestGET(URIKey.USER_GET_PERMISSIONS, params).subscribe((response: any) => {
if (!(response instanceof HttpErrorResponse)) {
// Step 4 - reverse map endpoints back to booleans
const menuNode = this.permissions.find(x => x.name === 'menu');
if (menuNode && response.transactionEndpoints) {
menuNode.accountToAccount = response.transactionEndpoints.includes(this.uriService.getURIForRequest(URIKey.ACCOUNT_TO_ACCOUNT));
menuNode.glToGl = response.transactionEndpoints.includes(this.uriService.getURIForRequest(URIKey.GL_TO_GL));
menuNode.accountToGl = response.transactionEndpoints.includes(this.uriService.getURIForRequest(URIKey.ACCOUNT_TO_GL));
menuNode.glToAccount = response.transactionEndpoints.includes(this.uriService.getURIForRequest(URIKey.GL_TO_ACCOUNT));
}
}
});
});
}
savePermissions() {
// Step 3 - extract menu node and build endpoints array
const menuNode = this.permissions.find(x => x.name === 'menu');
if (!menuNode) return;
const transactionEndpoints: string[] = [];
// Step 2 - map booleans to endpoint strings
if (menuNode.accountToAccount) transactionEndpoints.push(this.uriService.getURIForRequest(URIKey.ACCOUNT_TO_ACCOUNT));
if (menuNode.glToGl) transactionEndpoints.push(this.uriService.getURIForRequest(URIKey.GL_TO_GL));
if (menuNode.accountToGl) transactionEndpoints.push(this.uriService.getURIForRequest(URIKey.ACCOUNT_TO_GL));
if (menuNode.glToAccount) transactionEndpoints.push(this.uriService.getURIForRequest(URIKey.GL_TO_ACCOUNT));
// Step 1 - new payload structure
const payload = {
userId: this.permission.get('userCode')?.value,
transactionEndpoints
};
this.httpService.requestPUT(URIKey.TRANSACTION_PERMISSIONS_ASSIGN, payload).subscribe((response: any) => {
if (!(response instanceof HttpErrorResponse)) {
this.i18nService.success(SuccessMessages.SAVED_SUCCESSFULLY, []);
this.permission.get('userCode')?.setValue(null);
this.showPermissions = false;
}
});
}
updatePermissions(savedPermissions: PermissionNode[], existingPermissions: PermissionNode[]): void {
for (const existingNode of existingPermissions) {
const savedNode = savedPermissions.find(node => node.name === existingNode.name);
if (savedNode) {
// Update state from saved node
existingNode.checked = savedNode.checked;
//existingNode.expanded = savedNode.expanded;
// Recursively update children if they exist
if (existingNode.children) {
this.updatePermissions(savedNode.children || [], existingNode.children);
}
if (existingNode.buttons) {
this.updatePermissions(savedNode.buttons || [], existingNode.buttons);
}
}
}
}
}

@ -96,6 +96,12 @@
</li> </li>
</ul> </ul>
</li> --> </li> -->
<li *ngIf="permissions.permissions || authService.isAdminUser()">
<a routerLink="/home/menu" routerLinkActive="mm-active">
<i class="fa fa-database "></i>
<span> {{ 'menu' | translate }}</span>
</a>
</li>
<li *ngIf="permissions.permissions || authService.isAdminUser()"> <li *ngIf="permissions.permissions || authService.isAdminUser()">
<a routerLink="/home/permissions" routerLinkActive="mm-active"> <a routerLink="/home/permissions" routerLinkActive="mm-active">
<i class='fa fa-lock'></i> <i class='fa fa-lock'></i>

@ -169,6 +169,7 @@ export class ResetPasswordComponent implements OnInit{
.set('size', '1000'); .set('size', '1000');
this.httpService.requestGET<SetupUser[]>(URIKey.GET_ALL_USER_URI, params).subscribe({ this.httpService.requestGET<SetupUser[]>(URIKey.GET_ALL_USER_URI, params).subscribe({
next: (response) => { next: (response) => {
this.allUsersDropdown = response || []; this.allUsersDropdown = response || [];
this.isLoading = false; this.isLoading = false;

@ -4,6 +4,10 @@ export interface PermissionNode {
expanded: boolean; expanded: boolean;
children?: PermissionNode[]; children?: PermissionNode[];
buttons?: PermissionNode[]; buttons?: PermissionNode[];
accountToAccount?: boolean;
glToGl?: boolean;
accountToGl?: boolean;
glToAccount?: boolean;
} }
export interface LogsManagementResponse { export interface LogsManagementResponse {

@ -15,5 +15,11 @@ export enum URIKey {
THIRD_PARTY_REGISTER_URI = "THIRD_PARTY_REGISTER_URI", THIRD_PARTY_REGISTER_URI = "THIRD_PARTY_REGISTER_URI",
TRANSACTION_LOGS = "TRANSACTION_LOGS", TRANSACTION_LOGS = "TRANSACTION_LOGS",
LOGGER_MANAGER_URI = "LOGGER_MANAGER_URI", LOGGER_MANAGER_URI = "LOGGER_MANAGER_URI",
UPDATE_USER = 'UPDATE_USER' UPDATE_USER = 'UPDATE_USER',
ACCOUNT_TO_ACCOUNT = "ACCOUNT_TO_ACCOUNT",
GL_TO_GL = "GL_TO_GL",
ACCOUNT_TO_GL = "ACCOUNT_TO_GL",
GL_TO_ACCOUNT = "GL_TO_ACCOUNT",
TRANSACTION_PERMISSIONS_ASSIGN = "TRANSACTION_PERMISSIONS_ASSIGN"
} }

@ -86,6 +86,31 @@
"Id": "ENTITY_LOGGER_MANAGER_URI", "Id": "ENTITY_LOGGER_MANAGER_URI",
"URI": "/logs/getByDate", "URI": "/logs/getByDate",
"UUID": "LOGGER_MANAGER_URI" "UUID": "LOGGER_MANAGER_URI"
},
{
"Id": "ENTITY_TRANSACTION_PERMISSIONS_ASSIGN",
"URI": "/transaction-permissions/assign",
"UUID": "TRANSACTION_PERMISSIONS_ASSIGN"
},
{
"Id": "ENTITY_ACCOUNT_TO_ACCOUNT",
"URI": "/transactions/accounttoaccount",
"UUID": "ACCOUNT_TO_ACCOUNT"
},
{
"Id": "ENTITY_GL_TO_GL",
"URI": "/transactions/gltogls",
"UUID": "GL_TO_GL"
},
{
"Id": "ENTITY_ACCOUNT_TO_GL",
"URI": "/transactions/accounttogl",
"UUID": "ACCOUNT_TO_GL"
},
{
"Id": "ENTITY_GL_TO_ACCOUNT",
"URI": "/transactions/gl-account",
"UUID": "GL_TO_ACCOUNT"
} }
] ]
} }

@ -70,6 +70,17 @@
} }
] ]
}, },
{
"name": "menu",
"route": "/home/menu",
"checked": false,
"expanded": false,
"accountToAccount": false,
"glToGl": false,
"accountToGl": false,
"glToAccount": false,
"children": []
},
{ {
"name": "permissions", "name": "permissions",
"route": "/home/permissions", "route": "/home/permissions",

@ -313,5 +313,31 @@
"dateRangeError": "الرجاء تحديد نطاق تاريخ صالح", "dateRangeError": "الرجاء تحديد نطاق تاريخ صالح",
"exportAllData": "تصدير جميع البيانات", "exportAllData": "تصدير جميع البيانات",
"transactionLogDetails": "تفاصيل سجلات المعاملات", "transactionLogDetails": "تفاصيل سجلات المعاملات",
"USER_UPDATED_SUCCESS": "تم تحديث المستخدم بنجاح" "USER_UPDATED_SUCCESS": "تم تحديث المستخدم بنجاح",
"menu": "القائمة",
"type": "نوع المعاملة",
"ACCOUNT_TO_ACCOUNT": "من حساب إلى حساب",
"GL_TO_GL": "من دفتر الأستاذ إلى دفتر الأستاذ",
"ACCOUNT_TO_GL": "من حساب إلى دفتر الأستاذ",
"GL_TO_ACCOUNT": "من دفتر الأستاذ إلى حساب",
"one": "١",
"two": "٢",
"three": "٣",
"four": "٤",
"five": "٥",
"six": "٦",
"seven": "٧",
"eight": "٨",
"nine": "٩",
"ten": "١٠",
"eleven": "١١",
"twelve": "١٢",
"thirteen": "١٣",
"fourteen": "١٤",
"fifteen": "١٥",
"sixteen": "١٦",
"seventeen": "١٧",
"eighteen": "١٨",
"nineteen": "١٩",
"twenty": "٢٠"
} }

@ -314,6 +314,31 @@
"dateRangeError": "Please select a valid date range", "dateRangeError": "Please select a valid date range",
"exportAllData": "Export All Data", "exportAllData": "Export All Data",
"transactionLogDetails": "Transaction Logs Details", "transactionLogDetails": "Transaction Logs Details",
"USER_UPDATED_SUCCESS": "User Updated Successfully" "USER_UPDATED_SUCCESS": "User Updated Successfully",
"menu": "Menu",
"type": "Transaction Type",
"ACCOUNT_TO_ACCOUNT": "Account to Account",
"GL_TO_GL": "General Ledger to General Ledger",
"ACCOUNT_TO_GL": "Account to General Ledger",
"GL_TO_ACCOUNT": "General Ledger to Account",
"one": "1",
"two": "2",
"three": "3",
"four": "4",
"five": "5",
"six": "6",
"seven": "7",
"eight": "8",
"nine": "9",
"ten": "10",
"eleven": "11",
"twelve": "12",
"thirteen": "13",
"fourteen": "14",
"fifteen": "15",
"sixteen": "16",
"seventeen": "17",
"eighteen": "18",
"nineteen": "19",
"twenty": "20"
} }

@ -3,6 +3,7 @@
{ {
"compileOnSave": false, "compileOnSave": false,
"compilerOptions": { "compilerOptions": {
"rootDir": "./src",
"outDir": "./dist/out-tsc", "outDir": "./dist/out-tsc",
"strict": true, "strict": true,
"noImplicitOverride": true, "noImplicitOverride": true,

Loading…
Cancel
Save