Merge pull request 'develop' (#36) from develop into main
testing/ogGui-multibranch/pipeline/head There was a failure building this commit Details
oggui-debian-package/pipeline/head This commit looks good Details
oggui-debian-package/pipeline/tag This commit looks good Details

Reviewed-on: #36
pull/37/head^2 0.20.0
Manuel Aranda Rosales 2025-08-25 13:13:31 +02:00
commit 8f4e7a7319
28 changed files with 1382 additions and 226 deletions

View File

@ -1,4 +1,13 @@
# Changelog # Changelog
## [0.20.0] - 2025-08-25
### Added
- Se ha añadido un nuevo boton en "Trazas" para marcar la misma como completada cuando se requiera.
- Nuevo estado "ocupado" en las trazas para indicar que el cliente envia un "409" y que ya esta ejecutando una accion
### Improved
- Mejorada la interfaz para gestionar las tareas.
---
## [0.19.0] - 2025-08-06 ## [0.19.0] - 2025-08-06
### Added ### Added
- Se ha añadido un nuevo estado "enviado" para cuando se ejecuten acciones a equipos en estado Windows o Linux - Se ha añadido un nuevo estado "enviado" para cuando se ejecuten acciones a equipos en estado Windows o Linux

View File

@ -77,3 +77,64 @@ table {
color: white !important; color: white !important;
} }
/* Estilos para los botones de gestión */
.schedule-btn, .script-btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 8px 16px;
border-radius: 6px;
font-weight: 500;
transition: all 0.3s ease;
border: none;
cursor: pointer;
margin: 4px;
line-height: 1;
vertical-align: middle;
}
.schedule-btn {
background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%);
color: white;
}
.schedule-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.3);
}
.script-btn {
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
color: white;
}
.script-btn:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
}
.schedule-btn mat-icon, .script-btn mat-icon {
font-size: 18px;
width: 18px;
height: 18px;
display: flex;
align-items: center;
justify-content: center;
}
.schedule-btn span, .script-btn span {
display: inline-block;
vertical-align: middle;
line-height: 1.2;
}
/* Responsive para los botones */
@media (max-width: 768px) {
.schedule-btn, .script-btn {
width: 100%;
justify-content: center;
margin: 4px 0;
}
}

View File

@ -33,8 +33,14 @@
</ng-container> </ng-container>
<ng-container *ngIf="column.columnDef === 'management'"> <ng-container *ngIf="column.columnDef === 'management'">
<button class="action-button" (click)="openShowScheduleDialog(task)"> Programaciones</button> <button class="action-button schedule-btn" (click)="openShowScheduleDialog(task)">
<button class="action-button" style="margin-left: 0.5vw;" (click)="openShowScriptDialog(task)">Acciones</button> <mat-icon>schedule</mat-icon>
<span>Programaciones</span>
</button>
<button class="action-button script-btn" (click)="openShowScriptDialog(task)">
<mat-icon>code</mat-icon>
<span>Acciones</span>
</button>
</ng-container> </ng-container>
</td> </td>
</ng-container> </ng-container>
@ -42,12 +48,6 @@
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th> <th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
<td mat-cell *matCellDef="let task" style="text-align: center;" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}"> <td mat-cell *matCellDef="let task" style="text-align: center;" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
<button mat-icon-button color="primary" (click)="manageScheduleAction(task)">
<mat-icon>watch</mat-icon>
</button>
<button mat-icon-button color="primary" (click)="manageScriptAction(task)">
<mat-icon>code-blocks</mat-icon>
</button>
<button mat-icon-button color="primary" (click)="editTask(task)"> <button mat-icon-button color="primary" (click)="editTask(task)">
<mat-icon>edit</mat-icon> <mat-icon>edit</mat-icon>
</button> </button>

View File

@ -3,16 +3,11 @@ import { HttpClient } from '@angular/common/http';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ToastrService } from 'ngx-toastr'; import { ToastrService } from 'ngx-toastr';
import { CreateTaskComponent } from './create-task/create-task.component'; import { CreateTaskComponent } from './create-task/create-task.component';
import { DetailTaskComponent } from './detail-task/detail-task.component';
import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component'; import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component';
import { JoyrideService } from 'ngx-joyride'; import { JoyrideService } from 'ngx-joyride';
import { ConfigService } from '@services/config.service'; import { ConfigService } from '@services/config.service';
import {CreateTaskScheduleComponent} from "./create-task-schedule/create-task-schedule.component";
import {ShowClientsComponent} from "../../ogdhcp/show-clients/show-clients.component";
import {Subnet} from "../../ogdhcp/og-dhcp-subnets.component";
import {ShowTaskScheduleComponent} from "./show-task-schedule/show-task-schedule.component"; import {ShowTaskScheduleComponent} from "./show-task-schedule/show-task-schedule.component";
import {ShowTaskScriptComponent} from "./show-task-script/show-task-script.component"; import {ShowTaskScriptComponent} from "./show-task-script/show-task-script.component";
import {CreateTaskScriptComponent} from "./create-task-script/create-task-script.component";
import {DatePipe} from "@angular/common"; import {DatePipe} from "@angular/common";
@Component({ @Component({
@ -114,28 +109,6 @@ export class CommandsTaskComponent implements OnInit {
}); });
} }
manageScheduleAction(task: any): void {
this.dialog.open(CreateTaskScheduleComponent, {
width: '800px',
data: { task },
}).afterClosed().subscribe( result => {
if (result) {
this.loadTasks();
}
})
}
manageScriptAction(task: any): void {
this.dialog.open(CreateTaskScriptComponent, {
width: '900px',
data: { task },
}).afterClosed().subscribe( result => {
if (result) {
this.loadTasks();
}
})
}
onPageChange(event: any): void { onPageChange(event: any): void {
this.page = event.pageIndex + 1; this.page = event.pageIndex + 1;
this.itemsPerPage = event.pageSize; this.itemsPerPage = event.pageSize;

View File

@ -1,128 +1,243 @@
.dialog-title { .dialog-title {
font-weight: bold; color: #333;
font-weight: 600;
border-bottom: 2px solid #e0e0e0;
padding-bottom: 16px;
margin: 0;
}
.dialog-content {
padding: 40px;
margin-top: 20px;
margin-bottom: 20px;
} }
.task-form { .task-form {
padding: 20px; display: flex;
flex-direction: column;
gap: 20px;
padding: 0;
} }
.full-width { .w-full {
width: 100%; width: 100%;
} }
.w-half {
width: 48%;
}
.custom-time { .custom-time {
display: flex; display: flex;
gap: 15px;
}
.w-half {
width: 50%;
}
mat-form-field {
margin-bottom: 16px;
}
form {
display: flex;
flex-direction: column;
gap: 16px; gap: 16px;
margin: auto; flex-wrap: wrap;
} }
mat-form-field { .section-label {
width: 100%; display: block;
} font-weight: 600;
color: #333;
.action-container { margin-bottom: 12px;
display: flex; font-size: 0.9rem;
justify-content: flex-end;
gap: 1em;
padding: 1.5em;
} }
.weekday-toggle-group { .weekday-toggle-group {
display: flex; display: flex;
justify-content: space-between;
gap: 8px; gap: 8px;
margin-bottom: 16px; flex-wrap: wrap;
} }
.weekday-toggle { .weekday-toggle {
flex: 1; padding: 8px 12px;
padding: 8px 0; border: 2px solid #e0e0e0;
border: 1px solid #ccc; background: white;
border-radius: 4px; border-radius: 20px;
background: #f5f5f5;
cursor: pointer; cursor: pointer;
transition: all 0.3s ease;
font-size: 0.85rem;
font-weight: 500; font-weight: 500;
transition: background 0.2s ease; min-width: 60px;
}
.weekday-toggle:hover {
border-color: #2196f3;
background: #f3f8ff;
} }
.weekday-toggle.selected { .weekday-toggle.selected {
background: #1976d2; background: #2196f3;
color: white; color: white;
border-color: #1976d2; border-color: #2196f3;
}
.month-toggle-group {
display: flex;
flex-wrap: wrap;
gap: 6px;
margin-bottom: 16px;
} }
.month-toggle-row { .month-toggle-row {
display: flex; display: flex;
gap: 8px; gap: 8px;
margin-bottom: 8px; margin-bottom: 8px;
justify-content: space-between;
} }
.month-toggle { .month-toggle {
flex: 1; padding: 6px 10px;
padding: 8px; border: 2px solid #e0e0e0;
min-width: 48px; background: white;
border: 1px solid #ccc; border-radius: 16px;
border-radius: 6px;
background-color: #f0f0f0;
text-align: center;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s ease; transition: all 0.3s ease;
font-size: 0.8rem;
font-weight: 500;
min-width: 50px;
}
.month-toggle:hover {
border-color: #4caf50;
background: #f1f8e9;
} }
.month-toggle.selected { .month-toggle.selected {
background-color: #4caf50; background: #4caf50;
color: white; color: white;
border-color: #4caf50; border-color: #4caf50;
} }
.summary-card { .enabled-checkbox {
background: #f9fafb;
border-left: 5px solid #3f51b5;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
border-radius: 12px;
padding: 16px;
transition: box-shadow 0.3s ease;
}
.summary-card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.summary-card {
background-color: #e3f2fd;
border-left: 4px solid #2196f3;
padding: 12px 16px;
margin-top: 16px;
border-radius: 6px;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
font-size: 14px; padding: 16px;
background: #f8f9fa;
border-radius: 8px;
border: 1px solid #e0e0e0;
}
.checkbox-icon {
color: #4caf50;
}
/* Resumen mejorado */
.summary-card {
background: linear-gradient(135deg, #e3f2fd 0%, #f3e5f5 100%);
border: 1px solid #bbdefb;
border-radius: 12px;
margin-top: 20px;
}
.summary-title {
display: flex;
align-items: center;
gap: 8px;
color: #1976d2;
font-size: 1.1rem;
margin: 0;
}
.summary-title mat-icon {
font-size: 24px;
}
.schedule-summary {
display: flex;
flex-direction: column;
gap: 12px;
}
.summary-item {
display: flex;
align-items: center;
gap: 12px;
padding: 8px 0;
border-bottom: 1px solid rgba(25, 118, 210, 0.1);
}
.summary-item:last-child {
border-bottom: none;
}
.summary-icon {
color: #1976d2;
font-size: 20px;
} }
.summary-text { .summary-text {
color: #0d47a1; color: #333;
font-size: 0.95rem;
line-height: 1.4; line-height: 1.4;
} }
/* Acciones */
.action-container {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 20px;
border-top: 1px solid #e0e0e0;
}
.ordinary-button {
background: #f5f5f5;
color: #333;
border: 1px solid #ddd;
padding: 10px 20px;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
}
.ordinary-button:hover {
background: #e0e0e0;
}
.submit-button {
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
color: white;
border: none;
padding: 10px 24px;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
}
.submit-button:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.3);
}
.submit-button:disabled {
background: #ccc;
cursor: not-allowed;
}
.submit-button mat-icon {
font-size: 18px;
}
/* Responsive */
@media (max-width: 768px) {
.custom-time {
flex-direction: column;
}
.w-half {
width: 100%;
}
.weekday-toggle-group {
justify-content: center;
}
.month-toggle-row {
justify-content: center;
}
.action-container {
flex-direction: column;
}
.submit-button, .ordinary-button {
width: 100%;
justify-content: center;
}
}

View File

@ -1,4 +1,6 @@
<h2 mat-dialog-title class="dialog-title">Programar accion</h2> <h2 mat-dialog-title class="dialog-title">
{{ data.schedule ? 'Editar' : 'Crear' }} Programación de Tarea
</h2>
<mat-dialog-content class="dialog-content"> <mat-dialog-content class="dialog-content">
<form [formGroup]="form" (ngSubmit)="onSubmit()" class="task-form"> <form [formGroup]="form" (ngSubmit)="onSubmit()" class="task-form">
@ -18,12 +20,13 @@
</mat-form-field> </mat-form-field>
<mat-form-field appearance="fill" class="w-full"> <mat-form-field appearance="fill" class="w-full">
<mat-label>Hora</mat-label> <mat-label>Hora de ejecución</mat-label>
<input matInput formControlName="executionTime" placeholder="08:00" type="time"> <input matInput formControlName="executionTime" placeholder="08:00" type="time">
<mat-hint>Hora en formato 24h (ej: 14:30)</mat-hint>
</mat-form-field> </mat-form-field>
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="mb-4"> <div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="mb-4">
<label>Días de la semana:</label> <label class="section-label">Días de la semana:</label>
<div class="weekday-toggle-group"> <div class="weekday-toggle-group">
<button <button
*ngFor="let day of weekDays" *ngFor="let day of weekDays"
@ -37,7 +40,7 @@
</div> </div>
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" > <div *ngIf="form.get('recurrenceType')?.value !== 'none'" >
<label>Meses:</label> <label class="section-label">Meses:</label>
<div class="month-toggle-row" *ngFor="let row of monthRows"> <div class="month-toggle-row" *ngFor="let row of monthRows">
<button <button
*ngFor="let month of row" *ngFor="let month of row"
@ -52,27 +55,49 @@
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="custom-time" formGroupName="recurrenceDetails"> <div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="custom-time" formGroupName="recurrenceDetails">
<mat-form-field appearance="fill" class="w-half"> <mat-form-field appearance="fill" class="w-half">
<mat-label>Desde</mat-label> <mat-label>Fecha de inicio</mat-label>
<input matInput [matDatepicker]="fromPicker" formControlName="initDate"> <input matInput [matDatepicker]="fromPicker" formControlName="initDate">
<mat-datepicker-toggle matSuffix [for]="fromPicker"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="fromPicker"></mat-datepicker-toggle>
<mat-datepicker #fromPicker></mat-datepicker> <mat-datepicker #fromPicker></mat-datepicker>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="fill" class="w-half"> <mat-form-field appearance="fill" class="w-half">
<mat-label>Hasta</mat-label> <mat-label>Fecha de fin</mat-label>
<input matInput [matDatepicker]="toPicker" formControlName="endDate"> <input matInput [matDatepicker]="toPicker" formControlName="endDate">
<mat-datepicker-toggle matSuffix [for]="toPicker"></mat-datepicker-toggle> <mat-datepicker-toggle matSuffix [for]="toPicker"></mat-datepicker-toggle>
<mat-datepicker #toPicker></mat-datepicker> <mat-datepicker #toPicker></mat-datepicker>
</mat-form-field> </mat-form-field>
</div> </div>
<mat-checkbox formControlName="enabled">Activar tarea</mat-checkbox> <mat-checkbox formControlName="enabled" class="enabled-checkbox">
<mat-icon class="checkbox-icon">power_settings_new</mat-icon>
Activar programación
</mat-checkbox>
<!-- Resumen mejorado de la programación -->
<mat-card *ngIf="summaryText" class="summary-card"> <mat-card *ngIf="summaryText" class="summary-card">
<mat-icon color="primary" style="width: 50px;">info</mat-icon> <mat-card-header>
<span class="summary-text"> <mat-card-title class="summary-title">
{{ summaryText }} <mat-icon color="primary">info</mat-icon>
</span> Resumen de la Programación
</mat-card-title>
</mat-card-header>
<mat-card-content>
<div class="schedule-summary">
<div class="summary-item">
<mat-icon class="summary-icon">schedule</mat-icon>
<span class="summary-text">{{ summaryText }}</span>
</div>
<div class="summary-item" *ngIf="nextExecutionDate">
<mat-icon class="summary-icon">next_plan</mat-icon>
<span class="summary-text">Próxima ejecución: {{ nextExecutionDate | date:'full' }}</span>
</div>
<div class="summary-item" *ngIf="executionCount">
<mat-icon class="summary-icon">repeat</mat-icon>
<span class="summary-text">Se ejecutará {{ executionCount }} veces</span>
</div>
</div>
</mat-card-content>
</mat-card> </mat-card>
</form> </form>
@ -80,5 +105,8 @@
<mat-dialog-actions class="action-container"> <mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="onCancel()">{{ 'buttonCancel' | translate }}</button> <button class="ordinary-button" (click)="onCancel()">{{ 'buttonCancel' | translate }}</button>
<button class="submit-button" (click)="onSubmit()" >{{ 'buttonSave' | translate }}</button> <button class="submit-button" [disabled]="!form.valid" (click)="onSubmit()">
<mat-icon>save</mat-icon>
{{ 'buttonSave' | translate }}
</button>
</mat-dialog-actions> </mat-dialog-actions>

View File

@ -26,6 +26,8 @@ export class CreateTaskScheduleComponent implements OnInit{
editing: boolean = false; editing: boolean = false;
selectedMonths: { [key: string]: boolean } = {}; selectedMonths: { [key: string]: boolean } = {};
selectedDays: { [key: string]: boolean } = {}; selectedDays: { [key: string]: boolean } = {};
nextExecutionDate: Date | null = null;
executionCount: number = 0;
constructor( constructor(
private fb: FormBuilder, private fb: FormBuilder,
@ -176,6 +178,9 @@ export class CreateTaskScheduleComponent implements OnInit{
const days = Object.keys(this.selectedDays).filter(day => this.selectedDays[day]); const days = Object.keys(this.selectedDays).filter(day => this.selectedDays[day]);
const months = Object.keys(this.selectedMonths).filter(month => this.selectedMonths[month]); const months = Object.keys(this.selectedMonths).filter(month => this.selectedMonths[month]);
// Calcular próxima ejecución y conteo
this.calculateNextExecutionAndCount();
if (recurrence === 'none') { if (recurrence === 'none') {
return `Esta acción se ejecutará una sola vez el ${ this.formatDate(start)} a las ${time}.`; return `Esta acción se ejecutará una sola vez el ${ this.formatDate(start)} a las ${time}.`;
} }
@ -183,6 +188,45 @@ export class CreateTaskScheduleComponent implements OnInit{
return `Esta acción se ejecutará todos los ${days.join(', ')} de ${months.join(', ')}, desde el ${this.formatDate(start)} hasta el ${this.formatDate(end)} a las ${time}.`; return `Esta acción se ejecutará todos los ${days.join(', ')} de ${months.join(', ')}, desde el ${this.formatDate(start)} hasta el ${this.formatDate(end)} a las ${time}.`;
} }
private calculateNextExecutionAndCount(): void {
const recurrence = this.form.get('recurrenceType')?.value;
const time = this.form.get('executionTime')?.value;
if (recurrence === 'none') {
const execDate = this.form.get('executionDate')?.value;
if (execDate && time) {
this.nextExecutionDate = new Date(execDate);
const [hours, minutes] = time.split(':');
this.nextExecutionDate.setHours(parseInt(hours), parseInt(minutes), 0, 0);
this.executionCount = 1;
}
} else {
const startDate = this.form.get('recurrenceDetails.initDate')?.value;
const endDate = this.form.get('recurrenceDetails.endDate')?.value;
const days = Object.keys(this.selectedDays).filter(day => this.selectedDays[day]);
const months = Object.keys(this.selectedMonths).filter(month => this.selectedMonths[month]);
if (startDate && endDate && days.length > 0 && months.length > 0) {
// Calcular próxima ejecución (simplificado)
this.nextExecutionDate = new Date(startDate);
const [hours, minutes] = time.split(':');
this.nextExecutionDate.setHours(parseInt(hours), parseInt(minutes), 0, 0);
// Calcular número aproximado de ejecuciones
this.executionCount = this.calculateExecutionCount(startDate, endDate, days.length, months.length);
}
}
}
private calculateExecutionCount(startDate: Date, endDate: Date, daysCount: number, monthsCount: number): number {
const start = new Date(startDate);
const end = new Date(endDate);
const daysDiff = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24));
// Aproximación: días seleccionados por semana * semanas * meses activos
return Math.ceil((daysCount / 7) * (daysDiff / 7) * (monthsCount / 12));
}
formatDate(date: string | Date): string { formatDate(date: string | Date): string {
const realDate = (date instanceof Date) ? date : new Date(date); const realDate = (date instanceof Date) ? date : new Date(date);
return new Intl.DateTimeFormat('es-ES', { dateStyle: 'long' }).format(realDate); return new Intl.DateTimeFormat('es-ES', { dateStyle: 'long' }).format(realDate);

View File

@ -275,10 +275,134 @@ table {
padding: 16px; padding: 16px;
} }
.toggle-options { /* Estilos para las pestañas principales */
.action-tabs {
margin-top: 20px;
}
.action-tabs ::ng-deep .mat-tab-header {
border-bottom: 2px solid #e0e0e0;
margin-bottom: 20px;
}
.action-tabs ::ng-deep .mat-tab-label {
min-width: 160px;
padding: 12px 24px;
font-weight: 500;
color: #666;
transition: all 0.3s ease;
}
.action-tabs ::ng-deep .mat-tab-label:hover {
color: #1976d2;
background-color: rgba(25, 118, 210, 0.04);
}
.action-tabs ::ng-deep .mat-tab-label.mat-tab-label-active {
color: #1976d2;
font-weight: 600;
}
.action-tabs ::ng-deep .mat-ink-bar {
background-color: #1976d2;
height: 3px;
}
/* Contenido de las pestañas */
.tab-content {
padding: 20px 0;
}
.action-description {
display: flex; display: flex;
justify-content: start; align-items: center;
margin: 16px 0; gap: 12px;
margin-bottom: 24px;
padding: 16px;
background-color: #f8f9fa;
border-radius: 8px;
border-left: 4px solid #1976d2;
}
.action-description mat-icon {
color: #1976d2;
font-size: 24px;
width: 24px;
height: 24px;
}
.action-description span {
color: #495057;
font-size: 14px;
font-weight: 500;
}
/* Selector de tipo de script */
.script-type-selector {
margin-bottom: 24px;
padding: 20px;
background-color: #f8f9fa;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.radio-group {
display: flex;
gap: 24px;
}
.radio-button {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
border-radius: 6px;
transition: all 0.2s ease;
}
.radio-button:hover {
background-color: rgba(25, 118, 210, 0.04);
}
.radio-button mat-icon {
color: #1976d2;
font-size: 20px;
width: 20px;
height: 20px;
}
/* Contenedor de acciones básicas */
.basic-action-container {
display: flex;
flex-direction: column;
gap: 16px;
}
/* Contenedor de scripts */
.script-options {
display: flex;
flex-direction: column;
gap: 16px;
}
/* Responsive design */
@media (max-width: 768px) {
.action-tabs ::ng-deep .mat-tab-label {
min-width: 120px;
padding: 8px 16px;
font-size: 14px;
}
.radio-group {
flex-direction: column;
gap: 16px;
}
.action-description {
flex-direction: column;
text-align: center;
gap: 8px;
}
} }

View File

@ -3,20 +3,67 @@
<mat-dialog-content class="dialog-content"> <mat-dialog-content class="dialog-content">
<div class="task-form"> <div class="task-form">
<div class="toggle-options"> <!-- Pestañas principales -->
<mat-button-toggle-group [(ngModel)]="commandType" exclusive> <mat-tab-group [(selectedIndex)]="selectedTabIndex" (selectedIndexChange)="onTabChange($event)" class="action-tabs">
<mat-button-toggle value="new">
<mat-icon>edit</mat-icon> Nuevo Script <!-- Pestaña de Acciones Básicas -->
</mat-button-toggle> <mat-tab label="Acciones Básicas" class="basic-tab">
<mat-button-toggle value="existing"> <div class="tab-content">
<mat-icon>storage</mat-icon> Script Guardado <div class="action-description">
</mat-button-toggle> <mat-icon>power_settings_new</mat-icon>
</mat-button-toggle-group> <span>Selecciona una acción básica del sistema</span>
</div> </div>
<mat-form-field appearance="fill" class="full-width">
<mat-label>Tipo de Acción</mat-label>
<mat-select [(ngModel)]="selectedBasicAction" (selectionChange)="onBasicActionChange()">
<mat-option value="poweron">
<mat-icon>power</mat-icon> Encender Equipo
</mat-option>
<mat-option value="shutdown">
<mat-icon>power_off</mat-icon> Apagar Equipo
</mat-option>
<mat-option value="reboot">
<mat-icon>restart_alt</mat-icon> Reiniciar Equipo
</mat-option>
<mat-option value="login">
<mat-icon>login</mat-icon> Iniciar Sesión
</mat-option>
</mat-select>
</mat-form-field>
<mat-form-field appearance="fill" class="custom-width">
<mat-label>Orden de ejecución</mat-label>
<input matInput type="number" [(ngModel)]="executionOrder" placeholder="Orden de ejecución">
</mat-form-field>
</div>
</mat-tab>
<!-- Pestaña de Scripts -->
<mat-tab label="Ejecutar Script" class="script-tab">
<div class="tab-content">
<div class="action-description">
<mat-icon>code</mat-icon>
<span>Ejecuta un script personalizado o crea uno nuevo</span>
</div>
<!-- Opciones de script -->
<div class="script-options">
<div class="script-type-selector">
<mat-radio-group [(ngModel)]="commandType" class="radio-group">
<mat-radio-button value="new" class="radio-button">
<mat-icon>edit</mat-icon> Nuevo Script
</mat-radio-button>
<mat-radio-button value="existing" class="radio-button">
<mat-icon>storage</mat-icon> Script Guardado
</mat-radio-button>
</mat-radio-group>
</div>
<!-- Nuevo Script -->
<div *ngIf="commandType === 'new'" class="new-command-container"> <div *ngIf="commandType === 'new'" class="new-command-container">
<mat-form-field appearance="fill" class="custom-width"> <mat-form-field appearance="fill" class="custom-width">
<mat-label>Orden de ejecucion </mat-label> <mat-label>Orden de ejecución</mat-label>
<input matInput type="number" [(ngModel)]="executionOrder" placeholder="Orden de ejecución"> <input matInput type="number" [(ngModel)]="executionOrder" placeholder="Orden de ejecución">
</mat-form-field> </mat-form-field>
<mat-form-field appearance="fill" class="full-width"> <mat-form-field appearance="fill" class="full-width">
@ -26,6 +73,7 @@
<button mat-flat-button color="primary" (click)="saveNewScript()">Guardar Script</button> <button mat-flat-button color="primary" (click)="saveNewScript()">Guardar Script</button>
</div> </div>
<!-- Script Existente -->
<div *ngIf="commandType === 'existing'"> <div *ngIf="commandType === 'existing'">
<mat-form-field appearance="fill" class="custom-width"> <mat-form-field appearance="fill" class="custom-width">
<mat-label>Seleccione script a ejecutar</mat-label> <mat-label>Seleccione script a ejecutar</mat-label>
@ -35,9 +83,10 @@
</mat-form-field> </mat-form-field>
</div> </div>
<!-- Detalles del Script -->
<div *ngIf="selectedScript && commandType === 'existing'" class="script-container"> <div *ngIf="selectedScript && commandType === 'existing'" class="script-container">
<mat-form-field appearance="fill" class="custom-width"> <mat-form-field appearance="fill" class="custom-width">
<mat-label>Orden de ejecucion </mat-label> <mat-label>Orden de ejecución</mat-label>
<input matInput type="number" [(ngModel)]="executionOrder" placeholder="Orden de ejecución"> <input matInput type="number" [(ngModel)]="executionOrder" placeholder="Orden de ejecución">
</mat-form-field> </mat-form-field>
@ -57,6 +106,10 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</mat-tab>
</mat-tab-group>
</div>
</mat-dialog-content> </mat-dialog-content>
<mat-dialog-actions class="action-container"> <mat-dialog-actions class="action-container">

View File

@ -24,13 +24,15 @@ export class CreateTaskScriptComponent implements OnInit {
scripts: any[] = []; scripts: any[] = [];
scriptContent: string = ""; scriptContent: string = "";
parameters: any = {}; parameters: any = {};
selectedTabIndex: number = 0;
selectedBasicAction: string = '';
commandType: string = 'existing'; commandType: string = 'existing';
selectedScript: any = null; selectedScript: any = null;
newScript: string = ''; newScript: string = '';
executionOrder: Number = 0; executionOrder: Number = 0;
selection = new SelectionModel(true, []); selection = new SelectionModel(true, []);
parameterNames: string[] = Object.keys(this.parameters); parameterNames: string[] = Object.keys(this.parameters);
commandTask: any;
constructor( constructor(
private fb: FormBuilder, private fb: FormBuilder,
private http: HttpClient, private http: HttpClient,
@ -43,6 +45,8 @@ export class CreateTaskScriptComponent implements OnInit {
@Inject(MAT_DIALOG_DATA) public data: any @Inject(MAT_DIALOG_DATA) public data: any
) { ) {
this.baseUrl = this.configService.apiUrl; this.baseUrl = this.configService.apiUrl;
this.commandTask = this.data.commandTask;
this.loadScripts() this.loadScripts()
this.form = this.fb.group({ this.form = this.fb.group({
content: [''], content: [''],
@ -66,6 +70,46 @@ export class CreateTaskScriptComponent implements OnInit {
}); });
} }
onTabChange(index: number): void {
this.selectedTabIndex = index;
if (index === 0) {
this.selectedBasicAction = '';
} else {
this.selectedScript = null;
this.newScript = '';
}
}
onBasicActionChange(): void {
this.parameters = {};
this.parameterNames = Object.keys(this.parameters);
}
getBasicActionType(): string {
const actionMap: { [key: string]: string } = {
'poweron': 'POWER-ON',
'shutdown': 'SHUTDOWN',
'reboot': 'REBOOT',
'login': 'LOGIN',
};
return actionMap[this.selectedBasicAction] || '';
}
validateBasicAction(): boolean {
if (!this.selectedBasicAction) {
this.toastService.error('Debe seleccionar un tipo de acción');
return false;
}
if (this.executionOrder === null || this.executionOrder === undefined) {
this.toastService.error('Debe especificar el orden de ejecución');
return false;
}
return true;
}
saveNewScript() { saveNewScript() {
if (!this.newScript.trim()) { if (!this.newScript.trim()) {
this.toastService.error('Debe ingresar un script antes de guardar.'); this.toastService.error('Debe ingresar un script antes de guardar.');
@ -115,12 +159,60 @@ export class CreateTaskScriptComponent implements OnInit {
this.scriptContent = updatedScript; this.scriptContent = updatedScript;
} }
validateScript(): boolean {
if (this.commandType === 'new') {
if (!this.newScript.trim()) {
this.toastService.error('Debe ingresar un script');
return false;
}
} else if (this.commandType === 'existing') {
if (!this.selectedScript) {
this.toastService.error('Debe seleccionar un script');
return false;
}
}
if (this.executionOrder === null || this.executionOrder === undefined) {
this.toastService.error('Debe especificar el orden de ejecución');
return false;
}
return true;
}
onSubmit() { onSubmit() {
let isValid = false;
let content = '';
let type = '';
if (this.selectedTabIndex === 0) {
isValid = this.validateBasicAction();
if (isValid) {
content = this.getBasicActionType();
type = 'basic-action';
}
} else { // Pestaña de scripts
isValid = this.validateScript();
if (isValid) {
content = this.commandType === 'existing' ? this.scriptContent : this.newScript;
type = 'run-script';
}
}
if (!isValid) {
return;
}
if (!this.data) {
this.toastService.error('Error: No se recibieron datos del diálogo');
return;
}
this.http.post(`${this.baseUrl}/command-task-scripts`, { this.http.post(`${this.baseUrl}/command-task-scripts`, {
commandTask: this.data.task['@id'], commandTask: this.commandTask['@id'],
content: this.commandType === 'existing' ? this.scriptContent : this.newScript, content: content,
order: this.executionOrder, order: this.executionOrder,
type: 'run-script', type: this.selectedBasicAction,
}).subscribe({ }).subscribe({
next: () => { next: () => {
this.toastService.success('Tarea creada con éxito'); this.toastService.success('Tarea creada con éxito');

View File

@ -269,7 +269,7 @@ export class CreateTaskComponent implements OnInit {
}; };
if (scope === 'clients') { if (scope === 'clients') {
payload.clients = this.selectedClients.map(client => client.uuid); payload.clients = this.selectedClients.map(client => client['@id'] ? client['@id'] : client.uuid);
payload.organizationalUnit = null; payload.organizationalUnit = null;
} else { } else {
payload.organizationalUnit = formData.organizationalUnit; payload.organizationalUnit = formData.organizationalUnit;

View File

@ -87,3 +87,122 @@ mat-spinner {
display: flex; display: flex;
gap: 15px; gap: 15px;
} }
.header-actions {
display: flex;
justify-content: flex-end;
margin-bottom: 20px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
}
.action-button {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
color: white;
border: none;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
}
.action-button:hover {
background: #1565c0;
}
.action-button mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
}
.create-button {
color: white;
border: none;
padding: 12px 24px;
font-weight: 500;
}
.create-button mat-icon {
margin-right: 8px;
}
.search-container {
display: flex;
gap: 16px;
margin: 20px 0;
flex-wrap: wrap;
}
.search-string {
min-width: 250px;
}
/* Estilos para los chips de días y meses */
.days-display, .months-display {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.day-chip, .month-chip {
background: #e3f2fd;
color: #1976d2;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
border: 1px solid #bbdefb;
}
/* Estilos para la próxima ejecución */
.next-execution {
display: flex;
align-items: center;
gap: 8px;
color: #555;
}
.execution-icon {
font-size: 18px;
color: #2196f3;
}
/* Mejoras en la tabla */
.mat-table {
border-radius: 8px;
overflow: hidden;
}
.mat-header-cell {
background: #f5f5f5;
font-weight: 600;
color: #333;
}
.mat-row:hover {
background: #f8f9fa;
}
/* Responsive */
@media (max-width: 768px) {
.search-container {
flex-direction: column;
}
.search-string {
min-width: auto;
width: 100%;
}
.header-actions {
justify-content: center;
}
.create-button {
width: 100%;
}
}

View File

@ -3,26 +3,33 @@
<h2 mat-dialog-title>Gestionar programaciones de tareas en {{ data.commandTask?.name }}</h2> <h2 mat-dialog-title>Gestionar programaciones de tareas en {{ data.commandTask?.name }}</h2>
<mat-dialog-content> <mat-dialog-content>
<div class="header-actions">
<button (click)="createNewSchedule()" class="action-button">
<mat-icon>add</mat-icon>
Nueva Programación
</button>
</div>
<div class="search-container"> <div class="search-container">
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchNameStep" <mat-form-field appearance="fill" class="search-string" joyrideStep="searchNameStep"
text="Busca subredes por nombre para localizar una subred específica rápidamente."> text="Busca programaciones por nombre para localizar una programación específica rápidamente.">
<mat-label i18n="@@searchLabel">Buscar nombre del cliente</mat-label> <mat-label i18n="@@searchLabel">Buscar programación</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" i18n-placeholder="@@searchPlaceholder" <input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" i18n-placeholder="@@searchPlaceholder"
(keyup.enter)="loadData()" i18n-placeholder="@@searchPlaceholder"> (keyup.enter)="loadData()" i18n-placeholder="@@searchPlaceholder">
<mat-icon matSuffix>search</mat-icon> <mat-icon matSuffix>search</mat-icon>
<button *ngIf="filters['name']" mat-icon-button matSuffix aria-label="Clear tree search" <button *ngIf="filters['name']" mat-icon-button matSuffix aria-label="Clear search"
(click)="filters['name'] = ''; loadData()"> (click)="filters['name'] = ''; loadData()">
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
</button> </button>
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint> <mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
</mat-form-field> </mat-form-field>
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchIpStep" text="Busca programaciones por tipo."> <mat-form-field appearance="fill" class="search-string" joyrideStep="searchTypeStep" text="Busca programaciones por tipo de recurrencia.">
<mat-label i18n="@@searchLabel">Buscar por tipo</mat-label> <mat-label i18n="@@searchLabel">Buscar por tipo</mat-label>
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['recurrence']" i18n-placeholder="@@searchPlaceholder" <input matInput placeholder="Búsqueda" [(ngModel)]="filters['recurrence']" i18n-placeholder="@@searchPlaceholder"
(keyup.enter)="loadData()" i18n-placeholder="@@searchPlaceholder"> (keyup.enter)="loadData()" i18n-placeholder="@@searchPlaceholder">
<mat-icon matSuffix>search</mat-icon> <mat-icon matSuffix>search</mat-icon>
<button *ngIf="filters['ip']" mat-icon-button matSuffix aria-label="Clear tree search" <button *ngIf="filters['recurrence']" mat-icon-button matSuffix aria-label="Clear type search"
(click)="filters['ip'] = ''; loadData()"> (click)="filters['recurrence'] = ''; loadData()">
<mat-icon>close</mat-icon> <mat-icon>close</mat-icon>
</button> </button>
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint> <mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
@ -31,7 +38,7 @@
<app-loading [isLoading]="loading"></app-loading> <app-loading [isLoading]="loading"></app-loading>
<table *ngIf="!loading" mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" <table *ngIf="!loading" mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
text="Visualiza y administra las subredes listadas según los filtros aplicados."> text="Visualiza y administra las programaciones de tareas según los filtros aplicados.">
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef"> <ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th> <th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
<td mat-cell *matCellDef="let schedule"> <td mat-cell *matCellDef="let schedule">
@ -47,19 +54,45 @@
<ng-template #scheduledTemplate> <ng-template #scheduledTemplate>
Programado Programado
<div style="font-size: 12px;"> <div style="font-size: 12px;">
{{ schedule.recurrenceDetails.initDate | date }} → {{ schedule.recurrenceDetails.endDate | date}} {{ schedule.recurrenceDetails?.initDate | date }} → {{ schedule.recurrenceDetails?.endDate | date}}
</div> </div>
</ng-template> </ng-template>
</mat-chip> </mat-chip>
</ng-container> </ng-container>
<ng-container *ngIf="column.columnDef === 'executionTime'"> <ng-container *ngIf="column.columnDef === 'executionTime'">
{{ schedule.executionTime | date: 'HH:mm' }} {{ schedule.executionTime | date: 'HH:mm' }}
</ng-container> </ng-container>
<ng-container *ngIf="column.columnDef !== 'recurrenceType' && column.columnDef !== 'executionTime' && column.columnDef !== 'enabled'">
<ng-container *ngIf="column.columnDef === 'nextExecution'">
<div class="next-execution">
<mat-icon class="execution-icon">schedule</mat-icon>
<span>{{ column.cell(schedule) || 'No calculada' }}</span>
</div>
</ng-container>
<ng-container *ngIf="column.columnDef === 'daysOfWeek'">
<div class="days-display">
<span *ngFor="let day of schedule.recurrenceDetails?.daysOfWeek"
class="day-chip">{{ day | slice:0:3 }}</span>
</div>
</ng-container>
<ng-container *ngIf="column.columnDef === 'months'">
<div class="months-display">
<span *ngFor="let month of schedule.recurrenceDetails?.months"
class="month-chip">{{ month | slice:0:3 }}</span>
</div>
</ng-container>
<ng-container *ngIf="column.columnDef !== 'recurrenceType' && column.columnDef !== 'executionTime' &&
column.columnDef !== 'daysOfWeek' && column.columnDef !== 'months' &&
column.columnDef !== 'nextExecution' && column.columnDef !== 'enabled'">
{{ column.cell(schedule) }} {{ column.cell(schedule) }}
</ng-container> </ng-container>
<ng-container *ngIf="column.columnDef === 'enabled'"> <ng-container *ngIf="column.columnDef === 'enabled'">
<mat-chip> <mat-chip [color]="schedule.enabled ? 'accent' : 'warn'" selected>
<ng-container *ngIf="schedule.enabled"> <ng-container *ngIf="schedule.enabled">
Activo Activo
</ng-container> </ng-container>
@ -74,10 +107,10 @@
<ng-container matColumnDef="actions"> <ng-container matColumnDef="actions">
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th> <th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
<td mat-cell *matCellDef="let schedule" style="text-align: center;"> <td mat-cell *matCellDef="let schedule" style="text-align: center;">
<button mat-icon-button color="primary" (click)="editSchedule(schedule)"> <button mat-icon-button color="primary" (click)="editSchedule(schedule)" matTooltip="Editar programación">
<mat-icon i18n="@@deleteElementTooltip">edit</mat-icon> <mat-icon i18n="@@editElementTooltip">edit</mat-icon>
</button> </button>
<button mat-icon-button color="warn" (click)="deleteSchedule(schedule)"> <button mat-icon-button color="warn" (click)="deleteSchedule(schedule)" matTooltip="Eliminar programación">
<mat-icon i18n="@@deleteElementTooltip">delete</mat-icon> <mat-icon i18n="@@deleteElementTooltip">delete</mat-icon>
</button> </button>
</td> </td>
@ -88,7 +121,7 @@
</table> </table>
<div class="paginator-container" joyrideStep="paginationStep" <div class="paginator-container" joyrideStep="paginationStep"
text="Navega entre las páginas de subredes usando el paginador."> text="Navega entre las páginas de programaciones usando el paginador.">
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions" <mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
(page)="onPageChange($event)"> (page)="onPageChange($event)">
</mat-paginator> </mat-paginator>

View File

@ -28,13 +28,14 @@ export class ShowTaskScheduleComponent implements OnInit{
columns = [ columns = [
{ columnDef: 'id', header: 'ID', cell: (schedule: any) => schedule.id }, { columnDef: 'id', header: 'ID', cell: (schedule: any) => schedule.id },
{ columnDef: 'recurrenceType', header: 'Recurrencia', cell: (schedule: any) => schedule.recurrenceType }, { columnDef: 'recurrenceType', header: 'Recurrencia', cell: (schedule: any) => schedule.recurrenceType },
{ columnDef: 'time', header: 'Hora de ejecución', cell: (schedule: any) => this.datePipe.transform(schedule.executionTime, 'HH:mm') }, { columnDef: 'executionTime', header: 'Hora de ejecución', cell: (schedule: any) => this.datePipe.transform(schedule.executionTime, 'HH:mm') },
{ columnDef: 'daysOfWeek', header: 'Dias de la semana', cell: (schedule: any) => schedule.recurrenceDetails.daysOfWeek }, { columnDef: 'nextExecution', header: 'Próxima ejecución', cell: (schedule: any) => this.calculateNextExecution(schedule) },
{ columnDef: 'months', header: 'Meses', cell: (schedule: any) => schedule.recurrenceDetails.months }, { columnDef: 'daysOfWeek', header: 'Días de la semana', cell: (schedule: any) => schedule.recurrenceDetails?.daysOfWeek || [] },
{ columnDef: 'enabled', header: 'Activo', cell: (schedule: any) => schedule.enabled } { columnDef: 'months', header: 'Meses', cell: (schedule: any) => schedule.recurrenceDetails?.months || [] },
{ columnDef: 'enabled', header: 'Estado', cell: (schedule: any) => schedule.enabled }
]; ];
displayedColumns: string[] = ['id', 'recurrenceType', 'time', 'daysOfWeek', 'months', 'enabled', 'actions']; displayedColumns: string[] = ['id', 'recurrenceType', 'executionTime', 'nextExecution', 'daysOfWeek', 'months', 'enabled', 'actions'];
constructor( constructor(
private toastService: ToastrService, private toastService: ToastrService,
@ -63,21 +64,39 @@ export class ShowTaskScheduleComponent implements OnInit{
}, },
(error) => { (error) => {
this.loading = false; this.loading = false;
this.toastService.error('Error al cargar las programaciones');
} }
); );
} }
createNewSchedule(): void {
this.dialog.open(CreateTaskScheduleComponent, {
width: '800px',
data: { task: this.data.commandTask }
}).afterClosed().subscribe(result => {
if (result) {
this.loadData();
this.toastService.success('Programación creada correctamente');
}
});
}
editSchedule(schedule: any): void { editSchedule(schedule: any): void {
this.dialog.open(CreateTaskScheduleComponent, { this.dialog.open(CreateTaskScheduleComponent, {
width: '800px', width: '800px',
data: { schedule: schedule, task: this.data.commandTask } data: { schedule: schedule, task: this.data.commandTask }
}).afterClosed().subscribe(() => this.loadData()); }).afterClosed().subscribe(result => {
if (result) {
this.loadData();
this.toastService.success('Programación actualizada correctamente');
}
});
} }
deleteSchedule(schedule: any): void { deleteSchedule(schedule: any): void {
const dialogRef = this.dialog.open(DeleteModalComponent, { const dialogRef = this.dialog.open(DeleteModalComponent, {
width: '300px', width: '300px',
data: { name: 'tarea programada' } data: { name: 'programación de tarea' }
}); });
dialogRef.afterClosed().subscribe(result => { dialogRef.afterClosed().subscribe(result => {
@ -88,20 +107,52 @@ export class ShowTaskScheduleComponent implements OnInit{
this.loadData(); this.loadData();
}, },
(error) => { (error) => {
this.toastService.error(error.error['hydra:description']); this.toastService.error(error.error['hydra:description'] || 'Error al eliminar la programación');
} }
); );
} }
}) })
} }
calculateNextExecution(schedule: any): string {
if (!schedule.enabled) {
return 'Deshabilitada';
}
try {
if (schedule.recurrenceType === 'none') {
if (schedule.executionDate) {
const execDate = new Date(schedule.executionDate);
const now = new Date();
if (execDate < now) {
return 'Ya ejecutada';
}
return this.datePipe.transform(execDate, 'dd/MM/yyyy HH:mm') || 'Fecha inválida';
}
return 'Sin fecha';
}
if (schedule.recurrenceDetails) {
const days = schedule.recurrenceDetails.daysOfWeek?.join(', ') || 'Todos';
const months = schedule.recurrenceDetails.months?.join(', ') || 'Todos';
return `${days} - ${months}`;
}
return 'Configuración incompleta';
} catch (error) {
return 'Error en cálculo';
}
}
onPageChange(event: any): void {
this.page = event.pageIndex;
this.itemsPerPage = event.pageSize;
this.loadData();
}
onNoClick(): void { onNoClick(): void {
this.dialogRef.close(false); this.dialogRef.close(false);
} }
onPageChange(event: any) {
this.page = event.pageIndex;
this.itemsPerPage = event.pageSize;
this.loadData()
}
} }

View File

@ -87,3 +87,222 @@ mat-spinner {
display: flex; display: flex;
gap: 15px; gap: 15px;
} }
/* Estilos para el header de acciones */
.header-actions {
display: flex;
justify-content: flex-end;
margin-bottom: 20px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
}
.action-button {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 24px;
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
color: white;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 8px rgba(76, 175, 80, 0.3);
}
.action-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(76, 175, 80, 0.4);
background: linear-gradient(135deg, #45a049 0%, #3d8b40 100%);
}
.action-button mat-icon {
font-size: 20px;
width: 20px;
height: 20px;
}
/* Estilos para el contenido del script */
.script-content {
background: #f8f9fa;
border: 1px solid #e9ecef;
border-radius: 8px;
overflow: hidden;
transition: all 0.3s ease;
}
.script-content:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
border-color: #4caf50;
}
.script-header {
display: flex;
align-items: center;
gap: 8px;
padding: 12px 16px;
background: linear-gradient(135deg, #4caf50 0%, #45a049 100%);
color: white;
font-weight: 500;
}
.script-icon {
font-size: 18px;
width: 18px;
height: 18px;
}
.script-title {
font-size: 14px;
}
.script-body {
padding: 16px;
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
line-height: 1.5;
white-space: pre-wrap;
background: #ffffff;
color: #333;
max-height: 200px;
overflow-y: auto;
}
/* Estilos para el botón de parámetros */
.parameters-button {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 16px;
background: linear-gradient(135deg, #2196f3 0%, #1976d2 100%);
color: white;
border: none;
border-radius: 6px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
box-shadow: 0 2px 6px rgba(33, 150, 243, 0.3);
}
.parameters-button:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px rgba(33, 150, 243, 0.4);
background: linear-gradient(135deg, #1976d2 0%, #1565c0 100%);
}
.parameters-button mat-icon {
font-size: 16px;
width: 16px;
height: 16px;
}
.parameters-button span {
font-size: 13px;
}
/* Mejoras en la tabla */
.mat-table {
border-radius: 8px;
overflow: hidden;
}
.mat-header-cell {
background: #f5f5f5;
font-weight: 600;
color: #333;
}
.mat-row:hover {
background: #f8f9fa;
}
/* Responsive */
@media (max-width: 768px) {
.search-container {
flex-direction: column;
}
.search-string {
min-width: auto;
width: 100%;
}
.header-actions {
justify-content: center;
}
.action-button {
width: 100%;
}
.script-body {
max-height: 150px;
}
}
/* Estilos para los chips de tipo */
.type-chip {
font-weight: 500;
padding: 8px 12px;
border-radius: 16px;
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.type-script {
background: #e8f5e8;
color: #2e7d32;
border: 1px solid #c8e6c9;
}
.type-command {
background: #fff3e0;
color: #f57c00;
border: 1px solid #ffcc80;
}
.type-python {
background: #e3f2fd;
color: #1976d2;
border: 1px solid #bbdefb;
}
.type-bash {
background: #f3e5f5;
color: #7b1fa2;
border: 1px solid #e1bee7;
}
.type-default {
background: #f5f5f5;
color: #616161;
border: 1px solid #e0e0e0;
}
/* Estilos para el indicador de orden */
.order-indicator {
display: flex;
align-items: center;
gap: 8px;
padding: 6px 12px;
background: #f8f9fa;
border-radius: 20px;
border: 1px solid #e9ecef;
}
.order-icon {
font-size: 16px;
width: 16px;
height: 16px;
color: #6c757d;
}
.order-number {
font-weight: 600;
color: #495057;
font-size: 14px;
}

View File

@ -3,6 +3,13 @@
<h2 mat-dialog-title>Gestionar scripts de tareas en {{ data.commandTask?.name }}</h2> <h2 mat-dialog-title>Gestionar scripts de tareas en {{ data.commandTask?.name }}</h2>
<mat-dialog-content> <mat-dialog-content>
<div class="header-actions">
<button (click)="createNewScript()" class="action-button">
<mat-icon>add</mat-icon>
Nueva Acción
</button>
</div>
<div class="search-container"> <div class="search-container">
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchNameStep" <mat-form-field appearance="fill" class="search-string" joyrideStep="searchNameStep"
text="Busca subredes por nombre para localizar una subred específica rápidamente."> text="Busca subredes por nombre para localizar una subred específica rápidamente.">
@ -32,8 +39,9 @@
<ng-template #checkOtherColumn> <ng-template #checkOtherColumn>
<ng-container *ngIf="column.columnDef === 'parameters'; else normalCell"> <ng-container *ngIf="column.columnDef === 'parameters'; else normalCell">
<button mat-stroked-button color="primary" (click)="openParametersModal(schedule.parameters)"> <button class="parameters-button" (click)="openParametersModal(schedule.parameters)">
Ver parámetros <mat-icon>settings</mat-icon>
<span>Ver parámetros</span>
</button> </button>
</ng-container> </ng-container>
</ng-template> </ng-template>

View File

@ -5,6 +5,7 @@ import {HttpClient} from "@angular/common/http";
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog"; import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog";
import {ConfigService} from "@services/config.service"; import {ConfigService} from "@services/config.service";
import {CreateTaskScheduleComponent} from "../create-task-schedule/create-task-schedule.component"; import {CreateTaskScheduleComponent} from "../create-task-schedule/create-task-schedule.component";
import {CreateTaskScriptComponent} from "../create-task-script/create-task-script.component";
import {DeleteModalComponent} from "../../../../shared/delete_modal/delete-modal/delete-modal.component"; import {DeleteModalComponent} from "../../../../shared/delete_modal/delete-modal/delete-modal.component";
import {ViewParametersModalComponent} from "./view-parameters-modal/view-parameters-modal.component"; import {ViewParametersModalComponent} from "./view-parameters-modal/view-parameters-modal.component";
@ -96,6 +97,19 @@ export class ShowTaskScriptComponent implements OnInit{
}); });
} }
createNewScript(): void {
const dialogRef = this.dialog.open(CreateTaskScriptComponent, {
width: '800px',
data: { commandTask: this.data.commandTask }
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loadData();
}
});
}
onPageChange(event: any) { onPageChange(event: any) {
this.page = event.pageIndex; this.page = event.pageIndex;
this.itemsPerPage = event.pageSize; this.itemsPerPage = event.pageSize;

View File

@ -129,8 +129,9 @@
.button-row { .button-row {
display: flex; display: flex;
padding-right: 1em;
gap: 12px; gap: 12px;
padding-right: 0; align-items: center;
} }
/* Tabla de particiones modernizada */ /* Tabla de particiones modernizada */

View File

@ -23,6 +23,13 @@
<div class="button-row"> <div class="button-row">
<button class="action-button" id="execute-button" [disabled]="!selectedPartition || loading" (click)="save()">Ejecutar</button> <button class="action-button" id="execute-button" [disabled]="!selectedPartition || loading" (click)="save()">Ejecutar</button>
</div> </div>
<div class="button-row">
<button class="action-button" color="accent"
[disabled]="!isFormValid()"
(click)="openScheduleModal()">
Opciones de programación
</button>
</div>
</div> </div>
<div class="select-container"> <div class="select-container">

View File

@ -9,6 +9,7 @@ import {MatDialog} from "@angular/material/dialog";
import {QueueConfirmationModalComponent} from "../../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component"; import {QueueConfirmationModalComponent} from "../../../../../shared/queue-confirmation-modal/queue-confirmation-modal.component";
import {CreateRepositoryModalComponent} from "./create-repository-modal/create-repository-modal.component"; import {CreateRepositoryModalComponent} from "./create-repository-modal/create-repository-modal.component";
import {CreateBranchModalComponent} from "../../../../repositories/show-git-images/create-branch-modal/create-branch-modal.component"; import {CreateBranchModalComponent} from "../../../../repositories/show-git-images/create-branch-modal/create-branch-modal.component";
import {CreateTaskComponent} from "../../../../commands/commands-task/create-task/create-task.component";
@Component({ @Component({
selector: 'app-create-image', selector: 'app-create-image',
@ -108,7 +109,6 @@ export class CreateClientImageComponent implements OnInit{
} }
ngOnInit() { ngOnInit() {
console.log('CreateImageComponent ngOnInit ejecutado');
this.clientId = this.route.snapshot.paramMap.get('id'); this.clientId = this.route.snapshot.paramMap.get('id');
this.loadPartitions(); this.loadPartitions();
this.loadImages(); this.loadImages();
@ -612,8 +612,102 @@ export class CreateClientImageComponent implements OnInit{
this.isDestinationBranchEditable = !this.isDestinationBranchEditable; this.isDestinationBranchEditable = !this.isDestinationBranchEditable;
if (!this.isDestinationBranchEditable) { if (!this.isDestinationBranchEditable) {
// Opcional: Aquí se pueden agregar validaciones adicionales
console.log('Rama destino guardada:', this.destinationBranch); console.log('Rama destino guardada:', this.destinationBranch);
} }
} }
isFormValid(): boolean {
if (!this.selectedPartition) {
return false;
}
if (this.imageType === 'monolithic') {
if (this.monolithicAction === 'create') {
return this.name !== null && this.name.trim().length > 0;
} else if (this.monolithicAction === 'update') {
return this.selectedImage !== null;
}
} else if (this.imageType === 'git') {
if (this.hasValidGitData) {
return this.destinationBranch !== null && this.destinationBranch.trim().length > 0;
} else {
return this.selectedGitRepository !== null;
}
}
return false;
}
openScheduleModal(): void {
let scope = 'clients';
// Verificar que tenemos la información del cliente
if (!this.client) {
this.toastService.error('No se ha cargado la información del cliente');
return;
}
// Crear un array con el objeto cliente completo
let selectedClients = [this.client];
console.log(selectedClients);
const dialogRef = this.dialog.open(CreateTaskComponent, {
width: '800px',
data: {
scope: scope,
selectedClients: selectedClients,
organizationalUnit: this.client['@id'],
source: 'create-image',
runScriptContext: null
}
});
dialogRef.afterClosed().subscribe((result: any) => {
if (result) {
// Verificar que tenemos la partición seleccionada
if (!this.selectedPartition) {
this.toastService.error('Debe seleccionar una partición');
return;
}
let payload: any = {};
if (this.imageType === 'monolithic') {
payload = {
type: 'monolithic',
action: this.monolithicAction,
diskNumber: this.selectedPartition.diskNumber,
partitionNumber: this.selectedPartition.partitionNumber,
imageName: this.monolithicAction === 'create' ? this.name : this.selectedImage?.name
};
} else if (this.imageType === 'git') {
payload = {
type: 'git',
diskNumber: this.selectedPartition.diskNumber,
partitionNumber: this.selectedPartition.partitionNumber,
repository: this.selectedGitRepository?.name || this.gitData?.repo,
sourceBranch: this.gitData?.branch,
destinationBranch: this.destinationBranch
};
}
const taskId = result['taskId'] ? result['taskId']['@id'] : result['@id'];
const executionOrder = result['executionOrder'] || 1;
this.http.post(`${this.baseUrl}/command-task-scripts`, {
commandTask: taskId,
parameters: payload,
order: executionOrder,
type: 'create-image',
}).subscribe({
next: () => {
this.toastService.success('Tarea de creación de imagen programada con éxito');
},
error: (error) => {
this.toastService.error(error.error['hydra:description'] || 'Error al programar la tarea');
}
});
}
});
}
} }

View File

@ -341,6 +341,12 @@ table {
font-weight: 500; font-weight: 500;
} }
.chip-busy {
background-color: #ff8c00 !important;
color: white !important;
font-weight: 500;
}
.status-progress-flex { .status-progress-flex {
display: flex; display: flex;
align-items: center; align-items: center;
@ -366,6 +372,7 @@ table {
.status-indicator.in-progress { background-color: #ffc107; } .status-indicator.in-progress { background-color: #ffc107; }
.status-indicator.cancelled { background-color: #6c757d; } .status-indicator.cancelled { background-color: #6c757d; }
.status-indicator.sent { background-color: #b19cd9; } .status-indicator.sent { background-color: #b19cd9; }
.status-indicator.busy { background-color: #ff8c00; }
/* Opciones de cliente */ /* Opciones de cliente */
.client-option { .client-option {

View File

@ -41,6 +41,7 @@
<mat-option [value]="'in-progress'">Ejecutando</mat-option> <mat-option [value]="'in-progress'">Ejecutando</mat-option>
<mat-option [value]="'success'">Completado con éxito</mat-option> <mat-option [value]="'success'">Completado con éxito</mat-option>
<mat-option [value]="'cancelled'">Cancelado</mat-option> <mat-option [value]="'cancelled'">Cancelado</mat-option>
<mat-option [value]="'busy'">Ocupado</mat-option>
</mat-select> </mat-select>
<button *ngIf="commandStatusInput.value" mat-icon-button matSuffix aria-label="Clear input search" <button *ngIf="commandStatusInput.value" mat-icon-button matSuffix aria-label="Clear input search"
(click)="clearStatusFilter($event, commandStatusInput)"> (click)="clearStatusFilter($event, commandStatusInput)">
@ -97,7 +98,8 @@
'chip-pending': trace.status === 'pending', 'chip-pending': trace.status === 'pending',
'chip-in-progress': trace.status === 'in-progress', 'chip-in-progress': trace.status === 'in-progress',
'chip-cancelled': trace.status === 'cancelled', 'chip-cancelled': trace.status === 'cancelled',
'chip-sent': trace.status === 'sent' 'chip-sent': trace.status === 'sent',
'chip-busy': trace.status === 'busy'
}"> }">
{{ {{
trace.status === 'failed' ? 'Error' : trace.status === 'failed' ? 'Error' :
@ -106,6 +108,7 @@
trace.status === 'pending' ? 'Pendiente' : trace.status === 'pending' ? 'Pendiente' :
trace.status === 'cancelled' ? 'Cancelado' : trace.status === 'cancelled' ? 'Cancelado' :
trace.status === 'sent' ? 'Enviado' : trace.status === 'sent' ? 'Enviado' :
trace.status === 'busy' ? 'Ocupado' :
trace.status trace.status
}} }}
</mat-chip> </mat-chip>

View File

@ -235,7 +235,6 @@ export class ClientTaskLogsComponent implements OnInit {
params['executedAt[before]'] = this.datePipe.transform(params['endDate'], 'yyyy-MM-dd'); params['executedAt[before]'] = this.datePipe.transform(params['endDate'], 'yyyy-MM-dd');
delete params['endDate']; delete params['endDate'];
} }
console.log('🌐 GET', `${this.baseUrl}/traces`, params);
const url = `${this.baseUrl}/traces`; const url = `${this.baseUrl}/traces`;

View File

@ -497,6 +497,12 @@ table {
flex-shrink: 0; flex-shrink: 0;
} }
/* Ajuste para el botón de éxito en la barra de progreso */
.progress-container .success-button {
margin-left: 5px;
flex-shrink: 0;
}
.paginator-container { .paginator-container {
display: flex; display: flex;
justify-content: end; justify-content: end;
@ -540,10 +546,16 @@ table {
font-weight: 500; font-weight: 500;
} }
.chip-busy {
background-color: #ff8c00 !important;
color: white !important;
font-weight: 500;
}
.status-progress-flex { .status-progress-flex {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 4px;
} }
.status-option { .status-option {
@ -564,6 +576,7 @@ table {
.status-indicator.in-progress { background-color: #ffc107; } .status-indicator.in-progress { background-color: #ffc107; }
.status-indicator.cancelled { background-color: #6c757d; } .status-indicator.cancelled { background-color: #6c757d; }
.status-indicator.sent { background-color: #b19cd9; } .status-indicator.sent { background-color: #b19cd9; }
.status-indicator.busy { background-color: #ff8c00; }
.client-option { .client-option {
display: flex; display: flex;
@ -610,6 +623,26 @@ button.cancel-button {
color: #dc3545; color: #dc3545;
} }
.success-button {
display: flex;
align-items: center;
justify-content: center;
color: #28a745;
background-color: transparent;
border: none;
padding: 0px;
transition: all 0.3s ease;
}
.success-button:hover {
background-color: rgba(40, 167, 69, 0.1);
border-radius: 50%;
}
.success-button mat-icon {
color: #28a745;
}
.selected-row { .selected-row {
background-color: rgba(102, 126, 234, 0.1) !important; background-color: rgba(102, 126, 234, 0.1) !important;
} }

View File

@ -159,6 +159,12 @@
{{ 'sent' | translate }} {{ 'sent' | translate }}
</div> </div>
</mat-option> </mat-option>
<mat-option [value]="'busy'">
<div class="status-option">
<div class="status-indicator busy"></div>
{{ 'busy' | translate }}
</div>
</mat-option>
</mat-select> </mat-select>
<button *ngIf="commandStatusInput.value" mat-icon-button matSuffix aria-label="Clear input search" <button *ngIf="commandStatusInput.value" mat-icon-button matSuffix aria-label="Clear input search"
(click)="clearStatusFilter($event, commandStatusInput)"> (click)="clearStatusFilter($event, commandStatusInput)">
@ -235,6 +241,10 @@
(click)="cancelTrace(trace)" class="cancel-button" matTooltip="{{ 'cancelTask' | translate }}"> (click)="cancelTrace(trace)" class="cancel-button" matTooltip="{{ 'cancelTask' | translate }}">
<mat-icon>cancel</mat-icon> <mat-icon>cancel</mat-icon>
</button> </button>
<button mat-icon-button
(click)="markTraceAsSuccess(trace)" class="success-button" matTooltip="Marcar como exitosa">
<mat-icon>flag</mat-icon>
</button>
</div> </div>
</ng-container> </ng-container>
<ng-template #statusChip> <ng-template #statusChip>
@ -246,7 +256,8 @@
'chip-pending': trace.status === 'pending', 'chip-pending': trace.status === 'pending',
'chip-in-progress': trace.status === 'in-progress', 'chip-in-progress': trace.status === 'in-progress',
'chip-cancelled': trace.status === 'cancelled', 'chip-cancelled': trace.status === 'cancelled',
'chip-sent': trace.status === 'sent' 'chip-sent': trace.status === 'sent',
'chip-busy': trace.status === 'busy'
}"> }">
{{ {{
trace.status === 'failed' ? ('failed' | translate) : trace.status === 'failed' ? ('failed' | translate) :
@ -255,6 +266,7 @@
trace.status === 'pending' ? ('pending' | translate) : trace.status === 'pending' ? ('pending' | translate) :
trace.status === 'cancelled' ? ('cancelled' | translate) : trace.status === 'cancelled' ? ('cancelled' | translate) :
trace.status === 'sent' ? ('sent' | translate) : trace.status === 'sent' ? ('sent' | translate) :
trace.status === 'busy' ? ('busy' | translate) :
trace.status trace.status
}} }}
</mat-chip> </mat-chip>
@ -262,6 +274,10 @@
(click)="cancelTrace(trace)" class="cancel-button" matTooltip="{{ 'cancelTask' | translate }}"> (click)="cancelTrace(trace)" class="cancel-button" matTooltip="{{ 'cancelTask' | translate }}">
<mat-icon>cancel</mat-icon> <mat-icon>cancel</mat-icon>
</button> </button>
<button *ngIf="trace.status === 'in-progress'" mat-icon-button
(click)="markTraceAsSuccess(trace)" class="success-button" matTooltip="Marcar como exitosa">
<mat-icon>flag</mat-icon>
</button>
</div> </div>
</ng-template> </ng-template>
</ng-container> </ng-container>

View File

@ -321,6 +321,41 @@ export class TaskLogsComponent implements OnInit, OnDestroy {
}); });
} }
markTraceAsSuccess(trace: any): void {
if (trace.status !== 'in-progress') {
this.toastService.warning('Solo se pueden marcar como exitosas las trazas en progreso', 'Advertencia');
return;
}
const dialogRef = this.dialog.open(DeleteModalComponent, {
width: '400px',
data: {
title: 'Marcar Traza como Exitosa',
message: `¿Estás seguro de que quieres marcar la traza #${trace.id} como exitosa?`,
confirmText: 'Marcar como Exitosa',
cancelText: 'Cancelar'
}
});
dialogRef.afterClosed().subscribe(result => {
if (result) {
this.loading = true;
this.http.post(`${this.baseUrl}${trace['@id']}/mark-as-success`, {}).subscribe(
() => {
this.toastService.success('Traza marcada como exitosa correctamente', 'Éxito');
this.loadTraces();
this.loadTotalStats();
},
(error) => {
console.error('Error marking trace as success:', error);
this.toastService.error('Error al marcar la traza como exitosa', 'Error');
this.loading = false;
}
);
}
});
}
private updateTracesStatus(clientUuid: string, newStatus: string, progress: Number): void { private updateTracesStatus(clientUuid: string, newStatus: string, progress: Number): void {
const traceIndex = this.traces.findIndex(trace => trace['@id'] === clientUuid); const traceIndex = this.traces.findIndex(trace => trace['@id'] === clientUuid);
if (traceIndex !== -1) { if (traceIndex !== -1) {

View File

@ -1,10 +1,11 @@
<h1 mat-dialog-title i18n="@@deleteDialogTitle">Eliminar</h1> <h1 mat-dialog-title>{{ title }}</h1>
<div mat-dialog-content> <div mat-dialog-content>
<p i18n="@@deleteConfirmationMessage"> <p>
¿Estás seguro que deseas eliminar <strong>{{ data.name }}</strong>? {{ message }}
<strong *ngIf="showName">{{ name }}</strong>
</p> </p>
</div> </div>
<div mat-dialog-actions class="action-container"> <div mat-dialog-actions class="action-container">
<button class="ordinary-button" (click)="onNoClick()" i18n="@@cancelButton">Cancelar</button> <button class="ordinary-button" (click)="onNoClick()">{{ cancelText }}</button>
<button class="delete-button" (click)="onYesClick()" i18n="@@confirmButton">Eliminar</button> <button class="delete-button" (click)="onYesClick()">{{ confirmText }}</button>
</div> </div>

View File

@ -7,10 +7,27 @@ import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
styleUrl: './delete-modal.component.css' styleUrl: './delete-modal.component.css'
}) })
export class DeleteModalComponent { export class DeleteModalComponent {
title: string = 'Eliminar';
message: string = '¿Estás seguro que deseas eliminar este elemento?';
confirmText: string = 'Eliminar';
cancelText: string = 'Cancelar';
showName: boolean = false;
name: string = '';
constructor( constructor(
public dialogRef: MatDialogRef<DeleteModalComponent>, public dialogRef: MatDialogRef<DeleteModalComponent>,
@Inject(MAT_DIALOG_DATA) public data: { name: string } @Inject(MAT_DIALOG_DATA) public data: any
) {} ) {
// Configurar valores por defecto o usar los proporcionados
if (data) {
this.title = data.title || this.title;
this.message = data.message || this.message;
this.confirmText = data.confirmText || this.confirmText;
this.cancelText = data.cancelText || this.cancelText;
this.name = data.name || '';
this.showName = !!data.name;
}
}
onNoClick(): void { onNoClick(): void {
this.dialogRef.close(false); this.dialogRef.close(false);