feat: non-individual entity UI improvements — hide individual-only sections, fix legal form display, send extended fields to API

- Hide Family Members tab/section for non-individual entities in client view, create flow, and preview
- Hide Financial & Classification section for non-individual entities in personal data and preview
- Fix legal form column in clients list — fetch legalForm from client API for correct display
- Add person/business icon before client name in list rows
- Remove unsupportedNpd filter — send authorizedSignatory, additionalInfo, dateOfRegistration to API
- Add registration number format hint (mat-hint) based on constitution type
- Add spacing between verification row and constitution type dropdowns
- Reduce table cell padding and prevent action buttons from being clipped
- Add missing MPL-2.0 license headers

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Aditya
2026-04-04 15:10:51 +05:30
co-authored by Claude Opus 4.6
parent ab01111424
commit b890d24bb7
522 changed files with 51991 additions and 21885 deletions
@@ -6,12 +6,26 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="container">
<mat-card>
<div class="ar-container">
<!-- Header -->
<div class="ar-header">
<button mat-stroked-button [routerLink]="['../']" class="ar-back">
<i class="fas fa-arrow-left"></i> {{ 'labels.inputs.Manage Roles and Permission' | translate }}
</button>
<h2 class="ar-header__title">{{ 'labels.buttons.Add Role' | translate }}</h2>
</div>
<!-- Form Card -->
<div class="ar-card">
<form [formGroup]="roleForm" (ngSubmit)="submit()">
<mat-card-content>
<div class="layout-column">
<mat-form-field>
<!-- Role Details Section -->
<div class="ar-section">
<div class="ar-section__header">
<i class="fas fa-key"></i>
<h3>{{ 'labels.inputs.Role' | translate }}</h3>
</div>
<div class="ar-grid">
<mat-form-field appearance="outline" class="ar-field ar-field--full">
<mat-label>{{ 'labels.inputs.Name' | translate }}</mat-label>
<input matInput required formControlName="name" />
@if (roleForm.controls.name.hasError('required')) {
@@ -22,7 +36,7 @@
}
</mat-form-field>
<mat-form-field>
<mat-form-field appearance="outline" class="ar-field ar-field--full">
<mat-label>{{ 'labels.inputs.Description' | translate }}</mat-label>
<textarea matInput required formControlName="description"></textarea>
@if (roleForm.controls.description.hasError('required')) {
@@ -33,16 +47,17 @@
}
</mat-form-field>
</div>
</mat-card-content>
</div>
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
<button type="button" mat-raised-button [routerLink]="['../']">
<!-- Actions -->
<div class="ar-actions">
<button type="button" mat-stroked-button [routerLink]="['../']" class="ar-actions__cancel">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" [disabled]="!roleForm.valid">
{{ 'labels.buttons.Submit' | translate }}
<button type="submit" mat-flat-button color="primary" [disabled]="!roleForm.valid" class="ar-actions__submit">
<i class="fas fa-check"></i> {{ 'labels.buttons.Submit' | translate }}
</button>
</mat-card-actions>
</div>
</form>
</mat-card>
</div>
</div>
@@ -6,6 +6,128 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
.container {
max-width: 37rem;
$primary: #1353a0;
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-500: #9aa0a6;
$gray-700: #5f6368;
$gray-900: #202124;
.ar-container {
max-width: 960px;
margin: 0 auto;
padding: 20px 16px;
}
// Header
.ar-header {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 20px;
&__title {
font-size: 18px;
font-weight: 600;
color: $gray-900;
margin: 0;
}
}
.ar-back {
border-radius: 8px !important;
font-size: 12px !important;
font-weight: 600 !important;
height: 32px !important;
border-color: $border !important;
color: $gray-500 !important;
i { margin-right: 6px; }
&:hover { color: $primary !important; border-color: $primary !important; }
}
// Card
.ar-card {
background: #fff;
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
}
// Section
.ar-section {
&__header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 24px;
background: $gray-50;
border-bottom: 1px solid $gray-200;
i {
font-size: 14px;
color: $accent;
}
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
}
& + & {
border-top: 1px solid $gray-200;
}
}
// Grid
.ar-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
@media (max-width: 600px) { grid-template-columns: 1fr; }
}
.ar-field {
width: 100%;
&--full {
grid-column: 1 / -1;
}
}
// Actions
.ar-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 14px 24px;
border-top: 1px solid $gray-200;
background: $gray-50;
&__cancel {
border-radius: 8px !important;
font-size: 13px !important;
font-weight: 600 !important;
height: 36px !important;
border-color: $border !important;
color: $gray-700 !important;
}
&__submit {
border-radius: 8px !important;
font-size: 13px !important;
font-weight: 600 !important;
height: 36px !important;
box-shadow: 0 1px 3px rgba($primary, 0.3) !important;
i { margin-right: 6px; font-size: 12px; }
}
}
@@ -6,17 +6,31 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="container">
<mat-card>
<div class="er-container">
<!-- Header -->
<div class="er-header">
<button mat-stroked-button [routerLink]="['../../']" class="er-back">
<i class="fas fa-arrow-left"></i> {{ 'labels.inputs.Manage Roles and Permission' | translate }}
</button>
<h2 class="er-header__title">{{ 'labels.text.Edit Role' | translate }}</h2>
</div>
<!-- Form Card -->
<div class="er-card">
<form [formGroup]="roleForm" (ngSubmit)="submit()">
<mat-card-content>
<div class="layout-column">
<mat-form-field>
<!-- Role Details Section -->
<div class="er-section">
<div class="er-section__header">
<i class="fas fa-key"></i>
<h3>{{ 'labels.inputs.Role' | translate }}</h3>
</div>
<div class="er-grid">
<mat-form-field appearance="outline" class="er-field er-field--full">
<mat-label>{{ 'labels.inputs.Role Name' | translate }}</mat-label>
<input matInput disabled formControlName="name" />
</mat-form-field>
<mat-form-field>
<mat-form-field appearance="outline" class="er-field er-field--full">
<mat-label>{{ 'labels.inputs.Role Description' | translate }}</mat-label>
<input matInput required formControlName="description" />
@if (roleForm.controls.description.hasError('required')) {
@@ -27,16 +41,17 @@
}
</mat-form-field>
</div>
</mat-card-content>
</div>
<mat-card-actions class="layout-row align-center gap-5px responsive-column">
<button type="button" mat-raised-button [routerLink]="['../../']">
<!-- Actions -->
<div class="er-actions">
<button type="button" mat-stroked-button [routerLink]="['../../']" class="er-actions__cancel">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" [disabled]="!roleForm.valid || roleForm.pristine">
{{ 'labels.buttons.Submit' | translate }}
<button type="submit" mat-flat-button color="primary" [disabled]="!roleForm.valid || roleForm.pristine" class="er-actions__submit">
<i class="fas fa-check"></i> {{ 'labels.buttons.Submit' | translate }}
</button>
</mat-card-actions>
</div>
</form>
</mat-card>
</div>
</div>
@@ -6,14 +6,128 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
.container {
max-width: 37rem;
$primary: #1353a0;
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-500: #9aa0a6;
$gray-700: #5f6368;
$gray-900: #202124;
.er-container {
max-width: 960px;
margin: 0 auto;
padding: 20px 16px;
}
span {
font-size: 1rem;
// Header
.er-header {
display: flex;
align-items: center;
gap: 14px;
margin-bottom: 20px;
&__title {
font-size: 18px;
font-weight: 600;
color: $gray-900;
margin: 0;
}
}
.roleName {
line-height: 3rem;
.er-back {
border-radius: 8px !important;
font-size: 12px !important;
font-weight: 600 !important;
height: 32px !important;
border-color: $border !important;
color: $gray-500 !important;
i { margin-right: 6px; }
&:hover { color: $primary !important; border-color: $primary !important; }
}
// Card
.er-card {
background: #fff;
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
}
// Section
.er-section {
&__header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 24px;
background: $gray-50;
border-bottom: 1px solid $gray-200;
i {
font-size: 14px;
color: $accent;
}
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: $gray-900;
}
}
& + & {
border-top: 1px solid $gray-200;
}
}
// Grid
.er-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 4px 16px;
padding: 16px 24px;
@media (max-width: 600px) { grid-template-columns: 1fr; }
}
.er-field {
width: 100%;
&--full {
grid-column: 1 / -1;
}
}
// Actions
.er-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 14px 24px;
border-top: 1px solid $gray-200;
background: $gray-50;
&__cancel {
border-radius: 8px !important;
font-size: 13px !important;
font-weight: 600 !important;
height: 36px !important;
border-color: $border !important;
color: $gray-700 !important;
}
&__submit {
border-radius: 8px !important;
font-size: 13px !important;
font-weight: 600 !important;
height: 36px !important;
box-shadow: 0 1px 3px rgba($primary, 0.3) !important;
i { margin-right: 6px; font-size: 12px; }
}
}
@@ -6,25 +6,35 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="container m-b-20 layout-row layout-align-end gap-20px" [routerLink]="['add']">
<div #buttonAddRole class="in-block">
<button mat-raised-button color="primary">
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add Role' | translate }}
</button>
</div>
</div>
<div class="container">
<div #filter class="layout-row gap-20px">
<mat-form-field class="flex-fill">
<mat-label>{{ 'labels.inputs.Filter' | translate }}</mat-label>
<input matInput (keyup)="applyFilter($event.target.value)" />
</mat-form-field>
<div class="table-header">
<div class="table-title-group">
<h2 class="table-title">
<i class="fas fa-key title-icon"></i>
{{ 'labels.inputs.Manage Roles and Permission' | translate }}
</h2>
</div>
<div class="table-actions">
<div #filter class="table-search">
<mat-form-field appearance="outline">
<i matPrefix class="fas fa-search"></i>
<mat-label>{{ 'labels.inputs.Filter' | translate }}</mat-label>
<input matInput (keyup)="applyFilter($event.target.value)" />
</mat-form-field>
</div>
<div class="table-buttons">
<div #buttonAddRole>
<button mat-flat-button color="primary" class="btn-create" [routerLink]="['add']">
<fa-icon icon="plus" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Add Role' | translate }}
</button>
</div>
</div>
</div>
</div>
<div #tableRolesandPermissions class="mat-elevation-z8">
<table mat-table [dataSource]="dataSource" matSort>
<div #tableRolesandPermissions class="table-container">
<table mat-table [dataSource]="dataSource" matSort class="modern-table">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header>{{ 'labels.inputs.Name' | translate }}</th>
<td mat-cell *matCellDef="let role">{{ role.name }}</td>
@@ -63,7 +73,7 @@
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" [routerLink]="[row.id]" class="select-row"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns" [routerLink]="[row.id]" class="table-row"></tr>
</table>
<mat-paginator [pageSizeOptions]="[10, 25, 50, 100]" showFirstLastButtons></mat-paginator>
@@ -6,6 +6,8 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
@use '../../shared/styles/list-layout' as *;
table {
width: 100%;
@@ -6,145 +6,253 @@
file, You can obtain one at http://mozilla.org/MPL/2.0/.
-->
<div class="layout-row align-end gap-2px responsive-column container m-b-0">
<button
mat-raised-button
color="primary"
(click)="editRoles(); backupCheckValues()"
[disabled]="rolePermissionService.name === 'Super user'"
>
<fa-icon icon="edit" class="m-r-10"></fa-icon>
{{ 'labels.text.Edit Role' | translate }}
</button>
@if (!isRoleEnable(rolePermissionService.disabled)) {
<button
mat-raised-button
color="danger"
(click)="disableRolesConfirmation()"
[disabled]="rolePermissionService.name === 'Super user'"
>
<fa-icon icon="lock" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Disable' | translate }} {{ 'labels.inputs.Role' | translate }}
</button>
}
@if (isRoleEnable(rolePermissionService.disabled)) {
<button
mat-raised-button
color="primary"
(click)="enableRolesConfirmation()"
[disabled]="rolePermissionService.name === 'Super user'"
>
<fa-icon icon="lock-open" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Enable' | translate }} {{ 'labels.inputs.Role' | translate }}
</button>
}
<button
mat-raised-button
color="warn"
(click)="deleteRole()"
[disabled]="rolePermissionService.name === 'Super user'"
>
<fa-icon icon="trash" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Delete' | translate }} {{ 'labels.inputs.Role' | translate }}
</button>
</div>
<div class="container m-b-0">
<mat-card class="mat-elevation-z8 role-details-card">
<mat-card-content>
<div class="layout-row-wrap">
<div class="flex-40 flex-lt-md-50 header">
<b>{{ 'labels.inputs.Name' | translate }}:</b>
</div>
<div class="flex-60 flex-lt-md-50">
{{ rolePermissionService.name }}
</div>
<div class="flex-40 flex-lt-md-50 header">
<b>{{ 'labels.inputs.Role Description' | translate }}:</b>
</div>
<div class="flex-60 flex-lt-md-50">
{{ rolePermissionService.description }}
</div>
<div class="vr-container">
<!-- Top Bar -->
<div class="vr-topbar">
<div class="vr-topbar__left">
<i class="fas fa-key vr-topbar__icon"></i>
<div>
<h2 class="vr-topbar__title">{{ rolePermissionService.name }}</h2>
@if (rolePermissionService.description) {
<span class="vr-topbar__desc">{{ rolePermissionService.description }}</span>
}
</div>
</mat-card-content>
</mat-card>
</div>
</div>
<div class="vr-topbar__actions">
<button mat-flat-button color="primary" class="vr-btn vr-btn--edit"
(click)="editRoles(); backupCheckValues()"
[disabled]="rolePermissionService.name === 'Super user'">
<fa-icon icon="edit" class="m-r-10"></fa-icon>
{{ 'labels.text.Edit Role' | translate }}
</button>
@if (!isRoleEnable(rolePermissionService.disabled)) {
<button mat-stroked-button class="vr-btn vr-btn--disable"
(click)="disableRolesConfirmation()"
[disabled]="rolePermissionService.name === 'Super user'">
<fa-icon icon="lock" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Disable' | translate }} {{ 'labels.inputs.Role' | translate }}
</button>
}
@if (isRoleEnable(rolePermissionService.disabled)) {
<button mat-flat-button color="primary" class="vr-btn vr-btn--enable"
(click)="enableRolesConfirmation()"
[disabled]="rolePermissionService.name === 'Super user'">
<fa-icon icon="lock-open" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Enable' | translate }} {{ 'labels.inputs.Role' | translate }}
</button>
}
<button mat-stroked-button class="vr-btn vr-btn--delete"
(click)="deleteRole()"
[disabled]="rolePermissionService.name === 'Super user'">
<fa-icon icon="trash" class="m-r-10"></fa-icon>
{{ 'labels.buttons.Delete' | translate }} {{ 'labels.inputs.Role' | translate }}
</button>
</div>
</div>
<div class="container m-b-20">
<mat-card>
<ng-container>
<div class="permission-selected layout-row">
<div class="flex-75">
<h3>
{{ 'labels.buttons.Permissions' | translate }}: <strong>{{ formatName(previousGrouping) }}</strong>
</h3>
<!-- Permissions Card -->
<div class="vr-card">
<div class="vr-card__header">
<i class="fas fa-shield-alt"></i>
<h3>{{ 'labels.buttons.Permissions' | translate }}: <strong>{{ formatName(previousGrouping) }}</strong></h3>
@if (!isDisabled) {
<div class="vr-card__header-actions">
<button mat-flat-button color="primary" class="vr-btn-sm" (click)="selectAll()">
{{ 'labels.buttons.Select All' | translate }}
</button>
<button mat-stroked-button class="vr-btn-sm vr-btn-sm--outline" (click)="deselectAll()">
{{ 'labels.buttons.Deselect All' | translate }}
</button>
</div>
}
</div>
@if (!isDisabled) {
<div class="flex-25 layout-row layout-xs-column layout-align-end gap-5px select-deselect">
<button mat-raised-button color="primary" (click)="selectAll()">
{{ 'labels.buttons.Select All' | translate }}
</button>
<button mat-raised-button color="default" (click)="deselectAll()">
{{ 'labels.buttons.Deselect All' | translate }}
</button>
<div class="vr-permissions">
<!-- Grouped Category List (Left) -->
<div class="vr-categories">
@for (group of roleGroups; track group.name) {
<div class="vr-group">
<div class="vr-group__header" (click)="toggleGroup(group)">
<i class="fas {{ group.icon }} vr-group__icon"></i>
<span class="vr-group__name">{{ group.name }}</span>
<span class="vr-group__count">{{ getGroupPermCount(group) }}</span>
@if (!isDisabled) {
<button class="vr-group__select-btn vr-group__select-btn--check" (click)="selectAllGroup(group); $event.stopPropagation()" title="Select all in {{ group.name }}">
<i class="fas fa-check-double"></i>
</button>
<button class="vr-group__select-btn vr-group__select-btn--clear" (click)="deselectAllGroup(group); $event.stopPropagation()" title="Deselect all in {{ group.name }}">
<i class="fas fa-times"></i>
</button>
}
<i class="fas vr-group__chevron" [class.fa-chevron-down]="group.expanded" [class.fa-chevron-right]="!group.expanded"></i>
</div>
@if (group.expanded) {
<div class="vr-group__items">
@for (cat of group.categories; track cat) {
<div class="vr-category-item"
[class.vr-category-item--active]="selectedItem === cat"
(click)="showPermissions(cat)">
<span class="vr-category-item__name">{{ formatName(cat) }}</span>
@if (!isDisabled) {
<button class="vr-category-item__sel" (click)="selectAllCategory(cat); $event.stopPropagation()" title="Select all">
<i class="fas fa-check"></i>
</button>
<button class="vr-category-item__desel" (click)="deselectAllCategory(cat); $event.stopPropagation()" title="Deselect all">
<i class="fas fa-times"></i>
</button>
}
<span class="vr-category-item__count">{{ getCatPermCount(cat) }}</span>
</div>
}
</div>
}
</div>
}
</div>
<div class="layout-row">
<div class="layout-column flex-30">
<mat-list>
@for (grouping of groupings; track grouping) {
<mat-list-item
[ngClass]="selectedItem === grouping ? 'active' : 'inactive'"
(click)="showPermissions(grouping)"
>
<span class="grouping-name">
{{ formatName(grouping) }}
</span>
</mat-list-item>
}
</mat-list>
</div>
<!-- Permission Checkboxes (Right) -->
<div class="vr-checkboxes">
<form [formGroup]="formGroup" (submit)="submit()">
<mat-divider [vertical]="true"></mat-divider>
<div class="flex-70 layout-column list-permission">
<form [formGroup]="formGroup" (submit)="submit()">
@for (permission of permissions.permissions; track permission) {
<div class="display-permissions">
<div formArrayName="roster">
<div [formGroupName]="permission.id">
<mat-checkbox name="cp" id="{{ permission.code }}" formControlName="selected">
{{ permissionName(permission.code) }}
</mat-checkbox>
</div>
<!-- Sub-grouped view for large categories -->
@if (permissionSubGroups.length > 0) {
@for (subGroup of permissionSubGroups; track subGroup.entity) {
<div class="vr-subgroup">
<div class="vr-subgroup__header" (click)="toggleSubGroup(subGroup)">
<i class="fas" [class.fa-chevron-down]="subGroup.expanded" [class.fa-chevron-right]="!subGroup.expanded"></i>
<span class="vr-subgroup__name">{{ getEntityDisplayName(subGroup.entity) }}</span>
<span class="vr-subgroup__count">{{ getSubGroupCount(subGroup) }}</span>
@if (!isDisabled) {
<button type="button" class="vr-subgroup__select" (click)="selectAllSubGroup(subGroup); $event.stopPropagation()" title="Select all">
<i class="fas fa-check-double"></i>
</button>
<button type="button" class="vr-subgroup__deselect" (click)="deselectAllSubGroup(subGroup); $event.stopPropagation()" title="Deselect all">
<i class="fas fa-times"></i>
</button>
}
</div>
@if (subGroup.expanded) {
<div class="vr-subgroup__columns">
<div class="vr-subgroup__col">
<div class="vr-subgroup__col-label">
<span>Maker</span>
@if (!isDisabled && subGroup.makerPerms.length > 0) {
<button type="button" class="vr-col-btn vr-col-btn--select" (click)="selectAllMaker(subGroup.makerPerms)" title="Select all maker">
<i class="fas fa-check"></i> All
</button>
<button type="button" class="vr-col-btn vr-col-btn--clear" (click)="deselectAllMaker(subGroup.makerPerms)" title="Deselect all maker">
<i class="fas fa-times"></i>
</button>
}
</div>
@for (permission of subGroup.makerPerms; track permission) {
<div class="vr-checkbox-item" formArrayName="roster">
<div [formGroupName]="permission.id">
<mat-checkbox name="cp" id="{{ permission.code }}" formControlName="selected">
{{ permissionName(permission.code) }}
</mat-checkbox>
</div>
</div>
}
@if (subGroup.makerPerms.length === 0) {
<span class="vr-subgroup__empty">—</span>
}
</div>
<div class="vr-subgroup__col vr-subgroup__col--checker">
<div class="vr-subgroup__col-label vr-subgroup__col-label--checker">
<span>Checker</span>
@if (!isDisabled && subGroup.checkerPerms.length > 0) {
<button type="button" class="vr-col-btn vr-col-btn--select-warn" (click)="selectAllChecker(subGroup.checkerPerms)" title="Select all checker">
<i class="fas fa-check"></i> All
</button>
<button type="button" class="vr-col-btn vr-col-btn--clear" (click)="deselectAllChecker(subGroup.checkerPerms)" title="Deselect all checker">
<i class="fas fa-times"></i>
</button>
}
</div>
@for (permission of subGroup.checkerPerms; track permission) {
<div class="vr-checkbox-item" formArrayName="roster">
<div [formGroupName]="permission.id">
<mat-checkbox name="cp" id="{{ permission.code }}" formControlName="selected">
{{ permissionName(permission.code) }}
</mat-checkbox>
</div>
</div>
}
@if (subGroup.checkerPerms.length === 0) {
<span class="vr-subgroup__empty">—</span>
}
</div>
</div>
}
</div>
}
</form>
</div>
} @else {
<!-- Flat maker/checker columns for small categories -->
<div class="vr-flat-columns">
<div class="vr-flat-col">
<div class="vr-flat-col__label">
<span>Maker</span>
@if (!isDisabled && flatMakerPerms.length > 0) {
<button type="button" class="vr-col-btn vr-col-btn--select" (click)="selectAllMaker(flatMakerPerms)" title="Select all maker">
<i class="fas fa-check"></i> All
</button>
<button type="button" class="vr-col-btn vr-col-btn--clear" (click)="deselectAllMaker(flatMakerPerms)" title="Deselect all">
<i class="fas fa-times"></i>
</button>
}
</div>
@for (permission of flatMakerPerms; track permission) {
<div class="vr-checkbox-item" formArrayName="roster">
<div [formGroupName]="permission.id">
<mat-checkbox name="cp" id="{{ permission.code }}" formControlName="selected">
{{ permissionName(permission.code) }}
</mat-checkbox>
</div>
</div>
}
</div>
<div class="vr-flat-col vr-flat-col--checker">
<div class="vr-flat-col__label vr-flat-col__label--checker">
<span>Checker</span>
@if (!isDisabled && flatCheckerPerms.length > 0) {
<button type="button" class="vr-col-btn vr-col-btn--select-warn" (click)="selectAllChecker(flatCheckerPerms)" title="Select all checker">
<i class="fas fa-check"></i> All
</button>
<button type="button" class="vr-col-btn vr-col-btn--clear" (click)="deselectAllChecker(flatCheckerPerms)" title="Deselect all">
<i class="fas fa-times"></i>
</button>
}
</div>
@for (permission of flatCheckerPerms; track permission) {
<div class="vr-checkbox-item" formArrayName="roster">
<div [formGroupName]="permission.id">
<mat-checkbox name="cp" id="{{ permission.code }}" formControlName="selected">
{{ permissionName(permission.code) }}
</mat-checkbox>
</div>
</div>
}
@if (flatCheckerPerms.length === 0) {
<span class="vr-subgroup__empty">No checker permissions</span>
}
</div>
</div>
}
</form>
</div>
</ng-container>
</div>
@if (!isDisabled) {
<div class="layout-row layout-align-center gap-10px layout-xs-column">
<mat-card-actions>
<button type="button" mat-raised-button (click)="cancel(); restoreCheckboxes()">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-raised-button color="primary" (click)="submit()">
{{ 'labels.buttons.Submit' | translate }}
</button>
</mat-card-actions>
<div class="vr-card__footer">
<button type="button" mat-stroked-button class="vr-btn-action vr-btn-action--cancel" (click)="cancel(); restoreCheckboxes()">
{{ 'labels.buttons.Cancel' | translate }}
</button>
<button mat-flat-button color="primary" class="vr-btn-action vr-btn-action--submit" (click)="submit()">
<i class="fas fa-check"></i> {{ 'labels.buttons.Submit' | translate }}
</button>
</div>
}
</mat-card>
</div>
</div>
@@ -6,108 +6,620 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
@use '../../../../assets/styles/colours' as *;
$primary: #1353a0;
$accent: #0ea5e9;
$border: #e2e8f0;
$gray-50: #f8f9fa;
$gray-200: #e8eaed;
$gray-500: #9aa0a6;
$gray-700: #5f6368;
$gray-900: #202124;
.container {
padding: 1rem;
// Container
.vr-container {
max-width: 1100px;
margin: 0 auto;
padding: 20px 16px;
}
mat-card {
margin-bottom: 0.5rem;
padding: 1.5rem;
box-shadow: 0 2px 4px rgba($black, 0.1);
border: 1px solid var(--border-color, $mid-grey);
border-radius: 8px;
transition:
background-color 0.3s ease,
border-color 0.3s ease;
// Top Bar
.vr-topbar {
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
margin-bottom: 20px;
flex-wrap: wrap;
mat-card-content {
padding: 0;
}
.layout-row {
margin: 0;
padding: 0;
}
&.role-details-card {
padding: 0.5rem;
}
&__left {
display: flex;
align-items: center;
gap: 14px;
}
}
.display-permissions {
padding-top: 15px;
padding-bottom: 8px;
}
&__icon {
font-size: 18px;
color: $accent;
}
span {
font-size: 1rem;
}
mat-list-item {
cursor: pointer;
padding: 8px 0;
}
.mat-list-base .mat-list-item .mat-list-item-content {
cursor: pointer;
padding: 0 16px;
}
.grouping-name {
padding: 0 10px;
}
.list-permission {
padding-left: 24px;
padding-right: 16px;
}
.permission-selected {
margin-top: 0;
margin-bottom: 16px;
height: auto;
min-height: 40px;
padding-bottom: 8px;
h3 {
&__title {
font-size: 18px;
font-weight: 600;
color: $gray-900;
margin: 0;
font-size: 1.25rem;
font-weight: 500;
line-height: 1.2;
}
&__desc {
font-size: 12px;
color: $gray-500;
font-weight: 400;
}
&__actions {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
}
.inactive {
transition: all 0.2s ease-in-out;
// Top bar buttons
.vr-btn {
border-radius: 8px !important;
font-size: 12px !important;
font-weight: 600 !important;
height: 34px !important;
&--edit {
box-shadow: 0 1px 3px rgba($primary, 0.3) !important;
}
&--disable {
border-color: #f97316 !important;
color: #f97316 !important;
&:hover {
background: rgba(#f97316, 0.05) !important;
}
}
&--enable {
box-shadow: 0 1px 3px rgba($primary, 0.3) !important;
}
&--delete {
border-color: #ef4444 !important;
color: #ef4444 !important;
&:hover {
background: rgba(#ef4444, 0.05) !important;
}
}
}
// Section Cards
.vr-card {
background: #fff;
border: 1px solid $border;
border-radius: 10px;
overflow: hidden;
margin-bottom: 16px;
&__header {
display: flex;
align-items: center;
gap: 10px;
padding: 12px 24px;
background: $gray-50;
border-bottom: 1px solid $gray-200;
i {
font-size: 14px;
color: $accent;
}
h3 {
margin: 0;
font-size: 14px;
font-weight: 600;
color: $gray-900;
flex: 1;
}
}
&__header-actions {
display: flex;
gap: 8px;
margin-left: auto;
flex-shrink: 0;
}
&__footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 14px 24px;
border-top: 1px solid $gray-200;
background: $gray-50;
}
}
// Role Details
.vr-details {
padding: 16px 24px;
}
.vr-detail-row {
display: flex;
align-items: baseline;
padding: 10px 0;
border-bottom: 1px solid #f1f5f9;
&:last-child {
border-bottom: none;
}
&__label {
width: 200px;
flex-shrink: 0;
font-size: 12px;
font-weight: 600;
color: #94a3b8;
text-transform: uppercase;
letter-spacing: 0.3px;
}
&__value {
font-size: 14px;
font-weight: 500;
color: #1e293b;
line-height: 1.4;
}
}
// Permissions Split Layout
.vr-permissions {
display: flex;
min-height: 500px;
}
// Category List (Left)
.vr-categories {
width: 240px;
min-width: 200px;
flex-shrink: 0;
border-right: 1px solid $border;
overflow-y: auto;
background: #fafbfc;
max-height: none;
}
// Role Group
.vr-group {
border-bottom: 1px solid #f1f3f5;
&:last-child { border-bottom: none; }
&__header {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 14px;
cursor: pointer;
transition: background 0.15s;
user-select: none;
&:hover { background: #f0f4f8; }
}
&__icon {
font-size: 12px;
color: $accent;
width: 16px;
text-align: center;
}
&__name {
flex: 1;
font-size: 11px;
font-weight: 700;
color: $gray-900;
text-transform: uppercase;
letter-spacing: 0.04em;
}
&__count {
font-size: 10px;
font-weight: 600;
color: #fff;
background: $accent;
border-radius: 10px;
padding: 1px 7px;
min-width: 20px;
text-align: center;
flex-shrink: 0;
}
&__select-btn {
background: none;
border: 1px solid $border;
border-radius: 4px;
width: 20px;
height: 20px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 8px;
transition: all 0.15s;
flex-shrink: 0;
&--check {
color: $accent;
&:hover { background: rgba($accent, 0.1); border-color: $accent; }
}
&--clear {
color: $gray-500;
&:hover { background: rgba(#ef4444, 0.05); border-color: #ef4444; color: #ef4444; }
}
}
&__chevron {
font-size: 9px;
color: $gray-500;
flex-shrink: 0;
transition: transform 0.2s;
}
&__items {
padding-bottom: 4px;
}
}
.vr-category-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 7px 14px 7px 38px;
font-size: 12px;
font-weight: 500;
color: $gray-700;
cursor: pointer;
border-left: 3px solid transparent;
transition: all 0.15s ease;
&__name {
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__sel, &__desel {
background: none;
border: 1px solid transparent;
border-radius: 3px;
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 7px;
flex-shrink: 0;
opacity: 0;
transition: all 0.15s;
}
&:hover &__sel, &:hover &__desel { opacity: 1; }
&__sel {
color: $accent;
&:hover { background: rgba($accent, 0.1); border-color: $accent; }
}
&__desel {
color: $gray-500;
&:hover { background: rgba(#ef4444, 0.05); border-color: #ef4444; color: #ef4444; }
}
&__count {
font-size: 10px;
color: $gray-500;
background: #f1f3f5;
border-radius: 8px;
padding: 1px 6px;
flex-shrink: 0;
}
&:hover {
transform: scale(1.01);
background-color: $light-grey;
background: #f0f4f8;
color: $gray-900;
}
&--active {
background: #f0f9ff;
border-left: 3px solid $accent;
color: $accent;
font-weight: 600;
}
}
.content {
div {
line-height: 2.5rem;
margin-bottom: 0.5rem;
// Permission Checkboxes (Right)
.vr-checkboxes {
flex: 1;
padding: 12px 20px;
overflow-y: auto;
max-height: none;
display: grid;
grid-template-columns: 1fr;
align-content: start;
&.header {
font-weight: 500;
padding: 0.5rem 0;
form {
display: flex;
flex-direction: column;
gap: 0;
}
}
// Sub-groups for large categories
.vr-subgroup {
border: 1px solid #f1f3f5;
border-radius: 8px;
overflow: hidden;
grid-column: 1 / -1;
& + & { margin-top: 6px; }
&__header {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: #f8fafc;
cursor: pointer;
transition: background 0.15s;
user-select: none;
&:hover { background: #f0f4f8; }
> i { font-size: 9px; color: $gray-500; width: 12px; }
}
&__name {
flex: 1;
font-size: 12.5px;
font-weight: 600;
color: $gray-900;
}
&__count {
font-size: 10px;
font-weight: 600;
color: $accent;
background: rgba($accent, 0.1);
border-radius: 8px;
padding: 1px 7px;
flex-shrink: 0;
}
&__select, &__deselect {
background: none;
border: 1px solid $border;
border-radius: 4px;
width: 22px;
height: 22px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
font-size: 9px;
transition: all 0.15s;
flex-shrink: 0;
}
&__select {
color: $accent;
&:hover { background: rgba($accent, 0.1); border-color: $accent; }
}
&__deselect {
color: $gray-500;
&:hover { background: rgba(#ef4444, 0.05); border-color: #ef4444; color: #ef4444; }
}
&__columns {
display: flex;
border-top: 1px solid #f1f3f5;
}
&__col {
flex: 1;
padding: 8px 12px;
&--checker {
border-left: 1px solid #f1f3f5;
background: #fdfefe;
}
}
&__col-label {
display: flex;
align-items: center;
gap: 6px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: $accent;
padding: 2px 0 6px;
border-bottom: 1px solid #f1f3f5;
margin-bottom: 4px;
span { flex: 1; }
&--checker { color: #f59e0b; }
}
&__empty {
font-size: 11px;
color: $gray-500;
padding: 6px 0;
display: block;
}
}
// Column select/deselect buttons
.vr-col-btn {
background: none;
border: 1px solid $border;
border-radius: 4px;
padding: 1px 6px;
font-size: 9px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
display: inline-flex;
align-items: center;
gap: 3px;
flex-shrink: 0;
&--select {
color: $accent;
&:hover { background: rgba($accent, 0.1); border-color: $accent; }
}
&--select-warn {
color: #f59e0b;
&:hover { background: rgba(#f59e0b, 0.1); border-color: #f59e0b; }
}
&--clear {
color: $gray-500;
&:hover { background: rgba(#ef4444, 0.05); border-color: #ef4444; color: #ef4444; }
}
}
// Flat maker/checker columns for small categories
.vr-flat-columns {
display: flex;
width: 100%;
}
.vr-flat-col {
flex: 1;
padding: 4px 0;
&--checker {
border-left: 1px solid #f1f3f5;
padding-left: 16px;
background: #fdfefe;
}
&__label {
display: flex;
align-items: center;
gap: 6px;
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: $accent;
padding: 2px 0 6px;
border-bottom: 1px solid #f1f3f5;
margin-bottom: 4px;
span { flex: 1; }
&--checker { color: #f59e0b; }
}
}
.vr-checkbox-item {
padding: 3px 0;
::ng-deep .mat-mdc-checkbox {
.mdc-label {
font-size: 11.5px;
color: $gray-700;
letter-spacing: 0.01em;
}
}
}
mat-card-actions {
padding: 16px 0 0;
margin: 0;
// Select/Deselect buttons in header
.vr-btn-sm {
border-radius: 6px !important;
font-size: 11px !important;
font-weight: 600 !important;
height: 28px !important;
padding: 0 12px !important;
min-width: auto !important;
button {
margin-right: 8px;
&--outline {
border-color: $border !important;
color: $gray-700 !important;
}
}
&:last-child {
margin-right: 0;
// Footer action buttons
.vr-btn-action {
border-radius: 8px !important;
font-size: 13px !important;
font-weight: 600 !important;
height: 36px !important;
&--cancel {
border-color: $border !important;
color: $gray-700 !important;
}
&--submit {
box-shadow: 0 1px 3px rgba($primary, 0.3) !important;
i { margin-right: 6px; font-size: 12px; }
}
}
// Responsive
@media (max-width: 768px) {
.vr-topbar {
flex-direction: column;
align-items: flex-start;
&__actions {
width: 100%;
}
}
.vr-permissions {
flex-direction: column;
}
.vr-categories {
width: 100%;
min-width: 0;
border-right: none;
border-bottom: 1px solid $border;
max-height: 220px;
}
.vr-checkboxes {
width: 100%;
form {
grid-template-columns: 1fr;
}
}
.vr-detail-row {
flex-direction: column;
gap: 4px;
&__label {
width: auto;
}
}
}
@@ -20,9 +20,6 @@ import { DeleteDialogComponent } from '../../../shared/delete-dialog/delete-dial
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 { NgClass } from '@angular/common';
import { MatList, MatListItem } from '@angular/material/list';
import { MatDivider } from '@angular/material/divider';
import { MatCheckbox } from '@angular/material/checkbox';
import { STANDALONE_SHARED_IMPORTS } from 'app/standalone-shared.module';
@@ -40,10 +37,6 @@ import { AuthService } from 'app/zitadel/auth.service';
imports: [
...STANDALONE_SHARED_IMPORTS,
FaIconComponent,
MatList,
MatListItem,
NgClass,
MatDivider,
MatCheckbox
]
})
@@ -66,6 +59,65 @@ export class ViewRoleComponent implements OnInit {
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 */
@@ -88,6 +140,18 @@ export class ViewRoleComponent implements OnInit {
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 */
/**
@@ -114,6 +178,7 @@ export class ViewRoleComponent implements OnInit {
};
this.createForm();
this.groupRules();
this.buildRoleGroups();
this.selectedItem = 'special';
this.showPermissions('special');
this.route.params.subscribe((routeParams: any) => {
@@ -170,6 +235,285 @@ export class ViewRoleComponent implements OnInit {
}
}
/**
* 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
@@ -178,6 +522,7 @@ export class ViewRoleComponent implements OnInit {
this.permissions = this.tempPermissionUIData[grouping];
this.selectedItem = grouping;
this.previousGrouping = grouping;
this.buildSubGroups();
}
/**