refs #486. Added tree-view. Snackbar response, and some UX improvements

pull/4/head
Manuel Aranda Rosales 2024-07-12 13:08:14 +02:00
parent a1e2b7aec1
commit 8239aa0d53
51 changed files with 1191 additions and 194 deletions

View File

@ -36,7 +36,8 @@
],
"styles": [
"src/custom-theme.scss",
"src/styles.css"
"src/styles.css",
"node_modules/ngx-toastr/toastr.css"
],
"scripts": []
},
@ -104,10 +105,6 @@
}
},
"cli": {
<<<<<<< Updated upstream
"analytics": "95fac95c-8936-41a8-8c9c-1fae82fe6912"
=======
"analytics": "ba7c0825-8034-43ff-9c60-83dac232db7e"
>>>>>>> Stashed changes
}
}

View File

@ -19,6 +19,7 @@
"@angular/platform-browser-dynamic": "^18.0.0",
"@angular/router": "^18.0.0",
"jwt-decode": "^4.0.0",
"ngx-toastr": "^19.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "^0.14.6"
@ -9854,6 +9855,19 @@
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"dev": true
},
"node_modules/ngx-toastr": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-19.0.0.tgz",
"integrity": "sha512-6pTnktwwWD+kx342wuMOWB4+bkyX9221pAgGz3SHOJH0/MI9erLucS8PeeJDFwbUYyh75nQ6AzVtolgHxi52dQ==",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/common": ">=16.0.0-0",
"@angular/core": ">=16.0.0-0",
"@angular/platform-browser": ">=16.0.0-0"
}
},
"node_modules/nice-napi": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/nice-napi/-/nice-napi-1.0.2.tgz",

View File

@ -21,6 +21,7 @@
"@angular/platform-browser-dynamic": "^18.0.0",
"@angular/router": "^18.0.0",
"jwt-decode": "^4.0.0",
"ngx-toastr": "^19.0.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0",
"zone.js": "^0.14.6"

View File

@ -1,3 +1,5 @@
.sidenav-container {
height: 100%;
}
@ -6,3 +8,4 @@
.content {
padding: 16px;
}

View File

@ -1,4 +1,6 @@
import { NgModule } from '@angular/core';
// @ts-ignore
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
@ -43,16 +45,29 @@ import { DeleteModalComponent } from './components/groups/delete-modal/delete-mo
import { EditOrganizationalUnitComponent } from './components/groups/organizational-units/edit-organizational-unit/edit-organizational-unit.component';
import { EditClientComponent } from './components/groups/clients/edit-client/edit-client.component';
import { ClassroomViewComponent } from './components/groups/classroom-view/classroom-view.component';
import {MatProgressSpinner} from "@angular/material/progress-spinner";
import {MatMenu, MatMenuItem, MatMenuTrigger} from "@angular/material/menu";
import {MatAutocomplete} from "@angular/material/autocomplete";
import {MatChip, MatChipListbox, MatChipOption, MatChipSet} from "@angular/material/chips";
import {MatChip, MatChipListbox, MatChipOption, MatChipSet, MatChipsModule} from "@angular/material/chips";
import { ClientViewComponent } from './components/groups/client-view/client-view.component';
import {MatTab, MatTabGroup} from "@angular/material/tabs";
import {MatTooltip} from "@angular/material/tooltip";
import { DeleteGroupsModalComponent } from './components/groups/delete-groups-modal/delete-groups-modal.component';
import { ToastrModule } from 'ngx-toastr';
import { ShowOrganizationalUnitComponent } from './components/groups/organizational-units/show-organizational-unit/show-organizational-unit.component';
import {MatGridList} from "@angular/material/grid-list";
import { TreeViewComponent } from './components/groups/tree-view/tree-view.component';
import {
MatNestedTreeNode,
MatTree,
MatTreeNode,
MatTreeNodeDef, MatTreeNodeOutlet,
MatTreeNodePadding,
MatTreeNodeToggle
} from "@angular/material/tree";
import { LegendComponent } from './components/groups/legend/legend.component';
@NgModule({ declarations: [
AppComponent,
AuthLayoutComponent,
@ -78,7 +93,10 @@ import { DeleteGroupsModalComponent } from './components/groups/delete-groups-mo
EditClientComponent,
ClassroomViewComponent,
ClientViewComponent,
DeleteGroupsModalComponent
DeleteGroupsModalComponent,
ShowOrganizationalUnitComponent,
TreeViewComponent,
LegendComponent
],
bootstrap: [AppComponent],
imports: [BrowserModule,
@ -100,7 +118,21 @@ import { DeleteGroupsModalComponent } from './components/groups/delete-groups-mo
MatSelectModule,
MatDividerModule,
MatStepperModule,
MatSlideToggleModule, MatMenu, MatMenuTrigger, MatMenuItem, MatAutocomplete, MatChipListbox, MatChipOption, MatChipSet, MatChip, MatProgressSpinner, MatTabGroup, MatTab, MatTooltip],
MatSlideToggleModule, MatMenu, MatMenuTrigger, MatMenuItem, MatAutocomplete, MatChipListbox, MatChipOption, MatChipSet, MatChipsModule, MatChip, MatProgressSpinner, MatTabGroup, MatTab, MatTooltip,
ToastrModule.forRoot(
{
timeOut: 5000,
positionClass: 'toast-bottom-right',
preventDuplicates: true,
progressBar: true,
progressAnimation: 'increasing',
closeButton: true
}
), MatGridList, MatTree, MatTreeNode, MatNestedTreeNode, MatTreeNodeToggle, MatTreeNodeDef, MatTreeNodePadding, MatTreeNodeOutlet
],
schemas: [
CUSTOM_ELEMENTS_SCHEMA,
],
providers: [
{
provide: HTTP_INTERCEPTORS,
@ -109,5 +141,5 @@ import { DeleteGroupsModalComponent } from './components/groups/delete-groups-mo
},
provideAnimationsAsync(),
provideHttpClient(withInterceptorsFromDi())
] })
], })
export class AppModule { }

View File

@ -2,8 +2,7 @@
display: flex;
flex-wrap: wrap;
gap: 10px;
height: 57vh;
overflow-y: auto;
min-height: 43vh;
}
.classroom-group {
@ -11,10 +10,21 @@
background-color: #fafafa;
}
mat-card {
width: 150px;
}
mat-chip {
margin: 5px;
font-size: small;
}
mat-card-title {
display: flex;
justify-content: space-between;
margin: 10px;
font-size: medium;
}
.client-row {
display: flex;
@ -25,6 +35,7 @@ mat-card-title {
.client-container {
margin: 5px;
padding-bottom: 20px;
}
.client-box {
@ -34,14 +45,15 @@ mat-card-title {
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);
border-radius: 5px;
cursor: pointer;
transition: box-shadow 0.3s ease-in-out, transform 0.3s ease-in-out;
}
.client-box:hover {
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
scale: 1.1;
transition: 0.3s ease-in-out;
transform: scale(1.2);
}
.client-box p {
margin: 0;
}

View File

@ -9,14 +9,10 @@
<mat-card-title>{{ client.name }}</mat-card-title>
</mat-card-header>
<mat-card-content>
<mat-list role="list">
<mat-list-item>
<mat-chip>{{ client.ip }}</mat-chip>
</mat-list-item>
<mat-list-item>
<mat-chip>{{ client.mac }}</mat-chip>
</mat-list-item>
</mat-list>
<mat-chip-set >
<mat-chip mat-basic-chip>{{ client.ip }}</mat-chip>
<mat-chip mat-basic-chip>{{ client.mac }}</mat-chip>
</mat-chip-set>
</mat-card-content>
</mat-card>
</div>

View File

@ -58,7 +58,6 @@ export class ClassroomViewComponent implements OnInit {
}
handleClientClick(client: any): void {
console.log('Client clicked:', client);
const dialogRef = this.dialog.open(ClientViewComponent, { data: { client }, width: '700px', height:'700px'});
const dialogRef = this.dialog.open(ClientViewComponent, { data: { client }, width: '800px', height:'700px'});
}
}

View File

@ -1,12 +1,4 @@
.mat-dialog-content {
padding: 20px;
}
.button-column {
display: flex;
flex-direction: column;
gap: 20px;
margin: 20px;
mat-dialog-content {
padding: 20px;
}
@ -39,3 +31,27 @@
background-color: #9c27b0; /* Púrpura */
color: white;
}
table {
width: 100%;
}
.fixed-column {
width: 300px;
}
.mat-elevation-z8 {
box-shadow: 0px 4px 6px rgba(0,0,0,0.2);
}
.button-column {
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
}
.button-action {
width: 200px;
}

View File

@ -2,39 +2,59 @@
<div mat-dialog-content>
<mat-tab-group dynamicHeight>
<mat-tab label="Datos generales">
<mat-list role="list">
<mat-list-item role="listitem"><strong>Nombre:</strong> {{ data.client.name }}</mat-list-item>
<mat-list-item role="listitem"><strong>IP:</strong> {{ data.client.ip }}</mat-list-item>
<mat-list-item role="listitem"><strong>MAC:</strong> {{data.client.mac }}</mat-list-item>
<mat-list-item role="listitem"><strong>Nº de serie:</strong> {{data.client.serialNumber }}</mat-list-item>
<mat-list-item role="listitem"><strong>Netiface:</strong> {{data.client.netiface }}</mat-list-item>
<mat-list-item role="listitem"><strong>Fecha de creación:</strong> {{data.client.createdAt | date }}</mat-list-item>
<mat-list-item role="listitem"><strong>Creado por:</strong> {{data.client.createdBy }}</mat-list-item>
</mat-list>
<table mat-table [dataSource]="generalData" class="mat-elevation-z8">
<ng-container matColumnDef="property">
<th mat-header-cell *matHeaderCellDef class="fixed-column"> Propiedad </th>
<td mat-cell *matCellDef="let element" class="fixed-column"> {{element.property}} </td>
</ng-container>
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef> Valor </th>
<td mat-cell *matCellDef="let element"> {{element.value}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</mat-tab>
<mat-tab label="Propiedades de red">
<mat-list role="list">
<mat-list-item role="listitem"><strong>Menu:</strong> {{ data.client.menu }}</mat-list-item>
<mat-list-item role="listitem"><strong>Perfil hardware:</strong> {{ data.client.hardwareProfile }}</mat-list-item>
<mat-list-item role="listitem"><strong>OGlive:</strong> {{data.client.mac }}</mat-list-item>
<mat-list-item role="listitem"><strong>Autoexec:</strong> {{data.client.serialNumber }}</mat-list-item>
<mat-list-item role="listitem"><strong>Repositorio:</strong> {{data.client.netiface }}</mat-list-item>
<mat-list-item role="listitem"><strong>Validacion:</strong> {{data.client.netiface }}</mat-list-item>
<mat-list-item role="listitem"><strong>Página login:</strong> {{data.client.netiface }}</mat-list-item>
<mat-list-item role="listitem"><strong>Página validacion:</strong> {{data.client.netiface }}</mat-list-item>
<mat-list-item role="listitem"><strong>Fecha de creación:</strong> {{data.client.createdAt | date }}</mat-list-item>
<mat-list-item role="listitem"><strong>Creado por:</strong> {{data.client.createdBy }}</mat-list-item>
</mat-list>
<table mat-table [dataSource]="networkData" class="mat-elevation-z8">
<ng-container matColumnDef="property">
<th mat-header-cell *matHeaderCellDef class="fixed-column"> Propiedad </th>
<td mat-cell *matCellDef="let element" class="fixed-column"> {{element.property}} </td>
</ng-container>
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef> Valor </th>
<td mat-cell *matCellDef="let element"> {{element.value}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</mat-tab>
<mat-tab label="Propiedades del aula">
<table mat-table [dataSource]="classroomData" class="mat-elevation-z8">
<ng-container matColumnDef="property">
<th mat-header-cell *matHeaderCellDef class="fixed-column"> Propiedad </th>
<td mat-cell *matCellDef="let element" class="fixed-column"> {{element.property}} </td>
</ng-container>
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef> Valor </th>
<td mat-cell *matCellDef="let element"> {{element.value}} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</mat-tab>
<mat-tab label="Acciones">
<div class="button-column">
<button mat-flat-button color="primary" class="button-encender">Encender</button>
<button mat-flat-button color="accent" class="button-apagar">Apagar</button>
<button mat-flat-button color="warn" class="button-resetear">Resetear</button>
<button mat-flat-button class="button-otros-1">Otras acciones 1</button>
<button mat-flat-button class="button-otros-2">Otras acciones 2</button>
<button mat-flat-button class="button-otros-3">Otras acciones 3</button>
<button mat-flat-button color="primary" class="button-action button-encender">Encender</button>
<button mat-flat-button color="accent" class="button-action button-apagar">Apagar</button>
<button mat-flat-button color="warn" class="button-action button-resetear">Resetear</button>
<button mat-flat-button class="button-action button-otros-1">Otras acciones 1</button>
<button mat-flat-button class="button-action button-otros-2">Otras acciones 2</button>
<button mat-flat-button class="button-action button-otros-3">Otras acciones 3</button>
</div>
</mat-tab>
</mat-tab-group>
</div>
<mat-dialog-actions>
<button mat-button mat-dialog-close (click)="onNoClick()">Cerrar</button>
</mat-dialog-actions>

View File

@ -1,5 +1,5 @@
import {Component, Inject} from '@angular/core';
import {MAT_DIALOG_DATA} from "@angular/material/dialog";
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
@Component({
selector: 'app-client-view',
@ -7,8 +7,56 @@ import {MAT_DIALOG_DATA} from "@angular/material/dialog";
styleUrl: './client-view.component.css'
})
export class ClientViewComponent {
displayedColumns: string[] = ['property', 'value'];
generalData = [
{property: 'Nombre', value: this.data.client.name},
{property: 'Uuid', value: this.data.client.uuid},
{property: 'IP', value: this.data.client.ip},
{property: 'MAC', value: this.data.client.mac},
{property: 'Nº de serie', value: this.data.client.serialNumber},
{property: 'Netiface', value: this.data.client.netiface},
{property: 'Fecha de creación', value: this.data.client.createdAt},
{property: 'Creado por', value: this.data.client.createdBy}
];
networkData = [
{property: 'Menu', value: this.data.client.menu},
{property: 'Perfil hardware', value: this.data.client.hardwareProfile ? this.data.client.hardwareProfile.description : ''},
{property: 'OGlive', value: ''},
{property: 'Autoexec', value: ''},
{property: 'Repositorio', value: ''},
{property: 'Validacion', value: ''},
{property: 'Página login', value: ''},
{property: 'Página validacion', value: ''},
{property: 'Fecha de creación', value: this.data.client.createdAt},
{property: 'Creado por', value: this.data.client.createdBy}
];
classroomData = [
{property: 'Url servidor proxy', value: this.data.client.networkSettings ? this.data.client.networkSettings.proxy : ''},
{property: 'IP DNS', value: this.data.client.networkSettings ? this.data.client.networkSettings.dns : ''},
{property: 'Máscara de red', value: this.data.client.networkSettings ? this.data.client.networkSettings.mask : ''},
{property: 'Router', value: this.data.client.networkSettings ? this.data.client.networkSettings.router : ''},
{property: 'NTP', value: this.data.client.networkSettings ? this.data.client.networkSettings.ntp : ''},
{property: 'Modo p2p', value: this.data.client.networkSettings ? this.data.client.networkSettings.p2pMode : ''},
{property: 'Tiempo p2p', value: this.data.client.networkSettings ? this.data.client.networkSettings.p2pTime : ''},
{property: 'IP multicast', value: this.data.client.networkSettings ? this.data.client.networkSettings.mcastIp : ''},
{property: 'Modo multicast', value: this.data.client.networkSettings ? this.data.client.networkSettings.mcastMode : ''},
{property: 'Puerto multicast', value: this.data.client.networkSettings ? this.data.client.networkSettings.mcastPort : ''},
{property: 'Velocidad multicast', value: this.data.client.networkSettings ? this.data.client.networkSettings.mcastSpeed : ''},
{property: 'Perfil hardware', value: this.data.client.networkSettings && this.data.client.networkSettings.hardwareProfile ? this.data.client.networkSettings.hardwareProfile.description : ''},
{property: 'Menú', value: this.data.client.networkSettings && this.data.client.networkSettings.menu ? this.data.client.networkSettings.menu.description : ''}
];
constructor(
private dialogRef: MatDialogRef<ClientViewComponent>,
@Inject(MAT_DIALOG_DATA) public data: any // Inject data for edit mode
) {
}
onNoClick(): void {
this.dialogRef.close();
}
}

View File

@ -19,14 +19,20 @@
<input matInput formControlName="serialNumber" >
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Interfaz de Red</mat-label>
<mat-hint>Ejemplo: eth0</mat-hint>
<input matInput formControlName="netiface">
<mat-label>Interfaz de red</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>Controlador de Red</mat-label>
<mat-hint>Ejemplo: e1000e</mat-hint>
<input matInput formControlName="netDriver">
<mat-label>Controlador de red</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>MAC</mat-label>
@ -48,7 +54,7 @@
<mat-form-field class="form-field">
<mat-label>Perfil de Hardware</mat-label>
<mat-select formControlName="hardwareProfile">
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">{{ unit.name }} </mat-option>
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">{{ unit.description }} </mat-option>
</mat-select>
<mat-error>Formato de URL inválido.</mat-error>
</mat-form-field>

View File

@ -3,6 +3,8 @@ import {Component, Inject, OnInit} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {MatSnackBar} from "@angular/material/snack-bar";
import {ToastrService} from "ngx-toastr";
import {DataService} from "../../data.service";
@Component({
selector: 'app-create-client',
@ -14,19 +16,29 @@ export class CreateClientComponent implements OnInit {
parentUnits: any[] = []; // Array to store parent units fetched from API
hardwareProfiles: any[] = []; // Array to store hardware profiles fetched from API
private errorForm: boolean = false;
protected netifaceTypes = [
{"name": 'Eth0', "value": "eth0"},
{"name": 'Eth1', "value": "eth1"},
{"name": 'Eth2', "value": "eth2"},
];
protected netDriverTypes = [
{"name": 'Generic', "value": "generic"},
];
constructor(
private fb: FormBuilder,
private dialogRef: MatDialogRef<CreateClientComponent>,
private http: HttpClient,
private snackBar: MatSnackBar,
private toastService: ToastrService,
private dataService: DataService,
@Inject(MAT_DIALOG_DATA) public data: any // Inject data for edit mode
) {
}
ngOnInit(): void {
this.loadParentUnits(); // Load parent units when component initializes
this.loadHardwareProfiles(); // Load hardware profiles when component initializes
this.loadHardwareProfiles();
this.clientForm = this.fb.group({
organizationalUnit: [this.data.organizationalUnit ? this.data.organizationalUnit['@id'] : null, Validators.required],
name: ['', Validators.required],
@ -40,6 +52,17 @@ export class CreateClientComponent implements OnInit {
});
}
loadHardwareProfiles(): void {
this.dataService.getHardwareProfiles().subscribe(
(data: any[]) => {
this.hardwareProfiles = data;
},
(error: any) => {
console.error('Error fetching hardware profiles', error);
}
);
}
loadParentUnits() {
const url = 'http://127.0.0.1:8080/organizational-units?page=1&itemsPerPage=10000';
@ -53,19 +76,6 @@ export class CreateClientComponent implements OnInit {
);
}
loadHardwareProfiles() {
const url = 'http://127.0.0.1:8080/hardware-profiles';
this.http.get<any>(url).subscribe(
response => {
this.hardwareProfiles = response['hydra:member'];
},
error => {
console.error('Error fetching hardware profiles:', error);
}
);
}
onSubmit() {
if (this.clientForm.valid) {
this.errorForm = false
@ -73,7 +83,8 @@ export class CreateClientComponent implements OnInit {
this.http.post('http://127.0.0.1:8080/clients', formData).subscribe(
response => {
this.dialogRef.close(response);
this.openSnackBar(false, 'Cliente creado exitosamente'); },
this.openSnackBar(false, 'Cliente creado exitosamente');
},
error => {
console.error('Error during POST:', error);
this.errorForm = true
@ -88,8 +99,9 @@ export class CreateClientComponent implements OnInit {
}
openSnackBar(isError: boolean, message: string) {
this.snackBar.open(message, 'Cerrar', {
panelClass: isError ? ['snackbar-error'] : ['snackbar-success']
});
if (isError) {
this.toastService.error(' Error al crear el cliente: ' + message, 'Error');
} else
this.toastService.success(message, 'Éxito');
}
}

View File

@ -16,12 +16,20 @@
<input matInput formControlName="serialNumber" >
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Interfaz de Red</mat-label>
<input matInput formControlName="netiface">
<mat-label>Interfaz de red</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>Controlador de Red</mat-label>
<input matInput formControlName="netDriver">
<mat-label>Controlador de red</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>MAC</mat-label>
@ -41,7 +49,7 @@
<mat-form-field class="form-field">
<mat-label>Perfil de Hardware</mat-label>
<mat-select formControlName="hardwareProfile">
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">{{ unit.name }} </mat-option>
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">{{ unit.description }} </mat-option>
</mat-select>
<mat-error>Formato de URL inválido.</mat-error>
</mat-form-field>

View File

@ -3,6 +3,8 @@ import {Component, Inject} from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import { CreateClientComponent } from '../create-client/create-client.component';
import {DataService} from "../../data.service";
import {ToastrService} from "ngx-toastr";
@Component({
selector: 'app-edit-client',
@ -14,11 +16,21 @@ export class EditClientComponent {
parentUnits: any[] = []; // Array to store parent units fetched from API
hardwareProfiles: any[] = []; // Array to store hardware profiles fetched from API
isEditMode: boolean; // Flag to check if it's edit mode
protected netifaceTypes = [
{"name": 'Eth0', "value": "eth0"},
{"name": 'Eth1', "value": "eth1"},
{"name": 'Eth2', "value": "eth2"},
];
protected netDriverTypes = [
{"name": 'Generic', "value": "generic"},
];
constructor(
private fb: FormBuilder,
private dialogRef: MatDialogRef<CreateClientComponent>,
private http: HttpClient,
private dataService: DataService,
private toastService: ToastrService,
@Inject(MAT_DIALOG_DATA) public data: any // Inject data for edit mode
) {
this.isEditMode = !!data?.uuid; // Check if uuid is passed to determine edit mode
@ -56,15 +68,13 @@ export class EditClientComponent {
);
}
loadHardwareProfiles() {
const url = 'http://127.0.0.1:8080/hardware-profiles';
this.http.get<any>(url).subscribe(
response => {
this.hardwareProfiles = response['hydra:member'];
loadHardwareProfiles(): void {
this.dataService.getHardwareProfiles().subscribe(
(data: any[]) => {
this.hardwareProfiles = data;
},
error => {
console.error('Error fetching hardware profiles:', error);
(error: any) => {
console.error('Error fetching hardware profiles', error);
}
);
}
@ -101,9 +111,12 @@ export class EditClientComponent {
response => {
console.log('PUT successful:', response);
this.dialogRef.close();
this.openSnackBar(false, 'Cliente actualizado exitosamente');
},
error => {
console.error('Error al realizar PUT:', error);
this.openSnackBar(true, 'Error al actualizar el cliente: ' + error.error['hydra:description']);
}
);
} else {
@ -115,9 +128,11 @@ export class EditClientComponent {
response => {
console.log('POST successful:', response);
this.dialogRef.close();
this.openSnackBar(false, 'Cliente creado exitosamente');
},
error => {
console.error('Error al realizar POST:', error);
this.openSnackBar(true, 'Error al crear el cliente: ' + error.error['hydra:description']);
}
);
}
@ -127,4 +142,11 @@ export class EditClientComponent {
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');
}
}

View File

@ -14,11 +14,16 @@ export class DataService {
constructor(private http: HttpClient) {}
getOrganizationalUnits(): Observable<UnidadOrganizativa[]> {
return this.http.get<any>(`${this.apiUrl}&type=organizational-unit`).pipe(
getOrganizationalUnits(search: string = ''): Observable<UnidadOrganizativa[]> {
let url = `${this.apiUrl}&type=organizational-unit`;
if (search) {
url += `&name=${encodeURIComponent(search)}`;
}
return this.http.get<any>(url).pipe(
map(response => {
if (response['hydra:member'] && Array.isArray(response['hydra:member'])) {
return response['hydra:member']
return response['hydra:member'];
} else {
throw new Error('Unexpected response format');
}
@ -62,6 +67,23 @@ export class DataService {
);
}
getHardwareProfiles(): Observable<any[]> {
const url = 'http://127.0.0.1:8080/hardware-profiles';
return this.http.get<any>(url).pipe(
map(response => {
if (response['hydra:member'] && Array.isArray(response['hydra:member'])) {
return response['hydra:member']
} else {
throw new Error('Unexpected response format');
}
}),
catchError(error => {
console.error('Error fetching clients', error);
return throwError(error);
})
);
}
deleteElement(uuid: string, type: string): Observable<void> {
const url = type === 'client'
? `http://127.0.0.1:8080/clients/${uuid}`

View File

@ -3,6 +3,16 @@
flex-wrap: wrap;
}
.search-container {
display: flex;
flex-grow: 1;
margin: 10px;
}
.search-container mat-form-field {
width: 50%;
}
.card {
flex-grow: 1;
margin: 10px;
@ -15,7 +25,8 @@
.unidad-card, .elements-card {
flex: 1 1 45%;
background-color: #fafafa;
max-height: 400px;
height: 400px;
overflow-y: auto;
}
.element-content {

View File

@ -1,37 +1,61 @@
<div class="header-container">
<h2 class="title">Administrar grupos</h2>
<div class="groups-button-row">
<button mat-flat-button color="primary" (click)="addOU()">Nueva Unidad Organizativa</button>
<button mat-flat-button color="primary" (click)="addClient()">Nuevo Cliente</button>
<button mat-flat-button color="primary" (click)="addOU($event)">Nueva Unidad Organizativa</button>
<button mat-flat-button color="primary" (click)="addClient($event)">Nuevo Cliente</button>
<button mat-raised-button (click)="openBottomSheet()">Leyenda</button>
</div>
</div>
<div class="search-container">
<mat-form-field appearance="fill">
<mat-label>Búsqueda</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="searchTerm" (keyup.enter)="search()">
<mat-icon matSuffix>search</mat-icon>
<mat-hint>Pulsar 'enter' para buscar entre las unidades organizativas</mat-hint>
</mat-form-field>
</div>
<div class="groupLists-container">
<mat-card class="card unidad-card">
<mat-card-title>Unidad organizativa</mat-card-title>
<mat-card-title >Unidad organizativa</mat-card-title>
<mat-card-content>
<mat-list >
<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
class="edit-icon"
(click)="onTreeClick($event, unidad)"
#tooltip="matTooltip"
matTooltip="Visualizar en forma de arbol"
matTooltipHideDelay="0">account_tree
</mat-icon>
<mat-icon
class="edit-icon"
(click)="onEditClick(unidad.type, unidad.uuid)"
(click)="onEditClick($event, unidad.type, unidad.uuid)"
#tooltip="matTooltip"
matTooltip="Editar unidad organizativa"
matTooltipHideDelay="0">edit
</mat-icon>
<mat-icon
class="edit-icon"
(click)="addOU(unidad)"
(click)="onShowClick($event, unidad)"
#tooltip="matTooltip"
matTooltip="Visualizar unidad organizativa"
matTooltipHideDelay="0">visibility
</mat-icon>
<mat-icon
class="edit-icon"
(click)="addOU($event, unidad)"
#tooltip="matTooltip"
matTooltip="Crear unidad organizativa interna"
matTooltipHideDelay="0">add_home_work
</mat-icon>
<mat-icon
class="edit-icon" (click)="addClient(unidad)"
class="edit-icon" (click)="addClient($event, unidad)"
#tooltip="matTooltip"
matTooltip="Crear cliente en esta unidad organizativa"
matTooltipHideDelay="0">devices
@ -56,8 +80,8 @@
</div>
</mat-card-title>
<mat-card-content class="element-content">
<mat-spinner *ngIf="loading"></mat-spinner>
<mat-list>
<mat-spinner *ngIf="loadingChildren"></mat-spinner>
<mat-list *ngIf="!loadingChildren">
<div *ngIf="children.length === 0" class="empty-list">
<mat-icon>info</mat-icon>
<span>No hay elementos internos</span>
@ -66,7 +90,7 @@
<div class="item-content">
<mat-icon [ngSwitch]="child.type">
<ng-container *ngSwitchCase="'organizational-unit'">apartment</ng-container>
<ng-container *ngSwitchCase="'classrooms-group'">groups</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>
@ -74,9 +98,11 @@
</mat-icon>
{{child.name}}
<span class="actions">
<mat-icon class="edit-icon" (click)="onEditClick(child.type, child.uuid)">edit</mat-icon>
<mat-icon class="edit-icon" (click)="addClient(child)">devices</mat-icon>
<mat-icon class="delete-icon" (click)="onDeleteClick($event, child.uuid, child.name, child.type)">delete</mat-icon>
<mat-icon class="edit-icon" (click)="onEditClick($event, child.type, child.uuid)">edit</mat-icon>
<mat-icon *ngIf="child.type !== 'client'" class="edit-icon" (click)="onShowClick($event, child)" #tooltip="matTooltip" matTooltip="Visualizar unidad organizativa" matTooltipHideDelay="0">visibility</mat-icon>
<mat-icon *ngIf="child.type !== 'client'" class="edit-icon" (click)="addOU($event, child)" #tooltip="matTooltip" matTooltip="Crear unidad organizativa interna" matTooltipHideDelay="0">add_home_work</mat-icon>
<mat-icon *ngIf="child.type !== 'client'" class="edit-icon" (click)="addClient($event, child)" #tooltip="matTooltip" matTooltip="Crear cliente en esta unidad organizativa" matTooltipHideDelay="0">devices</mat-icon>
<mat-icon class="delete-icon" (click)="onDeleteClick($event, child.uuid, child.name, child.type)" #tooltip="matTooltip" matTooltip="Borrar elemento" matTooltipHideDelay="0">delete</mat-icon>
</span>
</div>
</mat-list-item>

View File

@ -7,7 +7,12 @@ import { DeleteModalComponent } from './delete-modal/delete-modal.component';
import { CreateClientComponent } from './clients/create-client/create-client.component';
import { EditOrganizationalUnitComponent } from './organizational-units/edit-organizational-unit/edit-organizational-unit.component';
import { EditClientComponent } from './clients/edit-client/edit-client.component';
import {DeleteGroupsModalComponent} from "./delete-groups-modal/delete-groups-modal.component";
import { DeleteGroupsModalComponent } from "./delete-groups-modal/delete-groups-modal.component";
import { ShowOrganizationalUnitComponent} from "./organizational-units/show-organizational-unit/show-organizational-unit.component";
import {ToastrService} from "ngx-toastr";
import {TreeViewComponent} from "./tree-view/tree-view.component";
import {MatBottomSheet} from "@angular/material/bottom-sheet";
import {LegendComponent} from "./legend/legend.component";
@Component({
selector: 'app-groups',
@ -23,15 +28,34 @@ export class GroupsComponent implements OnInit {
clientsData: any[] = []; // Nueva variable para almacenar los datos de clients
breadcrumbData: any[] = []; // Almacenar datos de breadcrumb para navegar
loading:boolean = false;
constructor(private dataService: DataService, public dialog: MatDialog) {}
loadingChildren:boolean = false;
searchTerm: string = '';
constructor(
private dataService: DataService,
public dialog: MatDialog,
private toastService: ToastrService,
private _bottomSheet: MatBottomSheet
) {}
ngOnInit(): void {
this.dataService.getOrganizationalUnits().subscribe(
data => this.organizationalUnits = data,
error => console.error('Error fetching unidades organizativas', error)
this.search();
}
search(): void {
this.loading = true;
this.dataService.getOrganizationalUnits(this.searchTerm).subscribe(
data => {
this.organizationalUnits = data;
this.loading = false; // Desactivar el spinner después de obtener los datos
},
error => {
console.error('Error fetching unidades organizativas', error);
this.loading = false; // Desactivar el spinner en caso de error
}
);
}
onSelectUnidad(unidad: UnidadOrganizativa): void {
this.selectedUnidad = unidad;
this.selectedDetail = unidad; // Mostrar detalles de la unidad seleccionada
@ -61,7 +85,7 @@ constructor(private dataService: DataService, public dialog: MatDialog) {}
}
loadChildrenAndClients(id: string): void {
this.loading = true
this.loadingChildren = true
this.dataService.getChildren(id).subscribe(
childrenData => {
console.log('Children data:', childrenData);
@ -74,44 +98,51 @@ constructor(private dataService: DataService, public dialog: MatDialog) {}
this.children = newChildren;
} else {
this.children = []; // Limpiar card2 cuando no hay elementos
// Si deseas que la unidad organizativa se limpie completamente, descomenta la línea siguiente:
// this.selectedUnidad = null;
}
this.loadingChildren = false
},
error => {
console.error('Error fetching clients', error);
this.clientsData = []; // Limpiar clientsData en caso de error
this.children = []; // Limpiar card2 en caso de error
this.loadingChildren = false
}
);
},
error => {
console.error('Error fetching children', error);
this.children = []; // Limpiar card2 en caso de error
this.loadingChildren = false
}
);
this.loading = false
}
addOU(parent:any = null): void {
console.log('Parent:', parent);
addOU(event: MouseEvent, parent:any = null): void {
event.stopPropagation();
const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, { data: { parent }, width: '700px'});
dialogRef.afterClosed().subscribe(() => {
this.dataService.getOrganizationalUnits().subscribe(
data => this.organizationalUnits = data,
data => {
this.organizationalUnits = data
this.loadChildrenAndClients(parent.id);
},
error => console.error('Error fetching unidades organizativas', error)
);
});
}
addClient(organizationalUnit:any = null): void {
addClient(event: MouseEvent, organizationalUnit:any = null): void {
event.stopPropagation();
const dialogRef = this.dialog.open(CreateClientComponent, { data: { organizationalUnit }, width: '700px'});
// Subscribirse al evento unitAdded del componente de creación después de cerrar el diálogo
dialogRef.afterClosed().subscribe(() => {
this.dataService.getOrganizationalUnits().subscribe(
data => this.organizationalUnits = data,
data => {
this.organizationalUnits = data
this.loadChildrenAndClients(organizationalUnit.id);
},
error => console.error('Error fetching unidades organizativas', error)
);
});
@ -134,7 +165,7 @@ constructor(private dataService: DataService, public dialog: MatDialog) {}
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)
);
@ -155,8 +186,12 @@ constructor(private dataService: DataService, public dialog: MatDialog) {}
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)
error => {
console.error('Error deleting element', error)
this.openSnackBar(true, error.error['hydra:description'])
}
);
} else if (result && result === 'change') {
this.dataService.changeParent(uuid).subscribe(
@ -167,14 +202,18 @@ constructor(private dataService: DataService, public dialog: MatDialog) {}
error => console.error('Error fetching unidades organizativas', error)
);
},
error => console.error('Error deleting element', error)
error => {
console.error('Error deleting element', error)
this.openSnackBar(true, error.error['hydra:description'])
}
);
}
});
}
}
onEditClick(type: any, uuid: string): void {
onEditClick(event: MouseEvent, type: any, uuid: string): void {
event.stopPropagation();
console.log('Tipo del elemento a editar:', type);
console.log('UUID del elemento a editar:', uuid);
if (type != "client") {
@ -184,4 +223,29 @@ constructor(private dataService: DataService, public dialog: MatDialog) {}
const dialogRef = this.dialog.open(EditClientComponent, { data: { uuid }, width: '700px' } );
}
}
onShowClick(event: MouseEvent, data: any): void {
event.stopPropagation();
if (data.type != "client") {
const dialogRef = 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'});
}
}
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);
}
}

View File

@ -0,0 +1,22 @@
<mat-list>
<mat-list-item>
<mat-icon matListItemIcon>apartment</mat-icon>
<div matListItemTitle>Unidad organizativa</div>
</mat-list-item>
<mat-list-item>
<mat-icon matListItemIcon>meeting_room</mat-icon>
<div matListItemTitle>Grupos de aula</div>
</mat-list-item>
<mat-list-item>
<mat-icon matListItemIcon>school</mat-icon>
<div matListItemTitle>Aula</div>
</mat-list-item>
<mat-list-item>
<mat-icon matListItemIcon>lan</mat-icon>
<div matListItemTitle>Grupos de clientes</div>
</mat-list-item>
<mat-list-item>
<mat-icon matListItemIcon>computer</mat-icon>
<div matListItemTitle>Cliente</div>
</mat-list-item>
</mat-list>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LegendComponent } from './legend.component';
describe('LegendComponent', () => {
let component: LegendComponent;
let fixture: ComponentFixture<LegendComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [LegendComponent]
})
.compileComponents();
fixture = TestBed.createComponent(LegendComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,16 @@
import { Component } from '@angular/core';
import {MatBottomSheetRef} from "@angular/material/bottom-sheet";
@Component({
selector: 'app-legend',
templateUrl: './legend.component.html',
styleUrl: './legend.component.css'
})
export class LegendComponent {
constructor(private _bottomSheetRef: MatBottomSheetRef<LegendComponent>) {}
openLink(event: MouseEvent): void {
this._bottomSheetRef.dismiss();
event.preventDefault();
}
}

View File

@ -97,7 +97,13 @@
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Modo P2P</mat-label>
<input matInput formControlName="p2pMode">
<mat-select formControlName="p2pMode">
<mat-option
*ngFor="let option of p2pModeOptions"
value="{{ option.value }}">
{{ option.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Tiempo P2P</mat-label>
@ -117,7 +123,13 @@
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Modo Multicast</mat-label>
<input matInput formControlName="mcastMode">
<mat-select formControlName="mcastMode">
<mat-option
*ngFor="let option of multicastModeOptions"
value="{{ option.value }}">
{{ option.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Menú URL</mat-label>
@ -125,7 +137,10 @@
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Perfil de Hardware</mat-label>
<input matInput formControlName="hardwareProfile" type="url">
<mat-select formControlName="hardwareProfile">
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">{{ unit.description }} </mat-option>
</mat-select>
<mat-error>Formato de URL inválido.</mat-error>
</mat-form-field>
<div>
<button mat-button matStepperPrevious>Atrás</button>

View File

@ -2,6 +2,8 @@ import {Component, OnInit, Output, EventEmitter, Inject} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import {ToastrService} from "ngx-toastr";
import {DataService} from "../../data.service";
@Component({
selector: 'app-create-organizational-unit',
@ -22,13 +24,24 @@ export class CreateOrganizationalUnitComponent implements OnInit {
'clients-group': 'Grupo de clientes'
};
parentUnits: any[] = [];
hardwareProfiles: any[] = [];
protected p2pModeOptions = [
{"name": 'Leecher', "value": "p2p-mode-leecher"},
{"name": 'Peer', "value": "p2p-mode-peer"},
{"name": 'Seeder', "value": "p2p-mode-seeder"},
];
protected multicastModeOptions = [
{"name": 'Half duplex', "value": "half-duplex"},
{"name": 'Full duplex', "value": "full-duplex"},
];
@Output() unitAdded = new EventEmitter();
constructor(
private _formBuilder: FormBuilder,
private dialogRef: MatDialogRef<CreateOrganizationalUnitComponent>,
private http: HttpClient,
private toastService: ToastrService,
private dataService: DataService,
@Inject(MAT_DIALOG_DATA) public data: any // Inject data for edit mode
) {
this.generalFormGroup = this._formBuilder.group({
@ -52,8 +65,8 @@ export class CreateOrganizationalUnitComponent implements OnInit {
mcastSpeed: [0, Validators.min(0)],
mcastPort: [0, Validators.min(0)],
mcastMode: [''],
menu: ['', Validators.pattern('https?://.+')],
hardwareProfile: ['', Validators.pattern('https?://.+')],
menu: [null],
hardwareProfile: [null],
validation: [false]
});
this.classroomInfoFormGroup = this._formBuilder.group({
@ -66,6 +79,7 @@ export class CreateOrganizationalUnitComponent implements OnInit {
ngOnInit() {
this.loadParentUnits();
this.loadHardwareProfiles();
}
loadParentUnits() {
@ -81,6 +95,17 @@ export class CreateOrganizationalUnitComponent implements OnInit {
);
}
loadHardwareProfiles(): void {
this.dataService.getHardwareProfiles().subscribe(
(data: any[]) => {
this.hardwareProfiles = data;
},
(error: any) => {
console.error('Error fetching hardware profiles', error);
}
);
}
private cleanFormValues(formGroup: FormGroup): any {
const cleanedValues: any = {};
Object.keys(formGroup.controls).forEach(key => {
@ -123,9 +148,11 @@ export class CreateOrganizationalUnitComponent implements OnInit {
console.log('POST successful:', response);
this.unitAdded.emit();
this.dialogRef.close();
this.openSnackBar(false, 'Cliente creado exitosamente');
},
error => {
console.error('Error al realizar POST:', error);
this.openSnackBar(true, 'Error al crear la unidad organizativa: ' + error.error['hydra:description']);
}
);
}
@ -134,4 +161,11 @@ export class CreateOrganizationalUnitComponent 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('Cliente creado exitosamente', 'Éxito');
}
}

View File

@ -31,7 +31,28 @@
</form>
</mat-step>
<!-- Step 2: Información Adicional -->
<!-- Step 2: Classroom Info -->
<mat-step *ngIf="generalFormGroup.value.type === 'classroom'" [stepControl]="classroomInfoFormGroup">
<form [formGroup]="classroomInfoFormGroup">
<ng-template matStepLabel>Información del Aula</ng-template>
<mat-form-field class="form-field">
<mat-label>Ubicación</mat-label>
<input matInput formControlName="location">
</mat-form-field>
<mat-slide-toggle formControlName="projector">Proyector</mat-slide-toggle>
<mat-slide-toggle formControlName="board">Pizarra</mat-slide-toggle>
<mat-form-field class="form-field">
<mat-label>Aforo</mat-label>
<input matInput formControlName="capacity" type="number">
</mat-form-field>
<div>
<button mat-button matStepperPrevious>Atrás</button>
<button mat-button matStepperNext>Siguiente</button>
</div>
</form>
</mat-step>
<!-- Step 3: Información Adicional -->
<mat-step [stepControl]="additionalInfoFormGroup">
<form [formGroup]="additionalInfoFormGroup">
<ng-template matStepLabel>Información Adicional</ng-template>
@ -46,7 +67,7 @@
</form>
</mat-step>
<!-- Step 3: Configuración de Red -->
<!-- Step 4: Configuración de Red -->
<mat-step [stepControl]="networkSettingsFormGroup">
<form [formGroup]="networkSettingsFormGroup">
<ng-template matStepLabel>Configuración de Red</ng-template>
@ -72,7 +93,13 @@
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Modo P2P</mat-label>
<input matInput formControlName="p2pMode">
<mat-select formControlName="p2pMode">
<mat-option
*ngFor="let option of p2pModeOptions"
value="{{ option.value }}">
{{ option.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Tiempo P2P</mat-label>
@ -92,7 +119,13 @@
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Modo Multicast</mat-label>
<input matInput formControlName="mcastMode">
<mat-select formControlName="mcastMode">
<mat-option
*ngFor="let option of multicastModeOptions"
value="{{ option.value }}">
{{ option.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Menú URL</mat-label>
@ -100,7 +133,10 @@
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Perfil de Hardware</mat-label>
<input matInput formControlName="hardwareProfile" type="url">
<mat-select formControlName="hardwareProfile">
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">{{ unit.description }} </mat-option>
</mat-select>
<mat-error>Formato de URL inválido.</mat-error>
</mat-form-field>
<mat-slide-toggle formControlName="validation">Validación</mat-slide-toggle>
<div>

View File

@ -3,6 +3,8 @@ import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { CreateOrganizationalUnitComponent } from '../create-organizational-unit/create-organizational-unit.component';
import {DataService} from "../../data.service";
import {ToastrService} from "ngx-toastr";
@Component({
selector: 'app-edit-organizational-unit',
@ -14,16 +16,28 @@ export class EditOrganizationalUnitComponent implements OnInit {
generalFormGroup: FormGroup;
additionalInfoFormGroup: FormGroup;
networkSettingsFormGroup: FormGroup;
classroomInfoFormGroup: FormGroup;
types: string[] = ['organizational-unit', 'classrooms-group', 'classroom', 'clients-group'];
parentUnits: any[] = []; // Array to store parent units fetched from API
hardwareProfiles: any[] = [];
isEditMode: boolean; // Flag to check if it's edit mode
protected p2pModeOptions = [
{"name": 'Leecher', "value": "p2p-mode-leecher"},
{"name": 'Peer', "value": "p2p-mode-peer"},
{"name": 'Seeder', "value": "p2p-mode-seeder"},
];
protected multicastModeOptions = [
{"name": 'Half duplex', "value": "half-duplex"},
{"name": 'Full duplex', "value": "full-duplex"},
];
@Output() unitAdded = new EventEmitter(); // Event emitter to notify parent component about unit addition
constructor(
private _formBuilder: FormBuilder,
private dialogRef: MatDialogRef<CreateOrganizationalUnitComponent>,
private http: HttpClient, // Inject HttpClient for HTTP requests
private dataService: DataService,
private toastService: ToastrService,
@Inject(MAT_DIALOG_DATA) public data: any // Inject data for edit mode
) {
this.isEditMode = !!data?.uuid; // Check if uuid is passed to determine edit mode
@ -51,11 +65,18 @@ export class EditOrganizationalUnitComponent implements OnInit {
mcastSpeed: [0, Validators.min(0)],
mcastPort: [0, Validators.min(0)],
mcastMode: [''],
menu: ['', Validators.pattern('https?://.+')],
hardwareProfile: ['', Validators.pattern('https?://.+')],
menu: [null],
hardwareProfile: [null],
validation: [false]
});
this.classroomInfoFormGroup = this._formBuilder.group({
location: [''],
projector: [false],
board: [false],
capacity: [0, Validators.min(0)]
});
if (this.isEditMode) {
this.loadData(data.uuid);
}
@ -63,6 +84,7 @@ export class EditOrganizationalUnitComponent implements OnInit {
ngOnInit() {
this.loadParentUnits(); // Load parent units when component initializes
this.loadHardwareProfiles();
}
loadParentUnits() {
@ -78,6 +100,17 @@ export class EditOrganizationalUnitComponent implements OnInit {
);
}
loadHardwareProfiles(): void {
this.dataService.getHardwareProfiles().subscribe(
(data: any[]) => {
this.hardwareProfiles = data;
},
(error: any) => {
console.error('Error fetching hardware profiles', error);
}
);
}
loadData(uuid: string) {
const url = `http://127.0.0.1:8080/organizational-units/${uuid}`;
@ -104,13 +137,21 @@ export class EditOrganizationalUnitComponent implements OnInit {
mcastSpeed: data.networkSettings.mcastSpeed,
mcastPort: data.networkSettings.mcastPort,
mcastMode: data.networkSettings.mcastMode,
menu: data.networkSettings.menu,
hardwareProfile: data.networkSettings.hardwareProfile,
menu: data.networkSettings.menu ? data.networkSettings.menu['@id'] : null,
hardwareProfile: data.networkSettings.hardwareProfile ? data.networkSettings.hardwareProfile['@id'] : null,
validation: data.networkSettings.validation
});
this.classroomInfoFormGroup.patchValue({
location: data.classroomInfo.location,
projector: data.classroomInfo.projector,
board: data.classroomInfo.board,
capacity: data.classroomInfo.capacity
});
},
error => {
console.error('Error fetching data for edit:', error);
this.openSnackBar(true, 'Error al cargar la unidad organizativa: ' + error.error['hydra:description'])
this.onNoClick()
}
);
}
@ -153,9 +194,11 @@ export class EditOrganizationalUnitComponent implements OnInit {
console.log('POST successful:', response);
this.unitAdded.emit();
this.dialogRef.close();
this.openSnackBar(false, 'Cliente creado exitosamente');
},
error => {
console.error('Error al realizar POST:', error);
this.openSnackBar(true, 'Error al crear la unidad organizativa: ' + error.error['hydra:description']);
}
);
}
@ -165,4 +208,11 @@ export class EditOrganizationalUnitComponent 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('Cliente creado exitosamente', 'Éxito');
}
}

View File

@ -0,0 +1,26 @@
mat-dialog-content {
display: flex;
justify-content: center;
align-items: center;
flex-direction: column;
}
table {
width: 80%;
margin: 20px 0;
}
.mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
}
.button-column {
display: flex;
flex-direction: column;
gap: 10px;
align-items: center;
}
button {
width: 200px;
}

View File

@ -0,0 +1,43 @@
<h1 mat-dialog-title>Propiedades unidad organizativa</h1>
<div mat-dialog-content>
<mat-tab-group dynamicHeight>
<mat-tab label="Datos generales">
<table mat-table [dataSource]="generalData" class="mat-elevation-z8">
<ng-container matColumnDef="property">
<th mat-header-cell *matHeaderCellDef> Propiedad </th>
<td mat-cell *matCellDef="let element"> {{ element.property }} </td>
</ng-container>
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef> Valor </th>
<td mat-cell *matCellDef="let element"> {{ element.value }} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</mat-tab>
<mat-tab [disabled]="data.data.type !== 'classroom'" label="Propiedades aula y de red">
<table mat-table [dataSource]="networkData" class="mat-elevation-z8">
<ng-container matColumnDef="property">
<th mat-header-cell *matHeaderCellDef> Propiedad </th>
<td mat-cell *matCellDef="let element"> {{ element.property }} </td>
</ng-container>
<ng-container matColumnDef="value">
<th mat-header-cell *matHeaderCellDef> Valor </th>
<td mat-cell *matCellDef="let element"> {{ element.value }} </td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
</table>
</mat-tab>
<mat-tab disabled label="Acciones">
<div class="button-column">
<button mat-flat-button color="primary" class="button-encender">Encender</button>
<button mat-flat-button color="accent" class="button-apagar">Apagar</button>
<button mat-flat-button color="warn" class="button-resetear">Resetear</button>
<button mat-flat-button class="button-otros-1">Otras acciones 1</button>
<button mat-flat-button class="button-otros-2">Otras acciones 2</button>
<button mat-flat-button class="button-otros-3">Otras acciones 3</button>
</div>
</mat-tab>
</mat-tab-group>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ShowOrganizationalUnitComponent } from './show-organizational-unit.component';
describe('ShowOrganizationalUnitComponent', () => {
let component: ShowOrganizationalUnitComponent;
let fixture: ComponentFixture<ShowOrganizationalUnitComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ShowOrganizationalUnitComponent]
})
.compileComponents();
fixture = TestBed.createComponent(ShowOrganizationalUnitComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,48 @@
import {Component, Inject} from '@angular/core';
import {MAT_DIALOG_DATA} from "@angular/material/dialog";
@Component({
selector: 'app-show-organizational-unit',
templateUrl: './show-organizational-unit.component.html',
styleUrl: './show-organizational-unit.component.css'
})
export class ShowOrganizationalUnitComponent {
displayedColumns: string[] = ['property', 'value'];
generalData = [
{ property: 'Nombre', value: this.data.data.name },
{ property: 'Uuid', value: this.data.data.uuid },
{ property: 'Comentarios', value: this.data.data.comments },
{ property: 'Tipo', value: this.data.data.type },
{ property: 'Unidad organizativa superior', value: this.data.data.parent ? this.data.data.parent.name : '-' },
{ property: 'Creado por', value: this.data.data.createdBy },
{ property: 'Creado el', value: this.data.data.createdAt }
];
networkData = [
{ property: 'Localización', value: this.data.data.location },
{ property: 'Proyector', value: this.data.data.projector ? 'Sí' : 'No' },
{ property: 'Pizzarra', value: this.data.data.board ? 'Sí' : 'No' },
{ property: 'Aforo', value: this.data.data.capacity },
{ property: 'Url servidor proxy', value: this.data.data.networkSettings ? this.data.data.networkSettings.proxy : '' },
{ property: 'IP DNS', value: this.data.data.networkSettings ? this.data.data.networkSettings.dns : '' },
{ property: 'Máscara de red', value: this.data.data.networkSettings ? this.data.data.networkSettings.mask : '' },
{ property: 'Router', value: this.data.data.networkSettings ? this.data.data.networkSettings.router : '' },
{ property: 'NTP', value: this.data.data.networkSettings ? this.data.data.networkSettings.ntp : '' },
{ property: 'Modo p2p', value: this.data.data.networkSettings ? this.data.data.networkSettings.p2pMode : '' },
{ property: 'Tiempo p2p', value: this.data.data.networkSettings ? this.data.data.networkSettings.p2pTime : '' },
{ property: 'IP multicast', value: this.data.data.networkSettings ? this.data.data.networkSettings.mcastIp : '' },
{ property: 'Modo multicast', value: this.data.data.networkSettings ? this.data.data.networkSettings.mcastMode : '' },
{ property: 'Puerto multicast', value: this.data.data.networkSettings ? this.data.data.networkSettings.mcastPort : '' },
{ property: 'Velocidad multicast', value: this.data.data.networkSettings ? this.data.data.networkSettings.mcastSpeed : '' },
{ property: 'Perfil hardware', value: this.data.data.networkSettings && this.data.data.networkSettings.hardwareProfile ? this.data.data.networkSettings.hardwareProfile.description : '' },
{ property: 'Menú', value: this.data.data.networkSettings && this.data.data.networkSettings.menu ? this.data.data.networkSettings.menu.description : '' }
];
constructor(
@Inject(MAT_DIALOG_DATA) public data: any // Inject data for edit mode
) {
console.log(this.data)
}
}

View File

@ -0,0 +1,40 @@
mat-content {
padding: 20px;
}
.item-content {
display: flex;
width: 100%;
padding: 10px;
}
.item-content mat-icon {
margin-right: 10px;
}
.tree-invisible {
display: none;
}
.tree ul,
.tree li {
margin-top: 0;
margin-bottom: 0;
list-style-type: none;
}
/*
* This padding sets alignment of the nested nodes.
*/
.tree .mat-nested-tree-node div[role=group] {
padding-left: 40px;
}
/*
* Padding for leaf nodes.
* Leaf nodes need to have padding so as to align with other non-leaf nodes
* under the same parent.
*/
.tree div[role=group] > .mat-tree-node {
padding-left: 40px;
}

View File

@ -0,0 +1,55 @@
<h1 mat-dialog-title>Visualizar arbol unidad Organizativa</h1>
<mat-dialog-content>
<mat-tree [dataSource]="dataSource" [treeControl]="treeControl" class="tree">
<mat-tree-node *matTreeNodeDef="let node" matTreeNodeToggle>
<mat-icon [ngSwitch]="node.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>
{{node.name}}
</mat-tree-node>
<!-- This is the tree node template for expandable nodes -->
<mat-nested-tree-node *matTreeNodeDef="let node; when: hasChild">
<div class="mat-tree-node">
<button mat-icon-button matTreeNodeToggle
[attr.aria-label]="'Toggle ' + node.name">
<mat-icon class="mat-icon-rtl-mirror">
{{treeControl.isExpanded(node) ? 'expand_more' : 'chevron_right'}}
</mat-icon>
</button>
<div class="item-content">
<mat-icon [ngSwitch]="node.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>
{{node.name}}
</div>
</div>
<!-- There is inline padding applied to this div using styles.
This padding value depends on the mat-icon-button width. -->
<div [class.tree-invisible]="!treeControl.isExpanded(node)"
role="group">
<ng-container matTreeNodeOutlet></ng-container>
<mat-list *ngIf="node.clients">
<mat-list-item *ngFor="let client of node.clients">
<mat-icon matListItemIcon>computer</mat-icon>
<span matListItemTitle>{{ client.name }}</span>
<span>{{ client.ip }} | {{ client.mac }}</span>
</mat-list-item>
</mat-list>
</div>
</mat-nested-tree-node>
</mat-tree>
</mat-dialog-content>
<mat-dialog-actions>
<button mat-button (click)="close()">Cerrar</button>
</mat-dialog-actions>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TreeViewComponent } from './tree-view.component';
describe('TreeViewComponent', () => {
let component: TreeViewComponent;
let fixture: ComponentFixture<TreeViewComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [TreeViewComponent]
})
.compileComponents();
fixture = TestBed.createComponent(TreeViewComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,70 @@
import {Component, Inject, OnInit} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {NestedTreeControl} from "@angular/cdk/tree";
import {MatTreeNestedDataSource} from "@angular/material/tree";
interface OrganizationalUnit {
id: number;
name: string;
type: string;
clients?: Client[];
children?: OrganizationalUnit[];
}
interface Client {
id: number;
name: string;
ip: string;
mac: string;
serialNumber: string;
}
@Component({
selector: 'app-tree-view',
templateUrl: './tree-view.component.html',
styleUrl: './tree-view.component.css'
})
export class TreeViewComponent implements OnInit {
treeControl = new NestedTreeControl<OrganizationalUnit>(node => node.children);
dataSource = new MatTreeNestedDataSource<OrganizationalUnit>();
constructor(
private dialogRef: MatDialogRef<TreeViewComponent>,
@Inject(MAT_DIALOG_DATA) public data: any
) {
}
ngOnInit() {
this.dataSource.data = [this.mapData(this.data.data)];
}
hasChild = (_: number, node: OrganizationalUnit) => (!!node.children && node.children.length > 0 || !!node.clients && node.clients.length > 0);
private mapData(data: any): OrganizationalUnit {
const mapClients = (clients: any[]): Client[] => {
console.log(clients)
return clients.map(client => ({
id: client.id,
name: client.name,
ip: client.ip,
mac: client.mac,
serialNumber: client.serialNumber,
}));
};
const mapChildren = (children: any[]): OrganizationalUnit[] => {
return children.map(child => this.mapData(child));
};
return {
id: data.id,
name: data.name,
type: data.type,
clients: data.clients ? mapClients(data.clients) : [],
children: data.children ? mapChildren(data.children) : []
};
}
close(): void {
this.dialogRef.close();
}
}

View File

@ -23,6 +23,5 @@
<div class="button-row">
<button mat-flat-button color="primary" type="submit" [disabled]="!loginObj.username || !loginObj.password">Iniciar sesión</button>
</div>
<div *ngIf="errorMessage" class="error-message">{{ errorMessage }}</div>
</form>
</div>

View File

@ -1,6 +1,7 @@
import { HttpClient } from '@angular/common/http';
import {Component, signal} from '@angular/core';
import { Router } from '@angular/router';
import {ToastrService} from "ngx-toastr";
@Component({
selector: 'app-login',
@ -15,7 +16,11 @@ export class LoginComponent {
errorMessage: string = '';
isLoading: boolean = false;
constructor(private http: HttpClient, private router: Router) { }
constructor(
private http: HttpClient,
private router: Router,
private toastService: ToastrService,
) { }
onLogin() {
this.errorMessage = '';
@ -44,20 +49,14 @@ export class LoginComponent {
localStorage.setItem('loginToken', res.token);
localStorage.setItem('refreshToken', res.refreshToken);
localStorage.setItem('username', this.loginObj.username);
this.openSnackBar(false, 'Bienvenido ' + this.loginObj.username);
this.router.navigateByUrl('/dashboard');
}
this.isLoading = false;
},
error: (err) => {
this.isLoading = false;
if (err.status === 401) {
this.errorMessage = 'Usuario o contraseña incorrectos';
} else {
this.errorMessage = 'Ha ocurrido un error. Por favor, inténtelo de nuevo.';
//BYPASS TO DASHBOARD
/* this.router.navigateByUrl('/dashboard'); */
}
this.openSnackBar(true, 'Error al iniciar sesion: ' + err.error.message);
}
});
}
@ -67,4 +66,11 @@ export class LoginComponent {
this.hide.set(!this.hide());
event.stopPropagation();
}
openSnackBar(isError: boolean, message: string) {
if (isError) {
this.toastService.error(message, 'Error');
} else
this.toastService.success(message, 'Éxito');
}
}

View File

@ -6,5 +6,8 @@ import { Component } from '@angular/core';
styleUrl: './admin.component.css'
})
export class AdminComponent {
}
export class UsersComponent {
}

View File

@ -14,3 +14,8 @@ table {
.header-container h1 {
margin: 0;
}
.mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
}

View File

@ -2,7 +2,7 @@
<h1>Gestión de roles</h1>
<button mat-flat-button color="primary" (click)="addUser()">+ Añadir</button>
</div>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8 demo-table">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>

View File

@ -1,10 +1,9 @@
.user-form .form-field {
display: block;
margin-bottom: 10px; /* Puedes ajustar el valor para cambiar la separación */
}
.checkbox-group label {
display: block;
margin-bottom: 8px; /* Ajusta este valor según necesites */
}
display: block;
margin-bottom: 10px; /* Puedes ajustar el valor para cambiar la separación */
}
.checkbox-group label {
display: block;
margin-bottom: 8px; /* Ajusta este valor según necesites */
}

View File

@ -2,6 +2,7 @@ import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { UserService } from '../users.service';
import {ToastrService} from "ngx-toastr";
interface UserGroup {
'@id': string;
@ -24,7 +25,8 @@ export class AddUserModalComponent implements OnInit {
public dialogRef: MatDialogRef<AddUserModalComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
private fb: FormBuilder,
private userService: UserService // Inyecta el servicio
private userService: UserService,
private toastService: ToastrService
) {
this.userForm = this.fb.group({
username: ['', Validators.required],
@ -41,7 +43,7 @@ export class AddUserModalComponent implements OnInit {
this.userService.getOrganizationalUnits().subscribe((data) => {
this.organizationalUnits = data['hydra:member'];
});
}
onNoClick(): void {
@ -64,12 +66,21 @@ export class AddUserModalComponent implements OnInit {
console.log('User added successfully:', response);
this.userAdded.emit();
this.dialogRef.close(this.userForm.value);
this.openSnackBar(false, 'Usuario creado correctamente')
},
error => {
console.error('Error adding user:', error);
this.openSnackBar(true, error.error['hydra:description']);
// Agregar alguna lógica para manejar el error en la interfaz de usuario
}
);
}
}
openSnackBar(isError: boolean, message: string) {
if (isError) {
this.toastService.error(' Error al eliminar la entidad: ' + message, 'Error');
} else
this.toastService.success(message, 'Éxito');
}
}

View File

@ -1,10 +1,16 @@
.user-form .form-field {
display: block;
margin-bottom: 10px; /* Puedes ajustar el valor para cambiar la separación */
}
.checkbox-group label {
display: block;
margin-bottom: 8px; /* Ajusta este valor según necesites */
}
display: block;
margin-bottom: 10px; /* Puedes ajustar el valor para cambiar la separación */
}
.checkbox-group label {
display: block;
margin-bottom: 8px; /* Ajusta este valor según necesites */
}
.loading-container{
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}

View File

@ -1,5 +1,6 @@
<h1 mat-dialog-title>Editar Usuario</h1>
<div mat-dialog-content>
<mat-spinner *ngIf="loading" class="loading-container"></mat-spinner>
<div *ngIf="!loading" mat-dialog-content>
<form [formGroup]="userForm" class="user-form">
<mat-form-field class="form-field">
<mat-label>Nombre de usuario</mat-label>

View File

@ -2,6 +2,7 @@ import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { UserService } from '../users.service';
import {ToastrService} from "ngx-toastr";
@Component({
selector: 'app-edit-user-modal',
@ -12,28 +13,34 @@ export class EditUserModalComponent implements OnInit {@Output() userEdited = ne
userForm: FormGroup;
userGroups: any[] = [];
organizationalUnits: any[] = [];
loading:boolean = false;
constructor(
public dialogRef: MatDialogRef<EditUserModalComponent>,
@Inject(MAT_DIALOG_DATA) public data: any,
private fb: FormBuilder,
private userService: UserService // Inyecta el servicio
private userService: UserService, // Inyecta el servicio
private toastService: ToastrService
) {
this.userForm = this.fb.group({
username: [this.data.user.username],
password: [''],
password: [null],
userGroups: [this.data.user.userGroups.map((group: { '@id': any; }) => group['@id'])],
allowedOrganizationalUnits: [this.data.user.allowedOrganizationalUnits.map((unit: { '@id': any; }) => unit['@id'])]
});
console.log(this.userForm.value)
}
ngOnInit(): void {
this.loading = true
this.userService.getUserGroups().subscribe((data) => {
this.userGroups = data['hydra:member'];
});
this.userService.getOrganizationalUnits().subscribe((data) => {
this.organizationalUnits = data['hydra:member'];
this.organizationalUnits = data['hydra:member'].filter((item: any) => item.type === 'organizational-unit');
this.loading = false
}, (error) => {
console.error('Error fetching organizational units', error);
this.loading = false
});
}
@ -56,11 +63,20 @@ export class EditUserModalComponent implements OnInit {@Output() userEdited = ne
console.log('User added successfully:', response);
this.userEdited.emit();
this.dialogRef.close(this.userForm.value);
this.openSnackBar(false, 'Usuario actualizado correctamente')
},
error => {
console.error('Error adding user:', error);
this.openSnackBar(true, error.message);
// Agregar alguna lógica para manejar el error en la interfaz de usuario
}
);
}
openSnackBar(isError: boolean, message: string) {
if (isError) {
this.toastService.error(' Error al eliminar la entidad: ' + message, 'Error');
} else
this.toastService.success(message, 'Éxito');
}
}

View File

@ -14,3 +14,7 @@ table {
.header-container h1 {
margin: 0;
}
.mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
}

View File

@ -2,7 +2,7 @@
<h1>Gestión de usuarios</h1>
<button mat-flat-button color="primary" (click)="addUser()">+ Añadir</button>
</div>
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8 demo-table">
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>

View File

@ -51,7 +51,7 @@ export class UsersComponent implements OnInit {
}
addUser() {
const dialogRef = this.dialog.open(AddUserModalComponent);
const dialogRef = this.dialog.open(AddUserModalComponent, { width: '500px' });
dialogRef.componentInstance.userAdded.subscribe(() => {
this.loadUsers();
@ -63,7 +63,7 @@ export class UsersComponent implements OnInit {
// Implementar la lógica de edición
const dialogRef = this.dialog.open(EditUserModalComponent, {
data: { user: user },
width: '400px'
width: '500px'
});
dialogRef.componentInstance.userEdited.subscribe(() => {
this.loadUsers();

View File

@ -60,7 +60,7 @@ export class UserService {
}
getOrganizationalUnits(): Observable<any> {
return this.http.get<any>(`${this.apiUrl}/organizational-units?page=1&itemsPerPage=30`);
return this.http.get<any>(`${this.apiUrl}/organizational-units?page=1&itemsPerPage=10000`);
}
}

110
package-lock.json generated
View File

@ -2,5 +2,113 @@
"name": "oggui",
"lockfileVersion": 3,
"requires": true,
"packages": {}
"packages": {
"": {
"dependencies": {
"@angular/animations": "^18.1.0",
"ngx-toastr": "^19.0.0"
}
},
"node_modules/@angular/animations": {
"version": "18.1.0",
"resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.1.0.tgz",
"integrity": "sha512-K0BhvZ/SIVoGXZVuh1KOJDdgcGlHfFGMGrs58utndndAb+gYXReMfz4GR5cQs2OObH6TKmIOY2EH7Og1CY2tsw==",
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
},
"peerDependencies": {
"@angular/core": "18.1.0"
}
},
"node_modules/@angular/common": {
"version": "18.1.0",
"resolved": "https://registry.npmjs.org/@angular/common/-/common-18.1.0.tgz",
"integrity": "sha512-noHDLarQSCZZh7hyNd0HR61Fut+q4QCVq9qc/jKPglfbV/6nPujQSmSpT+rNJlNuBOrCLuvH/CNBNbiqii+x3g==",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
},
"peerDependencies": {
"@angular/core": "18.1.0",
"rxjs": "^6.5.3 || ^7.4.0"
}
},
"node_modules/@angular/core": {
"version": "18.1.0",
"resolved": "https://registry.npmjs.org/@angular/core/-/core-18.1.0.tgz",
"integrity": "sha512-/57/s7CD/0CwlN+3FlhVmx7ypCWXjKi5UKtnlBAUg0D1denIf6ADxwTHFZABYZcYBqOTJgeQUtUw9u/A+0CIlg==",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
},
"peerDependencies": {
"rxjs": "^6.5.3 || ^7.4.0",
"zone.js": "~0.14.0"
}
},
"node_modules/@angular/platform-browser": {
"version": "18.1.0",
"resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.1.0.tgz",
"integrity": "sha512-jCmxthiI4Zef54crckNht60xwfIsuciGeyZvb7SsXna2maLW9fA4uz1VhZqIWTiBnHwNynVlyfBX3/jBD7S9+g==",
"peer": true,
"dependencies": {
"tslib": "^2.3.0"
},
"engines": {
"node": "^18.19.1 || ^20.11.1 || >=22.0.0"
},
"peerDependencies": {
"@angular/animations": "18.1.0",
"@angular/common": "18.1.0",
"@angular/core": "18.1.0"
},
"peerDependenciesMeta": {
"@angular/animations": {
"optional": true
}
}
},
"node_modules/ngx-toastr": {
"version": "19.0.0",
"resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-19.0.0.tgz",
"integrity": "sha512-6pTnktwwWD+kx342wuMOWB4+bkyX9221pAgGz3SHOJH0/MI9erLucS8PeeJDFwbUYyh75nQ6AzVtolgHxi52dQ==",
"dependencies": {
"tslib": "^2.3.0"
},
"peerDependencies": {
"@angular/common": ">=16.0.0-0",
"@angular/core": ">=16.0.0-0",
"@angular/platform-browser": ">=16.0.0-0"
}
},
"node_modules/rxjs": {
"version": "7.8.1",
"resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
"integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
"peer": true,
"dependencies": {
"tslib": "^2.1.0"
}
},
"node_modules/tslib": {
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
},
"node_modules/zone.js": {
"version": "0.14.7",
"resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.14.7.tgz",
"integrity": "sha512-0w6DGkX2BPuiK/NLf+4A8FLE43QwBfuqz2dVgi/40Rj1WmqUskCqj329O/pwrqFJLG5X8wkeG2RhIAro441xtg==",
"peer": true
}
}
}

6
package.json 100644
View File

@ -0,0 +1,6 @@
{
"dependencies": {
"@angular/animations": "^18.1.0",
"ngx-toastr": "^19.0.0"
}
}