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 Inicial | Coleta em Andamento |
|---|---|
![]() | ![]() |
| Tela inicial para iniciar o rastreamento | Indicador de coleta ativa com timer |
| Coleta Pausada | Coleta Finalizada |
|---|---|
![]() | ![]() |
| Estado pausado com opção de retomar | Confirmação de finalização da coleta |
Funcionalidades Principais
| Funcionalidade | Descrição |
|---|---|
| Rastreamento Contínuo | Coleta automática de localização GPS a cada 1 minuto |
| Background Tracking | Funciona mesmo com o aplicativo em segundo plano |
| Pausar/Retomar | Permite pausar e retomar a coleta sem perder dados |
| Persistência | Salva estado localmente para recuperação após fechamento do app |
| Cálculo Automático | Calcula distância percorrida entre pontos coletados |
| Sincronização | Envia 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)
| Endpoint | Método | Descrição |
|---|---|---|
/coletagps/iniciar | POST | Inicia nova coleta ou retoma existente |
/coletagps/atualizarColeta | POST | Atualiza localização durante coleta |
/coletagps/finalizar | POST | Finaliza coleta e calcula total |
/coletagps/listar | GET | Lista 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
| Arquivo | Localização | Responsabilidade |
|---|---|---|
GpsCollectionProvider.tsx | src/providers/ | Gerenciamento de estado global e lógica de rastreamento |
gps-collection-modal.tsx | src/components/gps-collection-modal/ | Interface visual do modal |
gps-collection.controller.ts | src/components/gps-collection-modal/ | Controller intermediário |
gps-collection.service.ts | src/components/gps-collection-modal/ | Comunicação com API |
gps-collection.styles.ts | src/components/gps-collection-modal/ | Estilos do componente |
profile.screen.tsx | src/screens/profile/ | Ponto de acesso ao modal |
routes.tsx | src/ | 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
| Estado | Botõ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ção | Tratamento |
|---|---|
| Permissão negada | Exibe alerta com opção de abrir configurações |
| GPS desligado | Solicita ativação via dialog nativo |
| Sem conexão | Coleta continua, sincroniza quando retornar |
| Coleta inativa | Limpa estado e retorna ao idle |
| Timeout de localização | Retry 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
-
Intervalo de Atualização: 1 minuto (60000ms)
- Balanceia precisão vs consumo de bateria
-
Refs para Valores Críticos:
const nroColetaRef = useRef<number | null>(null);
const codVendRef = useRef<string>("");- Evita re-renders desnecessários no timer
-
Cleanup Apropriado:
useEffect(() => {
return () => {
if (intervalRef.current) {
BackgroundTimer.clearInterval(intervalRef.current);
}
};
}, []); -
Memoização de Callbacks:
const stopLocationTracking = useCallback(() => {
// ...
}, []);
Testes
Casos de Teste Recomendados
- Iniciar e finalizar coleta normalmente
- Pausar e retomar coleta
- Fechar app durante coleta e reabrir
- Revogar permissão durante coleta
- Desligar GPS durante coleta
- Simular perda de conexão
- Múltiplas coletas no mesmo dia
- 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



