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 @@
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 @@
+
+
+
@@ -205,6 +267,10 @@
storage
{{ 'partitions' | translate }}
+
0)" [runScriptContext]="selectedNode?.name || ''"
@@ -254,6 +320,45 @@
text="{{ 'clientsViewStepText' | 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 }}
+
-
+
+
+
-
+
- {{ '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 }} |
-
- 1 || (selection.selected.length === 1 && !selection.isSelected(client))"
- [runScriptContext]="getRunScriptContext([client])">
-
+
+
|
- |
- |
+ |
+
| |
{
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,