refs #2596. Create task options
parent
d234cdb74c
commit
cc1a05cf0b
|
|
@ -77,3 +77,64 @@ table {
|
|||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,8 +33,14 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'management'">
|
||||
<button class="action-button" (click)="openShowScheduleDialog(task)"> Programaciones</button>
|
||||
<button class="action-button" style="margin-left: 0.5vw;" (click)="openShowScriptDialog(task)">Acciones</button>
|
||||
<button class="action-button schedule-btn" (click)="openShowScheduleDialog(task)">
|
||||
<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>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
|
@ -42,12 +48,6 @@
|
|||
<ng-container matColumnDef="actions">
|
||||
<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 }}">
|
||||
<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)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -3,16 +3,11 @@ import { HttpClient } from '@angular/common/http';
|
|||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
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 { JoyrideService } from 'ngx-joyride';
|
||||
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 {ShowTaskScriptComponent} from "./show-task-script/show-task-script.component";
|
||||
import {CreateTaskScriptComponent} from "./create-task-script/create-task-script.component";
|
||||
import {DatePipe} from "@angular/common";
|
||||
|
||||
@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 {
|
||||
this.page = event.pageIndex + 1;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
|
|
|
|||
|
|
@ -1,128 +1,243 @@
|
|||
.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 {
|
||||
padding: 20px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
.w-full {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.w-half {
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.custom-time {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.w-half {
|
||||
width: 50%;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
margin: auto;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
.section-label {
|
||||
display: block;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
margin-bottom: 12px;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.weekday-toggle-group {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.weekday-toggle {
|
||||
flex: 1;
|
||||
padding: 8px 0;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background: #f5f5f5;
|
||||
padding: 8px 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
background: white;
|
||||
border-radius: 20px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s ease;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 500;
|
||||
transition: background 0.2s ease;
|
||||
min-width: 60px;
|
||||
}
|
||||
|
||||
.weekday-toggle:hover {
|
||||
border-color: #2196f3;
|
||||
background: #f3f8ff;
|
||||
}
|
||||
|
||||
.weekday-toggle.selected {
|
||||
background: #1976d2;
|
||||
background: #2196f3;
|
||||
color: white;
|
||||
border-color: #1976d2;
|
||||
}
|
||||
|
||||
.month-toggle-group {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 6px;
|
||||
margin-bottom: 16px;
|
||||
border-color: #2196f3;
|
||||
}
|
||||
|
||||
.month-toggle-row {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.month-toggle {
|
||||
flex: 1;
|
||||
padding: 8px;
|
||||
min-width: 48px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 6px;
|
||||
background-color: #f0f0f0;
|
||||
text-align: center;
|
||||
padding: 6px 10px;
|
||||
border: 2px solid #e0e0e0;
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
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 {
|
||||
background-color: #4caf50;
|
||||
background: #4caf50;
|
||||
color: white;
|
||||
border-color: #4caf50;
|
||||
}
|
||||
|
||||
.summary-card {
|
||||
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;
|
||||
.enabled-checkbox {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
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 {
|
||||
color: #0d47a1;
|
||||
color: #333;
|
||||
font-size: 0.95rem;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
<form [formGroup]="form" (ngSubmit)="onSubmit()" class="task-form">
|
||||
|
|
@ -18,12 +20,13 @@
|
|||
</mat-form-field>
|
||||
|
||||
<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">
|
||||
<mat-hint>Hora en formato 24h (ej: 14:30)</mat-hint>
|
||||
</mat-form-field>
|
||||
|
||||
<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">
|
||||
<button
|
||||
*ngFor="let day of weekDays"
|
||||
|
|
@ -37,7 +40,7 @@
|
|||
</div>
|
||||
|
||||
<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">
|
||||
<button
|
||||
*ngFor="let month of row"
|
||||
|
|
@ -52,27 +55,49 @@
|
|||
|
||||
<div *ngIf="form.get('recurrenceType')?.value !== 'none'" class="custom-time" formGroupName="recurrenceDetails">
|
||||
<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">
|
||||
<mat-datepicker-toggle matSuffix [for]="fromPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #fromPicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
|
||||
<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">
|
||||
<mat-datepicker-toggle matSuffix [for]="toPicker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #toPicker></mat-datepicker>
|
||||
</mat-form-field>
|
||||
</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-icon color="primary" style="width: 50px;">info</mat-icon>
|
||||
<span class="summary-text">
|
||||
{{ summaryText }}
|
||||
</span>
|
||||
<mat-card-header>
|
||||
<mat-card-title class="summary-title">
|
||||
<mat-icon color="primary">info</mat-icon>
|
||||
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>
|
||||
</form>
|
||||
|
||||
|
|
@ -80,5 +105,8 @@
|
|||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<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>
|
||||
|
|
|
|||
|
|
@ -26,6 +26,8 @@ export class CreateTaskScheduleComponent implements OnInit{
|
|||
editing: boolean = false;
|
||||
selectedMonths: { [key: string]: boolean } = {};
|
||||
selectedDays: { [key: string]: boolean } = {};
|
||||
nextExecutionDate: Date | null = null;
|
||||
executionCount: number = 0;
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
|
|
@ -176,6 +178,9 @@ export class CreateTaskScheduleComponent implements OnInit{
|
|||
const days = Object.keys(this.selectedDays).filter(day => this.selectedDays[day]);
|
||||
const months = Object.keys(this.selectedMonths).filter(month => this.selectedMonths[month]);
|
||||
|
||||
// Calcular próxima ejecución y conteo
|
||||
this.calculateNextExecutionAndCount();
|
||||
|
||||
if (recurrence === 'none') {
|
||||
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}.`;
|
||||
}
|
||||
|
||||
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 {
|
||||
const realDate = (date instanceof Date) ? date : new Date(date);
|
||||
return new Intl.DateTimeFormat('es-ES', { dateStyle: 'long' }).format(realDate);
|
||||
|
|
|
|||
|
|
@ -275,10 +275,134 @@ table {
|
|||
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;
|
||||
justify-content: start;
|
||||
margin: 16px 0;
|
||||
align-items: center;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -2,60 +2,113 @@
|
|||
|
||||
<mat-dialog-content class="dialog-content">
|
||||
<div class="task-form">
|
||||
|
||||
<div class="toggle-options">
|
||||
<mat-button-toggle-group [(ngModel)]="commandType" exclusive>
|
||||
<mat-button-toggle value="new">
|
||||
<mat-icon>edit</mat-icon> Nuevo Script
|
||||
</mat-button-toggle>
|
||||
<mat-button-toggle value="existing">
|
||||
<mat-icon>storage</mat-icon> Script Guardado
|
||||
</mat-button-toggle>
|
||||
</mat-button-toggle-group>
|
||||
</div>
|
||||
|
||||
<div *ngIf="commandType === 'new'" class="new-command-container">
|
||||
<mat-form-field appearance="fill" class="custom-width">
|
||||
<mat-label>Orden de ejecucion </mat-label>
|
||||
<input matInput type="number" [(ngModel)]="executionOrder" placeholder="Orden de ejecución">
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Ingrese el script</mat-label>
|
||||
<textarea matInput [(ngModel)]="newScript" rows="6" placeholder="Escriba su script aquí"></textarea>
|
||||
</mat-form-field>
|
||||
<button mat-flat-button color="primary" (click)="saveNewScript()">Guardar Script</button>
|
||||
</div>
|
||||
|
||||
<div *ngIf="commandType === 'existing'">
|
||||
<mat-form-field appearance="fill" class="custom-width">
|
||||
<mat-label>Seleccione script a ejecutar</mat-label>
|
||||
<mat-select [(ngModel)]="selectedScript" (selectionChange)="onScriptChange()">
|
||||
<mat-option *ngFor="let script of scripts" [value]="script">{{ script.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="selectedScript && commandType === 'existing'" class="script-container">
|
||||
<mat-form-field appearance="fill" class="custom-width">
|
||||
<mat-label>Orden de ejecucion </mat-label>
|
||||
<input matInput type="number" [(ngModel)]="executionOrder" placeholder="Orden de ejecución">
|
||||
</mat-form-field>
|
||||
|
||||
<div class="script-content">
|
||||
<h3>Script:</h3>
|
||||
<div class="script-preview" [innerHTML]="scriptContent"></div>
|
||||
</div>
|
||||
|
||||
<div class="script-params" *ngIf="parameterNames.length > 0">
|
||||
<h3>Ingrese los parámetros:</h3>
|
||||
<div *ngFor="let paramName of parameterNames">
|
||||
|
||||
<!-- Pestañas principales -->
|
||||
<mat-tab-group [(selectedIndex)]="selectedTabIndex" (selectedIndexChange)="onTabChange($event)" class="action-tabs">
|
||||
|
||||
<!-- Pestaña de Acciones Básicas -->
|
||||
<mat-tab label="Acciones Básicas" class="basic-tab">
|
||||
<div class="tab-content">
|
||||
<div class="action-description">
|
||||
<mat-icon>power_settings_new</mat-icon>
|
||||
<span>Selecciona una acción básica del sistema</span>
|
||||
</div>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ paramName }}</mat-label>
|
||||
<input matInput [ngModel]="parameters[paramName]" (ngModelChange)="onParamChange(paramName, $event)" placeholder="Valor para {{ paramName }}">
|
||||
<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>
|
||||
</div>
|
||||
</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">
|
||||
<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>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Ingrese el script</mat-label>
|
||||
<textarea matInput [(ngModel)]="newScript" rows="6" placeholder="Escriba su script aquí"></textarea>
|
||||
</mat-form-field>
|
||||
<button mat-flat-button color="primary" (click)="saveNewScript()">Guardar Script</button>
|
||||
</div>
|
||||
|
||||
<!-- Script Existente -->
|
||||
<div *ngIf="commandType === 'existing'">
|
||||
<mat-form-field appearance="fill" class="custom-width">
|
||||
<mat-label>Seleccione script a ejecutar</mat-label>
|
||||
<mat-select [(ngModel)]="selectedScript" (selectionChange)="onScriptChange()">
|
||||
<mat-option *ngFor="let script of scripts" [value]="script">{{ script.name }}</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<!-- Detalles del Script -->
|
||||
<div *ngIf="selectedScript && commandType === 'existing'" class="script-container">
|
||||
<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 class="script-content">
|
||||
<h3>Script:</h3>
|
||||
<div class="script-preview" [innerHTML]="scriptContent"></div>
|
||||
</div>
|
||||
|
||||
<div class="script-params" *ngIf="parameterNames.length > 0">
|
||||
<h3>Ingrese los parámetros:</h3>
|
||||
<div *ngFor="let paramName of parameterNames">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ paramName }}</mat-label>
|
||||
<input matInput [ngModel]="parameters[paramName]" (ngModelChange)="onParamChange(paramName, $event)" placeholder="Valor para {{ paramName }}">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</mat-tab>
|
||||
</mat-tab-group>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
|
|
|
|||
|
|
@ -24,13 +24,15 @@ export class CreateTaskScriptComponent implements OnInit {
|
|||
scripts: any[] = [];
|
||||
scriptContent: string = "";
|
||||
parameters: any = {};
|
||||
selectedTabIndex: number = 0;
|
||||
selectedBasicAction: string = '';
|
||||
commandType: string = 'existing';
|
||||
selectedScript: any = null;
|
||||
newScript: string = '';
|
||||
executionOrder: Number = 0;
|
||||
selection = new SelectionModel(true, []);
|
||||
parameterNames: string[] = Object.keys(this.parameters);
|
||||
|
||||
commandTask: any;
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private http: HttpClient,
|
||||
|
|
@ -43,6 +45,8 @@ export class CreateTaskScriptComponent implements OnInit {
|
|||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.commandTask = this.data.commandTask;
|
||||
|
||||
this.loadScripts()
|
||||
this.form = this.fb.group({
|
||||
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() {
|
||||
if (!this.newScript.trim()) {
|
||||
this.toastService.error('Debe ingresar un script antes de guardar.');
|
||||
|
|
@ -115,12 +159,60 @@ export class CreateTaskScriptComponent implements OnInit {
|
|||
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() {
|
||||
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`, {
|
||||
commandTask: this.data.task['@id'],
|
||||
content: this.commandType === 'existing' ? this.scriptContent : this.newScript,
|
||||
commandTask: this.commandTask['@id'],
|
||||
content: content,
|
||||
order: this.executionOrder,
|
||||
type: 'run-script',
|
||||
type: this.selectedBasicAction,
|
||||
}).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Tarea creada con éxito');
|
||||
|
|
|
|||
|
|
@ -269,7 +269,7 @@ export class CreateTaskComponent implements OnInit {
|
|||
};
|
||||
|
||||
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;
|
||||
} else {
|
||||
payload.organizationalUnit = formData.organizationalUnit;
|
||||
|
|
|
|||
|
|
@ -87,3 +87,122 @@ mat-spinner {
|
|||
display: flex;
|
||||
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%;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,26 +3,33 @@
|
|||
<h2 mat-dialog-title>Gestionar programaciones de tareas en {{ data.commandTask?.name }}</h2>
|
||||
|
||||
<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">
|
||||
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchNameStep"
|
||||
text="Busca subredes por nombre para localizar una subred específica rápidamente.">
|
||||
<mat-label i18n="@@searchLabel">Buscar nombre del cliente</mat-label>
|
||||
text="Busca programaciones por nombre para localizar una programación específica rápidamente.">
|
||||
<mat-label i18n="@@searchLabel">Buscar programación</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" i18n-placeholder="@@searchPlaceholder"
|
||||
(keyup.enter)="loadData()" i18n-placeholder="@@searchPlaceholder">
|
||||
<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()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
|
||||
</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>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['recurrence']" i18n-placeholder="@@searchPlaceholder"
|
||||
(keyup.enter)="loadData()" i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<button *ngIf="filters['ip']" mat-icon-button matSuffix aria-label="Clear tree search"
|
||||
(click)="filters['ip'] = ''; loadData()">
|
||||
<button *ngIf="filters['recurrence']" mat-icon-button matSuffix aria-label="Clear type search"
|
||||
(click)="filters['recurrence'] = ''; loadData()">
|
||||
<mat-icon>close</mat-icon>
|
||||
</button>
|
||||
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
|
||||
|
|
@ -31,7 +38,7 @@
|
|||
|
||||
<app-loading [isLoading]="loading"></app-loading>
|
||||
<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">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let schedule">
|
||||
|
|
@ -47,19 +54,45 @@
|
|||
<ng-template #scheduledTemplate>
|
||||
Programado
|
||||
<div style="font-size: 12px;">
|
||||
{{ schedule.recurrenceDetails.initDate | date }} → {{ schedule.recurrenceDetails.endDate | date}}
|
||||
{{ schedule.recurrenceDetails?.initDate | date }} → {{ schedule.recurrenceDetails?.endDate | date}}
|
||||
</div>
|
||||
</ng-template>
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'executionTime'">
|
||||
{{ schedule.executionTime | date: 'HH:mm' }}
|
||||
</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) }}
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'enabled'">
|
||||
<mat-chip>
|
||||
<mat-chip [color]="schedule.enabled ? 'accent' : 'warn'" selected>
|
||||
<ng-container *ngIf="schedule.enabled">
|
||||
Activo
|
||||
</ng-container>
|
||||
|
|
@ -74,10 +107,10 @@
|
|||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions" style="text-align: center;">Acciones</th>
|
||||
<td mat-cell *matCellDef="let schedule" style="text-align: center;">
|
||||
<button mat-icon-button color="primary" (click)="editSchedule(schedule)">
|
||||
<mat-icon i18n="@@deleteElementTooltip">edit</mat-icon>
|
||||
<button mat-icon-button color="primary" (click)="editSchedule(schedule)" matTooltip="Editar programación">
|
||||
<mat-icon i18n="@@editElementTooltip">edit</mat-icon>
|
||||
</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>
|
||||
</button>
|
||||
</td>
|
||||
|
|
@ -88,7 +121,7 @@
|
|||
</table>
|
||||
|
||||
<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"
|
||||
(page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
|
|
|
|||
|
|
@ -28,13 +28,14 @@ export class ShowTaskScheduleComponent implements OnInit{
|
|||
columns = [
|
||||
{ columnDef: 'id', header: 'ID', cell: (schedule: any) => schedule.id },
|
||||
{ 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: 'daysOfWeek', header: 'Dias de la semana', cell: (schedule: any) => schedule.recurrenceDetails.daysOfWeek },
|
||||
{ columnDef: 'months', header: 'Meses', cell: (schedule: any) => schedule.recurrenceDetails.months },
|
||||
{ columnDef: 'enabled', header: 'Activo', cell: (schedule: any) => schedule.enabled }
|
||||
{ columnDef: 'executionTime', header: 'Hora de ejecución', cell: (schedule: any) => this.datePipe.transform(schedule.executionTime, 'HH:mm') },
|
||||
{ columnDef: 'nextExecution', header: 'Próxima ejecución', cell: (schedule: any) => this.calculateNextExecution(schedule) },
|
||||
{ columnDef: 'daysOfWeek', header: 'Días de la semana', cell: (schedule: any) => schedule.recurrenceDetails?.daysOfWeek || [] },
|
||||
{ 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(
|
||||
private toastService: ToastrService,
|
||||
|
|
@ -63,21 +64,39 @@ export class ShowTaskScheduleComponent implements OnInit{
|
|||
},
|
||||
(error) => {
|
||||
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 {
|
||||
this.dialog.open(CreateTaskScheduleComponent, {
|
||||
width: '800px',
|
||||
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 {
|
||||
const dialogRef = this.dialog.open(DeleteModalComponent, {
|
||||
width: '300px',
|
||||
data: { name: 'tarea programada' }
|
||||
data: { name: 'programación de tarea' }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
|
|
@ -88,20 +107,52 @@ export class ShowTaskScheduleComponent implements OnInit{
|
|||
this.loadData();
|
||||
},
|
||||
(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 {
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
|
||||
onPageChange(event: any) {
|
||||
this.page = event.pageIndex;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
this.loadData()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,3 +87,222 @@ mat-spinner {
|
|||
display: flex;
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,13 @@
|
|||
<h2 mat-dialog-title>Gestionar scripts de tareas en {{ data.commandTask?.name }}</h2>
|
||||
|
||||
<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">
|
||||
<mat-form-field appearance="fill" class="search-string" joyrideStep="searchNameStep"
|
||||
text="Busca subredes por nombre para localizar una subred específica rápidamente.">
|
||||
|
|
@ -32,8 +39,9 @@
|
|||
|
||||
<ng-template #checkOtherColumn>
|
||||
<ng-container *ngIf="column.columnDef === 'parameters'; else normalCell">
|
||||
<button mat-stroked-button color="primary" (click)="openParametersModal(schedule.parameters)">
|
||||
Ver parámetros
|
||||
<button class="parameters-button" (click)="openParametersModal(schedule.parameters)">
|
||||
<mat-icon>settings</mat-icon>
|
||||
<span>Ver parámetros</span>
|
||||
</button>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import {HttpClient} from "@angular/common/http";
|
|||
import {MAT_DIALOG_DATA, MatDialog, MatDialogRef} from "@angular/material/dialog";
|
||||
import {ConfigService} from "@services/config.service";
|
||||
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 {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) {
|
||||
this.page = event.pageIndex;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
|
|
|
|||
Loading…
Reference in New Issue