From 90d969ccd32f6f9ca726525258e94390c74c295f Mon Sep 17 00:00:00 2001 From: Manuel Aranda Date: Thu, 26 Jun 2025 15:55:19 +0200 Subject: [PATCH] refs 2335. Groups new UX --- .../client-main-view.component.html | 5 +- .../components/groups/groups.component.css | 1239 ++++++++++++++++- .../components/groups/groups.component.html | 323 +++-- .../groups/groups.component.spec.ts | 3 +- .../app/components/groups/groups.component.ts | 361 ++++- .../client-view/client-view.component.ts | 4 +- .../manage-organizational-unit.component.html | 4 +- .../manage-organizational-unit.component.ts | 9 +- 8 files changed, 1809 insertions(+), 139 deletions(-) diff --git a/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.html b/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.html index 31e967e..52a3268 100644 --- a/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.html +++ b/ogWebconsole/src/app/components/groups/components/client-main-view/client-main-view.component.html @@ -31,7 +31,10 @@

Discos/Particiones

- {{ clientData.firmwareType }} + + memory + {{ clientData.firmwareType }} +
diff --git a/ogWebconsole/src/app/components/groups/groups.component.css b/ogWebconsole/src/app/components/groups/groups.component.css index f4a5383..722ad3e 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.css +++ b/ogWebconsole/src/app/components/groups/groups.component.css @@ -20,6 +20,8 @@ width: 100%; height: 100%; overflow: hidden; + min-height: 0; + will-change: auto; } .clients-mat-divider { @@ -46,7 +48,7 @@ padding: 1rem !important; box-sizing: border-box !important; min-height: 250px !important; - overflow-y: auto !important; + overflow: hidden !important; } .filters-panel, @@ -56,6 +58,7 @@ padding: 0.5rem !important; box-sizing: border-box !important; margin-bottom: 0 !important; + max-height: 300px !important; } .filters-container { @@ -85,18 +88,20 @@ } .cards-view { - max-height: none !important; + max-height: calc(100vh - 400px) !important; } .clients-table { - max-height: unset !important; - overflow: unset !important; - display: table !important; - flex-direction: unset !important; + max-height: none !important; + overflow: auto !important; + display: flex !important; + flex-direction: column !important; + flex: 1 !important; } .clients-container { padding: 0em 1em 0em 1em !important; + overflow: auto !important; } } @@ -162,7 +167,9 @@ .clients-view { flex-grow: 1; - overflow-y: auto; + display: flex; + flex-direction: column; + scroll-behavior: smooth; } .cards-view { @@ -184,16 +191,23 @@ .list-view { overflow-x: auto; + display: flex; + flex-direction: column; + flex: 1; + scroll-behavior: smooth; } .clients-table { - max-height: calc(100vh - 330px); + flex: 1; overflow: auto; display: flex; flex-direction: column; width: 100%; table-layout: auto; border-collapse: collapse; + will-change: scroll-position; + scroll-behavior: smooth; + -webkit-overflow-scrolling: touch; } .clients-table th, @@ -223,11 +237,13 @@ width: 100%; margin-left: 1rem; background-color: #fafafa; + margin-bottom: 1rem; } .list-paginator { width: 100%; background-color: #f3f3f3; + margin-bottom: 1rem; } .actions mat-icon { @@ -305,7 +321,6 @@ mat-tree mat-tree-node:hover mat-icon { } mat-tree mat-tree-node mat-icon.node-icon { - color: #757575; margin-right: 10px; } @@ -382,19 +397,26 @@ mat-tree mat-tree-node.disabled:hover { .clients-container { flex: 8; box-sizing: border-box; - overflow: hidden; + overflow: auto; display: flex; flex-direction: column; padding: 0rem 1rem 0rem 0.5rem; + background: #fafbfc; + border-radius: 12px; + margin-left: 0.5rem; + min-height: 0; } .filters-and-tree-container { - flex: 2; display: flex; flex-direction: column; - flex-shrink: 1; - width: 100%; + gap: 0.5rem; + flex: 2; + min-width: 400px; + max-width: 100%; box-sizing: border-box; + width: 100%; + padding: 0 0.5rem; } .filters-container { @@ -418,8 +440,321 @@ mat-tree mat-tree-node.disabled:hover { .tree-container { flex-grow: 1; overflow-y: auto; - max-height: calc(100% - var(--filters-panel-height)); margin-bottom: 1rem; + background: white; + border-radius: 12px; + border: 1px solid #e9ecef; + width: 100%; + min-width: 0; + max-height: calc(100vh - 400px); + min-height: 200px; + will-change: scroll-position; + scroll-behavior: smooth; + -webkit-overflow-scrolling: touch; +} + +.tree-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 24px; + border-bottom: 2px solid #e9ecef; + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%); + border-radius: 12px 12px 0 0; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); +} + +.tree-title { + display: flex; + align-items: center; + gap: 12px; + margin: 0; + font-size: 1.2rem; + font-weight: 700; + color: #2c3e50; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.tree-title mat-icon { + color: #3f51b5; + font-size: 28px; + width: 28px; + height: 28px; + background: rgba(63, 81, 181, 0.1); + border-radius: 50%; + padding: 4px; +} + +.tree-actions { + display: flex; + gap: 8px; +} + +.tree-actions button { + color: #6c757d; + transition: all 0.3s ease; + border-radius: 50%; + width: 40px; + height: 40px; +} + +.tree-actions button:hover { + color: #3f51b5; + background-color: rgba(63, 81, 181, 0.1); + transform: scale(1.1); + box-shadow: 0 2px 8px rgba(63, 81, 181, 0.2); +} + +.modern-tree { + background: transparent; + padding: 12px 0; + border-radius: 0 0 12px 12px; +} + +.tree-node { + margin: 2px 8px; + border-radius: 8px; + transition: all 0.3s ease; + position: relative; + border-left: 3px solid transparent; +} + +.tree-node:hover { + background-color: rgba(102, 126, 234, 0.08); + transform: translateX(4px); + border-left-color: #667eea; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.node-content { + display: flex; + align-items: center; + padding: 10px 16px; + gap: 12px; + width: 100%; + min-height: 52px; +} + +.expand-button { + min-width: 36px; + color: #6c757d; + transition: all 0.3s ease; + border-radius: 50%; +} + +.expand-button:hover:not(.disabled-toggle) { + color: #667eea; + background-color: rgba(102, 126, 234, 0.1); + transform: scale(1.1); +} + +.expand-icon { + transition: transform 0.3s ease; + font-size: 20px; +} + +.tree-node:hover .expand-icon { + transform: scale(1.2); +} + +.node-info { + flex: 1; + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; +} + +.node-main { + display: flex; + align-items: center; + gap: 8px; +} + +.node-icon { + font-size: 22px; + width: 22px; + height: 22px; + transition: all 0.3s ease; + border-radius: 50%; + padding: 4px; + background: rgba(255, 255, 255, 0.8); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.tree-node:hover .node-icon { + transform: scale(1.1); + box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15); +} + +.node-name { + font-weight: 500; + color: #2c3e50; + font-size: 0.95rem; + transition: color 0.2s ease; +} + +.tree-node:hover .node-name { + color: #1a237e; + font-weight: 600; +} + +.node-details { + display: flex; + flex-direction: column; + gap: 4px; + font-size: 0.8rem; + color: #6c757d; +} + +.node-ip { + font-family: 'Courier New', monospace; + color: #495057; + font-weight: 500; +} + +.node-count { + background: rgba(102, 126, 234, 0.1); + color: #667eea; + padding: 2px 8px; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + display: inline-block; + margin-top: 2px; +} + +.node-status { + padding: 4px 8px; + border-radius: 12px; + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.node-status.status-off { + background: linear-gradient(135deg, #dc3545, #c82333); + color: white; + box-shadow: 0 1px 3px rgba(220, 53, 69, 0.3); +} + +.node-status.status-og-live { + background: linear-gradient(135deg, #28a745, #20c997); + color: white; + box-shadow: 0 1px 3px rgba(40, 167, 69, 0.3); +} + +.node-status.status-linux { + background: linear-gradient(135deg, #17a2b8, #138496); + color: white; + box-shadow: 0 1px 3px rgba(23, 162, 184, 0.3); +} + +.node-status.status-windows { + background: linear-gradient(135deg, #007bff, #0056b3); + color: white; + box-shadow: 0 1px 3px rgba(0, 123, 255, 0.3); +} + +.node-status.status-busy { + background: linear-gradient(135deg, #ffc107, #e0a800); + color: #212529; + box-shadow: 0 1px 3px rgba(255, 193, 7, 0.3); +} + +.node-status.status-disconnected { + background: linear-gradient(135deg, #6c757d, #545b62); + color: white; + box-shadow: 0 1px 3px rgba(108, 117, 125, 0.3); +} + +.node-status.status-initializing { + background: linear-gradient(135deg, #fd7e14, #e55a00); + color: white; + box-shadow: 0 1px 3px rgba(253, 126, 20, 0.3); +} + +.node-actions { + opacity: 0; + transition: opacity 0.2s ease; +} + +.tree-node:hover .node-actions { + opacity: 1; +} + +.menu-button { + min-width: 32px; + color: #6c757d; + transition: all 0.2s ease; +} + +.menu-button:hover { + color: #667eea; + background-color: rgba(102, 126, 234, 0.1); +} + +.selected-node { + background: linear-gradient(135deg, rgba(102, 126, 234, 0.15), rgba(102, 126, 234, 0.08)); + border-left-color: #667eea; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.2); +} + +.selected-node .node-name { + color: #1a237e; + font-weight: 600; +} + +.selected-node .node-icon { + background: rgba(102, 126, 234, 0.1); + box-shadow: 0 2px 8px rgba(102, 126, 234, 0.3); +} + +/* Animaciones */ +@keyframes slideIn { + from { + opacity: 0; + transform: translateX(-20px); + } + to { + opacity: 1; + transform: translateX(0); + } +} + +.tree-node { + animation: slideIn 0.3s ease-out; +} + +/* Responsive */ +@media (max-width: 768px) { + .tree-header { + padding: 12px 16px; + } + + .tree-title { + font-size: 1rem; + } + + .node-content { + padding: 8px 12px; + min-height: 48px; + } + + .node-icon { + font-size: 20px; + width: 20px; + height: 20px; + } + + .node-name { + font-size: 0.9rem; + } + + .node-details { + font-size: 0.75rem; + } } .client-item { @@ -428,24 +763,26 @@ mat-tree mat-tree-node.disabled:hover { } .client-card { - background-color: #fff; - border: 1px solid #ccc; + background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); + border: 1.5px solid #e1e5e9cc; border-radius: 8px; overflow: hidden; - box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06); width: 100%; - max-width: 300px; + max-width: 260px; display: flex; flex-direction: column; align-items: center; text-align: center; - transition: transform 0.3s ease, box-shadow 0.3s ease; + transition: all 0.2s ease; position: relative; + padding: 6px 4px; } .client-card:hover { - transform: translateY(-5px); - box-shadow: 0 6px 10px rgba(0, 0, 0, 0.15); + transform: translateY(-2px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12); + background: linear-gradient(135deg, #ffffff 0%, #f0f4ff 100%); } .client-details { @@ -454,6 +791,8 @@ mat-tree mat-tree-node.disabled:hover { flex-direction: column; justify-content: space-between; margin-top: 4px; + width: 100%; + gap: 4px; } .type-view-text { @@ -465,19 +804,76 @@ mat-tree mat-tree-node.disabled:hover { justify-content: center; align-items: center; gap: 1px; - margin-top: 10px; + margin-top: 4px; + padding: 3px; + border-radius: 4px; + width: 100%; } -.client-status-container { - display: flex; - align-items: center; - gap: 5px; +.action-icons button { + transition: all 0.15s ease; +} + +.action-icons button:hover { + transform: scale(1.05); + background: rgba(63, 81, 181, 0.08); +} + +.client-image { + width: 32px; + height: 32px; + margin: 4px 0; + filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1)); + transition: transform 0.2s ease; +} + +/* Iconos más grandes solo en vista de tarjetas */ +.cards-view .client-image { + width: 42px; + height: 42px; + margin: 5px 0; + filter: drop-shadow(0 2px 4px rgba(0, 0, 0, 0.15)); +} + +.client-card:hover .client-image { + transform: scale(1.05); +} + +.cards-view .client-card:hover .client-image { + transform: scale(1.1); +} + +.client-name { + display: block; + font-weight: 600; + font-size: 0.85rem; + margin-bottom: 2px; + padding: 0 4px; + color: #2c3e50; + line-height: 1.2; + word-break: break-word; } .client-ip { display: block; - font-size: 0.9em; - color: #666; + font-size: 0.7rem; + color: #6c757d; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + padding: 1px 4px; + border-radius: 3px; + margin: 1px 0; + word-break: break-all; +} + +.client-mac { + display: block; + font-size: 0.6rem; + color: #6c757d; + font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace; + padding: 1px 3px; + border-radius: 3px; + margin: 1px 0; + word-break: break-all; } .clients-list { @@ -487,8 +883,13 @@ mat-tree mat-tree-node.disabled:hover { } .sync-spinner { - margin-left: 1em; - margin-right: 1em; + position: absolute; + top: 4px; + right: 4px; + z-index: 2; + background: rgba(255, 255, 255, 0.9); + border-radius: 50%; + padding: 1px; } .mat-elevation-z8 { @@ -561,9 +962,783 @@ mat-button-toggle-group { .clients-grid { display: grid; - grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); - gap: 16px; + grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); + gap: 10px; padding: 0.5rem 1rem 1rem 1rem; width: 100%; box-sizing: border-box; } + +/* Estilo para hacer el backdrop no clickeable */ +::ng-deep .non-clickable-backdrop { + pointer-events: none !important; +} + +/* Estados de los PCs - Colores exactos de los SVG */ +.status-off .client-card { + border-color: #b0b0b0cc; + background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%); +} + +.status-off .client-image { + filter: drop-shadow(0 2px 4px rgba(102, 102, 102, 0.3)); +} + +.status-off:hover .client-card { + border-color: #a0a0a0cc; + background: linear-gradient(135deg, #f8f9fa 0%, #dee2e6 100%); +} + +.status-busy .client-card { + border-color: #ea332388; + background: linear-gradient(135deg, #fff5f5 0%, #ffe6e6 100%); +} + +.status-busy .client-image { + filter: drop-shadow(0 2px 4px rgba(234, 51, 35, 0.3)); +} + +.status-busy:hover .client-card { + border-color: #d6338488; + background: linear-gradient(135deg, #fff5f5 0%, #ffebeb 100%); +} + +.status-disconnected .client-card { + border-color: #ff000088; + background: linear-gradient(135deg, #fff5f5 0%, #ffe6e6 100%); +} + +.status-disconnected .client-image { + filter: drop-shadow(0 2px 4px rgba(255, 0, 0, 0.3)); +} + +.status-disconnected:hover .client-card { + border-color: #dc354588; + background: linear-gradient(135deg, #fff5f5 0%, #ffebeb 100%); +} + +.status-initializing .client-card { + border-color: #75fbfd88; + background: linear-gradient(135deg, #f0f9ff 0%, #e6f3ff 100%); +} + +.status-initializing .client-image { + filter: drop-shadow(0 2px 4px rgba(117, 251, 253, 0.3)); +} + +.status-initializing:hover .client-card { + border-color: #17a2b888; + background: linear-gradient(135deg, #f0f9ff 0%, #d1ecf1 100%); +} + +.status-og-live .client-card { + border-color: #ffff5588; + background: linear-gradient(135deg, #fffbf0 0%, #fff3cd 100%); +} + +.status-og-live .client-image { + filter: drop-shadow(0 2px 4px rgba(255, 255, 85, 0.3)); +} + +.status-og-live:hover .client-card { + border-color: #ffc10788; + background: linear-gradient(135deg, #fffbf0 0%, #ffeaa7 100%); +} + +.status-linux .client-card { + border-color: #ea33f788; + background: linear-gradient(135deg, #f8f0ff 0%, #f0e6ff 100%); +} + +.status-linux .client-image { + filter: drop-shadow(0 2px 4px rgba(234, 51, 247, 0.3)); +} + +.status-linux:hover .client-card { + border-color: #6f42c188; + background: linear-gradient(135deg, #f8f0ff 0%, #e6d9ff 100%); +} + +.status-linux-session .client-card { + border-color: #ea33f788; + background: linear-gradient(135deg, #f8f0ff 0%, #f0e6ff 100%); +} + +.status-linux-session .client-image { + filter: drop-shadow(0 2px 4px rgba(234, 51, 247, 0.3)); +} + +.status-linux-session:hover .client-card { + border-color: #6f42c188; + background: linear-gradient(135deg, #f8f0ff 0%, #e6d9ff 100%); +} + +.status-windows .client-card { + border-color: #0000f588; + background: linear-gradient(135deg, #f0f8ff 0%, #e6f3ff 100%); +} + +.status-windows .client-image { + filter: drop-shadow(0 2px 4px rgba(0, 0, 245, 0.3)); +} + +.status-windows:hover .client-card { + border-color: #007bff88; + background: linear-gradient(135deg, #f0f8ff 0%, #cce7ff 100%); +} + +.status-windows-session .client-card { + border-color: #0000f588; + background: linear-gradient(135deg, #f0f8ff 0%, #e6f3ff 100%); +} + +.status-windows-session .client-image { + filter: drop-shadow(0 2px 4px rgba(0, 0, 245, 0.3)); +} + +.status-windows-session:hover .client-card { + border-color: #007bff88; + background: linear-gradient(135deg, #f0f8ff 0%, #cce7ff 100%); +} + +.status-macos .client-card { + border-color: #f19e3988; + background: linear-gradient(135deg, #fff8f0 0%, #ffe6cc 100%); +} + +.status-macos .client-image { + filter: drop-shadow(0 2px 4px rgba(241, 158, 57, 0.3)); +} + +.status-macos:hover .client-card { + border-color: #fd7e1488; + background: linear-gradient(135deg, #fff8f0 0%, #ffd280 100%); +} + +/* Estilos para el checkbox */ +.client-card mat-checkbox { + position: absolute; + top: 4px; + left: 4px; + z-index: 2; +} + +/* Responsive adjustments - manteniendo tamaño compacto */ +@media (max-width: 768px) { + .clients-grid { + grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); + gap: 8px; + padding: 0.5rem 0.75rem 0.75rem 0.75rem; + } + + .client-card { + padding: 5px 3px; + } + + .cards-view .client-image { + width: 36px; + height: 36px; + } + + .client-name { + font-size: 0.75rem; + } + + .client-ip { + font-size: 0.6rem; + } + + .client-mac { + font-size: 0.5rem; + } +} + +@media (max-width: 480px) { + .clients-grid { + grid-template-columns: repeat(auto-fill, minmax(100px, 1fr)); + gap: 6px; + padding: 0.5rem; + } + + .client-card { + padding: 4px 2px; + } + + .cards-view .client-image { + width: 32px; + height: 32px; + } +} + +/* Gradiente solo en la celda de status (primera columna) */ +.status-off.mat-row .mat-column-status { + background: linear-gradient(to right, #fff 20%, #f2f4f7 100%) !important; +} +.status-busy.mat-row .mat-column-status { + background: linear-gradient(to right, #fff 20%, #ffeaea 100%) !important; +} +.status-disconnected.mat-row .mat-column-status { + background: linear-gradient(to right, #fff 20%, #ffdede 100%) !important; +} +.status-initializing.mat-row .mat-column-status { + background: linear-gradient(to right, #fff 20%, #e3fafd 100%) !important; +} +.status-og-live.mat-row .mat-column-status { + background: linear-gradient(to right, #fff 20%, #fffbcf 100%) !important; +} +.status-linux.mat-row .mat-column-status { + background: linear-gradient(to right, #fff 20%, #f7eafd 100%) !important; +} +.status-linux-session.mat-row .mat-column-status { + background: linear-gradient(to right, #fff 20%, #f1e3fd 100%) !important; +} +.status-windows.mat-row .mat-column-status { + background: linear-gradient(to right, #fff 20%, #e3eafd 100%) !important; +} +.status-windows-session.mat-row .mat-column-status { + background: linear-gradient(to right, #fff 20%, #e0e7ff 100%) !important; +} +.status-macos.mat-row .mat-column-status { + background: linear-gradient(to right, #fff 20%, #fff2de 100%) !important; +} + +/** Chip de firmware más bonito **/ +.firmware-chip { + display: inline-flex; + align-items: center; + background: #e0e0e0; + color: #333; + border-radius: 6px; + border: 1.5px solid #bdbdbd; + font-weight: 600; + font-size: 0.95em; + padding: 0 12px; + height: 28px; + box-shadow: none; + letter-spacing: 0.5px; +} +.firmware-chip mat-icon { + margin-left: 4px; + font-size: 16px; + width: 16px; + height: 16px; +} + +/* Estadísticas */ +.stats-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + margin: 1rem 0; + padding: 0 1rem; +} + +.stat-card { + background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); + border: 1px solid #e9ecef; + border-radius: 12px; + padding: 1.5rem; + display: flex; + align-items: center; + gap: 1rem; + transition: all 0.3s ease; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + position: relative; + overflow: hidden; +} + +.stat-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 3px; + background: linear-gradient(90deg, #667eea, #764ba2); + transform: scaleX(0); + transition: transform 0.3s ease; +} + +.stat-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1); +} + +.stat-card:hover::before { + transform: scaleX(1); +} + +.stat-icon { + display: flex; + align-items: center; + justify-content: center; + width: 48px; + height: 48px; + border-radius: 12px; + background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); + color: white; + font-size: 24px; + transition: all 0.3s ease; +} + +.stat-card:hover .stat-icon { + transform: scale(1.1); +} + +.stat-icon.online { + background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%); +} + +.stat-icon.offline { + background: linear-gradient(135deg, #f44336 0%, #d32f2f 100%); +} + +.stat-icon.busy { + background: linear-gradient(135deg, #ff9800 0%, #f57c00 100%); +} + +.stat-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.stat-number { + font-size: 1.75rem; + font-weight: 700; + color: #2c3e50; + line-height: 1; +} + +.stat-label { + font-size: 0.875rem; + color: #6c757d; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +/* Tabla mejorada */ +.table-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + background: #f8f9fa; + border-bottom: 1px solid #dee2e6; +} + +.table-info { + color: #6c757d; + font-size: 0.9rem; +} + +.table-actions { + display: flex; + gap: 10px; +} + +.column-header { + display: flex; + align-items: center; + gap: 5px; +} + +.sort-button { + opacity: 0.5; + transition: opacity 0.3s ease; +} + +.sort-button:hover { + opacity: 1; +} + +.sort-button.active { + opacity: 1; + color: #667eea; +} + +/* Celdas mejoradas */ +.client-cell { + display: flex; + flex-direction: column; + gap: 2px; +} + +.client-name { + font-weight: 500; + color: #212529; +} + +.client-ip { + font-size: 0.85rem; + color: #212529; +} + +.client-mac { + font-size: 0.75rem; + color: #6c757d; +} + +.oglive-cell { + display: flex; + flex-direction: column; + gap: 2px; +} + +.oglive-kernel { + font-size: 0.85rem; + color: #212529; +} + +.oglive-date { + font-size: 0.7rem; + color: #6c757d; + font-style: italic; +} + +/* Botones de acción */ +.action-buttons { + display: flex; + gap: 5px; + justify-content: center; +} + +/* Filas seleccionadas */ +.selected-row { + background-color: rgba(102, 126, 234, 0.1) !important; +} + +.mat-row:hover { + background-color: rgba(102, 126, 234, 0.05); + cursor: pointer; +} + +/* Responsive para nuevas funcionalidades */ +@media (max-width: 768px) { + .stats-container { + grid-template-columns: repeat(2, 1fr); + gap: 0.75rem; + padding: 0 0.5rem; + } + + .stat-card { + padding: 1rem; + gap: 0.75rem; + } + + .stat-icon { + width: 40px; + height: 40px; + font-size: 20px; + } + + .stat-number { + font-size: 1.5rem; + } + + .stat-label { + font-size: 0.75rem; + } + + .table-header { + flex-direction: column; + gap: 10px; + text-align: center; + } + + .action-buttons { + flex-direction: column; + gap: 2px; + } +} + +@media (max-width: 480px) { + .stats-container { + grid-template-columns: 1fr; + gap: 0.5rem; + } + + .stat-card { + padding: 0.75rem; + } + + .stat-icon { + width: 36px; + height: 36px; + font-size: 18px; + } + + .stat-number { + font-size: 1.25rem; + } + + .stat-label { + font-size: 0.7rem; + } +} + +/* Animaciones */ +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.stats-container, .table-header { + animation: fadeIn 0.5s ease-out; +} + +/* Separadores visuales */ +.tree-node:not(:last-child) { + border-bottom: 1px solid rgba(0, 0, 0, 0.05); + margin-bottom: 2px; +} + +.tree-node:hover:not(:last-child) { + border-bottom-color: rgba(102, 126, 234, 0.2); +} + +/* Dashboard de Estadísticas */ +.dashboard-stats { + margin: 1rem 0 1.5rem 0; + padding: 0 1rem; +} + +.stats-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 1rem; + max-width: 100%; +} + +.stat-card { + background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%); + border: 1px solid #e9ecef; + border-radius: 12px; + padding: 1.5rem; + display: flex; + align-items: center; + gap: 1rem; + transition: all 0.3s ease; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05); + position: relative; + overflow: hidden; +} + +.stat-card::before { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + height: 4px; + background: linear-gradient(90deg, #667eea, #764ba2); + opacity: 0; + transition: opacity 0.3s ease; +} + +.stat-card:hover { + transform: translateY(-4px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); + border-color: #667eea; +} + +.stat-card:hover::before { + opacity: 1; +} + +.stat-icon { + display: flex; + align-items: center; + justify-content: center; + width: 60px; + height: 60px; + border-radius: 50%; + background: linear-gradient(135deg, #667eea, #764ba2); + color: white; + font-size: 28px; + box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); + transition: all 0.3s ease; +} + +.stat-card:hover .stat-icon { + transform: scale(1.1); + box-shadow: 0 6px 20px rgba(102, 126, 234, 0.4); +} + +.stat-icon.online { + background: linear-gradient(135deg, #28a745, #20c997); + box-shadow: 0 4px 12px rgba(40, 167, 69, 0.3); +} + +.stat-icon.offline { + background: linear-gradient(135deg, #dc3545, #c82333); + box-shadow: 0 4px 12px rgba(220, 53, 69, 0.3); +} + +.stat-icon.busy { + background: linear-gradient(135deg, #ffc107, #e0a800); + box-shadow: 0 4px 12px rgba(255, 193, 7, 0.3); +} + +.stat-content { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +.stat-number { + font-size: 2rem; + font-weight: 700; + color: #2c3e50; + line-height: 1; + margin: 0; +} + +.stat-label { + font-size: 0.9rem; + color: #6c757d; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.5px; + margin: 0; +} + +/* Responsive para el dashboard */ +@media (max-width: 1200px) { + .stats-grid { + grid-template-columns: repeat(auto-fit, minmax(180px, 1fr)); + gap: 0.75rem; + } + + .stat-card { + padding: 1.25rem; + } + + .stat-icon { + width: 50px; + height: 50px; + font-size: 24px; + } + + .stat-number { + font-size: 1.75rem; + } +} + +@media (max-width: 768px) { + .dashboard-stats { + margin: 0.75rem 0 1rem 0; + padding: 0 0.5rem; + } + + .stats-grid { + grid-template-columns: repeat(2, 1fr); + gap: 0.5rem; + } + + .stat-card { + padding: 1rem; + flex-direction: column; + text-align: center; + gap: 0.75rem; + } + + .stat-icon { + width: 45px; + height: 45px; + font-size: 22px; + } + + .stat-number { + font-size: 1.5rem; + } + + .stat-label { + font-size: 0.8rem; + } +} + +@media (max-width: 480px) { + .stats-grid { + grid-template-columns: 1fr; + } +} + +/* Estilos para truncate en tabla */ +.truncate-cell { + max-width: 150px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.truncate-cell-wide { + max-width: 180px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.truncate-cell-medium { + max-width: 140px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +.truncate-cell-narrow { + max-width: 100px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +/* Para vista de tarjetas */ +.client-card .client-name { + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + display: block; +} + +/* Responsive para truncate */ +@media (max-width: 1200px) { + .truncate-cell { + max-width: 120px; + } + + .truncate-cell-wide { + max-width: 150px; + } + + .truncate-cell-medium { + max-width: 110px; + } + + .truncate-cell-narrow { + max-width: 80px; + } +} + +@media (max-width: 768px) { + .truncate-cell { + max-width: 100px; + } + + .truncate-cell-wide { + max-width: 120px; + } + + .truncate-cell-medium { + max-width: 90px; + } + + .truncate-cell-narrow { + max-width: 70px; + } +} + diff --git a/ogWebconsole/src/app/components/groups/groups.component.html b/ogWebconsole/src/app/components/groups/groups.component.html index bf30523..15345e3 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.html +++ b/ogWebconsole/src/app/components/groups/groups.component.html @@ -1,3 +1,8 @@ + + +
@@ -96,8 +101,6 @@ - -
- - +

+ account_tree + {{ 'organizationalStructure' | translate }} +

+
+ + +
+
+ + + - - - {{ - 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' ? 'business' + : node.type === 'classrooms-group' ? 'meeting_room' + : node.type === 'classroom' ? 'school' + : node.type === 'clients-group' ? 'dns' + : node.type === 'client' ? 'computer' + : 'folder' + }} + + {{ node.name }} +
+ +
+ + {{ node.ip }} + + + {{ node.children.length }} {{ getNodeCountLabel(node.children.length) }} + +
+
+ +
+ +
+
- - - - {{ - 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 }} - - + +
+ + +
+
+ + {{ + node.type === 'organizational-unit' ? 'business' + : node.type === 'classrooms-group' ? 'meeting_room' + : node.type === 'classroom' ? 'school' + : node.type === 'clients-group' ? 'dns' + : node.type === 'client' ? 'computer' + : 'folder' + }} + + {{ node.name }} +
+ +
+ + {{ node.ip }} + {{ node.mac }} + + {{ getStatusLabel(node.status) }} + + +
+
+ +
+ +
+
@@ -205,6 +267,10 @@ storage {{ 'partitions' | translate }} +
+
+
+
+ computer +
+
+
{{ totalStats.total }}
+
{{ 'totalClients' | translate }}
+
+
+
+
+ wifi_off +
+
+
{{ getStatusCount('off') }}
+
{{ 'offline' | translate }}
+
+
+
+
+ wifi +
+
+
{{ getStatusCount('og-live') + getStatusCount('linux') + getStatusCount('windows') }}
+
{{ 'online' | translate }}
+
+
+
+
+ hourglass_empty +
+
+
{{ getStatusCount('busy') }}
+
{{ 'busy' | translate }}
+
+
+
+
@@ -262,7 +367,7 @@ [indeterminate]="selection.hasValue() && !isAllSelected()">
-
+
@@ -271,9 +376,9 @@ alt="Client Icon" class="client-image" />
- {{ client.name }} + {{ client.name }} {{ client.ip }} - {{ client.mac }} + {{ client.mac }}
list_alt {{ 'procedimientosCliente' | translate }} +
- +
+
+
+ {{ 'showingResults' | translate: { from: getPaginationFrom(), to: getPaginationTo(), total: getPaginationTotal() } }} +
+
+ + +
+
+
- +
+ + - + - + - + - + - + - - + + - - + + - + - - + +
- {{ 'status' | translate }} +
+ {{ 'status' | translate }} + +
+
@@ -359,72 +489,101 @@ -
{{ 'name' | translate }} +
+ {{ 'name' | translate }} + +
+
-

{{ client.name }}

+
+ {{ client.name }} +
IP +
+ IP + +
+
-
- {{ client.ip }} - {{ client.mac }} +
+ {{ client.ip }} + {{ client.mac }}
{{ 'firmwareType' | translate }} +
+ {{ 'firmwareType' | translate }} + +
+
- + {{ client.firmwareType }} OG Live OG Live -
- {{ client.ogLive?.kernel }} - {{ client.ogLive?.date | date }} +
+ {{ client.ogLive?.kernel }} + {{ client.ogLive?.date | date }}
+
{{ 'maintenance' | translate }} {{ 'maintenance' | translate }} {{ client.maintenance }} {{ 'subnet' | translate }} {{ 'subnet' | translate }} {{ client.subnet }} {{ 'pxeTemplate' | translate }} {{ client.pxeTemplate?.name }} {{ 'pxeTemplate' | translate }} {{ client.pxeTemplate?.name }} {{ 'parent' | translate }} {{ client.parentName }} {{ 'parent' | translate }} {{ client.parentName }} {{ 'actions' | translate }} {{ 'actions' | translate }} - - - +
+ + + + +
+
{ let component: GroupsComponent; @@ -39,7 +40,7 @@ describe('GroupsComponent', () => { }; await TestBed.configureTestingModule({ - declarations: [GroupsComponent, ExecuteCommandComponent, LoadingComponent], + declarations: [GroupsComponent, ExecuteCommandComponent, LoadingComponent, ModalOverlayComponent], imports: [ HttpClientTestingModule, ToastrModule.forRoot(), diff --git a/ogWebconsole/src/app/components/groups/groups.component.ts b/ogWebconsole/src/app/components/groups/groups.component.ts index c301173..a0eea6c 100644 --- a/ogWebconsole/src/app/components/groups/groups.component.ts +++ b/ogWebconsole/src/app/components/groups/groups.component.ts @@ -15,7 +15,6 @@ import { ShowOrganizationalUnitComponent } from './shared/organizational-units/s import { LegendComponent } from './shared/legend/legend.component'; import { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/delete-modal.component'; import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom-view-modal'; -import { MatSort } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { PageEvent } from '@angular/material/paginator'; import { CreateMultipleClientComponent } from "./shared/clients/create-multiple-client/create-multiple-client.component"; @@ -31,6 +30,7 @@ import { PartitionTypeOrganizatorComponent } from './shared/partition-type-organ import { ClientTaskLogsComponent } from '../task-logs/client-task-logs/client-task-logs.component'; import {ChangeParentComponent} from "./shared/change-parent/change-parent.component"; import { AuthService } from '@services/auth.service'; +import { ClientPendingTasksComponent } from '../task-logs/client-pending-tasks/client-pending-tasks.component'; enum NodeType { OrganizationalUnit = 'organizational-unit', @@ -79,6 +79,29 @@ export class GroupsComponent implements OnInit, OnDestroy { arrayClients: any[] = []; filters: { [key: string]: string } = {}; private clientFilterSubject = new Subject(); + loading = false; + + // Nuevas propiedades para funcionalidades mejoradas + selectedClient: any = null; + sortBy: string = 'name'; + sortDirection: 'asc' | 'desc' = 'asc'; + currentSortColumn: string = 'name'; + + // Estadísticas totales + totalStats: { + total: number; + off: number; + online: number; + busy: number; + } = { + total: 0, + off: 0, + online: 0, + busy: 0 + }; + + // Tipos de firmware disponibles + firmwareTypes: string[] = []; protected status = [ { value: 'off', name: 'Apagado' }, @@ -95,16 +118,6 @@ export class GroupsComponent implements OnInit, OnDestroy { displayedColumns: string[] = ['select', 'status', 'ip', 'firmwareType', 'name', 'oglive', 'subnet', 'pxeTemplate', 'actions']; - private _sort!: MatSort; - - @ViewChild(MatSort) - set matSort(ms: MatSort) { - this._sort = ms; - if (this.selectedClients) { - this.selectedClients.sort = this._sort; - } - } - private subscriptions: Subscription = new Subscription(); constructor( @@ -404,8 +417,14 @@ export class GroupsComponent implements OnInit, OnDestroy { public fetchClientsForNode(node: any, selectedClientsBeforeEdit: string[] = []): void { const params = new HttpParams({ fromObject: this.filters }); + // Agregar parámetros de ordenamiento al backend + let backendParams = { ...this.filters }; + if (this.sortBy) { + backendParams['order[' + this.sortBy + ']'] = this.sortDirection; + } + this.isLoadingClients = true; - this.http.get(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}&page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params }).subscribe({ + this.http.get(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}&page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params: backendParams }).subscribe({ next: (response: any) => { this.selectedClients.data = response['hydra:member']; if (this.selectedNode) { @@ -423,6 +442,9 @@ export class GroupsComponent implements OnInit, OnDestroy { this.selection.select(client); } }); + + // Calcular estadísticas después de cargar los clientes + this.calculateLocalStats(); }, error: () => { this.isLoadingClients = false; @@ -438,25 +460,35 @@ export class GroupsComponent implements OnInit, OnDestroy { } addOU(event: MouseEvent, parent: TreeNode | null = null): void { + this.loading = true; event.stopPropagation(); const dialogRef = this.dialog.open(ManageOrganizationalUnitComponent, { data: { parent }, width: '900px', + disableClose: true, + hasBackdrop: true, + backdropClass: 'non-clickable-backdrop', }); dialogRef.afterClosed().subscribe((newUnit) => { if (newUnit) { this.refreshData(newUnit.uuid); } + this.loading = false; }); + } addClient(event: MouseEvent, organizationalUnit: TreeNode | null = null): void { + this.loading = true; event.stopPropagation(); const targetNode = organizationalUnit || this.selectedNode; const dialogRef = this.dialog.open(ManageClientComponent, { data: { organizationalUnit: targetNode }, width: '900px', + disableClose: true, + hasBackdrop: true, + backdropClass: 'non-clickable-backdrop', }); dialogRef.afterClosed().subscribe((result) => { @@ -469,17 +501,22 @@ export class GroupsComponent implements OnInit, OnDestroy { this.refreshData(parentNode.uuid); } } + this.loading = false; }); } addMultipleClients(event: MouseEvent, organizationalUnit: TreeNode | null = null): void { + this.loading = true; event.stopPropagation(); const targetNode = organizationalUnit || this.selectedNode; const dialogRef = this.dialog.open(CreateMultipleClientComponent, { data: { organizationalUnit: targetNode }, width: '900px', + disableClose: true, + hasBackdrop: true, + backdropClass: 'non-clickable-backdrop', }); dialogRef.afterClosed().subscribe((result) => { if (result?.success) { @@ -494,29 +531,33 @@ export class GroupsComponent implements OnInit, OnDestroy { console.error('No se encontró el nodo padre después de la creación masiva.'); } } + this.loading = false; }); } onEditNode(event: MouseEvent, node: TreeNode | null): void { event.stopPropagation(); + this.loading = true; const uuid = node ? this.extractUuid(node['@id']) : null; if (!uuid) return; const dialogRef = node?.type !== NodeType.Client - ? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px' }) - : this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px' }); + ? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px', disableClose: true, hasBackdrop: true, backdropClass: 'non-clickable-backdrop' }) + : this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px', disableClose: true, hasBackdrop: true, backdropClass: 'non-clickable-backdrop' }); dialogRef.afterClosed().subscribe((result) => { if (result?.success) { this.refreshData(node?.id); } this.menuTriggers.forEach(trigger => trigger.closeMenu()); + this.loading = false; }); } onDeleteClick(event: MouseEvent, entity: TreeNode | Client | null): void { event.stopPropagation(); + this.loading = true; if (!entity) return; const uuid = entity['@id'] ? this.extractUuid(entity['@id']) : null; @@ -533,6 +574,7 @@ export class GroupsComponent implements OnInit, OnDestroy { if (result === true) { this.deleteEntityorClient(uuid, type); } + this.loading = false; }); } @@ -570,16 +612,18 @@ export class GroupsComponent implements OnInit, OnDestroy { onEditClick(event: MouseEvent, type: string, uuid: string): void { event.stopPropagation(); + this.loading = true; const selectedClientsBeforeEdit = this.selection.selected.map(client => client.uuid); const dialogRef = type !== NodeType.Client - ? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px' }) - : this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px' }); + ? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px', disableClose: true, hasBackdrop: true, backdropClass: 'non-clickable-backdrop' }) + : this.dialog.open(ManageClientComponent, { data: { uuid }, width: '900px', disableClose: true, hasBackdrop: true, backdropClass: 'non-clickable-backdrop' }); dialogRef.afterClosed().subscribe((result) => { if (result?.success) { this.refreshData(this.selectedNode?.id, selectedClientsBeforeEdit); } this.menuTriggers.forEach(trigger => trigger.closeMenu()); + this.loading = false; }); } @@ -592,6 +636,9 @@ export class GroupsComponent implements OnInit, OnDestroy { this.dialog.open(ClassroomViewDialogComponent, { width: '90vw', data: { clients: response['hydra:member'] }, + disableClose: true, + hasBackdrop: true, + backdropClass: 'non-clickable-backdrop', }); }, (error) => { @@ -603,35 +650,46 @@ export class GroupsComponent implements OnInit, OnDestroy { executeCommand(command: Command, selectedNode: TreeNode | null): void { - + this.loading = true; if (!selectedNode) { this.toastr.error('No hay un nodo seleccionado.'); return; } else { this.toastr.success(`Ejecutando comando: ${command.name} en ${selectedNode.name}`); } + this.loading = false; } onShowClientDetail(event: MouseEvent, client: Client): void { event.stopPropagation(); - this.dialog.open(ClientDetailsComponent, { + this.loading = true; + const dialogRef = this.dialog.open(ClientDetailsComponent, { width: '70vw', height: '90vh', data: { clientData: client }, + disableClose: true, + hasBackdrop: true, + backdropClass: 'non-clickable-backdrop', }) + + dialogRef.afterClosed().subscribe((result) => { + this.loading = false; + }); } onShowDetailsClick(event: MouseEvent, data: TreeNode | null): void { event.stopPropagation(); + this.loading = true; if (data && data.type !== NodeType.Client) { - this.dialog.open(ShowOrganizationalUnitComponent, { data: { data }, width: '800px' }); + this.dialog.open(ShowOrganizationalUnitComponent, { data: { data }, width: '800px', disableClose: true, hasBackdrop: true, backdropClass: 'non-clickable-backdrop' }); } else { if (data) { this.router.navigate(['clients', this.extractUuid(data['@id'])], { state: { clientData: data } }); } } + this.loading = false; } @@ -873,6 +931,9 @@ export class GroupsComponent implements OnInit, OnDestroy { const dialogRef = this.dialog.open(ChangeParentComponent, { data: { clients: this.selection.selected }, width: '700px', + disableClose: true, + hasBackdrop: true, + backdropClass: 'non-clickable-backdrop', }); dialogRef.afterClosed().subscribe((result) => { @@ -883,11 +944,269 @@ export class GroupsComponent implements OnInit, OnDestroy { } openClientTaskLogs(event: MouseEvent, client: Client): void { + this.loading = true; event.stopPropagation(); - this.dialog.open(ClientTaskLogsComponent, { + const dialogRef = this.dialog.open(ClientTaskLogsComponent, { width: '1200px', - data: { client } + data: { client }, + disableClose: true, + hasBackdrop: true, + backdropClass: 'non-clickable-backdrop', }) + + dialogRef.afterClosed().subscribe((result) => { + this.loading = false; + }); + } + + openClientPendingTasks(event: MouseEvent, client: Client): void { + this.loading = true; + event.stopPropagation(); + + const dialogRef = this.dialog.open(ClientPendingTasksComponent, { + width: '1200px', + data: { client }, + disableClose: true, + hasBackdrop: true, + backdropClass: 'non-clickable-backdrop', + }) + + dialogRef.afterClosed().subscribe((result) => { + this.loading = false; + }); + } + + openOUPendingTasks(event: MouseEvent, node: any): void { + event.stopPropagation(); + this.loading = true; + + this.http.get(`${this.baseUrl}/clients?organizationalUnit.id=${node.id}&page=1&itemsPerPage=10000`).subscribe({ + next: (response) => { + const allClients = response['hydra:member'] || []; + + if (allClients.length === 0) { + this.toastr.warning('Esta unidad organizativa no tiene clientes'); + return; + } + + const ouClientData = { + name: node.name, + id: node.id, + uuid: node.uuid, + type: 'organizational-unit', + clients: allClients + }; + + const dialogRef = this.dialog.open(ClientPendingTasksComponent, { + width: '1200px', + data: { client: ouClientData, isOrganizationalUnit: true }, + disableClose: true, + hasBackdrop: true, + backdropClass: 'non-clickable-backdrop', + }); + + dialogRef.afterClosed().subscribe((result) => { + this.loading = false; + }); + }, + error: (error) => { + console.error('Error al obtener los clientes de la unidad organizativa:', error); + this.toastr.error('Error al cargar los clientes de la unidad organizativa'); + this.loading = false; + } + }); + } + + // Métodos para paginación + getPaginationFrom(): number { + return (this.page * this.itemsPerPage) + 1; + } + + getPaginationTo(): number { + return Math.min((this.page + 1) * this.itemsPerPage, this.length); + } + + getPaginationTotal(): number { + return this.length; + } + + refreshClientData(): void { + this.fetchClientsForNode(this.selectedNode); + this.toastr.success('Datos actualizados', 'Éxito'); + } + + exportToCSV(): void { + const headers = ['Nombre', 'IP', 'MAC', 'Estado', 'Firmware', 'Subnet', 'Parent']; + const csvData = this.arrayClients.map(client => [ + client.name, + client.ip || '', + client.mac || '', + client.status || '', + client.firmwareType || '', + client.subnet || '', + client.parentName || '' + ]); + + const csvContent = [headers, ...csvData] + .map(row => row.map(cell => `"${cell}"`).join(',')) + .join('\n'); + + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + const url = URL.createObjectURL(blob); + link.setAttribute('href', url); + link.setAttribute('download', `clients_${new Date().toISOString().split('T')[0]}.csv`); + link.style.visibility = 'hidden'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + this.toastr.success('Archivo CSV exportado correctamente', 'Éxito'); + } + + private calculateLocalStats(): void { + const clients = this.arrayClients; + this.totalStats = { + total: clients.length, + off: clients.filter(client => client.status === 'off').length, + online: clients.filter(client => ['og-live', 'linux', 'windows', 'linux-session', 'windows-session'].includes(client.status)).length, + busy: clients.filter(client => client.status === 'busy').length + }; + + // Actualizar tipos de firmware disponibles + this.firmwareTypes = [...new Set(clients.map(client => client.firmwareType).filter(Boolean))]; + } + + // Métodos para funcionalidades mejoradas + + selectClient(client: any): void { + this.selectedClient = client; + } + + sortColumn(columnDef: string): void { + if (this.currentSortColumn === columnDef) { + this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; + } else { + this.currentSortColumn = columnDef; + this.sortDirection = 'asc'; + } + this.sortBy = columnDef; + this.onSortChange(); + } + + getSortIcon(columnDef: string): string { + if (this.currentSortColumn !== columnDef) { + return 'unfold_more'; + } + return this.sortDirection === 'asc' ? 'expand_less' : 'expand_more'; + } + + onSortChange(): void { + // Hacer nueva llamada al backend con el ordenamiento actualizado + this.fetchClientsForNode(this.selectedNode); + } + + getStatusCount(status: string): number { + switch(status) { + case 'off': + return this.totalStats.off; + case 'online': + return this.totalStats.online; + case 'busy': + return this.totalStats.busy; + default: + return this.arrayClients.filter(client => client.status === status).length; + } + } + + // Métodos para el árbol mejorado + + expandAll(): void { + this.treeControl.expandAll(); + } + + collapseAll(): void { + this.treeControl.collapseAll(); + } + + getNodeTypeTooltip(nodeType: string): string { + switch (nodeType) { + case 'organizational-unit': + return 'Unidad Organizacional - Estructura principal de la organización'; + case 'classrooms-group': + return 'Grupo de Aulas - Conjunto de aulas relacionadas'; + case 'classroom': + return 'Aula - Espacio físico con equipos informáticos'; + case 'clients-group': + return 'Grupo de Equipos - Conjunto de equipos informáticos'; + case 'client': + return 'Equipo Informático - Computadora o dispositivo individual'; + case 'group': + return 'Grupo - Agrupación lógica de elementos'; + default: + return 'Elemento del árbol organizacional'; + } + } + + getNodeCountLabel(count: number): string { + if (count === 1) return 'elemento'; + return 'elementos'; + } + + getStatusLabel(status: string): string { + const statusLabels: { [key: string]: string } = { + 'off': 'Apagado', + 'og-live': 'OG Live', + 'linux': 'Linux', + 'linux-session': 'Linux Session', + 'windows': 'Windows', + 'windows-session': 'Windows Session', + 'busy': 'Ocupado', + 'mac': 'Mac', + 'disconnected': 'Desconectado', + 'initializing': 'Inicializando' + }; + return statusLabels[status] || status; + } + + // Funciones para el dashboard de estadísticas + getTotalOrganizationalUnits(): number { + let total = 0; + const countOrganizationalUnits = (nodes: TreeNode[]) => { + nodes.forEach(node => { + if (node.type === 'organizational-unit') { + total += 1; + } + if (node.children) { + countOrganizationalUnits(node.children); + } + }); + }; + countOrganizationalUnits(this.originalTreeData); + return total; + } + + getTotalClassrooms(): number { + let total = 0; + const countClassrooms = (nodes: TreeNode[]) => { + nodes.forEach(node => { + if (node.type === 'classroom') { + total += 1; + } + if (node.children) { + countClassrooms(node.children); + } + }); + }; + countClassrooms(this.originalTreeData); + return total; + } + + // Función para actualizar estadísticas cuando cambian los datos + private updateDashboardStats(): void { + // Las estadísticas de equipos ya se calculan en calculateLocalStats() + // Solo necesitamos asegurar que se actualicen cuando cambian los datos + this.calculateLocalStats(); } } diff --git a/ogWebconsole/src/app/components/groups/shared/client-view/client-view.component.ts b/ogWebconsole/src/app/components/groups/shared/client-view/client-view.component.ts index 1b7944a..a17d9b6 100644 --- a/ogWebconsole/src/app/components/groups/shared/client-view/client-view.component.ts +++ b/ogWebconsole/src/app/components/groups/shared/client-view/client-view.component.ts @@ -35,7 +35,7 @@ export class ClientViewComponent { { property: 'Fecha de creación', value: this.data.client.createdAt }, { property: 'NTP', value: this.data.client.organizationalUnit?.networkSettings?.ntp || '' }, { property: 'Modo p2p', value: this.data.client.organizationalUnit?.networkSettings?.p2pMode || '' }, - { property: 'Tiempo p2p', value: this.data.client.organizationalUnit?.networkSettings?.p2pTime || '' }, + ...(this.data.client.organizationalUnit?.networkSettings?.p2pMode === 'seeder' ? [{ property: 'Tiempo p2p (minutos)', value: this.data.client.organizationalUnit?.networkSettings?.p2pTime || '' }] : []), { property: 'IP multicast', value: this.data.client.organizationalUnit?.networkSettings?.mcastIp || '' }, { property: 'Modo multicast', value: this.data.client.organizationalUnit?.networkSettings?.mcastMode || '' }, { property: 'Puerto multicast', value: this.data.client.organizationalUnit?.networkSettings?.mcastPort || '' }, @@ -51,7 +51,7 @@ export class ClientViewComponent { { property: 'Router', value: this.data.client.organizationalUnit?.networkSettings?.router || '' }, { property: 'NTP', value: this.data.client.organizationalUnit?.networkSettings?.ntp || '' }, { property: 'Modo p2p', value: this.data.client.organizationalUnit?.networkSettings?.p2pMode || '' }, - { property: 'Tiempo p2p', value: this.data.client.organizationalUnit?.networkSettings?.p2pTime || '' }, + ...(this.data.client.organizationalUnit?.networkSettings?.p2pMode === 'seeder' ? [{ property: 'Tiempo p2p (minutos)', value: this.data.client.organizationalUnit?.networkSettings?.p2pTime || '' }] : []), { property: 'IP multicast', value: this.data.client.organizationalUnit?.networkSettings?.mcastIp || '' }, { property: 'Modo multicast', value: this.data.client.organizationalUnit?.networkSettings?.mcastMode || '' }, { property: 'Puerto multicast', value: this.data.client.organizationalUnit?.networkSettings?.mcastPort || '' }, diff --git a/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.html b/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.html index 697533b..ec3190c 100644 --- a/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.html +++ b/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.html @@ -134,8 +134,8 @@ - - {{ 'p2pTimeLabel' | translate }} + + {{ 'p2pTimeLabel' | translate }} (minutos) diff --git a/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.ts b/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.ts index 792db32..7d573b9 100644 --- a/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.ts +++ b/ogWebconsole/src/app/components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component.ts @@ -271,6 +271,13 @@ export class ManageOrganizationalUnitComponent implements OnInit { onSubmit() { if (this.generalFormGroup.valid && this.additionalInfoFormGroup.valid && this.networkSettingsFormGroup.valid) { const parentValue = this.generalFormGroup.value.parent; + + // Preparar networkSettings con lógica condicional para p2pTime + const networkSettings = { ...this.networkSettingsFormGroup.value }; + if (networkSettings.p2pMode !== 'seeder') { + networkSettings.p2pTime = null; + } + const formData = { name: this.generalFormGroup.value.name, excludeParentChanges: this.generalFormGroup.value.excludeParentChanges, @@ -279,7 +286,7 @@ export class ManageOrganizationalUnitComponent implements OnInit { comments: this.additionalInfoFormGroup.value.comments, remoteCalendar: this.generalFormGroup.value.remoteCalendar, type: this.generalFormGroup.value.type, - networkSettings: this.networkSettingsFormGroup.value, + networkSettings: networkSettings, location: this.classroomInfoFormGroup.value.location, projector: this.classroomInfoFormGroup.value.projector, board: this.classroomInfoFormGroup.value.board,