testes/Modulos Angular/projects/idt_app/docs/router/FALLBACK_IMPLEMENTATION_GUI...

18 KiB

🔄 Implementação de Fallback com Dados Mockados

Sistema de Contingência para o Módulo de Rotas


🎯 Objetivo

Implementar um sistema que utiliza os dados mockados automaticamente quando houver falha na requisição do backend, garantindo que o sistema continue operacional mesmo com problemas de conectividade.


🏗️ Arquitetura da Solução

1. Service com Fallback Automático

// routes.service.ts
import { Injectable } from '@angular/core';
import { HttpClient, HttpErrorResponse } from '@angular/common/http';
import { Observable, of, throwError } from 'rxjs';
import { catchError, map } from 'rxjs/operators';
import { environment } from '../../environments/environment';
import mockData from '../../../docs/router/ROUTES_MOCK_DATA_COMPLETE.json';

export interface Route {
  id: string;
  routeNumber: string;
  type: 'firstMile' | 'lineHaul' | 'lastMile';
  modal: 'rodoviario' | 'aereo' | 'aquaviario';
  priority: 'normal' | 'express' | 'urgent';
  driverId: string;
  vehicleId: string;
  companyId: string;
  customerId: string;
  origin: {
    address: string;
    coordinates: { lat: number; lng: number };
    contact: string;
    phone: string;
  };
  destination: {
    address: string;
    coordinates: { lat: number; lng: number };
    contact: string;
    phone: string;
  };
  scheduledDeparture: string;
  actualDeparture?: string;
  estimatedArrival: string;
  actualArrival?: string;
  status: 'pending' | 'inProgress' | 'completed' | 'delayed' | 'cancelled';
  currentLocation?: { lat: number; lng: number };
  contractId: string;
  tablePricesId: string;
  totalValue: number;
  totalWeight: number;
  estimatedCost: number;
  actualCost?: number;
  productType: string;
  createdAt: string;
  updatedAt: string;
  createdBy: string;
  vehiclePlate: string;
}

export interface RouteFilters {
  page?: number;
  limit?: number;
  type?: string[];
  status?: string[];
  driverId?: string;
  vehicleId?: string;
  dateRange?: { start: string; end: string };
  search?: string;
}

export interface RouteResponse {
  data: Route[];
  pagination: {
    total: number;
    page: number;
    limit: number;
    totalPages: number;
  };
  source: 'backend' | 'fallback';
  timestamp: string;
}

@Injectable({
  providedIn: 'root'
})
export class RoutesService {
  private readonly apiUrl = `${environment.apiUrl}/routes`;
  private readonly mockRoutes: Route[] = mockData.routes as Route[];
  
  constructor(private http: HttpClient) {}

  /**
   * Busca rotas com fallback automático para dados mockados
   */
  getRoutes(filters: RouteFilters = {}): Observable<RouteResponse> {
    return this.http.get<RouteResponse>(`${this.apiUrl}`, { params: this.buildParams(filters) })
      .pipe(
        map(response => ({
          ...response,
          source: 'backend' as const,
          timestamp: new Date().toISOString()
        })),
        catchError((error: HttpErrorResponse) => {
          console.warn('⚠️ Backend indisponível, usando dados mockados:', error.message);
          return this.getFallbackRoutes(filters);
        })
      );
  }

  /**
   * Busca uma rota específica por ID com fallback
   */
  getRouteById(id: string): Observable<Route | null> {
    return this.http.get<Route>(`${this.apiUrl}/${id}`)
      .pipe(
        catchError((error: HttpErrorResponse) => {
          console.warn('⚠️ Backend indisponível, buscando rota mockada:', error.message);
          const mockRoute = this.mockRoutes.find(route => route.id === id);
          return of(mockRoute || null);
        })
      );
  }

  /**
   * Cria nova rota (apenas backend, sem fallback)
   */
  createRoute(route: Partial<Route>): Observable<Route> {
    return this.http.post<Route>(`${this.apiUrl}`, route)
      .pipe(
        catchError((error: HttpErrorResponse) => {
          console.error('❌ Erro ao criar rota - backend necessário:', error.message);
          return throwError(() => new Error('Backend indisponível para criação de rotas'));
        })
      );
  }

  /**
   * Atualiza rota (apenas backend, sem fallback)
   */
  updateRoute(id: string, route: Partial<Route>): Observable<Route> {
    return this.http.put<Route>(`${this.apiUrl}/${id}`, route)
      .pipe(
        catchError((error: HttpErrorResponse) => {
          console.error('❌ Erro ao atualizar rota - backend necessário:', error.message);
          return throwError(() => new Error('Backend indisponível para atualização de rotas'));
        })
      );
  }

  /**
   * Deleta rota (apenas backend, sem fallback)
   */
  deleteRoute(id: string): Observable<void> {
    return this.http.delete<void>(`${this.apiUrl}/${id}`)
      .pipe(
        catchError((error: HttpErrorResponse) => {
          console.error('❌ Erro ao deletar rota - backend necessário:', error.message);
          return throwError(() => new Error('Backend indisponível para exclusão de rotas'));
        })
      );
  }

  /**
   * Verifica status de conectividade com o backend
   */
  checkBackendHealth(): Observable<boolean> {
    return this.http.get(`${environment.apiUrl}/health`, { 
      responseType: 'text',
      timeout: 5000 
    })
      .pipe(
        map(() => true),
        catchError(() => of(false))
      );
  }

  /**
   * Retorna dados mockados com filtros aplicados
   */
  private getFallbackRoutes(filters: RouteFilters): Observable<RouteResponse> {
    let filteredRoutes = [...this.mockRoutes];

    // Aplicar filtros
    if (filters.type?.length) {
      filteredRoutes = filteredRoutes.filter(route => 
        filters.type!.includes(route.type)
      );
    }

    if (filters.status?.length) {
      filteredRoutes = filteredRoutes.filter(route => 
        filters.status!.includes(route.status)
      );
    }

    if (filters.driverId) {
      filteredRoutes = filteredRoutes.filter(route => 
        route.driverId === filters.driverId
      );
    }

    if (filters.vehicleId) {
      filteredRoutes = filteredRoutes.filter(route => 
        route.vehicleId === filters.vehicleId
      );
    }

    if (filters.search) {
      const searchLower = filters.search.toLowerCase();
      filteredRoutes = filteredRoutes.filter(route => 
        route.routeNumber.toLowerCase().includes(searchLower) ||
        route.origin.address.toLowerCase().includes(searchLower) ||
        route.destination.address.toLowerCase().includes(searchLower) ||
        route.vehiclePlate.toLowerCase().includes(searchLower)
      );
    }

    if (filters.dateRange) {
      const startDate = new Date(filters.dateRange.start);
      const endDate = new Date(filters.dateRange.end);
      
      filteredRoutes = filteredRoutes.filter(route => {
        const routeDate = new Date(route.scheduledDeparture);
        return routeDate >= startDate && routeDate <= endDate;
      });
    }

    // Paginação
    const page = filters.page || 1;
    const limit = filters.limit || 50;
    const startIndex = (page - 1) * limit;
    const endIndex = startIndex + limit;
    const paginatedRoutes = filteredRoutes.slice(startIndex, endIndex);

    const response: RouteResponse = {
      data: paginatedRoutes,
      pagination: {
        total: filteredRoutes.length,
        page,
        limit,
        totalPages: Math.ceil(filteredRoutes.length / limit)
      },
      source: 'fallback',
      timestamp: new Date().toISOString()
    };

    return of(response);
  }

  /**
   * Constrói parâmetros da query para requisições HTTP
   */
  private buildParams(filters: RouteFilters): any {
    const params: any = {};

    if (filters.page) params.page = filters.page.toString();
    if (filters.limit) params.limit = filters.limit.toString();
    if (filters.type?.length) params.type = filters.type.join(',');
    if (filters.status?.length) params.status = filters.status.join(',');
    if (filters.driverId) params.driverId = filters.driverId;
    if (filters.vehicleId) params.vehicleId = filters.vehicleId;
    if (filters.search) params.search = filters.search;
    if (filters.dateRange) {
      params.startDate = filters.dateRange.start;
      params.endDate = filters.dateRange.end;
    }

    return params;
  }
}

2. Component com Indicação de Fonte dos Dados

// routes.component.ts
import { Component, OnInit, ChangeDetectorRef } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BaseDomainComponent } from '../../shared/components/base-domain/base-domain.component';
import { RoutesService, Route, RouteFilters, RouteResponse } from './routes.service';
import { TitleService } from '../../shared/services/title.service';
import { HeaderActionsService } from '../../shared/services/header-actions.service';
import { TabSystemComponent } from '../../shared/components/tab-system/tab-system.component';

@Component({
  selector: 'app-routes',
  standalone: true,
  imports: [CommonModule, TabSystemComponent],
  templateUrl: './routes.component.html',
  styleUrl: './routes.component.scss'
})
export class RoutesComponent extends BaseDomainComponent<Route> implements OnInit {
  isBackendAvailable = true;
  dataSource: 'backend' | 'fallback' = 'backend';
  lastUpdateTime = '';
  connectionCheckInterval: any;

  constructor(
    private routesService: RoutesService,
    titleService: TitleService,
    headerActionsService: HeaderActionsService,
    cdr: ChangeDetectorRef
  ) {
    super(titleService, headerActionsService, cdr, new RoutesServiceAdapter(routesService));
  }

  ngOnInit() {
    super.ngOnInit();
    this.startConnectionMonitoring();
    this.loadRoutes();
  }

  ngOnDestroy() {
    if (this.connectionCheckInterval) {
      clearInterval(this.connectionCheckInterval);
    }
  }

  protected override getDomainConfig() {
    return {
      domain: 'routes',
      title: 'Rotas',
      entityName: 'rota',
      subTabs: ['dados', 'localizacao', 'paradas', 'custos', 'documentos', 'historico'],
      columns: [
        { field: "routeNumber", header: "Número", sortable: true, filterable: true },
        { field: "type", header: "Tipo", sortable: true, filterable: true },
        { field: "status", header: "Status", sortable: true, filterable: true },
        { field: "origin.address", header: "Origem", sortable: false, filterable: true },
        { field: "destination.address", header: "Destino", sortable: false, filterable: true },
        { field: "scheduledDeparture", header: "Partida", sortable: true, filterable: false },
        { field: "vehiclePlate", header: "Veículo", sortable: true, filterable: true },
        { field: "totalValue", header: "Valor", sortable: true, filterable: false },
        { field: "productType", header: "Produto", sortable: true, filterable: true }
      ]
    };
  }

  private loadRoutes(filters: RouteFilters = {}) {
    this.routesService.getRoutes(filters).subscribe({
      next: (response: RouteResponse) => {
        this.dataSource = response.source;
        this.lastUpdateTime = response.timestamp;
        this.isBackendAvailable = response.source === 'backend';
        
        // Atualizar dados do componente
        this.items = response.data;
        this.totalItems = response.pagination.total;
        
        this.cdr.detectChanges();
      },
      error: (error) => {
        console.error('Erro ao carregar rotas:', error);
        this.isBackendAvailable = false;
        this.dataSource = 'fallback';
      }
    });
  }

  private startConnectionMonitoring() {
    // Verificar conectividade a cada 30 segundos
    this.connectionCheckInterval = setInterval(() => {
      this.routesService.checkBackendHealth().subscribe(isHealthy => {
        if (isHealthy !== this.isBackendAvailable) {
          this.isBackendAvailable = isHealthy;
          console.log(`🔄 Status do backend alterado: ${isHealthy ? 'Online' : 'Offline'}`);
          
          // Recarregar dados se backend voltar online
          if (isHealthy && this.dataSource === 'fallback') {
            this.loadRoutes();
          }
        }
      });
    }, 30000);
  }

  /**
   * Força tentativa de reconexão com backend
   */
  retryBackendConnection() {
    console.log('🔄 Tentando reconectar com backend...');
    this.loadRoutes();
  }

  /**
   * Obtém classe CSS para indicador de status
   */
  getStatusIndicatorClass(): string {
    return this.isBackendAvailable ? 'status-online' : 'status-offline';
  }

  /**
   * Obtém texto para indicador de status
   */
  getStatusText(): string {
    if (this.isBackendAvailable) {
      return 'Sistema Online';
    }
    return 'Modo Offline - Dados Locais';
  }
}

// Adapter para integração com BaseDomainComponent
class RoutesServiceAdapter {
  constructor(private routesService: RoutesService) {}

  getAll() {
    return this.routesService.getRoutes();
  }

  getById(id: string) {
    return this.routesService.getRouteById(id);
  }

  create(item: Partial<Route>) {
    return this.routesService.createRoute(item);
  }

  update(id: string, item: Partial<Route>) {
    return this.routesService.updateRoute(id, item);
  }

  delete(id: string) {
    return this.routesService.deleteRoute(id);
  }
}

3. Template com Indicador Visual

<!-- routes.component.html -->
<div class="routes-container">
  
  <!-- Indicador de Status da Conexão -->
  <div class="connection-status" [ngClass]="getStatusIndicatorClass()">
    <div class="status-indicator">
      <i class="fas" [ngClass]="{
        'fa-wifi text-success': isBackendAvailable,
        'fa-wifi-slash text-warning': !isBackendAvailable
      }"></i>
      <span class="status-text">{{ getStatusText() }}</span>
      
      <!-- Botão de retry quando offline -->
      <button 
        *ngIf="!isBackendAvailable" 
        class="btn btn-sm btn-outline-primary ms-2"
        (click)="retryBackendConnection()"
        title="Tentar reconectar">
        <i class="fas fa-sync-alt"></i>
        Reconectar
      </button>
    </div>
    
    <!-- Informações adicionais -->
    <div class="status-details">
      <small class="text-muted">
        Fonte: {{ dataSource === 'backend' ? 'Servidor' : 'Cache Local' }} | 
        Última atualização: {{ lastUpdateTime | date:'dd/MM/yyyy HH:mm:ss' }}
      </small>
    </div>
  </div>

  <!-- Alerta quando usando dados mockados -->
  <div *ngIf="dataSource === 'fallback'" class="alert alert-warning alert-dismissible fade show" role="alert">
    <i class="fas fa-exclamation-triangle me-2"></i>
    <strong>Modo Offline:</strong> 
    Exibindo dados locais. Algumas funcionalidades podem estar limitadas.
    <small class="d-block mt-1">
      Operações de criação, edição e exclusão requerem conexão com o servidor.
    </small>
  </div>

  <!-- Componente principal de rotas -->
  <app-tab-system 
    [config]="getDomainConfig()"
    [items]="items"
    [loading]="loading">
  </app-tab-system>

</div>

4. Estilos CSS

// routes.component.scss
.routes-container {
  .connection-status {
    background: #f8f9fa;
    border: 1px solid #dee2e6;
    border-radius: 6px;
    padding: 12px 16px;
    margin-bottom: 16px;
    transition: all 0.3s ease;

    &.status-online {
      border-color: #28a745;
      background: #d4edda;
    }

    &.status-offline {
      border-color: #ffc107;
      background: #fff3cd;
    }

    .status-indicator {
      display: flex;
      align-items: center;
      font-weight: 500;

      .fas {
        margin-right: 8px;
        font-size: 16px;
      }
    }

    .status-details {
      margin-top: 4px;
    }
  }

  .alert {
    .fas {
      color: #856404;
    }
  }

  // Indicador visual na tabela para dados offline
  .table-offline-mode {
    .table {
      opacity: 0.9;
      
      thead th {
        background-color: #fff3cd;
        border-color: #ffc107;
      }
    }
  }
}

// Animação para indicador de reconexão
@keyframes pulse {
  0% { opacity: 1; }
  50% { opacity: 0.5; }
  100% { opacity: 1; }
}

.reconnecting {
  animation: pulse 1.5s infinite;
}

🔧 Configuração do Environment

// environment.ts
export const environment = {
  production: false,
  apiUrl: 'https://prafrota-be-bff-tenant-api.grupopra.tech/api/v1',
  
  // Configurações de fallback
  fallback: {
    enabled: true,
    mockDataPath: '../../../docs/router/ROUTES_MOCK_DATA_COMPLETE.json',
    connectionCheckInterval: 30000, // 30 segundos
    requestTimeout: 10000 // 10 segundos
  }
};

🚀 Funcionalidades Implementadas

Fallback Automático

  • Detecção automática de falha no backend
  • Uso transparente dos dados mockados
  • Filtros e paginação funcionam offline

Indicadores Visuais

  • Status de conexão em tempo real
  • Alertas quando usando dados locais
  • Botão de reconexão manual

Monitoramento de Conectividade

  • Verificação periódica do backend
  • Reconexão automática quando disponível
  • Logs informativos no console

Operações Limitadas

  • Leitura: Funciona offline com dados mockados
  • Criação/Edição/Exclusão: Apenas com backend online
  • Mensagens claras sobre limitações

📊 Vantagens da Implementação

  1. Continuidade Operacional: Sistema funciona mesmo offline
  2. Experiência Transparente: Usuário continua navegando
  3. Feedback Visual: Status claro da conectividade
  4. Dados Realistas: 500 rotas mockadas com dados reais
  5. Filtros Funcionais: Busca e filtros funcionam offline
  6. Reconexão Inteligente: Volta automaticamente para backend

🔄 Fluxo de Funcionamento

graph TD
    A[Requisição para Backend] --> B{Backend Disponível?}
    B -->|Sim| C[Retorna Dados do Servidor]
    B -->|Não| D[Usa Dados Mockados]
    C --> E[Marca: source = 'backend']
    D --> F[Marca: source = 'fallback']
    E --> G[Exibe Dados + Status Online]
    F --> H[Exibe Dados + Status Offline]
    H --> I[Monitor de Reconexão]
    I --> J{Backend Voltou?}
    J -->|Sim| A
    J -->|Não| I

Esta implementação garante que o módulo de Rotas seja resiliente e continue funcionando mesmo com problemas de conectividade, proporcionando uma excelente experiência do usuário.