# 🔄 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 ```typescript // 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 { return this.http.get(`${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 { return this.http.get(`${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): Observable { return this.http.post(`${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): Observable { return this.http.put(`${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 { return this.http.delete(`${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 { 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 { 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 ```typescript // 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 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) { return this.routesService.createRoute(item); } update(id: string, item: Partial) { return this.routesService.updateRoute(id, item); } delete(id: string) { return this.routesService.deleteRoute(id); } } ``` ### 3. Template com Indicador Visual ```html
{{ getStatusText() }}
Fonte: {{ dataSource === 'backend' ? 'Servidor' : 'Cache Local' }} | Última atualização: {{ lastUpdateTime | date:'dd/MM/yyyy HH:mm:ss' }}
``` ### 4. Estilos CSS ```scss // 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 ```typescript // 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 ```mermaid 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.