Refactor groups

pull/10/head
Alvaro Puente Mella 2024-11-28 17:32:19 +01:00
parent 4d8c1f0991
commit d8dad3b14b
10 changed files with 888 additions and 836 deletions

View File

@ -24,6 +24,7 @@
"jwt-decode": "^4.0.0",
"ngx-joyride": "^2.5.0",
"ngx-toastr": "^19.0.0",
"papaparse": "^5.4.1",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "^0.14.6"
@ -35,6 +36,7 @@
"@angular/localize": "^18.1.0",
"@ngx-env/builder": "^18.0.1",
"@types/jasmine": "~5.1.0",
"@types/papaparse": "^5.3.15",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
@ -5902,6 +5904,15 @@
"@types/node": "*"
}
},
"node_modules/@types/papaparse": {
"version": "5.3.15",
"resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.15.tgz",
"integrity": "sha512-JHe6vF6x/8Z85nCX4yFdDslN11d+1pr12E526X8WAfhadOeaOTx5AuIkvDKIBopfvlzpzkdMx4YyvSKCM9oqtw==",
"dev": true,
"dependencies": {
"@types/node": "*"
}
},
"node_modules/@types/qs": {
"version": "6.9.15",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz",
@ -11955,6 +11966,11 @@
"node": "^16.14.0 || >=18.0.0"
}
},
"node_modules/papaparse": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz",
"integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw=="
},
"node_modules/parent-module": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",

View File

@ -26,6 +26,7 @@
"jwt-decode": "^4.0.0",
"ngx-joyride": "^2.5.0",
"ngx-toastr": "^19.0.0",
"papaparse": "^5.4.1",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "^0.14.6"
@ -37,6 +38,7 @@
"@angular/localize": "^18.1.0",
"@ngx-env/builder": "^18.0.1",
"@types/jasmine": "~5.1.0",
"@types/papaparse": "^5.3.15",
"jasmine-core": "~5.1.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",

View File

@ -1,321 +1,324 @@
.groupLists-container {
display: flex;
flex-wrap: wrap;
height: auto;
margin-bottom: 30px;
}
.search-container {
display: flex;
flex-grow: 1;
margin: 10px;
}
.search-container mat-form-field {
width: 50%;
}
.card {
flex-grow: 1;
margin: 10px;
border: 2px solid rgba(102, 102, 102, 0.103)
.card-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 20px;
padding: 20px;
}
.header-container {
display: flex;
justify-content: space-between;
align-items: center;
height: 100px;
padding: 10px;
}
.unidad-card {
flex: 1 1 20%;
background-color: #fafafa;
height: 600px;
overflow-y: auto;
box-shadow: none !important;
}
.elements-card {
flex: 1 1 75%;
background-color: #fafafa;
height: 600px;
overflow-y: auto;
box-shadow: none !important;
}
.element-content {
overflow-y: auto;
}
.title {
margin-left: 10px;
}
.details-card, .classroom-view {
flex: 1 1 25%;
}
mat-card-title {
display: flex;
justify-content: space-between;
margin: 10px;
}
.title-with-breadcrumb {
display: flex;
flex-direction: column;
}
mat-card-subtitle {
font-size: 0.875rem;
color: rgba(0, 0, 0, 0.54);
}
mat-card-subtitle a {
cursor: pointer;
text-decoration: underline;
color: #929292;
}
mat-card-subtitle a:hover {
text-decoration: none;
padding: 20px;
background-color: #f5f5f5;
border-bottom: 1px solid #ddd;
}
.groups-button-row {
display: flex;
gap: 10px;
gap: 15px;
}
.item-content {
.button-container {
display: flex;
width: 100%;
justify-content: center;
margin: 20px 0;
}
.item-content mat-icon {
margin-right: 10px;
button[mat-raised-button] {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
font-size: 16px;
}
.clickable-item:hover {
mat-card {
background-color: #ffffff;
border-radius: 10px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
transition: transform 0.2s, box-shadow 0.2s;
overflow: hidden;
align-items: center;
}
mat-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 12px rgba(0, 0, 0, 0.15);
}
.unidad-card {
cursor: pointer;
}
.selected-item {
background-color: #e0e0e0;
}
.actions {
padding: 16px;
font-size: 14px;
display: flex;
margin-left: auto;
align-self: center;
flex-direction: column;
justify-content: space-between;
}
.unidad-card.selected-item {
border: 2px solid #1976d2;
}
mat-card-title {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
display: flex;
align-items: center;
font-size: 16px;
font-weight: bold;
color: #333;
margin: 0;
}
mat-card-title mat-icon {
font-size: 20px;
margin-right: 8px;
color: #1976d2;
}
mat-card-actions {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 16px;
}
.actions mat-icon {
cursor: pointer;
margin-left: 16px;
color: #757575;
cursor: pointer;
transition: color 0.2s;
}
.actions mat-icon:hover {
color: #212121;
color: #1976d2;
}
.empty-list {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
height: 200px;
font-size: 16px;
color: #777;
}
mat-spinner {
margin: 0 auto;
align-self: center;
}
.container {
.search-container {
display: flex;
justify-content: flex-end;
}
.classroomBtn-container {
display: flex;
justify-content: flex-end;
width: 100%;
gap: 20px;
margin: 20px 0;
}
.container {
display: flex;
flex-direction: column;
}
.header {
display: flex;
align-items: center;
gap: 10px;
padding: 20px;
}
.header mat-form-field {
width: 300px;
}
.main-content {
display: flex;
.search-container mat-form-field {
flex: 1;
}
.filters {
padding: 20px;
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 8px;
}
.details-container {
display: flex;
flex-direction: column;
width: 300px;
}
.saved-filter {
display: flex;
flex-direction: column;
width: 300px;
margin-bottom: 10px;
padding: 10px;
}
.results {
width: 100%;
}
.results-container {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 16px;
margin-bottom: 16px;
}
.result-card {
width: 100%;
max-width: 250px;
height: 250px;
}
.paginator-container {
display: flex;
justify-content: center;
margin-bottom: 30px;
}
.divider {
margin: 20px 0;
}
mat-card {
margin-bottom: 20px;
}
.mat-tooltip {
white-space: pre-line;
}
.classroom-grid {
display: flex;
flex-wrap: wrap;
gap: 16px;
justify-content: flex-start; /* Opcional: para alinear a la izquierda */
}
.classroom-item {
flex: 0 1 calc(16.66% - 16px); /* 6 columnas */
max-width: calc(16.66% - 16px);
gap: 20px;
padding: 20px;
align-items: center;
text-align: center;
box-sizing: border-box;
}
.classroom-pc {
position: relative;
.details-placeholder {
background-color: #f5f5f5;
border: 1px solid #ddd;
border-radius: 8px;
padding: 20px;
width: 100%;
max-width: 600px;
}
button[mat-raised-button] {
align-self: flex-start;
}
@media (max-width: 1024px) {
.card-container {
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
}
.header-container {
flex-direction: column;
gap: 10px;
}
.groups-button-row {
flex-wrap: wrap;
gap: 10px;
}
}
@media (max-width: 768px) {
mat-card {
padding: 12px;
}
.unidad-card {
font-size: 12px;
}
}
.details-container {
display: flex;
flex-direction: column;
align-items: center;
padding: 8px;
background-color: #f4f4f4;
gap: 20px;
padding: 30px;
background-color: #f9f9f9;
border-radius: 12px;
box-shadow: 0 4px 10px rgba(0, 0, 0, 0.1);
max-width: 1200px;
margin: 20px auto;
}
.details-placeholder {
background-color: #ffffff;
border: 1px solid #e0e0e0;
border-radius: 12px;
padding: 20px;
width: 100%;
max-width: 800px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.05);
}
mat-tree {
width: 100%;
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 8px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
padding: 10px;
}
.pc-image {
width: 80px;
height: 80px;
}
.pc-details {
margin-top: 8px;
font-size: 12px;
}
.client-name {
font-weight: bold;
display: block;
}
.client-ip,
.client-mac {
color: #666;
font-size: 10px;
display: block;
}
.pc-actions {
margin-top: 8px;
mat-tree mat-tree-node {
display: flex;
justify-content: center;
gap: 8px;
align-items: center;
padding: 10px;
border-radius: 6px;
transition: background-color 0.2s, color 0.2s;
cursor: pointer;
}
.pc-og-live {
border: 2px solid #4caf50;
mat-tree mat-tree-node:hover {
background-color: #e3f2fd;
}
.pc-busy {
border: 2px solid #ff9800;
mat-tree mat-tree-node button.mat-icon-button {
margin-left: auto;
color: #757575;
}
.pc-off {
border: 2px solid #f44336;
mat-tree mat-tree-node button.mat-icon-button:hover {
color: #1976d2;
}
.pc-linux {
border: 2px solid #9c27b0;
mat-tree mat-tree-node span {
font-size: 16px;
font-weight: 500;
color: #555;
}
.pc-windows {
border: 2px solid #2196f3;
mat-tree mat-tree-node mat-icon {
margin-right: 10px;
color: #757575;
transition: color 0.2s;
}
/* Pantallas medianas: 4 columnas */
@media (max-width: 1024px) {
.classroom-item {
flex: 0 1 calc(25% - 16px); /* 4 columnas */
}
mat-tree mat-tree-node.expandable mat-icon {
color: black;
cursor: pointer;
}
/* Pantallas pequeñas: 2 columnas */
@media (max-width: 768px) {
.classroom-item {
flex: 0 1 calc(50% - 16px); /* 2 columnas */
}
mat-tree mat-tree-node.expandable.disabled mat-icon {
color: grey;
opacity: 0.5;
cursor: not-allowed;
}
/* Pantallas muy pequeñas: 1 columna */
@media (max-width: 480px) {
.classroom-item {
flex: 0 1 100%; /* 1 columna */
}
mat-tree mat-tree-node:hover mat-icon {
color: black;
}
.client-text {
font-size: 0.8rem;
color: rgba(0, 0, 0, 0.54);
/* Iconos por tipo */
mat-tree mat-tree-node mat-icon.node-icon {
color: #757575;
margin-right: 10px;
}
.client-name {
font-size: 0.9rem;
text-align: center;
mat-tree mat-tree-node mat-icon.node-icon.organizational-unit {
color: #1976d2; /* Azul para unidades organizativas */
}
mat-tree mat-tree-node mat-icon.node-icon.classroom {
color: #388e3c; /* Verde para aulas */
}
mat-tree mat-tree-node mat-icon.node-icon.client {
color: #f57c00; /* Naranja para clientes */
}
mat-tree mat-tree-node mat-icon.node-icon.group {
color: #d32f2f; /* Rojo para grupos */
}
mat-tree mat-tree-node button.mat-icon-button {
margin-right: 10px;
}
mat-tree mat-tree-node button.mat-icon-button.disabled-toggle {
color: grey;
opacity: 0.5;
cursor: not-allowed;
}
mat-tree mat-tree-node button.mat-icon-button.disabled-toggle:hover {
background-color: transparent; /* Desactiva hover */
}
mat-tree mat-tree-node mat-icon {
margin-right: 10px;
color: #757575;
transition: color 0.2s;
}
mat-tree mat-tree-node mat-icon.node-icon.organizational-unit {
color: #1976d2; /* Azul para unidades organizativas */
}
mat-tree mat-tree-node mat-icon.node-icon.classroom {
color: #388e3c; /* Verde para aulas */
}
mat-tree mat-tree-node mat-icon.node-icon.client {
color: #f57c00; /* Naranja para clientes */
}
mat-tree mat-tree-node mat-icon.node-icon.group {
color: #d32f2f; /* Rojo para grupos */
}
mat-tree mat-tree-node:hover {
background-color: #e3f2fd;
cursor: pointer;
}
mat-tree mat-tree-node.disabled {
cursor: not-allowed;
}
mat-tree mat-tree-node.disabled:hover {
background-color: transparent; /* Desactiva hover */
}

View File

@ -4,142 +4,199 @@
<button mat-icon-button color="primary" (click)="iniciarTour()">
<mat-icon>help</mat-icon>
</button>
<h2 class="title" joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">{{ 'adminGroupsTitle' | translate }}</h2>
<h2 class="title" joyrideStep="groupsTitleStepText" text="{{ 'groupsTitleStepText' | translate }}">
{{ 'adminGroupsTitle' | translate }}
</h2>
<div class="groups-button-row" joyrideStep="addStep" text="{{ 'groupsAddStepText' | translate }}">
<button mat-flat-button color="primary" (click)="addOU($event)" matTooltip="{{ 'newOrganizationalUnitTooltip' | translate }}" matTooltipShowDelay="1000">{{ 'newOrganizationalUnitButton' | translate }}</button>
<button mat-flat-button color="primary" (click)="addClient($event)" matTooltipShowDelay="1000">{{ 'newClientButton' | translate }}</button>
<button mat-raised-button (click)="openBottomSheet()" joyrideStep="keyStep" text="{{ 'keyStepText' | translate }}" matTooltipShowDelay="1000">{{ 'legendButton' | translate }}</button>
<button mat-flat-button color="primary" (click)="addOU($event)"
matTooltip="{{ 'newOrganizationalUnitTooltip' | translate }}" matTooltipShowDelay="1000">
{{ 'newOrganizationalUnitButton' | translate }}
</button>
<button mat-flat-button color="primary" (click)="addClient($event)" matTooltipShowDelay="1000">
{{ 'newClientButton' | translate }}
</button>
<button mat-raised-button (click)="openBottomSheet()" joyrideStep="keyStep"
text="{{ 'keyStepText' | translate }}" matTooltipShowDelay="1000">
{{ 'legendButton' | translate }}
</button>
</div>
</div>
<div class="groupLists-container">
<mat-card class="card unidad-card" joyrideStep="unitStep" text="{{ 'unitStepText' | translate }}" matTooltipShowDelay="1000" matTooltipPosition="above">
<mat-card-title>{{ 'organizationalUnitTitle' | translate }}</mat-card-title>
<mat-card-content>
<mat-spinner *ngIf="loading"></mat-spinner>
<mat-list *ngIf="!loading">
<mat-list-item *ngFor="let unidad of organizationalUnits" [ngClass]="{'selected-item': unidad === selectedUnidad, 'clickable-item': true}" (click)="onSelectUnidad(unidad)">
<div class="item-content">
<mat-icon>apartment</mat-icon>
{{ unidad.name }}
<span class="actions">
<mat-icon mat-button [matMenuTriggerFor]="menu" (click)="$event.stopPropagation()">more_vert</mat-icon>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="onTreeClick($event, unidad)">
<mat-icon matTooltip="{{ 'viewTreeTooltip' | translate }}" matTooltipHideDelay="0">account_tree</mat-icon>
<span>{{ 'viewTreeMenu' | translate }}</span>
</button>
<button mat-menu-item (click)="onEditClick($event, unidad.type, unidad.uuid)">
<mat-icon matTooltip="{{ 'editUnitTooltip' | translate }}" matTooltipHideDelay="0">edit</mat-icon>
<span>{{ 'editUnitMenu' | translate }}</span>
</button>
<button mat-menu-item (click)="onShowClick($event, unidad)">
<mat-icon matTooltip="{{ 'viewUnitTooltip' | translate }}" matTooltipHideDelay="0">visibility</mat-icon>
<span>{{ 'viewUnitMenu' | translate }}</span>
</button>
<button mat-menu-item (click)="addOU($event, unidad)">
<mat-icon matTooltip="{{ 'addInternalUnitTooltip' | translate }}" matTooltipHideDelay="0">add_home_work</mat-icon>
<span>{{ 'addInternalUnitMenu' | translate }}</span>
</button>
<button mat-menu-item (click)="addClient($event, unidad)">
<mat-icon matTooltip="{{ 'addClientTooltip' | translate }}" matTooltipHideDelay="0">devices</mat-icon>
<span>{{ 'addClientMenu' | translate }}</span>
</button>
</mat-menu>
</span>
</div>
</mat-list-item>
</mat-list>
</mat-card-content>
</mat-card>
<mat-card class="card elements-card">
<mat-card-title>
<div class="title-with-breadcrumb">
<span>{{ 'internalElementsTitle' | translate }}</span>
<mat-card-subtitle>
<ng-container *ngFor="let crumb of breadcrumb; let i = index">
<a (click)="navigateToBreadcrumb(i)">{{ crumb }}</a>
<span *ngIf="i < breadcrumb.length - 1"> > </span>
</ng-container>
</mat-card-subtitle>
<!-- Mostrar tarjetas si no hay unidad seleccionada -->
<div *ngIf="!selectedUnidad; else detailsTemplate" class="card-container">
<mat-card *ngFor="let unidad of organizationalUnits"
[ngClass]="{'selected-item': unidad === selectedUnidad, 'clickable-item': true}"
(click)="onSelectUnidad(unidad)" class="unidad-card small-card">
<mat-card-header>
<mat-card-title>
<mat-icon>apartment</mat-icon> {{ unidad.name }}
</mat-card-title>
</mat-card-header>
<mat-card-actions>
<div class="button-container">
<button mat-raised-button color="primary" [matMenuTriggerFor]="menu" (click)="$event.stopPropagation()">
<mat-icon>menu</mat-icon>
{{ 'Menu' | translate }}
</button>
</div>
</mat-card-title>
<mat-card-content class="element-content">
<mat-spinner *ngIf="loadingChildren"></mat-spinner>
<!-- Mostrar lista normal si no es un aula -->
<mat-list *ngIf="!loadingChildren && selectedDetail?.type !== 'classroom'">
<div *ngIf="children.length === 0" class="empty-list">
<mat-icon>info</mat-icon>
<span>{{ 'noInternalElementsMessage' | translate }}</span>
</div>
<mat-list-item *ngFor="let child of children"
[ngClass]="{'selected-item': child === selectedUnidad, 'clickable-item': true}"
(click)="onSelectChild(child)">
<div class="item-content">
<mat-icon [ngSwitch]="child.type">
<ng-container *ngSwitchCase="'organizational-unit'">apartment</ng-container>
<ng-container *ngSwitchCase="'classrooms-group'">meeting_room</ng-container>
<ng-container *ngSwitchCase="'classroom'">school</ng-container>
<ng-container *ngSwitchCase="'client'">computer</ng-container>
<ng-container *ngSwitchCase="'clients-group'">lan</ng-container>
<ng-container *ngSwitchDefault>help_outline</ng-container>
</mat-icon>
{{ child.name }}
<div class="actions">
<mat-icon mat-button [matMenuTriggerFor]="menu" (click)="$event.stopPropagation()">more_vert</mat-icon>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="onEditClick($event, child.type, child.uuid)">
<mat-icon>edit</mat-icon>
<span>{{ 'editElementMenu' | translate }}</span>
</button>
<button mat-menu-item (click)="onDeleteClick($event, child.uuid, child.name, child.type)">
<mat-icon>delete</mat-icon>
<span>{{ 'deleteElementMenu' | translate }}</span>
</button>
</mat-menu>
</div>
</div>
</mat-list-item>
</mat-list>
<!-- Mostrar cuadrícula si es un aula -->
<div *ngIf="selectedDetail?.type === 'classroom'" class="classroom-grid">
<div *ngFor="let pc of selectedDetail.clients" class="classroom-item">
<div class="classroom-pc" [ngClass]="{
'pc-og-live': pc.status === 'og-live',
'pc-busy': pc.status === 'busy',
'pc-windows': pc.status === 'windows' || pc.status === 'windows-session',
'pc-linux': pc.status === 'linux' || pc.status === 'linux-session',
'pc-macos': pc.status === 'macos',
'pc-off': pc.status === 'off'
}">
<img mat-card-image src="assets/images/client.png" alt="PC Icon" class="pc-image">
<div class="pc-details">
<span class="client-name">{{ pc.name }}</span>
<span class="client-ip">{{ pc.ip }}</span>
<span class="client-mac">{{ pc.mac }}</span>
</div>
<div class="pc-actions">
<button mat-icon-button color="primary" (click)="onEditClick($event, 'client', pc.uuid)">
<mat-icon>edit</mat-icon>
</button>
<button mat-icon-button color="warn" (click)="onDeleteClick($event, pc.uuid, pc.name, 'client')">
<mat-icon>delete</mat-icon>
</button>
</div>
</div>
</div>
</div>
</mat-card-content>
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="onTreeClick($event, unidad)">
<mat-icon matTooltip="{{ 'viewTreeTooltip' | translate }}" matTooltipHideDelay="0">account_tree</mat-icon>
<span>{{ 'viewTreeMenu' | translate }}</span>
</button>
<button mat-menu-item (click)="onEditClick($event, unidad.type, unidad.uuid)">
<mat-icon matTooltip="{{ 'editUnitTooltip' | translate }}" matTooltipHideDelay="0">edit</mat-icon>
<span>{{ 'editUnitMenu' | translate }}</span>
</button>
<button mat-menu-item (click)="onShowClick($event, unidad)">
<mat-icon matTooltip="{{ 'viewUnitTooltip' | translate }}" matTooltipHideDelay="0">visibility</mat-icon>
<span>{{ 'viewUnitMenu' | translate }}</span>
</button>
<button mat-menu-item (click)="addOU($event, unidad)">
<mat-icon matTooltip="{{ 'addInternalUnitTooltip' | translate }}" matTooltipHideDelay="0">add_home_work</mat-icon>
<span>{{ 'addInternalUnitMenu' | translate }}</span>
</button>
<button mat-menu-item (click)="addClient($event, unidad)">
<mat-icon matTooltip="{{ 'addClientTooltip' | translate }}" matTooltipHideDelay="0">devices</mat-icon>
<span>{{ 'addClientMenu' | translate }}</span>
</button>
</mat-menu>
</mat-card-actions>
</mat-card>
</div>
<!-- Plantilla para detalles -->
<ng-template #detailsTemplate>
<button mat-raised-button color="primary" (click)="clearSelection()">
<mat-icon>arrow_back</mat-icon>
{{ 'Back' | translate }}
</button>
<div>
<div class="details-placeholder">
<h2>{{ 'Details of' | translate }} {{ selectedUnidad?.name }}</h2>
<!-- Árbol jerárquico -->
<mat-tree [dataSource]="treeDataSource" [treeControl]="treeControl" class="tree-container">
<!-- Nodo expandible -->
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodePadding>
<button
mat-icon-button
matTreeNodeToggle
[disabled]="!node.expandable"
[ngClass]="{'disabled-toggle': !node.expandable}">
<mat-icon>
{{
treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'
}}
</mat-icon>
</button>
<mat-icon class="node-icon {{ node.type }}">
{{
node.type === 'organizational-unit'
? 'apartment'
: node.type === 'classroom'
? 'school'
: node.type === 'client'
? 'computer'
: 'group'
}}
</mat-icon>
<span>{{ node.name }} ({{ node.type }})</span>
<button mat-icon-button [matMenuTriggerFor]="menu" (click)="setSelectedNode(node)">
<mat-icon>more_vert</mat-icon>
</button>
</mat-tree-node>
<mat-tree-node *matTreeNodeDef="let node; when: isLeafNode" matTreeNodePadding>
<button
mat-icon-button
matTreeNodeToggle
[disabled]="true"
class="disabled-toggle">
<mat-icon>
{{
node.type === 'client' ? '' : 'chevron_right'
}}
</mat-icon>
</button>
<mat-icon class="node-icon {{ node.type }}">
{{
node.type === 'organizational-unit'
? 'apartment'
: node.type === 'classroom'
? 'school'
: node.type === 'client'
? 'computer'
: 'group'
}}
</mat-icon>
<span>{{ node.name }} ({{ node.type }})</span>
<ng-container *ngIf="node.type === 'client'">
<span>- IP: {{ node.ip }}</span>
</ng-container>
<button mat-icon-button [matMenuTriggerFor]="menu" (click)="setSelectedNode(node)">
<mat-icon>more_vert</mat-icon>
</button>
</mat-tree-node>
<mat-tree-node *matTreeNodeDef="let node; when: isLeafNode" matTreeNodePadding>
<mat-icon class="node-icon {{ node.type }}">
{{
node.type === 'organizational-unit'
? 'apartment'
: node.type === 'classroom'
? 'school'
: node.type === 'client'
? 'computer'
: 'group'
}}
</mat-icon>
<span>{{ node.name }} ({{ node.type }})</span>
<ng-container *ngIf="node.type === 'client'">
<span>- IP: {{ node.ip }}</span>
</ng-container>
<button mat-icon-button [matMenuTriggerFor]="menu" (click)="setSelectedNode(node)">
<mat-icon>more_vert</mat-icon>
</button>
</mat-tree-node>
<!-- Nodo hoja -->
<mat-tree-node *matTreeNodeDef="let node" matTreeNodePadding>
<mat-icon *ngIf="node.type === 'client'">computer</mat-icon>
<mat-icon *ngIf="node.type !== 'client'">device_hub</mat-icon>
<span>{{ node.name }} ({{ node.type }})</span>
<ng-container *ngIf="node.type === 'client'">
<span>- IP: {{ node.ip }}</span>
</ng-container>
<button mat-icon-button [matMenuTriggerFor]="menu" (click)="setSelectedNode(node)">
<mat-icon>more_vert</mat-icon>
</button>
</mat-tree-node>
</mat-tree>
<!-- Menú desplegable -->
<mat-menu #menu="matMenu">
<button mat-menu-item (click)="onEditNode($event, selectedNode)">
<mat-icon>edit</mat-icon>
<span>Edit</span>
</button>
<button mat-menu-item (click)="onDelete(selectedNode)">
<mat-icon>delete</mat-icon>
<span>Delete</span>
</button>
<button mat-menu-item (click)="onCustomAction(selectedNode)">
<mat-icon>settings</mat-icon>
<span>Custom Action</span>
</button>
</mat-menu>
</div>
</div>
</ng-template>
</mat-tab>
<mat-tab label="{{ 'advancedSearchTabLabel' | translate }}">
@ -153,4 +210,4 @@
<mat-tab label="{{ 'organizationalUnitsTabLabel' | translate }}">
<app-organizational-unit-tab-view #organizationalUnitTab></app-organizational-unit-tab-view>
</mat-tab>
</mat-tab-group>
</mat-tab-group>

View File

@ -1,33 +1,38 @@
import {Component, OnInit, ViewChild} from '@angular/core';
import { Component, OnInit, ViewChild } from '@angular/core';
import { DataService } from './services/data.service';
import { ClientCollection, UnidadOrganizativa } from './model/model';
import { UnidadOrganizativa } from './model/model';
import { MatDialog } from '@angular/material/dialog';
import { MatBottomSheet } from '@angular/material/bottom-sheet';
import { MatTabChangeEvent } from '@angular/material/tabs';
import { JoyrideService } from 'ngx-joyride';
import { FlatTreeControl } from '@angular/cdk/tree';
import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree';
import { CreateOrganizationalUnitComponent } from './shared/organizational-units/create-organizational-unit/create-organizational-unit.component';
import { DeleteModalComponent } from '../../shared/delete_modal/delete-modal/delete-modal.component';
import { CreateClientComponent } from './shared/clients/create-client/create-client.component';
import { EditOrganizationalUnitComponent } from './shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component';
import { EditClientComponent } from './shared/clients/edit-client/edit-client.component';
import { ShowOrganizationalUnitComponent} from "./shared/organizational-units/show-organizational-unit/show-organizational-unit.component";
import {ToastrService} from "ngx-toastr";
import {TreeViewComponent} from "./shared/tree-view/tree-view.component";
import {MatBottomSheet} from "@angular/material/bottom-sheet";
import {LegendComponent} from "./shared/legend/legend.component";
import { ClassroomViewDialogComponent } from './shared/classroom-view/classroom-view-modal';
import {HttpClient} from "@angular/common/http";
import {PageEvent} from "@angular/material/paginator";
import { SaveFiltersDialogComponent } from './shared/save-filters-dialog/save-filters-dialog.component';
import { AcctionsModalComponent } from './shared/acctions-modal/acctions-modal.component';
import {MatTableDataSource} from "@angular/material/table";
import {DatePipe} from "@angular/common";
import {AdvancedSearchComponent} from "./components/advanced-search/advanced-search.component";
import {MatTabChangeEvent} from "@angular/material/tabs";
import {ClientTabViewComponent} from "./components/client-tab-view/client-tab-view.component";
import {
OrganizationalUnitTabViewComponent
} from "./components/organizational-unit-tab-view/organizational-unit-tab-view.component";
import { ExecuteCommandComponent } from '../commands/main-commands/execute-command/execute-command.component';
import { ExecuteCommandOuComponent } from './shared/execute-command-ou/execute-command-ou.component';
import { JoyrideService } from 'ngx-joyride';
import { ShowOrganizationalUnitComponent } from './shared/organizational-units/show-organizational-unit/show-organizational-unit.component';
import { TreeViewComponent } from './shared/tree-view/tree-view.component';
import { LegendComponent } from './shared/legend/legend.component';
import { ClientTabViewComponent } from './components/client-tab-view/client-tab-view.component';
import { OrganizationalUnitTabViewComponent } from './components/organizational-unit-tab-view/organizational-unit-tab-view.component';
interface TreeNode {
name: string;
type: string;
children?: TreeNode[];
ip?: string;
'@id'?: string;
}
interface FlatNode {
name: string;
type: string;
level: number;
expandable: boolean;
ip?: string;
}
@Component({
selector: 'app-groups',
@ -36,53 +41,60 @@ import { JoyrideService } from 'ngx-joyride';
})
export class GroupsComponent implements OnInit {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
dataSource = new MatTableDataSource<any>();
organizationalUnits: UnidadOrganizativa[] = [];
selectedUnidad: UnidadOrganizativa | null = null;
selectedDetail: any | null = null;
children: any[] = [];
breadcrumb: string[] = [];
clientsData: any[] = [];
breadcrumbData: any[] = [];
loading:boolean = false;
loadingChildren:boolean = false;
loading: boolean = false;
loadingChildren: boolean = false;
searchTerm: string = '';
selectedFilter1: string = 'none';
selectedFilter2: string = 'none';
selectedFilterOS: string[] = [];
selectedFilterStatus: string[] = [];
filterIP: string = '';
filterMAC: string = '';
filterName: string = '';
filteredResults: any[] = [];
savedFilterNames: any[] = [];
length: number = 0;
itemsPerPage: number = 10;
page: number = 1;
pageSizeOptions: number[] = [5, 10, 25, 100];
selectedElements: any[] = [];
isAllSelected: boolean = false;
filters: { [key: string]: string } = {};
datePipe: DatePipe = new DatePipe('es-ES');
treeControl: FlatTreeControl<FlatNode>;
treeFlattener: MatTreeFlattener<TreeNode, FlatNode>;
treeDataSource: MatTreeFlatDataSource<TreeNode, FlatNode>;
selectedNode: TreeNode | null = null;
@ViewChild('clientTab') clientTabComponent!: ClientTabViewComponent;
@ViewChild('organizationalUnitTab') organizationalUnitTabComponent!: OrganizationalUnitTabViewComponent;
constructor(
private dataService: DataService,
public dialog: MatDialog,
private toastService: ToastrService,
private _bottomSheet: MatBottomSheet,
private http: HttpClient,
private joyrideService: JoyrideService
) {}
private dataService: DataService,
public dialog: MatDialog,
private _bottomSheet: MatBottomSheet,
private joyrideService: JoyrideService
) {
this.treeFlattener = new MatTreeFlattener<TreeNode, FlatNode>(
(node: TreeNode, level: number) => ({
name: node.name,
type: node.type,
level,
expandable: !!node.children?.length,
ip: node.ip,
['@id']: node['@id']
}),
node => node.level,
node => node.expandable,
node => node.children
);
this.treeControl = new FlatTreeControl<FlatNode>(
node => node.level,
node => node.expandable
);
this.treeDataSource = new MatTreeFlatDataSource(this.treeControl, this.treeFlattener);
}
ngOnInit(): void {
this.search();
this.getFilters();
}
@ViewChild('clientTab') clientTabComponent!: ClientTabViewComponent;
@ViewChild('organizationalUnitTab') organizationalUnitTabComponent!: OrganizationalUnitTabViewComponent;
clearSelection(): void {
this.selectedUnidad = null;
this.selectedDetail = null;
}
onTabChange(event: MatTabChangeEvent) {
onTabChange(event: MatTabChangeEvent): void {
switch (event.index) {
case 2:
this.clientTabComponent.search();
@ -97,12 +109,8 @@ export class GroupsComponent implements OnInit {
getFilters(): void {
this.dataService.getFilters().subscribe(
data => {
this.savedFilterNames = data.map((filter: any) => [filter.name, filter.uuid]);
},
error => {
console.error('Error fetching filters:', error);
}
data => {},
error => console.error('Error fetching filters:', error)
);
}
@ -123,67 +131,78 @@ export class GroupsComponent implements OnInit {
onSelectUnidad(unidad: UnidadOrganizativa): void {
this.selectedUnidad = unidad;
this.selectedDetail = unidad;
this.breadcrumb = [unidad.name];
this.breadcrumbData = [unidad];
this.loadChildrenAndClients(unidad.id);
this.loadChildrenAndClients(unidad.id).then(fullData => {
const treeData = this.convertToTreeData(fullData);
this.treeDataSource.data = treeData[0]?.children || [];
});
}
onSelectChild(child: any): void {
this.selectedDetail = child;
if (child.type !== 'client' && child.uuid && child.id) {
this.breadcrumb.push(child.name || child.name);
this.breadcrumbData.push(child);
this.loadChildrenAndClients(child.id);
async loadChildrenAndClients(id: string): Promise<any> {
try {
const childrenData = await this.dataService.getChildren(id).toPromise();
const clientsData = await this.dataService.getClients(id).toPromise();
const processHierarchy = (nodes: UnidadOrganizativa[]): TreeNode[] => {
return nodes.map(node => ({
name: node.name,
type: node.type,
'@id': node['@id'],
children: [
...(node.children ? processHierarchy(node.children) : []),
...(node.clients
? node.clients.map((client: any) => ({
name: client.name,
type: 'client',
'@id': client['@id'],
ip: client.ip
}))
: [])
],
ip: node.type === 'client' ? (node as any).ip : undefined
}));
};
return {
...this.selectedUnidad,
children: childrenData ? processHierarchy(childrenData) : [],
clients: clientsData || []
};
} catch (error) {
console.error('Error loading children and clients:', error);
return this.selectedUnidad;
}
}
navigateToBreadcrumb(index: number): void {
this.breadcrumb = this.breadcrumb.slice(0, index + 1);
const target = this.breadcrumbData[index];
this.breadcrumbData = this.breadcrumbData.slice(0, index + 1);
this.selectedDetail = target;
this.loadChildrenAndClients(target.id);
convertToTreeData(data: any): TreeNode[] {
const processNode = (node: UnidadOrganizativa): TreeNode => ({
name: node.name,
type: node.type,
'@id': node['@id'],
children: [
...(node.children?.map(processNode) || []),
...(node.clients
? node.clients.map((client: any) => ({
name: client.name,
type: 'client',
'@id': client['@id'],
ip: client.ip
}))
: [])
],
ip: node.type === 'client' ? (node as any).ip : undefined
});
return [processNode(data)];
}
loadChildrenAndClients(id: string): void {
this.loadingChildren = true
this.dataService.getChildren(id).subscribe(
childrenData => {
this.dataService.getClients(id).subscribe(
clientsData => {
this.clientsData = clientsData;
const newChildren = [...childrenData, ...clientsData];
if (newChildren.length > 0) {
this.children = newChildren;
} else {
this.children = [];
}
this.loadingChildren = false
},
error => {
console.error('Error fetching clients', error);
this.clientsData = [];
this.children = [];
this.loadingChildren = false
}
);
},
error => {
console.error('Error fetching children', error);
this.children = [];
this.loadingChildren = false
}
);
}
addOU(event: MouseEvent, parent:any = null): void {
addOU(event: MouseEvent, parent: any = null): void {
event.stopPropagation();
const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, { data: { parent }, width: '900px'});
const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, { data: { parent }, width: '900px' });
dialogRef.afterClosed().subscribe(() => {
this.dataService.getOrganizationalUnits().subscribe(
data => {
this.organizationalUnits = data
this.organizationalUnits = data;
},
error => console.error('Error fetching unidades organizativas', error)
);
@ -192,262 +211,76 @@ export class GroupsComponent implements OnInit {
addClient(event: MouseEvent, organizationalUnit: any = null): void {
event.stopPropagation();
const dialogRef = this.dialog.open(CreateClientComponent, { data: { organizationalUnit }, width: '900px' });
dialogRef.afterClosed().subscribe(() => {
this.dataService.getOrganizationalUnits().subscribe(
data => {
this.organizationalUnits = data;
if (organizationalUnit && organizationalUnit.id) {
this.loadChildrenAndClients(organizationalUnit.id);
}
},
error => console.error('Error fetching unidades organizativas', error)
);
this.dataService.getOrganizationalUnits().subscribe(
data => {
this.organizationalUnits = data;
if (organizationalUnit && organizationalUnit.id) {
this.loadChildrenAndClients(organizationalUnit.id);
}
},
error => console.error('Error fetching unidades organizativas', error)
);
});
}
onDeleteClick(event: MouseEvent, uuid: string, name: string, type: string): void {
}
setSelectedNode(node: TreeNode): void {
this.selectedNode = node;
}
onEditNode(event: MouseEvent, node: TreeNode | null): void {
if (!node) return;
const uuid = node['@id'] ? node['@id'].split('/').pop() : '';
const type = node.type;
event.stopPropagation();
if (type === 'client') {
const dialogRef = this.dialog.open(DeleteModalComponent, {
width: '400px',
data: { name }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.dataService.deleteElement(uuid, type).subscribe(
() => {
this.loadChildrenAndClients(this.selectedUnidad?.id || '');
this.dataService.getOrganizationalUnits().subscribe(
data => this.organizationalUnits = data,
error => console.error('Error fetching unidades organizativas', error)
);
this.openSnackBar(false, 'Entidad eliminada exitosamente')
},
error => {
console.error('Error deleting element', error)
this.openSnackBar(true, error.error['hydra:description'])
}
);
}
});
if (type !== 'client') {
this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' });
} else {
const dialogDeleteGroupRef = this.dialog.open(DeleteModalComponent, {
width: '400px',
data: { name }
});
dialogDeleteGroupRef.afterClosed().subscribe(result => {
if (result && result === 'delete') {
this.dataService.deleteElement(uuid, type).subscribe(
() => {
this.loadChildrenAndClients(this.selectedUnidad?.id || '');
this.dataService.getOrganizationalUnits().subscribe(
data => this.organizationalUnits = data,
error => console.error('Error fetching unidades organizativas', error)
);
this.openSnackBar(false, 'Entidad eliminada exitosamente')
},
error => {
console.error('Error deleting element', error)
this.openSnackBar(true, error.error['hydra:description'])
}
);
} else if (result && result === 'change') {
this.dataService.changeParent(uuid).subscribe(
() => {
this.loadChildrenAndClients(this.selectedUnidad?.id || '');
this.dataService.getOrganizationalUnits().subscribe(
data => this.organizationalUnits = data,
error => console.error('Error fetching unidades organizativas', error)
);
this.openSnackBar(false, 'Entidad eliminada exitosamente')
},
error => {
console.error('Error deleting element', error)
this.openSnackBar(true, error.error['hydra:description'])
}
);
}
});
this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' });
}
}
onDelete(node: TreeNode | null): void {
if (!node) return;
// Additional logic for deleting
}
onCustomAction(node: TreeNode | null): void {
if (!node) return;
// Logic for custom actions
}
onEditClick(event: MouseEvent, type: any, uuid: string): void {
event.stopPropagation();
if (type != "client") {
const dialogRef = this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px'});
if (type != 'client') {
this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' });
} else {
const dialogRef = this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' } );
this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' });
}
}
onShowClick(event: MouseEvent, data: any): void {
event.stopPropagation();
if (data.type != "client") {
const dialogRef = this.dialog.open(ShowOrganizationalUnitComponent, { data: { data }, width: '700px'});
if (data.type != 'client') {
this.dialog.open(ShowOrganizationalUnitComponent, { data: { data }, width: '700px' });
}
}
onTreeClick(event: MouseEvent, data: any): void {
event.stopPropagation();
if (data.type != "client") {
const dialogRef = this.dialog.open(TreeViewComponent, { data: { data }, width: '800px'});
if (data.type != 'client') {
this.dialog.open(TreeViewComponent, { data: { data }, width: '800px' });
}
}
onExecuteCommand(event: MouseEvent, child: any, name: string, type:string): void {
console.log('Executing command on:', child);
this.dialog.open(ExecuteCommandOuComponent, {
width: '50%',
data: { childUnitUuid: child }
}).afterClosed().subscribe((result) => {
if (result) {
console.log('Comando ejecutado con éxito');
} else {
console.log('Ejecución de comando cancelada');
}
});
}
openSnackBar(isError: boolean, message: string) {
if (isError) {
this.toastService.error(' Error al eliminar la entidad: ' + message, 'Error');
} else
this.toastService.success(message, 'Éxito');
}
openBottomSheet(): void {
this._bottomSheet.open(LegendComponent);
}
roomMap(): void {
if (this.selectedDetail && this.selectedDetail.type === 'classroom') {
const dialogRef = this.dialog.open(ClassroomViewDialogComponent, {
width: '90vw',
data: { clients: this.clientsData }
});
dialogRef.afterClosed().subscribe(result => {
console.log('The dialog was closed');
});
}
}
applyFilter() {
this.dataService.getFilteredResults(this.selectedFilter1, this.selectedFilter2, this.filterName, this.filterIP, this.filterMAC, this.page, this.itemsPerPage)
.subscribe(
response => {
this.filteredResults = response.results;
this.length = response.total;
},
error => {
console.error('Error al obtener los resultados filtrados', error);
this.filteredResults = [];
}
);
}
onPageChange(event: PageEvent) {
this.page = event.pageIndex;
this.itemsPerPage = event.pageSize;
this.applyFilter();
}
saveFilters() {
const dialogRef = this.dialog.open(SaveFiltersDialogComponent);
dialogRef.afterClosed().subscribe(result => {
if (result) {
const filters = {
name: result,
favourite: true,
filters: {
filter0: this.filterName,
filter1: this.selectedFilter1,
filter2: this.selectedFilter2,
filter3: this.selectedFilterOS,
filter4: this.selectedFilterStatus,
filter5: this.filterIP,
filter6: this.filterMAC,
}
};
this.http.post(`${this.baseUrl}/views`, filters).subscribe(response => {
console.log('Response from server:', response);
this.toastService.success('Se ha guardado el filtro correctamente');
}, error => {
console.error('Error:', error);
this.toastService.error(error);
});
}
});
}
loadSelectedFilter(savedFilter: any) {
const url = `${this.baseUrl}/views/` + savedFilter[1];
console.log('llamando a:', url);
this.dataService.getFilter(savedFilter[1]).subscribe(response => {
console.log('Response from server:', response.filters);
if (response) {
console.log('Filter1:', response.filters);
this.filterName = response.filters.filter0 || '';
this.selectedFilter1 = response.filters.filter1 || null;
this.selectedFilter2 = response.filters.filter2 || '';
this.selectedFilterOS = response.filters.filter3 || [];
this.selectedFilterStatus = response.filters.filter4 || [];
this.filterIP = response.filters.filter5 || '';
this.filterMAC = response.filters.filter6 || '';
this.applyFilter();
}
}, error => {
console.error('Error:', error);
});
}
onCheckboxChange(event: any, name: string, uuid: string) {
if (event.checked) {
this.selectedElements.push(uuid);
} else {
const index = this.selectedElements.indexOf(name);
if (index > -1) {
this.selectedElements.splice(index, 1);
}
}
this.isAllSelected = this.selectedElements.length === this.filteredResults.length;
}
toggleSelectAll() {
this.isAllSelected = !this.isAllSelected;
if (this.isAllSelected) {
this.selectedElements = this.filteredResults.map(result => result.uuid);
} else {
this.selectedElements = [];
}
}
isSelected(name: string): boolean {
return this.selectedElements.includes(name);
}
sendActions() {
const dialogRef = this.dialog.open(AcctionsModalComponent, { data: { selectedElements: this.selectedElements }, width: '700px'});
}
iniciarTour(): void {
this.joyrideService.startTour({
steps: ['groupsTitleStepText', 'addStep', 'keyStep', 'unitStep', 'elementsStep', 'tabsStep'],
@ -455,4 +288,8 @@ export class GroupsComponent implements OnInit {
themeColor: '#3f51b5'
});
}
hasChild = (_: number, node: FlatNode): boolean => node.expandable;
isLeafNode = (_: number, node: FlatNode): boolean => !node.expandable;
}

View File

@ -9,6 +9,9 @@ export interface Aula {
}
export interface UnidadOrganizativa {
clients: any[];
children: UnidadOrganizativa[];
'@id'?: string;
id: string;
name: string;
uuid: string;

View File

@ -183,6 +183,16 @@ export class DataService {
})
);
}
getOrganizationalUnitById(id: string): Observable<any> {
const url = `${this.baseUrl}/organizational-units/${id}`;
return this.http.get<any>(url).pipe(
catchError(error => {
console.error('Error fetching organizational unit', error);
return throwError(error);
})
);
}
}

View File

@ -18,7 +18,8 @@ h1 {
}
.mat-dialog-content {
padding: 50px;
padding-left: 50px;
padding-right: 20px;
}
button {
@ -61,3 +62,60 @@ mat-option .unit-path {
.form-field {
width: 100%;
}
.inputs-container {
display: inline-flex;
gap: 20px;
padding-left: 50px;
padding-right: 20px;
}
.upload-container {
display: flex;
justify-content: center;
align-items: center;
margin-top: 10px;
}
button[mat-raised-button] {
font-size: 14px;
text-transform: none;
padding: 10px 20px;
border-radius: 4px;
box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.2);
cursor: pointer;
transition: background-color 0.3s ease, box-shadow 0.3s ease;
}
button[mat-raised-button]:hover {
background-color: #303f9f;
box-shadow: 0px 4px 6px rgba(0, 0, 0, 0.3);
}
/* Contenedor para la tabla con scroll */
.scrollable-table {
max-height: 300px; /* Altura máxima de la tabla */
overflow-y: auto; /* Habilitar scroll vertical */
overflow-x: hidden; /* Ocultar scroll horizontal si no es necesario */
border: 1px solid #ddd;
border-radius: 4px;
margin-top: 10px;
}
/* Tabla */
.mat-elevation-z8 {
width: 100%;
}
.client-table th, .client-table td {
text-align: left;
padding: 10px;
}
.client-table th {
position: sticky;
top: 0;
background-color: #3f51b5;
color: white;
z-index: 2;
}

View File

@ -1,92 +1,131 @@
<div class="create-client-container">
<h1 mat-dialog-title>{{ 'addClientDialogTitle' | translate }}</h1>
<div class="mat-dialog-content" [ngClass]="{'loading': loading}">
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
<form [formGroup]="clientForm" class="client-form grid-form" *ngIf="!loading">
<mat-form-field class="form-field">
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
<mat-select formControlName="organizationalUnit">
<mat-option *ngFor="let unit of parentUnits" [value]="unit['@id']">
<div class="unit-name">{{ unit.name }}</div>
<div class="unit-path">{{ unit.path }}</div>
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
<input matInput formControlName="name">
</mat-form-field>
<div class="inputs-container">
<div class="mat-dialog-content" [ngClass]="{'loading': loading}">
<h3>Añadir un cliente</h3>
<mat-spinner class="loading-spinner" *ngIf="loading"></mat-spinner>
<form [formGroup]="clientForm" class="client-form grid-form" *ngIf="!loading">
<mat-form-field class="form-field">
<mat-label>{{ 'organizationalUnitLabel' | translate }}</mat-label>
<mat-select formControlName="organizationalUnit">
<mat-option *ngFor="let unit of parentUnits" [value]="unit['@id']">
<div class="unit-name">{{ unit.name }}</div>
<div class="unit-path">{{ unit.path }}</div>
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
<mat-select formControlName="ogLive">
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
{{ oglive.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
<input matInput formControlName="name">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'serialNumberLabel' | translate }}</mat-label>
<input matInput formControlName="serialNumber">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
<mat-select formControlName="ogLive">
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
{{ oglive.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'netifaceLabel' | translate }}</mat-label>
<mat-select formControlName="netiface">
<mat-option *ngFor="let type of netifaceTypes" [value]="type.value">
{{ type.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'serialNumberLabel' | translate }}</mat-label>
<input matInput formControlName="serialNumber">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'netDriverLabel' | translate }}</mat-label>
<mat-select formControlName="netDriver">
<mat-option *ngFor="let type of netDriverTypes" [value]="type.value">
{{ type.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'netifaceLabel' | translate }}</mat-label>
<mat-select formControlName="netiface">
<mat-option *ngFor="let type of netifaceTypes" [value]="type.value">
{{ type.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'macLabel' | translate }}</mat-label>
<mat-hint>{{ 'macHint' | translate }}</mat-hint>
<input matInput formControlName="mac">
<mat-error>{{ 'macError' | translate }}</mat-error>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'netDriverLabel' | translate }}</mat-label>
<mat-select formControlName="netDriver">
<mat-option *ngFor="let type of netDriverTypes" [value]="type.value">
{{ type.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'ipLabel' | translate }}</mat-label>
<mat-hint>{{ 'ipHint' | translate }}</mat-hint>
<input matInput formControlName="ip">
<mat-error>{{ 'ipError' | translate }}</mat-error>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'macLabel' | translate }}</mat-label>
<mat-hint>{{ 'macHint' | translate }}</mat-hint>
<input matInput formControlName="mac">
<mat-error>{{ 'macError' | translate }}</mat-error>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'templateLabel' | translate }}</mat-label>
<mat-select formControlName="template">
<mat-option *ngFor="let template of templates" [value]="template['@id']">
{{ template.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'ipLabel' | translate }}</mat-label>
<mat-hint>{{ 'ipHint' | translate }}</mat-hint>
<input matInput formControlName="ip">
<mat-error>{{ 'ipError' | translate }}</mat-error>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'hardwareProfileLabel' | translate }}</mat-label>
<mat-select formControlName="hardwareProfile">
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">
{{ unit.description }}
</mat-option>
</mat-select>
<mat-error>{{ 'hardwareProfileError' | translate }}</mat-error>
</mat-form-field>
</form>
<mat-form-field class="form-field">
<mat-label>{{ 'templateLabel' | translate }}</mat-label>
<mat-select formControlName="template">
<mat-option *ngFor="let template of templates" [value]="template['@id']">
{{ template.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'hardwareProfileLabel' | translate }}</mat-label>
<mat-select formControlName="hardwareProfile">
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">
{{ unit.description }}
</mat-option>
</mat-select>
<mat-error>{{ 'hardwareProfileError' | translate }}</mat-error>
</mat-form-field>
</form>
</div>
<mat-divider vertical></mat-divider>
<div class="create-multiple-client-container">
<h3>Añadir multiples clientes</h3>
<div class="upload-container">
<button mat-raised-button color="primary" (click)="fileInput.click()">Subir fichero</button>
<input #fileInput type="file" (change)="onFileUpload($event)" accept=".csv" hidden>
</div>
<h4 *ngIf="uploadedClients.length > 0">Clientes importados:</h4>
<div class="scrollable-table">
<table mat-table [dataSource]="uploadedClients" class="mat-elevation-z8" *ngIf="uploadedClients.length > 0">
<ng-container matColumnDef="name">
<th mat-header-cell *matHeaderCellDef> Nombre </th>
<td mat-cell *matCellDef="let client"> {{ client.name }} </td>
</ng-container>
<ng-container matColumnDef="ip">
<th mat-header-cell *matHeaderCellDef> IP </th>
<td mat-cell *matCellDef="let client"> {{ client.ip }} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</div>
</div>
</div>
<div mat-dialog-actions align="end">
<button mat-button (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
<button mat-button [disabled]="!clientForm.valid" (click)="onSubmit()">{{ 'addButton' | translate }}</button>
<button mat-button (click)="onSubmit()">{{ 'addButton' | translate }}</button>
</div>
</div>

View File

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