refs #1446 Refactor GroupsComponent: Improve loading state management
testing/ogGui-multibranch/pipeline/head This commit looks good Details

pull/16/head
Lucas Lara García 2025-02-05 12:24:06 +01:00
parent d9f3fd6203
commit 3bd923cbbb
4 changed files with 389 additions and 379 deletions

View File

@ -13,7 +13,8 @@
matTooltip="{{ 'newOrganizationalUnitTooltip' | translate }}" matTooltipShowDelay="1000"> matTooltip="{{ 'newOrganizationalUnitTooltip' | translate }}" matTooltipShowDelay="1000">
{{ 'newOrganizationalUnitButton' | translate }} {{ 'newOrganizationalUnitButton' | translate }}
</button> </button>
<button mat-flat-button color="primary" [matMenuTriggerFor]="menuClients">{{ 'newClientButton' | translate }}</button> <button mat-flat-button color="primary" [matMenuTriggerFor]="menuClients">{{ 'newClientButton' | translate
}}</button>
<mat-menu #menuClients="matMenu"> <mat-menu #menuClients="matMenu">
<button mat-menu-item (click)="addClient($event)">{{ 'newSingleClientButton' | translate }}</button> <button mat-menu-item (click)="addClient($event)">{{ 'newSingleClientButton' | translate }}</button>
<button mat-menu-item (click)="addMultipleClients($event)">{{ 'newMultipleClientButton' | translate }}</button> <button mat-menu-item (click)="addMultipleClients($event)">{{ 'newMultipleClientButton' | translate }}</button>
@ -26,32 +27,28 @@
</div> </div>
</div> </div>
<div *ngIf="initialLoading; else contentTemplate">
<app-loading [isLoading]="initialLoading"></app-loading>
</div>
<ng-template #contentTemplate>
<!-- Filters Panel --> <!-- Filters Panel -->
<div class="filters-panel" joyrideStep="filtersPanelStep" text="{{ 'filtersPanelStepText' | translate }}"> <div class="filters-panel" joyrideStep="filtersPanelStep" text="{{ 'filtersPanelStepText' | translate }}">
<div class="filters-container"> <div class="filters-container">
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>{{ 'searchTree' | translate }}</mat-label> <mat-label>{{ 'searchTree' | translate }}</mat-label>
<input matInput #treeSearchInput (input)="onTreeFilterInput($event)" placeholder="Centro, aula, grupos ..." /> <input matInput #treeSearchInput (input)="onTreeFilterInput($event)" placeholder="Centro, aula, grupos ..." />
<button <button *ngIf="treeSearchInput.value" mat-icon-button matSuffix aria-label="Clear tree search"
*ngIf="treeSearchInput.value" (click)="clearTreeSearch(treeSearchInput)">
mat-icon-button
matSuffix
aria-label="Clear tree search"
(click)="clearTreeSearch(treeSearchInput)"
>
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
</button> </button>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="outline"> <mat-form-field appearance="outline">
<mat-label>{{ 'searchClient' | translate }}</mat-label> <mat-label>{{ 'searchClient' | translate }}</mat-label>
<input matInput #clientSearchInput (input)="onClientFilterInput($event)" placeholder="Nombre, IP, estado o MAC" /> <input matInput #clientSearchInput (input)="onClientFilterInput($event)"
<button placeholder="Nombre, IP, estado o MAC" />
*ngIf="clientSearchInput.value" <button *ngIf="clientSearchInput.value" mat-icon-button matSuffix aria-label="Clear client search"
mat-icon-button (click)="clearClientSearch(clientSearchInput)">
matSuffix
aria-label="Clear client search"
(click)="clearClientSearch(clientSearchInput)"
>
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
</button> </button>
</mat-form-field> </mat-form-field>
@ -81,8 +78,10 @@
<!-- Tree view --> <!-- Tree view -->
<div class="tree-container"> <div class="tree-container">
<mat-tree [dataSource]="treeDataSource" [treeControl]="treeControl"> <mat-tree [dataSource]="treeDataSource" [treeControl]="treeControl">
<mat-tree-node [ngClass]="{'selected-node': selectedNode?.id === node.id}" *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding (click)="onNodeClick(node)"> <mat-tree-node [ngClass]="{'selected-node': selectedNode?.id === node.id}"
<button mat-icon-button matTreeNodeToggle [disabled]="!node.expandable" [ngClass]="{'disabled-toggle': !node.expandable}"> *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding (click)="onNodeClick(node)">
<button mat-icon-button matTreeNodeToggle [disabled]="!node.expandable"
[ngClass]="{'disabled-toggle': !node.expandable}">
<mat-icon>{{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}</mat-icon> <mat-icon>{{ treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right' }}</mat-icon>
</button> </button>
<mat-icon class="node-icon {{ node.type }}"> <mat-icon class="node-icon {{ node.type }}">
@ -100,7 +99,8 @@
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>
</button> </button>
</mat-tree-node> </mat-tree-node>
<mat-tree-node [ngClass]="{'selected-node': selectedNode?.id === node.id}" *matTreeNodeDef="let node; when: isLeafNode" matTreeNodePadding (click)="onNodeClick(node)"> <mat-tree-node [ngClass]="{'selected-node': selectedNode?.id === node.id}"
*matTreeNodeDef="let node; when: isLeafNode" matTreeNodePadding (click)="onNodeClick(node)">
<button mat-icon-button matTreeNodeToggle [disabled]="true" class="disabled-toggle"></button> <button mat-icon-button matTreeNodeToggle [disabled]="true" class="disabled-toggle"></button>
<mat-icon style="color: green;"> <mat-icon style="color: green;">
{{ {{
@ -165,15 +165,13 @@
<!-- Clients view --> <!-- Clients view -->
<div class="clients-container"> <div class="clients-container">
<div class="clients-view-header"> <div class="clients-view-header">
<span class="clients-title-name">{{ 'clients' | translate }} <span [ngStyle]="{ visibility: isLoadingClients ? 'hidden' : 'visible' }" class="clients-title-name">
{{ 'clients' | translate }}
<strong>{{ selectedNode?.name }}</strong> <strong>{{ selectedNode?.name }}</strong>
</span> </span>
<div class="view-type-container"> <div class="view-type-container">
<app-execute-command <app-execute-command [clientData]="arrayClients" [buttonType]="'text'"
[clientData]="arrayClients" [buttonText]="'Ejecutar comandos'"></app-execute-command>
[buttonType]="'text'"
[buttonText]="'Ejecutar comandos'"
></app-execute-command>
<button mat-button color="primary" (click)="toggleView('card')" [disabled]="currentView === 'card'"> <button mat-button color="primary" (click)="toggleView('card')" [disabled]="currentView === 'card'">
<mat-icon>grid_view</mat-icon> {{ 'Vista Tarjeta' | translate }} <mat-icon>grid_view</mat-icon> {{ 'Vista Tarjeta' | translate }}
</button> </button>
@ -183,14 +181,17 @@
</div> </div>
</div> </div>
<div *ngIf="(selectedClients.data?.length || 0) > 0; else noClientsTemplate"> <div *ngIf="isLoadingClients">
<app-loading [isLoading]="isLoadingClients"></app-loading>
</div>
<div *ngIf="!isLoadingClients">
<div *ngIf="hasClients; else noClientsTemplate">
<!-- Cards view --> <!-- Cards view -->
<div class="clients-grid" *ngIf="currentView === 'card'"> <div class="clients-grid" *ngIf="currentView === 'card'">
<div *ngFor="let client of selectedClients.data" class="client-item"> <div *ngFor="let client of selectedClients.data" class="client-item">
<div class="client-card"> <div class="client-card">
<img <img [src]="'assets/images/ordenador_' + client.status + '.png'" alt="Client Icon"
[src]="'assets/images/ordenador_' + client.status + '.png'"
alt="Client Icon"
class="client-image" /> class="client-image" />
<div class="client-details"> <div class="client-details">
@ -199,27 +200,20 @@
<span class="client-ip">{{ client.mac }}</span> <span class="client-ip">{{ client.mac }}</span>
<div class="action-icons"> <div class="action-icons">
<button <button *ngIf="(!syncStatus || syncingClientId !== client.uuid)" mat-icon-button color="primary"
*ngIf="(!syncStatus || syncingClientId !== client.uuid)"
mat-icon-button color="primary"
(click)="getStatus(client, selectedNode)"> (click)="getStatus(client, selectedNode)">
<mat-icon>sync</mat-icon> <mat-icon>sync</mat-icon>
</button> </button>
<button <button *ngIf="syncStatus && syncingClientId === client.uuid" mat-icon-button color="primary">
*ngIf="syncStatus && syncingClientId === client.uuid"
mat-icon-button color="primary">
<mat-spinner diameter="24"></mat-spinner> <mat-spinner diameter="24"></mat-spinner>
</button> </button>
<button mat-icon-button color="primary" (click)="onShowClientDetail($event, client)"> <button mat-icon-button color="primary" (click)="onShowClientDetail($event, client)">
<mat-icon>visibility</mat-icon> <mat-icon>visibility</mat-icon>
</button> </button>
<app-execute-command <app-execute-command [clientData]="[client]" [buttonType]="'icon'"
[clientData]="[client]" [icon]="'terminal'"></app-execute-command>
[buttonType]="'icon'"
[icon]="'terminal'"
></app-execute-command>
</div> </div>
</div> </div>
</div> </div>
@ -232,26 +226,20 @@
<th mat-header-cell *matHeaderCellDef> <th mat-header-cell *matHeaderCellDef>
<mat-checkbox (change)="$event ? toggleAllRows() : null" <mat-checkbox (change)="$event ? toggleAllRows() : null"
[checked]="selection.hasValue() && isAllSelected()" [checked]="selection.hasValue() && isAllSelected()"
[indeterminate]="selection.hasValue() && !isAllSelected()" [indeterminate]="selection.hasValue() && !isAllSelected()">
>
</mat-checkbox> </mat-checkbox>
</th> </th>
<td mat-cell *matCellDef="let row"> <td mat-cell *matCellDef="let row">
<mat-checkbox (click)="$event.stopPropagation()" <mat-checkbox (click)="$event.stopPropagation()" (change)="toggleRow(row)"
(change)="toggleRow(row)" [checked]="selection.isSelected(row)" [disabled]="row.status === 'busy'">
[checked]="selection.isSelected(row)"
[disabled]="row.status === 'busy'"
>
</mat-checkbox> </mat-checkbox>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="status"> <ng-container matColumnDef="status">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'status' | translate }} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'status' | translate }} </th>
<td mat-cell *matCellDef="let client" matTooltip="{{ getClientPath(client) }}" <td mat-cell *matCellDef="let client" matTooltip="{{ getClientPath(client) }}" matTooltipPosition="left"
matTooltipPosition="left" matTooltipShowDelay="500"> matTooltipShowDelay="500">
<img <img [src]="'assets/images/ordenador_' + client.status + '.png'" alt="Client Icon"
[src]="'assets/images/ordenador_' + client.status + '.png'"
alt="Client Icon"
class="client-image" /> class="client-image" />
</td> </td>
</ng-container> </ng-container>
@ -259,24 +247,20 @@
<ng-container matColumnDef="sync"> <ng-container matColumnDef="sync">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'sync' | translate }} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'sync' | translate }} </th>
<td mat-cell *matCellDef="let client"> <td mat-cell *matCellDef="let client">
<button <button *ngIf="(!syncStatus || syncingClientId !== client.uuid)" mat-icon-button color="primary"
*ngIf="(!syncStatus || syncingClientId !== client.uuid)"
mat-icon-button color="primary"
(click)="getStatus(client, selectedNode)"> (click)="getStatus(client, selectedNode)">
<mat-icon>sync</mat-icon> <mat-icon>sync</mat-icon>
</button> </button>
<button <button *ngIf="syncStatus && syncingClientId === client.uuid" mat-icon-button color="primary">
*ngIf="syncStatus && syncingClientId === client.uuid"
mat-icon-button color="primary">
<mat-spinner diameter="24"></mat-spinner> <mat-spinner diameter="24"></mat-spinner>
</button> </button>
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="name"> <ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'name' | translate }} </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> {{ 'name' | translate }} </th>
<td mat-cell *matCellDef="let client" matTooltip="{{ getClientPath(client) }}" <td mat-cell *matCellDef="let client" matTooltip="{{ getClientPath(client) }}" matTooltipPosition="left"
matTooltipPosition="left" matTooltipShowDelay="500"> matTooltipShowDelay="500">
<div class="client-info"> <div class="client-info">
<div class="client-name">{{ client.name }}</div> <div class="client-name">{{ client.name }}</div>
<div class="client-ip">{{ client.ip }}</div> <div class="client-ip">{{ client.ip }}</div>
@ -286,7 +270,8 @@
</ng-container> </ng-container>
<ng-container matColumnDef="oglive"> <ng-container matColumnDef="oglive">
<th mat-header-cell *matHeaderCellDef mat-sort-header> OG Live </th> <th mat-header-cell *matHeaderCellDef mat-sort-header> OG Live </th>
<td mat-cell *matCellDef="let client"> {{ (client.ogLive?.filename || '').slice(0, 15) }}{{ (client.ogLive?.filename?.length > 15) ? '...' : '' }} </td> <td mat-cell *matCellDef="let client"> {{ (client.ogLive?.filename || '').slice(0, 15) }}{{
(client.ogLive?.filename?.length > 15) ? '...' : '' }} </td>
</ng-container> </ng-container>
<ng-container matColumnDef="maintenace"> <ng-container matColumnDef="maintenace">
@ -313,11 +298,8 @@
<button mat-icon-button [matMenuTriggerFor]="clientMenu"> <button mat-icon-button [matMenuTriggerFor]="clientMenu">
<mat-icon>more_vert</mat-icon> <mat-icon>more_vert</mat-icon>
</button> </button>
<app-execute-command <app-execute-command [clientData]="[client]" [buttonType]="'icon'"
[clientData]="[client]" [icon]="'terminal'"></app-execute-command>
[buttonType]="'icon'"
[icon]="'terminal'"
></app-execute-command>
<mat-menu #clientMenu="matMenu"> <mat-menu #clientMenu="matMenu">
<button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)"> <button mat-menu-item (click)="onEditClick($event, client.type, client.uuid)">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
@ -340,18 +322,14 @@
<mat-paginator [pageSize]="10" [pageSizeOptions]="[5, 10, 20, 50]" showFirstLastButtons></mat-paginator> <mat-paginator [pageSize]="10" [pageSizeOptions]="[5, 10, 20, 50]" showFirstLastButtons></mat-paginator>
</div> </div>
</div> </div>
</div>
<!-- No clients view --> <!-- No clients view -->
<ng-template #noClientsTemplate> <ng-template #noClientsTemplate>
<div *ngIf="isLoadingClients" class="loading-container"> <div *ngIf="!initialLoading" class="no-clients-info">
<mat-spinner></mat-spinner>
</div>
<div *ngIf="!isLoadingClients" class="no-clients-info">
<span>{{ 'noClients' | translate }}</span> <span>{{ 'noClients' | translate }}</span>
<mat-icon>error_outline</mat-icon> <mat-icon>error_outline</mat-icon>
</div> </div>
</ng-template> </ng-template>
</div> </div>
</div>
</div>
</ng-template>

View File

@ -25,7 +25,8 @@ import { JoyrideModule } from 'ngx-joyride';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatTreeModule } from '@angular/material/tree'; import { MatTreeModule } from '@angular/material/tree';
import { TreeNode } from './model/model'; import { TreeNode } from './model/model';
import {ExecuteCommandComponent} from "../commands/main-commands/execute-command/execute-command.component"; import { LoadingComponent } from '../../shared/loading/loading.component'; // Importa el componente LoadingComponent
import { ExecuteCommandComponent } from '../commands/main-commands/execute-command/execute-command.component';
describe('GroupsComponent', () => { describe('GroupsComponent', () => {
let component: GroupsComponent; let component: GroupsComponent;
@ -33,7 +34,7 @@ describe('GroupsComponent', () => {
beforeEach(async () => { beforeEach(async () => {
await TestBed.configureTestingModule({ await TestBed.configureTestingModule({
declarations: [GroupsComponent, ExecuteCommandComponent], declarations: [GroupsComponent, ExecuteCommandComponent, LoadingComponent], // Declara LoadingComponent
imports: [ imports: [
HttpClientTestingModule, HttpClientTestingModule,
ToastrModule.forRoot(), ToastrModule.forRoot(),
@ -64,8 +65,7 @@ describe('GroupsComponent', () => {
{ provide: MatDialogRef, useValue: {} }, { provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: { data: { id: 123 } } } { provide: MAT_DIALOG_DATA, useValue: { data: { id: 123 } } }
] ]
}) }).compileComponents();
.compileComponents();
fixture = TestBed.createComponent(GroupsComponent); fixture = TestBed.createComponent(GroupsComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
@ -76,18 +76,6 @@ describe('GroupsComponent', () => {
expect(component).toBeTruthy(); expect(component).toBeTruthy();
}); });
it('should call search on ngOnInit', () => {
spyOn(component, 'search');
component.ngOnInit();
expect(component.search).toHaveBeenCalled();
});
it('should call search method', () => {
spyOn(component, 'search');
component.search();
expect(component.search).toHaveBeenCalled();
});
it('should clear selection', () => { it('should clear selection', () => {
spyOn(component, 'clearSelection'); spyOn(component, 'clearSelection');
component.clearSelection(); component.clearSelection();
@ -121,4 +109,20 @@ describe('GroupsComponent', () => {
component.expandPathToNode(node); component.expandPathToNode(node);
expect(component.expandPathToNode).toHaveBeenCalledWith(node); expect(component.expandPathToNode).toHaveBeenCalledWith(node);
}); });
it('should handle node click', () => {
const node: TreeNode = { id: '1', name: 'Node 1', type: 'type', children: [] };
spyOn<any>(component, 'fetchClientsForNode');
component.onNodeClick(node);
expect(component.selectedNode).toBe(node);
expect(component['fetchClientsForNode']).toHaveBeenCalledWith(node);
});
it('should fetch clients for node', () => {
const node: TreeNode = { id: '1', name: 'Node 1', type: 'type', children: [] };
spyOn(component['http'], 'get').and.callThrough();
component.fetchClientsForNode(node);
expect(component.isLoadingClients).toBeTrue();
expect(component['http'].get).toHaveBeenCalledWith(`${component.baseUrl}/clients?organizationalUnit.id=${node.id}`);
});
}); });

View File

@ -42,13 +42,14 @@ export class GroupsComponent implements OnInit, OnDestroy {
organizationalUnits: UnidadOrganizativa[] = []; organizationalUnits: UnidadOrganizativa[] = [];
selectedUnidad: UnidadOrganizativa | null = null; selectedUnidad: UnidadOrganizativa | null = null;
selectedDetail: UnidadOrganizativa | null = null; selectedDetail: UnidadOrganizativa | null = null;
loading = false; initialLoading: boolean = true;
isLoadingClients: boolean = false; isLoadingClients: boolean = false;
searchTerm = ''; searchTerm = '';
treeControl: FlatTreeControl<FlatNode>; treeControl: FlatTreeControl<FlatNode>;
treeFlattener: MatTreeFlattener<TreeNode, FlatNode>; treeFlattener: MatTreeFlattener<TreeNode, FlatNode>;
treeDataSource: MatTreeFlatDataSource<TreeNode, FlatNode>; treeDataSource: MatTreeFlatDataSource<TreeNode, FlatNode>;
selectedNode: TreeNode | null = null; selectedNode: TreeNode | null = null;
hasClients: boolean = false;
commands: Command[] = []; commands: Command[] = [];
commandsLoading = false; commandsLoading = false;
selectedClients = new MatTableDataSource<Client>([]); selectedClients = new MatTableDataSource<Client>([]);
@ -109,8 +110,8 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.treeDataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener); this.treeDataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
} }
ngOnInit(): void { ngOnInit(): void {
this.search();
this.updateGridCols(); this.updateGridCols();
this.refreshData(); this.refreshData();
window.addEventListener('resize', this.updateGridCols); window.addEventListener('resize', this.updateGridCols);
@ -126,11 +127,13 @@ export class GroupsComponent implements OnInit, OnDestroy {
}; };
} }
ngOnDestroy(): void { ngOnDestroy(): void {
window.removeEventListener('resize', this.updateGridCols); window.removeEventListener('resize', this.updateGridCols);
this.subscriptions.unsubscribe(); this.subscriptions.unsubscribe();
} }
private transformer = (node: TreeNode, level: number): FlatNode => ({ private transformer = (node: TreeNode, level: number): FlatNode => ({
id: node.id, id: node.id,
name: node.name, name: node.name,
@ -142,15 +145,18 @@ export class GroupsComponent implements OnInit, OnDestroy {
'@id': node['@id'], '@id': node['@id'],
}); });
toggleView(view: 'card' | 'list'): void { toggleView(view: 'card' | 'list'): void {
this.currentView = view; this.currentView = view;
} }
updateGridCols = (): void => { updateGridCols = (): void => {
const width = window.innerWidth; const width = window.innerWidth;
this.cols = width <= 600 ? 1 : width <= 960 ? 2 : width <= 1280 ? 3 : 4; this.cols = width <= 600 ? 1 : width <= 960 ? 2 : width <= 1280 ? 3 : 4;
}; };
clearSelection(): void { clearSelection(): void {
this.selectedUnidad = null; this.selectedUnidad = null;
this.selectedDetail = null; this.selectedDetail = null;
@ -158,6 +164,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.selectedNode = null; this.selectedNode = null;
} }
// Función para obtener los filtros guardados actualmente deshabilitada // Función para obtener los filtros guardados actualmente deshabilitada
// getFilters(): void { // getFilters(): void {
// this.subscriptions.add( // this.subscriptions.add(
@ -171,6 +178,8 @@ export class GroupsComponent implements OnInit, OnDestroy {
// ) // )
// ); // );
// } // }
getFilters(): void { getFilters(): void {
this.subscriptions.add( this.subscriptions.add(
this.dataService.getFilters().subscribe( this.dataService.getFilters().subscribe(
@ -184,44 +193,32 @@ export class GroupsComponent implements OnInit, OnDestroy {
); );
} }
search(): void {
this.loading = true;
this.subscriptions.add(
this.dataService.getOrganizationalUnits(this.searchTerm).subscribe(
(data) => {
this.organizationalUnits = data;
this.loading = false;
},
(error) => {
console.error('Error fetching organizational units', error);
this.loading = false;
}
)
);
}
private convertToTreeData(data: UnidadOrganizativa): TreeNode { private convertToTreeData(data: UnidadOrganizativa): TreeNode {
const processNode = (node: UnidadOrganizativa): TreeNode => ({ const processNode = (node: UnidadOrganizativa): TreeNode => {
const children = node.children?.map(processNode) || [];
const hasClients = (node.clients?.length ?? 0) > 0 || children.some(child => child.hasClients);
return {
id: node.id, id: node.id,
uuid: node.uuid, uuid: node.uuid,
name: node.name, name: node.name,
type: node.type, type: node.type,
'@id': node['@id'], '@id': node['@id'],
children: node.children?.map(processNode) || [], children: children,
hasClients: (node.clients?.length ?? 0) > 0, hasClients: hasClients,
}); };
};
return processNode(data); return processNode(data);
} }
private refreshData(selectedNodeIdOrUuid?: string): void {
this.loading = true;
this.isLoadingClients = !!selectedNodeIdOrUuid;
private refreshData(selectedNodeIdOrUuid?: string): void {
this.dataService.getOrganizationalUnits().subscribe({ this.dataService.getOrganizationalUnits().subscribe({
next: (data) => { next: (data) => {
this.originalTreeData = data.map((unidad) => this.convertToTreeData(unidad)); this.originalTreeData = data.map((unidad) => this.convertToTreeData(unidad));
this.treeDataSource.data = [...this.originalTreeData]; this.treeDataSource.data = [...this.originalTreeData];
if (selectedNodeIdOrUuid) { if (selectedNodeIdOrUuid) {
this.selectedNode = this.findNodeByIdOrUuid(this.treeDataSource.data, selectedNodeIdOrUuid); this.selectedNode = this.findNodeByIdOrUuid(this.treeDataSource.data, selectedNodeIdOrUuid);
@ -240,19 +237,15 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.selectedClients.data = []; this.selectedClients.data = [];
} }
} }
this.loading = false;
this.isLoadingClients = false;
}, },
error: (error) => { error: (error) => {
console.error('Error fetching organizational units', error); console.error('Error fetching organizational units', error);
this.toastr.error('Ocurrió un error al cargar las unidades organizativas'); this.toastr.error('Ocurrió un error al cargar las unidades organizativas');
this.loading = false;
this.isLoadingClients = false;
}, },
}); });
} }
expandPathToNode(node: TreeNode): void { expandPathToNode(node: TreeNode): void {
const path: TreeNode[] = []; const path: TreeNode[] = [];
let currentNode: TreeNode | null = node; let currentNode: TreeNode | null = node;
@ -270,6 +263,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
}); });
} }
private findParentNode(treeData: TreeNode[], childId: string): TreeNode | null { private findParentNode(treeData: TreeNode[], childId: string): TreeNode | null {
for (const node of treeData) { for (const node of treeData) {
if (node.children?.some((child) => child.id === childId)) { if (node.children?.some((child) => child.id === childId)) {
@ -286,6 +280,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
return null; return null;
} }
private findNodeByIdOrUuid(treeData: TreeNode[], identifier: string): TreeNode | null { private findNodeByIdOrUuid(treeData: TreeNode[], identifier: string): TreeNode | null {
const search = (nodes: TreeNode[]): TreeNode | null => { const search = (nodes: TreeNode[]): TreeNode | null => {
for (const node of nodes) { for (const node of nodes) {
@ -300,24 +295,30 @@ export class GroupsComponent implements OnInit, OnDestroy {
return search(treeData); return search(treeData);
} }
onNodeClick(node: TreeNode): void { onNodeClick(node: TreeNode): void {
this.selectedNode = node; this.selectedNode = node;
this.fetchClientsForNode(node); this.fetchClientsForNode(node);
} }
private fetchClientsForNode(node: TreeNode): void {
public fetchClientsForNode(node: TreeNode): void {
this.isLoadingClients = true; this.isLoadingClients = true;
this.http.get<any>(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}`).subscribe({ this.http.get<any>(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}`).subscribe({
next: (response) => { next: (response) => {
this.selectedClients.data = response['hydra:member']; this.selectedClients.data = response['hydra:member'];
this.hasClients = node.hasClients ?? false;
this.isLoadingClients = false; this.isLoadingClients = false;
this.initialLoading = false;
}, },
error: () => { error: () => {
this.isLoadingClients = false; this.isLoadingClients = false;
this.initialLoading = false;
} }
}); });
} }
addOU(event: MouseEvent, parent: TreeNode | null = null): void { addOU(event: MouseEvent, parent: TreeNode | null = null): void {
event.stopPropagation(); event.stopPropagation();
const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, { const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, {
@ -332,6 +333,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
}); });
} }
addClient(event: MouseEvent, organizationalUnit: TreeNode | null = null): void { addClient(event: MouseEvent, organizationalUnit: TreeNode | null = null): void {
event.stopPropagation(); event.stopPropagation();
const targetNode = organizationalUnit || this.selectedNode; const targetNode = organizationalUnit || this.selectedNode;
@ -353,6 +355,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
}); });
} }
addMultipleClients(event: MouseEvent, organizationalUnit: TreeNode | null = null): void { addMultipleClients(event: MouseEvent, organizationalUnit: TreeNode | null = null): void {
event.stopPropagation(); event.stopPropagation();
const targetNode = organizationalUnit || this.selectedNode; const targetNode = organizationalUnit || this.selectedNode;
@ -377,6 +380,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
}); });
} }
onEditNode(event: MouseEvent, node: TreeNode | null): void { onEditNode(event: MouseEvent, node: TreeNode | null): void {
event.stopPropagation(); event.stopPropagation();
const uuid = node ? this.extractUuid(node['@id']) : null; const uuid = node ? this.extractUuid(node['@id']) : null;
@ -393,6 +397,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
}); });
} }
onDeleteClick(event: MouseEvent, node: TreeNode | null): void { onDeleteClick(event: MouseEvent, node: TreeNode | null): void {
event.stopPropagation(); event.stopPropagation();
const uuid = node ? this.extractUuid(node['@id']) : null; const uuid = node ? this.extractUuid(node['@id']) : null;
@ -411,6 +416,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
}); });
} }
private deleteEntityorClient(uuid: string, type: string): void { private deleteEntityorClient(uuid: string, type: string): void {
if (!this.selectedNode) return; if (!this.selectedNode) return;
@ -441,6 +447,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
}); });
} }
onEditClick(event: MouseEvent, type: string, uuid: string): void { onEditClick(event: MouseEvent, type: string, uuid: string): void {
event.stopPropagation(); event.stopPropagation();
const dialogRef = type !== NodeType.Client const dialogRef = type !== NodeType.Client
@ -452,6 +459,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
}); });
} }
onRoomMap(room: TreeNode | null): void { onRoomMap(room: TreeNode | null): void {
if (!room || !room['@id']) return; if (!room || !room['@id']) return;
this.subscriptions.add( this.subscriptions.add(
@ -469,6 +477,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
); );
} }
executeCommand(command: Command, selectedNode: TreeNode | null): void { executeCommand(command: Command, selectedNode: TreeNode | null): void {
if (!selectedNode) { if (!selectedNode) {
@ -479,11 +488,13 @@ export class GroupsComponent implements OnInit, OnDestroy {
} }
} }
onShowClientDetail(event: MouseEvent, client: Client): void { onShowClientDetail(event: MouseEvent, client: Client): void {
event.stopPropagation(); event.stopPropagation();
this.router.navigate(['clients', client.uuid], { state: { clientData: client } }); this.router.navigate(['clients', client.uuid], { state: { clientData: client } });
} }
onShowDetailsClick(event: MouseEvent, data: TreeNode | null): void { onShowDetailsClick(event: MouseEvent, data: TreeNode | null): void {
event.stopPropagation(); event.stopPropagation();
if (data && data.type !== NodeType.Client) { if (data && data.type !== NodeType.Client) {
@ -495,10 +506,12 @@ export class GroupsComponent implements OnInit, OnDestroy {
} }
} }
openBottomSheet(): void { openBottomSheet(): void {
this.bottomSheet.open(LegendComponent); this.bottomSheet.open(LegendComponent);
} }
iniciarTour(): void { iniciarTour(): void {
this.joyrideService.startTour({ this.joyrideService.startTour({
steps: ['groupsTitleStepText', 'filtersPanelStep', 'addStep', 'keyStep', 'tabsStep'], steps: ['groupsTitleStepText', 'filtersPanelStep', 'addStep', 'keyStep', 'tabsStep'],
@ -507,9 +520,11 @@ export class GroupsComponent implements OnInit, OnDestroy {
}); });
} }
hasChild = (_: number, node: FlatNode): boolean => node.expandable; hasChild = (_: number, node: FlatNode): boolean => node.expandable;
isLeafNode = (_: number, node: FlatNode): boolean => !node.expandable; isLeafNode = (_: number, node: FlatNode): boolean => !node.expandable;
filterTree(searchTerm: string): void { filterTree(searchTerm: string): void {
const expandPaths: TreeNode[][] = []; const expandPaths: TreeNode[][] = [];
@ -545,6 +560,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
} }
} }
private expandPath(path: TreeNode[]): void { private expandPath(path: TreeNode[]): void {
path.forEach((pathNode) => { path.forEach((pathNode) => {
const flatNode = this.treeControl.dataNodes?.find((n) => n.id === pathNode.id); const flatNode = this.treeControl.dataNodes?.find((n) => n.id === pathNode.id);
@ -554,27 +570,32 @@ export class GroupsComponent implements OnInit, OnDestroy {
}); });
} }
onTreeFilterInput(event: Event): void { onTreeFilterInput(event: Event): void {
const input = event.target as HTMLInputElement; const input = event.target as HTMLInputElement;
const searchTerm = input?.value.trim() || ''; const searchTerm = input?.value.trim() || '';
this.filterTree(searchTerm); this.filterTree(searchTerm);
} }
onClientFilterInput(event: Event): void { onClientFilterInput(event: Event): void {
const input = event.target as HTMLInputElement; const input = event.target as HTMLInputElement;
const searchTerm = input?.value || ''; const searchTerm = input?.value || '';
this.filterClients(searchTerm); this.filterClients(searchTerm);
} }
filterClients(searchTerm: string): void { filterClients(searchTerm: string): void {
this.searchTerm = searchTerm.trim().toLowerCase(); this.searchTerm = searchTerm.trim().toLowerCase();
this.selectedClients.filter = this.searchTerm; this.selectedClients.filter = this.searchTerm;
} }
public setSelectedNode(node: TreeNode): void { public setSelectedNode(node: TreeNode): void {
this.selectedNode = node; this.selectedNode = node;
} }
getStatus(client: Client, node: any): void { getStatus(client: Client, node: any): void {
if (!client.uuid || !client['@id']) return; if (!client.uuid || !client['@id']) return;
@ -599,12 +620,14 @@ export class GroupsComponent implements OnInit, OnDestroy {
); );
} }
isAllSelected() { isAllSelected() {
const numSelected = this.selection.selected.length; const numSelected = this.selection.selected.length;
const numRows = this.selectedClients.data.length; const numRows = this.selectedClients.data.length;
return numSelected === numRows; return numSelected === numRows;
} }
toggleAllRows() { toggleAllRows() {
if (this.isAllSelected()) { if (this.isAllSelected()) {
this.selection.clear(); this.selection.clear();
@ -616,15 +639,18 @@ export class GroupsComponent implements OnInit, OnDestroy {
this.arrayClients = [...this.selection.selected]; this.arrayClients = [...this.selection.selected];
} }
toggleRow(row: any) { toggleRow(row: any) {
this.selection.toggle(row); this.selection.toggle(row);
this.updateSelectedClients(); this.updateSelectedClients();
} }
updateSelectedClients() { updateSelectedClients() {
this.arrayClients = [...this.selection.selected]; this.arrayClients = [...this.selection.selected];
} }
getClientPath(client: Client): string { getClientPath(client: Client): string {
const path: string[] = []; const path: string[] = [];
let currentNode: TreeNode | null = this.findNodeByIdOrUuid(this.treeDataSource.data, client.organizationalUnit.uuid); let currentNode: TreeNode | null = this.findNodeByIdOrUuid(this.treeDataSource.data, client.organizationalUnit.uuid);
@ -637,18 +663,20 @@ export class GroupsComponent implements OnInit, OnDestroy {
return path.join(' / '); return path.join(' / ');
} }
private extractUuid(idPath: string | undefined): string | null { private extractUuid(idPath: string | undefined): string | null {
return idPath ? idPath.split('/').pop() || null : null; return idPath ? idPath.split('/').pop() || null : null;
} }
clearTreeSearch(inputElement: HTMLInputElement): void { clearTreeSearch(inputElement: HTMLInputElement): void {
inputElement.value = ''; inputElement.value = '';
this.filterTree(''); this.filterTree('');
} }
clearClientSearch(inputElement: HTMLInputElement): void { clearClientSearch(inputElement: HTMLInputElement): void {
inputElement.value = ''; inputElement.value = '';
this.filterClients(''); this.filterClients('');
} }
} }

View File

@ -4,7 +4,7 @@
left: 0; left: 0;
width: 100vw; width: 100vw;
height: 100vh; height: 100vh;
background: rgba(0, 0, 0, 0.5); background: rgba(0, 0, 0, 0.2);
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;