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; 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; treeFlattener: MatTreeFlattener; treeDataSource: MatTreeFlatDataSource; selectedNode: TreeNode | null = null; hasClients: boolean = false; commands: Command[] = []; commandsLoading = false; selectedClients = new MatTableDataSource([]); selection = new SelectionModel(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(); 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( this.transformer, (node) => node.level, (node) => node.expandable, (node) => node.children ); this.treeControl = new FlatTreeControl( (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(`${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(` Logs - ${client.ip} `); } } 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(`${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(); } }