diff --git a/ogWebconsole/package-lock.json b/ogWebconsole/package-lock.json index befd096..461ff5a 100644 --- a/ogWebconsole/package-lock.json +++ b/ogWebconsole/package-lock.json @@ -24,6 +24,7 @@ "jwt-decode": "^4.0.0", "ngx-joyride": "^2.5.0", "ngx-toastr": "^19.0.0", + "papaparse": "^5.4.1", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "^0.14.6" @@ -35,6 +36,7 @@ "@angular/localize": "^18.1.0", "@ngx-env/builder": "^18.0.1", "@types/jasmine": "~5.1.0", + "@types/papaparse": "^5.3.15", "jasmine-core": "~5.1.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", @@ -5902,6 +5904,15 @@ "@types/node": "*" } }, + "node_modules/@types/papaparse": { + "version": "5.3.15", + "resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.15.tgz", + "integrity": "sha512-JHe6vF6x/8Z85nCX4yFdDslN11d+1pr12E526X8WAfhadOeaOTx5AuIkvDKIBopfvlzpzkdMx4YyvSKCM9oqtw==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.15", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz", @@ -11955,6 +11966,11 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/papaparse": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz", + "integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", diff --git a/ogWebconsole/package.json b/ogWebconsole/package.json index e3edac1..a2a5595 100644 --- a/ogWebconsole/package.json +++ b/ogWebconsole/package.json @@ -26,6 +26,7 @@ "jwt-decode": "^4.0.0", "ngx-joyride": "^2.5.0", "ngx-toastr": "^19.0.0", + "papaparse": "^5.4.1", "rxjs": "~7.8.0", "tslib": "^2.3.0", "zone.js": "^0.14.6" @@ -37,6 +38,7 @@ "@angular/localize": "^18.1.0", "@ngx-env/builder": "^18.0.1", "@types/jasmine": "~5.1.0", + "@types/papaparse": "^5.3.15", "jasmine-core": "~5.1.0", "karma": "~6.4.0", "karma-chrome-launcher": "~3.2.0", diff --git a/ogWebconsole/src/app/components/groups/groups.component.css b/ogWebconsole/src/app/components/groups/groups.component.css index b2923f7..f7b4608 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.css +++ b/ogWebconsole/src/app/components/groups/groups.component.css @@ -1,321 +1,324 @@ -.groupLists-container { - display: flex; - flex-wrap: wrap; - height: auto; - margin-bottom: 30px; -} - -.search-container { - display: flex; - flex-grow: 1; - margin: 10px; -} - -.search-container mat-form-field { - width: 50%; -} - -.card { - flex-grow: 1; - margin: 10px; - - border: 2px solid rgba(102, 102, 102, 0.103) +.card-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 20px; + padding: 20px; } .header-container { display: flex; justify-content: space-between; align-items: center; - height: 100px; - padding: 10px; -} - -.unidad-card { - flex: 1 1 20%; - background-color: #fafafa; - height: 600px; - overflow-y: auto; - box-shadow: none !important; - -} - -.elements-card { - flex: 1 1 75%; - background-color: #fafafa; - height: 600px; - overflow-y: auto; - box-shadow: none !important; -} - -.element-content { - overflow-y: auto; -} - -.title { - margin-left: 10px; -} - -.details-card, .classroom-view { - flex: 1 1 25%; -} - -mat-card-title { - display: flex; - justify-content: space-between; - margin: 10px; -} - -.title-with-breadcrumb { - display: flex; - flex-direction: column; -} - -mat-card-subtitle { - font-size: 0.875rem; - color: rgba(0, 0, 0, 0.54); -} - -mat-card-subtitle a { - cursor: pointer; - text-decoration: underline; - color: #929292; -} - -mat-card-subtitle a:hover { - text-decoration: none; + padding: 20px; + background-color: #f5f5f5; + border-bottom: 1px solid #ddd; } .groups-button-row { display: flex; - gap: 10px; + gap: 15px; } -.item-content { +.button-container { display: flex; - width: 100%; + justify-content: center; + margin: 20px 0; } -.item-content mat-icon { - margin-right: 10px; +button[mat-raised-button] { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 16px; + font-size: 16px; } -.clickable-item:hover { +mat-card { + background-color: #ffffff; + border-radius: 10px; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: transform 0.2s, box-shadow 0.2s; + overflow: hidden; + align-items: center; +} + +mat-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15); +} + +.unidad-card { cursor: pointer; -} - -.selected-item { - background-color: #e0e0e0; -} - -.actions { + padding: 16px; + font-size: 14px; display: flex; - margin-left: auto; - align-self: center; + flex-direction: column; + justify-content: space-between; +} + +.unidad-card.selected-item { + border: 2px solid #1976d2; +} + +mat-card-title { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: flex; + align-items: center; + font-size: 16px; + font-weight: bold; + color: #333; + margin: 0; +} + +mat-card-title mat-icon { + font-size: 20px; + margin-right: 8px; + color: #1976d2; +} + +mat-card-actions { + display: flex; + justify-content: flex-end; + gap: 10px; + margin-top: 16px; } .actions mat-icon { - cursor: pointer; - margin-left: 16px; color: #757575; + cursor: pointer; + transition: color 0.2s; } .actions mat-icon:hover { - color: #212121; + color: #1976d2; } .empty-list { display: flex; justify-content: center; align-items: center; - height: 100%; + height: 200px; + font-size: 16px; + color: #777; } -mat-spinner { - margin: 0 auto; - align-self: center; -} - -.container { +.search-container { display: flex; - justify-content: flex-end; -} -.classroomBtn-container { - display: flex; - justify-content: flex-end; - width: 100%; + gap: 20px; + margin: 20px 0; } -.container { - display: flex; - flex-direction: column; -} - -.header { - display: flex; - align-items: center; - gap: 10px; - padding: 20px; -} - -.header mat-form-field { - width: 300px; -} - -.main-content { - display: flex; +.search-container mat-form-field { + flex: 1; } .filters { padding: 20px; + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 8px; +} + +.details-container { display: flex; flex-direction: column; - width: 300px; -} - -.saved-filter { - display: flex; - flex-direction: column; - width: 300px; - margin-bottom: 10px; - padding: 10px; -} - -.results { - width: 100%; -} - -.results-container { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); - gap: 16px; - margin-bottom: 16px; -} - -.result-card { - width: 100%; - max-width: 250px; - height: 250px; -} -.paginator-container { - display: flex; - justify-content: center; - margin-bottom: 30px; - -} - -.divider { - margin: 20px 0; -} - -mat-card { - margin-bottom: 20px; -} - -.mat-tooltip { - white-space: pre-line; -} - -.classroom-grid { - display: flex; - flex-wrap: wrap; - gap: 16px; - justify-content: flex-start; /* Opcional: para alinear a la izquierda */ -} - -.classroom-item { - flex: 0 1 calc(16.66% - 16px); /* 6 columnas */ - max-width: calc(16.66% - 16px); + gap: 20px; + padding: 20px; + align-items: center; text-align: center; - box-sizing: border-box; } -.classroom-pc { - position: relative; +.details-placeholder { + background-color: #f5f5f5; + border: 1px solid #ddd; + border-radius: 8px; + padding: 20px; + width: 100%; + max-width: 600px; +} + +button[mat-raised-button] { + align-self: flex-start; +} + + +@media (max-width: 1024px) { + .card-container { + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 15px; + } + + .header-container { + flex-direction: column; + gap: 10px; + } + + .groups-button-row { + flex-wrap: wrap; + gap: 10px; + } +} + +@media (max-width: 768px) { + mat-card { + padding: 12px; + } + + .unidad-card { + font-size: 12px; + } +} + +.details-container { display: flex; flex-direction: column; align-items: center; - padding: 8px; - background-color: #f4f4f4; + gap: 20px; + padding: 30px; + background-color: #f9f9f9; + border-radius: 12px; + box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1); + max-width: 1200px; + margin: 20px auto; +} + +.details-placeholder { + background-color: #ffffff; + border: 1px solid #e0e0e0; + border-radius: 12px; + padding: 20px; + width: 100%; + max-width: 800px; + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05); +} + +mat-tree { + width: 100%; + background-color: #f9f9f9; + border: 1px solid #ddd; border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + padding: 10px; } -.pc-image { - width: 80px; - height: 80px; -} - -.pc-details { - margin-top: 8px; - font-size: 12px; -} - -.client-name { - font-weight: bold; - display: block; -} - -.client-ip, -.client-mac { - color: #666; - font-size: 10px; - display: block; -} - -.pc-actions { - margin-top: 8px; +mat-tree mat-tree-node { display: flex; - justify-content: center; - gap: 8px; + align-items: center; + padding: 10px; + border-radius: 6px; + transition: background-color 0.2s, color 0.2s; + cursor: pointer; } -.pc-og-live { - border: 2px solid #4caf50; +mat-tree mat-tree-node:hover { + background-color: #e3f2fd; } -.pc-busy { - border: 2px solid #ff9800; +mat-tree mat-tree-node button.mat-icon-button { + margin-left: auto; + color: #757575; } -.pc-off { - border: 2px solid #f44336; +mat-tree mat-tree-node button.mat-icon-button:hover { + color: #1976d2; } -.pc-linux { - border: 2px solid #9c27b0; +mat-tree mat-tree-node span { + font-size: 16px; + font-weight: 500; + color: #555; } -.pc-windows { - border: 2px solid #2196f3; +mat-tree mat-tree-node mat-icon { + margin-right: 10px; + color: #757575; + transition: color 0.2s; } -/* Pantallas medianas: 4 columnas */ -@media (max-width: 1024px) { - .classroom-item { - flex: 0 1 calc(25% - 16px); /* 4 columnas */ - } +mat-tree mat-tree-node.expandable mat-icon { + color: black; + cursor: pointer; } -/* Pantallas pequeñas: 2 columnas */ -@media (max-width: 768px) { - .classroom-item { - flex: 0 1 calc(50% - 16px); /* 2 columnas */ - } +mat-tree mat-tree-node.expandable.disabled mat-icon { + color: grey; + opacity: 0.5; + cursor: not-allowed; } -/* Pantallas muy pequeñas: 1 columna */ -@media (max-width: 480px) { - .classroom-item { - flex: 0 1 100%; /* 1 columna */ - } +mat-tree mat-tree-node:hover mat-icon { + color: black; } -.client-text { - font-size: 0.8rem; - color: rgba(0, 0, 0, 0.54); +/* Iconos por tipo */ +mat-tree mat-tree-node mat-icon.node-icon { + color: #757575; + margin-right: 10px; } -.client-name { - font-size: 0.9rem; - text-align: center; +mat-tree mat-tree-node mat-icon.node-icon.organizational-unit { + color: #1976d2; /* Azul para unidades organizativas */ +} + +mat-tree mat-tree-node mat-icon.node-icon.classroom { + color: #388e3c; /* Verde para aulas */ +} + +mat-tree mat-tree-node mat-icon.node-icon.client { + color: #f57c00; /* Naranja para clientes */ +} + +mat-tree mat-tree-node mat-icon.node-icon.group { + color: #d32f2f; /* Rojo para grupos */ +} + + +mat-tree mat-tree-node button.mat-icon-button { + margin-right: 10px; +} + +mat-tree mat-tree-node button.mat-icon-button.disabled-toggle { + color: grey; + opacity: 0.5; + cursor: not-allowed; +} + +mat-tree mat-tree-node button.mat-icon-button.disabled-toggle:hover { + background-color: transparent; /* Desactiva hover */ +} + +mat-tree mat-tree-node mat-icon { + margin-right: 10px; + color: #757575; + transition: color 0.2s; +} + +mat-tree mat-tree-node mat-icon.node-icon.organizational-unit { + color: #1976d2; /* Azul para unidades organizativas */ +} + +mat-tree mat-tree-node mat-icon.node-icon.classroom { + color: #388e3c; /* Verde para aulas */ +} + +mat-tree mat-tree-node mat-icon.node-icon.client { + color: #f57c00; /* Naranja para clientes */ +} + +mat-tree mat-tree-node mat-icon.node-icon.group { + color: #d32f2f; /* Rojo para grupos */ +} + +mat-tree mat-tree-node:hover { + background-color: #e3f2fd; + cursor: pointer; +} + +mat-tree mat-tree-node.disabled { + cursor: not-allowed; +} + +mat-tree mat-tree-node.disabled:hover { + background-color: transparent; /* Desactiva hover */ } diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index 2de059a..d2d2d6f 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -4,142 +4,199 @@ -

{{ 'adminGroupsTitle' | translate }}

+

+ {{ 'adminGroupsTitle' | translate }} +

- - - + + +
-
- - {{ 'organizationalUnitTitle' | translate }} - - - - -
- apartment - {{ unidad.name }} - - more_vert - - - - - - - - - - - - -
-
-
-
-
- - - -
- {{ 'internalElementsTitle' | translate }} - - - {{ crumb }} - > - - + +
+ + + + apartment {{ unidad.name }} + + + +
+
- - - - - - -
- info - {{ 'noInternalElementsMessage' | translate }} -
- -
- - apartment - meeting_room - school - computer - lan - help_outline - - {{ child.name }} -
- more_vert - - - - -
-
-
-
- - -
-
-
- PC Icon -
- {{ pc.name }} - {{ pc.ip }} - {{ pc.mac }} -
-
- - -
-
-
-
- -
+ + + + + + + +
+ + + + +
+
+

{{ 'Details of' | translate }} {{ selectedUnidad?.name }}

+ + + + + + + {{ + node.type === 'organizational-unit' + ? 'apartment' + : node.type === 'classroom' + ? 'school' + : node.type === 'client' + ? 'computer' + : 'group' + }} + + {{ node.name }} ({{ node.type }}) + + + + + + + {{ + node.type === 'organizational-unit' + ? 'apartment' + : node.type === 'classroom' + ? 'school' + : node.type === 'client' + ? 'computer' + : 'group' + }} + + {{ node.name }} ({{ node.type }}) + + - IP: {{ node.ip }} + + + + + + + + {{ + node.type === 'organizational-unit' + ? 'apartment' + : node.type === 'classroom' + ? 'school' + : node.type === 'client' + ? 'computer' + : 'group' + }} + + {{ node.name }} ({{ node.type }}) + + - IP: {{ node.ip }} + + + + + + + + computer + device_hub + {{ node.name }} ({{ node.type }}) + + - IP: {{ node.ip }} + + + + + + + + + + + + + +
+
+
+ + @@ -153,4 +210,4 @@ - + \ No newline at end of file diff --git a/ogWebconsole/src/app/components/groups/groups.component.ts b/ogWebconsole/src/app/components/groups/groups.component.ts index 3066539..ecd89a0 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.ts @@ -1,33 +1,38 @@ -import {Component, OnInit, ViewChild} from '@angular/core'; +import { Component, OnInit, ViewChild } from '@angular/core'; import { DataService } from './services/data.service'; -import { ClientCollection, UnidadOrganizativa } from './model/model'; +import { UnidadOrganizativa } from './model/model'; import { MatDialog } from '@angular/material/dialog'; +import { MatBottomSheet } from '@angular/material/bottom-sheet'; +import { MatTabChangeEvent } from '@angular/material/tabs'; +import { JoyrideService } from 'ngx-joyride'; +import { FlatTreeControl } from '@angular/cdk/tree'; +import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree'; + import { CreateOrganizationalUnitComponent } from './shared/organizational-units/create-organizational-unit/create-organizational-unit.component'; -import { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/delete-modal.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 {ToastrService} from "ngx-toastr"; -import {TreeViewComponent} from "./shared/tree-view/tree-view.component"; -import {MatBottomSheet} from "@angular/material/bottom-sheet"; -import {LegendComponent} from "./shared/legend/legend.component"; -import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom-view-modal'; -import {HttpClient} from "@angular/common/http"; -import {PageEvent} from "@angular/material/paginator"; -import { SaveFiltersDialogComponent } from './shared/save-filters-dialog/save-filters-dialog.component'; -import { AcctionsModalComponent } from './shared/acctions-modal/acctions-modal.component'; -import {MatTableDataSource} from "@angular/material/table"; -import {DatePipe} from "@angular/common"; -import {AdvancedSearchComponent} from "./components/advanced-search/advanced-search.component"; -import {MatTabChangeEvent} from "@angular/material/tabs"; -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 { ExecuteCommandComponent } from '../commands/main-commands/execute-command/execute-command.component'; -import { ExecuteCommandOuComponent } from './shared/execute-command-ou/execute-command-ou.component'; -import { JoyrideService } from 'ngx-joyride'; +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'; + +interface TreeNode { + name: string; + type: string; + children?: TreeNode[]; + ip?: string; + '@id'?: string; +} + +interface FlatNode { + name: string; + type: string; + level: number; + expandable: boolean; + ip?: string; +} @Component({ selector: 'app-groups', @@ -36,53 +41,60 @@ import { JoyrideService } from 'ngx-joyride'; }) export class GroupsComponent implements OnInit { baseUrl: string = import.meta.env.NG_APP_BASE_API_URL; - dataSource = new MatTableDataSource(); organizationalUnits: UnidadOrganizativa[] = []; selectedUnidad: UnidadOrganizativa | null = null; selectedDetail: any | null = null; - children: any[] = []; - breadcrumb: string[] = []; - clientsData: any[] = []; - breadcrumbData: any[] = []; - loading:boolean = false; - loadingChildren:boolean = false; + loading: boolean = false; + loadingChildren: boolean = false; searchTerm: string = ''; - selectedFilter1: string = 'none'; - selectedFilter2: string = 'none'; - selectedFilterOS: string[] = []; - selectedFilterStatus: string[] = []; - filterIP: string = ''; - filterMAC: string = ''; - filterName: string = ''; - filteredResults: any[] = []; - savedFilterNames: any[] = []; - length: number = 0; - itemsPerPage: number = 10; - page: number = 1; - pageSizeOptions: number[] = [5, 10, 25, 100]; - selectedElements: any[] = []; - isAllSelected: boolean = false; - filters: { [key: string]: string } = {}; - datePipe: DatePipe = new DatePipe('es-ES'); + + treeControl: FlatTreeControl; + treeFlattener: MatTreeFlattener; + treeDataSource: MatTreeFlatDataSource; + selectedNode: TreeNode | null = null; + + @ViewChild('clientTab') clientTabComponent!: ClientTabViewComponent; + @ViewChild('organizationalUnitTab') organizationalUnitTabComponent!: OrganizationalUnitTabViewComponent; constructor( - private dataService: DataService, - public dialog: MatDialog, - private toastService: ToastrService, - private _bottomSheet: MatBottomSheet, - private http: HttpClient, - private joyrideService: JoyrideService - ) {} + private dataService: DataService, + public dialog: MatDialog, + private _bottomSheet: MatBottomSheet, + private joyrideService: JoyrideService + ) { + this.treeFlattener = new MatTreeFlattener( + (node: TreeNode, level: number) => ({ + name: node.name, + type: node.type, + level, + expandable: !!node.children?.length, + 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(); } - @ViewChild('clientTab') clientTabComponent!: ClientTabViewComponent; - @ViewChild('organizationalUnitTab') organizationalUnitTabComponent!: OrganizationalUnitTabViewComponent; + clearSelection(): void { + this.selectedUnidad = null; + this.selectedDetail = null; + } - onTabChange(event: MatTabChangeEvent) { + onTabChange(event: MatTabChangeEvent): void { switch (event.index) { case 2: this.clientTabComponent.search(); @@ -97,12 +109,8 @@ export class GroupsComponent implements OnInit { getFilters(): void { this.dataService.getFilters().subscribe( - data => { - this.savedFilterNames = data.map((filter: any) => [filter.name, filter.uuid]); - }, - error => { - console.error('Error fetching filters:', error); - } + data => {}, + error => console.error('Error fetching filters:', error) ); } @@ -123,67 +131,78 @@ export class GroupsComponent implements OnInit { onSelectUnidad(unidad: UnidadOrganizativa): void { this.selectedUnidad = unidad; this.selectedDetail = unidad; - this.breadcrumb = [unidad.name]; - this.breadcrumbData = [unidad]; - this.loadChildrenAndClients(unidad.id); + + this.loadChildrenAndClients(unidad.id).then(fullData => { + const treeData = this.convertToTreeData(fullData); + this.treeDataSource.data = treeData[0]?.children || []; + }); } - onSelectChild(child: any): void { - this.selectedDetail = child; - if (child.type !== 'client' && child.uuid && child.id) { - this.breadcrumb.push(child.name || child.name); - this.breadcrumbData.push(child); - this.loadChildrenAndClients(child.id); + async loadChildrenAndClients(id: string): Promise { + try { + const childrenData = await this.dataService.getChildren(id).toPromise(); + const clientsData = await this.dataService.getClients(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) : []), + ...(node.clients + ? node.clients.map((client: any) => ({ + name: client.name, + type: 'client', + '@id': client['@id'], + ip: client.ip + })) + : []) + ], + ip: node.type === 'client' ? (node as any).ip : undefined + })); + }; + + return { + ...this.selectedUnidad, + children: childrenData ? processHierarchy(childrenData) : [], + clients: clientsData || [] + }; + } catch (error) { + console.error('Error loading children and clients:', error); + return this.selectedUnidad; } } - navigateToBreadcrumb(index: number): void { - this.breadcrumb = this.breadcrumb.slice(0, index + 1); - const target = this.breadcrumbData[index]; - this.breadcrumbData = this.breadcrumbData.slice(0, index + 1); - this.selectedDetail = target; - this.loadChildrenAndClients(target.id); + convertToTreeData(data: any): TreeNode[] { + const processNode = (node: UnidadOrganizativa): TreeNode => ({ + name: node.name, + type: node.type, + '@id': node['@id'], + children: [ + ...(node.children?.map(processNode) || []), + ...(node.clients + ? node.clients.map((client: any) => ({ + name: client.name, + type: 'client', + '@id': client['@id'], + ip: client.ip + })) + : []) + ], + ip: node.type === 'client' ? (node as any).ip : undefined + }); + + return [processNode(data)]; } - loadChildrenAndClients(id: string): void { - this.loadingChildren = true - this.dataService.getChildren(id).subscribe( - childrenData => { - this.dataService.getClients(id).subscribe( - clientsData => { - this.clientsData = clientsData; - const newChildren = [...childrenData, ...clientsData]; - - if (newChildren.length > 0) { - this.children = newChildren; - } else { - this.children = []; - } - this.loadingChildren = false - }, - error => { - console.error('Error fetching clients', error); - this.clientsData = []; - this.children = []; - this.loadingChildren = false - } - ); - }, - error => { - console.error('Error fetching children', error); - this.children = []; - this.loadingChildren = false - } - ); - } - - addOU(event: MouseEvent, parent:any = null): void { + addOU(event: MouseEvent, parent: any = null): void { event.stopPropagation(); - const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, { data: { parent }, width: '900px'}); + const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, { data: { parent }, width: '900px' }); dialogRef.afterClosed().subscribe(() => { this.dataService.getOrganizationalUnits().subscribe( data => { - this.organizationalUnits = data + this.organizationalUnits = data; }, error => console.error('Error fetching unidades organizativas', error) ); @@ -192,262 +211,76 @@ export class GroupsComponent implements OnInit { 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) - ); + this.dataService.getOrganizationalUnits().subscribe( + data => { + this.organizationalUnits = data; + if (organizationalUnit && organizationalUnit.id) { + this.loadChildrenAndClients(organizationalUnit.id); + } + }, + error => console.error('Error fetching unidades organizativas', error) + ); }); -} - onDeleteClick(event: MouseEvent, uuid: string, name: string, type: string): void { + } + + 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') { - const dialogRef = this.dialog.open(DeleteModalComponent, { - width: '400px', - data: { name } - }); - dialogRef.afterClosed().subscribe(result => { - if (result) { - this.dataService.deleteElement(uuid, type).subscribe( - () => { - this.loadChildrenAndClients(this.selectedUnidad?.id || ''); - this.dataService.getOrganizationalUnits().subscribe( - data => this.organizationalUnits = data, - error => console.error('Error fetching unidades organizativas', error) - ); - this.openSnackBar(false, 'Entidad eliminada exitosamente') - }, - error => { - console.error('Error deleting element', error) - this.openSnackBar(true, error.error['hydra:description']) - } - ); - } - }); + if (type !== 'client') { + this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' }); } else { - const dialogDeleteGroupRef = this.dialog.open(DeleteModalComponent, { - width: '400px', - data: { name } - }); - - dialogDeleteGroupRef.afterClosed().subscribe(result => { - if (result && result === 'delete') { - this.dataService.deleteElement(uuid, type).subscribe( - () => { - this.loadChildrenAndClients(this.selectedUnidad?.id || ''); - this.dataService.getOrganizationalUnits().subscribe( - data => this.organizationalUnits = data, - error => console.error('Error fetching unidades organizativas', error) - ); - this.openSnackBar(false, 'Entidad eliminada exitosamente') - }, - error => { - console.error('Error deleting element', error) - this.openSnackBar(true, error.error['hydra:description']) - } - ); - } else if (result && result === 'change') { - this.dataService.changeParent(uuid).subscribe( - () => { - this.loadChildrenAndClients(this.selectedUnidad?.id || ''); - this.dataService.getOrganizationalUnits().subscribe( - data => this.organizationalUnits = data, - error => console.error('Error fetching unidades organizativas', error) - ); - this.openSnackBar(false, 'Entidad eliminada exitosamente') - }, - error => { - console.error('Error deleting element', error) - this.openSnackBar(true, error.error['hydra:description']) - } - ); - } - }); + this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' }); } } + onDelete(node: TreeNode | null): void { + if (!node) return; + // Additional logic for deleting + } + + onCustomAction(node: TreeNode | null): void { + if (!node) return; + // Logic for custom actions + } + onEditClick(event: MouseEvent, type: any, uuid: string): void { event.stopPropagation(); - if (type != "client") { - const dialogRef = this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px'}); + if (type != 'client') { + this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' }); } else { - const dialogRef = this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' } ); + this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' }); } } onShowClick(event: MouseEvent, data: any): void { event.stopPropagation(); - if (data.type != "client") { - const dialogRef = this.dialog.open(ShowOrganizationalUnitComponent, { data: { data }, width: '700px'}); + if (data.type != 'client') { + this.dialog.open(ShowOrganizationalUnitComponent, { data: { data }, width: '700px' }); } } onTreeClick(event: MouseEvent, data: any): void { event.stopPropagation(); - if (data.type != "client") { - const dialogRef = this.dialog.open(TreeViewComponent, { data: { data }, width: '800px'}); + if (data.type != 'client') { + this.dialog.open(TreeViewComponent, { data: { data }, width: '800px' }); } } - onExecuteCommand(event: MouseEvent, child: any, name: string, type:string): void { - console.log('Executing command on:', child); - - this.dialog.open(ExecuteCommandOuComponent, { - width: '50%', - data: { childUnitUuid: child } - }).afterClosed().subscribe((result) => { - if (result) { - console.log('Comando ejecutado con éxito'); - } else { - console.log('Ejecución de comando cancelada'); - } - }); - } - - openSnackBar(isError: boolean, message: string) { - if (isError) { - this.toastService.error(' Error al eliminar la entidad: ' + message, 'Error'); - } else - this.toastService.success(message, 'Éxito'); - } - openBottomSheet(): void { this._bottomSheet.open(LegendComponent); } - roomMap(): void { - if (this.selectedDetail && this.selectedDetail.type === 'classroom') { - const dialogRef = this.dialog.open(ClassroomViewDialogComponent, { - width: '90vw', - data: { clients: this.clientsData } - }); - - dialogRef.afterClosed().subscribe(result => { - console.log('The dialog was closed'); - }); - } - } - - applyFilter() { - this.dataService.getFilteredResults(this.selectedFilter1, this.selectedFilter2, this.filterName, this.filterIP, this.filterMAC, this.page, this.itemsPerPage) - .subscribe( - response => { - this.filteredResults = response.results; - this.length = response.total; - }, - error => { - console.error('Error al obtener los resultados filtrados', error); - this.filteredResults = []; - } - ); - } - - onPageChange(event: PageEvent) { - this.page = event.pageIndex; - this.itemsPerPage = event.pageSize; - this.applyFilter(); - } - - saveFilters() { - const dialogRef = this.dialog.open(SaveFiltersDialogComponent); - - dialogRef.afterClosed().subscribe(result => { - if (result) { - const filters = { - name: result, - favourite: true, - filters: { - filter0: this.filterName, - filter1: this.selectedFilter1, - filter2: this.selectedFilter2, - filter3: this.selectedFilterOS, - filter4: this.selectedFilterStatus, - filter5: this.filterIP, - filter6: this.filterMAC, - - } - }; - - this.http.post(`${this.baseUrl}/views`, filters).subscribe(response => { - console.log('Response from server:', response); - this.toastService.success('Se ha guardado el filtro correctamente'); - }, error => { - console.error('Error:', error); - this.toastService.error(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); - this.filterName = response.filters.filter0 || ''; - this.selectedFilter1 = response.filters.filter1 || null; - this.selectedFilter2 = response.filters.filter2 || ''; - - this.selectedFilterOS = response.filters.filter3 || []; - this.selectedFilterStatus = response.filters.filter4 || []; - this.filterIP = response.filters.filter5 || ''; - this.filterMAC = response.filters.filter6 || ''; - - this.applyFilter(); - } - }, error => { - console.error('Error:', error); - }); - - } - - - onCheckboxChange(event: any, name: string, uuid: string) { - if (event.checked) { - this.selectedElements.push(uuid); - } else { - const index = this.selectedElements.indexOf(name); - if (index > -1) { - this.selectedElements.splice(index, 1); - } - } - - this.isAllSelected = this.selectedElements.length === this.filteredResults.length; - } - - toggleSelectAll() { - this.isAllSelected = !this.isAllSelected; - - if (this.isAllSelected) { - this.selectedElements = this.filteredResults.map(result => result.uuid); - } else { - this.selectedElements = []; - } - } - - isSelected(name: string): boolean { - return this.selectedElements.includes(name); - - } - - - sendActions() { - const dialogRef = this.dialog.open(AcctionsModalComponent, { data: { selectedElements: this.selectedElements }, width: '700px'}); - } - iniciarTour(): void { this.joyrideService.startTour({ steps: ['groupsTitleStepText', 'addStep', 'keyStep', 'unitStep', 'elementsStep', 'tabsStep'], @@ -455,4 +288,8 @@ export class GroupsComponent implements OnInit { themeColor: '#3f51b5' }); } + + hasChild = (_: number, node: FlatNode): boolean => node.expandable; + isLeafNode = (_: number, node: FlatNode): boolean => !node.expandable; + } diff --git a/ogWebconsole/src/app/components/groups/model/model.ts b/ogWebconsole/src/app/components/groups/model/model.ts index b37b389..289a796 100644 --- a/ogWebconsole/src/app/components/groups/model/model.ts +++ b/ogWebconsole/src/app/components/groups/model/model.ts @@ -9,6 +9,9 @@ export interface Aula { } export interface UnidadOrganizativa { + clients: any[]; + children: UnidadOrganizativa[]; + '@id'?: string; id: string; name: string; uuid: string; diff --git a/ogWebconsole/src/app/components/groups/services/data.service.ts b/ogWebconsole/src/app/components/groups/services/data.service.ts index ffc42f2..1946670 100644 --- a/ogWebconsole/src/app/components/groups/services/data.service.ts +++ b/ogWebconsole/src/app/components/groups/services/data.service.ts @@ -183,6 +183,16 @@ export class DataService { }) ); } - + + getOrganizationalUnitById(id: string): Observable { + const url = `${this.baseUrl}/organizational-units/${id}`; + return this.http.get(url).pipe( + catchError(error => { + console.error('Error fetching organizational unit', error); + return throwError(error); + }) + ); + } + } diff --git a/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.css b/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.css index fcf40bd..d32557e 100644 --- a/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.css +++ b/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.css @@ -18,7 +18,8 @@ h1 { } .mat-dialog-content { - padding: 50px; + padding-left: 50px; + padding-right: 20px; } button { @@ -61,3 +62,60 @@ mat-option .unit-path { .form-field { width: 100%; } + +.inputs-container { + display: inline-flex; + gap: 20px; + padding-left: 50px; + padding-right: 20px; +} + +.upload-container { + display: flex; + justify-content: center; + align-items: center; + margin-top: 10px; +} + +button[mat-raised-button] { + font-size: 14px; + text-transform: none; + padding: 10px 20px; + border-radius: 4px; + box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2); + cursor: pointer; + transition: background-color 0.3s ease, box-shadow 0.3s ease; +} + +button[mat-raised-button]:hover { + background-color: #303f9f; + box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.3); +} + +/* Contenedor para la tabla con scroll */ +.scrollable-table { + max-height: 300px; /* Altura máxima de la tabla */ + overflow-y: auto; /* Habilitar scroll vertical */ + overflow-x: hidden; /* Ocultar scroll horizontal si no es necesario */ + border: 1px solid #ddd; + border-radius: 4px; + margin-top: 10px; +} + +/* Tabla */ +.mat-elevation-z8 { + width: 100%; +} + +.client-table th, .client-table td { + text-align: left; + padding: 10px; +} + +.client-table th { + position: sticky; + top: 0; + background-color: #3f51b5; + color: white; + z-index: 2; +} diff --git a/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.html b/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.html index 130eaaa..e44a9d7 100644 --- a/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.html +++ b/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.html @@ -1,92 +1,131 @@

{{ 'addClientDialogTitle' | translate }}

-
- -
- - {{ 'organizationalUnitLabel' | translate }} - - -
{{ unit.name }}
-
{{ unit.path }}
-
-
-
- - {{ 'nameLabel' | translate }} - - +
+
+

Añadir un cliente

+ + + + {{ 'organizationalUnitLabel' | translate }} + + +
{{ unit.name }}
+
{{ unit.path }}
+
+
+
- - {{ 'ogLiveLabel' | translate }} - - - {{ oglive.name }} - - - + + {{ 'nameLabel' | translate }} + + - - {{ 'serialNumberLabel' | translate }} - - + + {{ 'ogLiveLabel' | translate }} + + + {{ oglive.name }} + + + - - {{ 'netifaceLabel' | translate }} - - - {{ type.name }} - - - + + {{ 'serialNumberLabel' | translate }} + + - - {{ 'netDriverLabel' | translate }} - - - {{ type.name }} - - - + + {{ 'netifaceLabel' | translate }} + + + {{ type.name }} + + + - - {{ 'macLabel' | translate }} - {{ 'macHint' | translate }} - - {{ 'macError' | translate }} - + + {{ 'netDriverLabel' | translate }} + + + {{ type.name }} + + + - - {{ 'ipLabel' | translate }} - {{ 'ipHint' | translate }} - - {{ 'ipError' | translate }} - + + {{ 'macLabel' | translate }} + {{ 'macHint' | translate }} + + {{ 'macError' | translate }} + - - {{ 'templateLabel' | translate }} - - - {{ template.name }} - - - + + {{ 'ipLabel' | translate }} + {{ 'ipHint' | translate }} + + {{ 'ipError' | translate }} + - - {{ 'hardwareProfileLabel' | translate }} - - - {{ unit.description }} - - - {{ 'hardwareProfileError' | translate }} - - + + {{ 'templateLabel' | translate }} + + + {{ template.name }} + + + + + + {{ 'hardwareProfileLabel' | translate }} + + + {{ unit.description }} + + + {{ 'hardwareProfileError' | translate }} + + +
+ + + +
+

Añadir multiples clientes

+
+ + +
+ + +

Clientes importados:

+
+ + + + + + + + + + + + + + + + + + +
Nombre {{ client.name }} IP {{ client.ip }}
+
+
+
- +
diff --git a/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.ts b/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.ts index 5cffebd..671a45e 100644 --- a/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.ts +++ b/ogWebconsole/src/app/components/groups/shared/clients/create-client/create-client.component.ts @@ -5,6 +5,7 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatSnackBar } from '@angular/material/snack-bar'; import { ToastrService } from 'ngx-toastr'; import { DataService } from '../../../services/data.service'; +import * as Papa from 'papaparse'; @Component({ selector: 'app-create-client', @@ -18,16 +19,17 @@ export class CreateClientComponent implements OnInit { hardwareProfiles: any[] = []; ogLives: any[] = []; templates: any[] = []; - private errorForm: boolean = false; + uploadedClients: any[] = []; + loading: boolean = false; + displayedColumns: string[] = ['name', 'ip']; protected netifaceTypes = [ - { "name": 'Eth0', "value": "eth0" }, - { "name": 'Eth1', "value": "eth1" }, - { "name": 'Eth2', "value": "eth2" }, + { name: 'Eth0', value: 'eth0' }, + { name: 'Eth1', value: 'eth1' }, + { name: 'Eth2', value: 'eth2' } ]; protected netDriverTypes = [ - { "name": 'Generic', "value": "generic" }, + { name: 'Generic', value: 'generic' } ]; - loading: boolean = false; constructor( private fb: FormBuilder, @@ -37,16 +39,22 @@ export class CreateClientComponent implements OnInit { private toastService: ToastrService, private dataService: DataService, @Inject(MAT_DIALOG_DATA) public data: any - ) { } + ) {} ngOnInit(): void { - console.log(this.data); + this.initForm(); this.loadParentUnits(); this.loadHardwareProfiles(); this.loadOgLives(); - this.loadPxeTemplates() + this.loadPxeTemplates(); + } + + initForm(): void { this.clientForm = this.fb.group({ - organizationalUnit: [this.data.organizationalUnit ? this.data.organizationalUnit['@id'] : null, Validators.required], + organizationalUnit: [ + this.data.organizationalUnit ? this.data.organizationalUnit['@id'] : null, + Validators.required + ], name: ['', Validators.required], serialNumber: [''], netiface: null, @@ -54,25 +62,14 @@ export class CreateClientComponent implements OnInit { mac: ['', Validators.required], ip: ['', Validators.required], template: [null], - hardwareProfile: [this.data.organizationalUnit && this.data.organizationalUnit.networkSettings && this.data.organizationalUnit.networkSettings.hardwareProfile ? this.data.organizationalUnit.networkSettings.hardwareProfile['@id'] : null], + hardwareProfile: [ + this.data.organizationalUnit?.networkSettings?.hardwareProfile?.['@id'] || null + ], ogLive: [null] }); } - loadHardwareProfiles(): void { - this.dataService.getHardwareProfiles().subscribe( - (data: any[]) => { - this.hardwareProfiles = data; - this.loading = false; - }, - (error: any) => { - console.error('Error fetching hardware profiles', error); - this.loading = false; - } - ); - } - - loadParentUnits() { + loadParentUnits(): void { this.loading = true; const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=10000`; @@ -88,9 +85,22 @@ export class CreateClientComponent implements OnInit { ); } - loadOgLives() { + loadHardwareProfiles(): void { + this.dataService.getHardwareProfiles().subscribe( + (data: any[]) => { + this.hardwareProfiles = data; + this.loading = false; + }, + error => { + console.error('Error fetching hardware profiles:', error); + this.loading = false; + } + ); + } + loadOgLives(): void { const url = `${this.baseUrl}/og-lives?page=1&itemsPerPage=30`; + this.http.get(url).subscribe( response => { this.ogLives = response['hydra:member']; @@ -109,25 +119,50 @@ export class CreateClientComponent implements OnInit { this.templates = response['hydra:member']; }, error => { - console.error('Error fetching ogLives:', error); + console.error('Error fetching PXE templates:', error); } ); } - onSubmit() { + onFileUpload(event: any): void { + const file = event.target.files[0]; + if (file) { + const reader = new FileReader(); + + // Leer archivo como texto + reader.onload = (e: any) => { + const csvData = e.target.result; + + // Usar PapaParse para convertir CSV a JSON + Papa.parse(csvData, { + header: true, // Utilizar la primera fila como encabezados + skipEmptyLines: true, // Ignorar líneas vacías + complete: (result) => { + this.uploadedClients = result.data; + this.toastService.success('Archivo CSV cargado correctamente', 'Éxito'); + }, + error: (error) => { + console.error('Error al procesar el archivo CSV:', error); + this.toastService.error('Error al procesar el archivo CSV', 'Error'); + } + }); + }; + + reader.readAsText(file); + } + } + + onSubmit(): void { if (this.clientForm.valid) { - this.errorForm = false; const formData = this.clientForm.value; - formData.ogLive = formData.ogLive; this.http.post(`${this.baseUrl}/clients`, formData).subscribe( response => { + this.toastService.success('Cliente creado exitosamente', 'Éxito'); this.dialogRef.close(response); - this.openSnackBar(false, 'Cliente creado exitosamente'); }, error => { console.error('Error during POST:', error); - this.errorForm = true; - this.openSnackBar(true, 'Error al crear el cliente: ' + error.error['hydra:description']); + this.toastService.error('Error al crear el cliente', 'Error'); } ); } @@ -136,12 +171,4 @@ export class CreateClientComponent implements OnInit { onNoClick(): void { this.dialogRef.close(); } - - openSnackBar(isError: boolean, message: string) { - if (isError) { - this.toastService.error(' Error al crear el cliente: ' + message, 'Error'); - } else { - this.toastService.success(message, 'Éxito'); - } - } }