18 KiB
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
- Continuidade Operacional: Sistema funciona mesmo offline
- Experiência Transparente: Usuário continua navegando
- Feedback Visual: Status claro da conectividade
- Dados Realistas: 500 rotas mockadas com dados reais
- Filtros Funcionais: Busca e filtros funcionam offline
- 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.