From dd649b51de57bbcd5a0899eb28c66a1412d33e3a Mon Sep 17 00:00:00 2001 From: Mazdak Gibran <141390141+mazdakgibran@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:33:02 +0500 Subject: [PATCH] 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. --- src/app/app.routes.ts | 11 +- src/app/menu/menu.component.html | 86 +++++++++++ src/app/menu/menu.component.scss | 0 src/app/menu/menu.component.spec.ts | 23 +++ src/app/menu/menu.component.ts | 133 ++++++++++++++++++ .../side-nav/side-nav.component.html | 6 + .../reset-password.component.ts | 1 + src/app/utils/app.interfaces.ts | 4 + src/app/utils/uri-enums.ts | 8 +- src/assets/data/app.uri.json | 25 ++++ src/assets/data/sideMenu.json | 11 ++ src/assets/i18n/Arabic.json | 28 +++- src/assets/i18n/English.json | 29 +++- tsconfig.json | 1 + 14 files changed, 361 insertions(+), 5 deletions(-) create mode 100644 src/app/menu/menu.component.html create mode 100644 src/app/menu/menu.component.scss create mode 100644 src/app/menu/menu.component.spec.ts create mode 100644 src/app/menu/menu.component.ts diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index ef5249d..2a66d09 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -139,7 +139,16 @@ export const routes: Routes = [ import('./user-management/change-password/change-password.component').then( m => m.ChangePasswordComponent ) - } + }, + { + path: 'menu', + canActivate: [ActivityGuard], + loadComponent: () => + import('./menu/menu.component').then( + m => m.MenuComponent + ) + }, + ] }, { diff --git a/src/app/menu/menu.component.html b/src/app/menu/menu.component.html new file mode 100644 index 0000000..acda97c --- /dev/null +++ b/src/app/menu/menu.component.html @@ -0,0 +1,86 @@ +
+
+ +
+
+
+ +
+ + + +
+ {{ item.userName }} + {{ item.userId }} +
+
+ + + + {{ item.userName }} ({{ item.userId }}) + +
+
+
+
+
+ +
+
+

{{ "menu" | translate }}

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#{{ "type" | translate }}{{ "allow" | translate }}
{{ "one" | translate }}{{ "ACCOUNT_TO_ACCOUNT" | translate }}
{{ "two" | translate }}{{ "GL_TO_GL" | translate }}
{{ "three" | translate }}{{ "ACCOUNT_TO_GL" | translate }} +
{{ "four" | translate }}{{ "GL_TO_ACCOUNT" | translate }} +
+
+ +
+ +
+
+
+
\ No newline at end of file diff --git a/src/app/menu/menu.component.scss b/src/app/menu/menu.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/menu/menu.component.spec.ts b/src/app/menu/menu.component.spec.ts new file mode 100644 index 0000000..7419205 --- /dev/null +++ b/src/app/menu/menu.component.spec.ts @@ -0,0 +1,23 @@ +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { MenuComponent } from './menu.component'; + +describe('MenuComponent', () => { + let component: MenuComponent; + let fixture: ComponentFixture; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [MenuComponent] + }) + .compileComponents(); + + fixture = TestBed.createComponent(MenuComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/menu/menu.component.ts b/src/app/menu/menu.component.ts new file mode 100644 index 0000000..e9d7dce --- /dev/null +++ b/src/app/menu/menu.component.ts @@ -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 { + return this.httpService.requestGET('assets/data/sideMenu.json'); + } + + getAllUsers() { + this.httpService.requestGET(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); + } + } + } + } +} \ No newline at end of file diff --git a/src/app/shared/components/side-nav/side-nav.component.html b/src/app/shared/components/side-nav/side-nav.component.html index 5e7b2e9..c617868 100644 --- a/src/app/shared/components/side-nav/side-nav.component.html +++ b/src/app/shared/components/side-nav/side-nav.component.html @@ -96,6 +96,12 @@ --> +
  • + + + {{ 'menu' | translate }} + +
  • diff --git a/src/app/user-management/reset-password/reset-password.component.ts b/src/app/user-management/reset-password/reset-password.component.ts index 3f1fcef..e386fea 100644 --- a/src/app/user-management/reset-password/reset-password.component.ts +++ b/src/app/user-management/reset-password/reset-password.component.ts @@ -169,6 +169,7 @@ export class ResetPasswordComponent implements OnInit{ .set('size', '1000'); this.httpService.requestGET(URIKey.GET_ALL_USER_URI, params).subscribe({ + next: (response) => { this.allUsersDropdown = response || []; this.isLoading = false; diff --git a/src/app/utils/app.interfaces.ts b/src/app/utils/app.interfaces.ts index 82d3eff..0ba657c 100644 --- a/src/app/utils/app.interfaces.ts +++ b/src/app/utils/app.interfaces.ts @@ -4,6 +4,10 @@ export interface PermissionNode { expanded: boolean; children?: PermissionNode[]; buttons?: PermissionNode[]; + accountToAccount?: boolean; + glToGl?: boolean; + accountToGl?: boolean; + glToAccount?: boolean; } export interface LogsManagementResponse { diff --git a/src/app/utils/uri-enums.ts b/src/app/utils/uri-enums.ts index 9842569..f1eb15d 100644 --- a/src/app/utils/uri-enums.ts +++ b/src/app/utils/uri-enums.ts @@ -15,5 +15,11 @@ export enum URIKey { THIRD_PARTY_REGISTER_URI = "THIRD_PARTY_REGISTER_URI", TRANSACTION_LOGS = "TRANSACTION_LOGS", 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" + } \ No newline at end of file diff --git a/src/assets/data/app.uri.json b/src/assets/data/app.uri.json index 4a0ad4e..4cd7100 100644 --- a/src/assets/data/app.uri.json +++ b/src/assets/data/app.uri.json @@ -86,6 +86,31 @@ "Id": "ENTITY_LOGGER_MANAGER_URI", "URI": "/logs/getByDate", "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" } ] } diff --git a/src/assets/data/sideMenu.json b/src/assets/data/sideMenu.json index beaed46..7929f92 100644 --- a/src/assets/data/sideMenu.json +++ b/src/assets/data/sideMenu.json @@ -70,6 +70,17 @@ } ] }, +{ + "name": "menu", + "route": "/home/menu", + "checked": false, + "expanded": false, + "accountToAccount": false, + "glToGl": false, + "accountToGl": false, + "glToAccount": false, + "children": [] +}, { "name": "permissions", "route": "/home/permissions", diff --git a/src/assets/i18n/Arabic.json b/src/assets/i18n/Arabic.json index 27726a9..2a4c5a2 100644 --- a/src/assets/i18n/Arabic.json +++ b/src/assets/i18n/Arabic.json @@ -313,5 +313,31 @@ "dateRangeError": "الرجاء تحديد نطاق تاريخ صالح", "exportAllData": "تصدير جميع البيانات", "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": "٢٠" } diff --git a/src/assets/i18n/English.json b/src/assets/i18n/English.json index d642fc2..ea1973a 100644 --- a/src/assets/i18n/English.json +++ b/src/assets/i18n/English.json @@ -314,6 +314,31 @@ "dateRangeError": "Please select a valid date range", "exportAllData": "Export All Data", "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" } diff --git a/tsconfig.json b/tsconfig.json index 5525117..2f1a8d6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,7 @@ { "compileOnSave": false, "compilerOptions": { + "rootDir": "./src", "outDir": "./dist/out-tsc", "strict": true, "noImplicitOverride": true,