1186 lines
34 KiB
Markdown
1186 lines
34 KiB
Markdown
|
|
# 🚚 **SISTEMA DE ROTEIRIZAÇÃO TSP - EXPO REACT NATIVE**
|
||
|
|
|
||
|
|
## **📋 VISÃO GERAL**
|
||
|
|
|
||
|
|
Sistema completo de otimização de rotas de entrega implementando o algoritmo **Traveling Salesman Problem (TSP)** usando **Nearest Neighbor** para organizar entregas pela distância mais eficiente.
|
||
|
|
|
||
|
|
**URL Base da API:** `https://api.entrega.homologacao.jurunense.com`
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## **🏗️ ARQUITETURA DO SISTEMA**
|
||
|
|
|
||
|
|
### **Estrutura de Arquivos**
|
||
|
|
```
|
||
|
|
src/
|
||
|
|
├── screens/
|
||
|
|
│ └── main/
|
||
|
|
│ └── RoutingScreen.tsx # Tela principal de roteirização
|
||
|
|
├── services/
|
||
|
|
│ └── api.ts # Serviços de API e algoritmos TSP
|
||
|
|
├── types/
|
||
|
|
│ └── index.ts # Tipos TypeScript
|
||
|
|
├── contexts/
|
||
|
|
│ └── AuthContext.tsx # Contexto de autenticação
|
||
|
|
└── config/
|
||
|
|
└── env.ts # Configurações de ambiente
|
||
|
|
```
|
||
|
|
|
||
|
|
### **Componentes Principais**
|
||
|
|
- **RoutingScreen**: Interface para execução de roteirização
|
||
|
|
- **ApiService**: Classe para comunicação com API
|
||
|
|
- **Algoritmos TSP**: Funções de otimização de rota
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## **🔧 IMPLEMENTAÇÃO PASSO A PASSO**
|
||
|
|
|
||
|
|
### **PASSO 1: CONFIGURAÇÃO DE AMBIENTE**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// src/config/env.ts
|
||
|
|
export const API_BASE_URL = 'https://api.entrega.homologacao.jurunense.com';
|
||
|
|
export const AUTH_TOKEN_KEY = 'AUTH_TOKEN';
|
||
|
|
export const USER_DATA_KEY = 'USER_DATA';
|
||
|
|
|
||
|
|
// Função para converter coordenadas (vírgula → ponto)
|
||
|
|
export const convertCoordinate = (coord: any): number => {
|
||
|
|
if (coord === null || coord === undefined || coord === '') {
|
||
|
|
return 0;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof coord === 'number') {
|
||
|
|
return coord;
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof coord === 'string') {
|
||
|
|
const normalized = coord.trim().replace(',', '.');
|
||
|
|
const parsed = parseFloat(normalized);
|
||
|
|
return isNaN(parsed) ? 0 : parsed;
|
||
|
|
}
|
||
|
|
|
||
|
|
return 0;
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### **PASSO 2: TIPOS TYPESCRIPT**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// src/types/index.ts
|
||
|
|
export interface Delivery {
|
||
|
|
id?: string;
|
||
|
|
outId: number;
|
||
|
|
customerId: number;
|
||
|
|
customerName: string;
|
||
|
|
street: string;
|
||
|
|
streetNumber: string;
|
||
|
|
neighborhood: string;
|
||
|
|
city: string;
|
||
|
|
state: string;
|
||
|
|
zipCode: string;
|
||
|
|
lat: number | string | null;
|
||
|
|
lng: number | string | null;
|
||
|
|
coordinates?: {
|
||
|
|
latitude: number | string;
|
||
|
|
longitude: number | string;
|
||
|
|
};
|
||
|
|
deliverySeq: number;
|
||
|
|
routing: number; // 0 = não roteirizada, 1 = roteirizada
|
||
|
|
status: 'pending' | 'in_progress' | 'delivered' | 'failed';
|
||
|
|
outDate: string;
|
||
|
|
latFrom?: string;
|
||
|
|
lngFrom?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
export interface RoutingData {
|
||
|
|
outId: number;
|
||
|
|
customerId: number;
|
||
|
|
deliverySeq: number;
|
||
|
|
lat: number;
|
||
|
|
lng: number;
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
### **PASSO 3: FUNÇÕES UTILITÁRIAS**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// src/services/api.ts
|
||
|
|
|
||
|
|
// 1. Cálculo de distância (Fórmula de Haversine)
|
||
|
|
export const calculateDistance = (lat1: number, lon1: number, lat2: number, lon2: number): number => {
|
||
|
|
const R = 6371; // Raio da Terra em km
|
||
|
|
const dLat = (lat2 - lat1) * Math.PI / 180;
|
||
|
|
const dLon = (lon2 - lon1) * Math.PI / 180;
|
||
|
|
const a =
|
||
|
|
Math.sin(dLat/2) * Math.sin(dLat/2) +
|
||
|
|
Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) *
|
||
|
|
Math.sin(dLon/2) * Math.sin(dLon/2);
|
||
|
|
const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a));
|
||
|
|
return R * c;
|
||
|
|
};
|
||
|
|
|
||
|
|
// 2. Centro de distribuição dinâmico
|
||
|
|
export const getDistributionCenter = (deliveries: Delivery[]): {
|
||
|
|
latitude: number;
|
||
|
|
longitude: number;
|
||
|
|
address: string
|
||
|
|
} => {
|
||
|
|
const deliveryWithCoords = deliveries.find(delivery =>
|
||
|
|
delivery.latFrom && delivery.lngFrom &&
|
||
|
|
delivery.latFrom !== 'null' && delivery.lngFrom !== 'null' &&
|
||
|
|
delivery.latFrom !== '' && delivery.lngFrom !== ''
|
||
|
|
);
|
||
|
|
|
||
|
|
if (deliveryWithCoords && deliveryWithCoords.latFrom && deliveryWithCoords.lngFrom) {
|
||
|
|
const lat = parseFloat(deliveryWithCoords.latFrom);
|
||
|
|
const lng = parseFloat(deliveryWithCoords.lngFrom);
|
||
|
|
|
||
|
|
if (!isNaN(lat) && !isNaN(lng)) {
|
||
|
|
return {
|
||
|
|
latitude: lat,
|
||
|
|
longitude: lng,
|
||
|
|
address: "Centro de Distribuição (API)"
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Fallback para coordenadas padrão
|
||
|
|
return {
|
||
|
|
latitude: -1.3654,
|
||
|
|
longitude: -48.3722,
|
||
|
|
address: "Centro de Distribuição (Padrão)"
|
||
|
|
};
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### **PASSO 4: ALGORITMO TSP - NEAREST NEIGHBOR**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// src/services/api.ts
|
||
|
|
|
||
|
|
const nearestNeighborTSP = (distanceMatrix: number[][], deliveries: Delivery[]): Delivery[] => {
|
||
|
|
console.log('=== 🔍 EXECUTANDO ALGORITMO NEAREST NEIGHBOR ===');
|
||
|
|
|
||
|
|
const n = deliveries.length;
|
||
|
|
const visited = new Array(n).fill(false);
|
||
|
|
const route: Delivery[] = [];
|
||
|
|
|
||
|
|
// Começar do centro de distribuição (índice 0 na matriz)
|
||
|
|
let currentIndex = 0;
|
||
|
|
visited[0] = true;
|
||
|
|
|
||
|
|
// Adicionar primeira entrega (mais próxima do centro)
|
||
|
|
if (n > 0) {
|
||
|
|
route.push(deliveries[0]);
|
||
|
|
console.log(`📍 Primeira entrega: ${deliveries[0].customerName} (mais próxima do centro)`);
|
||
|
|
}
|
||
|
|
|
||
|
|
// Encontrar o próximo ponto mais próximo
|
||
|
|
for (let step = 1; step < n; step++) {
|
||
|
|
let minDistance = Infinity;
|
||
|
|
let nextIndex = -1;
|
||
|
|
|
||
|
|
// Procurar o ponto não visitado mais próximo
|
||
|
|
for (let i = 1; i < n; i++) { // Pular índice 0 (centro de distribuição)
|
||
|
|
if (!visited[i]) {
|
||
|
|
const distance = distanceMatrix[currentIndex][i];
|
||
|
|
if (distance < minDistance) {
|
||
|
|
minDistance = distance;
|
||
|
|
nextIndex = i;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (nextIndex !== -1) {
|
||
|
|
visited[nextIndex] = true;
|
||
|
|
route.push(deliveries[nextIndex]);
|
||
|
|
console.log(`📍 Próxima entrega: ${deliveries[nextIndex].customerName} (distância: ${minDistance.toFixed(2)} km)`);
|
||
|
|
currentIndex = nextIndex;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log(`✅ Rota otimizada criada com ${route.length} entregas`);
|
||
|
|
return route;
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### **PASSO 5: FUNÇÃO PRINCIPAL DE OTIMIZAÇÃO**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// src/services/api.ts
|
||
|
|
|
||
|
|
export const optimizeRouteWithTSP = async (deliveries: Delivery[]): Promise<Delivery[]> => {
|
||
|
|
console.log('=== 🚚 INICIANDO OTIMIZAÇÃO DE ROTA COM ALGORITMO TSP ===');
|
||
|
|
console.log('Total de entregas para otimizar:', deliveries.length);
|
||
|
|
|
||
|
|
// 1. Obter centro de distribuição
|
||
|
|
const distributionCenter = getDistributionCenter(deliveries);
|
||
|
|
console.log('Centro usado:', distributionCenter);
|
||
|
|
|
||
|
|
// 2. Preparar entregas garantindo coordenadas
|
||
|
|
const preparedDeliveries: Delivery[] = [];
|
||
|
|
for (let index = 0; index < deliveries.length; index++) {
|
||
|
|
const delivery = deliveries[index];
|
||
|
|
|
||
|
|
let latNum: number | null = null;
|
||
|
|
let lngNum: number | null = null;
|
||
|
|
|
||
|
|
// Tentar usar lat/lng já existentes
|
||
|
|
if (typeof delivery.lat === 'number' && typeof delivery.lng === 'number' &&
|
||
|
|
!isNaN(delivery.lat) && !isNaN(delivery.lng)) {
|
||
|
|
latNum = delivery.lat;
|
||
|
|
lngNum = delivery.lng;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Normalizar strings (vírgula → ponto)
|
||
|
|
if ((latNum === null || lngNum === null) &&
|
||
|
|
(typeof delivery.lat === 'string' || typeof delivery.lng === 'string')) {
|
||
|
|
const maybeLat = convertCoordinate(delivery.lat);
|
||
|
|
const maybeLng = convertCoordinate(delivery.lng);
|
||
|
|
if (typeof maybeLat === 'number' && !isNaN(maybeLat)) latNum = maybeLat;
|
||
|
|
if (typeof maybeLng === 'number' && !isNaN(maybeLng)) lngNum = maybeLng;
|
||
|
|
}
|
||
|
|
|
||
|
|
// Usar coordinates se existirem
|
||
|
|
if ((latNum === null || lngNum === null) && delivery.coordinates) {
|
||
|
|
const maybeLat = convertCoordinate((delivery.coordinates as any).latitude);
|
||
|
|
const maybeLng = convertCoordinate((delivery.coordinates as any).longitude);
|
||
|
|
if (typeof maybeLat === 'number' && !isNaN(maybeLat)) latNum = maybeLat;
|
||
|
|
if (typeof maybeLng === 'number' && !isNaN(maybeLng)) lngNum = maybeLng;
|
||
|
|
}
|
||
|
|
|
||
|
|
// 4. Se ainda não tem coordenadas, tentar geocodificar
|
||
|
|
if (latNum === null || lngNum === null) {
|
||
|
|
try {
|
||
|
|
const coords = await getCoordinatesFromAddress({
|
||
|
|
address: delivery.street || '',
|
||
|
|
addressNumber: delivery.streetNumber || '',
|
||
|
|
neighborhood: delivery.neighborhood || '',
|
||
|
|
city: delivery.city || '',
|
||
|
|
state: delivery.state || ''
|
||
|
|
});
|
||
|
|
if (coords) {
|
||
|
|
latNum = coords.latitude;
|
||
|
|
lngNum = coords.longitude;
|
||
|
|
console.log('✅ Geolocalização obtida:', coords);
|
||
|
|
}
|
||
|
|
} catch (geoErr) {
|
||
|
|
console.warn('⚠️ Falha ao geolocalizar:', geoErr);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
preparedDeliveries.push({
|
||
|
|
...delivery,
|
||
|
|
lat: typeof latNum === 'number' ? latNum : delivery.lat,
|
||
|
|
lng: typeof lngNum === 'number' ? lngNum : delivery.lng,
|
||
|
|
coordinates: (typeof latNum === 'number' && typeof lngNum === 'number')
|
||
|
|
? { latitude: latNum, longitude: lngNum }
|
||
|
|
: delivery.coordinates
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
// 3. Separar entregas com e sem coordenadas
|
||
|
|
const withCoords = preparedDeliveries.filter(d =>
|
||
|
|
typeof d.lat === 'number' && typeof d.lng === 'number' &&
|
||
|
|
!isNaN(d.lat as number) && !isNaN(d.lng as number)
|
||
|
|
);
|
||
|
|
const withoutCoords = preparedDeliveries.filter(d =>
|
||
|
|
!(typeof d.lat === 'number' && typeof d.lng === 'number' &&
|
||
|
|
!isNaN(d.lat as number) && !isNaN(d.lng as number))
|
||
|
|
);
|
||
|
|
|
||
|
|
if (withCoords.length === 0) {
|
||
|
|
console.log('❌ Nenhuma entrega com coordenadas válidas');
|
||
|
|
return preparedDeliveries.map((d, i) => ({ ...d, deliverySeq: i + 1 }));
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log(`✅ ${withCoords.length} entregas com coordenadas válidas`);
|
||
|
|
|
||
|
|
// 4. Calcular matriz de distâncias
|
||
|
|
const distanceMatrix: number[][] = [];
|
||
|
|
const allPoints = [distributionCenter, ...withCoords.map(d => ({
|
||
|
|
latitude: d.lat as number,
|
||
|
|
longitude: d.lng as number
|
||
|
|
}))];
|
||
|
|
|
||
|
|
for (let i = 0; i < allPoints.length; i++) {
|
||
|
|
distanceMatrix[i] = [];
|
||
|
|
for (let j = 0; j < allPoints.length; j++) {
|
||
|
|
if (i === j) {
|
||
|
|
distanceMatrix[i][j] = 0;
|
||
|
|
} else {
|
||
|
|
distanceMatrix[i][j] = calculateDistance(
|
||
|
|
allPoints[i].latitude,
|
||
|
|
allPoints[i].longitude,
|
||
|
|
allPoints[j].latitude,
|
||
|
|
allPoints[j].longitude
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// 5. Executar algoritmo TSP
|
||
|
|
const optimizedWithCoords = nearestNeighborTSP(distanceMatrix, withCoords);
|
||
|
|
|
||
|
|
// 6. Montar rota final
|
||
|
|
const finalRoute: Delivery[] = [...optimizedWithCoords, ...withoutCoords];
|
||
|
|
|
||
|
|
// 7. Aplicar deliverySeq sequencialmente
|
||
|
|
const finalDeliveries = finalRoute.map((delivery, idx) => {
|
||
|
|
const newSeq = idx + 1; // Garantir que comece em 1
|
||
|
|
console.log(`🔄 Atualizando ${delivery.customerName}: deliverySeq ${delivery.deliverySeq} → ${newSeq}`);
|
||
|
|
return { ...delivery, deliverySeq: newSeq, distance: (delivery as any).distance || 0 };
|
||
|
|
});
|
||
|
|
|
||
|
|
console.log('=== 🎯 ENTREGAS COM DELIVERYSEQ ATUALIZADO ===');
|
||
|
|
finalDeliveries.forEach((delivery, index) => {
|
||
|
|
console.log(`📦 Entrega ${index + 1} (deliverySeq: ${delivery.deliverySeq}): ${delivery.customerName}`);
|
||
|
|
});
|
||
|
|
|
||
|
|
return finalDeliveries;
|
||
|
|
};
|
||
|
|
```
|
||
|
|
|
||
|
|
### **PASSO 6: SERVIÇO DE API**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// src/services/api.ts
|
||
|
|
|
||
|
|
class ApiService {
|
||
|
|
private baseUrl: string;
|
||
|
|
private token: string | null = null;
|
||
|
|
|
||
|
|
constructor() {
|
||
|
|
this.baseUrl = 'https://api.entrega.homologacao.jurunense.com';
|
||
|
|
this.loadToken();
|
||
|
|
}
|
||
|
|
|
||
|
|
// Enviar ordem de roteamento
|
||
|
|
async sendRoutingOrder(routingData: RoutingData[]): Promise<any> {
|
||
|
|
try {
|
||
|
|
console.log('=== DEBUG: INICIANDO sendRoutingOrder ===');
|
||
|
|
console.log('Dados recebidos:', JSON.stringify(routingData, null, 2));
|
||
|
|
|
||
|
|
// Validar e normalizar coordenadas
|
||
|
|
const validatedRoutingData = routingData.map((item, index) => {
|
||
|
|
let lat: any = item.lat;
|
||
|
|
let lng: any = item.lng;
|
||
|
|
|
||
|
|
// Converter strings com vírgula para ponto
|
||
|
|
if (typeof lat === 'string') {
|
||
|
|
lat = parseFloat(lat.replace(',', '.'));
|
||
|
|
if (isNaN(lat)) {
|
||
|
|
console.warn(`⚠️ Coordenada lat inválida: ${item.lat}, usando 0`);
|
||
|
|
lat = 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
if (typeof lng === 'string') {
|
||
|
|
lng = parseFloat(lng.replace(',', '.'));
|
||
|
|
if (isNaN(lng)) {
|
||
|
|
console.warn(`⚠️ Coordenada lng inválida: ${item.lng}, usando 0`);
|
||
|
|
lng = 0;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Garantir que sejam números
|
||
|
|
lat = Number(lat) || 0;
|
||
|
|
lng = Number(lng) || 0;
|
||
|
|
|
||
|
|
return { ...item, lat, lng };
|
||
|
|
});
|
||
|
|
|
||
|
|
const token = await this.loadToken();
|
||
|
|
if (!token) throw new Error('Token não encontrado');
|
||
|
|
|
||
|
|
const ENDPOINT = `${this.baseUrl}/v1/driver/routing`;
|
||
|
|
const headers = {
|
||
|
|
'Authorization': `Bearer ${token}`,
|
||
|
|
'Content-Type': 'application/json'
|
||
|
|
};
|
||
|
|
|
||
|
|
const response = await fetch(ENDPOINT, {
|
||
|
|
method: 'POST',
|
||
|
|
headers,
|
||
|
|
body: JSON.stringify(validatedRoutingData)
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.status === 401) {
|
||
|
|
throw new Error('Sessão expirada. Faça login novamente.');
|
||
|
|
}
|
||
|
|
|
||
|
|
const result = await response.json();
|
||
|
|
|
||
|
|
if (!response.ok || !result.success) {
|
||
|
|
throw new Error(result.message || 'Erro ao enviar ordem de roteamento');
|
||
|
|
}
|
||
|
|
|
||
|
|
console.log('✅ Roteirização bem-sucedida');
|
||
|
|
return result.data;
|
||
|
|
} catch (error) {
|
||
|
|
console.error('❌ Erro em sendRoutingOrder:', error);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// Carregar entregas
|
||
|
|
async getDeliveries(): Promise<Delivery[]> {
|
||
|
|
try {
|
||
|
|
const token = await this.loadToken();
|
||
|
|
if (!token) throw new Error('Token não encontrado');
|
||
|
|
|
||
|
|
const response = await fetch(`${this.baseUrl}/v1/driver/deliveries`, {
|
||
|
|
headers: {
|
||
|
|
'Authorization': `Bearer ${token}`,
|
||
|
|
'Content-Type': 'application/json'
|
||
|
|
}
|
||
|
|
});
|
||
|
|
|
||
|
|
if (response.status === 401) {
|
||
|
|
throw new Error('Sessão expirada');
|
||
|
|
}
|
||
|
|
|
||
|
|
const result = await response.json();
|
||
|
|
return Array.isArray(result) ? result : [];
|
||
|
|
} catch (error) {
|
||
|
|
console.error('❌ Erro ao carregar entregas:', error);
|
||
|
|
throw error;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async loadToken(): Promise<string | null> {
|
||
|
|
try {
|
||
|
|
const storedToken = await AsyncStorage.getItem(AUTH_TOKEN_KEY);
|
||
|
|
if (storedToken) {
|
||
|
|
this.token = storedToken;
|
||
|
|
return storedToken;
|
||
|
|
}
|
||
|
|
return null;
|
||
|
|
} catch (error) {
|
||
|
|
console.error("Erro ao carregar token:", error);
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
export const api = new ApiService();
|
||
|
|
```
|
||
|
|
|
||
|
|
### **PASSO 7: TELA PRINCIPAL DE ROTEIRIZAÇÃO**
|
||
|
|
|
||
|
|
```typescript
|
||
|
|
// src/screens/main/RoutingScreen.tsx
|
||
|
|
|
||
|
|
import React, { useState, useEffect } from 'react';
|
||
|
|
import {
|
||
|
|
View,
|
||
|
|
Text,
|
||
|
|
StyleSheet,
|
||
|
|
ScrollView,
|
||
|
|
TouchableOpacity,
|
||
|
|
ActivityIndicator,
|
||
|
|
Alert,
|
||
|
|
RefreshControl
|
||
|
|
} from 'react-native';
|
||
|
|
import { Ionicons } from '@expo/vector-icons';
|
||
|
|
import { LinearGradient } from 'expo-linear-gradient';
|
||
|
|
import { COLORS, SHADOWS } from '../../constants/theme';
|
||
|
|
import { Delivery } from '../../types';
|
||
|
|
import { api, optimizeRouteWithTSP } from '../../services/api';
|
||
|
|
|
||
|
|
const RoutingScreen: React.FC<{ navigation: any; route: any }> = ({ navigation, route }) => {
|
||
|
|
const [deliveries, setDeliveries] = useState<Delivery[]>([]);
|
||
|
|
const [isLoading, setIsLoading] = useState(true);
|
||
|
|
const [isRouting, setIsRouting] = useState(false);
|
||
|
|
const [routingProgress, setRoutingProgress] = useState(0);
|
||
|
|
const [error, setError] = useState<string | null>(null);
|
||
|
|
|
||
|
|
// Carregar entregas iniciais
|
||
|
|
useEffect(() => {
|
||
|
|
loadDeliveries();
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
// Carregar entregas do endpoint
|
||
|
|
const loadDeliveries = async () => {
|
||
|
|
try {
|
||
|
|
setIsLoading(true);
|
||
|
|
setError(null);
|
||
|
|
|
||
|
|
const response = await api.getDeliveries();
|
||
|
|
|
||
|
|
if (response) {
|
||
|
|
const deliveriesData = Array.isArray(response) ? response : [];
|
||
|
|
console.log('📦 Entregas processadas:', deliveriesData.length);
|
||
|
|
|
||
|
|
// Ordenar entregas usando algoritmo TSP
|
||
|
|
try {
|
||
|
|
const sortedDeliveries = await optimizeRouteWithTSP(deliveriesData);
|
||
|
|
console.log('✅ Entregas ordenadas com sucesso:', sortedDeliveries.length);
|
||
|
|
setDeliveries(sortedDeliveries);
|
||
|
|
|
||
|
|
// Verificar se já tem roteirização
|
||
|
|
const hasRouting = deliveriesData.some((delivery: Delivery) => delivery.routing === 1);
|
||
|
|
if (hasRouting) {
|
||
|
|
console.log('✅ Já tem roteirização - Redirecionando');
|
||
|
|
navigation.reset({
|
||
|
|
index: 0,
|
||
|
|
routes: [{ name: 'Main' as never }],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
} catch (sortError) {
|
||
|
|
console.error('❌ Erro ao ordenar entregas:', sortError);
|
||
|
|
setDeliveries(deliveriesData);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (error: any) {
|
||
|
|
console.error('❌ Erro ao carregar entregas:', error);
|
||
|
|
setError(error.message || 'Erro ao carregar entregas');
|
||
|
|
} finally {
|
||
|
|
setIsLoading(false);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Executar roteirização
|
||
|
|
const executeRouting = async () => {
|
||
|
|
try {
|
||
|
|
setIsRouting(true);
|
||
|
|
setRoutingProgress(0);
|
||
|
|
setError(null);
|
||
|
|
|
||
|
|
// Filtrar entregas sem roteirização
|
||
|
|
const deliveriesToRoute = deliveries.filter(delivery => delivery.routing === 0);
|
||
|
|
|
||
|
|
if (deliveriesToRoute.length === 0) {
|
||
|
|
Alert.alert('Info', 'Todas as entregas já estão roteirizadas');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
setRoutingProgress(20);
|
||
|
|
|
||
|
|
// SOLUÇÃO DEFINITIVA: OTIMIZAR ROTA ANTES DE ENVIAR PARA API
|
||
|
|
console.log('🎯 OTIMIZANDO ROTA COM ALGORITMO TSP ANTES DO ENVIO...');
|
||
|
|
|
||
|
|
const optimizedDeliveries = await optimizeRouteWithTSP(deliveriesToRoute);
|
||
|
|
console.log('✅ Rota otimizada com sucesso');
|
||
|
|
|
||
|
|
if (optimizedDeliveries && optimizedDeliveries.length > 0) {
|
||
|
|
// Preparar dados OTIMIZADOS para roteirização
|
||
|
|
const optimizedRoutingData = optimizedDeliveries.map((delivery) => ({
|
||
|
|
outId: delivery.outId,
|
||
|
|
customerId: delivery.customerId,
|
||
|
|
deliverySeq: delivery.deliverySeq, // JÁ OTIMIZADO (1, 2, 3, ...)
|
||
|
|
lat: delivery.lat as number,
|
||
|
|
lng: delivery.lng as number
|
||
|
|
}));
|
||
|
|
|
||
|
|
setRoutingProgress(60);
|
||
|
|
|
||
|
|
// Executar roteirização com dados OTIMIZADOS
|
||
|
|
const routingResponse = await api.sendRoutingOrder(optimizedRoutingData);
|
||
|
|
|
||
|
|
setRoutingProgress(80);
|
||
|
|
|
||
|
|
if (routingResponse) {
|
||
|
|
console.log('✅ Roteirização com sequência otimizada executada com sucesso!');
|
||
|
|
|
||
|
|
// Atualizar estado local
|
||
|
|
setDeliveries(optimizedDeliveries);
|
||
|
|
|
||
|
|
setRoutingProgress(100);
|
||
|
|
|
||
|
|
Alert.alert(
|
||
|
|
'Roteirização Concluída! 🎉',
|
||
|
|
`As entregas foram organizadas e ordenadas com sucesso!\n\nTotal: ${optimizedDeliveries.length} entregas\nPróxima: ${optimizedDeliveries[0]?.customerName || 'N/A'}`,
|
||
|
|
[
|
||
|
|
{
|
||
|
|
text: 'OK',
|
||
|
|
onPress: () => {
|
||
|
|
navigation.reset({
|
||
|
|
index: 0,
|
||
|
|
routes: [{
|
||
|
|
name: 'Main' as never,
|
||
|
|
params: {
|
||
|
|
screen: 'Home',
|
||
|
|
params: {
|
||
|
|
routingUpdated: true,
|
||
|
|
refreshDeliveries: true
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}],
|
||
|
|
});
|
||
|
|
}
|
||
|
|
}
|
||
|
|
]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
} catch (error: any) {
|
||
|
|
console.error('Erro na roteirização:', error);
|
||
|
|
setError(error.message || 'Erro ao executar roteirização');
|
||
|
|
Alert.alert('Erro', 'Falha na roteirização. Tente novamente.');
|
||
|
|
} finally {
|
||
|
|
setIsRouting(false);
|
||
|
|
setRoutingProgress(0);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Refresh control
|
||
|
|
const onRefresh = async () => {
|
||
|
|
try {
|
||
|
|
await loadDeliveries();
|
||
|
|
} catch (error: any) {
|
||
|
|
console.error('Erro no refresh:', error);
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
if (isLoading) {
|
||
|
|
return (
|
||
|
|
<View style={styles.loadingContainer}>
|
||
|
|
<ActivityIndicator size="large" color={COLORS.primary} />
|
||
|
|
<Text style={styles.loadingText}>Carregando entregas...</Text>
|
||
|
|
</View>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<View style={styles.container}>
|
||
|
|
{/* Header */}
|
||
|
|
<LinearGradient colors={[COLORS.primary, COLORS.primary]} style={styles.header}>
|
||
|
|
<TouchableOpacity
|
||
|
|
style={styles.backButton}
|
||
|
|
onPress={() => navigation.reset({
|
||
|
|
index: 0,
|
||
|
|
routes: [{ name: 'Main' as never }],
|
||
|
|
})}
|
||
|
|
>
|
||
|
|
<Ionicons name="arrow-back" size={24} color="white" />
|
||
|
|
</TouchableOpacity>
|
||
|
|
|
||
|
|
<Text style={styles.headerTitle}>Roteirização de Entregas</Text>
|
||
|
|
|
||
|
|
<TouchableOpacity style={styles.refreshButton} onPress={onRefresh}>
|
||
|
|
<Ionicons name="refresh" size={24} color="white" />
|
||
|
|
</TouchableOpacity>
|
||
|
|
</LinearGradient>
|
||
|
|
|
||
|
|
{/* Conteúdo */}
|
||
|
|
<ScrollView style={styles.content} refreshControl={
|
||
|
|
<RefreshControl refreshing={false} onRefresh={onRefresh} />
|
||
|
|
}>
|
||
|
|
{/* Resumo */}
|
||
|
|
<View style={styles.summaryCard}>
|
||
|
|
<Text style={styles.summaryTitle}>Resumo das Entregas</Text>
|
||
|
|
<View style={styles.summaryStats}>
|
||
|
|
<View style={styles.statItem}>
|
||
|
|
<Text style={styles.statNumber}>{deliveries.length}</Text>
|
||
|
|
<Text style={styles.statLabel}>Total</Text>
|
||
|
|
</View>
|
||
|
|
<View style={styles.statItem}>
|
||
|
|
<Text style={styles.statNumber}>
|
||
|
|
{deliveries.filter(d => d.routing === 0).length}
|
||
|
|
</Text>
|
||
|
|
<Text style={styles.statLabel}>Pendentes</Text>
|
||
|
|
</View>
|
||
|
|
<View style={styles.statItem}>
|
||
|
|
<Text style={styles.statNumber}>
|
||
|
|
{deliveries.filter(d => d.routing === 1).length}
|
||
|
|
</Text>
|
||
|
|
<Text style={styles.statLabel}>Roteirizadas</Text>
|
||
|
|
</View>
|
||
|
|
</View>
|
||
|
|
|
||
|
|
{/* Informações de Ordenação */}
|
||
|
|
{deliveries.length > 0 && (
|
||
|
|
<View style={styles.orderingInfo}>
|
||
|
|
<Text style={styles.orderingTitle}>📍 Ordenação por Sequência</Text>
|
||
|
|
<Text style={styles.orderingText}>
|
||
|
|
Próxima entrega: {deliveries[0]?.customerName || 'N/A'}
|
||
|
|
</Text>
|
||
|
|
<Text style={styles.orderingText}>
|
||
|
|
Sequência: #{deliveries[0]?.deliverySeq || 'N/A'}
|
||
|
|
</Text>
|
||
|
|
{deliveries[0]?.distance && (
|
||
|
|
<Text style={styles.orderingText}>
|
||
|
|
Distância: {deliveries[0].distance.toFixed(2)} km
|
||
|
|
</Text>
|
||
|
|
)}
|
||
|
|
</View>
|
||
|
|
)}
|
||
|
|
</View>
|
||
|
|
|
||
|
|
{/* Botão de Roteirização */}
|
||
|
|
{deliveries.some(d => d.routing === 0) && (
|
||
|
|
<TouchableOpacity
|
||
|
|
style={[styles.routingButton, isRouting && styles.routingButtonDisabled]}
|
||
|
|
onPress={executeRouting}
|
||
|
|
disabled={isRouting}
|
||
|
|
>
|
||
|
|
<LinearGradient
|
||
|
|
colors={isRouting ? [COLORS.textLight, COLORS.textLight] : [COLORS.success, '#059669']}
|
||
|
|
style={styles.routingButtonGradient}
|
||
|
|
>
|
||
|
|
{isRouting ? (
|
||
|
|
<View style={styles.routingProgress}>
|
||
|
|
<ActivityIndicator size="small" color="white" />
|
||
|
|
<Text style={styles.routingProgressText}>
|
||
|
|
Roteirizando... {routingProgress}%
|
||
|
|
</Text>
|
||
|
|
</View>
|
||
|
|
) : (
|
||
|
|
<>
|
||
|
|
<Ionicons name="map" size={24} color="white" />
|
||
|
|
<Text style={styles.routingButtonText}>
|
||
|
|
Executar Roteirização
|
||
|
|
</Text>
|
||
|
|
</>
|
||
|
|
)}
|
||
|
|
</LinearGradient>
|
||
|
|
</TouchableOpacity>
|
||
|
|
)}
|
||
|
|
|
||
|
|
{/* Lista de Entregas */}
|
||
|
|
<View style={styles.deliveriesSection}>
|
||
|
|
<Text style={styles.sectionTitle}>
|
||
|
|
Entregas ({deliveries.length})
|
||
|
|
</Text>
|
||
|
|
|
||
|
|
{deliveries.map((delivery, index) => (
|
||
|
|
<View key={delivery.id || index} style={styles.deliveryItem}>
|
||
|
|
<View style={styles.deliveryHeader}>
|
||
|
|
<Text style={styles.deliveryNumber}>
|
||
|
|
#{delivery.outId}
|
||
|
|
</Text>
|
||
|
|
<View style={[styles.statusBadge, { backgroundColor: getStatusColor(delivery.status) }]}>
|
||
|
|
<Text style={styles.statusText}>
|
||
|
|
{getStatusText(delivery.status)}
|
||
|
|
</Text>
|
||
|
|
</View>
|
||
|
|
</View>
|
||
|
|
|
||
|
|
<Text style={styles.customerName}>
|
||
|
|
{delivery.customerName}
|
||
|
|
</Text>
|
||
|
|
|
||
|
|
<Text style={styles.address}>
|
||
|
|
{delivery.street}, {delivery.streetNumber}
|
||
|
|
</Text>
|
||
|
|
|
||
|
|
<View style={styles.deliveryMeta}>
|
||
|
|
<View style={styles.metaItem}>
|
||
|
|
<Ionicons name="location" size={16} color={COLORS.textLight} />
|
||
|
|
<Text style={styles.metaText}>
|
||
|
|
{delivery.neighborhood}, {delivery.city}
|
||
|
|
</Text>
|
||
|
|
</View>
|
||
|
|
|
||
|
|
<View style={styles.metaItem}>
|
||
|
|
<Ionicons name="time" size={16} color={COLORS.textLight} />
|
||
|
|
<Text style={styles.metaText}>
|
||
|
|
{delivery.routing === 0 ? 'Aguardando roteirização' : 'Roteirizada'}
|
||
|
|
</Text>
|
||
|
|
</View>
|
||
|
|
</View>
|
||
|
|
</View>
|
||
|
|
))}
|
||
|
|
</View>
|
||
|
|
|
||
|
|
{/* Mensagem de erro */}
|
||
|
|
{error && (
|
||
|
|
<View style={styles.errorContainer}>
|
||
|
|
<Ionicons name="alert-circle" size={24} color={COLORS.danger} />
|
||
|
|
<Text style={styles.errorText}>{error}</Text>
|
||
|
|
</View>
|
||
|
|
)}
|
||
|
|
</ScrollView>
|
||
|
|
</View>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
// Funções auxiliares
|
||
|
|
const getStatusColor = (status: string) => {
|
||
|
|
switch (status) {
|
||
|
|
case 'delivered': return COLORS.success;
|
||
|
|
case 'in_progress': return COLORS.info;
|
||
|
|
case 'failed': return COLORS.danger;
|
||
|
|
default: return COLORS.warning;
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const getStatusText = (status: string) => {
|
||
|
|
switch (status) {
|
||
|
|
case 'delivered': return 'Entregue';
|
||
|
|
case 'in_progress': return 'Em andamento';
|
||
|
|
case 'failed': return 'Falhou';
|
||
|
|
default: return 'Pendente';
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
// Estilos (implementar conforme necessário)
|
||
|
|
const styles = StyleSheet.create({
|
||
|
|
container: {
|
||
|
|
flex: 1,
|
||
|
|
backgroundColor: '#F5F5F5',
|
||
|
|
},
|
||
|
|
header: {
|
||
|
|
flexDirection: 'row',
|
||
|
|
alignItems: 'center',
|
||
|
|
justifyContent: 'space-between',
|
||
|
|
paddingTop: 50,
|
||
|
|
paddingBottom: 20,
|
||
|
|
paddingHorizontal: 20,
|
||
|
|
},
|
||
|
|
backButton: {
|
||
|
|
width: 40,
|
||
|
|
height: 40,
|
||
|
|
borderRadius: 20,
|
||
|
|
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||
|
|
alignItems: 'center',
|
||
|
|
justifyContent: 'center',
|
||
|
|
},
|
||
|
|
headerTitle: {
|
||
|
|
fontSize: 18,
|
||
|
|
fontWeight: 'bold',
|
||
|
|
color: 'white',
|
||
|
|
flex: 1,
|
||
|
|
textAlign: 'center',
|
||
|
|
},
|
||
|
|
refreshButton: {
|
||
|
|
width: 40,
|
||
|
|
height: 40,
|
||
|
|
borderRadius: 20,
|
||
|
|
backgroundColor: 'rgba(255, 255, 255, 0.2)',
|
||
|
|
alignItems: 'center',
|
||
|
|
justifyContent: 'center',
|
||
|
|
},
|
||
|
|
content: {
|
||
|
|
flex: 1,
|
||
|
|
padding: 20,
|
||
|
|
},
|
||
|
|
summaryCard: {
|
||
|
|
backgroundColor: 'white',
|
||
|
|
borderRadius: 12,
|
||
|
|
padding: 20,
|
||
|
|
marginBottom: 20,
|
||
|
|
shadowColor: '#000',
|
||
|
|
shadowOffset: { width: 0, height: 2 },
|
||
|
|
shadowOpacity: 0.1,
|
||
|
|
shadowRadius: 4,
|
||
|
|
elevation: 3,
|
||
|
|
},
|
||
|
|
summaryTitle: {
|
||
|
|
fontSize: 16,
|
||
|
|
fontWeight: '600',
|
||
|
|
color: '#333',
|
||
|
|
marginBottom: 16,
|
||
|
|
},
|
||
|
|
summaryStats: {
|
||
|
|
flexDirection: 'row',
|
||
|
|
justifyContent: 'space-around',
|
||
|
|
},
|
||
|
|
statItem: {
|
||
|
|
alignItems: 'center',
|
||
|
|
},
|
||
|
|
statNumber: {
|
||
|
|
fontSize: 24,
|
||
|
|
fontWeight: 'bold',
|
||
|
|
color: '#007AFF',
|
||
|
|
},
|
||
|
|
statLabel: {
|
||
|
|
fontSize: 12,
|
||
|
|
color: '#666',
|
||
|
|
marginTop: 4,
|
||
|
|
},
|
||
|
|
routingButton: {
|
||
|
|
marginBottom: 20,
|
||
|
|
borderRadius: 12,
|
||
|
|
overflow: 'hidden',
|
||
|
|
},
|
||
|
|
routingButtonDisabled: {
|
||
|
|
opacity: 0.7,
|
||
|
|
},
|
||
|
|
routingButtonGradient: {
|
||
|
|
flexDirection: 'row',
|
||
|
|
alignItems: 'center',
|
||
|
|
justifyContent: 'center',
|
||
|
|
paddingVertical: 16,
|
||
|
|
paddingHorizontal: 24,
|
||
|
|
},
|
||
|
|
routingButtonText: {
|
||
|
|
color: 'white',
|
||
|
|
fontSize: 16,
|
||
|
|
fontWeight: '600',
|
||
|
|
marginLeft: 8,
|
||
|
|
},
|
||
|
|
routingProgress: {
|
||
|
|
flexDirection: 'row',
|
||
|
|
alignItems: 'center',
|
||
|
|
},
|
||
|
|
routingProgressText: {
|
||
|
|
color: 'white',
|
||
|
|
fontSize: 14,
|
||
|
|
fontWeight: '500',
|
||
|
|
marginLeft: 8,
|
||
|
|
},
|
||
|
|
deliveriesSection: {
|
||
|
|
marginBottom: 20,
|
||
|
|
},
|
||
|
|
sectionTitle: {
|
||
|
|
fontSize: 18,
|
||
|
|
fontWeight: '600',
|
||
|
|
color: '#333',
|
||
|
|
marginBottom: 16,
|
||
|
|
},
|
||
|
|
deliveryItem: {
|
||
|
|
backgroundColor: 'white',
|
||
|
|
borderRadius: 12,
|
||
|
|
padding: 16,
|
||
|
|
marginBottom: 12,
|
||
|
|
shadowColor: '#000',
|
||
|
|
shadowOffset: { width: 0, height: 1 },
|
||
|
|
shadowOpacity: 0.05,
|
||
|
|
shadowRadius: 2,
|
||
|
|
elevation: 2,
|
||
|
|
},
|
||
|
|
deliveryHeader: {
|
||
|
|
flexDirection: 'row',
|
||
|
|
justifyContent: 'space-between',
|
||
|
|
alignItems: 'center',
|
||
|
|
marginBottom: 8,
|
||
|
|
},
|
||
|
|
deliveryNumber: {
|
||
|
|
fontSize: 14,
|
||
|
|
fontWeight: '600',
|
||
|
|
color: '#007AFF',
|
||
|
|
},
|
||
|
|
statusBadge: {
|
||
|
|
paddingHorizontal: 8,
|
||
|
|
paddingVertical: 4,
|
||
|
|
borderRadius: 8,
|
||
|
|
},
|
||
|
|
statusText: {
|
||
|
|
fontSize: 10,
|
||
|
|
fontWeight: '500',
|
||
|
|
color: 'white',
|
||
|
|
},
|
||
|
|
customerName: {
|
||
|
|
fontSize: 16,
|
||
|
|
fontWeight: '600',
|
||
|
|
color: '#333',
|
||
|
|
marginBottom: 4,
|
||
|
|
},
|
||
|
|
address: {
|
||
|
|
fontSize: 14,
|
||
|
|
color: '#666',
|
||
|
|
marginBottom: 8,
|
||
|
|
},
|
||
|
|
deliveryMeta: {
|
||
|
|
flexDirection: 'row',
|
||
|
|
justifyContent: 'space-between',
|
||
|
|
},
|
||
|
|
metaItem: {
|
||
|
|
flexDirection: 'row',
|
||
|
|
alignItems: 'center',
|
||
|
|
},
|
||
|
|
metaText: {
|
||
|
|
fontSize: 12,
|
||
|
|
color: '#666',
|
||
|
|
marginLeft: 4,
|
||
|
|
},
|
||
|
|
loadingContainer: {
|
||
|
|
flex: 1,
|
||
|
|
alignItems: 'center',
|
||
|
|
justifyContent: 'center',
|
||
|
|
backgroundColor: '#F5F5F5',
|
||
|
|
},
|
||
|
|
loadingText: {
|
||
|
|
marginTop: 16,
|
||
|
|
fontSize: 16,
|
||
|
|
color: '#333',
|
||
|
|
},
|
||
|
|
errorContainer: {
|
||
|
|
flexDirection: 'row',
|
||
|
|
alignItems: 'center',
|
||
|
|
backgroundColor: '#FEE2E2',
|
||
|
|
padding: 16,
|
||
|
|
borderRadius: 12,
|
||
|
|
marginTop: 20,
|
||
|
|
},
|
||
|
|
errorText: {
|
||
|
|
marginLeft: 8,
|
||
|
|
fontSize: 14,
|
||
|
|
color: '#DC2626',
|
||
|
|
flex: 1,
|
||
|
|
},
|
||
|
|
orderingInfo: {
|
||
|
|
marginTop: 16,
|
||
|
|
padding: 16,
|
||
|
|
backgroundColor: '#E0F2FE',
|
||
|
|
borderRadius: 8,
|
||
|
|
borderLeftWidth: 4,
|
||
|
|
borderLeftColor: '#007AFF',
|
||
|
|
},
|
||
|
|
orderingTitle: {
|
||
|
|
fontSize: 14,
|
||
|
|
fontWeight: '600',
|
||
|
|
color: '#007AFF',
|
||
|
|
marginBottom: 8,
|
||
|
|
},
|
||
|
|
orderingText: {
|
||
|
|
fontSize: 12,
|
||
|
|
color: '#666',
|
||
|
|
marginBottom: 4,
|
||
|
|
},
|
||
|
|
});
|
||
|
|
|
||
|
|
export default RoutingScreen;
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## **🚀 FLUXO COMPLETO DE ROTEIRIZAÇÃO**
|
||
|
|
|
||
|
|
### **1. CARREGAMENTO INICIAL**
|
||
|
|
- ✅ Carregar entregas da API (`/v1/driver/deliveries`)
|
||
|
|
- ✅ Verificar se já tem roteirização (`routing === 1`)
|
||
|
|
- ✅ Se sim → redirecionar para Home
|
||
|
|
- ✅ Se não → mostrar tela de roteirização
|
||
|
|
|
||
|
|
### **2. EXECUÇÃO DE ROTEIRIZAÇÃO**
|
||
|
|
- ✅ Filtrar entregas pendentes (`routing === 0`)
|
||
|
|
- ✅ **OTIMIZAR ROTA COM TSP** (antes de enviar para API)
|
||
|
|
- ✅ Preparar dados otimizados
|
||
|
|
- ✅ Enviar para API com `deliverySeq` já otimizado
|
||
|
|
- ✅ Atualizar estado local
|
||
|
|
- ✅ Redirecionar para Home
|
||
|
|
|
||
|
|
### **3. ALGORITMO TSP**
|
||
|
|
- ✅ Obter centro de distribuição
|
||
|
|
- ✅ Preparar coordenadas (normalizar + geolocalizar)
|
||
|
|
- ✅ Calcular matriz de distâncias
|
||
|
|
- ✅ Executar Nearest Neighbor
|
||
|
|
- ✅ Aplicar sequência sequencial (1, 2, 3, ...)
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## ** PONTOS CRÍTICOS DE IMPLEMENTAÇÃO**
|
||
|
|
|
||
|
|
### **1. ORDEM DE EXECUÇÃO CORRETA**
|
||
|
|
```typescript
|
||
|
|
// ❌ ERRADO: Enviar para API primeiro
|
||
|
|
await api.sendRoutingOrder(routingData);
|
||
|
|
const optimized = await optimizeRouteWithTSP(deliveries);
|
||
|
|
|
||
|
|
// ✅ CORRETO: Otimizar primeiro, depois enviar
|
||
|
|
const optimized = await optimizeRouteWithTSP(deliveries);
|
||
|
|
await api.sendRoutingOrder(optimizedRoutingData);
|
||
|
|
```
|
||
|
|
|
||
|
|
### **2. NORMALIZAÇÃO DE COORDENADAS**
|
||
|
|
```typescript
|
||
|
|
// Sempre converter vírgula para ponto
|
||
|
|
const lat = parseFloat(coord.replace(',', '.'));
|
||
|
|
```
|
||
|
|
|
||
|
|
### **3. SEQUÊNCIA INICIAL**
|
||
|
|
```typescript
|
||
|
|
// Garantir que deliverySeq comece em 1, não em 0
|
||
|
|
const newSeq = idx + 1; // ✅ Correto
|
||
|
|
const newSeq = idx; // ❌ Errado (começa em 0)
|
||
|
|
```
|
||
|
|
|
||
|
|
### **4. TRATAMENTO DE ERROS**
|
||
|
|
```typescript
|
||
|
|
try {
|
||
|
|
// Lógica principal
|
||
|
|
} catch (error: any) {
|
||
|
|
console.error('Erro detalhado:', error);
|
||
|
|
// Sempre reabilitar botões e mostrar feedback
|
||
|
|
} finally {
|
||
|
|
// Limpeza obrigatória
|
||
|
|
setIsRouting(false);
|
||
|
|
setRoutingProgress(0);
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## **📱 DEPENDÊNCIAS NECESSÁRIAS**
|
||
|
|
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"dependencies": {
|
||
|
|
"@react-native-async-storage/async-storage": "^1.19.0",
|
||
|
|
"expo-linear-gradient": "^12.0.0",
|
||
|
|
"@expo/vector-icons": "^13.0.0",
|
||
|
|
"react-native-safe-area-context": "^4.7.0"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## **🔗 ENDPOINTS DA API**
|
||
|
|
|
||
|
|
### **Base URL:** `https://api.entrega.homologacao.jurunense.com`
|
||
|
|
|
||
|
|
| Endpoint | Método | Descrição |
|
||
|
|
|----------|--------|-----------|
|
||
|
|
| `/v1/driver/deliveries` | GET | Carregar lista de entregas |
|
||
|
|
| `/v1/driver/routing` | POST | Enviar ordem de roteirização |
|
||
|
|
| `/v1/geolocation/google` | GET | Geocodificar endereços |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## **📊 ESTRUTURA DE DADOS**
|
||
|
|
|
||
|
|
### **Entrada da API (sendRoutingOrder)**
|
||
|
|
```json
|
||
|
|
[
|
||
|
|
{
|
||
|
|
"outId": 3673,
|
||
|
|
"customerId": 422973,
|
||
|
|
"deliverySeq": 1,
|
||
|
|
"lat": -1.3461972,
|
||
|
|
"lng": -48.3938122
|
||
|
|
}
|
||
|
|
]
|
||
|
|
```
|
||
|
|
|
||
|
|
### **Resposta da API**
|
||
|
|
```json
|
||
|
|
{
|
||
|
|
"success": true,
|
||
|
|
"message": "Roteirização de entrega atualizada com sucesso!",
|
||
|
|
"data": {
|
||
|
|
"message": "Roteirização de entrega atualizada com sucesso!"
|
||
|
|
},
|
||
|
|
"timestamp": "2025-08-18T14:50:34.618Z",
|
||
|
|
"statusCode": 201
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## **🎯 RESULTADO ESPERADO**
|
||
|
|
|
||
|
|
Após implementação, o sistema deve:
|
||
|
|
|
||
|
|
1. **Carregar entregas** automaticamente da API
|
||
|
|
2. **Otimizar rotas** usando algoritmo TSP (Nearest Neighbor)
|
||
|
|
3. **Enviar sequência** otimizada para API (`/v1/driver/routing`)
|
||
|
|
4. **Persistir dados** no banco de dados
|
||
|
|
5. **Redirecionar** para tela principal
|
||
|
|
6. **Manter ordem** das entregas em todas as telas
|
||
|
|
|
||
|
|
**Este sistema garante que as entregas sejam sempre ordenadas pela distância mais eficiente, começando do centro de distribuição e seguindo o algoritmo do caixeiro viajante! 🚚✨**
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## **📝 NOTAS IMPORTANTES**
|
||
|
|
|
||
|
|
- **URL da API**: `https://api.entrega.homologacao.jurunense.com`
|
||
|
|
- **Algoritmo**: Nearest Neighbor TSP
|
||
|
|
- **Sequência**: Sempre começa em 1 (não em 0)
|
||
|
|
- **Coordenadas**: Sempre normalizar (vírgula → ponto)
|
||
|
|
- **Ordem**: Otimizar ANTES de enviar para API
|
||
|
|
- **Tratamento de Erro**: Sempre implementar try-catch-finally
|