commit
c5c670cb87
|
@ -1,5 +1,13 @@
|
|||
# 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
|
||||
### Added
|
||||
- Se ha incluido una integracion con el agente, para generar los scripts que se llaman al particionar.
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
<mat-form-field appearance="fill" class="w-full">
|
||||
<mat-label>Repetición</mat-label>
|
||||
<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-form-field>
|
||||
|
||||
|
|
|
@ -14,7 +14,10 @@ export class CreateTaskScheduleComponent implements OnInit{
|
|||
form: FormGroup;
|
||||
baseUrl: 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'];
|
||||
isSingleDateSelected: boolean = true;
|
||||
monthsList: string[] = [
|
||||
|
@ -108,12 +111,27 @@ export class CreateTaskScheduleComponent implements OnInit{
|
|||
}
|
||||
|
||||
formatExecutionTime(time: string | Date): string {
|
||||
const date = (time instanceof Date) ? time : new Date(time);
|
||||
if (isNaN(date.getTime())) {
|
||||
console.error('Invalid execution time:', time);
|
||||
if (typeof time === 'string') {
|
||||
const hhmmMatch = time.match(/^\d{2}:\d{2}/);
|
||||
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 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 {
|
||||
|
@ -191,7 +209,7 @@ export class CreateTaskScheduleComponent implements OnInit{
|
|||
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) {
|
||||
|
@ -200,31 +218,102 @@ export class CreateTaskScheduleComponent implements OnInit{
|
|||
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);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
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 {
|
||||
const start = new Date(startDate);
|
||||
private calculateExecutionCountExact(startDate: Date, endDate: Date, selectedDays: Set<number>, selectedMonths: Set<number>): number {
|
||||
const cursor = new Date(startDate);
|
||||
cursor.setHours(0, 0, 0, 0);
|
||||
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));
|
||||
end.setHours(23, 59, 59, 999);
|
||||
|
||||
let count = 0;
|
||||
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 {
|
||||
|
|
|
@ -69,48 +69,287 @@ table {
|
|||
}
|
||||
|
||||
.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;
|
||||
justify-content: end;
|
||||
margin-bottom: 30px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
mat-spinner {
|
||||
margin: 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.subnets-button-row {
|
||||
.recurrence-chip {
|
||||
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 {
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 20px;
|
||||
padding: 16px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.action-button {
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
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;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.action-button mat-icon {
|
||||
|
@ -119,90 +358,60 @@ mat-spinner {
|
|||
height: 20px;
|
||||
}
|
||||
|
||||
.create-button {
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 12px 24px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.create-button mat-icon {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
/* Estilos para el contenedor de búsqueda */
|
||||
.search-container {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
margin: 20px 0;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-string {
|
||||
flex: 1;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
/* Estilos para los chips de días y meses */
|
||||
.days-display, .months-display {
|
||||
/* Estilos para el paginador */
|
||||
.paginator-container {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.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 los botones de acción en la tabla */
|
||||
.mat-column-actions .mat-cell {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/* Estilos para la próxima ejecución */
|
||||
.next-execution {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
color: #555;
|
||||
.mat-column-actions button {
|
||||
margin: 0 4px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.execution-icon {
|
||||
font-size: 18px;
|
||||
color: #2196f3;
|
||||
.mat-column-actions button:hover {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
/* 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 */
|
||||
/* Responsive design */
|
||||
@media (max-width: 768px) {
|
||||
.recurrence-chip {
|
||||
min-width: 160px;
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.recurrence-label {
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.recurrence-details {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.search-string {
|
||||
min-width: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.create-button {
|
||||
width: 100%;
|
||||
min-width: 100%;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,24 +44,22 @@
|
|||
<td mat-cell *matCellDef="let schedule">
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'recurrenceType'">
|
||||
<mat-chip style="padding: 10px; margin: 5px;">
|
||||
<ng-container *ngIf="column.cell(schedule) === 'none'; else scheduledTemplate">
|
||||
No programado
|
||||
<div style="font-size: 12px;">
|
||||
{{ schedule.executionDate | date }}
|
||||
<div class="recurrence-type-container">
|
||||
<div class="recurrence-chip" [ngClass]="'recurrence-' + column.cell(schedule).type">
|
||||
<mat-icon class="recurrence-icon">{{ column.cell(schedule).icon }}</mat-icon>
|
||||
<div class="recurrence-content">
|
||||
<div class="recurrence-label">{{ column.cell(schedule).label }}</div>
|
||||
<div class="recurrence-details">{{ column.cell(schedule).details }}</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #scheduledTemplate>
|
||||
Programado
|
||||
<div style="font-size: 12px;">
|
||||
{{ schedule.recurrenceDetails?.initDate | date }} → {{ schedule.recurrenceDetails?.endDate | date}}
|
||||
</div>
|
||||
</ng-template>
|
||||
</mat-chip>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
|
||||
<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 *ngIf="column.columnDef === 'nextExecution'">
|
||||
|
@ -72,16 +70,26 @@
|
|||
</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 class="days-display" *ngIf="column.cell(schedule)">
|
||||
<div class="days-chips">
|
||||
<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>
|
||||
</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 class="months-display" *ngIf="column.cell(schedule)">
|
||||
<div class="months-chips">
|
||||
<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>
|
||||
</ng-container>
|
||||
|
||||
|
@ -92,14 +100,12 @@
|
|||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'enabled'">
|
||||
<mat-chip [color]="schedule.enabled ? 'accent' : 'warn'" selected>
|
||||
<ng-container *ngIf="schedule.enabled">
|
||||
Activo
|
||||
</ng-container>
|
||||
<ng-container *ngIf="!schedule.enabled">
|
||||
Inactivo
|
||||
</ng-container>
|
||||
</mat-chip>
|
||||
<div class="status-container">
|
||||
<div class="status-indicator" [ngClass]="schedule.enabled ? 'status-active' : 'status-inactive'">
|
||||
<mat-icon class="status-icon">{{ schedule.enabled ? 'check_circle' : 'cancel' }}</mat-icon>
|
||||
<span class="status-text">{{ schedule.enabled ? 'Activo' : 'Inactivo' }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
|
|
@ -27,11 +27,11 @@ 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: '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: '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: 'daysOfWeek', header: 'Días de la semana', cell: (schedule: any) => this.formatDaysOfWeek(schedule.recurrenceDetails?.daysOfWeek) },
|
||||
{ columnDef: 'months', header: 'Meses', cell: (schedule: any) => this.formatMonths(schedule.recurrenceDetails?.months) },
|
||||
{ columnDef: 'enabled', header: 'Estado', cell: (schedule: any) => schedule.enabled }
|
||||
];
|
||||
|
||||
|
@ -121,23 +121,49 @@ export class ShowTaskScheduleComponent implements OnInit{
|
|||
|
||||
try {
|
||||
if (schedule.recurrenceType === 'none') {
|
||||
if (schedule.executionDate) {
|
||||
const execDate = new Date(schedule.executionDate);
|
||||
if (schedule.executionDate && schedule.executionTime) {
|
||||
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();
|
||||
|
||||
if (execDate < now) {
|
||||
if (fullExecutionDateTime < now) {
|
||||
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) {
|
||||
const days = schedule.recurrenceDetails.daysOfWeek?.join(', ') || 'Todos';
|
||||
const months = schedule.recurrenceDetails.months?.join(', ') || 'Todos';
|
||||
return `${days} - ${months}`;
|
||||
const startDate = schedule.recurrenceDetails.initDate ? new Date(schedule.recurrenceDetails.initDate) : null;
|
||||
const endDate = schedule.recurrenceDetails.endDate ? new Date(schedule.recurrenceDetails.endDate) : null;
|
||||
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';
|
||||
|
@ -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 {
|
||||
this.page = event.pageIndex;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
|
@ -155,4 +251,105 @@ export class ShowTaskScheduleComponent implements OnInit{
|
|||
onNoClick(): void {
|
||||
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
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -28,8 +28,8 @@ export class ShowTaskScriptComponent implements OnInit{
|
|||
{ columnDef: 'id', header: 'ID', cell: (client: any) => client.id },
|
||||
{ columnDef: 'order', header: 'Orden', cell: (client: any) => client.order },
|
||||
{ columnDef: 'content', header: 'Script', cell: (client: any) => client.content },
|
||||
{ columnDef: 'type', header: 'Type', cell: (client: any) => client.type },
|
||||
{ columnDef: 'parameters', header: 'Parameters', cell: (client: any) => client.parameters },
|
||||
{ columnDef: 'type', header: 'Tipo', cell: (client: any) => client.type },
|
||||
{ columnDef: 'parameters', header: 'Parámetros', cell: (client: any) => client.parameters },
|
||||
];
|
||||
|
||||
displayedColumns: string[] = ['id', 'order', 'type', 'parameters', 'content', 'actions'];
|
||||
|
|
|
@ -124,6 +124,13 @@
|
|||
</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">
|
||||
<button *ngIf="imageType === 'git'"
|
||||
class="create-repository-button"
|
||||
|
|
|
@ -56,6 +56,7 @@ export class CreateClientImageComponent implements OnInit{
|
|||
isDestinationBranchEditable: boolean = false;
|
||||
repositoryNotFound: boolean = false;
|
||||
newlyCreatedRepository: any = null;
|
||||
forcePush: boolean = false; // Nueva propiedad para el checkbox de forzar push
|
||||
|
||||
get hasValidGitData(): boolean {
|
||||
return this.gitData && this.gitData.repo && this.gitData.branch;
|
||||
|
@ -406,6 +407,7 @@ export class CreateClientImageComponent implements OnInit{
|
|||
gitRepository: gitRepoName,
|
||||
originalBranch: originBranch,
|
||||
destinationBranch: this.destinationBranch,
|
||||
options: this.forcePush,
|
||||
queue: result
|
||||
};
|
||||
}
|
||||
|
@ -641,15 +643,12 @@ export class CreateClientImageComponent implements OnInit{
|
|||
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',
|
||||
|
@ -657,14 +656,13 @@ export class CreateClientImageComponent implements OnInit{
|
|||
scope: scope,
|
||||
selectedClients: selectedClients,
|
||||
organizationalUnit: this.client['@id'],
|
||||
source: 'create-image',
|
||||
source: 'assistant',
|
||||
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;
|
||||
|
@ -678,7 +676,8 @@ export class CreateClientImageComponent implements OnInit{
|
|||
action: this.monolithicAction,
|
||||
diskNumber: this.selectedPartition.diskNumber,
|
||||
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') {
|
||||
payload = {
|
||||
|
|
|
@ -48,6 +48,6 @@ export class ConvertImageToVirtualComponent implements OnInit {
|
|||
}
|
||||
|
||||
close() {
|
||||
this.dialogRef.close(true);
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,6 +50,6 @@ export class ConvertImageComponent implements OnInit{
|
|||
}
|
||||
|
||||
close() {
|
||||
this.dialogRef.close(true);
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,6 +65,6 @@ export class EditImageComponent implements OnInit{
|
|||
}
|
||||
|
||||
close() {
|
||||
this.dialogRef.close();
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -49,6 +49,6 @@ export class ImportImageComponent implements OnInit {
|
|||
}
|
||||
|
||||
close() {
|
||||
this.dialogRef.close(true);
|
||||
this.dialogRef.close(false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -84,8 +84,10 @@ export class RepositoriesComponent implements OnInit {
|
|||
width: '600px'
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(() => {
|
||||
this.search();
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.search();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -108,7 +110,11 @@ export class RepositoriesComponent implements OnInit {
|
|||
this.dialog.open(ManageRepositoryComponent, {
|
||||
width: '600px',
|
||||
data: repository['@id']
|
||||
}).afterClosed().subscribe(() => this.search());
|
||||
}).afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.search();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteRepository(event: MouseEvent,command: any): void {
|
||||
|
@ -141,7 +147,9 @@ export class RepositoriesComponent implements OnInit {
|
|||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
this.search();
|
||||
if (result) {
|
||||
this.search();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -155,7 +163,9 @@ export class RepositoriesComponent implements OnInit {
|
|||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
this.search();
|
||||
if (result) {
|
||||
this.search();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -7,6 +7,11 @@
|
|||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
::ng-deep .mat-mdc-dialog-content {
|
||||
height: 100% !important;
|
||||
max-height: 100vh !important;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
|
|
@ -144,7 +144,6 @@ export class ShowMonoliticImagesComponent implements OnInit {
|
|||
|
||||
showImageInfo(event: MouseEvent, image:any) {
|
||||
event.stopPropagation();
|
||||
this.loading = true;
|
||||
this.loadImageAlert(image).subscribe(
|
||||
response => {
|
||||
this.alertMessage = response;
|
||||
|
@ -193,7 +192,6 @@ export class ShowMonoliticImagesComponent implements OnInit {
|
|||
}
|
||||
|
||||
toggleAction(image: any, action:string): void {
|
||||
this.loading = true;
|
||||
switch (action) {
|
||||
case 'get-aux':
|
||||
this.http.post(`${this.baseUrl}/image-image-repositories/server/${image.uuid}/create-aux-files`, {}).subscribe({
|
||||
|
|
Loading…
Reference in New Issue