import { Component, OnInit, OnDestroy, ViewChild } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Router } from '@angular/router'; import { MatDialog } from '@angular/material/dialog'; import { MatBottomSheet } from '@angular/material/bottom-sheet'; import { MatTabChangeEvent } from '@angular/material/tabs'; 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 { CreateOrganizationalUnitComponent } from './shared/organizational-units/create-organizational-unit/create-organizational-unit.component'; import { CreateClientComponent } from './shared/clients/create-client/create-client.component'; import { EditOrganizationalUnitComponent } from './shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component'; import { EditClientComponent } from './shared/clients/edit-client/edit-client.component'; import { ShowOrganizationalUnitComponent } from './shared/organizational-units/show-organizational-unit/show-organizational-unit.component'; import { TreeViewComponent } from './shared/tree-view/tree-view.component'; import { LegendComponent } from './shared/legend/legend.component'; import { ClientTabViewComponent } from './components/client-tab-view/client-tab-view.component'; import { OrganizationalUnitTabViewComponent } from './components/organizational-unit-tab-view/organizational-unit-tab-view.component'; import { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/delete-modal.component'; import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom-view-modal'; import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { MatPaginator } from '@angular/material/paginator'; 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 { baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; organizationalUnits: UnidadOrganizativa[] = []; selectedUnidad: UnidadOrganizativa | null = null; selectedDetail: UnidadOrganizativa | null = null; loading = false; searchTerm = ''; treeControl: FlatTreeControl; treeFlattener: MatTreeFlattener; treeDataSource: MatTreeFlatDataSource; selectedNode: TreeNode | null = null; commands: Command[] = []; commandsLoading = false; selectedClients = new MatTableDataSource([]); cols = 4; selectedClientsOriginal: Client[] = []; currentView: 'card' | 'list' = 'list'; isTreeViewActive = false; savedFilterNames: [string, string][] = []; selectedTreeFilter = ''; syncStatus = false; syncingClientId: string | null = null; private originalTreeData: TreeNode[] = []; displayedColumns: string[] = ['name', 'oglive', 'status', 'maintenace', 'subnet', 'pxeTemplate', 'parentName', 'actions']; private _sort!: MatSort; private _paginator!: MatPaginator; @ViewChild(MatSort) set matSort(ms: MatSort) { this._sort = ms; if (this.selectedClients) { this.selectedClients.sort = this._sort; } } @ViewChild(MatPaginator) set matPaginator(mp: MatPaginator) { this._paginator = mp; if (this.selectedClients) { this.selectedClients.paginator = this._paginator; } } @ViewChild('clientTab') clientTabComponent!: ClientTabViewComponent; @ViewChild('organizationalUnitTab') organizationalUnitTabComponent!: OrganizationalUnitTabViewComponent; 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 toastr: ToastrService ) { 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); } ngOnInit(): void { this.search(); this.getFilters(); this.updateGridCols(); 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) ); }; } ngOnDestroy(): void { window.removeEventListener('resize', this.updateGridCols); this.subscriptions.unsubscribe(); } private transformer = (node: TreeNode, level: number): FlatNode => ({ name: node.name, type: node.type, level, expandable: !!node.children?.length, hasClients: node.hasClients, ip: node.ip, '@id': node['@id'], }); 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.isTreeViewActive = false; } onTabChange(event: MatTabChangeEvent): void { if (event.index === 2) { this.clientTabComponent.search(); } else if (event.index === 3) { this.organizationalUnitTabComponent.search(); } } 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); } ) ); } loadSelectedFilter(savedFilter: [string, string]): void { this.subscriptions.add( this.dataService.getFilter(savedFilter[1]).subscribe( (response) => { if (response) { console.log('Filter:', response.filters); } }, (error) => { console.error('Error:', error); } ) ); } 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; } ) ); } onSelectUnidad(unidad: UnidadOrganizativa): void { this.selectedUnidad = unidad; this.selectedDetail = unidad; this.selectedClients.data = this.collectAllClients(unidad); this.selectedClientsOriginal = [...this.selectedClients.data]; this.loadChildrenAndClients(unidad.id).then((fullData) => { const treeData = this.convertToTreeData(fullData); this.treeDataSource.data = treeData[0]?.children || []; }); this.isTreeViewActive = true; console.log('Selected unidad:', unidad); } private collectAllClients(node: UnidadOrganizativa): Client[] { let clients = (node.clients || []).map(client => ({ ...client, parentName: node.name })); if (node.children) { node.children.forEach((child) => { clients = clients.concat(this.collectAllClients(child).map(client => ({ ...client, parentName: client.parentName || '' }))); }); } return clients; } private async loadChildrenAndClients(id: string): Promise { try { const childrenData = await this.dataService.getChildren(id).toPromise(); const processHierarchy = (nodes: UnidadOrganizativa[]): UnidadOrganizativa[] => { return nodes.map((node) => ({ ...node, children: node.children ? processHierarchy(node.children) : [], })); }; return { ...this.selectedUnidad!, children: childrenData ? processHierarchy(childrenData) : [], }; } catch (error) { console.error('Error loading children:', error); return this.selectedUnidad!; } } private convertToTreeData(data: UnidadOrganizativa): TreeNode[] { const processNode = (node: UnidadOrganizativa): TreeNode => ({ name: node.name, type: node.type, '@id': node['@id'], children: node.children?.map(processNode) || [], hasClients: (node.clients?.length ?? 0) > 0, }); return [processNode(data)]; } onNodeClick(node: TreeNode): void { this.selectedNode = node; this.fetchClientsForNode(node); } private fetchClientsForNode(node: TreeNode): void { if (node.hasClients && node['@id']) { this.subscriptions.add( this.http.get<{ clients: Client[] }>(`${this.baseUrl}${node['@id']}`).subscribe( (data) => { const clientsWithParentName = (data.clients || []).map(client => ({ ...client, parentName: node.name })); this.selectedClients.data = clientsWithParentName; this.selectedClients._updateChangeSubscription(); if (this._paginator) { this._paginator.firstPage(); } }, (error) => { console.error('Error fetching clients:', error); } ) ); } else { this.selectedClients.data = []; this.selectedClients._updateChangeSubscription(); } } getNodeIcon(node: TreeNode): string { switch (node.type) { case NodeType.OrganizationalUnit: return 'apartment'; case NodeType.ClassroomsGroup: return 'doors'; case NodeType.Classroom: return 'school'; case NodeType.ClientsGroup: return 'lan'; case NodeType.Client: return 'computer'; default: return 'group'; } } addOU(event: MouseEvent, parent: TreeNode | null = null): void { event.stopPropagation(); const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, { data: { parent }, width: '900px', }); dialogRef.afterClosed().subscribe(() => { this.refreshOrganizationalUnits(); }); } addClient(event: MouseEvent, organizationalUnit: TreeNode | null = null): void { event.stopPropagation(); const dialogRef = this.dialog.open(CreateClientComponent, { data: { organizationalUnit }, width: '900px', }); dialogRef.afterClosed().subscribe(() => { this.refreshOrganizationalUnits(); if (organizationalUnit && organizationalUnit['@id']) { this.refreshClientsForNode(organizationalUnit); } }); } private refreshOrganizationalUnits(): void { this.subscriptions.add( this.dataService.getOrganizationalUnits().subscribe( (data) => { this.organizationalUnits = data; if (this.selectedUnidad) { this.loadChildrenAndClients(this.selectedUnidad?.id || '').then((updatedData) => { this.selectedUnidad = updatedData; const treeData = this.convertToTreeData(updatedData); this.originalTreeData = treeData[0]?.children || []; this.treeDataSource.data = [...this.originalTreeData]; }); } }, (error) => console.error('Error fetching organizational units', error) ) ); } onEditNode(event: MouseEvent, node: TreeNode | null): void { event.stopPropagation(); const uuid = node ? this.extractUuid(node['@id']) : null; if (!uuid) return; if (node && node.type !== NodeType.Client) { this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' }); } else { this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' }); } } onDeleteClick(event: MouseEvent, node: TreeNode | null, clientNode?: TreeNode | null): void { event.stopPropagation(); const uuid = node ? this.extractUuid(node['@id']) : null; if (!uuid) return; if (!node) return; const dialogRef = this.dialog.open(DeleteModalComponent, { width: '400px', data: { name: node.name }, }); dialogRef.afterClosed().subscribe((result) => { if (result === true) { this.deleteEntity(uuid, node.type, node); } }); } private deleteEntity(uuid: string, type: string, node: TreeNode): void { this.subscriptions.add( this.dataService.deleteElement(uuid, type).subscribe( () => { this.refreshOrganizationalUnits(); if (type === NodeType.Client) { this.refreshClientsForNode(node); } this.toastr.success('Entidad eliminada exitosamente'); }, (error) => { console.error('Error deleting entity:', error); this.toastr.error('Error al eliminar la entidad', error.message); } ) ); } private refreshClientsForNode(node: TreeNode): void { if (!node['@id']) { this.selectedClients.data = []; return; } this.fetchClientsForNode(node); } onEditClick(event: MouseEvent, type: string, uuid: string): void { event.stopPropagation(); if (type !== NodeType.Client) { this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' }); } else { this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' }); } } onRoomMap(room: TreeNode | null): void { if (!room || !room['@id']) return; this.subscriptions.add( this.http.get<{ clients: Client[] }>(`${this.baseUrl}${room['@id']}`).subscribe( (response) => { this.dialog.open(ClassroomViewDialogComponent, { width: '90vw', data: { clients: response.clients }, }); }, (error) => { console.error('Error fetching room data:', error); } ) ); } fetchCommands(): void { this.commandsLoading = true; this.subscriptions.add( this.http.get<{ 'hydra:member': Command[] }>(`${this.baseUrl}/commands?page=1&itemsPerPage=30`).subscribe( (response) => { this.commands = response['hydra:member']; this.commandsLoading = false; }, (error) => { console.error('Error fetching commands:', error); this.commandsLoading = false; } ) ); } executeCommand(command: Command, selectedNode: TreeNode | null): void { if (!selectedNode) { this.toastr.error('No hay un nodo seleccionado.'); return; } else { this.toastr.success(`Ejecutando comando: ${command.name} en ${selectedNode.name}`); } } executeClientCommand(command: Command, client: Client): void { this.toastr.success(`Ejecutando comando: ${command.name} en ${client.name}`); } onShowClientDetail(event: MouseEvent, client: Client): void { event.stopPropagation(); this.router.navigate(['clients', client.uuid], { state: { clientData: client } }); } onShowDetailsClick(event: MouseEvent, data: TreeNode | null): void { event.stopPropagation(); if (data && data.type !== NodeType.Client) { this.dialog.open(ShowOrganizationalUnitComponent, { data: { data }, width: '700px' }); } else { if (data) { this.router.navigate(['clients', this.extractUuid(data['@id'])], { state: { clientData: data } }); } } } onTreeClick(event: MouseEvent, data: TreeNode): void { event.stopPropagation(); if (data.type !== NodeType.Client) { this.dialog.open(TreeViewComponent, { data: { data }, width: '800px' }); } } openBottomSheet(): void { this.bottomSheet.open(LegendComponent); } iniciarTour(): void { this.joyrideService.startTour({ steps: ['groupsTitleStepText', 'addStep', 'keyStep', 'unitStep', 'elementsStep', 'tabsStep'], showPrevButton: true, themeColor: '#3f51b5', }); } hasChild = (_: number, node: FlatNode): boolean => node.expandable; isLeafNode = (_: number, node: FlatNode): boolean => !node.expandable; filterTree(searchTerm: string, filterType: string): void { const filterNodes = (nodes: TreeNode[]): TreeNode[] => { const filteredNodes: TreeNode[] = []; for (const node of nodes) { const matchesName = node.name.toLowerCase().includes(searchTerm.toLowerCase()); const matchesType = filterType ? node.type.toLowerCase() === filterType.toLowerCase() : true; const filteredChildren = node.children ? filterNodes(node.children) : []; if ((matchesName && matchesType) || filteredChildren.length > 0) { filteredNodes.push({ ...node, children: filteredChildren }); } } return filteredNodes; }; const filteredData = filterNodes(this.originalTreeData); this.treeDataSource.data = filteredData; } onTreeFilterInput(event: Event): void { const input = event.target as HTMLInputElement; const searchTerm = input?.value || ''; this.filterTree(searchTerm, this.selectedTreeFilter); } onClientFilterInput(event: Event): void { const input = event.target as HTMLInputElement; const searchTerm = input?.value || ''; this.filterClients(searchTerm); } filterClients(searchTerm: string): void { this.searchTerm = searchTerm.trim().toLowerCase(); this.selectedClients.filter = this.searchTerm; } public setSelectedNode(node: TreeNode): void { this.selectedNode = node; } getStatus(client: Client): void { if (!client.uuid || !client['@id']) return; this.syncingClientId = client.uuid; this.syncStatus = true; this.subscriptions.add( this.http.post(`${this.baseUrl}${client['@id']}/agent/status`, {}).subscribe( () => { this.toastr.success('Cliente actualizado correctamente'); this.search(); this.syncStatus = false; this.syncingClientId = null; }, () => { this.toastr.error('Error de conexión con el cliente'); this.syncStatus = false; this.syncingClientId = null; } ) ); } private extractUuid(idPath: string | undefined): string | null { return idPath ? idPath.split('/').pop() || null : null; } }