develop #39

Merged
maranda merged 6 commits from develop into main 2025-09-03 08:32:15 +02:00
16 changed files with 701 additions and 173 deletions

View File

@ -1,5 +1,13 @@
# Changelog # Changelog
## [0.22.0] - 2025-09-03
### Added
- Nueva ux a la hora de gestionar tareas programadas.
- En modificar imagen git, nueva opcion para "forzar push".
### Improved
- Nuevo manejo de errores en el modulo de ogRepository.
---
## [0.21.0] - 2025-08-28 ## [0.21.0] - 2025-08-28
### Added ### Added
- Se ha incluido una integracion con el agente, para generar los scripts que se llaman al particionar. - Se ha incluido una integracion con el agente, para generar los scripts que se llaman al particionar.

View File

@ -8,7 +8,7 @@
<mat-form-field appearance="fill" class="w-full"> <mat-form-field appearance="fill" class="w-full">
<mat-label>Repetición</mat-label> <mat-label>Repetición</mat-label>
<mat-select formControlName="recurrenceType"> <mat-select formControlName="recurrenceType">
<mat-option *ngFor="let type of recurrenceTypes" [value]="type">{{ type | titlecase }}</mat-option> <mat-option *ngFor="let opt of recurrenceTypes" [value]="opt.value">{{ opt.label }}</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>

View File

@ -14,7 +14,10 @@ export class CreateTaskScheduleComponent implements OnInit{
form: FormGroup; form: FormGroup;
baseUrl: string; baseUrl: string;
apiUrl: string; apiUrl: string;
recurrenceTypes = ['none', 'custom']; recurrenceTypes = [
{ value: 'none', label: 'Sin repetición' },
{ value: 'custom', label: 'Repetición personalizada' }
];
weekDays: string[] = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday']; weekDays: string[] = ['monday', 'tuesday', 'wednesday', 'thursday', 'friday', 'saturday', 'sunday'];
isSingleDateSelected: boolean = true; isSingleDateSelected: boolean = true;
monthsList: string[] = [ monthsList: string[] = [
@ -108,12 +111,27 @@ export class CreateTaskScheduleComponent implements OnInit{
} }
formatExecutionTime(time: string | Date): string { formatExecutionTime(time: string | Date): string {
const date = (time instanceof Date) ? time : new Date(time); if (typeof time === 'string') {
if (isNaN(date.getTime())) { const hhmmMatch = time.match(/^\d{2}:\d{2}/);
console.error('Invalid execution time:', time); if (hhmmMatch) {
return hhmmMatch[0];
}
const parsed = new Date(time);
if (!isNaN(parsed.getTime())) {
const hours = String(parsed.getHours()).padStart(2, '0');
const minutes = String(parsed.getMinutes()).padStart(2, '0');
return `${hours}:${minutes}`;
}
return ''; return '';
} }
return date.toISOString().substring(11, 16);
if (time instanceof Date && !isNaN(time.getTime())) {
const hours = String(time.getHours()).padStart(2, '0');
const minutes = String(time.getMinutes()).padStart(2, '0');
return `${hours}:${minutes}`;
}
return '';
} }
convertDateToLocalISO(date: Date): string { convertDateToLocalISO(date: Date): string {
@ -191,7 +209,7 @@ export class CreateTaskScheduleComponent implements OnInit{
private calculateNextExecutionAndCount(): void { private calculateNextExecutionAndCount(): void {
const recurrence = this.form.get('recurrenceType')?.value; const recurrence = this.form.get('recurrenceType')?.value;
const time = this.form.get('executionTime')?.value; const time = this.form.get('executionTime')?.value;
if (recurrence === 'none') { if (recurrence === 'none') {
const execDate = this.form.get('executionDate')?.value; const execDate = this.form.get('executionDate')?.value;
if (execDate && time) { if (execDate && time) {
@ -200,31 +218,102 @@ export class CreateTaskScheduleComponent implements OnInit{
this.nextExecutionDate.setHours(parseInt(hours), parseInt(minutes), 0, 0); this.nextExecutionDate.setHours(parseInt(hours), parseInt(minutes), 0, 0);
this.executionCount = 1; this.executionCount = 1;
} }
} else { return;
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);
}
} }
const startDateValue = this.form.get('recurrenceDetails.initDate')?.value;
const endDateValue = this.form.get('recurrenceDetails.endDate')?.value;
const selectedDayNames = Object.keys(this.selectedDays).filter(day => this.selectedDays[day]);
const selectedMonthNames = Object.keys(this.selectedMonths).filter(month => this.selectedMonths[month]);
if (!startDateValue || !endDateValue || selectedDayNames.length === 0 || selectedMonthNames.length === 0) {
this.nextExecutionDate = null;
this.executionCount = 0;
return;
}
const startDate = new Date(startDateValue);
const endDate = new Date(endDateValue);
const selectedDayIndices = new Set<number>(selectedDayNames.map(name => this.getDayIndexFromName(name)));
const selectedMonthIndices = new Set<number>(selectedMonthNames.map(name => this.getMonthIndexFromName(name)));
// Conteo exacto de ejecuciones en el rango [startDate, endDate]
this.executionCount = this.calculateExecutionCountExact(startDate, endDate, selectedDayIndices, selectedMonthIndices);
// Próxima ejecución >= ahora dentro del rango y criterios
this.nextExecutionDate = this.findNextExecutionDate(startDate, endDate, time, selectedDayIndices, selectedMonthIndices);
} }
private calculateExecutionCount(startDate: Date, endDate: Date, daysCount: number, monthsCount: number): number { private calculateExecutionCountExact(startDate: Date, endDate: Date, selectedDays: Set<number>, selectedMonths: Set<number>): number {
const start = new Date(startDate); const cursor = new Date(startDate);
cursor.setHours(0, 0, 0, 0);
const end = new Date(endDate); const end = new Date(endDate);
const daysDiff = Math.ceil((end.getTime() - start.getTime()) / (1000 * 60 * 60 * 24)); end.setHours(23, 59, 59, 999);
// Aproximación: días seleccionados por semana * semanas * meses activos let count = 0;
return Math.ceil((daysCount / 7) * (daysDiff / 7) * (monthsCount / 12)); while (cursor <= end) {
if (selectedMonths.has(cursor.getMonth()) && selectedDays.has(cursor.getDay())) {
count += 1;
}
cursor.setDate(cursor.getDate() + 1);
}
return count;
}
private findNextExecutionDate(startDate: Date, endDate: Date, time: string, selectedDays: Set<number>, selectedMonths: Set<number>): Date | null {
const now = new Date();
const startCandidate = new Date(Math.max(new Date(startDate).setHours(0, 0, 0, 0), new Date(now).setHours(0, 0, 0, 0)));
const endLimit = new Date(endDate);
endLimit.setHours(23, 59, 59, 999);
const [hours, minutes] = (time || '00:00').split(':');
const candidate = new Date(startCandidate);
while (candidate <= endLimit) {
if (selectedMonths.has(candidate.getMonth()) && selectedDays.has(candidate.getDay())) {
const candidateDateTime = new Date(candidate);
candidateDateTime.setHours(parseInt(hours), parseInt(minutes), 0, 0);
if (candidateDateTime >= now) {
return candidateDateTime;
}
}
candidate.setDate(candidate.getDate() + 1);
}
return null;
}
private getDayIndexFromName(dayName: string): number {
const mapping: { [key: string]: number } = {
sunday: 0,
monday: 1,
tuesday: 2,
wednesday: 3,
thursday: 4,
friday: 5,
saturday: 6
};
return mapping[dayName.toLowerCase()] ?? -1;
}
private getMonthIndexFromName(monthName: string): number {
const mapping: { [key: string]: number } = {
january: 0,
february: 1,
march: 2,
april: 3,
may: 4,
june: 5,
july: 6,
august: 7,
september: 8,
october: 9,
november: 10,
december: 11
};
return mapping[monthName.toLowerCase()] ?? -1;
} }
formatDate(date: string | Date): string { formatDate(date: string | Date): string {

View File

@ -69,48 +69,287 @@ table {
} }
.mat-elevation-z8 { .mat-elevation-z8 {
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2); border-radius: 12px;
overflow: hidden;
box-shadow: none;
} }
.paginator-container { .mat-header-cell {
background-color: #f8f9fa;
font-weight: 600;
color: #495057;
padding: 16px 8px;
border-bottom: 2px solid #e9ecef;
}
.mat-cell {
padding: 16px 8px;
border-bottom: 1px solid #f1f3f4;
}
.recurrence-type-container {
display: flex; display: flex;
justify-content: end; justify-content: center;
margin-bottom: 30px;
} }
mat-spinner { .recurrence-chip {
margin: 0 auto;
align-self: center;
}
.subnets-button-row {
display: flex; display: flex;
gap: 15px; align-items: center;
padding: 7px 12px;
border-radius: 20px;
min-width: 200px;
transition: all 0.3s ease;
margin: 4px 0;
} }
.recurrence-icon {
margin-right: 12px;
font-size: 20px;
width: 20px;
height: 20px;
}
.recurrence-content {
flex: 1;
}
.recurrence-label {
font-weight: 600;
font-size: 14px;
margin-bottom: 4px;
}
.recurrence-details {
font-size: 12px;
opacity: 0.8;
line-height: 1.2;
}
/* Colores para diferentes tipos de recurrencia */
.recurrence-none {
background-color: #667eea;
color: white;
}
.recurrence-daily {
background-color: #f093fb;
color: white;
}
.recurrence-weekly {
background-color: #4facfe;
color: white;
}
.recurrence-monthly {
background-color: #43e97b;
color: white;
}
.recurrence-yearly {
background-color: #fa709a;
color: white;
}
.recurrence-custom {
background-color: #ff9a9e;
color: #333;
}
/* Estilos para el campo executionTime */
.time-display {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 12px;
background-color: #667eea;
color: white;
border-radius: 16px;
min-width: 100px;
}
.time-icon {
margin-right: 8px;
font-size: 16px;
width: 16px;
height: 16px;
}
.time-value {
font-weight: 600;
font-size: 14px;
}
/* Estilos para el campo nextExecution */
.next-execution {
display: flex;
align-items: center;
justify-content: center;
padding: 8px 12px;
background-color: #4facfe;
color: white;
border-radius: 16px;
min-width: 120px;
}
.execution-icon {
margin-right: 8px;
font-size: 16px;
width: 16px;
height: 16px;
}
/* Estilos para el campo daysOfWeek */
.days-display {
text-align: center;
}
.days-header {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
color: #495057;
}
.days-icon {
margin-right: 6px;
font-size: 16px;
width: 16px;
height: 16px;
color: #4facfe;
}
.days-count {
font-size: 12px;
font-weight: 500;
}
.days-chips {
display: flex;
flex-wrap: wrap;
gap: 4px;
justify-content: center;
}
.day-chip {
background-color: #4facfe;
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
min-width: 32px;
text-align: center;
}
/* Estilos para el campo months */
.months-display {
text-align: center;
}
.months-header {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
color: #495057;
}
.months-icon {
margin-right: 6px;
font-size: 16px;
width: 16px;
height: 16px;
color: #43e97b;
}
.months-count {
font-size: 12px;
font-weight: 500;
}
.months-chips {
display: flex;
flex-wrap: wrap;
gap: 4px;
justify-content: center;
}
.month-chip {
background-color: #43e97b;
color: white;
padding: 4px 8px;
border-radius: 12px;
font-size: 11px;
font-weight: 600;
min-width: 32px;
text-align: center;
}
/* Estilos para el campo enabled */
.status-container {
display: flex;
justify-content: center;
}
.status-indicator {
display: flex;
align-items: center;
padding: 8px 16px;
border-radius: 20px;
font-weight: 600;
font-size: 14px;
min-width: 100px;
justify-content: center;
}
.status-active {
background-color: #43e97b;
color: white;
}
.status-inactive {
background-color: #ff9a9e;
color: #333;
}
.status-icon {
margin-right: 8px;
font-size: 16px;
width: 16px;
height: 16px;
}
/* Estilos para datos no definidos */
.no-data {
text-align: center;
padding: 8px;
}
.no-data-text {
color: #6c757d;
font-style: italic;
font-size: 12px;
}
/* Estilos para el header de acciones */
.header-actions { .header-actions {
margin-bottom: 20px;
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
margin-bottom: 20px;
padding: 16px;
background: #f8f9fa;
border-radius: 8px;
} }
.action-button { .action-button {
color: white;
border: none;
padding: 12px 24px;
font-weight: 600;
cursor: pointer;
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
padding: 12px 24px; transition: all 0.3s ease;
color: white;
border: none;
font-weight: 500;
cursor: pointer;
transition: background-color 0.2s ease;
}
.action-button:hover {
background: #1565c0;
} }
.action-button mat-icon { .action-button mat-icon {
@ -119,90 +358,60 @@ mat-spinner {
height: 20px; height: 20px;
} }
.create-button { /* Estilos para el contenedor de búsqueda */
color: white;
border: none;
padding: 12px 24px;
font-weight: 500;
}
.create-button mat-icon {
margin-right: 8px;
}
.search-container { .search-container {
display: flex; display: flex;
gap: 16px; gap: 16px;
margin: 20px 0; margin-bottom: 20px;
flex-wrap: wrap; flex-wrap: wrap;
} }
.search-string { .search-string {
flex: 1;
min-width: 250px; min-width: 250px;
} }
/* Estilos para los chips de días y meses */ /* Estilos para el paginador */
.days-display, .months-display { .paginator-container {
margin-top: 20px;
display: flex; display: flex;
flex-wrap: wrap; justify-content: center;
gap: 4px;
} }
.day-chip, .month-chip { /* Estilos para los botones de acción en la tabla */
background: #e3f2fd; .mat-column-actions .mat-cell {
color: #1976d2; text-align: center;
padding: 4px 8px;
border-radius: 12px;
font-size: 0.75rem;
font-weight: 500;
border: 1px solid #bbdefb;
} }
/* Estilos para la próxima ejecución */ .mat-column-actions button {
.next-execution { margin: 0 4px;
display: flex; transition: all 0.3s ease;
align-items: center;
gap: 8px;
color: #555;
} }
.execution-icon { .mat-column-actions button:hover {
font-size: 18px; transform: scale(1.1);
color: #2196f3;
} }
/* Mejoras en la tabla */ /* Responsive design */
.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) { @media (max-width: 768px) {
.recurrence-chip {
min-width: 160px;
padding: 10px 12px;
}
.recurrence-label {
font-size: 13px;
}
.recurrence-details {
font-size: 11px;
}
.search-container { .search-container {
flex-direction: column; flex-direction: column;
} }
.search-string { .search-string {
min-width: auto; min-width: 100%;
width: 100%;
}
.header-actions {
justify-content: center;
}
.create-button {
width: 100%;
} }
} }

View File

@ -44,24 +44,22 @@
<td mat-cell *matCellDef="let schedule"> <td mat-cell *matCellDef="let schedule">
<ng-container *ngIf="column.columnDef === 'recurrenceType'"> <ng-container *ngIf="column.columnDef === 'recurrenceType'">
<mat-chip style="padding: 10px; margin: 5px;"> <div class="recurrence-type-container">
<ng-container *ngIf="column.cell(schedule) === 'none'; else scheduledTemplate"> <div class="recurrence-chip" [ngClass]="'recurrence-' + column.cell(schedule).type">
No programado <mat-icon class="recurrence-icon">{{ column.cell(schedule).icon }}</mat-icon>
<div style="font-size: 12px;"> <div class="recurrence-content">
{{ schedule.executionDate | date }} <div class="recurrence-label">{{ column.cell(schedule).label }}</div>
<div class="recurrence-details">{{ column.cell(schedule).details }}</div>
</div> </div>
</ng-container> </div>
<ng-template #scheduledTemplate> </div>
Programado
<div style="font-size: 12px;">
{{ schedule.recurrenceDetails?.initDate | date }} → {{ schedule.recurrenceDetails?.endDate | date}}
</div>
</ng-template>
</mat-chip>
</ng-container> </ng-container>
<ng-container *ngIf="column.columnDef === 'executionTime'"> <ng-container *ngIf="column.columnDef === 'executionTime'">
{{ schedule.executionTime | date: 'HH:mm' }} <div class="time-display">
<mat-icon class="time-icon">schedule</mat-icon>
<span class="time-value">{{ schedule.executionTime | date: 'HH:mm' }}</span>
</div>
</ng-container> </ng-container>
<ng-container *ngIf="column.columnDef === 'nextExecution'"> <ng-container *ngIf="column.columnDef === 'nextExecution'">
@ -72,16 +70,26 @@
</ng-container> </ng-container>
<ng-container *ngIf="column.columnDef === 'daysOfWeek'"> <ng-container *ngIf="column.columnDef === 'daysOfWeek'">
<div class="days-display"> <div class="days-display" *ngIf="column.cell(schedule)">
<span *ngFor="let day of schedule.recurrenceDetails?.daysOfWeek" <div class="days-chips">
class="day-chip">{{ day | slice:0:3 }}</span> <span *ngFor="let day of column.cell(schedule).days"
class="day-chip">{{ day }}</span>
</div>
</div>
<div class="no-data" *ngIf="!column.cell(schedule)">
<span class="no-data-text">No definido</span>
</div> </div>
</ng-container> </ng-container>
<ng-container *ngIf="column.columnDef === 'months'"> <ng-container *ngIf="column.columnDef === 'months'">
<div class="months-display"> <div class="months-display" *ngIf="column.cell(schedule)">
<span *ngFor="let month of schedule.recurrenceDetails?.months" <div class="months-chips">
class="month-chip">{{ month | slice:0:3 }}</span> <span *ngFor="let month of column.cell(schedule).months"
class="month-chip">{{ month }}</span>
</div>
</div>
<div class="no-data" *ngIf="!column.cell(schedule)">
<span class="no-data-text">No definido</span>
</div> </div>
</ng-container> </ng-container>
@ -92,14 +100,12 @@
</ng-container> </ng-container>
<ng-container *ngIf="column.columnDef === 'enabled'"> <ng-container *ngIf="column.columnDef === 'enabled'">
<mat-chip [color]="schedule.enabled ? 'accent' : 'warn'" selected> <div class="status-container">
<ng-container *ngIf="schedule.enabled"> <div class="status-indicator" [ngClass]="schedule.enabled ? 'status-active' : 'status-inactive'">
Activo <mat-icon class="status-icon">{{ schedule.enabled ? 'check_circle' : 'cancel' }}</mat-icon>
</ng-container> <span class="status-text">{{ schedule.enabled ? 'Activo' : 'Inactivo' }}</span>
<ng-container *ngIf="!schedule.enabled"> </div>
Inactivo </div>
</ng-container>
</mat-chip>
</ng-container> </ng-container>
</td> </td>
</ng-container> </ng-container>

View File

@ -27,11 +27,11 @@ 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: 'Tipo de Programación', cell: (schedule: any) => this.getRecurrenceTypeDisplay(schedule) },
{ columnDef: 'executionTime', 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: 'nextExecution', header: 'Próxima ejecución', cell: (schedule: any) => this.calculateNextExecution(schedule) }, { 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: 'daysOfWeek', header: 'Días de la semana', cell: (schedule: any) => this.formatDaysOfWeek(schedule.recurrenceDetails?.daysOfWeek) },
{ columnDef: 'months', header: 'Meses', cell: (schedule: any) => schedule.recurrenceDetails?.months || [] }, { columnDef: 'months', header: 'Meses', cell: (schedule: any) => this.formatMonths(schedule.recurrenceDetails?.months) },
{ columnDef: 'enabled', header: 'Estado', cell: (schedule: any) => schedule.enabled } { columnDef: 'enabled', header: 'Estado', cell: (schedule: any) => schedule.enabled }
]; ];
@ -121,23 +121,49 @@ export class ShowTaskScheduleComponent implements OnInit{
try { try {
if (schedule.recurrenceType === 'none') { if (schedule.recurrenceType === 'none') {
if (schedule.executionDate) { if (schedule.executionDate && schedule.executionTime) {
const execDate = new Date(schedule.executionDate); const executionDate = new Date(schedule.executionDate);
const [hours, minutes] = this.parseExecutionTime(schedule.executionTime);
const fullExecutionDateTime = new Date(
executionDate.getFullYear(),
executionDate.getMonth(),
executionDate.getDate(),
hours,
minutes,
0,
0
);
const now = new Date(); const now = new Date();
if (fullExecutionDateTime < now) {
if (execDate < now) {
return 'Ya ejecutada'; return 'Ya ejecutada';
} }
return this.datePipe.transform(execDate, 'dd/MM/yyyy HH:mm') || 'Fecha inválida'; return this.datePipe.transform(fullExecutionDateTime, 'dd/MM/yyyy HH:mm') || 'Fecha inválida';
} }
return 'Sin fecha'; return 'Sin fecha/hora definida';
} }
// Personalizada (u otros tipos con detalles): calcular siguiente ejecución real
if (schedule.recurrenceDetails) { if (schedule.recurrenceDetails) {
const days = schedule.recurrenceDetails.daysOfWeek?.join(', ') || 'Todos'; const startDate = schedule.recurrenceDetails.initDate ? new Date(schedule.recurrenceDetails.initDate) : null;
const months = schedule.recurrenceDetails.months?.join(', ') || 'Todos'; const endDate = schedule.recurrenceDetails.endDate ? new Date(schedule.recurrenceDetails.endDate) : null;
return `${days} - ${months}`; const days = Array.isArray(schedule.recurrenceDetails.daysOfWeek) ? schedule.recurrenceDetails.daysOfWeek : [];
const months = Array.isArray(schedule.recurrenceDetails.months) ? schedule.recurrenceDetails.months : [];
if (!startDate || !endDate || days.length === 0 || months.length === 0) {
return 'Configuración incompleta';
}
const selectedDayIndices = new Set<number>(days.map((d: string) => this.getDayIndexFromName(d)));
const selectedMonthIndices = new Set<number>(months.map((m: string) => this.getMonthIndexFromName(m)));
const [hours, minutes] = this.parseExecutionTime(schedule.executionTime);
const next = this.findNextExecutionDate(startDate, endDate, hours, minutes, selectedDayIndices, selectedMonthIndices);
if (next) {
return this.datePipe.transform(next, 'dd/MM/yyyy HH:mm') || 'Fecha inválida';
}
return 'No hay próximas ejecuciones';
} }
return 'Configuración incompleta'; return 'Configuración incompleta';
@ -146,6 +172,76 @@ export class ShowTaskScheduleComponent implements OnInit{
} }
} }
private parseExecutionTime(executionTime: any): [number, number] {
if (typeof executionTime === 'string') {
const match = executionTime.match(/^(\d{2}):(\d{2})/);
if (match) {
return [parseInt(match[1], 10), parseInt(match[2], 10)];
}
const parsed = new Date(executionTime);
if (!isNaN(parsed.getTime())) {
return [parsed.getHours(), parsed.getMinutes()];
}
return [0, 0];
}
if (executionTime instanceof Date && !isNaN(executionTime.getTime())) {
return [executionTime.getHours(), executionTime.getMinutes()];
}
return [0, 0];
}
private findNextExecutionDate(startDate: Date, endDate: Date, hours: number, minutes: number, selectedDays: Set<number>, selectedMonths: Set<number>): Date | null {
const now = new Date();
const startCandidate = new Date(Math.max(new Date(startDate).setHours(0, 0, 0, 0), new Date(now).setHours(0, 0, 0, 0)));
const endLimit = new Date(endDate);
endLimit.setHours(23, 59, 59, 999);
const cursor = new Date(startCandidate);
while (cursor <= endLimit) {
if (selectedMonths.has(cursor.getMonth()) && selectedDays.has(cursor.getDay())) {
const candidateDateTime = new Date(cursor);
candidateDateTime.setHours(hours, minutes, 0, 0);
if (candidateDateTime >= now) {
return candidateDateTime;
}
}
cursor.setDate(cursor.getDate() + 1);
}
return null;
}
private getDayIndexFromName(dayName: string): number {
const mapping: { [key: string]: number } = {
sunday: 0,
monday: 1,
tuesday: 2,
wednesday: 3,
thursday: 4,
friday: 5,
saturday: 6
};
return mapping[dayName?.toLowerCase?.()] ?? -1;
}
private getMonthIndexFromName(monthName: string): number {
const mapping: { [key: string]: number } = {
january: 0,
february: 1,
march: 2,
april: 3,
may: 4,
june: 5,
july: 6,
august: 7,
september: 8,
october: 9,
november: 10,
december: 11
};
return mapping[monthName?.toLowerCase?.()] ?? -1;
}
onPageChange(event: any): void { onPageChange(event: any): void {
this.page = event.pageIndex; this.page = event.pageIndex;
this.itemsPerPage = event.pageSize; this.itemsPerPage = event.pageSize;
@ -155,4 +251,105 @@ export class ShowTaskScheduleComponent implements OnInit{
onNoClick(): void { onNoClick(): void {
this.dialogRef.close(false); this.dialogRef.close(false);
} }
getRecurrenceTypeDisplay(schedule: any): any {
if (schedule.recurrenceType === 'none') {
return {
type: 'none',
label: 'Ejecución única',
icon: 'event',
color: 'primary',
details: schedule.executionDate ?
`Fecha: ${this.datePipe.transform(schedule.executionDate, 'dd/MM/yyyy')}` :
'Sin fecha definida'
};
} else if (schedule.recurrenceType === 'daily') {
return {
type: 'daily',
label: 'Diaria',
icon: 'repeat',
color: 'accent',
details: 'Todos los días'
};
} else if (schedule.recurrenceType === 'weekly') {
return {
type: 'weekly',
label: 'Semanal',
icon: 'view_week',
color: 'accent',
details: schedule.recurrenceDetails?.daysOfWeek?.length > 0 ?
`Días: ${schedule.recurrenceDetails.daysOfWeek.join(', ')}` :
'Todos los días'
};
} else if (schedule.recurrenceType === 'monthly') {
return {
type: 'monthly',
label: 'Mensual',
icon: 'calendar_month',
color: 'accent',
details: schedule.recurrenceDetails?.months?.length > 0 ?
`Meses: ${schedule.recurrenceDetails.months.join(', ')}` :
'Todos los meses'
};
} else if (schedule.recurrenceType === 'yearly') {
return {
type: 'yearly',
label: 'Anual',
icon: 'event_note',
color: 'accent',
details: 'Una vez al año'
};
} else {
return {
type: 'custom',
label: 'Personalizada',
icon: 'settings',
color: 'warn',
details: 'Configuración especial'
};
}
}
formatDaysOfWeek(days: string[]): any {
if (!days || days.length === 0) return null;
const dayMap: { [key: string]: string } = {
'monday': 'Lun',
'tuesday': 'Mar',
'wednesday': 'Mié',
'thursday': 'Jue',
'friday': 'Vie',
'saturday': 'Sáb',
'sunday': 'Dom'
};
return {
days: days.map(day => dayMap[day.toLowerCase()] || day),
count: days.length
};
}
formatMonths(months: string[]): any {
if (!months || months.length === 0) return null;
const monthMap: { [key: string]: string } = {
'january': 'Ene',
'february': 'Feb',
'march': 'Mar',
'april': 'Abr',
'may': 'May',
'june': 'Jun',
'july': 'Jul',
'august': 'Ago',
'september': 'Sep',
'october': 'Oct',
'november': 'Nov',
'december': 'Dic'
};
return {
months: months.map(month => monthMap[month.toLowerCase()] || month),
count: months.length
};
}
} }

View File

@ -28,8 +28,8 @@ export class ShowTaskScriptComponent implements OnInit{
{ columnDef: 'id', header: 'ID', cell: (client: any) => client.id }, { columnDef: 'id', header: 'ID', cell: (client: any) => client.id },
{ columnDef: 'order', header: 'Orden', cell: (client: any) => client.order }, { columnDef: 'order', header: 'Orden', cell: (client: any) => client.order },
{ columnDef: 'content', header: 'Script', cell: (client: any) => client.content }, { columnDef: 'content', header: 'Script', cell: (client: any) => client.content },
{ columnDef: 'type', header: 'Type', cell: (client: any) => client.type }, { columnDef: 'type', header: 'Tipo', cell: (client: any) => client.type },
{ columnDef: 'parameters', header: 'Parameters', cell: (client: any) => client.parameters }, { columnDef: 'parameters', header: 'Parámetros', cell: (client: any) => client.parameters },
]; ];
displayedColumns: string[] = ['id', 'order', 'type', 'parameters', 'content', 'actions']; displayedColumns: string[] = ['id', 'order', 'type', 'parameters', 'content', 'actions'];

View File

@ -124,6 +124,13 @@
</div> </div>
</div> </div>
<!-- Checkbox para forzar push cuando estamos en modo modificar imagen git -->
<div class="force-push-container" *ngIf="imageType === 'git' && hasValidGitData">
<mat-checkbox [(ngModel)]="forcePush" class="force-push-checkbox">
Opción 'Forzar push'
</mat-checkbox>
</div>
<div class="repository-header" *ngIf="!hasValidGitData"> <div class="repository-header" *ngIf="!hasValidGitData">
<button *ngIf="imageType === 'git'" <button *ngIf="imageType === 'git'"
class="create-repository-button" class="create-repository-button"

View File

@ -56,6 +56,7 @@ export class CreateClientImageComponent implements OnInit{
isDestinationBranchEditable: boolean = false; isDestinationBranchEditable: boolean = false;
repositoryNotFound: boolean = false; repositoryNotFound: boolean = false;
newlyCreatedRepository: any = null; newlyCreatedRepository: any = null;
forcePush: boolean = false; // Nueva propiedad para el checkbox de forzar push
get hasValidGitData(): boolean { get hasValidGitData(): boolean {
return this.gitData && this.gitData.repo && this.gitData.branch; return this.gitData && this.gitData.repo && this.gitData.branch;
@ -406,6 +407,7 @@ export class CreateClientImageComponent implements OnInit{
gitRepository: gitRepoName, gitRepository: gitRepoName,
originalBranch: originBranch, originalBranch: originBranch,
destinationBranch: this.destinationBranch, destinationBranch: this.destinationBranch,
options: this.forcePush,
queue: result queue: result
}; };
} }
@ -641,15 +643,12 @@ export class CreateClientImageComponent implements OnInit{
openScheduleModal(): void { openScheduleModal(): void {
let scope = 'clients'; let scope = 'clients';
// Verificar que tenemos la información del cliente
if (!this.client) { if (!this.client) {
this.toastService.error('No se ha cargado la información del cliente'); this.toastService.error('No se ha cargado la información del cliente');
return; return;
} }
// Crear un array con el objeto cliente completo
let selectedClients = [this.client]; let selectedClients = [this.client];
console.log(selectedClients);
const dialogRef = this.dialog.open(CreateTaskComponent, { const dialogRef = this.dialog.open(CreateTaskComponent, {
width: '800px', width: '800px',
@ -657,14 +656,13 @@ export class CreateClientImageComponent implements OnInit{
scope: scope, scope: scope,
selectedClients: selectedClients, selectedClients: selectedClients,
organizationalUnit: this.client['@id'], organizationalUnit: this.client['@id'],
source: 'create-image', source: 'assistant',
runScriptContext: null runScriptContext: null
} }
}); });
dialogRef.afterClosed().subscribe((result: any) => { dialogRef.afterClosed().subscribe((result: any) => {
if (result) { if (result) {
// Verificar que tenemos la partición seleccionada
if (!this.selectedPartition) { if (!this.selectedPartition) {
this.toastService.error('Debe seleccionar una partición'); this.toastService.error('Debe seleccionar una partición');
return; return;
@ -678,7 +676,8 @@ export class CreateClientImageComponent implements OnInit{
action: this.monolithicAction, action: this.monolithicAction,
diskNumber: this.selectedPartition.diskNumber, diskNumber: this.selectedPartition.diskNumber,
partitionNumber: this.selectedPartition.partitionNumber, partitionNumber: this.selectedPartition.partitionNumber,
imageName: this.monolithicAction === 'create' ? this.name : this.selectedImage?.name imageName: this.monolithicAction === 'create' ? this.name : this.selectedImage?.name,
imageUuid: this.monolithicAction === 'update' ? this.selectedImage?.uuid : null
}; };
} else if (this.imageType === 'git') { } else if (this.imageType === 'git') {
payload = { payload = {

View File

@ -48,6 +48,6 @@ export class ConvertImageToVirtualComponent implements OnInit {
} }
close() { close() {
this.dialogRef.close(true); this.dialogRef.close(false);
} }
} }

View File

@ -50,6 +50,6 @@ export class ConvertImageComponent implements OnInit{
} }
close() { close() {
this.dialogRef.close(true); this.dialogRef.close(false);
} }
} }

View File

@ -65,6 +65,6 @@ export class EditImageComponent implements OnInit{
} }
close() { close() {
this.dialogRef.close(); this.dialogRef.close(false);
} }
} }

View File

@ -49,6 +49,6 @@ export class ImportImageComponent implements OnInit {
} }
close() { close() {
this.dialogRef.close(true); this.dialogRef.close(false);
} }
} }

View File

@ -84,8 +84,10 @@ export class RepositoriesComponent implements OnInit {
width: '600px' width: '600px'
}); });
dialogRef.afterClosed().subscribe(() => { dialogRef.afterClosed().subscribe((result) => {
this.search(); if (result) {
this.search();
}
}); });
} }
@ -108,7 +110,11 @@ export class RepositoriesComponent implements OnInit {
this.dialog.open(ManageRepositoryComponent, { this.dialog.open(ManageRepositoryComponent, {
width: '600px', width: '600px',
data: repository['@id'] data: repository['@id']
}).afterClosed().subscribe(() => this.search()); }).afterClosed().subscribe((result) => {
if (result) {
this.search();
}
});
} }
deleteRepository(event: MouseEvent,command: any): void { deleteRepository(event: MouseEvent,command: any): void {
@ -141,7 +147,9 @@ export class RepositoriesComponent implements OnInit {
}); });
dialogRef.afterClosed().subscribe(result => { dialogRef.afterClosed().subscribe(result => {
this.search(); if (result) {
this.search();
}
}); });
} }
@ -155,7 +163,9 @@ export class RepositoriesComponent implements OnInit {
}); });
dialogRef.afterClosed().subscribe(result => { dialogRef.afterClosed().subscribe(result => {
this.search(); if (result) {
this.search();
}
}); });
} }

View File

@ -7,6 +7,11 @@
box-sizing: border-box; box-sizing: border-box;
} }
::ng-deep .mat-mdc-dialog-content {
height: 100% !important;
max-height: 100vh !important;
}
.header-container { .header-container {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -144,7 +144,6 @@ export class ShowMonoliticImagesComponent implements OnInit {
showImageInfo(event: MouseEvent, image:any) { showImageInfo(event: MouseEvent, image:any) {
event.stopPropagation(); event.stopPropagation();
this.loading = true;
this.loadImageAlert(image).subscribe( this.loadImageAlert(image).subscribe(
response => { response => {
this.alertMessage = response; this.alertMessage = response;
@ -193,7 +192,6 @@ export class ShowMonoliticImagesComponent implements OnInit {
} }
toggleAction(image: any, action:string): void { toggleAction(image: any, action:string): void {
this.loading = true;
switch (action) { switch (action) {
case 'get-aux': case 'get-aux':
this.http.post(`${this.baseUrl}/image-image-repositories/server/${image.uuid}/create-aux-files`, {}).subscribe({ this.http.post(`${this.baseUrl}/image-image-repositories/server/${image.uuid}/create-aux-files`, {}).subscribe({