Pular para o conteúdo principal

Controle de KM por Vendedor

Ticket: FORCE-4173
Última atualização: 10 de outubro de 2025

Visão Geral

Sistema de coleta e rastreamento automático de quilometragem para vendedores em campo, permitindo o registro preciso de deslocamentos durante o horário de trabalho. A solução implementa rastreamento em background, persistência de dados offline e sincronização automática com o servidor.

Interface de Coleta de KM

O sistema apresenta uma interface intuitiva com diferentes estados visuais durante o processo de coleta:

Tela InicialColeta em Andamento
Tela Inicial - Coleta de KMColetando KM
Tela inicial para iniciar o rastreamentoIndicador de coleta ativa com timer
Coleta PausadaColeta Finalizada
Coleta PausadaColeta Finalizada
Estado pausado com opção de retomarConfirmação de finalização da coleta

Funcionalidades Principais

FuncionalidadeDescrição
Rastreamento ContínuoColeta automática de localização GPS a cada 1 minuto
Background TrackingFunciona mesmo com o aplicativo em segundo plano
Pausar/RetomarPermite pausar e retomar a coleta sem perder dados
PersistênciaSalva estado localmente para recuperação após fechamento do app
Cálculo AutomáticoCalcula distância percorrida entre pontos coletados
SincronizaçãoEnvia dados automaticamente para o servidor

Arquitetura

Estrutura de Componentes

src/
├── components/
│ └── gps-collection-modal/
│ ├── index.ts # Exports públicos
│ ├── gps-collection-modal.tsx # Componente visual
│ ├── gps-collection.controller.ts # Controller/lógica
│ ├── gps-collection.service.ts # Serviços API
│ └── gps-collection.styles.ts # Estilos
├── providers/
│ └── GpsCollectionProvider.tsx # Context/State global
└── screens/
└── profile/
└── profile.screen.tsx # Tela de acesso

Configuração de Campos

Campos na API (Backend)

EndpointMétodoDescrição
/coletagps/iniciarPOSTInicia nova coleta ou retoma existente
/coletagps/atualizarColetaPOSTAtualiza localização durante coleta
/coletagps/finalizarPOSTFinaliza coleta e calcula total
/coletagps/listarGETLista coletas por vendedor e período

Payload de Requisições

Iniciar Coleta

{
codVend: string; // Código do vendedor
nroColeta?: number; // Número da coleta (opcional, para retomar)
latitude: string; // Latitude inicial
longitude: string; // Longitude inicial
}

Atualizar Coleta

{
codVend: string; // Código do vendedor
nroColeta: number; // Número da coleta ativa
latitude: string; // Latitude atual
longitude: string; // Longitude atual
}

Finalizar Coleta

{
codVend: string; // Código do vendedor
nroColeta: number; // Número da coleta
latitude: string; // Latitude final
longitude: string; // Longitude final
cdMotivoColeta?: string; // Código do motivo (opcional)
}

Arquivos Implementados

ArquivoLocalizaçãoResponsabilidade
GpsCollectionProvider.tsxsrc/providers/Gerenciamento de estado global e lógica de rastreamento
gps-collection-modal.tsxsrc/components/gps-collection-modal/Interface visual do modal
gps-collection.controller.tssrc/components/gps-collection-modal/Controller intermediário
gps-collection.service.tssrc/components/gps-collection-modal/Comunicação com API
gps-collection.styles.tssrc/components/gps-collection-modal/Estilos do componente
profile.screen.tsxsrc/screens/profile/Ponto de acesso ao modal
routes.tsxsrc/Wrapper do Provider na árvore de componentes

Fluxo de Funcionamento

Diagrama de Estados

Fluxograma de Operação


Implementação Técnica

Provider Principal

GpsCollectionProvider - Estrutura
// src/providers/GpsCollectionProvider.tsx

// Estados possíveis
export type CollectionStatus = "idle" | "collecting" | "paused" | "finished";

// Interface do estado
export interface GpsCollectionState {
status: CollectionStatus;
nroColeta: number | null;
codVend: string;
distanciaTotal: number;
distanciaPercorrida: number;
startTime: number | null;
lastUpdateTime: number | null;
currentLatitude: string | null;
currentLongitude: string | null;
elapsedTime: number;
isLoading: boolean;
error: string | null;
}

// Context data
interface GpsCollectionContextData {
state: GpsCollectionState;
startCollection: (codVend: string) => Promise<void>;
pauseCollection: () => void;
resumeCollection: () => void;
finalizarCollection: (cdMotivoColeta?: string) => Promise<boolean>;
resetState: () => void;
formatElapsedTime: (milliseconds: number) => string;
}

Rastreamento em Background

O componente utiliza react-native-background-timer com fallback para timers nativos:

// Fallback seguro
let BackgroundTimer: any = {
setInterval: (callback: () => void, delay: number) =>
setInterval(callback, delay),
clearInterval: (id: number) => clearInterval(id),
};

try {
const bgTimer = require("react-native-background-timer");
if (bgTimer && typeof bgTimer.setInterval === "function") {
BackgroundTimer = bgTimer;
}
} catch (e) {
console.log("⚠️ BackgroundTimer não disponível, usando fallback");
}

Persistência de Dados

Utiliza MMKV Storage para persistência local:

const STORAGE_KEY = "gps_collection_data";

// Salvar estado
const saveState = (newState: Partial<GpsCollectionState>) => {
setState((prevState) => {
const updatedState = { ...prevState, ...newState };
MMKVStorage.update(STORAGE_KEY, updatedState);
return updatedState;
});
};

// Carregar estado salvo
const loadSavedState = () => {
const savedState = MMKVStorage.get<GpsCollectionState>(STORAGE_KEY);
if (savedState && savedState.status === "collecting") {
setState(savedState);
startLocationTracking();
}
};

Gerenciamento de AppState

Detecta quando o app volta do background e retoma tracking:

useEffect(() => {
const subscription = AppState.addEventListener("change", (nextAppState) => {
if (
appStateRef.current.match(/inactive|background/) &&
nextAppState === "active"
) {
console.log("📱 App voltou para foreground");
loadSavedState();
}
appStateRef.current = nextAppState;
});

return () => subscription.remove();
}, []);

Interface do Usuário

Estados Visuais

Estado Idle

// Tela inicial antes de iniciar coleta
<View style={styles.idleContainer}>
<View style={styles.idleIconWrapper}>
<Icon name="navigation" pack="material" />
</View>
<Text style={styles.idleTitle}>Coleta de KM</Text>
<Text style={styles.idleDescription}>
Pressione "Iniciar Coleta" para começar o rastreamento automático da sua
localização.
</Text>
</View>

Estado Collecting

// Indicador visual durante coleta
<View style={styles.statusContainer}>
<View style={[styles.statusIndicator, { backgroundColor: '#3CB778' }]}>
<Icon name="navigation" pack="material" fill="#fff" />
</View>
<Text style={styles.statusText}>Coletando</Text>
<Text style={styles.timerText}>
{formatElapsedTime(state.elapsedTime)}
</Text>
</View>

// Informações da coleta
<View style={styles.infoContainer}>
<View style={styles.infoCard}>
<Icon name="bookmark" />
<Text>Nº Coleta: {state.nroColeta}</Text>
</View>

<View style={styles.infoCard}>
<Icon name="map" />
<Text>Distância: {state.distanciaTotal.toFixed(2)} km</Text>
</View>

<View style={styles.locationCard}>
<Icon name="pin" />
<Text>{state.currentLatitude}, {state.currentLongitude}</Text>
</View>
</View>

Botões de Ação

EstadoBotões Disponíveis
Idle• Iniciar Coleta
Collecting• Pausar Coleta• Finalizar Coleta
Paused• Retomar Coleta• Finalizar Coleta
Finished• Fechar

Permissões Requeridas

iOS (Info.plist)

<key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
<string>Necessário para rastreamento de KM em background</string>
<key>NSLocationWhenInUseUsageDescription</key>
<string>Necessário para rastreamento de KM</string>
<key>UIBackgroundModes</key>
<array>
<string>location</string>
</array>

Android (AndroidManifest.xml)

<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

Solicitação Runtime

const requestLocationPermission = async () => {
if (Platform.OS === "ios") {
const result = await request(PERMISSIONS.IOS.LOCATION_ALWAYS);
return {
granted: result === RESULTS.GRANTED,
canOpenSettings: result === RESULTS.BLOCKED,
};
} else {
const result = await request(PERMISSIONS.ANDROID.ACCESS_FINE_LOCATION);
return {
granted: result === RESULTS.GRANTED,
canOpenSettings: result === RESULTS.BLOCKED,
};
}
};

Exemplos Práticos

Cenário 1: Iniciar Coleta

// 1. Usuário abre o modal
<GpsCollectionModal
visible={true}
codVend="001"
onClose={() => setVisible(false)}
/>

// 2. Provider solicita permissão
const permission = await requestLocationPermission();

// 3. Obtém localização inicial
const location = await getCurrentLocation();
// Retorna: { latitude: "-23.550520", longitude: "-46.633308" }

// 4. Envia para API
const response = await GpsCollectionService.iniciarColeta({
codVend: "001",
latitude: "-23.550520",
longitude: "-46.633308"
});

// 5. Resposta da API
{
data: {
nroColeta: 12345,
mensagem: "Coleta iniciada com sucesso",
tipo: "NOVA"
},
hasError: false
}

// 6. Inicia tracking
BackgroundTimer.setInterval(() => {
// Atualiza a cada 1 minuto
updateLocation();
}, 60000);

Cenário 2: Atualização Automática

// Executado a cada 1 minuto
const updateLocation = async () => {
// 1. Obter nova localização
const location = await getCurrentLocation();

// 2. Enviar para API
const response = await GpsCollectionService.atualizarColeta({
codVend: "001",
nroColeta: 12345,
latitude: "-23.551020",
longitude: "-46.634108"
});

// 3. Resposta com distância calculada
{
data: {
nroColeta: 12345,
distanciaPercorrida: 0.15, // km desde último ponto
distanciaTotal: 5.47, // km total da coleta
latitude: "-23.551020",
longitude: "-46.634108",
mensagem: "Coleta atualizada",
tipo: "ATUALIZADA"
},
hasError: false
}

// 4. Atualizar UI e storage
saveState({
currentLatitude: location.latitude,
currentLongitude: location.longitude,
distanciaPercorrida: 0.15,
distanciaTotal: 5.47,
lastUpdateTime: Date.now()
});
};

Cenário 3: Finalizar Coleta

// 1. Usuário clica em Finalizar
Alert.alert(
'Finalizar Coleta',
'Tem certeza que deseja finalizar a coleta?',
[
{ text: 'Cancelar', style: 'cancel' },
{
text: 'Finalizar',
onPress: async () => {
// 2. Obter localização final
const location = await getCurrentLocation();

// 3. Enviar para API
const response = await GpsCollectionService.finalizarColeta({
codVend: "001",
nroColeta: 12345,
latitude: "-23.552320",
longitude: "-46.635808"
});

// 4. Resposta final
{
data: {
nroColeta: 12345,
distanciaTotal: 8.73,
latitude: "-23.552320",
longitude: "-46.635808",
mensagem: "Coleta finalizada com sucesso"
},
hasError: false
}

// 5. Limpar tudo
stopLocationTracking();
MMKVStorage.deleteById(STORAGE_KEY);
setState(initialState);

// 6. Mostrar sucesso
Alert.alert('Sucesso', 'Coleta finalizada com sucesso!');
}
}
]
);

Tratamento de Erros

Cenários de Erro

SituaçãoTratamento
Permissão negadaExibe alerta com opção de abrir configurações
GPS desligadoSolicita ativação via dialog nativo
Sem conexãoColeta continua, sincroniza quando retornar
Coleta inativaLimpa estado e retorna ao idle
Timeout de localizaçãoRetry automático após 15s

Exemplo de Tratamento

try {
const location = await getCurrentLocation();
// ... processar
} catch (error) {
if (error.code === "PERMISSION_DENIED") {
Alert.alert(
"Permissão Negada",
"É necessário permitir acesso à localização",
[
{ text: "Cancelar", style: "cancel" },
{ text: "Abrir Configurações", onPress: () => openSettings() },
]
);
} else if (error.code === "TIMEOUT") {
// Retry automático
setTimeout(() => getCurrentLocation(), 5000);
}
}

Integração com Redux

Não utiliza Redux

Este componente utiliza React Context API em vez de Redux para manter o estado isolado e evitar poluir o store global.

// Provider
export const GpsCollectionProvider: React.FC = ({ children }) => {
const [state, setState] = useState<GpsCollectionState>(initialState);

return (
<GpsCollectionContext.Provider
value={{
state,
startCollection,
pauseCollection,
resumeCollection,
finalizarCollection,
resetState,
formatElapsedTime,
}}
>
{children}
</GpsCollectionContext.Provider>
);
};

// Hook de consumo
export const useGpsCollection = () => {
const context = useContext(GpsCollectionContext);
if (!context) {
throw new Error(
"useGpsCollection must be used within GpsCollectionProvider"
);
}
return context;
};

Dependências

Pacotes NPM

{
"react-native-geolocation-service": "^5.3.1",
"react-native-permissions": "^4.0.1",
"react-native-background-timer": "^2.4.1",
"react-native-mmkv": "^2.12.2"
}

Instalação iOS

cd ios && pod install

Configuração Android

Adicionar no android/app/build.gradle:

dependencies {
implementation project(':react-native-background-timer')
}

Logs e Debugging

Console Logs Estruturados

O componente utiliza emojis para facilitar identificação de logs:

console.log("🚀 Iniciando tracking em background...");
console.log("📍 Localização obtida:", location);
console.log("✅ Coleta iniciada - Nro:", nroColeta);
console.log("⏰ Executando atualização agendada...");
console.log("🔄 Atualizando localização...");
console.log("⏸️ Pausando coleta...");
console.log("▶️ Retomando coleta...");
console.log("🏁 Finalizando coleta...");
console.log("🛑 Parando tracking...");
console.log("🧹 Limpando coleta anterior...");
console.log("❌ Erro:", error);
console.log("⚠️ Atenção:", message);
console.log("📱 App voltou para foreground");
console.log("💾 Estado salvo:", state);

Performance

Otimizações Implementadas

  1. Intervalo de Atualização: 1 minuto (60000ms)

    • Balanceia precisão vs consumo de bateria
  2. Refs para Valores Críticos:

    const nroColetaRef = useRef<number | null>(null);
    const codVendRef = useRef<string>("");
    • Evita re-renders desnecessários no timer
  3. Cleanup Apropriado:

    useEffect(() => {
    return () => {
    if (intervalRef.current) {
    BackgroundTimer.clearInterval(intervalRef.current);
    }
    };
    }, []);
  4. Memoização de Callbacks:

    const stopLocationTracking = useCallback(() => {
    // ...
    }, []);

Testes

Casos de Teste Recomendados

  1. Iniciar e finalizar coleta normalmente
  2. Pausar e retomar coleta
  3. Fechar app durante coleta e reabrir
  4. Revogar permissão durante coleta
  5. Desligar GPS durante coleta
  6. Simular perda de conexão
  7. Múltiplas coletas no mesmo dia
  8. Limites de memória (coleta longa)

Simulação de Localização

iOS (Xcode)

Debug > Simulate Location > Custom Location
Latitude: -23.550520
Longitude: -46.633308

Android (ADB)

adb shell setprop debug.location.gps_location_provider.latitude -23.550520
adb shell setprop debug.location.gps_location_provider.longitude -46.633308