entregas_app/SISTEMA_ROTEIRIZACAO_TSP.md

34 KiB

🚚 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

// 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

// 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

// 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

// 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

// 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

// 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

// 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

// ❌ 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

// Sempre converter vírgula para ponto
const lat = parseFloat(coord.replace(',', '.'));

3. SEQUÊNCIA INICIAL

// 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

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

{
  "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)

[
  {
    "outId": 3673,
    "customerId": 422973,
    "deliverySeq": 1,
    "lat": -1.3461972,
    "lng": -48.3938122
  }
]

Resposta da API

{
  "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