Implement organization maker-checker workflows

This commit is contained in:
2026-09-24 17:22:50 +05:30
parent 08a427467a
commit 7b9a6394c8
30 changed files with 787 additions and 90 deletions
@@ -7,6 +7,26 @@
-->
<div class="layout-row align-end gap-2px responsive-column container m-b-20">
<mat-form-field class="mc-search-field">
<input
matInput
type="text"
placeholder="Search permissions..."
[value]="searchTerm"
(input)="onSearchTermChange($any($event.target).value)"
/>
@if (isSearchActive) {
<button
class="mc-clear-search"
matSuffix
type="button"
(click)="clearSearch()"
[attr.aria-label]="'Clear search'"
>
<fa-icon icon="times"></fa-icon>
</button>
}
</mat-form-field>
<div #buttonEdit class="in-block">
<button
mat-raised-button
@@ -41,14 +61,20 @@
<mat-divider [vertical]="true"></mat-divider>
<div class="list-permission layout-column flex-70">
@if (isSearchActive && displayedPermissions.length === 0) {
<p class="no-results">No permissions found.</p>
}
<form [formGroup]="formGroup" (submit)="submit()">
<div formArrayName="roster">
@for (permission of permissions.permissions; track permission; let i = $index) {
@for (permission of displayedPermissions; track permission.code) {
<div class="display-permissions">
<div [formGroupName]="i">
<div [formGroupName]="permission.globalIndex">
<mat-checkbox name="cp" id="{{ permission.code }}" formControlName="selected">
{{ permissionName(permission.code) }}
</mat-checkbox>
@if (isSearchActive) {
<span class="search-result-grouping">({{ formatName(permission.grouping || '') }})</span>
}
</div>
</div>
}
@@ -46,3 +46,32 @@ mat-list-item {
.inactive:hover {
transform: scale(1.1);
}
.mc-search-field {
width: 260px;
margin-right: 12px;
}
.mc-clear-search {
border: none;
background: transparent;
cursor: pointer;
color: inherit;
opacity: 0.6;
}
.mc-clear-search:hover {
opacity: 1;
}
.no-results {
padding-top: 15px;
font-style: italic;
opacity: 0.7;
}
.search-result-grouping {
padding-left: 6px;
font-size: 0.85rem;
opacity: 0.6;
}
@@ -27,6 +27,11 @@ interface Permission {
id: number;
selected: boolean;
grouping?: string;
/** Index of this permission's control within the `roster` FormArray (which is built from the
* full, globally-ordered permission list). Needed because the checkbox list on screen is
* rendered from a per-grouping filtered subset, whose own local index does not match the
* roster's global index. */
globalIndex?: number;
}
interface PermissionGroup {
@@ -70,6 +75,12 @@ export class ConfigureMakerCheckerTasksComponent implements OnInit, AfterViewIni
currentGrouping = '';
tempPermissionUIData: PermissionData = {};
permissions: PermissionGroup = { permissions: [] };
/** Flat list of every permission across all groupings, built once alongside
* tempPermissionUIData. Used only for global search - it is a read-only view over the same
* Permission objects (each still carrying its correct globalIndex), never a separate copy
* that could drift out of sync with the roster FormArray. */
allPermissions: Permission[] = [];
searchTerm = '';
formData = {};
isDisabled = true;
@@ -137,22 +148,30 @@ export class ConfigureMakerCheckerTasksComponent implements OnInit, AfterViewIni
setMakerCheckerTask(): void {
this.tempPermissionUIData = {};
this.allPermissions = [];
for (const permission of this.permissionsData) {
this.permissionsData.forEach((permission: any, globalIndex: number) => {
if (permission.grouping !== this.currentGrouping) {
this.currentGrouping = permission.grouping || '';
this.groupings.push(this.currentGrouping);
this.tempPermissionUIData[this.currentGrouping] = { permissions: [] };
}
// globalIndex matches this permission's position in `this.formGroup.controls.roster`,
// since both this list and `roster` are built from the same `this.permissionsData` array
// in the same order (see createForm()). The checkbox must address the roster control by
// this global index, never by its local position within the current grouping's filtered view.
const temp: Permission = {
code: permission.code,
id: permission.id,
selected: permission.selected
selected: permission.selected,
grouping: this.currentGrouping,
globalIndex
};
this.tempPermissionUIData[this.currentGrouping].permissions.push(temp);
}
this.allPermissions.push(temp);
});
}
showPermissions(grouping: string): void {
@@ -164,6 +183,38 @@ export class ConfigureMakerCheckerTasksComponent implements OnInit, AfterViewIni
}
}
/**
* The permissions currently rendered as checkboxes. While a search term is active this is a
* global, cross-grouping match over `allPermissions`; otherwise it is the normal per-grouping
* view. Either way each item still carries its original globalIndex, so the checkbox template
* always resolves the same roster control regardless of which list produced it.
*/
get displayedPermissions(): Permission[] {
if (!this.isSearchActive) {
return this.permissions.permissions;
}
const term = this.searchTerm.trim().toLowerCase();
return this.allPermissions.filter((permission) => this.matchesSearch(permission, term));
}
get isSearchActive(): boolean {
return this.searchTerm.trim().length > 0;
}
onSearchTermChange(value: string): void {
this.searchTerm = value;
}
clearSearch(): void {
this.searchTerm = '';
}
private matchesSearch(permission: Permission, term: string): boolean {
const readableName = permission.code.replace(/_/g, ' ').toLowerCase();
const haystack = `${permission.code.toLowerCase()} ${readableName}`;
return haystack.includes(term);
}
permissionName = function (name: any) {
name = name || '';
// replace '_' with ' '