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..01d8902 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.css +++ b/ogWebconsole/src/app/components/groups/groups.component.css @@ -1,321 +1,559 @@ -.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; - display: flex; - flex-direction: column; - width: 300px; + background-color: #f9f9f9; + border: 1px solid #ddd; + border-radius: 8px; } -.saved-filter { +.details-container { display: flex; flex-direction: column; - width: 300px; - margin-bottom: 10px; - padding: 10px; + gap: 20px; + padding: 20px; + align-items: center; + text-align: center; } -.results { +.details-wrapper { + width: 95%; + padding: 20px; + display: block; +} + +.details-placeholder { width: 100%; } -.results-container { - display: grid; - grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); +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; + 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; +} + +mat-tree { + background-color: #f9f9f9; + border-right: 1px solid #ddd; + padding: 10px; +} + +button { + margin: 5px; +} + +mat-tree mat-tree-node { + display: flex; + align-items: center; + padding: 10px; + border-radius: 6px; + transition: background-color 0.2s, color 0.2s; + cursor: pointer; +} + +mat-tree mat-tree-node:hover { + background-color: #e3f2fd; +} + +mat-tree mat-tree-node button.mat-icon-button { + margin-left: auto; + color: #757575; +} + +mat-tree mat-tree-node button.mat-icon-button:hover { + color: #1976d2; +} + +mat-tree mat-tree-node span { + font-size: 16px; + font-weight: 500; + color: #555; +} + +mat-tree mat-tree-node mat-icon { + margin-right: 10px; + color: #757575; + transition: color 0.2s; +} + +mat-tree mat-tree-node.expandable mat-icon { + color: black; + cursor: pointer; +} + +mat-tree mat-tree-node.expandable.disabled mat-icon { + color: grey; + opacity: 0.5; + cursor: not-allowed; +} + +mat-tree mat-tree-node:hover mat-icon { + color: black; +} + +mat-tree mat-tree-node mat-icon.node-icon { + color: #757575; + margin-right: 10px; +} + +mat-tree mat-tree-node mat-icon.node-icon.organizational-unit { + color: #1976d2; +} + +mat-tree mat-tree-node mat-icon.node-icon.classroom { + color: #757575; +} + +mat-tree mat-tree-node mat-icon.node-icon.client { + color: #757575; +} + +mat-tree mat-tree-node mat-icon.node-icon.group { + color: #757575; +} + +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; +} + +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; +} + +.selected-node { + background-color: #e0f7fa; + border-left: 4px solid #3F51B5; + padding-left: calc(16px - 4px); +} + +.mat-menu-item .mat-menu-item-submenu-icon { + display: none; +} + +.filters-container { + display: flex; + flex-wrap: wrap; gap: 16px; margin-bottom: 16px; } -.result-card { - width: 100%; - max-width: 250px; - height: 250px; -} -.paginator-container { - display: flex; - justify-content: center; - margin-bottom: 30px; - +.filters-container mat-form-field { + flex: 1 1 100%; + max-width: 300px; } -.divider { - margin: 20px 0; +.filter-container { + margin-bottom: 16px; } -mat-card { - margin-bottom: 20px; +.pc-og-live { + color: #4caf50; } -.mat-tooltip { - white-space: pre-line; +.pc-busy { + color: #ff9800; } -.classroom-grid { +.pc-windows { + color: #0078d7; +} + +.pc-linux { + color: #f0ad4e; +} + +.pc-macos { + color: #999999; +} + +.pc-off { + color: #f44336; +} + +.clients-card-container { display: flex; flex-wrap: wrap; gap: 16px; - justify-content: flex-start; /* Opcional: para alinear a la izquierda */ + padding: 16px; + justify-content: flex-start; } .classroom-item { - flex: 0 1 calc(16.66% - 16px); /* 6 columnas */ - max-width: calc(16.66% - 16px); - text-align: center; + flex: 1 1 calc(25% - 16px); + max-width: calc(25% - 16px); box-sizing: border-box; } .classroom-pc { - position: relative; + border: 1px solid #ccc; + border-radius: 8px; + padding: 16px; display: flex; flex-direction: column; align-items: center; - padding: 8px; - background-color: #f4f4f4; - border-radius: 8px; - box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + text-align: center; } -.pc-image { - width: 80px; - height: 80px; +.classroom-pc .pc-image { + width: 64px; + height: 64px; + margin-bottom: 8px; } .pc-details { - margin-top: 8px; - font-size: 12px; + margin-bottom: 8px; } -.client-name { - font-weight: bold; +.pc-details .client-name, +.pc-details .client-ip, +.pc-details .client-mac { display: block; -} - -.client-ip, -.client-mac { + font-size: 14px; color: #666; - font-size: 10px; - display: block; } -.pc-actions { - margin-top: 8px; +.pc-actions button { + margin: 0 4px; +} + +.main-container { + display: flex; + flex-direction: row; +} + +.tree-container { + width: 25%; + padding: 16px; + overflow-x: hidden; + overflow-y: auto; +} + +.clients-container { + width: 75%; + padding: 16px; + box-sizing: border-box; + overflow-y: auto; +} + +.clients-container h3 { + margin-bottom: 15px; + font-size: 1.5em; + color: #333; +} + +.client-item { display: flex; justify-content: center; - gap: 8px; } -.pc-og-live { - border: 2px solid #4caf50; +.client-card { + background-color: #fff; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + width: 100%; + max-width: 300px; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + padding: 15px; + transition: transform 0.3s ease, box-shadow 0.3s ease; } -.pc-busy { - border: 2px solid #ff9800; +.client-card:hover { + transform: translateY(-5px); + box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15); } -.pc-off { - border: 2px solid #f44336; +.client-image { + width: 60px; + height: 60px; + margin-bottom: 15px; } -.pc-linux { - border: 2px solid #9c27b0; -} - -.pc-windows { - border: 2px solid #2196f3; -} - -/* Pantallas medianas: 4 columnas */ -@media (max-width: 1024px) { - .classroom-item { - flex: 0 1 calc(25% - 16px); /* 4 columnas */ - } -} - -/* Pantallas pequeñas: 2 columnas */ -@media (max-width: 768px) { - .classroom-item { - flex: 0 1 calc(50% - 16px); /* 2 columnas */ - } -} - -/* Pantallas muy pequeñas: 1 columna */ -@media (max-width: 480px) { - .classroom-item { - flex: 0 1 100%; /* 1 columna */ - } -} - -.client-text { - font-size: 0.8rem; - color: rgba(0, 0, 0, 0.54); +.client-details { + margin-top: 10px; } .client-name { - font-size: 0.9rem; - text-align: center; + display: block; + font-size: 1.2em; + font-weight: 600; + color: #333; + margin-bottom: 5px; +} + +.client-ip { + display: block; + font-size: 0.9em; + color: #666; +} + +button[mat-raised-button] { + margin-top: 15px; +} + +.clients-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 16px; +} + +.clients-list { + display: flex; + flex-direction: column; + gap: 16px; +} + +.client-item-list { + display: flex; + justify-content: space-between; + align-items: center; + padding: 8px; + border: 1px solid #ccc; + border-radius: 4px; +} + +.client-details-list { + display: flex; + flex-direction: column; +} + +.view-toggle-container { + display: flex; + gap: 1rem; + margin-bottom: 1rem; +} + +.clients-grid { + display: flex; + flex-wrap: wrap; + gap: 1rem; +} + +.clients-list .list-item-content { + display: flex; + justify-content: space-between; + width: 100%; + align-items: center; +} + +.client-card, .list-item-content { + border: 1px solid #ccc; + padding: 1rem; + border-radius: 5px; +} + +.client-card { + display: flex; + flex-direction: column; + align-items: center; +} + +.client-image { + width: 50px; + height: 50px; + margin-bottom: 0.5rem; +} + +.header-actions-container { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 1rem; +} + +.back-button { + flex-shrink: 0; +} + +.view-toggle-container { + display: flex; + gap: 1rem; + align-items: center; +} + +.filters-container { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin: 16px 0; + padding: 0 16px; +} + +.filters-container mat-form-field { + flex: 1 1 300px; + max-width: 300px; } diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index 2de059a..e25deee 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -1,156 +1,309 @@ - - -
- +

+ {{ 'adminGroupsTitle' | translate }} +

+
+ + + +
+
+ + + + Filtros + +
+ + + + {{ savedFilter[0] }} + + + + + Buscar en el árbol + + + + Filtrar por tipo + + Todos + Grupos de aulas + Aulas + Grupos de ordenadores + + + + Buscar cliente + + +
+
+ +
+ + + + apartment {{ unidad.name }} + + + +
+ +
+ + + + + + + + +
+
+
+ + +
+ +
+ -

{{ 'adminGroupsTitle' | translate }}

-
- - - + +
+
+
+
+

{{ selectedUnidad?.name }}

+ + + + + {{ + node.type === 'organizational-unit' ? 'apartment' + : node.type === 'classrooms-group' ? 'meeting_room' + : node.type === 'classroom' ? 'school' + : node.type === 'clients-group' ? 'lan' + : node.type === 'client' ? 'computer' + : 'group' + }} + + {{ node.name }} + + + + + + {{ + node.type === 'organizational-unit' ? 'apartment' + : node.type === 'classrooms-group' ? 'meeting_room' + : node.type === 'classroom' ? 'school' + : node.type === 'clients-group' ? 'lan' + : node.type === 'client' ? 'computer' + : 'group' + }} + + {{ node.name }} + + - IP: {{ node.ip }} + + + + +
+ + + + + + + + + + + + +
+

Clientes {{ selectedNode?.name ? 'del ' + selectedNode?.name : '' }}

+
+
+
+ Client Icon +
+ {{ client.name }} + {{ client.ip }} + + {{ client.status || 'off' }} + + + + + + + +
+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Nombre {{ client.name }} IP {{ client.ip }} MAC {{ client.mac }} OG Live {{ client.oglive }} Estado + + {{ client.status || 'off' }} + + Mantenimiento {{ client.mantenimiento }} Subred {{ client.subnet }} Plantilla PXE {{ client.pxeTemplate }} Acciones + + + + + + +
+
- -
- - {{ 'organizationalUnitTitle' | translate }} - - - - -
- apartment - {{ unidad.name }} - - more_vert - - - - - - - - - - - - -
-
-
-
-
- - - -
- {{ 'internalElementsTitle' | translate }} - - - {{ crumb }} - > - - -
-
- - - - - -
- info - {{ 'noInternalElementsMessage' | translate }} -
- -
- - apartment - meeting_room - school - computer - lan - help_outline - - {{ child.name }} -
- more_vert - - - - -
-
-
-
- - -
-
-
- PC Icon -
- {{ pc.name }} - {{ pc.ip }} - {{ pc.mac }} -
-
- - -
-
-
-
- -
-
-
- - - - - - - - - - - - - - +
+ diff --git a/ogWebconsole/src/app/components/groups/groups.component.spec.ts b/ogWebconsole/src/app/components/groups/groups.component.spec.ts index 5fb654f..b5646bf 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.spec.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.spec.ts @@ -113,131 +113,4 @@ describe('GroupsComponent', () => { expect(component.onSelectUnidad).toHaveBeenCalledWith(unidad); }); - it('should call onSelectChild method', () => { - spyOn(component, 'onSelectChild'); - const child = { id: '1', name: 'Test', type: 'unit' } as any; - component.onSelectChild(child); - expect(component.onSelectChild).toHaveBeenCalledWith(child); - }); - - it('should call navigateToBreadcrumb method', () => { - spyOn(component, 'navigateToBreadcrumb'); - component.navigateToBreadcrumb(1); - expect(component.navigateToBreadcrumb).toHaveBeenCalledWith(1); - }); - - it('should call loadChildrenAndClients method', () => { - spyOn(component, 'loadChildrenAndClients'); - component.loadChildrenAndClients('1'); - expect(component.loadChildrenAndClients).toHaveBeenCalledWith('1'); - }); - - it('should call onDeleteClick method', () => { - spyOn(component, 'onDeleteClick'); - const event = new MouseEvent('click'); - component.onDeleteClick(event, 'uuid', 'name', 'client'); - expect(component.onDeleteClick).toHaveBeenCalledWith(event, 'uuid', 'name', 'client'); - }); - - it('should call onEditClick method', () => { - spyOn(component, 'onEditClick'); - const event = new MouseEvent('click'); - component.onEditClick(event, 'client', 'uuid'); - expect(component.onEditClick).toHaveBeenCalledWith(event, 'client', 'uuid'); - }); - - it('should call onShowClick method', () => { - spyOn(component, 'onShowClick'); - const event = new MouseEvent('click'); - component.onShowClick(event, { type: 'unit' }); - expect(component.onShowClick).toHaveBeenCalledWith(event, { type: 'unit' }); - }); - - it('should call onTreeClick method', () => { - spyOn(component, 'onTreeClick'); - const event = new MouseEvent('click'); - component.onTreeClick(event, { type: 'unit' }); - expect(component.onTreeClick).toHaveBeenCalledWith(event, { type: 'unit' }); - }); - - it('should call onExecuteCommand method', () => { - spyOn(component, 'onExecuteCommand'); - const event = new MouseEvent('click'); - component.onExecuteCommand(event, 'child', 'name', 'type'); - expect(component.onExecuteCommand).toHaveBeenCalledWith(event, 'child', 'name', 'type'); - }); - - it('should call openSnackBar method', () => { - spyOn(component, 'openSnackBar'); - component.openSnackBar(true, 'message'); - expect(component.openSnackBar).toHaveBeenCalledWith(true, 'message'); - }); - - it('should call openBottomSheet method', () => { - spyOn(component, 'openBottomSheet'); - component.openBottomSheet(); - expect(component.openBottomSheet).toHaveBeenCalled(); - }); - - it('should call roomMap method', () => { - spyOn(component, 'roomMap'); - component.roomMap(); - expect(component.roomMap).toHaveBeenCalled(); - }); - - it('should call applyFilter method', () => { - spyOn(component, 'applyFilter'); - component.applyFilter(); - expect(component.applyFilter).toHaveBeenCalled(); - }); - - it('should call onPageChange method', () => { - spyOn(component, 'onPageChange'); - const event = { pageIndex: 1, pageSize: 10 } as any; - component.onPageChange(event); - expect(component.onPageChange).toHaveBeenCalledWith(event); - }); - - it('should call saveFilters method', () => { - spyOn(component, 'saveFilters'); - component.saveFilters(); - expect(component.saveFilters).toHaveBeenCalled(); - }); - - it('should call loadSelectedFilter method', () => { - spyOn(component, 'loadSelectedFilter'); - component.loadSelectedFilter(['name', 'uuid']); - expect(component.loadSelectedFilter).toHaveBeenCalledWith(['name', 'uuid']); - }); - - it('should call onCheckboxChange method', () => { - spyOn(component, 'onCheckboxChange'); - const event = { checked: true } as any; - component.onCheckboxChange(event, 'name', 'uuid'); - expect(component.onCheckboxChange).toHaveBeenCalledWith(event, 'name', 'uuid'); - }); - - it('should call toggleSelectAll method', () => { - spyOn(component, 'toggleSelectAll'); - component.toggleSelectAll(); - expect(component.toggleSelectAll).toHaveBeenCalled(); - }); - - it('should call isSelected method', () => { - spyOn(component, 'isSelected'); - component.isSelected('name'); - expect(component.isSelected).toHaveBeenCalledWith('name'); - }); - - it('should call sendActions method', () => { - spyOn(component, 'sendActions'); - component.sendActions(); - expect(component.sendActions).toHaveBeenCalled(); - }); - - it('should call iniciarTour method', () => { - spyOn(component, 'iniciarTour'); - component.iniciarTour(); - expect(component.iniciarTour).toHaveBeenCalled(); - }); }); diff --git a/ogWebconsole/src/app/components/groups/groups.component.ts b/ogWebconsole/src/app/components/groups/groups.component.ts index 3066539..972f0c8 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.ts @@ -1,33 +1,46 @@ -import {Component, OnInit, ViewChild} from '@angular/core'; -import { DataService } from './services/data.service'; -import { ClientCollection, UnidadOrganizativa } from './model/model'; +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 { 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 { 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 { 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 {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'; + +interface TreeNode { + clients?: any[]; + 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', @@ -36,62 +49,87 @@ 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[] = []; + 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[] = []; - 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'); + selectedTreeFilter: string = ''; + @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 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; } - @ViewChild('clientTab') clientTabComponent!: ClientTabViewComponent; - @ViewChild('organizationalUnitTab') organizationalUnitTabComponent!: OrganizationalUnitTabViewComponent; + updateGridCols(): void { + const width = window.innerWidth; + this.cols = width <= 600 ? 1 : width <= 960 ? 2 : width <= 1280 ? 3 : 4; + } - onTabChange(event: MatTabChangeEvent) { - switch (event.index) { - case 2: - this.clientTabComponent.search(); - break; - case 3: - this.organizationalUnitTabComponent.search(); - break; - default: - break; + 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(); } } @@ -106,6 +144,21 @@ export class GroupsComponent implements OnInit { ); } + 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( @@ -120,70 +173,112 @@ export class GroupsComponent implements OnInit { ); } - onSelectUnidad(unidad: UnidadOrganizativa): void { + onSelectUnidad(unidad: any): void { this.selectedUnidad = unidad; this.selectedDetail = unidad; - this.breadcrumb = [unidad.name]; - this.breadcrumbData = [unidad]; - this.loadChildrenAndClients(unidad.id); + + this.selectedClients = this.collectAllClients(unidad); + this.selectedClientsOriginal = [...this.selectedClients]; + + this.loadChildrenAndClients(unidad.id).then(fullData => { + const treeData = this.convertToTreeData(fullData); + this.treeDataSource.data = treeData[0]?.children || []; + }); + + this.isTreeViewActive = true; } + - 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); + private collectAllClients(node: any): any[] { + let clients = node.clients || []; + if (node.children && node.children.length > 0) { + node.children.forEach((child: any) => { + clients = clients.concat(this.collectAllClients(child)); + }); + } + return clients; + } + + + 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; } } - 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) || [], + hasClients: node.clients && node.clients.length > 0, + }); + + 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 - } - ); + onNodeClick(node: TreeNode): void { + this.selectedNode = node; + this.selectedClients = node.clients || []; + this.selectedClientsOriginal = [...this.selectedClients]; + 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 = []; + } } - addOU(event: MouseEvent, parent:any = null): void { + 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'}); + const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, { data: { parent }, width: '900px' }); dialogRef.afterClosed().subscribe(() => { this.dataService.getOrganizationalUnits().subscribe( data => { - this.organizationalUnits = data + this.organizationalUnits = data; + this.loadChildrenAndClients(this.selectedUnidad?.id || '').then(updatedData => { + const treeData = this.convertToTreeData(updatedData); + this.treeDataSource.data = treeData[0]?.children || []; + }); }, error => console.error('Error fetching unidades organizativas', error) ); @@ -192,262 +287,198 @@ 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; + this.loadChildrenAndClients(this.selectedUnidad?.id || '').then(updatedData => { + const treeData = this.convertToTreeData(updatedData); + this.treeDataSource.data = treeData[0]?.children || []; + }); + if (organizationalUnit && organizationalUnit.id) { + this.loadChildrenAndClients(organizationalUnit.id); + this.refreshClients(organizationalUnit); + } + }, + 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 { + 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") { - 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 { + onRoomMap(room: any): void { + this.http.get(`${this.baseUrl}`+ 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(`${this.baseUrl}`+'/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(); - if (data.type != "client") { - const dialogRef = this.dialog.open(ShowOrganizationalUnitComponent, { data: { data }, width: '700px'}); + 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") { - 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 +486,63 @@ export class GroupsComponent implements OnInit { 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: any[]): any[] => { + return nodes + .map(node => { + 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) { + return { ...node, children: filteredChildren }; + } + return null; + }) + .filter(node => node !== null); + }; + + const filteredData = filterNodes(this.treeDataSource.data); + + 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 { + if (!searchTerm) { + 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; + }); + } + + + } 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..e27c39f 100644 --- a/ogWebconsole/src/app/components/groups/services/data.service.ts +++ b/ogWebconsole/src/app/components/groups/services/data.service.ts @@ -90,6 +90,8 @@ export class DataService { const url = type === 'client' ? `${this.baseUrl}/clients/${uuid}` : `${this.baseUrl}/organizational-units/${uuid}`; + + console.log('DELETE URL:', url); // Depuración return this.http.delete(url).pipe( catchError(error => { console.error('Error deleting element', error); @@ -97,6 +99,7 @@ export class DataService { }) ); } + changeParent(uuid: string): Observable { const url = `${this.baseUrl}/organizational-units/${uuid}/change-parent`; @@ -183,6 +186,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/classroom-view/classroom-view.component.css b/ogWebconsole/src/app/components/groups/shared/classroom-view/classroom-view.component.css index 371b00b..bcf134e 100644 --- a/ogWebconsole/src/app/components/groups/shared/classroom-view/classroom-view.component.css +++ b/ogWebconsole/src/app/components/groups/shared/classroom-view/classroom-view.component.css @@ -31,18 +31,11 @@ mat-card { } .client-info { - position: absolute; - top: 20px; - left: 5px; - width: 100%; - height: 100%; - display: flex; - flex-direction: column; - background-color: rgba(0, 0, 0, 0); - color: black; text-align: center; - padding: 15px; - box-sizing: border-box; + margin-top: 5px; + font-size: medium; + color: gray; + align-items: center; } .client-name { diff --git a/ogWebconsole/src/app/components/groups/shared/classroom-view/classroom-view.component.html b/ogWebconsole/src/app/components/groups/shared/classroom-view/classroom-view.component.html index 5085467..5485a3d 100644 --- a/ogWebconsole/src/app/components/groups/shared/classroom-view/classroom-view.component.html +++ b/ogWebconsole/src/app/components/groups/shared/classroom-view/classroom-view.component.html @@ -10,12 +10,9 @@
{{ 'clientAlt' | translate }} -
-
{{ client.name }}
-
- {{ client.ip }} -
-
+
+
+ {{ client.name }}
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..b7bc183 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 @@ -1,63 +1,161 @@ -h1 { - text-align: center; - font-family: 'Roboto', sans-serif; - font-weight: 400; - color: #3f51b5; - margin-bottom: 20px; -} - -.network-form { +.create-client-container { display: flex; flex-direction: column; - gap: 15px; + padding: 16px; + font-family: Arial, sans-serif; + font-size: 14px; + align-items: center; } -.form-field { - width: 100%; - margin-top: 10px; +h1, h3, h4 { + margin: 0 0 16px; + color: #333; + font-weight: 600; +} + +h1 { + font-size: 20px; +} + +h3 { + font-size: 18px; +} + +h4 { + font-size: 16px; + margin-top: 16px; +} + +.inputs-container { + display: flex; + gap: 24px; + margin-top: 16px; } .mat-dialog-content { - padding: 50px; + flex: 1; + background-color: #f9f9f9; + border-radius: 8px; + padding: 16px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); + min-width: 600px; + max-width: 90vw; + width: 800px; } -button { - text-transform: none; - font-size: 16px; - font-weight: 500; +.create-multiple-client-container { + flex: 1; + background-color: #f9f9f9; + border-radius: 8px; + padding: 16px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); } -.mat-slide-toggle { - margin-top: 20px; -} - -mat-option .unit-name { - display: block; -} - -mat-option .unit-path { - display: block; - font-size: 0.8em; - color: gray; -} - -.loading-spinner { - display: block; - margin: 0 auto; - align-items: center; - justify-content: center; -} - -.create-client-container { - position: relative; -} - -.grid-form { +.client-form { display: grid; - grid-template-columns: repeat(2, 1fr); - gap: 20px; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 16px; } .form-field { width: 100%; } + +.mat-form-field { + width: 100%; +} + +.scrollable-table { + max-height: 200px; + overflow-y: auto; + margin-top: 16px; + border: 1px solid #ddd; + border-radius: 8px; +} + +table { + width: 100%; + border-collapse: collapse; +} + +th, td { + text-align: left; + padding: 8px; + border-bottom: 1px solid #ddd; +} + +th { + background-color: #f1f1f1; + font-weight: bold; +} + +tr:hover { + background-color: #f9f9f9; +} + +button { + margin-right: 8px; +} + +button:last-child { + margin-right: 0; +} + +.mat-dialog-actions { + margin-top: 16px; + display: flex; + justify-content: space-between; +} + +button.mat-raised-button { + text-transform: none; + font-weight: 600; +} + +.loading-spinner { + margin: 16px auto; + display: block; +} + +.toggle-button { + background: none; + border: none; + color: #007BFF; + cursor: pointer; + font-size: 14px; + text-decoration: underline; +} + +.toggle-button:hover { + text-decoration: none; +} + +.mat-divider { + margin: 0 16px; +} + +.upload-container { + display: flex; + flex-direction: column; + align-items: flex-start; + gap: 8px; +} + +input[type="file"] { + display: none; +} + +@media (max-width: 768px) { + .inputs-container { + flex-direction: column; + gap: 16px; + } + + .mat-dialog-content, .create-multiple-client-container { + padding: 12px; + } + + .scrollable-table { + max-height: 150px; + } +} 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..56ca328 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,136 @@ -
-

{{ 'addClientDialogTitle' | translate }}

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

{{ 'addClientTitle' | translate }}s

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

Añadir múltiples clientes

+
+ + +

o añadelos manualmente:

+
+ + +
+
- - {{ 'ogLiveLabel' | translate }} - - - {{ oglive.name }} - - - +

Clientes importados:

+
+ + + + + - - {{ 'serialNumberLabel' | translate }} - - + + + + - - {{ 'netifaceLabel' | translate }} - - - {{ type.name }} - - - + + +
Nombre {{ client.name }} IP {{ client.ip }}
+
+
- - {{ 'netDriverLabel' | translate }} - - - {{ type.name }} - - - + - - {{ 'macLabel' | translate }} - {{ 'macHint' | translate }} - - {{ 'macError' | translate }} - + +

Añadir un cliente

+ +
+ + + {{ 'nameLabel' | translate }} + + - - {{ 'ipLabel' | translate }} - {{ 'ipHint' | translate }} - - {{ 'ipError' | translate }} - + + {{ 'ogLiveLabel' | translate }} + + + {{ oglive.name }} + + + - - {{ 'templateLabel' | translate }} - - - {{ template.name }} - - - + + {{ 'serialNumberLabel' | translate }} + + - - {{ 'hardwareProfileLabel' | translate }} - - - {{ unit.description }} - - - {{ 'hardwareProfileError' | translate }} - -
+ + {{ 'netifaceLabel' | translate }} + + + {{ type.name }} + + + + + + {{ 'netDriverLabel' | translate }} + + + {{ type.name }} + + + + + + {{ 'macLabel' | translate }} + {{ 'macHint' | translate }} + + {{ 'macError' | translate }} + + + + {{ 'ipLabel' | translate }} + {{ 'ipHint' | translate }} + + {{ 'ipError' | translate }} + + + + {{ 'templateLabel' | translate }} + + + {{ template.name }} + + + + + + {{ 'hardwareProfileLabel' | translate }} + + + {{ unit.description }} + + + {{ 'hardwareProfileError' | translate }} + + +
+
- - + + +
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..d79f63f 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,19 @@ export class CreateClientComponent implements OnInit { hardwareProfiles: any[] = []; ogLives: any[] = []; templates: any[] = []; - private errorForm: boolean = false; + uploadedClients: any[] = []; + loading: boolean = false; + displayedColumns: string[] = ['name', 'ip']; + isSingleClientForm: boolean = false; + showTextarea: boolean = true; 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 +41,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 +64,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 +87,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,39 +121,123 @@ 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() { - 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.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']); + onFileUpload(event: any): void { + const file = event.target.files[0]; + if (file) { + const reader = new FileReader(); + + reader.onload = (e: any) => { + const textData = e.target.result; + const regex = /host\s+(\S+)\s+\{\s+hardware\s+ethernet\s+([\da-fA-F:]+);\s+fixed-address\s+([\d.]+);\s+\}/g; + let match; + const clients = []; + + while ((match = regex.exec(textData)) !== null) { + clients.push({ + name: match[1], + mac: match[2], + ip: match[3] + }); } - ); + + if (clients.length > 0) { + this.uploadedClients = clients; + this.toastService.success('Archivo cargado correctamente, los datos están listos para enviarse.', 'Éxito'); + this.showTextarea = false; + } else { + this.toastService.error('No se encontraron datos válidos', 'Error'); + this.showTextarea = true; + } + }; + + reader.readAsText(file); } } - + + onTextarea(text: string): void { + const regex = /host\s+(\S+)\s+\{\s+hardware\s+ethernet\s+([\da-fA-F:]+);\s+fixed-address\s+([\d.]+);\s+\}/g; + let match; + const clients = []; + + while ((match = regex.exec(text)) !== null) { + clients.push({ + name: match[1], + mac: match[2], + ip: match[3] + }); + } + + if (clients.length > 0) { + this.uploadedClients = clients; + this.toastService.success('Datos cargados correctamente, los datos están listos para enviarse.', 'Éxito'); + this.showTextarea = false; + } else { + this.toastService.error('No se encontraron datos válidos', 'Error'); + this.showTextarea = true; + } + } + + onSubmit(): void { + if (this.isSingleClientForm) { + if (this.clientForm.valid) { + const formData = this.clientForm.value; + console.log('Form data:', formData); + this.http.post(`${this.baseUrl}/clients`, formData).subscribe( + response => { + this.toastService.success('Cliente creado exitosamente', 'Éxito'); + this.dialogRef.close(response); + }, + error => { + console.error('Error durante POST:', error); + this.toastService.error('Error al crear el cliente', 'Error'); + } + ); + } + } else { + if (this.uploadedClients.length > 0) { + this.uploadedClients.forEach(client => { + const formData = { + organizationalUnit: this.clientForm.value.organizationalUnit || null, + name: client.name || null, + mac: client.mac || null, + ip: client.ip || null, + template: this.clientForm.value.template || null, + hardwareProfile: this.clientForm.value.hardwareProfile || null, + ogLive: this.clientForm.value.ogLive || null, + serialNumber: null, + netiface: null, + netDriver: null + }; + + this.http.post(`${this.baseUrl}/clients`, formData).subscribe( + response => { + this.toastService.success(`Cliente ${client.name} creado exitosamente`, 'Éxito'); + }, + error => { + console.error(`Error al crear el cliente ${client.name}:`, error); + this.toastService.error(`Error al crear el cliente ${client.name}`, 'Error'); + } + ); + }); + + this.uploadedClients = []; + this.dialogRef.close(); + } else { + this.toastService.error('No hay clientes cargados para añadir', 'Error'); + } + } + } + + toggleClientForm(): void { + this.isSingleClientForm = !this.isSingleClientForm; + } + 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'); - } - } } diff --git a/ogWebconsole/src/app/components/groups/shared/legend/legend.component.html b/ogWebconsole/src/app/components/groups/shared/legend/legend.component.html index c60c785..6cd66de 100644 --- a/ogWebconsole/src/app/components/groups/shared/legend/legend.component.html +++ b/ogWebconsole/src/app/components/groups/shared/legend/legend.component.html @@ -19,4 +19,14 @@ computer
{{ 'clientTitle' | translate }}
+ + + school +
Disponible acceso remoto
+
+ + school +
No disponible acceso remoto
+
+ diff --git a/ogWebconsole/src/locale/en.json b/ogWebconsole/src/locale/en.json index 4bb7273..59fe4cd 100644 --- a/ogWebconsole/src/locale/en.json +++ b/ogWebconsole/src/locale/en.json @@ -419,5 +419,6 @@ "menus": "Menus", "TOOLTIP_MENUS": "Menu management (option disabled)", "search": "Search", - "TOOLTIP_SEARCH": "Search function (option disabled)" + "TOOLTIP_SEARCH": "Search function (option disabled)", + "detailsOf": "Details of" } diff --git a/ogWebconsole/src/locale/es.json b/ogWebconsole/src/locale/es.json index bec0ac1..b13e8b4 100644 --- a/ogWebconsole/src/locale/es.json +++ b/ogWebconsole/src/locale/es.json @@ -420,5 +420,9 @@ "menus": "Menús", "TOOLTIP_MENUS": "Gestión de menús (opción deshabilitada)", "search": "Buscar", - "TOOLTIP_SEARCH": "Función de búsqueda (opción deshabilitada)" + "TOOLTIP_SEARCH": "Función de búsqueda (opción deshabilitada)", + "detailsOf": "Detalles de", + "editUnitMenu": "Editar", + "addInternalUnitMenu": "Añadir", + "addClientMenu": "Añadir cliente" }