entregas_app/SISTEMA_ROTEIRIZACAO_TSP.md

1186 lines
34 KiB
Markdown
Raw Permalink Normal View History

# 🚚 **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