refs #486. Some several improvements. Change in UX, and added new client-classroom view

pull/4/head
Manuel Aranda Rosales 2024-07-10 16:13:29 +02:00
parent 876ab85a8e
commit a1e2b7aec1
43 changed files with 843 additions and 427 deletions

View File

@ -2,6 +2,7 @@
height: 100%;
}
.content {
padding: 16px;
}

View File

@ -11,8 +11,8 @@ import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
import { CustomInterceptor } from './services/custom.interceptor';
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
import {MatToolbarModule} from '@angular/material/toolbar';
import {MatIconModule} from '@angular/material/icon';
import {MatToolbarModule} from '@angular/material/toolbar';
import {MatIconModule} from '@angular/material/icon';
import { MatButtonModule } from '@angular/material/button';
import { MatSidenavModule } from '@angular/material/sidenav';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
@ -46,6 +46,12 @@ import { ClassroomViewComponent } from './components/groups/classroom-view/class
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 { 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';
@NgModule({ declarations: [
AppComponent,
@ -70,7 +76,9 @@ import {MatMenu, MatMenuItem, MatMenuTrigger} from "@angular/material/menu";
DeleteModalComponent,
EditOrganizationalUnitComponent,
EditClientComponent,
ClassroomViewComponent
ClassroomViewComponent,
ClientViewComponent,
DeleteGroupsModalComponent
],
bootstrap: [AppComponent],
imports: [BrowserModule,
@ -92,7 +100,7 @@ import {MatMenu, MatMenuItem, MatMenuTrigger} from "@angular/material/menu";
MatSelectModule,
MatDividerModule,
MatStepperModule,
MatSlideToggleModule, MatMenu, MatMenuTrigger, MatMenuItem],
MatSlideToggleModule, MatMenu, MatMenuTrigger, MatMenuItem, MatAutocomplete, MatChipListbox, MatChipOption, MatChipSet, MatChip, MatProgressSpinner, MatTabGroup, MatTab, MatTooltip],
providers: [
{
provide: HTTP_INTERCEPTORS,

View File

@ -1,7 +1,9 @@
.classroom {
display: flex;
flex-direction: column;
align-items: center;
flex-wrap: wrap;
gap: 10px;
height: 57vh;
overflow-y: auto;
}
.classroom-group {
@ -9,12 +11,11 @@
background-color: #fafafa;
}
.organizational-unit-name {
font-weight: bold;
font-size: 18px;
margin-bottom: 10px;
mat-card-title {
display: flex;
justify-content: space-between;
margin: 10px;
}
.client-row {
display: flex;
justify-content: center;
@ -27,18 +28,20 @@
}
.client-box {
width: 100px;
height: 100px;
width: 100%;
height: auto;
background-color: lightblue;
display: flex;
justify-content: center;
align-items: center;
border: 1px solid #000;
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.1);
border-radius: 5px;
cursor: pointer;
}
.client-box:hover {
box-shadow: 2px 2px 5px rgba(0, 0, 0, 0.2);
scale: 1.1;
transition: 0.3s ease-in-out;
}
.client-box p {
margin: 0;
}

View File

@ -1,12 +1,26 @@
<div class="classroom">
<mat-card *ngFor="let group of groupedClients" class="classroom-group">
<p class="organizational-unit-name">Disposición {{ group.organizationalUnitName }}</p>
<mat-card-title>Clientes dentro de: {{ group.organizationalUnitName }}</mat-card-title>
<div *ngFor="let row of group.clientRows" class="client-row">
<div class="client-container" *ngFor="let client of row">
<div class="client-box" (click)="handleClientClick(client)">
<p>{{ client.name }}</p>
<mat-card appearance="outlined">
<mat-card-header>
<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-card-content>
</mat-card>
</div>
</div>
</div>
</mat-card>
</div>
</div>

View File

@ -1,6 +1,12 @@
// src/app/classroom-view/classroom-view.component.ts
import { Component, Input, OnInit } from '@angular/core';
import {
ChangePasswordModalComponent
} from "../../pages/admin/users/users/change-password-modal/change-password-modal.component";
import {MatDialog} from "@angular/material/dialog";
import {CreateClientComponent} from "../clients/create-client/create-client.component";
import {ClientViewComponent} from "../client-view/client-view.component";
interface GroupedClients {
organizationalUnitName: string;
@ -12,19 +18,19 @@ interface GroupedClients {
templateUrl: './classroom-view.component.html',
styleUrls: ['./classroom-view.component.css']
})
export class ClassroomViewComponent implements OnInit {
export class ClassroomViewComponent implements OnInit {
@Input() clients: any[] = [];
@Input() pcInTable: number = 3;
groupedClients: GroupedClients[] = [];
constructor() {}
constructor(public dialog: MatDialog) {}
ngOnInit(): void {
this.groupClientsByOrganizationalUnit();
}
ngOnChanges(): void {
this.groupClientsByOrganizationalUnit();
this.groupClientsByOrganizationalUnit();
}
groupClientsByOrganizationalUnit(): void {
@ -53,5 +59,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'});
}
}

View File

@ -0,0 +1,41 @@
.mat-dialog-content {
padding: 20px;
}
.button-column {
display: flex;
flex-direction: column;
gap: 20px;
margin: 20px;
padding: 20px;
}
.button-encender {
background-color: #3f51b5; /* Azul */
color: white;
}
.button-apagar {
background-color: #e91e63; /* Rosa */
color: white;
}
.button-resetear {
background-color: #f44336; /* Rojo */
color: white;
}
.button-otros-1 {
background-color: #4caf50; /* Verde */
color: white;
}
.button-otros-2 {
background-color: #ff9800; /* Naranja */
color: white;
}
.button-otros-3 {
background-color: #9c27b0; /* Púrpura */
color: white;
}

View File

@ -0,0 +1,40 @@
<h1 mat-dialog-title>Propiedades cliente</h1>
<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>
</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>
</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>
</div>
</mat-tab>
</mat-tab-group>
</div>

View File

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

View File

@ -0,0 +1,14 @@
import {Component, Inject} from '@angular/core';
import {MAT_DIALOG_DATA} from "@angular/material/dialog";
@Component({
selector: 'app-client-view',
templateUrl: './client-view.component.html',
styleUrl: './client-view.component.css'
})
export class ClientViewComponent {
constructor(
@Inject(MAT_DIALOG_DATA) public data: any // Inject data for edit mode
) {
}
}

View File

@ -1,39 +1,48 @@
h1 {
text-align: center;
font-family: 'Roboto', sans-serif;
font-weight: 400;
color: #3f51b5;
margin-bottom: 20px;
}
.network-form {
display: flex;
flex-direction: column;
gap: 15px;
}
.form-field {
width: 100%;
margin-top: 10px;
}
.mat-dialog-content {
padding: 20px;
}
.mat-dialog-actions {
display: flex;
justify-content: flex-end;
padding: 10px 20px;
}
button {
text-transform: none;
font-size: 16px;
font-weight: 500;
}
.mat-slide-toggle {
margin-top: 20px;
}
text-align: center;
font-family: 'Roboto', sans-serif;
font-weight: 400;
color: #3f51b5;
margin-bottom: 20px;
}
.network-form {
display: flex;
flex-direction: column;
gap: 15px;
}
.form-field {
width: 100%;
margin-top: 10px;
}
.mat-dialog-content {
padding: 50px;
}
.mat-dialog-actions {
display: flex;
justify-content: flex-end;
padding: 10px 20px;
}
button {
text-transform: none;
font-size: 16px;
font-weight: 500;
}
.mat-slide-toggle {
margin-top: 20px;
}
mat-option .unit-name {
display: block;
}
mat-option .unit-path {
display: block;
font-size: 0.8em;
color: gray;
}

View File

@ -1,10 +1,13 @@
<h1 mat-dialog-title>Añadir Cliente</h1>
<div mat-dialog-content>
<div class="mat-dialog-content">
<form [formGroup]="clientForm" class="client-form">
<mat-form-field class="form-field">
<mat-label>Padre</mat-label>
<mat-select formControlName="organizationalUnit">
<mat-option *ngFor="let unit of parentUnits" [value]="unit['@id']">{{ unit.name }}</mat-option>
<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">
@ -17,26 +20,26 @@
</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-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-form-field>
<mat-form-field class="form-field">
<mat-label>MAC</mat-label>
<mat-hint>Ejemplo: 00:11:22:33:44:55</mat-hint>
<input matInput formControlName="mac" >
<mat-error>Formato de MAC inválido. Ejemplo válido: 00:11:22:33:44:55</mat-error>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Dirección IP</mat-label>
<mat-hint>Ejemplo: 127.0.0.1</mat-hint>
<input matInput formControlName="ip">
<mat-error>Formato de dirección IP inválido. Ejemplo válido: 127.0.0.1</mat-error>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Estado</mat-label>
<input matInput formControlName="status">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Menú URL</mat-label>
<input matInput formControlName="menu" type="url">
@ -44,7 +47,9 @@
</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.name }} </mat-option>
</mat-select>
<mat-error>Formato de URL inválido.</mat-error>
</mat-form-field>
</form>

View File

@ -1,7 +1,8 @@
import { HttpClient } from '@angular/common/http';
import { Component, OnInit } from '@angular/core';
import {Component, Inject, OnInit} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialogRef } from '@angular/material/dialog';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {MatSnackBar} from "@angular/material/snack-bar";
@Component({
selector: 'app-create-client',
@ -11,31 +12,36 @@ import { MatDialogRef } from '@angular/material/dialog';
export class CreateClientComponent implements OnInit {
clientForm!: FormGroup;
parentUnits: any[] = []; // Array to store parent units fetched from API
hardwareProfiles: any[] = []; // Array to store hardware profiles fetched from API
private errorForm: boolean = false;
constructor(
private fb: FormBuilder,
private dialogRef: MatDialogRef<CreateClientComponent>,
private http: HttpClient
) { }
private http: HttpClient,
private snackBar: MatSnackBar,
@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.clientForm = this.fb.group({
organizationalUnit: [''],
organizationalUnit: [this.data.organizationalUnit ? this.data.organizationalUnit['@id'] : null, Validators.required],
name: ['', Validators.required],
serialNumber: [''],
netiface: "eth0",
netDriver: "e1000e",
mac: "00:11:22:33:44:55",
ip: "127.0.0.1",
status: "test",
menu: null,
hardwareProfile: null,
serialNumber: ['', Validators.required],
netiface: null,
netDriver: null,
mac: ['', Validators.required],
ip: ['', Validators.required],
menu: [this.data.organizationalUnit && this.data.organizationalUnit.menu ? this.data.organizationalUnit.menu['@id'] : null],
hardwareProfile: [this.data.organizationalUnit && this.data.organizationalUnit.hardwareProfile ? this.data.organizationalUnit.hardwareProfile['@id'] : null],
});
}
loadParentUnits() {
const url = 'http://127.0.0.1:8080/organizational-units?page=1&itemsPerPage=30';
const url = 'http://127.0.0.1:8080/organizational-units?page=1&itemsPerPage=10000';
this.http.get<any>(url).subscribe(
response => {
@ -47,15 +53,31 @@ 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
const formData = this.clientForm.value;
this.http.post('http://127.0.0.1:8080/clients', formData).subscribe(
response => {
this.dialogRef.close(response);
},
this.openSnackBar(false, 'Cliente creado exitosamente'); },
error => {
console.error('Error during POST:', error);
this.errorForm = true
this.openSnackBar(true, 'Error al crear el cliente: ' + error.error['hydra:description']);
}
);
}
@ -64,4 +86,10 @@ export class CreateClientComponent implements OnInit {
onNoClick(): void {
this.dialogRef.close();
}
openSnackBar(isError: boolean, message: string) {
this.snackBar.open(message, 'Cerrar', {
panelClass: isError ? ['snackbar-error'] : ['snackbar-success']
});
}
}

View File

@ -1,39 +1,38 @@
h1 {
text-align: center;
font-family: 'Roboto', sans-serif;
font-weight: 400;
color: #3f51b5;
margin-bottom: 20px;
}
.network-form {
display: flex;
flex-direction: column;
gap: 15px;
}
.form-field {
width: 100%;
margin-top: 10px;
}
.mat-dialog-content {
padding: 20px;
}
.mat-dialog-actions {
display: flex;
justify-content: flex-end;
padding: 10px 20px;
}
button {
text-transform: none;
font-size: 16px;
font-weight: 500;
}
.mat-slide-toggle {
margin-top: 20px;
}
text-align: center;
font-family: 'Roboto', sans-serif;
font-weight: 400;
color: #3f51b5;
margin-bottom: 20px;
}
.network-form {
display: flex;
flex-direction: column;
gap: 15px;
}
.form-field {
width: 100%;
margin-top: 10px;
}
.mat-dialog-content {
padding: 50px;
}
.mat-dialog-actions {
display: flex;
justify-content: flex-end;
padding: 10px 20px;
}
button {
text-transform: none;
font-size: 16px;
font-weight: 500;
}
.mat-slide-toggle {
margin-top: 20px;
}

View File

@ -1,5 +1,5 @@
<h1 mat-dialog-title>Editar Cliente</h1>
<div mat-dialog-content>
<div class="mat-dialog-content">
<form [formGroup]="clientForm" class="client-form">
<mat-form-field class="form-field">
<mat-label>Padre</mat-label>
@ -11,6 +11,40 @@
<mat-label>Nombre</mat-label>
<input matInput formControlName="name" >
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Número de Serie</mat-label>
<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-form-field>
<mat-form-field class="form-field">
<mat-label>Controlador de Red</mat-label>
<input matInput formControlName="netDriver">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>MAC</mat-label>
<input matInput formControlName="mac" >
<mat-error>Formato de MAC inválido. Ejemplo válido: 00:11:22:33:44:55</mat-error>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Dirección IP</mat-label>
<input matInput formControlName="ip">
<mat-error>Formato de dirección IP inválido. Ejemplo válido: 127.0.0.1</mat-error>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Menú URL</mat-label>
<input matInput formControlName="menu" type="url">
<mat-error>Formato de URL inválido.</mat-error>
</mat-form-field>
<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-select>
<mat-error>Formato de URL inválido.</mat-error>
</mat-form-field>
</form>
</div>
<div mat-dialog-actions>

View File

@ -12,6 +12,7 @@ import { CreateClientComponent } from '../create-client/create-client.component'
export class EditClientComponent {
clientForm!: FormGroup;
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
constructor(
@ -19,7 +20,6 @@ export class EditClientComponent {
private dialogRef: MatDialogRef<CreateClientComponent>,
private http: HttpClient,
@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
if (this.isEditMode) {
@ -29,22 +29,22 @@ export class EditClientComponent {
ngOnInit(): void {
this.loadParentUnits(); // Load parent units when component initializes
this.loadHardwareProfiles(); // Load hardware profiles when component initializes
this.clientForm = this.fb.group({
organizationalUnit: [''],
organizationalUnit: [null,Validators.required],
name: ['', Validators.required],
serialNumber: [''],
netiface: "eth0",
netDriver: "e1000e",
mac: "00:11:22:33:44:55",
ip: "127.0.0.1",
status: "test",
serialNumber: null,
netiface: null,
netDriver: null,
mac: null,
ip: null,
menu: null,
hardwareProfile: null,
});
}
loadParentUnits() {
const url = 'http://127.0.0.1:8080/organizational-units?page=1&itemsPerPage=30';
const url = 'http://127.0.0.1:8080/organizational-units?page=1&itemsPerPage=10000';
this.http.get<any>(url).subscribe(
response => {
@ -56,6 +56,19 @@ 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'];
},
error => {
console.error('Error fetching hardware profiles:', error);
}
);
}
loadData(uuid: string) {
const url = `http://127.0.0.1:8080/clients/${uuid}`;
@ -63,7 +76,13 @@ export class EditClientComponent {
data => {
this.clientForm.patchValue({
name: data.name,
organizationalUnit: data.organizationalUnit ? data.organizationalUnit['@id'] : ''
ip: data.ip,
mac: data.mac,
netiface: data.netiface,
netDriver: data.netDriver,
serialNumber: data.serialNumber,
hardwareProfile: data.hardwareProfile ? data.hardwareProfile['@id'] : null,
organizationalUnit: data.organizationalUnit ? data.organizationalUnit['@id'] : null
});
});
}

View File

@ -9,24 +9,16 @@ import { UnidadOrganizativa } from './model';
})
export class DataService {
private apiUrl = 'http://127.0.0.1:8080/organizational-units?page=1&itemsPerPage=30';
private apiUrl = 'http://127.0.0.1:8080/organizational-units?page=1&itemsPerPage=1000';
private clientsUrl = 'http://127.0.0.1:8080/clients?page=1&itemsPerPage=30';
constructor(private http: HttpClient) {}
getUnidadesOrganizativas(): Observable<UnidadOrganizativa[]> {
return this.http.get<any>(this.apiUrl).pipe(
getOrganizationalUnits(): Observable<UnidadOrganizativa[]> {
return this.http.get<any>(`${this.apiUrl}&type=organizational-unit`).pipe(
map(response => {
if (response['hydra:member'] && Array.isArray(response['hydra:member'])) {
return response['hydra:member']
.filter((unidad: any) => unidad.type === 'organizational-unit')
.map((unidad: any) => ({
id: unidad.id,
nombre: unidad.name,
uuid: unidad.uuid,
type: unidad.type,
aulas: []
}));
} else {
throw new Error('Unexpected response format');
}
@ -38,11 +30,11 @@ export class DataService {
);
}
getChildren(uuid: string): Observable<any[]> {
return this.http.get<any>(`${this.apiUrl}&parent=${uuid}`).pipe(
getChildren(id: string): Observable<any[]> {
return this.http.get<any>(`${this.apiUrl}&parent.id=${id}`).pipe(
map(response => {
if (response['hydra:member'] && Array.isArray(response['hydra:member'])) {
return response['hydra:member'].filter((element: any) => element.parent && element.parent['@id'] === `/organizational-units/${uuid}`);
return response['hydra:member']
} else {
throw new Error('Unexpected response format');
}
@ -54,11 +46,11 @@ export class DataService {
);
}
getClients(uuid: string): Observable<any[]> {
return this.http.get<any>(this.clientsUrl).pipe(
getClients(id: string): Observable<any[]> {
return this.http.get<any>(`${this.clientsUrl}&organizationalUnit.id=${id}`).pipe(
map(response => {
if (response['hydra:member'] && Array.isArray(response['hydra:member'])) {
return response['hydra:member'].filter((client: any) => client.organizationalUnit && client.organizationalUnit['@id'] === `/organizational-units/${uuid}`);
return response['hydra:member']
} else {
throw new Error('Unexpected response format');
}
@ -82,6 +74,16 @@ export class DataService {
);
}
changeParent(uuid: string): Observable<void> {
const url = `http://127.0.0.1:8080/organizational-units/${uuid}/change-parent`;
// @ts-ignore
return this.http.post<void>(url).pipe(
catchError(error => {
console.error('Error deleting element', error);
return throwError(error);
})
);
}
}

View File

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

View File

@ -0,0 +1,30 @@
import { Component, Inject } from '@angular/core';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
@Component({
selector: 'app-delete-confirm-dialog',
template: `
<h1 mat-dialog-title>Eliminar</h1>
<div mat-dialog-content>
<p>¿Quiere borrar los clientes situados en {{data.name}} o quiere resituarlos en el nivel superior?</p>
</div>
<div mat-dialog-actions>
<button mat-button (click)="deleteClick()">Borrar todos los clientes</button>
<button mat-button (click)="changeClick()">Resituar </button>
</div>
`
})
export class DeleteGroupsModalComponent {
constructor(
public dialogRef: MatDialogRef<DeleteGroupsModalComponent>,
@Inject(MAT_DIALOG_DATA) public data: { name: string }
) {}
deleteClick(): void {
this.dialogRef.close('delete');
}
changeClick(): void {
this.dialogRef.close('change');
}
}

View File

@ -8,13 +8,24 @@
margin: 10px;
}
.header-container {
height: 100px;
}
.unidad-card, .elements-card {
flex: 1 1 45%;
background-color: #fafafa;
max-height: 300px;
max-height: 400px;
}
.element-content {
overflow-y: auto;
}
.title {
margin-left: 10px;
}
.details-card, .classroom-view {
flex: 1 1 25%;
}
@ -60,6 +71,10 @@ button {
width: 100%;
}
.item-content mat-icon {
margin-right: 10px;
}
.clickable-item:hover {
cursor: pointer;
}
@ -71,6 +86,7 @@ button {
.actions {
display: flex;
margin-left: auto;
align-self: center;
}
.actions mat-icon {
@ -81,4 +97,16 @@ button {
.actions mat-icon:hover {
color: #212121;
}
}
.empty-list {
display: flex;
justify-content: center;
align-items: center;
height: 100%;
}
mat-spinner {
margin: 0 auto;
align-self: center;
}

View File

@ -1,24 +1,41 @@
<h2>Crear</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>
<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>
</div>
</div>
<mat-divider></mat-divider>
<h2>Grupos</h2>
<div class="groupLists-container">
<mat-card class="card unidad-card">
<mat-card-title>Unidad organizativa</mat-card-title>
<mat-card-content>
<mat-list role="list">
<mat-list-item *ngFor="let unidad of unidadesOrganizativas"
[ngClass]="{'selected-item': unidad === selectedUnidad, 'clickable-item': true}">
<mat-list >
<mat-list-item *ngFor="let unidad of organizationalUnits"
[ngClass]="{'selected-item': unidad === selectedUnidad, 'clickable-item': true}" (click)="onSelectUnidad(unidad)">
<div class="item-content">
<span (click)="onSelectUnidad(unidad)">
<mat-icon>apartment</mat-icon>
{{ unidad.nombre }}
</span>
<mat-icon>apartment</mat-icon>
{{ unidad.name }}
<span class="actions">
<mat-icon class="edit-icon" (click)="onEditClick(unidad.type, unidad.uuid)">edit</mat-icon>
<mat-icon
class="edit-icon"
(click)="onEditClick(unidad.type, unidad.uuid)"
#tooltip="matTooltip"
matTooltip="Editar unidad organizativa"
matTooltipHideDelay="0">edit
</mat-icon>
<mat-icon
class="edit-icon"
(click)="addOU(unidad)"
#tooltip="matTooltip"
matTooltip="Crear unidad organizativa interna"
matTooltipHideDelay="0">add_home_work
</mat-icon>
<mat-icon
class="edit-icon" (click)="addClient(unidad)"
#tooltip="matTooltip"
matTooltip="Crear cliente en esta unidad organizativa"
matTooltipHideDelay="0">devices
</mat-icon>
</span>
</div>
</mat-list-item>
@ -29,7 +46,7 @@
<mat-card class="card elements-card">
<mat-card-title>
<div class="title-with-breadcrumb">
<span>Elementos</span>
<span>Elementos internos</span>
<mat-card-subtitle>
<ng-container *ngFor="let crumb of breadcrumb; let i = index">
<a (click)="navigateToBreadcrumb(i)">{{ crumb }}</a>
@ -38,19 +55,28 @@
</mat-card-subtitle>
</div>
</mat-card-title>
<mat-card-content>
<mat-list role="list">
<mat-list-item *ngFor="let child of children" [ngClass]="{'clickable-item': true}">
<mat-card-content class="element-content">
<mat-spinner *ngIf="loading"></mat-spinner>
<mat-list>
<div *ngIf="children.length === 0" class="empty-list">
<mat-icon>info</mat-icon>
<span>No hay elementos internos</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">
<span (click)="onSelectChild(child)">
<ng-container [ngSwitch]="getIcon(child.type)">
<!-- Iconos aquí -->
</ng-container>
{{ child.name || child.nombre }}
</span>
<mat-icon [ngSwitch]="child.type">
<ng-container *ngSwitchCase="'organizational-unit'">apartment</ng-container>
<ng-container *ngSwitchCase="'classrooms-group'">groups</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}}
<span class="actions">
<mat-icon class="edit-icon" (click)="onEditClick(child.type, child.uuid)">edit</mat-icon>
<mat-icon class="delete-icon" (click)="onDeleteClick(child.uuid, child.name || child.nombre, child.type)">delete</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>
</span>
</div>
</mat-list-item>
@ -58,19 +84,5 @@
</mat-card-content>
</mat-card>
<mat-card class="card details-card">
<mat-card-title>Detalles del elemento</mat-card-title>
<mat-card-content *ngIf="selectedDetail">
<p><strong>Nombre:</strong> {{ selectedDetail.name || selectedDetail.nombre }}</p>
<p><strong>Tipo:</strong> {{ selectedDetail.type }}</p>
<p><strong>ID:</strong> {{ selectedDetail.id }}</p>
<p *ngIf="selectedDetail.uuid"><strong>UUID:</strong> {{ selectedDetail.uuid }}</p>
<!-- Aquí puedes agregar más detalles específicos del elemento -->
</mat-card-content>
<mat-card-content *ngIf="!selectedDetail">
<p>Selecciona un elemento para ver sus detalles.</p>
</mat-card-content>
</mat-card>
<app-classroom-view class="card classroom-view" [clients]="clientsData" [pcInTable]="3"></app-classroom-view>
</div>
<app-classroom-view class="card classroom-view" [clients]="clientsData" [pcInTable]="5"></app-classroom-view>
</div>

View File

@ -7,6 +7,7 @@ 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";
@Component({
selector: 'app-groups',
@ -14,21 +15,19 @@ import { EditClientComponent } from './clients/edit-client/edit-client.component
styleUrls: ['./groups.component.css']
})
export class GroupsComponent implements OnInit {
unidadesOrganizativas: UnidadOrganizativa[] = [];
organizationalUnits: UnidadOrganizativa[] = [];
selectedUnidad: UnidadOrganizativa | null = null;
selectedDetail: any | null = null; // Nueva variable para el detalle del elemento seleccionado
children: any[] = [];
breadcrumb: string[] = [];
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) {}
ngOnInit(): void {
this.dataService.getUnidadesOrganizativas().subscribe(
data => this.unidadesOrganizativas = data,
this.dataService.getOrganizationalUnits().subscribe(
data => this.organizationalUnits = data,
error => console.error('Error fetching unidades organizativas', error)
);
}
@ -36,17 +35,17 @@ constructor(private dataService: DataService, public dialog: MatDialog) {}
onSelectUnidad(unidad: UnidadOrganizativa): void {
this.selectedUnidad = unidad;
this.selectedDetail = unidad; // Mostrar detalles de la unidad seleccionada
this.breadcrumb = [unidad.nombre];
this.breadcrumb = [unidad.name];
this.breadcrumbData = [unidad];
this.loadChildrenAndClients(unidad.uuid);
this.loadChildrenAndClients(unidad.id);
}
onSelectChild(child: any): void {
this.selectedDetail = child; // Mostrar detalles del niño seleccionado
if (child.type !== 'client' && child.uuid && child.id) {
this.breadcrumb.push(child.name || child.nombre);
this.breadcrumb.push(child.name || child.name);
this.breadcrumbData.push(child);
this.loadChildrenAndClients(child.uuid);
this.loadChildrenAndClients(child.id);
}
}
@ -57,25 +56,25 @@ constructor(private dataService: DataService, public dialog: MatDialog) {}
if (target.type === 'client') {
this.selectedDetail = target;
} else {
this.loadChildrenAndClients(target.uuid);
this.loadChildrenAndClients(target.id);
}
}
loadChildrenAndClients(uuid: string): void {
this.dataService.getChildren(uuid).subscribe(
loadChildrenAndClients(id: string): void {
this.loading = true
this.dataService.getChildren(id).subscribe(
childrenData => {
console.log('Children data:', childrenData);
this.dataService.getClients(uuid).subscribe(
this.dataService.getClients(id).subscribe(
clientsData => {
this.clientsData = clientsData; // Almacenar clientsData para pasarlo al componente hijo
const newChildren = [...childrenData, ...clientsData];
if (newChildren.length > 0) {
this.children = newChildren;
} else {
this.children = []; // Limpiar card2 cuando no hay elementos
this.breadcrumb.pop(); // Revertir breadcrumb solo si no hay elementos
// Si deseas que la unidad organizativa se limpie completamente, descomenta la línea siguiente:
// this.selectedUnidad = null;
}
@ -92,79 +91,97 @@ constructor(private dataService: DataService, public dialog: MatDialog) {}
this.children = []; // Limpiar card2 en caso de error
}
);
this.loading = false
}
addOU(): void {
const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent);
addOU(parent:any = null): void {
console.log('Parent:', parent);
const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, { data: { parent }, width: '700px'});
dialogRef.afterClosed().subscribe(() => {
this.dataService.getUnidadesOrganizativas().subscribe(
data => this.unidadesOrganizativas = data,
this.dataService.getOrganizationalUnits().subscribe(
data => this.organizationalUnits = data,
error => console.error('Error fetching unidades organizativas', error)
);
});
}
addClient(): void {
const dialogRef = this.dialog.open(CreateClientComponent);
addClient(organizationalUnit:any = null): void {
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.getUnidadesOrganizativas().subscribe(
data => this.unidadesOrganizativas = data,
this.dataService.getOrganizationalUnits().subscribe(
data => this.organizationalUnits = data,
error => console.error('Error fetching unidades organizativas', error)
);
});
}
onDeleteClick(event: MouseEvent, uuid: string, name: string, type: string): void {
event.stopPropagation();
if (type === 'client') {
const dialogRef = this.dialog.open(DeleteModalComponent, {
width: '400px',
data: { name }
});
getIcon(type: string): string {
switch(type) {
case 'organizational-unit':
return 'apartment';
case 'classroom-group':
return 'classrooms-group-icon';
case 'classroom':
return 'classroom-icon';
case 'client':
return 'client-icon';
case 'clients-group':
return 'clients-group-icon';
default:
return '';
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)
);
},
error => console.error('Error deleting element', error)
);
}
});
} else {
const dialogDeleteGroupRef = this.dialog.open(DeleteGroupsModalComponent, {
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)
);
},
error => console.error('Error deleting element', error)
);
} 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)
);
},
error => console.error('Error deleting element', error)
);
}
});
}
}
onDeleteClick(uuid: string, name: string, type: string): void {
const dialogRef = this.dialog.open(DeleteModalComponent, {
width: '250px',
data: { name }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.dataService.deleteElement(uuid, type).subscribe(
() => {
this.loadChildrenAndClients(this.selectedUnidad?.uuid || '');
this.dataService.getUnidadesOrganizativas().subscribe(
data => this.unidadesOrganizativas = data,
error => console.error('Error fetching unidades organizativas', error)
);
},
error => console.error('Error deleting element', error)
);
}
});
}
onEditClick(type: any, uuid: string): void {
console.log('Tipo del elemento a editar:', type);
console.log('UUID del elemento a editar:', uuid);
if (type != "client") {
const dialogRef = this.dialog.open(EditOrganizationalUnitComponent);
const dialogRef = this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '700px'});
} else {
console.log('Editar cliente');
const dialogRef = this.dialog.open(EditClientComponent);
const dialogRef = this.dialog.open(EditClientComponent, { data: { uuid }, width: '700px' } );
}
}
}

View File

@ -9,11 +9,11 @@ export interface Aula {
}
export interface UnidadOrganizativa {
id: number;
nombre: string;
id: string;
name: string;
uuid: string;
type: string;
aulas: Aula[];
parent: UnidadOrganizativa[];
}
export interface OrganizationalUnit {
@ -50,4 +50,4 @@ export interface ClientCollection {
"@id": string;
"@type": string;
};
}
}

View File

@ -9,7 +9,9 @@
<mat-form-field class="form-field">
<mat-label>Tipo</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let type of types" [value]="type">{{ type }}</mat-option>
<mat-option *ngFor="let type of types" [value]="type">
{{ typeTranslations[type] }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
@ -17,8 +19,9 @@
<input matInput formControlName="name" required>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Padre</mat-label>
<mat-label>Unidad organizativa padre</mat-label>
<mat-select formControlName="parent">
<mat-option>--</mat-option>
<mat-option *ngFor="let unit of parentUnits" [value]="unit['@id']">{{ unit.name }}</mat-option>
</mat-select>
</mat-form-field>
@ -46,10 +49,6 @@
<mat-label>Aforo</mat-label>
<input matInput formControlName="capacity" type="number">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Foto (URL) (string de prueba)</mat-label>
<input matInput formControlName="photo">
</mat-form-field>
<div>
<button mat-button matStepperPrevious>Atrás</button>
<button mat-button matStepperNext>Siguiente</button>
@ -128,7 +127,6 @@
<mat-label>Perfil de Hardware</mat-label>
<input matInput formControlName="hardwareProfile" type="url">
</mat-form-field>
<mat-slide-toggle formControlName="validation">Validación</mat-slide-toggle>
<div>
<button mat-button matStepperPrevious>Atrás</button>
<button mat-button (click)="onSubmit()" [disabled]="!networkSettingsFormGroup.valid">Añadir</button>

View File

@ -1,6 +1,6 @@
import { Component, OnInit, Output, EventEmitter } from '@angular/core';
import {Component, OnInit, Output, EventEmitter, Inject} from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MatDialogRef } from '@angular/material/dialog';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import { HttpClient, HttpHeaders } from '@angular/common/http';
@Component({
@ -13,20 +13,27 @@ export class CreateOrganizationalUnitComponent implements OnInit {
generalFormGroup: FormGroup;
additionalInfoFormGroup: FormGroup;
networkSettingsFormGroup: FormGroup;
classroomInfoFormGroup: FormGroup;
classroomInfoFormGroup: FormGroup;
types: string[] = ['organizational-unit', 'classrooms-group', 'classroom', 'clients-group'];
parentUnits: any[] = [];
typeTranslations: { [key: string]: string } = {
'organizational-unit': 'Unidad organizativa',
'classrooms-group': 'Grupo de aulas',
'classroom': 'Aula',
'clients-group': 'Grupo de clientes'
};
parentUnits: any[] = [];
@Output() unitAdded = new EventEmitter();
@Output() unitAdded = new EventEmitter();
constructor(
private _formBuilder: FormBuilder,
private dialogRef: MatDialogRef<CreateOrganizationalUnitComponent>,
private http: HttpClient
private http: HttpClient,
@Inject(MAT_DIALOG_DATA) public data: any // Inject data for edit mode
) {
this.generalFormGroup = this._formBuilder.group({
name: ['', Validators.required],
parent: [''],
parent: [this.data.parent ? this.data.parent['@id'] : null],
description: [''],
type: ['', Validators.required]
});
@ -53,17 +60,16 @@ export class CreateOrganizationalUnitComponent implements OnInit {
location: [''],
projector: [false],
board: [false],
capacity: [0, Validators.min(0)],
photo: ['', Validators.pattern('https?://.+')]
capacity: [0, Validators.min(0)]
});
}
ngOnInit() {
this.loadParentUnits();
this.loadParentUnits();
}
loadParentUnits() {
const url = 'http://127.0.0.1:8080/organizational-units?page=1&itemsPerPage=30';
const url = 'http://127.0.0.1:8080/organizational-units?page=1&itemsPerPage=10000';
this.http.get<any>(url).subscribe(
response => {
@ -106,11 +112,8 @@ export class CreateOrganizationalUnitComponent implements OnInit {
if (classroomInfoFormValues.capacity !== undefined) {
formData.capacity = classroomInfoFormValues.capacity;
}
if (classroomInfoFormValues.photo !== undefined) {
formData.photo = classroomInfoFormValues.photo;
}
}
console.log('POST data:', formData);
const postUrl = 'http://127.0.0.1:8080/organizational-units';
const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
@ -119,7 +122,7 @@ export class CreateOrganizationalUnitComponent implements OnInit {
response => {
console.log('POST successful:', response);
this.unitAdded.emit();
this.dialogRef.close();
this.dialogRef.close();
},
error => {
console.error('Error al realizar POST:', error);

View File

@ -66,7 +66,7 @@ export class EditOrganizationalUnitComponent implements OnInit {
}
loadParentUnits() {
const url = 'http://127.0.0.1:8080/organizational-units?page=1&itemsPerPage=30';
const url = 'http://127.0.0.1:8080/organizational-units?page=1&itemsPerPage=10000';
this.http.get<any>(url).subscribe(
response => {
@ -93,20 +93,20 @@ export class EditOrganizationalUnitComponent implements OnInit {
comments: data.comments
});
this.networkSettingsFormGroup.patchValue({
proxy: data.proxy,
dns: data.dns,
netmask: data.netmask,
router: data.router,
ntp: data.ntp,
p2pMode: data.p2pMode,
p2pTime: data.p2pTime,
mcastIp: data.mcastIp,
mcastSpeed: data.mcastSpeed,
mcastPort: data.mcastPort,
mcastMode: data.mcastMode,
menu: data.menu,
hardwareProfile: data.hardwareProfile,
validation: data.validation
proxy: data.networkSettings.proxy,
dns: data.networkSettings.dns,
netmask: data.networkSettings.netmask,
router: data.networkSettings.router,
ntp: data.networkSettings.ntp,
p2pMode: data.networkSettings.p2pMode,
p2pTime: data.networkSettings.p2pTime,
mcastIp: data.networkSettings.mcastIp,
mcastSpeed: data.networkSettings.mcastSpeed,
mcastPort: data.networkSettings.mcastPort,
mcastMode: data.networkSettings.mcastMode,
menu: data.networkSettings.menu,
hardwareProfile: data.networkSettings.hardwareProfile,
validation: data.networkSettings.validation
});
},
error => {

View File

@ -1,10 +1,15 @@
mat-toolbar {
height: 50px;
height: 60px;
background-color: #3f51b5;
color: white;
}
.admin-button,
.user-button{
background-color: #e0e0e0;
}
.navbar-button-row {
display: flex;
justify-content: end;
@ -16,6 +21,5 @@ button[mat-flat-button] {
}
.navbar-tittle{
padding-left: 20px;
cursor: pointer;
}

View File

@ -1,7 +1,8 @@
<mat-toolbar>
<span class="navbar-tittle" routerLink="/dashboard">Opengnsys webconsole</span>
<div class="navbar-button-row">
<button mat-button [matMenuTriggerFor]="menu">Administracion</button>
<button class="admin-button" *ngIf="isSuperAdmin" mat-button [matMenuTriggerFor]="menu">Administracion</button>
<button class="user-button" mat-button *ngIf="!isSuperAdmin" (click)="editUser()">Editar usuario</button>
<mat-menu #menu="matMenu">
<button mat-menu-item routerLink="/users">Usuarios</button>
<button mat-menu-item routerLink="/user-groups">Roles</button>

View File

@ -1,5 +1,9 @@
import { Component, EventEmitter, OnInit, Output } from '@angular/core';
import {jwtDecode} from 'jwt-decode';
import {
ChangePasswordModalComponent
} from "../../pages/admin/users/users/change-password-modal/change-password-modal.component";
import {MatDialog} from "@angular/material/dialog";
@Component({
selector: 'app-header',
@ -7,14 +11,44 @@ import {jwtDecode} from 'jwt-decode';
styleUrls: ['./header.component.css'],
})
export class HeaderComponent implements OnInit {
isSuperAdmin: boolean = false;
@Output() toggleSidebar = new EventEmitter<void>();
private decodedToken: any;
private username: any;
onToggleSidebar() {
this.toggleSidebar.emit();
}
constructor() { }
constructor(public dialog: MatDialog) {}
ngOnInit(): void {
const token = localStorage.getItem('loginToken');
if (token) {
try {
this.decodedToken = jwtDecode(token);
this.isSuperAdmin = this.decodedToken.roles.includes('ROLE_SUPER_ADMIN');
localStorage.setItem('isSuperAdmin', String(this.isSuperAdmin));
console.log('isSuperAdmin:', this.isSuperAdmin);
this.username = this.decodedToken.username;
} catch (error) {
console.error('Error decoding JWT:', error);
}
}
}
ngDoCheck(): void {
this.isSuperAdmin = localStorage.getItem('isSuperAdmin') === 'true';
}
editUser() {
const dialogRef = this.dialog.open(ChangePasswordModalComponent, {
data: { user: this.decodedToken.username, uuid: this.decodedToken.uuid },
width: '400px',
});
/* dialogRef.componentInstance.userEdited.subscribe(() => {
console.log("User edited successfully!")
}); */
}
}

View File

@ -1,7 +1,10 @@
.header {
height: 10vh;
}
.container {
width: auto;
height: 100%;
height: calc(100vh - 60px);
}
.content {

View File

@ -1,4 +1,4 @@
<app-header (toggleSidebar)="toggleSidebar()"></app-header>
<app-header class="header" (toggleSidebar)="toggleSidebar()"></app-header>
<mat-drawer-container class="container" >
<mat-drawer class="sidebar" mode="side" opened>

View File

@ -21,6 +21,7 @@ mat-icon {
.user-logged{
align-items: center;
height: 70px;
gap: 2rem;
padding:1rem;
font-size: medium;
@ -35,3 +36,4 @@ mat-icon {
.admin-link {
margin-top: auto;
}

View File

@ -22,20 +22,12 @@ export class SidebarComponent {
try {
this.decodedToken = jwtDecode(token);
this.isSuperAdmin = this.decodedToken.roles.includes('ROLE_SUPER_ADMIN');
localStorage.setItem('isSuperAdmin', String(this.isSuperAdmin));
console.log('isSuperAdmin:', this.isSuperAdmin);
this.username = this.decodedToken.username;
} catch (error) {
console.error('Error decoding JWT:', error);
}
}
}
editUser(user: any) {
// Implementar la lógica de edición
const dialogRef = this.dialog.open(ChangePasswordModalComponent, {
data: { user: this.decodedToken.username, uuid: this.decodedToken.uuid },
});
/* dialogRef.componentInstance.userEdited.subscribe(() => {
console.log("User edited successfully!")
}); */
}
}

View File

@ -30,36 +30,19 @@
margin: 0 auto 20px;
}
.login input {
font-family: "Asap", sans-serif;
display: block;
border-radius: 5px;
font-size: 16px;
background: rgba(230, 230, 230);
.button-row {
display: flex;
justify-content: center;
width: 100%;
margin-top: 20px;
}
.button-row button {
width: 100%;
}
mat-form-field {
width: 100%;
padding: 10px;
margin: 15px -10px;
text-align: center;
}
.login button {
font-family: "Asap", sans-serif;
cursor: pointer;
color: #fff;
font-size: 16px;
text-transform: uppercase;
width: 80px;
border: 0;
padding: 10px 0;
margin-top: 10px;
margin-left: -5px;
border-radius: 5px;
background-color: #3f51b5;
transition: background-color 300ms;
}
.login button:hover {
background-color: #0271da;
}
.invalid {
@ -71,7 +54,6 @@
margin-top: 10px;
}
@keyframes rotate360 {
from {
transform: rotate(0deg);
@ -80,7 +62,7 @@
transform: rotate(360deg);
}
}
.rotating {
animation: rotate360 0.6s cubic-bezier(.42,0,1,1) infinite;
}
}

View File

@ -1,11 +1,28 @@
<div>
<form class="login" (ngSubmit)="onLogin()" #loginForm="ngForm">
<img src="assets/images/logo.png" alt="Opengnsys"
<img src="assets/images/logo.png" alt="Opengnsys"
class="login-logo" [class.rotating]="isLoading">
<h2>Opengnsys</h2>
<input [(ngModel)]="loginObj.username" type="text" id="username" name="username" placeholder="usuario" required #usernameInput="ngModel" [ngClass]="{'invalid': !usernameInput.valid && usernameInput.touched}">
<input [(ngModel)]="loginObj.password" type="password" id="password" name="password" placeholder="contraseña" required #passwordInput="ngModel" [ngClass]="{'invalid': !passwordInput.valid && passwordInput.touched}">
<button type="submit">Login</button>
<mat-form-field>
<mat-label>Introducte tu usuario</mat-label>
<input matInput [(ngModel)]="loginObj.username" name="username" required #usernameInput="ngModel" [ngClass]="{'invalid': !usernameInput.valid && usernameInput.touched}" />
</mat-form-field>
<mat-form-field>
<mat-label>Introduce tu contraseña</mat-label>
<input matInput [type]="hide() ? 'password' : 'text'" required [(ngModel)]="loginObj.password" name="password"/>
<button
mat-icon-button
matSuffix
(click)="clickEvent($event)"
[attr.aria-label]="'Ocultar contraseña'"
[attr.aria-pressed]="hide()"
>
<mat-icon>{{hide() ? 'visibility_off' : 'visibility'}}</mat-icon>
</button>
</mat-form-field>
<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,5 +1,5 @@
import { HttpClient } from '@angular/common/http';
import { Component } from '@angular/core';
import {Component, signal} from '@angular/core';
import { Router } from '@angular/router';
@Component({
@ -13,13 +13,13 @@ export class LoginComponent {
"password": ""
};
errorMessage: string = '';
isLoading: boolean = false;
isLoading: boolean = false;
constructor(private http: HttpClient, private router: Router) { }
onLogin() {
this.errorMessage = '';
this.isLoading = true;
this.isLoading = true;
if (!this.loginObj.username || !this.loginObj.password) {
if (!this.loginObj.username) {
@ -34,7 +34,7 @@ export class LoginComponent {
document.getElementById('password')?.classList.remove('invalid');
}
this.isLoading = false;
this.isLoading = false;
return;
}
@ -49,7 +49,7 @@ export class LoginComponent {
this.isLoading = false;
},
error: (err) => {
this.isLoading = false;
this.isLoading = false;
if (err.status === 401) {
this.errorMessage = 'Usuario o contraseña incorrectos';
} else {
@ -61,4 +61,10 @@ export class LoginComponent {
}
});
}
hide = signal(true);
clickEvent(event: MouseEvent) {
this.hide.set(!this.hide());
event.stopPropagation();
}
}

View File

@ -1,15 +1,19 @@
.user-form .form-field {
display: block;
margin-bottom: 10px;
}
.checkbox-group label {
display: block;
margin-bottom: 8px;
}
.error-message {
color: red;
margin-top: 10px;
}
margin-bottom: 10px;
}
.checkbox-group label {
display: block;
margin-bottom: 8px;
}
.error-message {
color: red;
margin-top: 10px;
}
mat-spinner {
margin: 0 auto;
align-self: center;
}

View File

@ -1,33 +1,33 @@
<h1 mat-dialog-title>Editar Usuario</h1>
<div mat-dialog-content>
<form [formGroup]="userForm" class="user-form">
<mat-form-field class="form-field">
<mat-label>Nombre de usuario</mat-label>
<input matInput formControlName="username">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Contraseña actual</mat-label>
<input matInput formControlName="currentPassword" type="password">
<input matInput formControlName="currentPassword" type="currentPassword">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Nueva contraseña</mat-label>
<input matInput formControlName="password" type="password">
<input matInput formControlName="newPassword" type="password">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Repite la contraseña</mat-label>
<input matInput formControlName="confirmPassword" type="password">
<input matInput formControlName="repeatNewPassword" type="password">
</mat-form-field>
@if (loading){
<mat-spinner></mat-spinner>
}
@if (passwordMismatch) {
<div class="error-message">Las contraseñas no coinciden</div>
}
@if (updateError) {
<div class="error-message">Error, inténtelo de nuevo</div>
<div class="error-message">{{resetPasswordError}}</div>
}
</form>
</div>
<div mat-dialog-actions>
<button mat-button (click)="onNoClick()">Cancelar</button>
<button mat-button (click)="onSubmit()">Editar</button>
<button mat-button (click)="onSubmit()" [disabled]="loading">Editar</button>
</div>

View File

@ -14,6 +14,8 @@ export class ChangePasswordModalComponent {
userForm: FormGroup;
passwordMismatch: boolean = false;
updateError: boolean = false;
loading: boolean = false;
resetPasswordError: string = '';
constructor(
public dialogRef: MatDialogRef<EditUserModalComponent>,
@ -22,9 +24,9 @@ export class ChangePasswordModalComponent {
private userService: UserService
) {
this.userForm = this.fb.group({
username: [this.data.user, Validators.required],
password: ['', Validators.required],
confirmPassword: ['', Validators.required]
currentPassword: ['', Validators.required],
newPassword: ['', Validators.required],
repeatNewPassword: ['', Validators.required]
}, { validators: this.passwordMatchValidator });
}
@ -40,34 +42,36 @@ export class ChangePasswordModalComponent {
if (this.userForm.valid) {
this.passwordMismatch = false;
this.updateError = false;
this.loading = true
const userPayload = {
username: this.userForm.value.username,
allowedOrganizationalUnits: [],
password: this.userForm.value.password,
enabled: true,
userGroups: []
currentPassword: this.userForm.value.currentPassword,
newPassword: this.userForm.value.newPassword,
repeatNewPassword: this.userForm.value.repeatNewPassword
};
console.log("THIS IS THE USER PAYLOAD: ", userPayload);
this.userService.updateUser(this.data.uuid, userPayload).subscribe(
this.userService.changePassword(this.data.uuid, userPayload).subscribe(
response => {
console.log('User updated successfully:', response);
this.userEdited.emit();
this.dialogRef.close(this.userForm.value);
},
error => {
console.error('Error updating user:', error);
console.error('Error updating user:', error.error['hydra:description']);
this.resetPasswordError = error.error['hydra:description']
this.updateError = true;
this.loading = false
}
);
} else {
console.error('Form is invalid');
this.passwordMismatch = this.userForm.hasError('mismatch');
this.updateError = true;
this.loading = false
}
}
private passwordMatchValidator(form: FormGroup): { [key: string]: boolean } | null {
return form.get('password')?.value === form.get('confirmPassword')?.value ? null : { mismatch: true };
return form.get('newPassword')?.value === form.get('repeatNewPassword')?.value ? null : { mismatch: true };
}
}

View File

@ -9,9 +9,9 @@
<mat-label>Contraseña</mat-label>
<input matInput formControlName="password" type="password">
</mat-form-field>
<mat-form-field appearance="fill">
<mat-form-field class="form-field">
<mat-label>Rol</mat-label>
<mat-select formControlName="role">
<mat-select multiple formControlName="userGroups">
<mat-option *ngFor="let group of userGroups" [value]="group['@id']">
{{ group.name }}
</mat-option>
@ -20,13 +20,13 @@
<mat-form-field class="form-field">
<mat-label>Unidad organiativa</mat-label>
<mat-select multiple formControlName="organizationalUnit">
<mat-select multiple formControlName="allowedOrganizationalUnits">
<mat-option *ngFor="let unit of organizationalUnits" [value]="unit['@id']">
{{unit.name}}
</mat-option>
</mat-select>
</mat-form-field>
</form>
</div>
<div mat-dialog-actions>

View File

@ -3,12 +3,6 @@ import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { UserService } from '../users.service';
interface UserGroup {
'@id': string;
name: string;
role: string[];
}
@Component({
selector: 'app-edit-user-modal',
templateUrl: './edit-user-modal.component.html',
@ -16,7 +10,7 @@ interface UserGroup {
})
export class EditUserModalComponent implements OnInit {@Output() userEdited = new EventEmitter<void>();
userForm: FormGroup;
userGroups: UserGroup[] = [];
userGroups: any[] = [];
organizationalUnits: any[] = [];
constructor(
@ -26,11 +20,12 @@ export class EditUserModalComponent implements OnInit {@Output() userEdited = ne
private userService: UserService // Inyecta el servicio
) {
this.userForm = this.fb.group({
username: [this.data],
username: [this.data.user.username],
password: [''],
role: this.data.user.allowedOrganizationalUnits,
organizationalUnit: [[this.data.user.allowedOrganizationalUnits], Validators.required]
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 {
@ -47,25 +42,25 @@ export class EditUserModalComponent implements OnInit {@Output() userEdited = ne
}
onSubmit(): void {
console.log(this.userForm.value);
const userPayload = {
username: this.userForm.value.username,
allowedOrganizationalUnits: this.userForm.value.allowedOrganizationalUnits,
password: this.userForm.value.password,
enabled: true,
userGroups: this.userForm.value.userGroups
};
const userPayload = {
username: this.userForm.value.username,
allowedOrganizationalUnits: [],
password: this.userForm.value.password,
enabled: true,
userGroups: [this.userForm.value.role ]
};
this.userService.updateUser(this.data.user.uuid, userPayload).subscribe(
response => {
console.log('User added successfully:', response);
this.userEdited.emit();
this.dialogRef.close(this.userForm.value);
},
error => {
console.error('Error adding user:', error);
// Agregar alguna lógica para manejar el error en la interfaz de usuario
}
);
this.userService.updateUser(this.data.user.uuid, userPayload).subscribe(
response => {
console.log('User added successfully:', response);
this.userEdited.emit();
this.dialogRef.close(this.userForm.value);
},
error => {
console.error('Error adding user:', error);
// Agregar alguna lógica para manejar el error en la interfaz de usuario
}
);
}
}

View File

@ -62,7 +62,8 @@ export class UsersComponent implements OnInit {
editUser(user: any) {
// Implementar la lógica de edición
const dialogRef = this.dialog.open(EditUserModalComponent, {
data: { user: user }
data: { user: user },
width: '400px'
});
dialogRef.componentInstance.userEdited.subscribe(() => {
this.loadUsers();
@ -71,7 +72,7 @@ export class UsersComponent implements OnInit {
deleteUser(user: any) {
const dialogRef = this.dialog.open(DeleteUserModalComponent, {
data: user
data: user,
});
dialogRef.afterClosed().subscribe(result => {

View File

@ -10,6 +10,12 @@ interface UserPayload {
allowedOrganizationalUnits: any[];
}
interface ChangePasswordPayload {
currentPassword: string;
newPassword: string;
repeatNewPassword: string;
}
interface UserGroup {
'@id': string;
name: string;
@ -38,6 +44,13 @@ export class UserService {
return this.http.put(`${this.apiUrl}/users/${userId}`, userPayload, { headers });
}
changePassword(userId: number, userPayload: ChangePasswordPayload): Observable<any> {
const headers = new HttpHeaders({
'Content-Type': 'application/ld+json',
});
return this.http.put(`${this.apiUrl}/users/${userId}/reset-password`, userPayload, { headers });
}
getUserGroups(): Observable<{ 'hydra:member': UserGroup[] }> {
return this.http.get<{ 'hydra:member': UserGroup[] }>(`${this.apiUrl}/user-groups`);
}
@ -48,6 +61,6 @@ export class UserService {
getOrganizationalUnits(): Observable<any> {
return this.http.get<any>(`${this.apiUrl}/organizational-units?page=1&itemsPerPage=30`);
}
}