import { Component, OnInit, 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 { MatMenuTrigger } from '@angular/material/menu'; import { ToastrService } from 'ngx-toastr'; import { JoyrideService } from 'ngx-joyride'; import { FlatTreeControl } from '@angular/cdk/tree'; import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree'; import { DataService } from './services/data.service'; import { UnidadOrganizativa } 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'; interface TreeNode { name: string; type: string; children?: TreeNode[]; ip?: string; '@id'?: string; hasClients?: boolean; status?: string ; } interface FlatNode { name: string; type: string; level: number; expandable: boolean; ip?: string; hasClients?: boolean; } @Component({ selector: 'app-groups', templateUrl: './groups.component.html', styleUrls: ['./groups.component.css'] }) export class GroupsComponent implements OnInit { baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; organizationalUnits: UnidadOrganizativa[] = []; selectedUnidad: UnidadOrganizativa | null = null; selectedDetail: any | null = null; loading: boolean = false; loadingChildren: boolean = false; searchTerm: string = ''; treeControl: FlatTreeControl; treeFlattener: MatTreeFlattener; treeDataSource: MatTreeFlatDataSource; selectedNode: TreeNode | null = null; commands: any[] = []; commandsLoading: boolean = false; selectedClients: any[] = []; cols: number = 4; selectedClientsOriginal: any[] = []; currentView: 'card' | 'list' = 'list'; isTreeViewActive: boolean = false; savedFilterNames: any[] = []; @ViewChild('clientTab') clientTabComponent!: ClientTabViewComponent; @ViewChild('organizationalUnitTab') organizationalUnitTabComponent!: OrganizationalUnitTabViewComponent; 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( (node: TreeNode, level: number) => ({ name: node.name, type: node.type, level, expandable: !!node.children?.length, hasClients: node.hasClients, ip: node.ip, ['@id']: node['@id'] }), 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()); } toggleView(view: 'card' | 'list') { 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 = []; 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.dataService.getFilters().subscribe( data => { this.savedFilterNames = data.map((filter: any) => [filter.name, filter.uuid]); }, error => { console.error('Error fetching filters:', error); } ); } loadSelectedFilter(savedFilter: any) { const url = `${this.baseUrl}/views/` + savedFilter[1]; console.log('llamando a:', url); this.dataService.getFilter(savedFilter[1]).subscribe(response => { console.log('Response from server:', response.filters); if (response) { console.log('Filter1:', response.filters); } }, error => { console.error('Error:', error); }); } search(): void { this.loading = true; this.dataService.getOrganizationalUnits(this.searchTerm).subscribe( data => { this.organizationalUnits = data; this.loading = false; }, error => { console.error('Error fetching unidades organizativas', error); this.loading = false; } ); } onSelectUnidad(unidad: UnidadOrganizativa): void { this.selectedUnidad = unidad; this.selectedDetail = unidad; this.loadChildrenAndClients(unidad.id).then(fullData => { const treeData = this.convertToTreeData(fullData); this.treeDataSource.data = treeData[0]?.children || []; }); this.isTreeViewActive = true; } async loadChildrenAndClients(id: string): Promise { try { const childrenData = await this.dataService.getChildren(id).toPromise(); const processHierarchy = (nodes: UnidadOrganizativa[]): TreeNode[] => { return nodes.map(node => ({ name: node.name, type: node.type, '@id': node['@id'], children: node.children ? processHierarchy(node.children) : [], clients: node.clients || [] })); }; return { ...this.selectedUnidad, children: childrenData ? processHierarchy(childrenData) : [] }; } catch (error) { console.error('Error loading children:', error); return this.selectedUnidad; } } convertToTreeData(data: any): TreeNode[] { const processNode = (node: UnidadOrganizativa): TreeNode => ({ name: node.name, type: node.type, '@id': node['@id'], children: node.children?.map(processNode) || [], hasClients: node.clients && node.clients.length > 0, }); return [processNode(data)]; } onNodeClick(node: TreeNode): void { this.selectedNode = node; if (node.hasClients) { const url = `${this.baseUrl}${node['@id']}`; this.http.get(url).subscribe( (data: any) => { this.selectedClientsOriginal = [...data.clients]; this.selectedClients = data.clients || []; }, (error) => { console.error('Error fetching clients:', error); } ); } else { this.selectedClients = []; this.selectedClientsOriginal = []; } } getNodeIcon(node: any): string { console.log('Node:', node); switch (node.type) { case 'organizational-unit': return 'apartment'; case 'classrooms-group': return 'doors'; case 'classroom': return 'school'; case 'clients-group': return 'lan'; case 'client': return 'computer'; default: return 'group'; } } addOU(event: MouseEvent, parent: any = null): void { event.stopPropagation(); const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, { data: { parent }, width: '900px' }); dialogRef.afterClosed().subscribe(() => { this.dataService.getOrganizationalUnits().subscribe( data => { this.organizationalUnits = data; }, error => console.error('Error fetching unidades organizativas', error) ); }); } addClient(event: MouseEvent, organizationalUnit: any = null): void { event.stopPropagation(); const dialogRef = this.dialog.open(CreateClientComponent, { data: { organizationalUnit }, width: '900px' }); dialogRef.afterClosed().subscribe(() => { this.dataService.getOrganizationalUnits().subscribe( data => { this.organizationalUnits = data; if (organizationalUnit && organizationalUnit.id) { this.loadChildrenAndClients(organizationalUnit.id); } }, error => console.error('Error fetching unidades organizativas', error) ); }); } setSelectedNode(node: TreeNode): void { this.selectedNode = node; } onEditNode(event: MouseEvent, node: TreeNode | null): void { if (!node) return; const uuid = node['@id'] ? node['@id'].split('/').pop() : ''; const type = node.type; event.stopPropagation(); if (type !== 'client') { this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' }); } else { this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' }); } } onDelete(node: TreeNode | null): void { console.log('Deleting node:', node); } onDeleteClick(event: MouseEvent, node: TreeNode | null, clientNode?: TreeNode | null): void { console.log('Deleting node or client:', node); const uuid = node && node['@id'] ? node['@id'].split('/').pop() || '' : ''; const name = node?.name || 'Elemento desconocido'; const type = node?.type || ''; event.stopPropagation(); const dialogRef = this.dialog.open(DeleteModalComponent, { width: '400px', data: { name } }); dialogRef.afterClosed().subscribe(result => { if (result === true) { this.dataService.deleteElement(uuid, type).subscribe( () => { console.log('Entity deleted successfully:', uuid); this.loadChildrenAndClients(this.selectedUnidad?.id || '').then(updatedData => { const treeData = this.convertToTreeData(updatedData); this.treeDataSource.data = treeData[0]?.children || []; }); if (type === 'client' && clientNode) { console.log('Refreshing clients for node:', clientNode); this.refreshClients(clientNode); } this.dataService.getOrganizationalUnits().subscribe( data => { this.organizationalUnits = data; }, error => console.error('Error fetching unidades organizativas:', error) ); this.toastr.success('Entidad eliminada exitosamente'); }, error => { console.error('Error deleting entity:', error); this.toastr.error('Error al eliminar la entidad', error.message); } ); } }); } private refreshClients(node: TreeNode): void { if (!node || !node['@id']) { console.warn('Node or @id is missing, clearing clients.'); this.selectedClients = []; return; } const url = `${this.baseUrl}${node['@id']}`; console.log('Fetching clients for node with URL:', url); this.http.get(url).subscribe( (data: any) => { console.log('Response data:', data); if (data && Array.isArray(data.clients)) { this.selectedClients = data.clients; console.log('Clients updated successfully:', this.selectedClients); } else { console.warn('No "clients" field found in response, clearing clients.'); this.selectedClients = []; } }, error => { console.error('Error refreshing clients:', error); const errorMessage = error.status === 404 ? 'No se encontraron clientes para este nodo.' : 'Error al comunicarse con el servidor.'; this.toastr.error(errorMessage); } ); } onEditClick(event: MouseEvent, type: any, uuid: string): void { event.stopPropagation(); if (type != 'client') { this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' }); } else { this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' }); } } onRoomMap(room: any): void { this.http.get('https://127.0.0.1:8443' + room['@id']).subscribe( (response: any) => { this.dialog.open(ClassroomViewDialogComponent, { width: '90vw', data: { clients: response.clients } }); }, (error: any) => { console.error('Error en la solicitud HTTP:', error); } ); } fetchCommands(): void { this.commandsLoading = true; this.http.get('https://127.0.0.1:8443/commands?page=1&itemsPerPage=30').subscribe( (response: any) => { this.commands = response['hydra:member']; this.commandsLoading = false; }, (error) => { console.error('Error fetching commands:', error); this.commandsLoading = false; } ); } executeCommand(command: any, selectedNode: any): void { this.toastr.success('Ejecutando comando: ' + command.name + " en " + selectedNode.name); } onClientActions(client: any): void { console.log('Client actions:', client); } onShowClientDetail(event: MouseEvent, client: any): void { event.stopPropagation(); this.router.navigate(['clients', client.uuid], { state: { clientData: client } }); } onShowDetailsClick(event: MouseEvent, data: any): void { event.stopPropagation(); if (data.type != 'client') { this.dialog.open(ShowOrganizationalUnitComponent, { data: { data }, width: '700px' }); } if (data.type == 'client') { this.router.navigate(['clients', data['@id'].split('/').pop()], { state: { clientData: data } }); } } onTreeClick(event: MouseEvent, data: any): void { event.stopPropagation(); if (data.type != '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; /* filtros */ selectedTreeFilter: string = ''; filterTree(searchTerm: string, filterType: string): void { const filterNodes = (nodes: any[]): any[] => { return nodes .map(node => { const matchesName = node.name.toLowerCase().includes(searchTerm.toLowerCase()); const matchesType = filterType ? node.type.toLowerCase() === filterType.toLowerCase() : true; // Filtrar hijos recursivamente const filteredChildren = node.children ? filterNodes(node.children) : []; // Si el nodo o algún hijo coincide, incluirlo if (matchesName && matchesType || filteredChildren.length > 0) { return { ...node, children: filteredChildren }; } return null; // Excluir nodos que no coinciden }) .filter(node => node !== null); // Eliminar nodos excluidos }; // Aplicar filtro sobre el árbol completo const filteredData = filterNodes(this.treeDataSource.data); // Actualizar el origen de datos del árbol this.treeDataSource.data = filteredData; } onTreeFilterInput(event: Event): void { const input = event.target as HTMLInputElement; const searchTerm = input?.value || ''; // Maneja el caso de nulo o indefinido this.filterTree(searchTerm, this.selectedTreeFilter); } onClientFilterInput(event: Event): void { const input = event.target as HTMLInputElement; const searchTerm = input?.value || ''; // Maneja valores nulos o indefinidos this.filterClients(searchTerm); } filterClients(searchTerm: string): void { if (!searchTerm) { // Restaurar los datos originales si no hay filtro this.selectedClients = [...this.selectedClientsOriginal]; return; } const lowerTerm = searchTerm.toLowerCase(); this.selectedClients = this.selectedClientsOriginal.filter(client => { const matchesName = client.name.toLowerCase().includes(lowerTerm); const matchesIP = client.ip?.toLowerCase().includes(lowerTerm) || false; const matchesStatus = client.status?.toLowerCase().includes(lowerTerm) || false; const matchesMac = client.mac?.toLowerCase().includes(lowerTerm) || false; return matchesName || matchesIP || matchesStatus || matchesMac; }); } }