refs #1520 and #1524. Unify edit and create organizational unit into one single component. Refactor form structure.
testing/ogGui-multibranch/pipeline/head This commit looks good Details

pull/16/head
Lucas Lara García 2025-02-17 14:05:58 +01:00
parent 952938a253
commit 024914d993
13 changed files with 328 additions and 697 deletions

View File

@ -32,12 +32,10 @@ import { AddRoleModalComponent } from './components/admin/roles/roles/add-role-m
import { ChangePasswordModalComponent } from './components/admin/users/users/change-password-modal/change-password-modal.component';
import { GroupsComponent } from './components/groups/groups.component';
import { MatDividerModule } from '@angular/material/divider';
import { CreateOrganizationalUnitComponent } from './components/groups/shared/organizational-units/create-organizational-unit/create-organizational-unit.component';
import { MatStepperModule } from '@angular/material/stepper';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { CreateClientComponent } from './components/groups/shared/clients/create-client/create-client.component';
import { DeleteModalComponent } from './shared/delete_modal/delete-modal/delete-modal.component';
import { EditOrganizationalUnitComponent } from './components/groups/shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component';
import { EditClientComponent } from './components/groups/shared/clients/edit-client/edit-client.component';
import { ClassroomViewComponent } from './components/groups/shared/classroom-view/classroom-view.component';
import { MatProgressSpinner } from "@angular/material/progress-spinner";
@ -130,6 +128,7 @@ import {ImportImageComponent} from "./components/repositories/import-image/impor
import { LoadingComponent } from './shared/loading/loading.component';
import { RepositoryImagesComponent } from './components/repositories/repository-images/repository-images.component';
import { InputDialogComponent } from './components/commands/commands-task/task-logs/input-dialog/input-dialog.component';
import { ManageOrganizationalUnitComponent } from './components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component';
export function HttpLoaderFactory(http: HttpClient) {
return new TranslateHttpLoader(http, './locale/', '.json');
}
@ -150,10 +149,8 @@ export function HttpLoaderFactory(http: HttpClient) {
AddRoleModalComponent,
ChangePasswordModalComponent,
GroupsComponent,
CreateOrganizationalUnitComponent,
CreateClientComponent,
DeleteModalComponent,
EditOrganizationalUnitComponent,
EditClientComponent,
ClassroomViewComponent,
ClientViewComponent,
@ -217,6 +214,7 @@ export function HttpLoaderFactory(http: HttpClient) {
LoadingComponent,
RepositoryImagesComponent,
InputDialogComponent,
ManageOrganizationalUnitComponent,
],
bootstrap: [AppComponent],
imports: [BrowserModule,

View File

@ -10,9 +10,8 @@ import { MatTreeFlatDataSource, MatTreeFlattener } from '@angular/material/tree'
import { Subscription } from 'rxjs';
import { DataService } from './services/data.service';
import { UnidadOrganizativa, Client, TreeNode, FlatNode, Command } from './model/model';
import { CreateOrganizationalUnitComponent } from './shared/organizational-units/create-organizational-unit/create-organizational-unit.component';
import { CreateClientComponent } from './shared/clients/create-client/create-client.component';
import { EditOrganizationalUnitComponent } from './shared/organizational-units/edit-organizational-unit/edit-organizational-unit.component';
import { ManageOrganizationalUnitComponent } from './shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component';
import { EditClientComponent } from './shared/clients/edit-client/edit-client.component';
import { ShowOrganizationalUnitComponent } from './shared/organizational-units/show-organizational-unit/show-organizational-unit.component';
import { LegendComponent } from './shared/legend/legend.component';
@ -325,13 +324,12 @@ export class GroupsComponent implements OnInit, OnDestroy {
addOU(event: MouseEvent, parent: TreeNode | null = null): void {
event.stopPropagation();
const dialogRef = this.dialog.open(CreateOrganizationalUnitComponent, {
const dialogRef = this.dialog.open(ManageOrganizationalUnitComponent, {
data: { parent },
width: '900px',
});
dialogRef.afterClosed().subscribe((newUnit) => {
if (newUnit?.uuid) {
console.log('Unidad organizativa creada:', newUnit);
this.refreshData(newUnit.uuid);
}
});
@ -391,7 +389,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
if (!uuid) return;
const dialogRef = node?.type !== NodeType.Client
? this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' })
? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px' })
: this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' });
dialogRef.afterClosed().subscribe(() => {
@ -457,7 +455,7 @@ export class GroupsComponent implements OnInit, OnDestroy {
onEditClick(event: MouseEvent, type: string, uuid: string): void {
event.stopPropagation();
const dialogRef = type !== NodeType.Client
? this.dialog.open(EditOrganizationalUnitComponent, { data: { uuid }, width: '900px' })
? this.dialog.open(ManageOrganizationalUnitComponent, { data: { uuid }, width: '900px' })
: this.dialog.open(EditClientComponent, { data: { uuid }, width: '900px' });
dialogRef.afterClosed().subscribe(() => {

View File

@ -4,21 +4,15 @@ h1 {
font-weight: 400;
color: #3f51b5;
margin-bottom: 20px;
}
.network-form {
display: flex;
flex-direction: column;
gap: 15px;
margin-top: 20px;
}
.form-field {
width: 100%;
margin-top: 10px;
}
.mat-dialog-content {
padding: 50px;
padding: 15px 50px 15px 50px;
}
button {
@ -27,10 +21,6 @@ button {
font-weight: 500;
}
.mat-slide-toggle {
margin-top: 20px;
}
mat-option .unit-name {
display: block;
}
@ -55,9 +45,6 @@ mat-option .unit-path {
.grid-form {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 20px;
}
.form-field {
width: 100%;
}
column-gap: 20px;
row-gap: 20px;
}

View File

@ -1,42 +0,0 @@
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;
}
mat-slide-toggle{
margin-left: 10px;
}

View File

@ -1,160 +0,0 @@
<h1 mat-dialog-title>{{ 'addOrgUnitTitle' | translate }}</h1>
<div mat-dialog-content>
<!-- Paso 1: General -->
<form [formGroup]="generalFormGroup">
<mat-form-field class="form-field">
<mat-label>{{ 'typeLabel' | translate }}</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let type of filteredTypes" [value]="type">
{{ typeTranslations[type] }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
<input matInput formControlName="name" required>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'createOrgUnitparentLabel' | translate }}</mat-label>
<mat-select formControlName="parent">
<mat-select-trigger>
{{ getSelectedParentName() }}
</mat-select-trigger>
<mat-option *ngFor="let unit of parentUnitsWithPaths" [value]="unit.id">
<div>{{ unit.name }}</div>
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'descriptionLabel' | translate }}</mat-label>
<textarea matInput formControlName="description"></textarea>
</mat-form-field>
</form>
<!-- Paso 2: Información del Aula -->
<form *ngIf="generalFormGroup.value.type === 'classroom'" [formGroup]="classroomInfoFormGroup">
<mat-form-field class="form-field">
<mat-label>{{ 'locationLabel' | translate }}</mat-label>
<input matInput formControlName="location">
</mat-form-field>
<mat-slide-toggle formControlName="projector">{{ 'projectorToggle' | translate }}</mat-slide-toggle>
<mat-slide-toggle formControlName="board">{{ 'boardToggle' | translate }}</mat-slide-toggle>
<mat-form-field class="form-field">
<mat-label>{{ 'capacityLabel' | translate }}</mat-label>
<input matInput formControlName="capacity" type="number">
</mat-form-field>
<mat-form-field class="form-field" appearance="fill">
<mat-label>{{ 'associatedCalendarLabel' | translate }}</mat-label>
<mat-select formControlName="remoteCalendar" (selectionChange)="onCalendarChange($event)">
<mat-option *ngFor="let calendar of calendars" [value]="calendar['@id']">
{{ calendar.name }}
</mat-option>
</mat-select>
</mat-form-field>
</form>
<!-- Paso 3: Información Adicional -->
<form [formGroup]="additionalInfoFormGroup">
<mat-form-field class="form-field">
<mat-label>{{ 'commentsLabel' | translate }}</mat-label>
<textarea matInput formControlName="comments"></textarea>
</mat-form-field>
</form>
<!-- Paso 4: Configuración de Red -->
<form *ngIf="generalFormGroup.value.type === 'classroom' || generalFormGroup.value.type === 'clients-group'" [formGroup]="networkSettingsFormGroup">
<mat-form-field class="form-field">
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
<mat-select formControlName="oglive" (selectionChange)="onOgLiveChange($event)">
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
{{ oglive.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'repositoryLabel' | translate }}</mat-label>
<mat-select formControlName="repository" (selectionChange)="onRepositoryChange($event)">
<mat-option *ngFor="let repository of repositories" [value]="repository['@id']">
{{ repository.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'nextServerLabel' | translate }}</mat-label>
<input matInput formControlName="nextServer">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'bootFileNameLabel' | translate }}</mat-label>
<input matInput formControlName="bootFileName">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'proxyUrlLabel' | translate }}</mat-label>
<input matInput formControlName="proxy">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'dnsIpLabel' | translate }}</mat-label>
<input matInput formControlName="dns">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'netmaskLabel' | translate }}</mat-label>
<input matInput formControlName="netmask">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'routerLabel' | translate }}</mat-label>
<input matInput formControlName="router">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'ntpIpLabel' | translate }}</mat-label>
<input matInput formControlName="ntp">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'p2pModeLabel' | translate }}</mat-label>
<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>{{ 'p2pTimeLabel' | translate }}</mat-label>
<input matInput formControlName="p2pTime" type="number">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'mcastIpLabel' | translate }}</mat-label>
<input matInput formControlName="mcastIp">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'mcastSpeedLabel' | translate }}</mat-label>
<input matInput formControlName="mcastSpeed" type="number">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'mcastPortLabel' | translate }}</mat-label>
<input matInput formControlName="mcastPort" type="number">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'mcastModeLabel' | translate }}</mat-label>
<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>{{ 'menuUrlLabel' | translate }}</mat-label>
<input matInput formControlName="menu" type="url">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'hardwareProfileLabel' | translate }}</mat-label>
<mat-select formControlName="hardwareProfile">
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">{{ unit.description }}</mat-option>
</mat-select>
<mat-error>{{ 'urlFormatError' | translate }}</mat-error>
</mat-form-field>
</form>
</div>
<div mat-dialog-actions align="end">
<button mat-button (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
<button mat-button (click)="onSubmit()" [disabled]="!networkSettingsFormGroup.valid">{{ 'addOUSubmitButton' | translate }}</button>
</div>

View File

@ -1,240 +0,0 @@
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 '../../../services/data.service';
@Component({
selector: 'app-create-organizational-unit',
templateUrl: './create-organizational-unit.component.html',
styleUrls: ['./create-organizational-unit.component.css']
})
export class CreateOrganizationalUnitComponent implements OnInit {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
isLinear = true;
generalFormGroup: FormGroup;
additionalInfoFormGroup: FormGroup;
networkSettingsFormGroup: FormGroup;
classroomInfoFormGroup: FormGroup;
types: string[] = ['organizational-unit', 'classrooms-group', 'classroom', 'clients-group'];
typeTranslations: { [key: string]: string } = {
'organizational-unit': 'Centro',
'classrooms-group': 'Grupo de aulas',
'classroom': 'Aula',
'clients-group': 'Grupo de clientes'
};
protected p2pModeOptions = [
{ name: 'Leecher', value: 'leecher' },
{ name: 'Peer', value: 'peer' },
{ name: 'Seeder', value: 'seeder' },
];
protected multicastModeOptions = [
{"name": 'Half duplex', "value": "half"},
{"name": 'Full duplex', "value": "full"},
];
parentUnits: any[] = [];
hardwareProfiles: any[] = [];
calendars: any[] = [];
ogLives: any[] = [];
repositories: any[] = [];
selectedCalendarUuid: string | null = null;
parentUnitsWithPaths: { id: string, name: string, path: string }[] = [];
@Output() unitAdded = new EventEmitter<{ uuid: string; name: string }>();
constructor(
private _formBuilder: FormBuilder,
private dialogRef: MatDialogRef<CreateOrganizationalUnitComponent>,
private http: HttpClient,
private toastService: ToastrService,
private dataService: DataService,
@Inject(MAT_DIALOG_DATA) public data: any
) {
this.generalFormGroup = this._formBuilder.group({
name: ['', Validators.required],
parent: [this.data.parent ? this.data.parent['@id'] : null],
description: [''],
type: ['', Validators.required]
});
this.additionalInfoFormGroup = this._formBuilder.group({
comments: [''],
});
this.networkSettingsFormGroup = this._formBuilder.group({
ogLive: [null],
ogRepository: [null],
nextServer: [''],
bootFileName: [''],
proxy: [''],
dns: [''],
netmask: [''],
router: [''],
ntp: [''],
p2pMode: [''],
p2pTime: [0, Validators.min(0)],
mcastIp: [''],
mcastSpeed: [0, Validators.min(0)],
mcastPort: [0, Validators.min(0)],
mcastMode: [''],
menu: [''],
hardwareProfile: [''],
validation: [false]
});
this.classroomInfoFormGroup = this._formBuilder.group({
location: [''],
projector: [false],
board: [false],
capacity: [0, Validators.min(0)],
remoteCalendar: ['']
});
}
ngOnInit() {
this.loadParentUnits();
this.loadHardwareProfiles();
this.loadCalendars();
this.loadOgLives();
this.loadRepositories();
}
get filteredTypes(): string[] {
return this.generalFormGroup.get('parent')?.value ? this.types.filter(type => type !== 'organizational-unit') : this.types;
}
loadParentUnits() {
const url = `${this.baseUrl}/organizational-units?page=1&itemsPerPage=1000`;
this.http.get<any>(url).subscribe(
response => {
this.parentUnits = response['hydra:member'];
this.parentUnitsWithPaths = this.parentUnits.map(unit => ({
id: unit['@id'],
name: unit.name,
path: this.dataService.getOrganizationalUnitPath(unit, this.parentUnits)
}));
},
error => console.error('Error fetching parent units:', error)
);
}
getSelectedParentName(): string | undefined {
const parentId = this.generalFormGroup.get('parent')?.value;
return this.parentUnitsWithPaths.find(unit => unit.id === parentId)?.name;
}
loadHardwareProfiles(): void {
this.dataService.getHardwareProfiles().subscribe(
(data: any[]) => this.hardwareProfiles = data,
error => console.error('Error fetching hardware profiles', error)
);
}
loadOgLives() {
this.dataService.getOgLives().subscribe(
(data: any[]) => {
this.ogLives = data
},
error => console.error('Error fetching ogLives', error)
);
}
loadRepositories() {
this.dataService.getRepositories().subscribe(
(data: any[]) => this.repositories = data,
error => console.error('Error fetching repositories', error)
);
}
loadCalendars() {
const apiUrl = `${this.baseUrl}/remote-calendars?page=1&itemsPerPage=30`;
this.http.get<any>(apiUrl).subscribe(
response => this.calendars = response['hydra:member'],
error => {
console.error('Error loading calendars', error);
this.openSnackBar(true, 'Error loading calendars');
}
);
}
private cleanFormValues(formGroup: FormGroup): any {
const cleanedValues: any = {};
Object.keys(formGroup.controls).forEach(key => {
const value = formGroup.get(key)?.value;
if (value !== '' && value !== 0) {
cleanedValues[key] = value;
}
});
return cleanedValues;
}
onSubmit() {
if (this.isFormValid()) {
const formData: any = this.buildPayload();
const postUrl = `${this.baseUrl}/organizational-units`;
const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
this.http.post<any>(postUrl, formData, { headers }).subscribe(
response => {
this.unitAdded.emit(response);
this.dialogRef.close(response);
this.toastService.success('Unidad creada exitosamente', 'Éxito');
this.openSnackBar(false, 'Unidad creada exitosamente');
},
error => {
console.error('Error al realizar POST:', error);
this.toastService.error('Ha ocurrido un error');
this.openSnackBar(true, 'Error al crear la unidad organizativa: ' + error.error['hydra:description']);
}
);
}
}
private isFormValid(): boolean {
return this.generalFormGroup.valid &&
this.additionalInfoFormGroup.valid &&
this.networkSettingsFormGroup.valid &&
(this.generalFormGroup.value.type !== 'classroom' || this.classroomInfoFormGroup.valid);
}
private buildPayload(): any {
const generalFormValues = this.cleanFormValues(this.generalFormGroup);
const additionalInfoFormValues = this.cleanFormValues(this.additionalInfoFormGroup);
const networkSettingsFormValues = this.cleanFormValues(this.networkSettingsFormGroup);
const classroomInfoFormValues = this.cleanFormValues(this.classroomInfoFormGroup);
return {
...generalFormValues,
...classroomInfoFormValues,
comments: additionalInfoFormValues.comments,
networkSettings: { ...networkSettingsFormValues },
menu: networkSettingsFormValues.menu || null,
ogLive: networkSettingsFormValues.ogLive || null,
ogRepository: networkSettingsFormValues.ogRepository || null,
hardwareProfile: networkSettingsFormValues.hardwareProfile || null,
};
}
onCalendarChange(event: any) {
this.generalFormGroup.value.remoteCalendar = event.value;
this.selectedCalendarUuid = event.value;
}
onOgLiveChange(event: any) {
this.networkSettingsFormGroup.value.ogLive = event.value;
}
onRepositoryChange(event: any) {
this.networkSettingsFormGroup.value.ogRepository = event
}
onNoClick(): void {
this.dialogRef.close();
}
openSnackBar(isError: boolean, message: string) {
if (isError) {
this.toastService.error('Error al crear la unidad: ' + message, 'Error');
} else {
this.toastService.success('Unidad creada exitosamente', 'Éxito');
}
}
}

View File

@ -1,38 +0,0 @@
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;
}

View File

@ -1,148 +0,0 @@
<h1 mat-dialog-title>{{ 'editOrgUnitTitle' | translate }}</h1>
<div mat-dialog-content>
<!-- Paso 1: General -->
<form [formGroup]="generalFormGroup">
<mat-form-field class="form-field">
<mat-label>{{ 'typeLabel' | translate }}</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let type of filteredTypes" [value]="type">
{{ typeTranslations[type] }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'nameLabel' | translate }}</mat-label>
<input matInput formControlName="name" required>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'editOrgUnitParentLabel' | translate }}</mat-label>
<mat-select formControlName="parent">
<mat-select-trigger>
{{ getSelectedParentName() }}
</mat-select-trigger>
<mat-option *ngFor="let unit of parentUnitsWithPaths" [value]="unit.id">
<div>{{ unit.name }}</div>
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'descriptionLabel' | translate }}</mat-label>
<textarea matInput formControlName="description"></textarea>
</mat-form-field>
</form>
<!-- Paso 2: Información del Aula -->
<form [formGroup]="classroomInfoFormGroup">
<mat-form-field class="form-field">
<mat-label>{{ 'locationLabel' | translate }}</mat-label>
<input matInput formControlName="location">
</mat-form-field>
<mat-slide-toggle formControlName="projector">{{ 'projectorToggle' | translate }}</mat-slide-toggle>
<mat-slide-toggle formControlName="board">{{ 'boardToggle' | translate }}</mat-slide-toggle>
<mat-form-field class="form-field">
<mat-label>{{ 'capacityLabel' | translate }}</mat-label>
<input matInput formControlName="capacity" type="number">
</mat-form-field>
<mat-form-field class="form-field" appearance="fill">
<mat-label>{{ 'associatedCalendarLabel' | translate }}</mat-label>
<mat-select formControlName="remoteCalendar" (selectionChange)="onCalendarChange($event)">
<mat-option *ngFor="let calendar of calendars" [value]="calendar['@id']">
{{ calendar.name }}
</mat-option>
</mat-select>
</mat-form-field>
</form>
<!-- Paso 3: Información Adicional -->
<form [formGroup]="additionalInfoFormGroup">
<mat-form-field class="form-field">
<mat-label>{{ 'commentsLabel' | translate }}</mat-label>
<textarea matInput formControlName="comments"></textarea>
</mat-form-field>
</form>
<!-- Paso 4: Configuración de Red -->
<form [formGroup]="networkSettingsFormGroup">
<mat-form-field class="form-field">
<mat-label>{{ 'ogLiveLabel' | translate }}</mat-label>
<mat-select formControlName="ogLive" (selectionChange)="onOgLiveChange($event)">
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
{{ oglive.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'repositoryLabel' | translate }}</mat-label>
<mat-select formControlName="repository" (selectionChange)="onRepositoryChange($event)">
<mat-option *ngFor="let repository of repositories" [value]="repository['@id']">
{{ repository.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'proxyUrlLabel' | translate }}</mat-label>
<input matInput formControlName="proxy">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'dnsIpLabel' | translate }}</mat-label>
<input matInput formControlName="dns">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'netmaskLabel' | translate }}</mat-label>
<input matInput formControlName="netmask">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'routerLabel' | translate }}</mat-label>
<input matInput formControlName="router">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'ntpIpLabel' | translate }}</mat-label>
<input matInput formControlName="ntp">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'p2pModeLabel' | translate }}</mat-label>
<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>{{ 'p2pTimeLabel' | translate }}</mat-label>
<input matInput formControlName="p2pTime" type="number">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'mcastIpLabel' | translate }}</mat-label>
<input matInput formControlName="mcastIp">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'mcastSpeedLabel' | translate }}</mat-label>
<input matInput formControlName="mcastSpeed" type="number">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'mcastPortLabel' | translate }}</mat-label>
<input matInput formControlName="mcastPort" type="number">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'mcastModeLabel' | translate }}</mat-label>
<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>{{ 'menuUrlLabel' | translate }}</mat-label>
<input matInput formControlName="menu" type="url">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>{{ 'hardwareProfileLabel' | translate }}</mat-label>
<mat-select formControlName="hardwareProfile">
<mat-option *ngFor="let unit of hardwareProfiles" [value]="unit['@id']">{{ unit.description }}</mat-option>
</mat-select>
<mat-error>{{ 'urlFormatError' | translate }}</mat-error>
</mat-form-field>
<mat-slide-toggle formControlName="validation">{{ 'validationToggle' | translate }}</mat-slide-toggle>
</form>
</div>
<div mat-dialog-actions align="end">
<button mat-button (click)="onNoClick()">{{ 'cancelButton' | translate }}</button>
<button mat-button (click)="onSubmit()" [disabled]="!networkSettingsFormGroup.valid">{{ 'editOUSubmitButton' | translate }}</button>
</div>

View File

@ -0,0 +1,64 @@
h1 {
text-align: center;
font-family: 'Roboto', sans-serif;
font-weight: 400;
color: #3f51b5;
margin-top: 20px;
}
.form-field {
width: 100%;
}
.description-form-field {
grid-column: span 2;
}
.mat-dialog-content {
padding: 0px 40px 15px 50px;
max-height: 70vh;
overflow-y: auto;
}
.mat-dialog-actions {
display: flex;
justify-content: flex-end;
gap: 1em;
padding: 1em;
margin-right: 1em;
}
button {
text-transform: none;
font-size: 16px;
font-weight: 500;
}
.grid-form {
display: grid;
grid-template-columns: repeat(2, 1fr);
column-gap: 20px;
row-gap: 10px;
}
.step-title {
font-family: 'Roboto', sans-serif;
color: #3f51b5;
margin: 40px 0 15px 0;
display: block;
}
.projector-board-field {
display: flex;
align-items: center;
grid-column: span 2;
gap: 2em;
margin-bottom: 10px;
}
.validation-field {
display: flex;
align-items: center;
grid-column: span 2;
margin-bottom: 10px;
}

View File

@ -0,0 +1,166 @@
<h1 mat-dialog-title>{{ isEditMode ? 'Editar' : 'Crear' }} Unidad Organizativa</h1>
<div class="mat-dialog-content">
<!-- Paso 1: General -->
<span class="step-title">General</span>
<form [formGroup]="generalFormGroup" class="grid-form">
<mat-form-field class="form-field" appearance="fill">
<mat-label>Tipo</mat-label>
<mat-select formControlName="type" required>
<mat-option *ngFor="let type of filteredTypes" [value]="type">
{{ typeTranslations[type] }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field" appearance="fill">
<mat-label>Nombre</mat-label>
<input matInput formControlName="name" required>
</mat-form-field>
<mat-form-field class="form-field" appearance="fill">
<mat-label>Padre</mat-label>
<mat-select formControlName="parent">
<mat-select-trigger>
{{ getSelectedParentName() }}
</mat-select-trigger>
<mat-option *ngFor="let unit of parentUnitsWithPaths" [value]="unit.id">
<div>{{ unit.name }}</div>
<div style="font-size: smaller; color: gray;">{{ unit.path }}</div>
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field description-form-field" appearance="fill">
<mat-label>Descripción</mat-label>
<textarea matInput formControlName="description"></textarea>
</mat-form-field>
</form>
<!-- Paso 2: Información del Aula -->
<span *ngIf="generalFormGroup.value.type === 'classroom'" class="step-title">Información del aula</span>
<form *ngIf="generalFormGroup.value.type === 'classroom'" class="grid-form" [formGroup]="classroomInfoFormGroup">
<mat-form-field class="form-field">
<mat-label>Localización</mat-label>
<input matInput formControlName="location">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Aforo</mat-label>
<input matInput formControlName="capacity" type="number" min="0">
<mat-error *ngIf="classroomInfoFormGroup.get('capacity')?.hasError('min')">
El aforo no puede ser negativo
</mat-error>
</mat-form-field>
<mat-form-field class="form-field" appearance="fill" style="grid-column: span 1;">
<mat-label>Calendario Asociado</mat-label>
<mat-select formControlName="remoteCalendar" (selectionChange)="onCalendarChange($event)">
<mat-option *ngFor="let calendar of calendars" [value]="calendar['@id']">
{{ calendar.name }}
</mat-option>
</mat-select>
</mat-form-field>
<div class="projector-board-field">
<mat-slide-toggle formControlName="projector">Proyector</mat-slide-toggle>
<mat-slide-toggle formControlName="board">Pizarra</mat-slide-toggle>
</div>
</form>
<!-- Paso 3: Configuración de Red -->
<span class="step-title">Configuración de Red</span>
<form [formGroup]="networkSettingsFormGroup" class="grid-form">
<mat-form-field class="form-field">
<mat-label>OgLive</mat-label>
<mat-select formControlName="ogLive" (selectionChange)="onOgLiveChange($event)">
<mat-option *ngFor="let oglive of ogLives" [value]="oglive['@id']">
{{ oglive.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Repositorio</mat-label>
<mat-select formControlName="repository" (selectionChange)="onRepositoryChange($event)">
<mat-option *ngFor="let repository of repositories" [value]="repository['@id']">
{{ repository.name }}
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Proxy</mat-label>
<input matInput formControlName="proxy">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>DNS</mat-label>
<input matInput formControlName="dns">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Máscara de Red</mat-label>
<input matInput formControlName="netmask">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Router</mat-label>
<input matInput formControlName="router">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>NTP</mat-label>
<input matInput formControlName="ntp">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Modo P2P</mat-label>
<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>
<input matInput formControlName="p2pTime" type="number">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Mcast IP</mat-label>
<input matInput formControlName="mcastIp">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Mcast Speed</mat-label>
<input matInput formControlName="mcastSpeed" type="number">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Mcast Port</mat-label>
<input matInput formControlName="mcastPort" type="number">
</mat-form-field>
<mat-form-field class="form-field">
<mat-label>Mcast Mode</mat-label>
<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ú</mat-label>
<input matInput formControlName="menu" type="url">
</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.description
}}</mat-option>
</mat-select>
<mat-error>Formato de URL incorrecto</mat-error>
</mat-form-field>
<div class="validation-field">
<mat-slide-toggle formControlName="validation">Validación</mat-slide-toggle>
</div>
</form>
<!-- Paso 4: Información Adicional -->
<span class="step-title">Información Adicional</span>
<form [formGroup]="additionalInfoFormGroup">
<mat-form-field class="form-field">
<mat-label>Comentarios</mat-label>
<textarea matInput formControlName="comments"></textarea>
</mat-form-field>
</form>
</div>
<div class="mat-dialog-actions">
<button mat-button (click)="onNoClick()">Cancelar</button>
<button mat-button (click)="onSubmit()"
[disabled]="!generalFormGroup.valid || !additionalInfoFormGroup.valid || !networkSettingsFormGroup.valid">{{
isEditMode ? 'Editar' : 'Crear' }}</button>
</div>

View File

@ -0,0 +1,45 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ManageOrganizationalUnitComponent } from './manage-organizational-unit.component';
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
import { HttpClientTestingModule } from '@angular/common/http/testing';
import { ReactiveFormsModule } from '@angular/forms';
import { ToastrModule } from 'ngx-toastr';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
describe('ManageOrganizationalUnitComponent', () => {
let component: ManageOrganizationalUnitComponent;
let fixture: ComponentFixture<ManageOrganizationalUnitComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
declarations: [ManageOrganizationalUnitComponent],
imports: [
HttpClientTestingModule,
ReactiveFormsModule,
ToastrModule.forRoot(),
MatFormFieldModule,
MatInputModule,
MatSelectModule,
MatSlideToggleModule,
BrowserAnimationsModule
],
providers: [
{ provide: MatDialogRef, useValue: {} },
{ provide: MAT_DIALOG_DATA, useValue: {} }
]
})
.compileComponents();
fixture = TestBed.createComponent(ManageOrganizationalUnitComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -1,17 +1,16 @@
import {HttpClient, HttpHeaders} from '@angular/common/http';
import {Component, EventEmitter, Inject, OnInit, Output} from '@angular/core';
import {FormBuilder, FormGroup} from '@angular/forms';
import {MAT_DIALOG_DATA, MatDialogRef} from '@angular/material/dialog';
import {CreateOrganizationalUnitComponent} from '../create-organizational-unit/create-organizational-unit.component';
import {DataService} from "../../../services/data.service";
import {ToastrService} from "ngx-toastr";
import { HttpClient, HttpHeaders } from '@angular/common/http';
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 { DataService } from "../../../services/data.service";
import { ToastrService } from "ngx-toastr";
@Component({
selector: 'app-edit-organizational-unit',
templateUrl: './edit-organizational-unit.component.html',
styleUrl: './edit-organizational-unit.component.css'
selector: 'app-manage-organizational-unit',
templateUrl: './manage-organizational-unit.component.html',
styleUrls: ['./manage-organizational-unit.component.css']
})
export class EditOrganizationalUnitComponent implements OnInit {
export class ManageOrganizationalUnitComponent implements OnInit {
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
isLinear = true;
generalFormGroup: FormGroup;
@ -33,20 +32,20 @@ export class EditOrganizationalUnitComponent implements OnInit {
repositories: any[] = [];
parentUnitsWithPaths: { id: string, name: string, path: string }[] = [];
protected p2pModeOptions = [
{"name": 'Leecher', "value": "leecher"},
{"name": 'Peer', "value": "peer"},
{"name": 'Seeder', "value": "seeder"},
{ "name": 'Leecher', "value": "leecher" },
{ "name": 'Peer', "value": "peer" },
{ "name": 'Seeder', "value": "seeder" },
];
protected multicastModeOptions = [
{"name": 'Half duplex', "value": "half"},
{"name": 'Full duplex', "value": "full"},
{ "name": 'Half duplex', "value": "half" },
{ "name": 'Full duplex', "value": "full" },
];
@Output() unitAdded = new EventEmitter();
calendars: any;
constructor(
private _formBuilder: FormBuilder,
private dialogRef: MatDialogRef<CreateOrganizationalUnitComponent>,
private dialogRef: MatDialogRef<ManageOrganizationalUnitComponent>,
private http: HttpClient,
private dataService: DataService,
private toastService: ToastrService,
@ -55,10 +54,10 @@ export class EditOrganizationalUnitComponent implements OnInit {
this.isEditMode = !!data?.uuid;
this.generalFormGroup = this._formBuilder.group({
name: [null],
parent: [null],
name: [null, Validators.required],
parent: [data?.parent ? data.parent['@id'] : null],
description: [null],
type: [null]
type: [null, Validators.required]
});
this.additionalInfoFormGroup = this._formBuilder.group({
@ -88,7 +87,7 @@ export class EditOrganizationalUnitComponent implements OnInit {
location: [null],
projector: [false],
board: [false],
capacity: [null],
capacity: [null, [Validators.required, Validators.min(0)]],
remoteCalendar: [null]
});
@ -142,7 +141,9 @@ export class EditOrganizationalUnitComponent implements OnInit {
loadOgLives() {
this.dataService.getOgLives().subscribe(
(data: any[]) => this.ogLives = data,
(data: any[]) => {
this.ogLives = data
},
error => console.error('Error fetching ogLives', error)
);
}
@ -173,7 +174,8 @@ export class EditOrganizationalUnitComponent implements OnInit {
console.error('Error loading current calendar', error);
this.toastService.error('Error loading current calendar');
}
);}
);
}
onCalendarChange(event: any) {
this.generalFormGroup.value.remoteCalendar = event.value;
@ -184,12 +186,12 @@ export class EditOrganizationalUnitComponent implements OnInit {
}
onRepositoryChange(event: any) {
this.networkSettingsFormGroup.value.repository = event.value
this.networkSettingsFormGroup.value.repository = event.value;
}
loadData(uuid: string) {
const url = `${this.baseUrl}/organizational-units/${uuid}`;
this.http.get<any>(url).subscribe(
data => {
this.generalFormGroup.patchValue({
@ -227,14 +229,13 @@ export class EditOrganizationalUnitComponent implements OnInit {
remoteCalendar: data.remoteCalendar ? data.remoteCalendar['@id'] : null
});
},
error => {
console.error('Error fetching data for edit:', error);
this.toastService.error('Error fetching data');
this.onNoClick()
this.onNoClick();
}
);
console.log(this.classroomInfoFormGroup.value);
}
onSubmit() {
@ -247,18 +248,19 @@ export class EditOrganizationalUnitComponent implements OnInit {
comments: this.additionalInfoFormGroup.value.comments,
remoteCalendar: this.generalFormGroup.value.remoteCalendar,
type: this.generalFormGroup.value.type,
networkSettings: this.networkSettingsFormGroup.value
networkSettings: this.networkSettingsFormGroup.value,
location: this.classroomInfoFormGroup.value.location,
projector: this.classroomInfoFormGroup.value.projector,
board: this.classroomInfoFormGroup.value.board,
capacity: this.classroomInfoFormGroup.value.capacity,
};
if (this.isEditMode) {
const putUrl = `${this.baseUrl}/organizational-units/${this.data.uuid}`;
const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
console.log('PUT URLLLLL:', formData);
this.http.put<any>(putUrl, formData, { headers }).subscribe(
response => {
console.log('PUT successful:', response);
this.unitAdded.emit();
this.dialogRef.close();
this.toastService.success('Editado exitosamente', 'Éxito');
@ -271,16 +273,16 @@ export class EditOrganizationalUnitComponent implements OnInit {
} else {
const postUrl = `${this.baseUrl}/organizational-units`;
const headers = new HttpHeaders({ 'Content-Type': 'application/json' });
this.http.post<any>(postUrl, formData, { headers }).subscribe(
response => {
this.unitAdded.emit();
this.dialogRef.close();
this.toastService.success('Editado exitosamente', 'Éxito');
this.unitAdded.emit(response);
this.dialogRef.close(response);
this.toastService.success('Creado exitosamente', 'Éxito');
},
error => {
console.error('Error al realizar POST:', error);
this.toastService.error('Error al editar:', error.error['hydra:description']);
this.toastService.error('Error al crear:', error.error['hydra:description']);
}
);
}
@ -290,4 +292,4 @@ export class EditOrganizationalUnitComponent implements OnInit {
onNoClick(): void {
this.dialogRef.close();
}
}
}

View File

@ -1,7 +1,6 @@
import { HttpClient } from '@angular/common/http';
import {Component, Inject, OnInit} from '@angular/core';
import {MAT_DIALOG_DATA, MatDialogRef} from "@angular/material/dialog";
import {CreateOrganizationalUnitComponent} from "../create-organizational-unit/create-organizational-unit.component";
import {DatePipe} from "@angular/common";
@Component({