diff --git a/ogWebconsole/src/app/components/task-logs/task-logs.component.css b/ogWebconsole/src/app/components/task-logs/task-logs.component.css index 195127a..127ad84 100644 --- a/ogWebconsole/src/app/components/task-logs/task-logs.component.css +++ b/ogWebconsole/src/app/components/task-logs/task-logs.component.css @@ -4,6 +4,9 @@ align-items: center; padding: 10px 10px; border-bottom: 1px solid #ddd; + background: white; + color: #333; + border-radius: 8px 8px 0 0; } .header-container-title { @@ -12,6 +15,182 @@ margin-left: 1em; } +.header-container-title h2 { + margin: 0; + font-weight: 500; +} + +.header-actions { + display: flex; + gap: 10px; + align-items: center; +} + +.action-button { + display: flex; + align-items: center; + gap: 5px; + padding: 8px 16px; + border-radius: 20px; + border: 1px solid #ddd; + cursor: pointer; + font-weight: 500; + transition: all 0.3s ease; + background: white; + color: #333; +} + +.action-button:hover { + background: #f8f9fa; + transform: translateY(-1px); + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.action-button.secondary { + background: #f8f9fa; + border-color: #adb5bd; +} + +.action-button.secondary:hover { + background: #e9ecef; +} + +.stats-container { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); + gap: 15px; + margin: 20px 0; + padding: 0 10px; +} + +.stat-card { + background: white; + border-radius: 12px; + padding: 20px; + text-align: center; + box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); + transition: transform 0.3s ease, box-shadow 0.3s ease; + border-left: 4px solid #667eea; +} + +.stat-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 15px rgba(0, 0, 0, 0.15); +} + +.stat-total { + border-left-color: #667eea; + background: #667eea; + color: white; +} + +.stat-total .stat-number, +.stat-total .stat-label { + color: white; +} + +.stat-today { + border-left-color: #17a2b8; + background: #17a2b8; + color: white; +} + +.stat-today .stat-number, +.stat-today .stat-label { + color: white; +} + +.stat-success { + border-left-color: #28a745; + background: #28a745; + color: white; +} + +.stat-success .stat-number, +.stat-success .stat-label { + color: white; +} + +.stat-failed { + border-left-color: #dc3545; + background: #dc3545; + color: white; +} + +.stat-failed .stat-number, +.stat-failed .stat-label { + color: white; +} + +.stat-in-progress { + border-left-color: #ffc107; + background: #ffc107; + color: #212529; +} + +.stat-in-progress .stat-number, +.stat-in-progress .stat-label { + color: #212529; +} + +.stat-number { + font-size: 2rem; + font-weight: bold; + color: #667eea; + margin-bottom: 5px; +} + +.stat-label { + font-size: 0.9rem; + color: #666; + font-weight: 500; +} + +.filters-section { + background: #f8f9fa; + border-radius: 8px; + margin: 20px 0; + padding: 20px; + border: 1px solid #e9ecef; +} + +.filters-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 15px; +} + +.filters-header h3 { + margin: 0; + color: #495057; + font-weight: 500; +} + +.search-container { + display: flex; + flex-direction: column; + gap: 15px; + transition: all 0.3s ease; +} + +.search-container.expanded { + gap: 20px; +} + +.filter-row { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); + gap: 15px; + align-items: end; +} + +.advanced-filters { + border-top: 1px solid #dee2e6; + padding-top: 15px; + margin-top: 10px; +} + .calendar-button-row { display: flex; gap: 15px; @@ -34,14 +213,6 @@ table { width: 100%; } -.search-container { - display: flex; - justify-content: space-between; - align-items: center; - margin: 1.5rem 0rem 1.5rem 0rem; - box-sizing: border-box; -} - .search-string { flex: 1; padding: 5px; @@ -57,40 +228,145 @@ table { padding: 5px; } +.search-date { + flex: 1; + padding: 5px; +} + .mat-elevation-z8 { - box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2); + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); + border-radius: 8px; + overflow: hidden; +} + +.table-container { + background: white; + border-radius: 8px; + overflow: hidden; + box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1); +} + +.table-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 15px 20px; + background: #f8f9fa; + border-bottom: 1px solid #dee2e6; +} + +.table-info { + color: #6c757d; + font-size: 0.9rem; +} + +.table-actions { + display: flex; + gap: 10px; +} + +.column-header { + display: flex; + align-items: center; + gap: 5px; +} + +.sort-button { + opacity: 0.5; + transition: opacity 0.3s ease; +} + +.sort-button:hover { + opacity: 1; +} + +.sort-button.active { + opacity: 1; + color: #667eea; +} + +.command-cell, .client-cell, .date-cell { + display: flex; + flex-direction: column; + gap: 2px; +} + +.command-name, .client-name { + font-weight: 500; + color: #212529; +} + +.command-id, .client-ip { + font-size: 0.75rem; + color: #6c757d; +} + +.date-time { + font-size: 0.85rem; + color: #212529; +} + +.date-relative { + font-size: 0.7rem; + color: #6c757d; + font-style: italic; } .progress-container { display: flex; align-items: center; gap: 10px; + width: 100%; +} + +.progress-text { + font-size: 0.8rem; + font-weight: 500; + color: #667eea; + min-width: 35px; +} + +/* Ajuste para el botón de cancelar en la barra de progreso */ +.progress-container .cancel-button { + margin-left: auto; + flex-shrink: 0; } .paginator-container { display: flex; justify-content: end; - margin-bottom: 30px; + margin: 20px 0; + padding: 0 10px; } .chip-failed { - background-color: #e87979 !important; - color: white; + background-color: #ff6b6b !important; + color: white !important; + font-weight: 500; } .chip-success { - background-color: #46c446 !important; - color: white; + background-color: #51cf66 !important; + color: white !important; + font-weight: 500; } .chip-pending { - background-color: #bebdbd !important; - color: black; + background-color: #74c0fc !important; + color: white !important; + font-weight: 500; } .chip-in-progress { - background-color: #f5a623 !important; - color: white; + background-color: #ffd43b !important; + color: #212529 !important; + font-weight: 500; +} + +.chip-cancelled { + background-color: #adb5bd !important; + color: white !important; + font-weight: 500; } .status-progress-flex { @@ -99,6 +375,45 @@ table { gap: 8px; } +.status-option { + display: flex; + align-items: center; + gap: 8px; +} + +.status-indicator { + width: 12px; + height: 12px; + border-radius: 50%; +} + +.status-indicator.failed { background-color: #dc3545; } +.status-indicator.success { background-color: #28a745; } +.status-indicator.pending { background-color: #17a2b8; } +.status-indicator.in-progress { background-color: #ffc107; } +.status-indicator.cancelled { background-color: #6c757d; } + +.client-option { + display: flex; + flex-direction: column; + gap: 2px; +} + +.client-name { + font-weight: 500; +} + +.client-details { + font-size: 0.8rem; + color: #6c757d; +} + +.action-buttons { + display: flex; + gap: 5px; + justify-content: center; +} + button.cancel-button { display: flex; align-items: center; @@ -107,12 +422,83 @@ button.cancel-button { } .cancel-button { - color: red; + color: #dc3545; background-color: transparent; border: none; padding: 0; + transition: all 0.3s ease; +} + +.cancel-button:hover { + background-color: rgba(220, 53, 69, 0.1); + border-radius: 50%; } .cancel-button mat-icon { - color: red; + color: #dc3545; +} + +.selected-row { + background-color: rgba(102, 126, 234, 0.1) !important; +} + +.mat-row:hover { + background-color: rgba(102, 126, 234, 0.05); + cursor: pointer; +} + +@media (max-width: 768px) { + .header-container { + flex-direction: column; + gap: 15px; + text-align: center; + background: white; + color: #333; + } + + .header-actions { + width: 100%; + justify-content: center; + } + + .stats-container { + grid-template-columns: repeat(2, 1fr); + } + + .filter-row { + grid-template-columns: 1fr; + } + + .table-header { + flex-direction: column; + gap: 10px; + text-align: center; + } + + .action-buttons { + flex-direction: column; + gap: 2px; + } +} + +@keyframes fadeIn { + from { opacity: 0; transform: translateY(10px); } + to { opacity: 1; transform: translateY(0); } +} + +.stats-container, .filters-section, .table-container { + animation: fadeIn 0.5s ease-out; +} + +.loading-overlay { + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(255, 255, 255, 0.8); + display: flex; + align-items: center; + justify-content: center; + z-index: 1000; } diff --git a/ogWebconsole/src/app/components/task-logs/task-logs.component.html b/ogWebconsole/src/app/components/task-logs/task-logs.component.html index eeb2930..39ca165 100644 --- a/ogWebconsole/src/app/components/task-logs/task-logs.component.html +++ b/ogWebconsole/src/app/components/task-logs/task-logs.component.html @@ -1,5 +1,5 @@
- @@ -8,91 +8,181 @@ translate }}
-
+
+
-
- - - - -
- {{ client.name }} - - {{ client.ip }} — {{ client.mac }} - -
-
+
+
+
{{ totalStats.total }}
+
{{ 'totalTraces' | translate }}
+
+
+
{{ getStatusCount('today') }}
+
{{ 'todayTraces' | translate }}
+
+
+
{{ getStatusCount('success') }}
+
{{ 'successful' | translate }}
+
+
+
{{ getStatusCount('failed') }}
+
{{ 'failed' | translate }}
+
+
+
{{ getStatusCount('in-progress') }}
+
{{ 'inProgress' | translate }}
+
+
-
- - Por favor, ingrese el nombre del cliente -
+
- - {{ 'commandSelectStepText' | translate }} - - - {{ translateCommand(command.name) }} - - - - +
+
+ + + + +
+ {{ client.name }} + {{ client.ip }} — {{ client.mac }} +
+
+
+ + {{ 'enterClientName' | translate }} +
+ + {{ 'commandSelectStepText' | translate }} + + + {{ translateCommand(command.name) }} + + + + - - Estado - - Fallido - Pendiente de ejecutar - Ejecutando - Completado con éxito - Cancelado - - - + + {{ 'status' | translate }} + + +
+
+ {{ 'failed' | translate }} +
+
+ +
+
+ {{ 'pending' | translate }} +
+
+ +
+
+ {{ 'inProgress' | translate }} +
+
+ +
+
+ {{ 'success' | translate }} +
+
+ +
+
+ {{ 'cancelled' | translate }} +
+
+
+ +
+
- - Desde - - - - +
+ + {{ 'fromDate' | translate }} + + + + - - Hasta - - - - + + {{ 'toDate' | translate }} + + + + + + {{ 'sortBy' | translate }} + + {{ 'executionDate' | translate }} + {{ 'status' | translate }} + {{ 'command' | translate }} + {{ 'client' | translate }} + + +
+
-
+
+
+
+ {{ 'showingResults' | translate: { from: getPaginationFrom(), to: getPaginationTo(), total: getPaginationTotal() } }} +
+
+ +
+
+ - + - +
{{ column.header }} +
+ {{ column.header }} + +
+
@@ -102,7 +192,11 @@ - {{trace.progress}}% + {{trace.progress}}% + @@ -116,16 +210,16 @@ 'chip-cancelled': trace.status === 'cancelled' }"> {{ - trace.status === 'failed' ? 'Error' : - trace.status === 'in-progress' ? 'En ejecución' : - trace.status === 'success' ? 'Completado' : - trace.status === 'pending' ? 'Pendiente' : - trace.status === 'cancelled' ? 'Cancelado' : + trace.status === 'failed' ? ('failed' | translate) : + trace.status === 'in-progress' ? ('inProgress' | translate) : + trace.status === 'success' ? ('successful' | translate) : + trace.status === 'pending' ? ('pending' | translate) : + trace.status === 'cancelled' ? ('cancelled' | translate) : trace.status }} - @@ -133,29 +227,30 @@ -
- {{ translateCommand(trace.command) }} - {{ trace.jobId }} +
+ {{ translateCommand(trace.command) }} + {{ trace.jobId }}
-
- {{ trace.client?.name }} - {{ trace.client?.ip }} +
+ {{ trace.client?.name }} + {{ trace.client?.ip }}
- -
- {{ trace.executedAt |date: 'dd/MM/yyyy hh:mm:ss'}} +
+ {{ trace.executedAt |date: 'dd/MM/yyyy hh:mm:ss'}} + {{ getRelativeTime(trace.executedAt) }}
-
- {{ trace.finishedAt |date: 'dd/MM/yyyy hh:mm:ss'}} +
+ {{ trace.finishedAt |date: 'dd/MM/yyyy hh:mm:ss'}} + {{ getRelativeTime(trace.finishedAt) }}
@@ -170,26 +265,31 @@
{{ 'informationLabel' | translate }} - - +
+ + + +
diff --git a/ogWebconsole/src/app/components/task-logs/task-logs.component.ts b/ogWebconsole/src/app/components/task-logs/task-logs.component.ts index 18e441b..5386f4d 100644 --- a/ogWebconsole/src/app/components/task-logs/task-logs.component.ts +++ b/ogWebconsole/src/app/components/task-logs/task-logs.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, OnInit, OnDestroy } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { Observable, forkJoin } from 'rxjs'; import { FormControl } from '@angular/forms'; @@ -20,7 +20,7 @@ import { COMMAND_TYPES } from '../../shared/constants/command-types'; templateUrl: './task-logs.component.html', styleUrls: ['./task-logs.component.css'] }) -export class TaskLogsComponent implements OnInit { +export class TaskLogsComponent implements OnInit, OnDestroy { baseUrl: string; mercureUrl: string; traces: any[] = []; @@ -38,6 +38,30 @@ export class TaskLogsComponent implements OnInit { bufferValue = 0; today = new Date(); + showAdvancedFilters: boolean = false; + selectedTrace: any = null; + sortBy: string = 'executedAt'; + sortDirection: 'asc' | 'desc' = 'desc'; + currentSortColumn: string = 'executedAt'; + + totalStats: { + total: number; + success: number; + failed: number; + pending: number; + inProgress: number; + cancelled: number; + today: number; + } = { + total: 0, + success: 0, + failed: 0, + pending: 0, + inProgress: 0, + cancelled: 0, + today: 0 + }; + filteredCommands2 = Object.keys(COMMAND_TYPES).map(key => ({ name: key, value: key, @@ -49,31 +73,37 @@ export class TaskLogsComponent implements OnInit { columnDef: 'id', header: 'ID', cell: (trace: any) => `${trace.id}`, + sortable: true }, { columnDef: 'command', header: 'Comando', - cell: (trace: any) => trace.command + cell: (trace: any) => trace.command, + sortable: true }, { columnDef: 'client', header: 'Cliente', - cell: (trace: any) => trace.client?.name + cell: (trace: any) => trace.client?.name, + sortable: true }, { columnDef: 'status', header: 'Estado', - cell: (trace: any) => trace.status + cell: (trace: any) => trace.status, + sortable: true }, { columnDef: 'executedAt', header: 'Ejecución', cell: (trace: any) => this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy hh:mm:ss'), + sortable: true }, { columnDef: 'finishedAt', header: 'Finalización', cell: (trace: any) => this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy hh:mm:ss'), + sortable: true }, ]; displayedColumns = [...this.columns.map(column => column.columnDef), 'information']; @@ -100,6 +130,7 @@ export class TaskLogsComponent implements OnInit { this.loadTraces(); this.loadCommands(); this.loadClients(); + this.loadTotalStats(); this.filteredCommands = this.commandControl.valueChanges.pipe( startWith(''), map(value => (typeof value === 'string' ? value : value?.name)), @@ -122,6 +153,169 @@ export class TaskLogsComponent implements OnInit { } } + ngOnDestroy(): void { + } + + toggleFilters(): void { + this.showAdvancedFilters = !this.showAdvancedFilters; + } + + refreshData(): void { + this.loadTraces(); + this.toastService.success('Datos actualizados', 'Éxito'); + } + + selectTrace(trace: any): void { + this.selectedTrace = trace; + } + + sortColumn(columnDef: string): void { + if (this.currentSortColumn === columnDef) { + this.sortDirection = this.sortDirection === 'asc' ? 'desc' : 'asc'; + } else { + this.currentSortColumn = columnDef; + this.sortDirection = 'desc'; + } + this.sortBy = columnDef; + this.onSortChange(); + } + + getSortIcon(columnDef: string): string { + if (this.currentSortColumn !== columnDef) { + return 'unfold_more'; + } + return this.sortDirection === 'asc' ? 'expand_less' : 'expand_more'; + } + + onSortChange(): void { + this.loadTraces(); + } + + getStatusCount(status: string): number { + switch(status) { + case 'success': + return this.totalStats.success; + case 'failed': + return this.totalStats.failed; + case 'pending': + return this.totalStats.pending; + case 'in-progress': + return this.totalStats.inProgress; + case 'cancelled': + return this.totalStats.cancelled; + case 'today': + return this.totalStats.today; + default: + return 0; + } + } + + getTodayTracesCount(): number { + const today = new Date(); + const todayString = this.datePipe.transform(today, 'yyyy-MM-dd'); + return this.traces.filter(trace => + trace.executedAt && trace.executedAt.startsWith(todayString) + ).length; + } + + getRelativeTime(date: string): string { + if (!date) return ''; + + const now = new Date(); + const traceDate = new Date(date); + const diffInSeconds = Math.floor((now.getTime() - traceDate.getTime()) / 1000); + + if (diffInSeconds < 60) { + return 'hace un momento'; + } else if (diffInSeconds < 3600) { + const minutes = Math.floor(diffInSeconds / 60); + return `hace ${minutes} minuto${minutes > 1 ? 's' : ''}`; + } else if (diffInSeconds < 86400) { + const hours = Math.floor(diffInSeconds / 3600); + return `hace ${hours} hora${hours > 1 ? 's' : ''}`; + } else { + const days = Math.floor(diffInSeconds / 86400); + return `hace ${days} día${days > 1 ? 's' : ''}`; + } + } + + exportToCSV(): void { + const headers = ['ID', 'Comando', 'Cliente', 'Estado', 'Fecha Ejecución', 'Fecha Finalización', 'Job ID']; + const csvData = this.traces.map(trace => [ + trace.id, + this.translateCommand(trace.command), + trace.client?.name || '', + trace.status, + this.datePipe.transform(trace.executedAt, 'dd/MM/yyyy hh:mm:ss'), + this.datePipe.transform(trace.finishedAt, 'dd/MM/yyyy hh:mm:ss'), + trace.jobId || '' + ]); + + const csvContent = [headers, ...csvData] + .map(row => row.map(cell => `"${cell}"`).join(',')) + .join('\n'); + + const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' }); + const link = document.createElement('a'); + const url = URL.createObjectURL(blob); + link.setAttribute('href', url); + link.setAttribute('download', `traces_${new Date().toISOString().split('T')[0]}.csv`); + link.style.visibility = 'hidden'; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + this.toastService.success('Archivo CSV exportado correctamente', 'Éxito'); + } + + cancelTrace(trace: any): void { + if (trace.status !== 'pending' && trace.status !== 'in-progress') { + this.toastService.warning('Solo se pueden cancelar trazas pendientes o en ejecución', 'Advertencia'); + return; + } + + const dialogRef = this.dialog.open(DeleteModalComponent, { + width: '400px', + data: { + title: 'Cancelar Traza', + message: `¿Estás seguro de que quieres cancelar la traza #${trace.id}?`, + confirmText: 'Cancelar', + cancelText: 'No cancelar' + } + }); + + dialogRef.afterClosed().subscribe(result => { + if (result) { + if (trace.status === 'in-progress') { + this.http.post(`${this.baseUrl}${trace['@id']}/kill-job`, { + jobId: trace.jobId + }).subscribe( + () => { + this.toastService.success('Traza cancelada correctamente', 'Éxito'); + this.loadTraces(); + this.loadTotalStats(); + }, + (error) => { + this.toastService.error('Error al cancelar la traza', 'Error'); + } + ); + } else { + this.http.post(`${this.baseUrl}/traces/cancel-multiple`, {traces: [trace['@id']]}).subscribe( + () => { + this.toastService.success('Traza cancelada correctamente', 'Éxito'); + this.loadTraces(); + this.loadTotalStats(); + }, + (error) => { + console.error('Error cancelling pending trace:', error); + this.toastService.error('Error al cancelar la traza', 'Error'); + } + ); + } + } + }); + } + private updateTracesStatus(clientUuid: string, newStatus: string, progress: Number): void { const traceIndex = this.traces.findIndex(trace => trace['@id'] === clientUuid); if (traceIndex !== -1) { @@ -135,14 +329,9 @@ export class TaskLogsComponent implements OnInit { this.traces = updatedTraces; this.cdr.detectChanges(); - - console.log(`Estado actualizado para la traza ${clientUuid}: ${newStatus}`); - } else { - console.warn(`Traza con UUID ${clientUuid} no encontrado en la lista.`); } } - private _filterClients(value: string): any[] { const filterValue = value.toLowerCase(); @@ -153,7 +342,6 @@ export class TaskLogsComponent implements OnInit { ); } - private _filterCommands(name: string): any[] { const filterValue = name.toLowerCase(); return this.commands.filter(command => command.name.toLowerCase().includes(filterValue)); @@ -193,30 +381,11 @@ export class TaskLogsComponent implements OnInit { }); } - cancelTrace(trace: any): void { - this.dialog.open(DeleteModalComponent, { - width: '300px', - data: { name: trace.jobId }, - }).afterClosed().subscribe((result) => { - if (result) { - this.http.post(`${this.baseUrl}/traces/server/${trace.uuid}/cancel`, {}).subscribe({ - next: () => { - this.toastService.success('Transmision de imagen cancelada'); - this.loadTraces(); - }, - error: (error) => { - this.toastService.error(error.error['hydra:description']); - console.error(error.error['hydra:description']); - } - }); - } - }); - } - loadTraces(): void { this.loading = true; const url = `${this.baseUrl}/traces?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`; const params: any = { ...this.filters }; + if (params['status'] === undefined) { delete params['status']; } @@ -230,12 +399,19 @@ export class TaskLogsComponent implements OnInit { delete params['endDate']; } + if (this.sortBy) { + params['order[' + this.sortBy + ']'] = this.sortDirection; + } + this.http.get(url, { params }).subscribe( (data) => { this.traces = data['hydra:member']; this.length = data['hydra:totalItems']; this.groupedTraces = this.groupByCommandId(this.traces); this.loading = false; + if (Object.keys(this.filters).length === 0) { + this.loadTotalStats(); + } }, (error) => { console.error('Error fetching traces', error); @@ -272,6 +448,52 @@ export class TaskLogsComponent implements OnInit { ); } + loadTotalStats(): void { + this.calculateLocalStats(); + } + + private calculateLocalStats(): void { + const statuses = ['success', 'failed', 'pending', 'in-progress', 'cancelled']; + const requests = statuses.map(status => + this.http.get(`${this.baseUrl}/traces?status=${status}&page=1&itemsPerPage=1`) + ); + + const totalRequest = this.http.get(`${this.baseUrl}/traces?page=1&itemsPerPage=1`); + + const todayString = this.datePipe.transform(new Date(), 'yyyy-MM-dd'); + const todayRequest = this.http.get(`${this.baseUrl}/traces?executedAt[after]=${todayString}&page=1&itemsPerPage=1`); + + forkJoin([totalRequest, ...requests, todayRequest]).subscribe( + (responses) => { + const totalData = responses[0]; + const statusData = responses.slice(1, 6); + const todayData = responses[6]; + + this.totalStats = { + total: totalData['hydra:totalItems'], + success: statusData[0]['hydra:totalItems'], + failed: statusData[1]['hydra:totalItems'], + pending: statusData[2]['hydra:totalItems'], + inProgress: statusData[3]['hydra:totalItems'], + cancelled: statusData[4]['hydra:totalItems'], + today: todayData['hydra:totalItems'] + }; + }, + error => { + console.error('Error fetching stats by status:', error); + const todayString = this.datePipe.transform(new Date(), 'yyyy-MM-dd'); + this.totalStats = { + total: this.length, + success: this.traces.filter(trace => trace.status === 'success').length, + failed: this.traces.filter(trace => trace.status === 'failed').length, + pending: this.traces.filter(trace => trace.status === 'pending').length, + inProgress: this.traces.filter(trace => trace.status === 'in-progress').length, + cancelled: this.traces.filter(trace => trace.status === 'cancelled').length, + today: this.traces.filter(trace => trace.executedAt && trace.executedAt.startsWith(todayString)).length + }; + } + ); + } resetFilters(clientSearchCommandInput: any, clientSearchStatusInput: any, clientSearchClientInput: any) { this.loading = true; @@ -361,4 +583,16 @@ export class TaskLogsComponent implements OnInit { }); } + // Métodos para paginación + getPaginationFrom(): number { + return (this.page * this.itemsPerPage) + 1; + } + + getPaginationTo(): number { + return Math.min((this.page + 1) * this.itemsPerPage, this.length); + } + + getPaginationTotal(): number { + return this.length; + } } diff --git a/ogWebconsole/src/app/shared/modal-overlay/modal-overlay.component.css b/ogWebconsole/src/app/shared/modal-overlay/modal-overlay.component.css new file mode 100644 index 0000000..d666f43 --- /dev/null +++ b/ogWebconsole/src/app/shared/modal-overlay/modal-overlay.component.css @@ -0,0 +1,35 @@ +.custom-icon { + font-size: 48px; + width: 48px; + height: 48px; + color: white; +} + +/* Animaciones adicionales */ +.modal-overlay-blur { + animation: fadeIn 0.3s ease-in-out; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + to { + opacity: 1; + } +} + +.spinner-container { + animation: slideIn 0.3s ease-out; +} + +@keyframes slideIn { + from { + transform: translateY(-20px); + opacity: 0; + } + to { + transform: translateY(0); + opacity: 1; + } +} \ No newline at end of file diff --git a/ogWebconsole/src/app/shared/modal-overlay/modal-overlay.component.html b/ogWebconsole/src/app/shared/modal-overlay/modal-overlay.component.html new file mode 100644 index 0000000..4c5975b --- /dev/null +++ b/ogWebconsole/src/app/shared/modal-overlay/modal-overlay.component.html @@ -0,0 +1,7 @@ + \ No newline at end of file diff --git a/ogWebconsole/src/app/shared/modal-overlay/modal-overlay.component.ts b/ogWebconsole/src/app/shared/modal-overlay/modal-overlay.component.ts new file mode 100644 index 0000000..d855799 --- /dev/null +++ b/ogWebconsole/src/app/shared/modal-overlay/modal-overlay.component.ts @@ -0,0 +1,14 @@ +import { Component, Input } from '@angular/core'; + +@Component({ + selector: 'app-modal-overlay', + templateUrl: './modal-overlay.component.html', + styleUrls: ['./modal-overlay.component.css'] +}) +export class ModalOverlayComponent { + @Input() isVisible: boolean = false; + @Input() message: string = 'Procesando...'; + @Input() variant: 'default' | 'success' | 'warning' | 'error' = 'default'; + @Input() showSpinner: boolean = true; + @Input() customIcon?: string; +} \ No newline at end of file