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 1/4] 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, From 16165456002eda149da7f92bac4d0deec54461fd Mon Sep 17 00:00:00 2001 From: Mazdak Gibran <141390141+mazdakgibran@users.noreply.github.com> Date: Mon, 6 Apr 2026 15:57:26 +0500 Subject: [PATCH 2/4] Refactor menu permissions UI & endpoint handling --- src/app/menu/menu.component.html | 31 ++---- src/app/menu/menu.component.ts | 165 +++++++++++++++++++++---------- src/app/utils/uri-enums.ts | 3 +- src/assets/data/app.uri.json | 5 + src/assets/data/sideMenu.json | 4 - 5 files changed, 126 insertions(+), 82 deletions(-) diff --git a/src/app/menu/menu.component.html b/src/app/menu/menu.component.html index acda97c..70442ab 100644 --- a/src/app/menu/menu.component.html +++ b/src/app/menu/menu.component.html @@ -45,33 +45,14 @@ - - - - {{ "one" | translate }} - {{ "ACCOUNT_TO_ACCOUNT" | translate }} + + + {{ i + 1 }} + {{ item.name | translate }} + [(ngModel)]="item.checked" /> - - {{ "two" | translate }} - {{ "GL_TO_GL" | translate }} - - - - {{ "three" | translate }} - {{ "ACCOUNT_TO_GL" | translate }} - - - - - {{ "four" | translate }} - {{ "GL_TO_ACCOUNT" | translate }} - - - - - +
  • diff --git a/src/app/menu/menu.component.ts b/src/app/menu/menu.component.ts index e9d7dce..c63dc1f 100644 --- a/src/app/menu/menu.component.ts +++ b/src/app/menu/menu.component.ts @@ -9,7 +9,7 @@ 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 { filter, Observable, take } from 'rxjs'; import { SuccessMessages } from '../utils/enums'; import { URIService } from '../app.uri'; @@ -24,6 +24,13 @@ export class MenuComponent { permission: FormGroup; showPermissions = false; permissions: PermissionNode[] = []; + saving = false; + menuItems = [ + { name: 'accountToAccount', endpoint: URIKey.ACCOUNT_TO_ACCOUNT, checked: true }, + { name: 'glToGl', endpoint: URIKey.GL_TO_GL, checked: false }, + { name: 'accountToGl', endpoint: URIKey.ACCOUNT_TO_GL, checked: false }, + { name: 'glToAccount', endpoint: URIKey.GL_TO_ACCOUNT, checked: false } +]; constructor( private credentialService: CredentialService, @@ -32,15 +39,10 @@ export class MenuComponent { private i18nService: I18NService, private uriService: URIService ) { - this.permission = this.fb.group({ - allocation: [''], - userCode: [null], - userRole: [''], - }); - - this.defaultPermissions().subscribe((data: PermissionNode[]) => { - this.permissions = data; - }); + this.permission = this.fb.group({ userCode: [null] }); + this.defaultPermissions().subscribe((data: PermissionNode[]) => { + this.permissions = data; // needed for savePermissions + }); } ngOnInit() { @@ -52,7 +54,6 @@ export class MenuComponent { 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, @@ -61,56 +62,116 @@ export class MenuComponent { } }); } - - onUserChange() { +// mapEndpointsToPermissions(endpoints: string[]): PermissionNode[] { +// return [{ +// name: 'menu', +// checked: false, +// expanded: false, +// children: [ +// { name: 'accountToAccount', checked: endpoints.includes(this.uriService.getURIForRequest(URIKey.ACCOUNT_TO_ACCOUNT)), expanded: false }, +// { name: 'glToGl', checked: endpoints.includes(this.uriService.getURIForRequest(URIKey.GL_TO_GL)), expanded: false }, +// { name: 'accountToGl', checked: endpoints.includes(this.uriService.getURIForRequest(URIKey.ACCOUNT_TO_GL)), expanded: false }, +// { name: 'glToAccount', checked: endpoints.includes(this.uriService.getURIForRequest(URIKey.GL_TO_ACCOUNT)), expanded: false } +// ] +// }]; +// } +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) => { + const userId = this.permission.get('userCode')?.value; + const params = new HttpParams().set('userId', userId); + + this.httpService.requestGET(URIKey.TRANSACTION_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)); - } + + const allowedEndpoints: string[] = (response as any[]) + .filter(p => p.allowed) + .map(p => this.normalizeUrl(p.transactionEndpoint)); // ← normalize + + this.uriService.canSubscribe.pipe( + filter(ready => ready === true), + take(1) + ).subscribe(() => { + this.menuItems.forEach(item => { + const resolvedEndpoint = this.normalizeUrl( + this.uriService.getURIForRequest(item.endpoint) // ← normalize + ); + item.checked = allowedEndpoints.includes(resolvedEndpoint); + }); + }); + + } else { + this.menuItems.forEach(item => item.checked = false); } }); - }); } +// Add this helper method to the class +private normalizeUrl(url: string): string { + return url?.trim().toLowerCase().replace(/\/+$/, ''); // trim, lowercase, remove trailing slash +} + + +// savePermissions() { +// const selectedUser = this.permission.get('userCode')?.value; +// const menuNode = this.permissions.find(x => x.name === 'menu'); +// if (!menuNode) return; + +// const nameToURIKey: { [key: string]: URIKey } = { +// accountToAccount: URIKey.ACCOUNT_TO_ACCOUNT, +// glToGl: URIKey.GL_TO_GL, +// accountToGl: URIKey.ACCOUNT_TO_GL, +// glToAccount: URIKey.GL_TO_ACCOUNT +// }; + +// const transactionEndpoints: string[] = (menuNode.children || []) +// .filter(c => c.checked) +// .map(c => this.uriService.getURIForRequest(nameToURIKey[c.name])); + +// const payload = { userId: selectedUser, transactionEndpoints, permissions: JSON.stringify(this.permissions) }; + +// this.httpService.requestPOST(URIKey.TRANSACTION_PERMISSIONS_ASSIGN, payload).subscribe((response: any) => { +// if (!(response instanceof HttpErrorResponse)) { +// this.i18nService.success(SuccessMessages.SAVED_SUCCESSFULLY, []); +// this.permission.get('userCode')?.setValue(selectedUser); +// this.onUserChange(); +// } +// }); +// } + 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; - } - }); + if (this.saving) return; // ← guard against double clicks + this.saving = true; -} + const selectedUser = this.permission.get('userCode')?.value; + + this.uriService.canSubscribe.pipe( + filter(ready => ready === true), + take(1) + ).subscribe(() => { + const transactionEndpoints: string[] = this.menuItems + .filter(item => item.checked) + .map(item => this.uriService.getURIForRequest(item.endpoint)); + + const payload = { userId: selectedUser, transactionEndpoints }; + this.httpService.requestPOST(URIKey.TRANSACTION_PERMISSIONS_ASSIGN, payload) + .subscribe({ + next: (response: any) => { + if (!(response instanceof HttpErrorResponse)) { + this.i18nService.success(SuccessMessages.SAVED_SUCCESSFULLY, []); + this.onUserChange(); // this triggers one GET — expected + } + }, + complete: () => { + this.saving = false; // ← reset after done + }, + error: () => { + this.saving = false; + } + }); + }); +} updatePermissions(savedPermissions: PermissionNode[], existingPermissions: PermissionNode[]): void { for (const existingNode of existingPermissions) { const savedNode = savedPermissions.find(node => node.name === existingNode.name); diff --git a/src/app/utils/uri-enums.ts b/src/app/utils/uri-enums.ts index f1eb15d..c1e3236 100644 --- a/src/app/utils/uri-enums.ts +++ b/src/app/utils/uri-enums.ts @@ -20,6 +20,7 @@ export enum URIKey { GL_TO_GL = "GL_TO_GL", ACCOUNT_TO_GL = "ACCOUNT_TO_GL", GL_TO_ACCOUNT = "GL_TO_ACCOUNT", - TRANSACTION_PERMISSIONS_ASSIGN = "TRANSACTION_PERMISSIONS_ASSIGN" + TRANSACTION_PERMISSIONS_ASSIGN = "TRANSACTION_PERMISSIONS_ASSIGN", + TRANSACTION_PERMISSIONS = "TRANSACTION_PERMISSIONS" } \ No newline at end of file diff --git a/src/assets/data/app.uri.json b/src/assets/data/app.uri.json index 4cd7100..9abba5b 100644 --- a/src/assets/data/app.uri.json +++ b/src/assets/data/app.uri.json @@ -111,6 +111,11 @@ "Id": "ENTITY_GL_TO_ACCOUNT", "URI": "/transactions/gl-account", "UUID": "GL_TO_ACCOUNT" + }, + { + "Id": "ENTITY_TRANSACTION_PERMISSIONS", + "URI": "/transaction-permissions/{userId}", + "UUID": "TRANSACTION_PERMISSIONS" } ] } diff --git a/src/assets/data/sideMenu.json b/src/assets/data/sideMenu.json index 7929f92..1e64e9b 100644 --- a/src/assets/data/sideMenu.json +++ b/src/assets/data/sideMenu.json @@ -75,10 +75,6 @@ "route": "/home/menu", "checked": false, "expanded": false, - "accountToAccount": false, - "glToGl": false, - "accountToGl": false, - "glToAccount": false, "children": [] }, { From 3ea351d20dd4160e6ac7834c04dd5fb8063f0807 Mon Sep 17 00:00:00 2001 From: Mazdak Gibran <141390141+mazdakgibran@users.noreply.github.com> Date: Mon, 6 Apr 2026 17:51:41 +0500 Subject: [PATCH 3/4] dynamically show transaction permission apis from backend --- src/app/menu/menu.component.html | 2 +- src/app/menu/menu.component.ts | 36 ++++++++++++++++++-------------- src/app/utils/uri-enums.ts | 4 +++- src/assets/data/app.uri.json | 7 ++++++- src/assets/i18n/English.json | 2 +- 5 files changed, 31 insertions(+), 20 deletions(-) diff --git a/src/app/menu/menu.component.html b/src/app/menu/menu.component.html index 70442ab..782a6a4 100644 --- a/src/app/menu/menu.component.html +++ b/src/app/menu/menu.component.html @@ -48,7 +48,7 @@ {{ i + 1 }} - {{ item.name | translate }} + {{ item.endpoint | translate }} diff --git a/src/app/menu/menu.component.ts b/src/app/menu/menu.component.ts index c63dc1f..5df2f5c 100644 --- a/src/app/menu/menu.component.ts +++ b/src/app/menu/menu.component.ts @@ -25,12 +25,7 @@ export class MenuComponent { showPermissions = false; permissions: PermissionNode[] = []; saving = false; - menuItems = [ - { name: 'accountToAccount', endpoint: URIKey.ACCOUNT_TO_ACCOUNT, checked: true }, - { name: 'glToGl', endpoint: URIKey.GL_TO_GL, checked: false }, - { name: 'accountToGl', endpoint: URIKey.ACCOUNT_TO_GL, checked: false }, - { name: 'glToAccount', endpoint: URIKey.GL_TO_ACCOUNT, checked: false } -]; + menuItems: { endpoint: string; checked: boolean }[] = []; constructor( private credentialService: CredentialService, @@ -47,7 +42,21 @@ export class MenuComponent { ngOnInit() { this.getAllUsers(); + this.loadTransactionEndpoints(); } + + loadTransactionEndpoints() { + this.httpService.requestGET(URIKey.TRANSACTION_ENDPOINTS).subscribe((response) => { + if (!(response instanceof HttpErrorResponse)) { + this.menuItems = response.map(endpoint => ({ + endpoint, + checked: false + })); + } + }); +} + + defaultPermissions(): Observable { return this.httpService.requestGET('assets/data/sideMenu.json'); } @@ -88,18 +97,13 @@ onUserChange() { .filter(p => p.allowed) .map(p => this.normalizeUrl(p.transactionEndpoint)); // ← normalize - this.uriService.canSubscribe.pipe( - filter(ready => ready === true), - take(1) - ).subscribe(() => { this.menuItems.forEach(item => { - const resolvedEndpoint = this.normalizeUrl( - this.uriService.getURIForRequest(item.endpoint) // ← normalize + item.checked = allowedEndpoints.some(allowed => + allowed.endsWith(this.normalizeUrl(item.endpoint)) ); - item.checked = allowedEndpoints.includes(resolvedEndpoint); + }); - }); - + } else { this.menuItems.forEach(item => item.checked = false); } @@ -151,7 +155,7 @@ savePermissions() { ).subscribe(() => { const transactionEndpoints: string[] = this.menuItems .filter(item => item.checked) - .map(item => this.uriService.getURIForRequest(item.endpoint)); + .map(item => item.endpoint); const payload = { userId: selectedUser, transactionEndpoints }; diff --git a/src/app/utils/uri-enums.ts b/src/app/utils/uri-enums.ts index c1e3236..0fb8fb8 100644 --- a/src/app/utils/uri-enums.ts +++ b/src/app/utils/uri-enums.ts @@ -21,6 +21,8 @@ export enum URIKey { ACCOUNT_TO_GL = "ACCOUNT_TO_GL", GL_TO_ACCOUNT = "GL_TO_ACCOUNT", TRANSACTION_PERMISSIONS_ASSIGN = "TRANSACTION_PERMISSIONS_ASSIGN", - TRANSACTION_PERMISSIONS = "TRANSACTION_PERMISSIONS" + TRANSACTION_PERMISSIONS = "TRANSACTION_PERMISSIONS", + TRANSACTION_ENDPOINTS = "TRANSACTION_ENDPOINTS" + } \ No newline at end of file diff --git a/src/assets/data/app.uri.json b/src/assets/data/app.uri.json index 9abba5b..44ee2a7 100644 --- a/src/assets/data/app.uri.json +++ b/src/assets/data/app.uri.json @@ -112,10 +112,15 @@ "URI": "/transactions/gl-account", "UUID": "GL_TO_ACCOUNT" }, - { + { "Id": "ENTITY_TRANSACTION_PERMISSIONS", "URI": "/transaction-permissions/{userId}", "UUID": "TRANSACTION_PERMISSIONS" + }, + { + "Id": "ENTITY_TRANSACTION_ENDPOINTS", + "URI": "/transaction-permissions/endpoints", + "UUID": "TRANSACTION_ENDPOINTS" } ] } diff --git a/src/assets/i18n/English.json b/src/assets/i18n/English.json index ea1973a..bd8e133 100644 --- a/src/assets/i18n/English.json +++ b/src/assets/i18n/English.json @@ -315,7 +315,7 @@ "exportAllData": "Export All Data", "transactionLogDetails": "Transaction Logs Details", "USER_UPDATED_SUCCESS": "User Updated Successfully", - "menu": "Menu", + "menu": "Transaction Permissions", "type": "Transaction Type", "ACCOUNT_TO_ACCOUNT": "Account to Account", "GL_TO_GL": "General Ledger to General Ledger", From 7403e4d81de0c87311f48084236c18fc3f8f5f08 Mon Sep 17 00:00:00 2001 From: Mazdak Gibran <141390141+mazdakgibran@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:47:56 +0500 Subject: [PATCH 4/4] Update menu.component.ts --- src/app/menu/menu.component.ts | 57 ++++++---------------------------- 1 file changed, 9 insertions(+), 48 deletions(-) diff --git a/src/app/menu/menu.component.ts b/src/app/menu/menu.component.ts index 5df2f5c..8de6097 100644 --- a/src/app/menu/menu.component.ts +++ b/src/app/menu/menu.component.ts @@ -36,7 +36,7 @@ export class MenuComponent { ) { this.permission = this.fb.group({ userCode: [null] }); this.defaultPermissions().subscribe((data: PermissionNode[]) => { - this.permissions = data; // needed for savePermissions + this.permissions = data; }); } @@ -71,19 +71,7 @@ export class MenuComponent { } }); } -// mapEndpointsToPermissions(endpoints: string[]): PermissionNode[] { -// return [{ -// name: 'menu', -// checked: false, -// expanded: false, -// children: [ -// { name: 'accountToAccount', checked: endpoints.includes(this.uriService.getURIForRequest(URIKey.ACCOUNT_TO_ACCOUNT)), expanded: false }, -// { name: 'glToGl', checked: endpoints.includes(this.uriService.getURIForRequest(URIKey.GL_TO_GL)), expanded: false }, -// { name: 'accountToGl', checked: endpoints.includes(this.uriService.getURIForRequest(URIKey.ACCOUNT_TO_GL)), expanded: false }, -// { name: 'glToAccount', checked: endpoints.includes(this.uriService.getURIForRequest(URIKey.GL_TO_ACCOUNT)), expanded: false } -// ] -// }]; -// } + onUserChange() { this.showPermissions = true; const userId = this.permission.get('userCode')?.value; @@ -110,41 +98,12 @@ onUserChange() { }); } -// Add this helper method to the class private normalizeUrl(url: string): string { return url?.trim().toLowerCase().replace(/\/+$/, ''); // trim, lowercase, remove trailing slash } - -// savePermissions() { -// const selectedUser = this.permission.get('userCode')?.value; -// const menuNode = this.permissions.find(x => x.name === 'menu'); -// if (!menuNode) return; - -// const nameToURIKey: { [key: string]: URIKey } = { -// accountToAccount: URIKey.ACCOUNT_TO_ACCOUNT, -// glToGl: URIKey.GL_TO_GL, -// accountToGl: URIKey.ACCOUNT_TO_GL, -// glToAccount: URIKey.GL_TO_ACCOUNT -// }; - -// const transactionEndpoints: string[] = (menuNode.children || []) -// .filter(c => c.checked) -// .map(c => this.uriService.getURIForRequest(nameToURIKey[c.name])); - -// const payload = { userId: selectedUser, transactionEndpoints, permissions: JSON.stringify(this.permissions) }; - -// this.httpService.requestPOST(URIKey.TRANSACTION_PERMISSIONS_ASSIGN, payload).subscribe((response: any) => { -// if (!(response instanceof HttpErrorResponse)) { -// this.i18nService.success(SuccessMessages.SAVED_SUCCESSFULLY, []); -// this.permission.get('userCode')?.setValue(selectedUser); -// this.onUserChange(); -// } -// }); -// } - savePermissions() { - if (this.saving) return; // ← guard against double clicks + if (this.saving) return; this.saving = true; const selectedUser = this.permission.get('userCode')?.value; @@ -157,7 +116,11 @@ savePermissions() { .filter(item => item.checked) .map(item => item.endpoint); - const payload = { userId: selectedUser, transactionEndpoints }; + const payload = { + porOrgacode: this.credentialService.getPorOrgacode(), + userId: selectedUser, + transactionEndpoints + }; this.httpService.requestPOST(URIKey.TRANSACTION_PERMISSIONS_ASSIGN, payload) .subscribe({ @@ -183,9 +146,7 @@ savePermissions() { 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); }