소스 검색

componente para mostrar el historial de ejecuciones por workflow, restriccion en el form para estado Borrador. Búsqueda funcionando.

EmilianoOrtiz 3 주 전
부모
커밋
9dddaa31ee

+ 2 - 0
src/app/app.module.ts

@@ -437,6 +437,7 @@ import { ViewGraphicWorkflowComponent } from './components/process-management/wo
 import { WorkflowDetailsComponent } from './components/process-management/workflow-management/workflow-details/workflow-details.component';
 import { ChangeStatusWorkflowComponent } from './components/process-management/workflow-management/change-status-workflow/change-status-workflow.component';
 import { WorkflowHistoryComponent } from './components/process-management/workflow-management/workflow-history/workflow-history.component';
+import { WorkflowExecutionHistoryComponent } from './components/process-management/workflow-management/workflow-execution-history/workflow-execution-history.component';
 import { SelectColorComponent } from './components/system-admin/system-params/select-color/select-color.component';
 import { GridIconsSelectorComponent } from './components/system-admin/system-params/grid-icons-selector/grid-icons-selector.component';
 import { OrganizationComponent } from './components/personal-management/organization/organization.component';
@@ -844,6 +845,7 @@ import { SignaturePadModule } from 'angular-signature-pad-v2';
     WorkflowDetailsComponent,
     ChangeStatusWorkflowComponent,
     WorkflowHistoryComponent,
+    WorkflowExecutionHistoryComponent,
     SelectColorComponent,
     GridIconsSelectorComponent,
     TableManagementFormComponent,

+ 5 - 5
src/app/components/process-management/multicriteria-searches/multicriteria-searches.component.html

@@ -170,9 +170,9 @@
             </div>
             <div class="container_table">
               <table mat-table matSort [dataSource]="dataSourceWorkflow" class="animated fadeIn" [style.display]="isLoadingWorkflow ? 'none' : 'revert'">
-                <ng-container matColumnDef="ID_WORKFLOW">
-                  <th mat-header-cell *matHeaderCellDef mat-sort-header> ID </th>
-                  <td mat-cell *matCellDef="let element"> {{ element.ID_WORKFLOW }} </td>
+                <ng-container matColumnDef="CONTADOR">
+                  <th mat-header-cell *matHeaderCellDef> No </th>
+                  <td mat-cell *matCellDef="let element; let i = index"> {{ i + 1 }} </td>
                 </ng-container>
                 <ng-container matColumnDef="NOMBRE_WORKFLOW">
                   <th mat-header-cell *matHeaderCellDef mat-sort-header> {{ interService.get('nombre_workflow') }} </th>
@@ -212,9 +212,9 @@
                     color="primary"
                     class="override_no_shadow ml-4" 
                     (click)="viewWorkflowHistory(element)"
-                    [matTooltip]="interService.get('ver_historial')"  
+                    [matTooltip]="'Ver historial de ejecución'"  
                     [disabled]="isLoadingForm">
-                      <mat-icon>history</mat-icon>
+                      <mat-icon>timeline</mat-icon>
                     </button>
                   </td>
                 </ng-container>

+ 41 - 6
src/app/components/process-management/multicriteria-searches/multicriteria-searches.component.ts

@@ -4,7 +4,10 @@ import {
   FilterNotificate,
   FilterRequest,
   FilterWorkflow,
+  ResponseDataWorkflowExecutionHistory,
 } from '../../../interfaces/process-managementv/workflow-management.interface';
+import { WorkflowExecutionHistoryComponent } from '../workflow-management/workflow-execution-history/workflow-execution-history.component';
+import { HttpErrorResponse } from '@angular/common/http';
 import { MatPaginator } from '@angular/material/paginator';
 import { MatSort } from '@angular/material/sort';
 import { ResourcesService } from '../../../services/resources.service';
@@ -15,6 +18,8 @@ import { Router } from '@angular/router';
 import { SearchRequestComponent } from './search-request/search-request.component';
 import { SearchNotificateComponent } from './search-notificate/search-notificate.component';
 import { SearchWorkflowComponent } from './search-workflow/search-workflow.component';
+import { ProcessManagementService } from '../../../services/process-management/process-management.service';
+import { lastValueFrom } from 'rxjs';
 
 @Component({
   selector: 'app-multicriteria-searches',
@@ -48,7 +53,8 @@ export class MulticriteriaSearchesComponent implements AfterViewInit {
     public interService: InternationalizationService,
     private _dialog: MatDialog,
     private _encService: EncService,
-    private _router: Router
+    private _router: Router,
+    private _processManagementService: ProcessManagementService
   ) {
     this.isLoadingRequest = false;
     this.isLoadingNotificate = false;
@@ -75,7 +81,7 @@ export class MulticriteriaSearchesComponent implements AfterViewInit {
 
     this.dataSourceWorkflow = new MatTableDataSource<FilterWorkflow>();
     this.displayedColumnsWorkflow = [
-      'ID_WORKFLOW',
+      'CONTADOR',
       'NOMBRE_WORKFLOW',
       'DESC_WORKFLOW',
       'ESTADO_WORKFLOW',
@@ -116,8 +122,6 @@ export class MulticriteriaSearchesComponent implements AfterViewInit {
       });
   }
 
-
-
   public openSearchNotificate() {
     this._dialog
       .open(SearchNotificateComponent, {
@@ -156,7 +160,38 @@ export class MulticriteriaSearchesComponent implements AfterViewInit {
       });
   }
 
-  public viewWorkflowHistory(filterWorkflow: FilterWorkflow) {
-    console.log('Ver historial del workflow ID:', filterWorkflow.ID_WORKFLOW);
+  public async viewWorkflowHistory(filterWorkflow: FilterWorkflow) {
+    this.isLoadingForm = true;
+    const user = this.resourcesService.getUser();
+    const line = this.resourcesService.getLineNumber();
+
+    await lastValueFrom(
+      this._processManagementService.getWorkflowExecutionsHistory(
+        filterWorkflow.ID_WORKFLOW.toString(),
+        user,
+        line
+      )
+    ).then(
+      (responseData: ResponseDataWorkflowExecutionHistory) => {
+        if (!responseData.error) {
+          this._dialog.open(WorkflowExecutionHistoryComponent, {
+            disableClose: true,
+            width: '900px',
+            maxWidth: '90vw',
+            maxHeight: '90vh',
+            data: { historyData: responseData.response },
+          });
+        } else {
+          this.resourcesService.openSnackBar(`${responseData.msg}`);
+        }
+      },
+      async (httpErrorResponse: HttpErrorResponse) => {
+        let response = this.resourcesService.checkErrors(httpErrorResponse);
+        if (response !== null && response.reload) {
+          await this.viewWorkflowHistory(filterWorkflow);
+        }
+      }
+    );
+    this.isLoadingForm = false;
   }
 }

+ 8 - 1
src/app/components/process-management/multicriteria-searches/search-workflow/search-workflow.component.css

@@ -1 +1,8 @@
-/* Estilos específicos para search-workflow */
+.disabled-field {
+  opacity: 0.6;
+  pointer-events: none;
+}
+
+.disabled-field .mat-mdc-form-field-label {
+  color: rgba(0, 0, 0, 0.38) !important;
+}

+ 8 - 8
src/app/components/process-management/multicriteria-searches/search-workflow/search-workflow.component.html

@@ -38,20 +38,20 @@
         </mat-form-field>
       </div>
       <div class="col-flex-12">
-        <mat-form-field appearance="outline" class="w-100">
+        <mat-form-field appearance="outline" class="w-100" [class.disabled-field]="isExecutionFieldsDisabled">
           <mat-label>{{ interService.get('rango_ejecucion') }}</mat-label>
-          <mat-date-range-input [rangePicker]="pickerEjecucion">
-            <input matStartDate formControlName="EJECUCION_INICIO" [placeholder]="interService.get('fecha_inicial')" readonly>
-            <input matEndDate formControlName="EJECUCION_FIN" [placeholder]="interService.get('fecha_final')" readonly>
+          <mat-date-range-input [rangePicker]="pickerEjecucion" [disabled]="isExecutionFieldsDisabled">
+            <input matStartDate formControlName="EJECUCION_INICIO" [placeholder]="interService.get('fecha_inicial')" readonly [disabled]="isExecutionFieldsDisabled">
+            <input matEndDate formControlName="EJECUCION_FIN" [placeholder]="interService.get('fecha_final')" readonly [disabled]="isExecutionFieldsDisabled">
           </mat-date-range-input>
-          <mat-datepicker-toggle matIconSuffix [for]="pickerEjecucion"></mat-datepicker-toggle>
-          <mat-date-range-picker #pickerEjecucion></mat-date-range-picker>
+          <mat-datepicker-toggle matIconSuffix [for]="pickerEjecucion" [disabled]="isExecutionFieldsDisabled"></mat-datepicker-toggle>
+          <mat-date-range-picker #pickerEjecucion [disabled]="isExecutionFieldsDisabled"></mat-date-range-picker>
         </mat-form-field>
       </div>
       <div class="col-flex-12">
-        <mat-form-field appearance="outline" class="w-100">
+        <mat-form-field appearance="outline" class="w-100" [class.disabled-field]="isExecutionFieldsDisabled">
           <mat-label>{{ interService.get('estado_ejecucion') }}</mat-label>
-          <mat-select formControlName="ESTADO_EJECUCION">
+          <mat-select formControlName="ESTADO_EJECUCION" [disabled]="isExecutionFieldsDisabled">
             @for (estado of estadosEjecucion; track estado) {
               <mat-option [value]="estado">{{ estado }}</mat-option>
             }

+ 4 - 0
src/app/components/process-management/multicriteria-searches/search-workflow/search-workflow.component.ts

@@ -56,6 +56,10 @@ export class SearchWorkflowComponent implements OnInit {
     this.isLoading = false;
   }
 
+  public get isExecutionFieldsDisabled(): boolean {
+    return this.formGroup.get('ESTADO')?.value === 'Borrador';
+  }
+
   private async _getUsers() {
     const user: string = this.resourcesService.getUser();
     const line: number = this.resourcesService.getLineNumber();

+ 231 - 0
src/app/components/process-management/workflow-management/workflow-execution-history/workflow-execution-history.component.css

@@ -0,0 +1,231 @@
+.execution-history-container {
+  max-height: 70vh;
+  overflow-y: auto;
+  padding: 16px;
+}
+
+.executions-list {
+  display: flex;
+  flex-direction: column;
+  gap: 16px;
+}
+
+.execution-card {
+  border: 1px solid #e0e0e0;
+  border-radius: 8px;
+  padding: 16px;
+  background: #fafafa;
+  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
+}
+
+.execution-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: flex-start;
+  margin-bottom: 16px;
+  padding-bottom: 12px;
+  border-bottom: 1px solid #e0e0e0;
+}
+
+.execution-info h3 {
+  margin: 0 0 8px 0;
+  color: #333;
+  font-size: 18px;
+}
+
+.execution-meta {
+  display: flex;
+  gap: 8px;
+  align-items: center;
+  flex-wrap: wrap;
+}
+
+.object-badge {
+  background: #e3f2fd;
+  color: #1976d2;
+  padding: 4px 8px;
+  border-radius: 12px;
+  font-size: 12px;
+  font-weight: 500;
+}
+
+.status-badge {
+  padding: 4px 8px;
+  border-radius: 12px;
+  font-size: 12px;
+  font-weight: 500;
+}
+
+.status-completed {
+  background: #e8f5e8;
+  color: #2e7d32;
+}
+
+.status-in-progress {
+  background: #fff3e0;
+  color: #f57c00;
+}
+
+.status-interrupted {
+  background: #ffebee;
+  color: #d32f2f;
+}
+
+.status-default {
+  background: #f5f5f5;
+  color: #666;
+}
+
+.execution-dates {
+  text-align: right;
+  font-size: 14px;
+  min-width: 200px;
+}
+
+.date-info, .duration {
+  margin-bottom: 4px;
+  color: #666;
+}
+
+.tasks-timeline {
+  display: flex;
+  flex-direction: column;
+  gap: 8px;
+}
+
+.task-item {
+  display: flex;
+  align-items: flex-start;
+  gap: 12px;
+  padding: 12px;
+  background: white;
+  border-radius: 6px;
+  border-left: 3px solid #2196f3;
+  transition: all 0.2s ease;
+}
+
+.task-item:hover {
+  box-shadow: 0 2px 8px rgba(0,0,0,0.1);
+  transform: translateY(-1px);
+}
+
+.task-sequence {
+  background: #2196f3;
+  color: white;
+  width: 28px;
+  height: 28px;
+  border-radius: 50%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  font-size: 12px;
+  font-weight: bold;
+  flex-shrink: 0;
+}
+
+.task-content {
+  flex: 1;
+}
+
+.task-name {
+  font-weight: 500;
+  margin-bottom: 6px;
+  color: #333;
+  font-size: 14px;
+}
+
+.task-meta {
+  display: flex;
+  gap: 12px;
+  align-items: center;
+  font-size: 12px;
+  color: #666;
+  flex-wrap: wrap;
+}
+
+.task-status {
+  padding: 2px 6px;
+  border-radius: 8px;
+  font-weight: 500;
+}
+
+.task-user {
+  background: #f5f5f5;
+  padding: 2px 6px;
+  border-radius: 4px;
+}
+
+.task-date {
+  font-style: italic;
+}
+
+.no-data {
+  text-align: center;
+  padding: 40px;
+  color: #666;
+}
+
+.no-data p {
+  font-size: 16px;
+  margin: 0;
+}
+
+/* Responsive design */
+@media (max-width: 768px) {
+  .execution-header {
+    flex-direction: column;
+    gap: 12px;
+  }
+  
+  .execution-dates {
+    text-align: left;
+    min-width: auto;
+  }
+  
+  .task-meta {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 4px;
+  }
+  
+  .execution-meta {
+    flex-direction: column;
+    align-items: flex-start;
+    gap: 4px;
+  }
+}
+
+/* Animaciones */
+.execution-card {
+  animation: fadeInUp 0.3s ease-out;
+}
+
+@keyframes fadeInUp {
+  from {
+    opacity: 0;
+    transform: translateY(20px);
+  }
+  to {
+    opacity: 1;
+    transform: translateY(0);
+  }
+}
+
+/* Scrollbar personalizado */
+.execution-history-container::-webkit-scrollbar {
+  width: 6px;
+}
+
+.execution-history-container::-webkit-scrollbar-track {
+  background: #f1f1f1;
+  border-radius: 3px;
+}
+
+.execution-history-container::-webkit-scrollbar-thumb {
+  background: #c1c1c1;
+  border-radius: 3px;
+}
+
+.execution-history-container::-webkit-scrollbar-thumb:hover {
+  background: #a8a8a8;
+}

+ 56 - 0
src/app/components/process-management/workflow-management/workflow-execution-history/workflow-execution-history.component.html

@@ -0,0 +1,56 @@
+<div mat-dialog-content class="execution-history-container">
+  <h2 mat-dialog-title>Historial de Ejecución - {{ workflowName }}</h2>
+  
+  <div class="executions-list" *ngIf="groupedExecutions.length > 0; else noData">
+    <div class="execution-card" *ngFor="let execution of groupedExecutions">
+      <div class="execution-header">
+        <div class="execution-info">
+          <h3>Ejecución #{{ execution.ID_EJECUCION_WORKFLOW }}</h3>
+          <div class="execution-meta">
+            <span class="object-badge">Objeto: {{ execution.OBJETO }}</span>
+            <span class="status-badge" [ngClass]="getStatusClass(execution.ESTADO_WORKFLOW)">
+              {{ execution.ESTADO_WORKFLOW }}
+            </span>
+          </div>
+        </div>
+        <div class="execution-dates">
+          <div class="date-info">
+            <strong>Inicio:</strong> {{ execution.FECHA_INICIO | date:'dd/MM/yyyy HH:mm' }}
+          </div>
+          <div class="date-info" *ngIf="execution.FECHA_COMPLETADO">
+            <strong>Fin:</strong> {{ execution.FECHA_COMPLETADO | date:'dd/MM/yyyy HH:mm' }}
+          </div>
+          <div class="duration">
+            <strong>Duración:</strong> {{ getDuration(execution.FECHA_INICIO, execution.FECHA_COMPLETADO) }}
+          </div>
+        </div>
+      </div>
+      
+      <div class="tasks-timeline">
+        <div class="task-item" *ngFor="let tarea of execution.TAREAS">
+          <div class="task-sequence">{{ tarea.SECUENCIA }}</div>
+          <div class="task-content">
+            <div class="task-name">{{ tarea.NOMBRE_TAREA }}</div>
+            <div class="task-meta">
+              <span class="task-status" [ngClass]="getStatusClass(tarea.ESTADO_TAREA)">
+                {{ tarea.ESTADO_TAREA }}
+              </span>
+              <span class="task-user">Usuario: {{ tarea.USUARIO }}</span>
+              <span class="task-date">{{ tarea.FECHA_EJECUCION | date:'dd/MM/yyyy HH:mm' }}</span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+
+  <ng-template #noData>
+    <div class="no-data">
+      <p>No hay historial de ejecución disponible</p>
+    </div>
+  </ng-template>
+</div>
+
+<div mat-dialog-actions align="end">
+  <button mat-button (click)="close()">Cerrar</button>
+</div>

+ 92 - 0
src/app/components/process-management/workflow-management/workflow-execution-history/workflow-execution-history.component.ts

@@ -0,0 +1,92 @@
+import { Component, Inject, OnInit } from '@angular/core';
+import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
+import { WorkflowExecutionHistory } from '../../../../interfaces/process-managementv/workflow-management.interface';
+
+interface GroupedExecution {
+  ID_EJECUCION_WORKFLOW: number;
+  OBJETO: string;
+  ESTADO_WORKFLOW: string;
+  FECHA_INICIO: string;
+  FECHA_COMPLETADO: string | null;
+  TAREAS: WorkflowExecutionHistory[];
+}
+
+@Component({
+  selector: 'app-workflow-execution-history',
+  templateUrl: './workflow-execution-history.component.html',
+  styleUrls: ['./workflow-execution-history.component.css'],
+  standalone: false
+})
+export class WorkflowExecutionHistoryComponent implements OnInit {
+  public groupedExecutions: GroupedExecution[] = [];
+  public workflowName: string = '';
+
+  constructor(
+    public dialogRef: MatDialogRef<WorkflowExecutionHistoryComponent>,
+    @Inject(MAT_DIALOG_DATA) public data: { historyData: WorkflowExecutionHistory[] }
+  ) {}
+
+  ngOnInit() {
+    this.processHistoryData();
+  }
+
+  private processHistoryData() {
+    if (!this.data.historyData || this.data.historyData.length === 0) return;
+
+    this.workflowName = this.data.historyData[0].NOMBRE_WORKFLOW;
+    
+    const executionsMap = new Map<number, GroupedExecution>();
+
+    this.data.historyData.forEach(item => {
+      if (!executionsMap.has(item.ID_EJECUCION_WORKFLOW)) {
+        executionsMap.set(item.ID_EJECUCION_WORKFLOW, {
+          ID_EJECUCION_WORKFLOW: item.ID_EJECUCION_WORKFLOW,
+          OBJETO: item.OBJETO,
+          ESTADO_WORKFLOW: item.ESTADO_WORKFLOW,
+          FECHA_INICIO: item.FECHA_INICIO,
+          FECHA_COMPLETADO: item.FECHA_COMPLETADO,
+          TAREAS: []
+        });
+      }
+      executionsMap.get(item.ID_EJECUCION_WORKFLOW)!.TAREAS.push(item);
+    });
+
+    this.groupedExecutions = Array.from(executionsMap.values())
+      .sort((a, b) => new Date(b.FECHA_INICIO).getTime() - new Date(a.FECHA_INICIO).getTime());
+
+    this.groupedExecutions.forEach(execution => {
+      execution.TAREAS.sort((a, b) => a.SECUENCIA - b.SECUENCIA);
+    });
+  }
+
+  public getStatusClass(estado: string): string {
+    switch (estado) {
+      case 'Completada':
+      case 'Finalizada':
+        return 'status-completed';
+      case 'En proceso':
+        return 'status-in-progress';
+      case 'Interrumpida':
+        return 'status-interrupted';
+      default:
+        return 'status-default';
+    }
+  }
+
+  public getDuration(fechaInicio: string, fechaCompletado: string | null): string {
+    if (!fechaCompletado) return 'En curso';
+    
+    const inicio = new Date(fechaInicio);
+    const fin = new Date(fechaCompletado);
+    const diff = fin.getTime() - inicio.getTime();
+    
+    const hours = Math.floor(diff / (1000 * 60 * 60));
+    const minutes = Math.floor((diff % (1000 * 60 * 60)) / (1000 * 60));
+    
+    return `${hours}h ${minutes}m`;
+  }
+
+  public close() {
+    this.dialogRef.close();
+  }
+}

+ 22 - 0
src/app/interfaces/process-managementv/workflow-management.interface.ts

@@ -381,3 +381,25 @@ export interface ResponseDataValidateApplications {
   msg: string;
   response: ValidateApplication[];
 }
+
+export interface WorkflowExecutionHistory {
+  ID_WORKFLOW: number;
+  NOMBRE_WORKFLOW: string;
+  ID_EJECUCION_WORKFLOW: number;
+  OBJETO: string;
+  ESTADO_WORKFLOW: string;
+  FECHA_INICIO: string;
+  FECHA_COMPLETADO: string | null;
+  ID_EJECUCION_TAREA: number;
+  NOMBRE_TAREA: string;
+  SECUENCIA: number;
+  ESTADO_TAREA: string;
+  USUARIO: string;
+  FECHA_EJECUCION: string;
+}
+
+export interface ResponseDataWorkflowExecutionHistory {
+  error: boolean;
+  msg: string;
+  response: WorkflowExecutionHistory[];
+}

+ 11 - 0
src/app/services/process-management/process-management.service.ts

@@ -27,6 +27,7 @@ import {
   ResponseDataTask,
   ResponseDataValidateApplications,
   ResponseDataWorkflow,
+  ResponseDataWorkflowExecutionHistory,
 } from '../../interfaces/process-managementv/workflow-management.interface';
 import { ResponseData } from '../resources.service';
 import { RequestUserNumberLine } from '../../interfaces/response-data';
@@ -315,4 +316,14 @@ export class ProcessManagementService {
       .postQuery(`${this._url}/search-workflows`, requestWorkflowSearch)
       .pipe(map((data: any) => data));
   }
+
+  public getWorkflowExecutionsHistory(
+    idWorkflow: string,
+    user: string,
+    line: number
+  ): Observable<ResponseDataWorkflowExecutionHistory> {
+    return this._httpRequestService
+      .getQuery(`${this._url}/get-workflow-executions-history/${idWorkflow}/${user}/${line}`)
+      .pipe(map((data: any) => data));
+  }
 }