Merge pull request 'mazdak/UI-2896' (#65) from mazdak/UI-2896 into PRE-PRODUCTION-2026
Reviewed-on: https://ct.mfsys.com.pk/aConnect/aConnect-UX/pulls/65PRE-PRODUCTION-2026
commit
b12008db80
@ -0,0 +1,67 @@
|
|||||||
|
<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>
|
||||||
|
|
||||||
|
<tr *ngFor="let item of menuItems; let i = index">
|
||||||
|
<td class="text-muted">{{ i + 1 }}</td>
|
||||||
|
<td>{{ item.endpoint | translate }}</td>
|
||||||
|
<td class="text-center"><input type="checkbox"
|
||||||
|
[(ngModel)]="item.checked" /></td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
</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,159 @@
|
|||||||
|
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 { filter, Observable, take } 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[] = [];
|
||||||
|
saving = false;
|
||||||
|
menuItems: { endpoint: string; checked: boolean }[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private credentialService: CredentialService,
|
||||||
|
private fb: FormBuilder,
|
||||||
|
private httpService: HttpURIService,
|
||||||
|
private i18nService: I18NService,
|
||||||
|
private uriService: URIService
|
||||||
|
) {
|
||||||
|
this.permission = this.fb.group({ userCode: [null] });
|
||||||
|
this.defaultPermissions().subscribe((data: PermissionNode[]) => {
|
||||||
|
this.permissions = data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.getAllUsers();
|
||||||
|
this.loadTransactionEndpoints();
|
||||||
|
}
|
||||||
|
|
||||||
|
loadTransactionEndpoints() {
|
||||||
|
this.httpService.requestGET<string[]>(URIKey.TRANSACTION_ENDPOINTS).subscribe((response) => {
|
||||||
|
if (!(response instanceof HttpErrorResponse)) {
|
||||||
|
this.menuItems = response.map(endpoint => ({
|
||||||
|
endpoint,
|
||||||
|
checked: false
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
defaultPermissions(): Observable<PermissionNode[]> {
|
||||||
|
return this.httpService.requestGET<PermissionNode[]>('assets/data/sideMenu.json');
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllUsers() {
|
||||||
|
this.httpService.requestGET<any[]>(URIKey.GET_ALL_USER_URI).subscribe((response) => {
|
||||||
|
if (!(response instanceof HttpErrorResponse)) {
|
||||||
|
this.users = response.map(item => ({
|
||||||
|
userName: item.userFullname,
|
||||||
|
userId: item.userId,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
onUserChange() {
|
||||||
|
this.showPermissions = true;
|
||||||
|
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)) {
|
||||||
|
|
||||||
|
const allowedEndpoints: string[] = (response as any[])
|
||||||
|
.filter(p => p.allowed)
|
||||||
|
.map(p => this.normalizeUrl(p.transactionEndpoint)); // ← normalize
|
||||||
|
|
||||||
|
this.menuItems.forEach(item => {
|
||||||
|
item.checked = allowedEndpoints.some(allowed =>
|
||||||
|
allowed.endsWith(this.normalizeUrl(item.endpoint))
|
||||||
|
);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
} else {
|
||||||
|
this.menuItems.forEach(item => item.checked = false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeUrl(url: string): string {
|
||||||
|
return url?.trim().toLowerCase().replace(/\/+$/, ''); // trim, lowercase, remove trailing slash
|
||||||
|
}
|
||||||
|
|
||||||
|
savePermissions() {
|
||||||
|
if (this.saving) return;
|
||||||
|
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 => item.endpoint);
|
||||||
|
|
||||||
|
const payload = {
|
||||||
|
porOrgacode: this.credentialService.getPorOrgacode(),
|
||||||
|
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);
|
||||||
|
|
||||||
|
if (savedNode) {
|
||||||
|
// Update state from saved node
|
||||||
|
existingNode.checked = savedNode.checked;
|
||||||
|
|
||||||
|
if (existingNode.children) {
|
||||||
|
this.updatePermissions(savedNode.children || [], existingNode.children);
|
||||||
|
}
|
||||||
|
if (existingNode.buttons) {
|
||||||
|
this.updatePermissions(savedNode.buttons || [], existingNode.buttons);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue