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