1231 lines
38 KiB
TypeScript
1231 lines
38 KiB
TypeScript
import { Component, OnInit, OnDestroy, ViewChild, QueryList, ViewChildren, ChangeDetectorRef } from '@angular/core';
|
|
import { HttpClient, HttpParams } from '@angular/common/http';
|
|
import { Router } from '@angular/router';
|
|
import { MatDialog } from '@angular/material/dialog';
|
|
import { MatBottomSheet } from '@angular/material/bottom-sheet';
|
|
import { ToastrService } from 'ngx-toastr';
|
|
import { JoyrideService } from 'ngx-joyride';
|
|
import { FlatTreeControl } from '@angular/cdk/tree';
|
|
import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree';
|
|
import { Subscription } from 'rxjs';
|
|
import { DataService } from './services/data.service';
|
|
import { UnidadOrganizativa, Client, TreeNode, FlatNode, Command } from './model/model';
|
|
import { ManageOrganizationalUnitComponent } from './shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component';
|
|
import { ShowOrganizationalUnitComponent } from './shared/organizational-units/show-organizational-unit/show-organizational-unit.component';
|
|
import { LegendComponent } from './shared/legend/legend.component';
|
|
import { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/delete-modal.component';
|
|
import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom-view-modal';
|
|
import { MatTableDataSource } from '@angular/material/table';
|
|
import { PageEvent } from '@angular/material/paginator';
|
|
import { CreateMultipleClientComponent } from "./shared/clients/create-multiple-client/create-multiple-client.component";
|
|
import { SelectionModel } from "@angular/cdk/collections";
|
|
import { ManageClientComponent } from "./shared/clients/manage-client/manage-client.component";
|
|
import { debounceTime } from 'rxjs/operators';
|
|
import { Subject } from 'rxjs';
|
|
import { ConfigService } from '@services/config.service';
|
|
import { BreakpointObserver } from '@angular/cdk/layout';
|
|
import { MatMenuTrigger } from '@angular/material/menu';
|
|
import { ClientDetailsComponent } from './shared/client-details/client-details.component';
|
|
import { PartitionTypeOrganizatorComponent } from './shared/partition-type-organizator/partition-type-organizator.component';
|
|
import { ClientTaskLogsComponent } from '../task-logs/client-task-logs/client-task-logs.component';
|
|
import {ChangeParentComponent} from "./shared/change-parent/change-parent.component";
|
|
import { AuthService } from '@services/auth.service';
|
|
import { ClientPendingTasksComponent } from '../task-logs/client-pending-tasks/client-pending-tasks.component';
|
|
|
|
enum NodeType {
|
|
OrganizationalUnit = 'organizational-unit',
|
|
ClassroomsGroup = 'classrooms-group',
|
|
Classroom = 'classroom',
|
|
ClientsGroup = 'clients-group',
|
|
Client = 'client',
|
|
}
|
|
|
|
@Component({
|
|
selector: 'app-groups',
|
|
templateUrl: './groups.component.html',
|
|
styleUrls: ['./groups.component.css'],
|
|
})
|
|
export class GroupsComponent implements OnInit, OnDestroy {
|
|
@ViewChildren(MatMenuTrigger) menuTriggers!: QueryList<MatMenuTrigger>;
|
|
isSmallScreen: boolean = false;
|
|
baseUrl: string;
|
|
mercureUrl: string;
|
|
organizationalUnits: UnidadOrganizativa[] = [];
|
|
selectedUnidad: UnidadOrganizativa | null = null;
|
|
selectedDetail: UnidadOrganizativa | null = null;
|
|
length: number = 0;
|
|
itemsPerPage: number = 20;
|
|
page: number = 0;
|
|
pageSizeOptions: number[] = [5, 10, 20, 50, 100];
|
|
initialLoading: boolean = true;
|
|
isLoadingClients: boolean = false;
|
|
searchTerm = '';
|
|
treeControl: FlatTreeControl<FlatNode>;
|
|
treeFlattener: MatTreeFlattener<TreeNode, FlatNode>;
|
|
treeDataSource: MatTreeFlatDataSource<TreeNode, FlatNode>;
|
|
selectedNode: TreeNode | null = null;
|
|
hasClients: boolean = false;
|
|
commands: Command[] = [];
|
|
commandsLoading = false;
|
|
selectedClients = new MatTableDataSource<Client>([]);
|
|
selection = new SelectionModel<any>(true, []);
|
|
cols = 4;
|
|
currentView: string = 'list';
|
|
savedFilterNames: [string, string][] = [];
|
|
selectedTreeFilter = '';
|
|
syncStatus = false;
|
|
syncingClientId: string | null = null;
|
|
private originalTreeData: TreeNode[] = [];
|
|
arrayClients: any[] = [];
|
|
filters: { [key: string]: string } = {};
|
|
private clientFilterSubject = new Subject<string>();
|
|
loading = false;
|
|
|
|
// Nuevas propiedades para funcionalidades mejoradas
|
|
selectedClient: any = null;
|
|
sortBy: string = 'name';
|
|
sortDirection: 'asc' | 'desc' = 'asc';
|
|
currentSortColumn: string = 'name';
|
|
|
|
// Estadísticas totales
|
|
totalStats: {
|
|
total: number;
|
|
off: number;
|
|
online: number;
|
|
busy: number;
|
|
} = {
|
|
total: 0,
|
|
off: 0,
|
|
online: 0,
|
|
busy: 0
|
|
};
|
|
|
|
// Tipos de firmware disponibles
|
|
firmwareTypes: string[] = [];
|
|
|
|
protected status = [
|
|
{ value: 'off', name: 'Apagado' },
|
|
{ value: 'initializing', name: 'Inicializando' },
|
|
{ value: 'og-live', name: 'Og Live' },
|
|
{ value: 'linux', name: 'Linux' },
|
|
{ value: 'linux-session', name: 'Linux Session' },
|
|
{ value: 'windows', name: 'Windows' },
|
|
{ value: 'windows-session', name: 'Windows Session' },
|
|
{ value: 'busy', name: 'Ocupado' },
|
|
{ value: 'mac', name: 'Mac' },
|
|
{ value: 'disconnected', name: 'Desconectado' }
|
|
];
|
|
|
|
displayedColumns: string[] = ['select', 'status', 'ip', 'firmwareType', 'name', 'oglive', 'subnet', 'pxeTemplate', 'actions'];
|
|
|
|
private subscriptions: Subscription = new Subscription();
|
|
|
|
constructor(
|
|
private http: HttpClient,
|
|
private router: Router,
|
|
private dataService: DataService,
|
|
public dialog: MatDialog,
|
|
private bottomSheet: MatBottomSheet,
|
|
private joyrideService: JoyrideService,
|
|
private breakpointObserver: BreakpointObserver,
|
|
private toastr: ToastrService,
|
|
public auth: AuthService,
|
|
private configService: ConfigService,
|
|
private cd: ChangeDetectorRef,
|
|
) {
|
|
this.baseUrl = this.configService.apiUrl;
|
|
this.mercureUrl = this.configService.mercureUrl;
|
|
this.treeFlattener = new MatTreeFlattener<TreeNode, FlatNode>(
|
|
this.transformer,
|
|
(node) => node.level,
|
|
(node) => node.expandable,
|
|
(node) => node.children
|
|
);
|
|
|
|
this.treeControl = new FlatTreeControl<FlatNode>(
|
|
(node) => node.level,
|
|
(node) => node.expandable
|
|
);
|
|
|
|
this.treeDataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
|
|
this.currentView = this.auth.groupsView || 'list';
|
|
}
|
|
|
|
|
|
ngOnInit(): void {
|
|
this.updateGridCols();
|
|
this.refreshData();
|
|
window.addEventListener('resize', this.updateGridCols);
|
|
|
|
this.selectedClients.filterPredicate = (client: Client, filter: string): boolean => {
|
|
const lowerTerm = filter.toLowerCase();
|
|
return (
|
|
client.name.toLowerCase().includes(lowerTerm) ||
|
|
client.ip?.toLowerCase().includes(lowerTerm) ||
|
|
client.status?.toLowerCase().includes(lowerTerm) ||
|
|
client.mac?.toLowerCase().includes(lowerTerm)
|
|
);
|
|
};
|
|
|
|
this.arrayClients = this.selectedClients.data;
|
|
|
|
const eventSource = new EventSource(`${this.mercureUrl}?topic=` + encodeURIComponent(`clients`));
|
|
|
|
eventSource.onmessage = (event) => {
|
|
const data = JSON.parse(event.data);
|
|
|
|
if (Array.isArray(data)) {
|
|
data.forEach((client) => {
|
|
if (client && client['@id']) {
|
|
this.updateClientStatus(client['@id'], client.status);
|
|
}
|
|
});
|
|
}
|
|
else if (data && data['@id']) {
|
|
this.updateClientStatus(data['@id'], data.status);
|
|
}
|
|
};
|
|
|
|
this.clientFilterSubject.pipe(debounceTime(500)).subscribe(searchTerm => {
|
|
this.filters['query'] = searchTerm;
|
|
this.filterClients(searchTerm);
|
|
});
|
|
|
|
this.breakpointObserver.observe(['(max-width: 992px)']).subscribe((result) => {
|
|
this.isSmallScreen = result.matches;
|
|
})
|
|
}
|
|
|
|
private updateClientStatus(clientUuid: string, status: string): void {
|
|
let updated = false;
|
|
|
|
const index = this.arrayClients.findIndex(client => client['@id'] === clientUuid);
|
|
if (index !== -1) {
|
|
const updatedClient = { ...this.arrayClients[index], status };
|
|
this.arrayClients = [
|
|
...this.arrayClients.slice(0, index),
|
|
updatedClient,
|
|
...this.arrayClients.slice(index + 1)
|
|
];
|
|
updated = true;
|
|
}
|
|
|
|
const tableIndex = this.selectedClients.data.findIndex(client => client['@id'] === clientUuid);
|
|
|
|
if (tableIndex !== -1) {
|
|
const updatedClients = [...this.selectedClients.data];
|
|
|
|
updatedClients[tableIndex] = {
|
|
...updatedClients[tableIndex],
|
|
status: status
|
|
};
|
|
|
|
this.selectedClients.data = updatedClients;
|
|
}
|
|
|
|
if (updated) {
|
|
this.cd.detectChanges();
|
|
}
|
|
}
|
|
|
|
|
|
ngOnDestroy(): void {
|
|
window.removeEventListener('resize', this.updateGridCols);
|
|
this.subscriptions.unsubscribe();
|
|
}
|
|
|
|
|
|
private transformer = (node: TreeNode, level: number): FlatNode => ({
|
|
id: node.id,
|
|
name: node.name,
|
|
type: node.type,
|
|
level,
|
|
expandable: !!node.children?.length,
|
|
hasClients: node.hasClients,
|
|
ip: node.ip,
|
|
'@id': node['@id'],
|
|
networkSettings: node.networkSettings,
|
|
});
|
|
|
|
|
|
toggleView(view: 'card' | 'list'): void {
|
|
this.currentView = view;
|
|
}
|
|
|
|
|
|
updateGridCols = (): void => {
|
|
const width = window.innerWidth;
|
|
this.cols = width <= 600 ? 1 : width <= 960 ? 2 : width <= 1280 ? 3 : 4;
|
|
};
|
|
|
|
|
|
clearSelection(): void {
|
|
this.selectedUnidad = null;
|
|
this.selectedDetail = null;
|
|
this.selectedClients.data = [];
|
|
this.selectedNode = null;
|
|
}
|
|
|
|
|
|
// Función para obtener los filtros guardados actualmente deshabilitada
|
|
// getFilters(): void {
|
|
// this.subscriptions.add(
|
|
// this.dataService.getFilters().subscribe(
|
|
// (data) => {
|
|
// this.savedFilterNames = data.map((filter: { name: string; uuid: string; }) => [filter.name, filter.uuid]);
|
|
// },
|
|
// (error) => {
|
|
// console.error('Error fetching filters:', error);
|
|
// }
|
|
// )
|
|
// );
|
|
// }
|
|
|
|
|
|
getFilters(): void {
|
|
this.subscriptions.add(
|
|
this.dataService.getFilters().subscribe(
|
|
(data) => {
|
|
this.savedFilterNames = data.map((filter: { name: string; uuid: string; }) => [filter.name, filter.uuid]);
|
|
},
|
|
(error) => {
|
|
console.error('Error fetching filters:', error);
|
|
}
|
|
)
|
|
);
|
|
}
|
|
|
|
|
|
private convertToTreeData(data: 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,
|
|
uuid: node.uuid,
|
|
name: node.name,
|
|
type: node.type,
|
|
'@id': node['@id'],
|
|
children: children,
|
|
hasClients: hasClients,
|
|
};
|
|
};
|
|
|
|
return processNode(data);
|
|
}
|
|
|
|
|
|
private refreshData(selectedNodeIdOrUuid?: string, selectedClientsBeforeEdit: string[] = []): void {
|
|
this.dataService.getOrganizationalUnits().subscribe({
|
|
next: (data) => {
|
|
this.originalTreeData = data.map((unidad) => this.convertToTreeData(unidad));
|
|
this.treeDataSource.data = [...this.originalTreeData];
|
|
if (selectedNodeIdOrUuid) {
|
|
this.selectedNode = this.findNodeByIdOrUuid(this.treeDataSource.data, selectedNodeIdOrUuid);
|
|
|
|
if (this.selectedNode) {
|
|
this.treeControl.collapseAll();
|
|
this.expandPathToNode(this.selectedNode);
|
|
this.fetchClientsForNode(this.selectedNode, selectedClientsBeforeEdit);
|
|
}
|
|
} else {
|
|
this.treeControl.collapseAll();
|
|
if (this.treeDataSource.data.length > 0) {
|
|
this.selectedNode = this.treeDataSource.data[0];
|
|
this.fetchClientsForNode(this.selectedNode, selectedClientsBeforeEdit);
|
|
} else {
|
|
this.selectedNode = null;
|
|
this.selectedClients.data = [];
|
|
this.initialLoading = false;
|
|
}
|
|
}
|
|
},
|
|
error: (error) => {
|
|
console.error('Error fetching organizational units', error);
|
|
this.toastr.error('Ocurrió un error al cargar las unidades organizativas');
|
|
},
|
|
});
|
|
}
|
|
|
|
|
|
expandPathToNode(node: TreeNode): void {
|
|
const path: TreeNode[] = [];
|
|
let currentNode: TreeNode | null = node;
|
|
|
|
while (currentNode) {
|
|
path.unshift(currentNode);
|
|
currentNode = currentNode.id ? this.findParentNode(this.treeDataSource.data, currentNode.id) : null;
|
|
}
|
|
|
|
path.forEach((pathNode) => {
|
|
const flatNode = this.treeControl.dataNodes?.find((n) => n.id === pathNode.id);
|
|
if (flatNode) {
|
|
this.treeControl.expand(flatNode);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
private findParentNode(treeData: TreeNode[], childId: string): TreeNode | null {
|
|
for (const node of treeData) {
|
|
if (node.children?.some((child) => child.id === childId)) {
|
|
return node;
|
|
}
|
|
|
|
if (node.children && node.children.length > 0) {
|
|
const parent = this.findParentNode(node.children, childId);
|
|
if (parent) {
|
|
return parent;
|
|
}
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
|
|
private findNodeByIdOrUuid(treeData: TreeNode[], identifier: string): TreeNode | null {
|
|
const search = (nodes: TreeNode[]): TreeNode | null => {
|
|
for (const node of nodes) {
|
|
if (node.id === identifier || node.uuid === identifier) return node;
|
|
if (node.children && node.children.length > 0) {
|
|
const found = search(node.children);
|
|
if (found) return found;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
return search(treeData);
|
|
}
|
|
|
|
|
|
onNodeClick(event: MouseEvent, node: TreeNode): void {
|
|
event.stopPropagation();
|
|
this.selectedNode = node;
|
|
const selectedClientsBeforeEdit = this.selection.selected.map(client => client.uuid);
|
|
this.fetchClientsForNode(node, selectedClientsBeforeEdit);
|
|
}
|
|
|
|
onMenuClick(event: Event, node: any): void {
|
|
event.stopPropagation();
|
|
this.selectedNode = node;
|
|
const selectedClientsBeforeEdit = this.selection.selected.map(client => client.uuid);
|
|
this.fetchClientsForNode(node, selectedClientsBeforeEdit);
|
|
}
|
|
|
|
|
|
public fetchClientsForNode(node: any, selectedClientsBeforeEdit: string[] = []): void {
|
|
const params = new HttpParams({ fromObject: this.filters });
|
|
|
|
// Agregar parámetros de ordenamiento al backend
|
|
let backendParams = { ...this.filters };
|
|
if (this.sortBy) {
|
|
backendParams['order[' + this.sortBy + ']'] = this.sortDirection;
|
|
}
|
|
|
|
this.isLoadingClients = true;
|
|
this.http.get<any>(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}&page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params: backendParams }).subscribe({
|
|
next: (response: any) => {
|
|
this.selectedClients.data = response['hydra:member'];
|
|
if (this.selectedNode) {
|
|
this.selectedNode.clients = response['hydra:member'];
|
|
}
|
|
this.length = response['hydra:totalItems'];
|
|
this.arrayClients = this.selectedClients.data;
|
|
this.hasClients = this.selectedClients.data.length > 0;
|
|
this.isLoadingClients = false;
|
|
this.initialLoading = false;
|
|
this.selection.clear();
|
|
selectedClientsBeforeEdit.forEach(uuid => {
|
|
const client = this.selectedClients.data.find(client => client.uuid === uuid);
|
|
if (client) {
|
|
this.selection.select(client);
|
|
}
|
|
});
|
|
|
|
// Calcular estadísticas después de cargar los clientes
|
|
this.calculateLocalStats();
|
|
},
|
|
error: () => {
|
|
this.isLoadingClients = false;
|
|
this.initialLoading = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
onPageChange(event: PageEvent): void {
|
|
this.page = event.pageIndex;
|
|
this.itemsPerPage = event.pageSize;
|
|
this.fetchClientsForNode(this.selectedNode);
|
|
}
|
|
|
|
addOU(event: MouseEvent, parent: TreeNode | null = null): void {
|
|
this.loading = true;
|
|
event.stopPropagation();
|
|
const dialogRef = this.dialog.open(ManageOrganizationalUnitComponent, {
|
|
data: { parent },
|
|
width: '900px',
|
|
disableClose: true,
|
|
hasBackdrop: true,
|
|
backdropClass: 'non-clickable-backdrop',
|
|
});
|
|
dialogRef.afterClosed().subscribe((newUnit) => {
|
|
if (newUnit) {
|
|
this.refreshData(newUnit.uuid);
|
|
}
|
|
this.loading = false;
|
|
});
|
|
|
|
}
|
|
|
|
|
|
addClient(event: MouseEvent, organizationalUnit: TreeNode | null = null): void {
|
|
this.loading = true;
|
|
event.stopPropagation();
|
|
const targetNode = organizationalUnit || this.selectedNode;
|
|
const dialogRef = this.dialog.open(ManageClientComponent, {
|
|
data: { organizationalUnit: targetNode },
|
|
width: '900px',
|
|
disableClose: true,
|
|
hasBackdrop: true,
|
|
backdropClass: 'non-clickable-backdrop',
|
|
});
|
|
|
|
dialogRef.afterClosed().subscribe((result) => {
|
|
if (result?.client && result?.organizationalUnit) {
|
|
const organizationalUnitUrl = result.organizationalUnit;
|
|
const uuid = organizationalUnitUrl.split('/')[2];
|
|
const parentNode = this.findNodeByIdOrUuid(this.treeDataSource.data, uuid);
|
|
|
|
if (parentNode) {
|
|
this.refreshData(parentNode.uuid);
|
|
}
|
|
}
|
|
this.loading = false;
|
|
});
|
|
}
|
|
|
|
|
|
addMultipleClients(event: MouseEvent, organizationalUnit: TreeNode | null = null): void {
|
|
this.loading = true;
|
|
event.stopPropagation();
|
|
const targetNode = organizationalUnit || this.selectedNode;
|
|
|
|
const dialogRef = this.dialog.open(CreateMultipleClientComponent, {
|
|
data: { organizationalUnit: targetNode },
|
|
width: '900px',
|
|
disableClose: true,
|
|
hasBackdrop: true,
|
|
backdropClass: 'non-clickable-backdrop',
|
|
});
|
|
dialogRef.afterClosed().subscribe((result) => {
|
|
if (result?.success) {
|
|
const organizationalUnitUrl = result.organizationalUnit;
|
|
const uuid = organizationalUnitUrl.split('/')[2];
|
|
const parentNode = this.findNodeByIdOrUuid(this.treeDataSource.data, uuid);
|
|
|
|
if (parentNode) {
|
|
console.log('Nodo padre encontrado para actualización:', parentNode);
|
|
this.refreshData(parentNode.uuid);
|
|
} else {
|
|
console.error('No se encontró el nodo padre después de la creación masiva.');
|
|
}
|
|
}
|
|
this.loading = false;
|
|
});
|
|
}
|
|
|
|
|
|
onEditNode(event: MouseEvent, node: TreeNode | null): void {
|
|
event.stopPropagation();
|
|
this.loading = true;
|
|
const uuid = node ? this.extractUuid(node['@id']) : null;
|
|
if (!uuid) return;
|
|
|
|
const dialogRef = node?.type !== NodeType.Client
|
|
? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px', disableClose: true, hasBackdrop: true, backdropClass: 'non-clickable-backdrop' })
|
|
: this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px', disableClose: true, hasBackdrop: true, backdropClass: 'non-clickable-backdrop' });
|
|
|
|
dialogRef.afterClosed().subscribe((result) => {
|
|
if (result?.success) {
|
|
this.refreshData(node?.id);
|
|
}
|
|
this.menuTriggers.forEach(trigger => trigger.closeMenu());
|
|
this.loading = false;
|
|
});
|
|
}
|
|
|
|
onDeleteClick(event: MouseEvent, entity: TreeNode | Client | null): void {
|
|
event.stopPropagation();
|
|
this.loading = true;
|
|
if (!entity) return;
|
|
|
|
const uuid = entity['@id'] ? this.extractUuid(entity['@id']) : null;
|
|
const type = entity.hasOwnProperty('mac') ? NodeType.Client : NodeType.OrganizationalUnit;
|
|
|
|
if (!uuid) return;
|
|
|
|
const dialogRef = this.dialog.open(DeleteModalComponent, {
|
|
width: '400px',
|
|
data: { name: entity.name },
|
|
});
|
|
|
|
dialogRef.afterClosed().subscribe((result) => {
|
|
if (result === true) {
|
|
this.deleteEntityorClient(uuid, type);
|
|
}
|
|
this.loading = false;
|
|
});
|
|
}
|
|
|
|
|
|
private deleteEntityorClient(uuid: string, type: string): void {
|
|
if (!this.selectedNode) return;
|
|
|
|
const parentNode = this.selectedNode?.id
|
|
? this.findParentNode(this.treeDataSource.data, this.selectedNode.id)
|
|
: null;
|
|
|
|
this.dataService.deleteElement(uuid, type).subscribe({
|
|
next: () => {
|
|
const entityType = type === NodeType.Client ? 'Cliente' : 'Entidad';
|
|
const verb = type === NodeType.Client ? 'eliminado' : 'eliminada';
|
|
|
|
this.toastr.success(`${entityType} ${verb} exitosamente`);
|
|
|
|
if (type === NodeType.Client) {
|
|
this.refreshData(this.selectedNode?.id);
|
|
} else if (parentNode) {
|
|
this.refreshData(parentNode.id);
|
|
} else {
|
|
this.refreshData();
|
|
}
|
|
},
|
|
error: (error) => {
|
|
console.error('Error deleting entity:', error);
|
|
const entityType = type === NodeType.Client ? 'cliente' : 'entidad';
|
|
this.toastr.error(`Error al eliminar el ${entityType}`);
|
|
},
|
|
});
|
|
}
|
|
|
|
|
|
onEditClick(event: MouseEvent, type: string, uuid: string): void {
|
|
event.stopPropagation();
|
|
this.loading = true;
|
|
const selectedClientsBeforeEdit = this.selection.selected.map(client => client.uuid);
|
|
const dialogRef = type !== NodeType.Client
|
|
? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px', disableClose: true, hasBackdrop: true, backdropClass: 'non-clickable-backdrop' })
|
|
: this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px', disableClose: true, hasBackdrop: true, backdropClass: 'non-clickable-backdrop' });
|
|
|
|
dialogRef.afterClosed().subscribe((result) => {
|
|
if (result?.success) {
|
|
this.refreshData(this.selectedNode?.id, selectedClientsBeforeEdit);
|
|
}
|
|
this.menuTriggers.forEach(trigger => trigger.closeMenu());
|
|
this.loading = false;
|
|
});
|
|
}
|
|
|
|
|
|
onRoomMap(room: TreeNode | null): void {
|
|
if (!room || !room['@id']) return;
|
|
this.subscriptions.add(
|
|
this.http.get<{ clients: Client[] }>(`${this.baseUrl}/clients?organizationalUnit.id=${room.id}`).subscribe(
|
|
(response: any) => {
|
|
this.dialog.open(ClassroomViewDialogComponent, {
|
|
width: '90vw',
|
|
data: { clients: response['hydra:member'] },
|
|
disableClose: true,
|
|
hasBackdrop: true,
|
|
backdropClass: 'non-clickable-backdrop',
|
|
});
|
|
},
|
|
(error) => {
|
|
console.error('Error fetching room data:', error);
|
|
}
|
|
)
|
|
);
|
|
}
|
|
|
|
|
|
executeCommand(command: Command, selectedNode: TreeNode | null): void {
|
|
this.loading = true;
|
|
if (!selectedNode) {
|
|
this.toastr.error('No hay un nodo seleccionado.');
|
|
return;
|
|
} else {
|
|
this.toastr.success(`Ejecutando comando: ${command.name} en ${selectedNode.name}`);
|
|
}
|
|
this.loading = false;
|
|
}
|
|
|
|
|
|
onShowClientDetail(event: MouseEvent, client: Client): void {
|
|
event.stopPropagation();
|
|
this.loading = true;
|
|
const dialogRef = this.dialog.open(ClientDetailsComponent, {
|
|
width: '70vw',
|
|
height: '90vh',
|
|
data: { clientData: client },
|
|
disableClose: true,
|
|
hasBackdrop: true,
|
|
backdropClass: 'non-clickable-backdrop',
|
|
})
|
|
|
|
dialogRef.afterClosed().subscribe((result) => {
|
|
this.loading = false;
|
|
});
|
|
}
|
|
|
|
|
|
onShowDetailsClick(event: MouseEvent, data: TreeNode | null): void {
|
|
event.stopPropagation();
|
|
this.loading = true;
|
|
if (data && data.type !== NodeType.Client) {
|
|
this.dialog.open(ShowOrganizationalUnitComponent, { data: { data }, width: '800px', disableClose: true, hasBackdrop: true, backdropClass: 'non-clickable-backdrop' });
|
|
} else {
|
|
if (data) {
|
|
this.router.navigate(['clients', this.extractUuid(data['@id'])], { state: { clientData: data } });
|
|
}
|
|
}
|
|
this.loading = false;
|
|
}
|
|
|
|
|
|
openBottomSheet(): void {
|
|
this.bottomSheet.open(LegendComponent);
|
|
}
|
|
|
|
|
|
initTour(): void {
|
|
this.joyrideService.startTour({
|
|
steps: ['groupsTitleStep', 'filtersPanelStep', 'treePanelStep', 'addStep', 'keyStep', 'executeCommandStep', 'tabsStep', 'clientsViewStep'],
|
|
showPrevButton: true,
|
|
themeColor: '#3f51b5',
|
|
});
|
|
}
|
|
|
|
|
|
hasChild = (_: number, node: FlatNode): boolean => node.expandable;
|
|
isLeafNode = (_: number, node: FlatNode): boolean => !node.expandable;
|
|
|
|
|
|
filterTree(searchTerm: string): void {
|
|
const expandPaths: TreeNode[][] = [];
|
|
|
|
const filterNodes = (nodes: TreeNode[], parentPath: TreeNode[] = []): TreeNode[] => {
|
|
return nodes
|
|
.map((node) => {
|
|
const matchesName = node.name.toLowerCase().includes(searchTerm.toLowerCase());
|
|
const filteredChildren = node.children ? filterNodes(node.children, [...parentPath, node]) : [];
|
|
|
|
if (matchesName) {
|
|
expandPaths.push([...parentPath, node]);
|
|
return {
|
|
...node,
|
|
children: node.children,
|
|
} as TreeNode;
|
|
} else if (filteredChildren.length > 0) {
|
|
return {
|
|
...node,
|
|
children: filteredChildren,
|
|
} as TreeNode;
|
|
}
|
|
return null;
|
|
})
|
|
.filter((node): node is TreeNode => node !== null);
|
|
};
|
|
|
|
if (!searchTerm) {
|
|
this.treeDataSource.data = [...this.originalTreeData];
|
|
this.treeControl.collapseAll();
|
|
} else {
|
|
this.treeDataSource.data = filterNodes(this.originalTreeData);
|
|
expandPaths.forEach((path) => this.expandPath(path));
|
|
}
|
|
}
|
|
|
|
|
|
private expandPath(path: TreeNode[]): void {
|
|
path.forEach((pathNode) => {
|
|
const flatNode = this.treeControl.dataNodes?.find((n) => n.id === pathNode.id);
|
|
if (flatNode) {
|
|
this.treeControl.expand(flatNode);
|
|
}
|
|
});
|
|
}
|
|
|
|
|
|
onTreeFilterInput(event: Event): void {
|
|
const input = event.target as HTMLInputElement;
|
|
const searchTerm = input?.value.trim() || '';
|
|
this.filterTree(searchTerm);
|
|
}
|
|
|
|
|
|
onClientFilterInput(event: Event): void {
|
|
const input = event.target as HTMLInputElement;
|
|
const searchTerm = input?.value || '';
|
|
this.clientFilterSubject.next(searchTerm);
|
|
}
|
|
|
|
onClientFilterStatusInput(event: Event): void {
|
|
// @ts-ignore
|
|
this.filters['status'] = event;
|
|
this.fetchClientsForNode(this.selectedNode);
|
|
}
|
|
|
|
|
|
filterClients(searchTerm: string): void {
|
|
this.searchTerm = searchTerm.trim().toLowerCase();
|
|
//this.selectedClients.filter = this.searchTerm;
|
|
|
|
this.fetchClientsForNode(this.selectedNode);
|
|
this.arrayClients = this.selectedClients.filteredData;
|
|
}
|
|
|
|
|
|
public setSelectedNode(node: TreeNode): void {
|
|
this.selectedNode = node;
|
|
}
|
|
|
|
|
|
getStatus(client: Client, node: any): void {
|
|
if (!client.uuid || !client['@id']) return;
|
|
|
|
this.syncingClientId = client.uuid;
|
|
this.syncStatus = true;
|
|
|
|
const parentNodeId = client.organizationalUnit?.id || node.id;
|
|
console.log('Parent node id:', parentNodeId);
|
|
|
|
this.subscriptions.add(
|
|
this.http.post(`${this.baseUrl}${client['@id']}/agent/status`, {}).subscribe(
|
|
() => {
|
|
this.toastr.success('Cliente actualizado correctamente');
|
|
this.syncStatus = false;
|
|
this.syncingClientId = null;
|
|
this.refreshData(parentNodeId)
|
|
},
|
|
(error) => {
|
|
this.toastr.error(error.error['hydra:description'] || 'Error al actualizar el cliente');
|
|
this.syncStatus = false;
|
|
this.syncingClientId = null;
|
|
this.refreshData(parentNodeId)
|
|
}
|
|
)
|
|
);
|
|
}
|
|
|
|
|
|
isAllSelected() {
|
|
const numSelected = this.selection.selected.length;
|
|
const numRows = this.selectedClients.data.length;
|
|
return numSelected === numRows;
|
|
}
|
|
|
|
|
|
toggleAllRows() {
|
|
if (this.isAllSelected()) {
|
|
this.selection.clear();
|
|
} else {
|
|
this.selection.select(...this.selectedClients.data);
|
|
}
|
|
this.updateSelectedClients();
|
|
}
|
|
|
|
toggleAllCards() {
|
|
if (this.isAllSelected()) {
|
|
this.selection.clear();
|
|
} else {
|
|
this.selection.select(...this.selectedClients.data);
|
|
}
|
|
this.updateSelectedClients();
|
|
}
|
|
|
|
|
|
toggleRow(row: any) {
|
|
this.selection.toggle(row);
|
|
this.updateSelectedClients();
|
|
}
|
|
|
|
|
|
updateSelectedClients() {
|
|
this.arrayClients = this.selectedClients.data;
|
|
}
|
|
|
|
|
|
getClientPath(client: Client): string {
|
|
const path: string[] = [];
|
|
let currentNode: TreeNode | null = this.findNodeByIdOrUuid(this.treeDataSource.data, client.organizationalUnit.uuid);
|
|
|
|
while (currentNode) {
|
|
path.unshift(currentNode.name);
|
|
currentNode = currentNode.id ? this.findParentNode(this.treeDataSource.data, currentNode.id) : null;
|
|
}
|
|
|
|
return path.join(' / ');
|
|
}
|
|
|
|
|
|
private extractUuid(idPath: string | undefined): string | null {
|
|
return idPath ? idPath.split('/').pop() || null : null;
|
|
}
|
|
|
|
|
|
clearTreeSearch(inputElement: HTMLInputElement): void {
|
|
inputElement.value = '';
|
|
this.filterTree('');
|
|
}
|
|
|
|
|
|
clearClientSearch(inputElement: HTMLInputElement): void {
|
|
inputElement.value = '';
|
|
delete this.filters['query'];
|
|
this.filterClients('');
|
|
}
|
|
|
|
clearStatusFilter(event: Event, clientSearchStatusInput: any): void {
|
|
event.stopPropagation();
|
|
delete this.filters['status'];
|
|
clientSearchStatusInput.value = null;
|
|
this.fetchClientsForNode(this.selectedNode);
|
|
}
|
|
|
|
getRunScriptContext(clientData: any[]): any {
|
|
const selectedClientNames = clientData.map(client => client.name);
|
|
|
|
if (clientData.length === 1) {
|
|
return clientData[0]; // devuelve el objeto cliente completo
|
|
} else if (
|
|
clientData.length === this.selectedClients.data.length &&
|
|
selectedClientNames.every(name => this.selectedClients.data.some(c => c.name === name))
|
|
) {
|
|
return this.selectedNode || null; // devuelve el nodo completo
|
|
} else if (clientData.length > 1) {
|
|
return clientData; // devuelve array de objetos cliente
|
|
} else if (this.selectedNode && clientData.length === 0) {
|
|
return this.selectedNode;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
openPartitionTypeModal(event: MouseEvent, node: TreeNode | null = null): void {
|
|
event.stopPropagation();
|
|
|
|
const simplifiedClientsData = node?.clients?.map((client: any) => ({
|
|
name: client.name,
|
|
partitions: client.partitions
|
|
}));
|
|
|
|
this.dialog.open(PartitionTypeOrganizatorComponent, {
|
|
width: '1200px',
|
|
data: simplifiedClientsData
|
|
});
|
|
}
|
|
|
|
changeParent(event: MouseEvent, ): void {
|
|
event.stopPropagation();
|
|
|
|
const dialogRef = this.dialog.open(ChangeParentComponent, {
|
|
data: { clients: this.selection.selected },
|
|
width: '700px',
|
|
disableClose: true,
|
|
hasBackdrop: true,
|
|
backdropClass: 'non-clickable-backdrop',
|
|
});
|
|
|
|
dialogRef.afterClosed().subscribe((result) => {
|
|
if (result) {
|
|
this.refreshData();
|
|
}
|
|
});
|
|
}
|
|
|
|
openClientTaskLogs(event: MouseEvent, client: Client): void {
|
|
this.loading = true;
|
|
event.stopPropagation();
|
|
|
|
const dialogRef = this.dialog.open(ClientTaskLogsComponent, {
|
|
width: '1200px',
|
|
data: { client },
|
|
disableClose: true,
|
|
hasBackdrop: true,
|
|
backdropClass: 'non-clickable-backdrop',
|
|
})
|
|
|
|
dialogRef.afterClosed().subscribe((result) => {
|
|
this.loading = false;
|
|
});
|
|
}
|
|
|
|
openClientPendingTasks(event: MouseEvent, client: Client): void {
|
|
event.stopPropagation();
|
|
const dialogRef = this.dialog.open(ClientPendingTasksComponent, {
|
|
width: '90vw',
|
|
height: '80vh',
|
|
data: {
|
|
client: client,
|
|
parentNode: this.selectedNode
|
|
}
|
|
});
|
|
|
|
dialogRef.afterClosed().subscribe(result => {
|
|
if (result) {
|
|
this.refreshClientData();
|
|
}
|
|
});
|
|
}
|
|
|
|
openClientLogsInNewTab(event: MouseEvent, client: Client): void {
|
|
event.stopPropagation();
|
|
if (client.ip) {
|
|
const logsUrl = `${this.baseUrl}/pcclients/${client.ip}/cgi-bin/httpd-log.sh`;
|
|
const windowName = `logs_${client.ip.replace(/\./g, '_')}`;
|
|
const newWindow = window.open(logsUrl, windowName);
|
|
if (newWindow) {
|
|
newWindow.document.write(`
|
|
<title>Logs - ${client.ip}</title>
|
|
<iframe src="${logsUrl}" width="100%" height="100%" style="border:none;"></iframe>
|
|
`);
|
|
}
|
|
} else {
|
|
this.toastr.error('No se puede acceder a los logs: IP del cliente no disponible', 'Error');
|
|
}
|
|
}
|
|
|
|
openOUPendingTasks(event: MouseEvent, node: any): void {
|
|
event.stopPropagation();
|
|
this.loading = true;
|
|
|
|
this.http.get<any>(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}&page=1&itemsPerPage=10000`).subscribe({
|
|
next: (response) => {
|
|
const allClients = response['hydra:member'] || [];
|
|
|
|
if (allClients.length === 0) {
|
|
this.toastr.warning('Esta unidad organizativa no tiene clientes');
|
|
return;
|
|
}
|
|
|
|
const ouClientData = {
|
|
name: node.name,
|
|
id: node.id,
|
|
uuid: node.uuid,
|
|
type: 'organizational-unit',
|
|
clients: allClients
|
|
};
|
|
|
|
const dialogRef = this.dialog.open(ClientPendingTasksComponent, {
|
|
width: '1200px',
|
|
data: { client: ouClientData, isOrganizationalUnit: true },
|
|
disableClose: true,
|
|
hasBackdrop: true,
|
|
backdropClass: 'non-clickable-backdrop',
|
|
});
|
|
|
|
dialogRef.afterClosed().subscribe((result) => {
|
|
this.loading = false;
|
|
});
|
|
},
|
|
error: (error) => {
|
|
console.error('Error al obtener los clientes de la unidad organizativa:', error);
|
|
this.toastr.error('Error al cargar los clientes de la unidad organizativa');
|
|
this.loading = false;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Métodos para paginación
|
|
getPaginationFrom(): number {
|
|
return (this.page * this.itemsPerPage) + 1;
|
|
}
|
|
|
|
getPaginationTo(): number {
|
|
return Math.min((this.page + 1) * this.itemsPerPage, this.length);
|
|
}
|
|
|
|
getPaginationTotal(): number {
|
|
return this.length;
|
|
}
|
|
|
|
refreshClientData(): void {
|
|
this.fetchClientsForNode(this.selectedNode);
|
|
this.toastr.success('Datos actualizados', 'Éxito');
|
|
}
|
|
|
|
exportToCSV(): void {
|
|
const headers = ['Nombre', 'IP', 'MAC', 'Estado', 'Firmware', 'Subnet', 'Parent'];
|
|
const csvData = this.arrayClients.map(client => [
|
|
client.name,
|
|
client.ip || '',
|
|
client.mac || '',
|
|
client.status || '',
|
|
client.firmwareType || '',
|
|
client.subnet || '',
|
|
client.parentName || ''
|
|
]);
|
|
|
|
const csvContent = [headers, ...csvData]
|
|
.map(row => row.map(cell => `"${cell}"`).join(','))
|
|
.join('\n');
|
|
|
|
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
|
|
const link = document.createElement('a');
|
|
const url = URL.createObjectURL(blob);
|
|
link.setAttribute('href', url);
|
|
link.setAttribute('download', `clients_${new Date().toISOString().split('T')[0]}.csv`);
|
|
link.style.visibility = 'hidden';
|
|
document.body.appendChild(link);
|
|
link.click();
|
|
document.body.removeChild(link);
|
|
|
|
this.toastr.success('Archivo CSV exportado correctamente', 'Éxito');
|
|
}
|
|
|
|
private calculateLocalStats(): void {
|
|
const clients = this.arrayClients;
|
|
this.totalStats = {
|
|
total: clients.length,
|
|
off: clients.filter(client => client.status === 'off').length,
|
|
online: clients.filter(client => ['og-live', 'linux', 'windows', 'linux-session', 'windows-session'].includes(client.status)).length,
|
|
busy: clients.filter(client => client.status === 'busy').length
|
|
};
|
|
|
|
// Actualizar tipos de firmware disponibles
|
|
this.firmwareTypes = [...new Set(clients.map(client => client.firmwareType).filter(Boolean))];
|
|
}
|
|
|
|
// Métodos para funcionalidades mejoradas
|
|
|
|
selectClient(client: any): void {
|
|
this.selectedClient = client;
|
|
}
|
|
|
|
sortColumn(columnDef: string): void {
|
|
if (this.currentSortColumn === columnDef) {
|
|
this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc';
|
|
} else {
|
|
this.currentSortColumn = columnDef;
|
|
this.sortDirection = 'asc';
|
|
}
|
|
this.sortBy = columnDef;
|
|
this.onSortChange();
|
|
}
|
|
|
|
getSortIcon(columnDef: string): string {
|
|
if (this.currentSortColumn !== columnDef) {
|
|
return 'unfold_more';
|
|
}
|
|
return this.sortDirection === 'asc' ? 'expand_less' : 'expand_more';
|
|
}
|
|
|
|
onSortChange(): void {
|
|
// Hacer nueva llamada al backend con el ordenamiento actualizado
|
|
this.fetchClientsForNode(this.selectedNode);
|
|
}
|
|
|
|
getStatusCount(status: string): number {
|
|
switch(status) {
|
|
case 'off':
|
|
return this.totalStats.off;
|
|
case 'online':
|
|
return this.totalStats.online;
|
|
case 'busy':
|
|
return this.totalStats.busy;
|
|
default:
|
|
return this.arrayClients.filter(client => client.status === status).length;
|
|
}
|
|
}
|
|
|
|
// Métodos para el árbol mejorado
|
|
|
|
expandAll(): void {
|
|
this.treeControl.expandAll();
|
|
}
|
|
|
|
collapseAll(): void {
|
|
this.treeControl.collapseAll();
|
|
}
|
|
|
|
getNodeTypeTooltip(nodeType: string): string {
|
|
switch (nodeType) {
|
|
case 'organizational-unit':
|
|
return 'Unidad Organizacional - Estructura principal de la organización';
|
|
case 'classrooms-group':
|
|
return 'Grupo de Aulas - Conjunto de aulas relacionadas';
|
|
case 'classroom':
|
|
return 'Aula - Espacio físico con equipos informáticos';
|
|
case 'clients-group':
|
|
return 'Grupo de Equipos - Conjunto de equipos informáticos';
|
|
case 'client':
|
|
return 'Equipo Informático - Computadora o dispositivo individual';
|
|
case 'group':
|
|
return 'Grupo - Agrupación lógica de elementos';
|
|
default:
|
|
return 'Elemento del árbol organizacional';
|
|
}
|
|
}
|
|
|
|
getNodeCountLabel(count: number): string {
|
|
if (count === 1) return 'elemento';
|
|
return 'elementos';
|
|
}
|
|
|
|
getStatusLabel(status: string): string {
|
|
const statusLabels: { [key: string]: string } = {
|
|
'off': 'Apagado',
|
|
'og-live': 'OG Live',
|
|
'linux': 'Linux',
|
|
'linux-session': 'Linux Session',
|
|
'windows': 'Windows',
|
|
'windows-session': 'Windows Session',
|
|
'busy': 'Ocupado',
|
|
'mac': 'Mac',
|
|
'disconnected': 'Desconectado',
|
|
'initializing': 'Inicializando'
|
|
};
|
|
return statusLabels[status] || status;
|
|
}
|
|
|
|
// Funciones para el dashboard de estadísticas
|
|
getTotalOrganizationalUnits(): number {
|
|
let total = 0;
|
|
const countOrganizationalUnits = (nodes: TreeNode[]) => {
|
|
nodes.forEach(node => {
|
|
if (node.type === 'organizational-unit') {
|
|
total += 1;
|
|
}
|
|
if (node.children) {
|
|
countOrganizationalUnits(node.children);
|
|
}
|
|
});
|
|
};
|
|
countOrganizationalUnits(this.originalTreeData);
|
|
return total;
|
|
}
|
|
|
|
getTotalClassrooms(): number {
|
|
let total = 0;
|
|
const countClassrooms = (nodes: TreeNode[]) => {
|
|
nodes.forEach(node => {
|
|
if (node.type === 'classroom') {
|
|
total += 1;
|
|
}
|
|
if (node.children) {
|
|
countClassrooms(node.children);
|
|
}
|
|
});
|
|
};
|
|
countClassrooms(this.originalTreeData);
|
|
return total;
|
|
}
|
|
|
|
// Función para actualizar estadísticas cuando cambian los datos
|
|
private updateDashboardStats(): void {
|
|
// Las estadísticas de equipos ya se calculan en calculateLocalStats()
|
|
// Solo necesitamos asegurar que se actualicen cuando cambian los datos
|
|
this.calculateLocalStats();
|
|
}
|
|
}
|