add SQLite and adjusts sistems

This commit is contained in:
Rayan Konecny 2026-05-03 01:16:25 -03:00
parent 960761984d
commit 2712fd6fa9
20 changed files with 1740 additions and 148 deletions

View file

@ -31,6 +31,7 @@ yarn-error.*
*.pem
# local env files
.env
.env*.local
# typescript

View file

@ -20,7 +20,10 @@
"output": "static",
"favicon": "./assets/toptranico.png"
},
"plugins": ["expo-router"],
"plugins": [
"expo-router",
"expo-secure-store"
],
"experiments": {
"typedRoutes": true
}

View file

@ -9,24 +9,23 @@
"version": "1.0.0",
"dependencies": {
"@expo/vector-icons": "^15.0.3",
"@react-navigation/native": "^7.1.8",
"@react-native-picker/picker": "^2.7.5",
"axios": "^1.15.2",
"expo": "~54.0.33",
"expo-constants": "~18.0.13",
"expo-font": "~14.0.11",
"expo-linking": "~8.0.11",
"expo-router": "~6.0.23",
"expo-secure-store": "~15.0.8",
"expo-splash-screen": "~31.0.13",
"expo-sqlite": "^55.0.15",
"expo-status-bar": "~3.0.9",
"expo-web-browser": "~15.0.10",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-web": "~0.21.0",
"react-native-worklets": "0.5.1"
"react-native-web": "~0.21.0"
},
"devDependencies": {
"@types/react": "~19.1.0",
@ -1363,21 +1362,6 @@
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-template-literals": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz",
"integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==",
"license": "MIT",
"dependencies": {
"@babel/helper-plugin-utils": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0"
}
},
"node_modules/@babel/plugin-transform-typescript": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz",
@ -2386,6 +2370,19 @@
}
}
},
"node_modules/@react-native-picker/picker": {
"version": "2.11.4",
"resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.4.tgz",
"integrity": "sha512-Kf8h1AMnBo54b1fdiVylP2P/iFcZqzpMYcglC28EEFB1DEnOjsNr6Ucqc+3R9e91vHxEDnhZFbYDmAe79P2gjA==",
"license": "MIT",
"workspaces": [
"example"
],
"peerDependencies": {
"react": "*",
"react-native": "*"
}
},
"node_modules/@react-native/assets-registry": {
"version": "0.81.5",
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz",
@ -3103,6 +3100,12 @@
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
"license": "MIT"
},
"node_modules/await-lock": {
"version": "2.2.2",
"resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz",
"integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==",
"license": "MIT"
},
"node_modules/axios": {
"version": "1.15.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz",
@ -4607,6 +4610,15 @@
"node": ">=10"
}
},
"node_modules/expo-secure-store": {
"version": "15.0.8",
"resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-15.0.8.tgz",
"integrity": "sha512-lHnzvRajBu4u+P99+0GEMijQMFCOYpWRO4dWsXSuMt77+THPIGjzNvVKrGSl6mMrLsfVaKL8BpwYZLGlgA+zAw==",
"license": "MIT",
"peerDependencies": {
"expo": "*"
}
},
"node_modules/expo-server": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.6.tgz",
@ -4628,6 +4640,20 @@
"expo": "*"
}
},
"node_modules/expo-sqlite": {
"version": "55.0.15",
"resolved": "https://registry.npmjs.org/expo-sqlite/-/expo-sqlite-55.0.15.tgz",
"integrity": "sha512-vxE5fs6l953QSIyievQ8TuSstj62eC7zUREjNzbUOwRWaHGGnhnlPJM1HLoTIv+oIt3+b1m7k2fmcDGkpK5t3w==",
"license": "MIT",
"dependencies": {
"await-lock": "^2.2.2"
},
"peerDependencies": {
"expo": "*",
"react": "*",
"react-native": "*"
}
},
"node_modules/expo-status-bar": {
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-3.0.9.tgz",
@ -4641,16 +4667,6 @@
"react-native": "*"
}
},
"node_modules/expo-web-browser": {
"version": "15.0.11",
"resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-15.0.11.tgz",
"integrity": "sha512-r2LS4Ro6DgUPZkcaEfgt8mp9eJuoA93x11Jh7S6utFe0FEzvUNn2yFhxg8XVwESaaHGt2k5V8LuK36rsp0BeIw==",
"license": "MIT",
"peerDependencies": {
"expo": "*",
"react-native": "*"
}
},
"node_modules/expo/node_modules/@expo/cli": {
"version": "54.0.24",
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.24.tgz",
@ -7450,33 +7466,6 @@
"react-native": "*"
}
},
"node_modules/react-native-reanimated": {
"version": "4.1.7",
"resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.7.tgz",
"integrity": "sha512-Q4H6xA3Tn7QL0/E/KjI86I1KK4tcf+ErRE04LH34Etka2oVQhW6oXQ+Q8ZcDCVxiWp5vgbBH6XcH8BOo4w/Rhg==",
"license": "MIT",
"dependencies": {
"react-native-is-edge-to-edge": "^1.2.1",
"semver": "^7.7.2"
},
"peerDependencies": {
"react": "*",
"react-native": "0.78 - 0.82",
"react-native-worklets": "0.5 - 0.8"
}
},
"node_modules/react-native-reanimated/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/react-native-safe-area-context": {
"version": "5.6.2",
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
@ -7534,42 +7523,6 @@
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/react-native-worklets": {
"version": "0.5.1",
"resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz",
"integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==",
"license": "MIT",
"dependencies": {
"@babel/plugin-transform-arrow-functions": "^7.0.0-0",
"@babel/plugin-transform-class-properties": "^7.0.0-0",
"@babel/plugin-transform-classes": "^7.0.0-0",
"@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0",
"@babel/plugin-transform-optional-chaining": "^7.0.0-0",
"@babel/plugin-transform-shorthand-properties": "^7.0.0-0",
"@babel/plugin-transform-template-literals": "^7.0.0-0",
"@babel/plugin-transform-unicode-regex": "^7.0.0-0",
"@babel/preset-typescript": "^7.16.7",
"convert-source-map": "^2.0.0",
"semver": "7.7.2"
},
"peerDependencies": {
"@babel/core": "^7.0.0-0",
"react": "*",
"react-native": "*"
}
},
"node_modules/react-native-worklets/node_modules/semver": {
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
},
"node_modules/react-native/node_modules/@react-native/virtualized-lists": {
"version": "0.81.5",
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz",

View file

@ -10,24 +10,23 @@
},
"dependencies": {
"@expo/vector-icons": "^15.0.3",
"@react-navigation/native": "^7.1.8",
"@react-native-picker/picker": "^2.7.5",
"axios": "^1.15.2",
"expo": "~54.0.33",
"expo-constants": "~18.0.13",
"expo-font": "~14.0.11",
"expo-linking": "~8.0.11",
"expo-router": "~6.0.23",
"expo-secure-store": "~15.0.8",
"expo-splash-screen": "~31.0.13",
"expo-sqlite": "^55.0.15",
"expo-status-bar": "~3.0.9",
"expo-web-browser": "~15.0.10",
"react": "19.1.0",
"react-dom": "19.1.0",
"react-native": "0.81.5",
"react-native-reanimated": "~4.1.1",
"react-native-safe-area-context": "~5.6.0",
"react-native-screens": "~4.16.0",
"react-native-web": "~0.21.0",
"react-native-worklets": "0.5.1"
"react-native-web": "~0.21.0"
},
"devDependencies": {
"@types/react": "~19.1.0",

View file

@ -1,7 +1,20 @@
import { AuthProvider } from "@/contexts/AuthContext";
import { initDB } from "@/services/db";
import { Stack } from "expo-router";
import React, { useEffect } from "react";
import { Alert } from "react-native";
function RootLayoutNav() {
useEffect(() => {
initDB().catch((error) => {
console.error("Database initialization failed:", error);
Alert.alert(
"Erro",
"Falha ao inicializar o banco de dados. Alguns recursos podem não funcionar corretamente.",
);
});
}, []);
// Layout para as páginas de login e cadastro de usuário, sem header
export default function Layout() {
return (
<Stack
screenOptions={{
@ -10,3 +23,11 @@ export default function Layout() {
/>
);
}
export default function Layout() {
return (
<AuthProvider>
<RootLayoutNav />
</AuthProvider>
);
}

View file

@ -0,0 +1,121 @@
import { useAuth } from "@/contexts/AuthContext";
import { obterCorridas } from "@/services/db";
import React, { useCallback, useEffect, useState } from "react";
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import Historico from "./historico";
import Lancamento, { type Corrida } from "./lancamento";
export default function Corrida() {
const { user } = useAuth();
const [activeTab, setActiveTab] = useState<"lancamento" | "historico">(
"lancamento",
);
const [historico, setHistorico] = useState<Corrida[]>([]);
const loadHistorico = useCallback(async () => {
if (!user?.id) return;
try {
const corridasDB = await obterCorridas(user.id);
setHistorico(
corridasDB.map((c) => ({
id: c.id,
data: c.data,
empresa: c.empresa,
km: c.km,
custoPorKm: c.custo_por_km,
total: c.total,
usuario: user.email,
})),
);
} catch (error) {
console.error("Erro ao carregar histórico:", error);
}
}, [user?.id, user?.email]);
useEffect(() => {
loadHistorico();
}, [loadHistorico]);
const handleLancarCorrida = async (_corrida: Corrida) => {
await loadHistorico();
setActiveTab("historico");
};
return (
<SafeAreaView style={styles.container}>
<View style={styles.tabsContainer}>
<TouchableOpacity
style={[styles.tab, activeTab === "lancamento" && styles.tabActive]}
onPress={() => setActiveTab("lancamento")}
>
<Text
style={[
styles.tabLabel,
activeTab === "lancamento" && styles.tabLabelActive,
]}
>
📍 Lançar
</Text>
</TouchableOpacity>
<TouchableOpacity
style={[styles.tab, activeTab === "historico" && styles.tabActive]}
onPress={() => setActiveTab("historico")}
>
<Text
style={[
styles.tabLabel,
activeTab === "historico" && styles.tabLabelActive,
]}
>
📋 Histórico
</Text>
</TouchableOpacity>
</View>
<View style={styles.content}>
{activeTab === "lancamento" ? (
<Lancamento onLancar={handleLancarCorrida} />
) : (
<Historico historico={historico} />
)}
</View>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#ffffff",
},
tabsContainer: {
flexDirection: "row",
backgroundColor: "#ffffff",
borderBottomWidth: 2,
borderBottomColor: "#000000",
},
tab: {
flex: 1,
paddingVertical: 16,
justifyContent: "center",
alignItems: "center",
borderBottomWidth: 3,
borderBottomColor: "transparent",
},
tabActive: {
borderBottomColor: "#000000",
},
tabLabel: {
fontSize: 14,
fontWeight: "600",
color: "#666666",
},
tabLabelActive: {
color: "#000000",
},
content: {
flex: 1,
},
});

View file

@ -0,0 +1,191 @@
import React from "react";
import { FlatList, ScrollView, StyleSheet, Text, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
export type Corrida = {
id: string;
data: string;
empresa: string;
km: number;
custoPorKm: number;
total: number;
usuario: string;
};
interface HistoricoProps {
historico: Corrida[];
}
const HistoricoItem = ({ item, index }: { item: Corrida; index: number }) => (
<View style={styles.item}>
<View style={styles.itemHeader}>
<Text style={styles.itemNumber}>#{index + 1}</Text>
<Text style={styles.itemDate}>{item.data}</Text>
</View>
<View style={styles.itemEmpresa}>
<Text style={styles.empresaLabel}>{item.empresa}</Text>
</View>
<View style={styles.itemDetails}>
<View style={styles.detailBox}>
<Text style={styles.detailLabel}>Distância</Text>
<Text style={styles.detailValue}>{item.km} km</Text>
</View>
<View style={styles.detailBox}>
<Text style={styles.detailLabel}>Valor/km</Text>
<Text style={styles.detailValue}>R$ {item.custoPorKm.toFixed(2)}</Text>
</View>
<View style={styles.detailBox}>
<Text style={styles.detailLabel}>Total</Text>
<Text style={styles.detailValue}>R$ {item.total.toFixed(2)}</Text>
</View>
</View>
</View>
);
export default function Historico({ historico }: HistoricoProps) {
return (
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<Text style={styles.title}>Histórico de Corridas</Text>
<Text style={styles.subtitle}>
{historico.length} corrida{historico.length !== 1 ? "s" : ""}
</Text>
</View>
{historico.length === 0 ? (
<ScrollView contentContainerStyle={styles.emptyContainer}>
<View style={styles.emptyBox}>
<Text style={styles.emptyEmoji}>🚗</Text>
<Text style={styles.emptyTitle}>Nenhuma corrida registrada</Text>
<Text style={styles.emptyText}>Suas corridas aparecerão aqui</Text>
</View>
</ScrollView>
) : (
<FlatList
data={historico}
keyExtractor={(item) => item.id}
renderItem={({ item, index }) => (
<HistoricoItem item={item} index={index} />
)}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
/>
)}
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#ffffff",
},
header: {
paddingHorizontal: 16,
paddingTop: 16,
paddingBottom: 24,
backgroundColor: "#ffffff",
borderBottomWidth: 2,
borderBottomColor: "#000000",
},
title: {
fontSize: 28,
fontWeight: "800",
color: "#000000",
marginBottom: 4,
},
subtitle: {
fontSize: 14,
color: "#333333",
},
listContent: {
padding: 16,
paddingBottom: 32,
},
item: {
backgroundColor: "#ffffff",
borderRadius: 12,
marginBottom: 12,
overflow: "hidden",
borderWidth: 1,
borderColor: "#000000",
},
itemHeader: {
flexDirection: "row",
paddingHorizontal: 16,
paddingTop: 12,
paddingBottom: 8,
justifyContent: "space-between",
alignItems: "center",
borderBottomWidth: 1,
borderBottomColor: "#e0e0e0",
},
itemNumber: {
fontSize: 12,
fontWeight: "700",
color: "#000000",
},
itemDate: {
fontSize: 12,
color: "#666666",
},
itemEmpresa: {
paddingHorizontal: 16,
paddingVertical: 10,
backgroundColor: "#f5f5f5",
},
empresaLabel: {
fontSize: 14,
fontWeight: "600",
color: "#000000",
},
itemDetails: {
flexDirection: "row",
paddingHorizontal: 16,
paddingVertical: 12,
gap: 8,
},
detailBox: {
flex: 1,
paddingVertical: 8,
},
detailLabel: {
fontSize: 11,
color: "#666666",
fontWeight: "500",
marginBottom: 2,
},
detailValue: {
fontSize: 14,
fontWeight: "700",
color: "#000000",
},
emptyContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: 16,
},
emptyBox: {
alignItems: "center",
},
emptyEmoji: {
fontSize: 48,
marginBottom: 16,
},
emptyTitle: {
fontSize: 18,
fontWeight: "600",
color: "#000000",
marginBottom: 8,
},
emptyText: {
fontSize: 14,
color: "#666666",
textAlign: "center",
},
});

View file

@ -0,0 +1,387 @@
import { BORDER_RADIUS, COLORS, SPACING } from "@/constants/theme";
import { useAuth } from "@/contexts/AuthContext";
import { obterCorridas } from "@/services/db";
import { router, useFocusEffect } from "expo-router";
import React, { useCallback, useState } from "react";
import {
Alert,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
type Stats = {
totalCorridas: number;
totalKm: number;
totalGanhos: number;
};
type Action = {
icon: string;
label: string;
description: string;
onPress: () => void;
highlight?: boolean;
};
function greeting() {
const h = new Date().getHours();
if (h < 12) return "Bom dia";
if (h < 18) return "Boa tarde";
return "Boa noite";
}
export default function Home() {
const { user, logout } = useAuth();
const userName = user?.name ?? "Usuário";
const firstName = userName.split(" ")[0];
const [stats, setStats] = useState<Stats>({
totalCorridas: 0,
totalKm: 0,
totalGanhos: 0,
});
const loadStats = useCallback(async () => {
if (!user?.id) return;
try {
const corridas = await obterCorridas(user.id);
setStats({
totalCorridas: corridas.length,
totalKm: corridas.reduce((acc, c) => acc + c.km, 0),
totalGanhos: corridas.reduce((acc, c) => acc + c.total, 0),
});
} catch (error) {
console.error("Erro ao carregar estatísticas:", error);
}
}, [user?.id]);
useFocusEffect(useCallback(() => { loadStats(); }, [loadStats]));
const handleProfilePress = () => {
Alert.alert(userName, user?.email ?? "", [
{
text: "Editar Perfil",
onPress: () => Alert.alert("Em breve", "Funcionalidade em desenvolvimento."),
},
{
text: "Sair",
style: "destructive",
onPress: () =>
Alert.alert("Sair", "Tem certeza que deseja sair?", [
{ text: "Cancelar", style: "cancel" },
{
text: "Sair",
style: "destructive",
onPress: async () => {
try {
await logout();
} catch (e) {
console.error("Logout error:", e);
}
},
},
]),
},
{ text: "Cancelar", style: "cancel" },
]);
};
const today = new Date().toLocaleDateString("pt-BR", {
weekday: "long",
day: "numeric",
month: "long",
});
const actions: Action[] = [
{
icon: "🚗",
label: "Registrar Corrida",
description: "Lançar nova corrida",
onPress: () => router.push("/corrida"),
highlight: true,
},
{
icon: "📋",
label: "Histórico",
description: "Ver corridas anteriores",
onPress: () => router.push("/corrida"),
},
{
icon: "📊",
label: "Relatório",
description: "Análise de ganhos",
onPress: () => Alert.alert("Em breve", "Funcionalidade em desenvolvimento."),
},
{
icon: "⚙️",
label: "Configurações",
description: "Preferências do app",
onPress: () => Alert.alert("Em breve", "Funcionalidade em desenvolvimento."),
},
];
return (
<SafeAreaView style={styles.container}>
{/* Header */}
<View style={styles.header}>
<View>
<Text style={styles.headerLogo}>TopTran</Text>
<Text style={styles.headerSub}>Sistema de Gestão</Text>
</View>
<TouchableOpacity style={styles.avatarButton} onPress={handleProfilePress} activeOpacity={0.8}>
<View style={styles.avatar}>
<Text style={styles.avatarText}>
{firstName.charAt(0).toUpperCase()}
</Text>
</View>
<View style={styles.avatarOnline} />
</TouchableOpacity>
</View>
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
{/* Greeting */}
<View style={styles.greetingSection}>
<Text style={styles.greetingText}>
{greeting()}, {firstName}! 👋
</Text>
<Text style={styles.dateText}>{today}</Text>
</View>
{/* Stats */}
<Text style={styles.sectionTitle}>Resumo Geral</Text>
<View style={styles.statsRow}>
<View style={styles.statCard}>
<Text style={styles.statValue}>{stats.totalCorridas}</Text>
<Text style={styles.statLabel}>Corridas</Text>
</View>
<View style={styles.statCard}>
<Text style={styles.statValue}>
{stats.totalKm.toFixed(0)}
</Text>
<Text style={styles.statLabel}>Km Rodados</Text>
</View>
<View style={[styles.statCard, styles.statCardAccent]}>
<Text style={[styles.statValue, styles.statValueAccent]}>
R${stats.totalGanhos.toFixed(0)}
</Text>
<Text style={[styles.statLabel, styles.statLabelAccent]}>
Total Ganho
</Text>
</View>
</View>
{/* Actions Grid */}
<Text style={styles.sectionTitle}>Acesso Rápido</Text>
<View style={styles.actionsGrid}>
{actions.map((action, index) => (
<TouchableOpacity
key={index}
style={[
styles.actionCard,
action.highlight && styles.actionCardHighlight,
]}
onPress={action.onPress}
activeOpacity={0.75}
>
<Text style={styles.actionIcon}>{action.icon}</Text>
<Text
style={[
styles.actionLabel,
action.highlight && styles.actionLabelHighlight,
]}
>
{action.label}
</Text>
<Text style={styles.actionDescription}>{action.description}</Text>
</TouchableOpacity>
))}
</View>
<Text style={styles.footer}>© 2025 TopTran</Text>
</ScrollView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: COLORS.background,
},
// Header
header: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: SPACING.lg,
paddingVertical: SPACING.md,
borderBottomWidth: 1,
borderBottomColor: COLORS.border,
},
headerLogo: {
fontSize: 22,
fontWeight: "900",
color: COLORS.text,
letterSpacing: 1,
},
headerSub: {
fontSize: 11,
color: COLORS.textTertiary,
letterSpacing: 0.5,
},
avatarButton: {
position: "relative",
},
avatar: {
width: 42,
height: 42,
borderRadius: BORDER_RADIUS.round,
backgroundColor: COLORS.surfaceLight,
borderWidth: 2,
borderColor: COLORS.borderLight,
justifyContent: "center",
alignItems: "center",
},
avatarText: {
color: COLORS.text,
fontSize: 18,
fontWeight: "700",
},
avatarOnline: {
position: "absolute",
bottom: 1,
right: 1,
width: 10,
height: 10,
borderRadius: 5,
backgroundColor: COLORS.success,
borderWidth: 2,
borderColor: COLORS.background,
},
// Scroll
scrollContent: {
padding: SPACING.lg,
paddingBottom: SPACING.xxl,
},
// Greeting
greetingSection: {
marginTop: SPACING.md,
marginBottom: SPACING.xl,
},
greetingText: {
fontSize: 26,
fontWeight: "800",
color: COLORS.text,
marginBottom: 4,
},
dateText: {
fontSize: 13,
color: COLORS.textTertiary,
textTransform: "capitalize",
},
// Section title
sectionTitle: {
fontSize: 12,
fontWeight: "700",
color: COLORS.textTertiary,
textTransform: "uppercase",
letterSpacing: 1.2,
marginBottom: SPACING.md,
},
// Stats
statsRow: {
flexDirection: "row",
gap: SPACING.sm,
marginBottom: SPACING.xl,
},
statCard: {
flex: 1,
backgroundColor: COLORS.surface,
borderRadius: BORDER_RADIUS.lg,
padding: SPACING.md,
borderWidth: 1,
borderColor: COLORS.border,
},
statCardAccent: {
backgroundColor: COLORS.surfaceLight,
borderColor: COLORS.borderLight,
},
statValue: {
fontSize: 20,
fontWeight: "800",
color: COLORS.text,
marginBottom: 2,
},
statValueAccent: {
color: COLORS.success,
},
statLabel: {
fontSize: 10,
color: COLORS.textTertiary,
fontWeight: "600",
textTransform: "uppercase",
letterSpacing: 0.5,
},
statLabelAccent: {
color: COLORS.textSecondary,
},
// Actions grid
actionsGrid: {
flexDirection: "row",
flexWrap: "wrap",
gap: SPACING.sm,
marginBottom: SPACING.xl,
},
actionCard: {
width: "48%",
backgroundColor: COLORS.surface,
borderRadius: BORDER_RADIUS.lg,
padding: SPACING.lg,
borderWidth: 1,
borderColor: COLORS.border,
minHeight: 110,
justifyContent: "space-between",
},
actionCardHighlight: {
backgroundColor: COLORS.surfaceLight,
borderColor: COLORS.text,
},
actionIcon: {
fontSize: 28,
marginBottom: SPACING.sm,
},
actionLabel: {
fontSize: 14,
fontWeight: "700",
color: COLORS.text,
marginBottom: 2,
},
actionLabelHighlight: {
color: COLORS.text,
},
actionDescription: {
fontSize: 11,
color: COLORS.textTertiary,
},
// Footer
footer: {
textAlign: "center",
fontSize: 12,
color: COLORS.textTertiary,
marginTop: SPACING.sm,
},
});

View file

@ -1,4 +1,4 @@
import { useState } from "react";
import React, { useState } from "react";
import {
Alert,
@ -13,28 +13,71 @@ import {
import { Button } from "@/components/Button";
import { Input } from "@/components/Input";
import { COLORS, SPACING } from "@/constants/theme";
import { useAuth } from "@/contexts/AuthContext";
import { api, setAuthToken } from "@/server/api";
import { salvarUsuario } from "@/services/db";
import { Link } from "expo-router";
export default function IndexPage() {
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
const [loading, setLoading] = useState(false);
const { login } = useAuth();
function isValidEmail(emailToCheck: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(emailToCheck);
}
async function handleSignIn() {
if (!email.trim() || !password.trim()) {
const emailTrimmed = email.trim();
const passwordTrimmed = password.trim();
if (!emailTrimmed || !passwordTrimmed) {
Alert.alert("Entrar", "Por favor, preencha todos os campos.");
return;
}
if (!isValidEmail(emailTrimmed)) {
Alert.alert(
"Email inválido",
"Por favor, digite um email válido (ex: usuario@email.com).",
);
return;
}
if (passwordTrimmed.length < 6) {
Alert.alert("Senha fraca", "A senha deve ter pelo menos 6 caracteres.");
return;
}
try {
setLoading(true);
const { data } = await api.post("/auth/login", { email, password });
const { data } = await api.post("/auth/login", {
email: emailTrimmed,
password: passwordTrimmed,
});
console.log(data.accessToken);
setAuthToken(data.accessToken);
// Salvar no SQLite
const userId = Date.now().toString();
await salvarUsuario({
id: userId,
email: emailTrimmed,
name: data.name || emailTrimmed.split("@")[0],
token: data.accessToken,
});
// Fazer login no contexto
await login(emailTrimmed, passwordTrimmed, data.accessToken, {
id: userId,
email: emailTrimmed,
name: data.name || emailTrimmed.split("@")[0],
});
} catch (err: any) {
const message = err.response?.data?.error ?? "Erro ao fazer login.";
const message = err.response?.data?.error ?? err.message ?? "Erro ao fazer login.";
Alert.alert("Erro", message);
} finally {
setLoading(false);
@ -98,33 +141,33 @@ export default function IndexPage() {
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#0e0d0d",
padding: 32,
backgroundColor: COLORS.background,
padding: SPACING.xxl,
},
illustration: {
width: "100%",
height: 330,
resizeMode: "contain",
marginTop: 62,
marginTop: SPACING.xxl,
},
title: {
marginTop: 62,
marginTop: SPACING.xxl,
fontSize: 32,
fontWeight: "900",
color: "#e7e7e7",
color: COLORS.text,
},
subtitle: {
fontSize: 16,
color: "#a1a1a1",
color: COLORS.textSecondary,
},
form: {
marginTop: 24,
gap: 16,
marginTop: SPACING.lg,
gap: SPACING.lg,
},
footerText: {
textAlign: "center",
marginTop: 24,
color: "#a1a1a1",
marginTop: SPACING.lg,
color: COLORS.textSecondary,
},
footerLink: {
color: "#007AFF",

View file

@ -0,0 +1,222 @@
import { Button } from "@/components/Button";
import { Input } from "@/components/Input";
import { Select } from "@/components/Select";
import { useAuth } from "@/contexts/AuthContext";
import { salvarCorrida } from "@/services/db";
import React, { useState } from "react";
import {
Alert,
KeyboardAvoidingView,
Platform,
ScrollView,
StyleSheet,
Text,
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
const EMPRESAS = [
{ label: "Selecione uma empresa", value: "" },
{ label: "MULTI B", value: "MULTI_B" },
{ label: "TOP TRANS", value: "TOP_TRANS" },
{ label: "LOGISTICA XYZ", value: "LOGISTICA_XYZ" },
];
const CUSTO_POR_KM = 2.7;
export type Corrida = {
id: string;
data: string;
empresa: string;
km: number;
custoPorKm: number;
total: number;
usuario: string;
};
interface LancamentoProps {
onLancar: (corrida: Corrida) => void;
}
export default function Lancamento({ onLancar }: LancamentoProps) {
const { user } = useAuth();
const [empresa, setEmpresa] = useState("");
const [distancia, setDistancia] = useState("");
const distanciaNum = parseFloat(distancia) || 0;
const totalCorrida = distanciaNum * CUSTO_POR_KM;
const handleLancarCorrida = async () => {
if (!empresa) {
Alert.alert("Validação", "Por favor, selecione uma empresa.");
return;
}
if (!distancia || distanciaNum <= 0) {
Alert.alert("Validação", "Por favor, insira uma distância válida.");
return;
}
const empresaSelecionada = EMPRESAS.find((e) => e.value === empresa);
const novaCorrida: Corrida = {
id: Date.now().toString(),
data: new Date().toLocaleString("pt-BR"),
empresa: empresaSelecionada?.label || empresa,
km: distanciaNum,
custoPorKm: CUSTO_POR_KM,
total: totalCorrida,
usuario: user?.email ?? "",
};
try {
await salvarCorrida({
id: novaCorrida.id,
usuario_id: user?.id ?? "",
empresa: novaCorrida.empresa,
km: novaCorrida.km,
custo_por_km: novaCorrida.custoPorKm,
total: novaCorrida.total,
data: novaCorrida.data,
sincronizado: 0,
});
} catch (error) {
console.error("Erro ao salvar corrida:", error);
}
onLancar(novaCorrida);
setEmpresa("");
setDistancia("");
Alert.alert("Sucesso", "Corrida registrada com sucesso!");
};
return (
<SafeAreaView style={styles.container}>
<KeyboardAvoidingView
style={{ flex: 1 }}
behavior={Platform.select({ ios: "padding", android: "height" })}
>
<ScrollView
contentContainerStyle={styles.scrollContent}
showsVerticalScrollIndicator={false}
>
<View style={styles.header}>
<Text style={styles.title}>Nova Corrida</Text>
<Text style={styles.subtitle}>Preencha os dados da corrida</Text>
</View>
<View style={styles.card}>
<Text style={styles.cardLabel}>Empresa</Text>
<Select
value={empresa}
onValueChange={setEmpresa}
items={EMPRESAS}
/>
</View>
<View style={styles.card}>
<Text style={styles.cardLabel}>Distância (km)</Text>
<Input
placeholder="Digite a distância"
keyboardType="decimal-pad"
value={distancia}
onChangeText={setDistancia}
/>
</View>
<View style={styles.card}>
<View style={styles.valorRow}>
<Text style={styles.valorLabel}>Custo por km</Text>
<Text style={styles.valorAmount}>
R$ {CUSTO_POR_KM.toFixed(2)}
</Text>
</View>
</View>
<View style={styles.card}>
<View style={styles.totalBox}>
<Text style={styles.totalLabel}>Valor Total</Text>
<Text style={styles.totalAmount}>
R$ {totalCorrida.toFixed(2)}
</Text>
</View>
</View>
<Button label="Lançar Corrida" onPress={handleLancarCorrida} />
</ScrollView>
</KeyboardAvoidingView>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#ffffff",
},
scrollContent: {
padding: 16,
paddingBottom: 32,
},
header: {
marginBottom: 24,
},
title: {
fontSize: 28,
fontWeight: "800",
color: "#000000",
marginBottom: 4,
},
subtitle: {
fontSize: 14,
color: "#333333",
},
card: {
backgroundColor: "#ffffff",
borderRadius: 12,
padding: 16,
marginBottom: 12,
borderWidth: 1,
borderColor: "#000000",
},
cardLabel: {
fontSize: 12,
fontWeight: "600",
color: "#000000",
marginBottom: 8,
textTransform: "uppercase",
letterSpacing: 0.5,
},
valorRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
valorLabel: {
fontSize: 14,
color: "#000000",
fontWeight: "500",
},
valorAmount: {
fontSize: 16,
fontWeight: "700",
color: "#000000",
},
totalBox: {
backgroundColor: "#ffffff",
borderLeftWidth: 4,
borderLeftColor: "#000000",
padding: 12,
borderRadius: 8,
},
totalLabel: {
fontSize: 12,
color: "#000000",
marginBottom: 4,
fontWeight: "500",
},
totalAmount: {
fontSize: 24,
fontWeight: "800",
color: "#000000",
},
});

View file

@ -1,4 +1,4 @@
import { useState } from "react";
import React, { useState } from "react";
import {
Alert,
@ -13,11 +13,17 @@ import {
import { Button } from "@/components/Button";
import { Input } from "@/components/Input";
import { api } from "@/server/api";
import { Link, useRouter } from "expo-router";
import { useAuth } from "@/contexts/AuthContext";
import { api, setAuthToken } from "@/server/api";
import { salvarUsuario } from "@/services/db";
import { Link } from "expo-router";
function isValidEmail(emailToCheck: string): boolean {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(emailToCheck);
}
export default function Signup() {
const router = useRouter();
const { signup } = useAuth();
const [name, setName] = useState("");
const [email, setEmail] = useState("");
const [password, setPassword] = useState("");
@ -25,16 +31,24 @@ export default function Signup() {
const [loading, setLoading] = useState(false);
async function handleSignUp() {
if (
!name.trim() ||
!email.trim() ||
!password.trim() ||
!confirmPassword.trim()
) {
const nameTrimmed = name.trim();
const emailTrimmed = email.trim();
if (!nameTrimmed || !emailTrimmed || !password.trim() || !confirmPassword.trim()) {
Alert.alert("Cadastrar", "Por favor, preencha todos os campos.");
return;
}
if (!isValidEmail(emailTrimmed)) {
Alert.alert("Email inválido", "Por favor, digite um email válido (ex: usuario@email.com).");
return;
}
if (password.length < 6) {
Alert.alert("Senha fraca", "A senha deve ter pelo menos 6 caracteres.");
return;
}
if (password !== confirmPassword) {
Alert.alert("Cadastrar", "As senhas não coincidem.");
return;
@ -42,12 +56,25 @@ export default function Signup() {
try {
setLoading(true);
await api.post("/auth/register", { name, email, password });
Alert.alert("Sucesso", "Conta criada com sucesso!", [
{ text: "OK", onPress: () => router.replace("/") },
]);
const { data } = await api.post("/auth/register", {
name: nameTrimmed,
email: emailTrimmed,
password,
});
setAuthToken(data.accessToken);
const userId = Date.now().toString();
await salvarUsuario({
id: userId,
email: emailTrimmed,
name: nameTrimmed,
token: data.accessToken,
});
await signup(nameTrimmed, emailTrimmed, password, data.accessToken);
} catch (err: any) {
const message = err.response?.data?.error ?? "Erro ao criar conta.";
const message = err.response?.data?.error ?? err.message ?? "Erro ao criar conta.";
Alert.alert("Erro", message);
} finally {
setLoading(false);

View file

@ -1,19 +1,28 @@
import React from "react";
import {
StyleSheet,
Text,
TouchableOpacity,
TouchableOpacityProps,
} from "react-native";
import { COLORS, SPACING, BORDER_RADIUS } from "@/constants/theme";
type ButtonProps = TouchableOpacityProps & {
label: string;
disabled?: boolean;
};
// Componente de botão personalizado
export function Button({ label, ...rest }: ButtonProps) {
export function Button({ label, disabled = false, ...rest }: ButtonProps) {
return (
<TouchableOpacity style={styles.container} activeOpacity={0.7} {...rest}>
<Text style={styles.label}>{label}</Text>
<TouchableOpacity
style={[styles.container, disabled && styles.containerDisabled]}
activeOpacity={disabled ? 1 : 0.7}
disabled={disabled}
{...rest}
>
<Text style={[styles.label, disabled && styles.labelDisabled]}>
{label}
</Text>
</TouchableOpacity>
);
}
@ -22,14 +31,21 @@ const styles = StyleSheet.create({
container: {
width: "100%",
height: 48,
backgroundColor: "#a19f9f",
borderRadius: 8,
backgroundColor: COLORS.buttonBackground,
borderRadius: BORDER_RADIUS.md,
alignItems: "center",
justifyContent: "center",
marginTop: SPACING.lg,
},
containerDisabled: {
backgroundColor: COLORS.buttonDisabledBackground,
},
label: {
color: "#050505",
color: COLORS.buttonText,
fontSize: 16,
fontWeight: "600",
},
labelDisabled: {
color: COLORS.buttonDisabledText,
},
});

View file

@ -0,0 +1,100 @@
import { COLORS, SPACING } from "@/constants/theme";
import { useAuth } from "@/contexts/AuthContext";
import { useRouter } from "expo-router";
import React from "react";
import { Alert, StyleSheet, Text, TouchableOpacity, View } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
interface HeaderProps {
title: string;
showHomeButton?: boolean;
showLogoutButton?: boolean;
}
export function Header({
title,
showHomeButton = false,
showLogoutButton = false,
}: HeaderProps) {
const router = useRouter();
const { logout } = useAuth();
const insets = useSafeAreaInsets();
const handleHome = () => {
router.replace("/home");
};
const handleLogout = () => {
Alert.alert("Sair", "Tem certeza que deseja sair?", [
{ text: "Cancelar", style: "cancel" },
{
text: "Sair",
style: "destructive",
onPress: async () => {
try {
await logout();
} catch (error) {
console.error("Logout error:", error);
}
},
},
]);
};
return (
<View style={[styles.container, { paddingTop: insets.top }]}>
<View style={styles.content}>
<Text style={styles.title}>{title}</Text>
<View style={styles.actions}>
{showHomeButton && (
<TouchableOpacity onPress={handleHome} style={styles.button}>
<Text style={styles.buttonText}>🏠</Text>
</TouchableOpacity>
)}
{showLogoutButton && (
<TouchableOpacity onPress={handleLogout} style={styles.button}>
<Text style={styles.buttonText}>🚪</Text>
</TouchableOpacity>
)}
</View>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: COLORS.headerBackground,
borderBottomWidth: 1,
borderBottomColor: COLORS.headerBorder,
},
content: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
paddingHorizontal: SPACING.lg,
paddingVertical: SPACING.md,
},
title: {
fontSize: 20,
fontWeight: "700",
color: COLORS.headerText,
},
actions: {
flexDirection: "row",
gap: SPACING.md,
},
button: {
paddingHorizontal: SPACING.md,
paddingVertical: SPACING.sm,
borderRadius: 8,
backgroundColor: COLORS.surface,
borderWidth: 1,
borderColor: COLORS.border,
},
buttonText: {
fontSize: 18,
},
});

View file

@ -1,21 +1,26 @@
import React from "react";
import { StyleSheet, TextInput, TextInputProps } from "react-native";
import { COLORS, BORDER_RADIUS } from "@/constants/theme";
export function Input({ ...rest }: TextInputProps) {
return (
<TextInput style={styles.input} placeholderTextColor="#7c7c7c" {...rest} />
<TextInput
style={styles.input}
placeholderTextColor={COLORS.inputPlaceholder}
{...rest}
/>
);
}
// Estilos para o componente Input
const styles = StyleSheet.create({
input: {
width: "100%",
height: 48,
borderWidth: 1,
borderColor: "#2a2a2a",
backgroundColor: "#1a1a1a",
borderRadius: 8,
color: "#e7e7e7",
borderColor: COLORS.inputBorder,
backgroundColor: COLORS.inputBackground,
borderRadius: BORDER_RADIUS.md,
color: COLORS.inputText,
fontSize: 16,
paddingLeft: 12,
},

View file

@ -0,0 +1,57 @@
import { Picker } from "@react-native-picker/picker";
import React from "react";
import { StyleSheet, Text, View } from "react-native";
import { COLORS, BORDER_RADIUS, SPACING } from "@/constants/theme";
type SelectProps = {
label?: string;
value: string;
onValueChange: (value: string) => void;
items: { label: string; value: string }[];
};
export function Select({ label, value, onValueChange, items }: SelectProps) {
return (
<View style={styles.container}>
{label && <Text style={styles.label}>{label}</Text>}
<View style={styles.pickerContainer}>
<Picker
selectedValue={value}
onValueChange={onValueChange}
style={styles.picker}
>
{items.map((item) => (
<Picker.Item
key={item.value}
label={item.label}
value={item.value}
/>
))}
</Picker>
</View>
</View>
);
}
const styles = StyleSheet.create({
container: {
width: "100%",
},
label: {
color: COLORS.text,
fontSize: 14,
fontWeight: "600",
marginBottom: SPACING.sm,
},
pickerContainer: {
borderWidth: 1,
borderColor: COLORS.inputBorder,
backgroundColor: COLORS.inputBackground,
borderRadius: BORDER_RADIUS.md,
overflow: "hidden",
},
picker: {
height: 48,
color: COLORS.inputText,
},
});

View file

@ -0,0 +1,83 @@
export const COLORS = {
// Background
background: "#0a0a0a",
surface: "#1a1a1a",
surfaceLight: "#242424",
// Text
text: "#ffffff",
textSecondary: "#b0b0b0",
textTertiary: "#808080",
// Borders
border: "#333333",
borderLight: "#404040",
// Actions
primary: "#000000",
primaryText: "#ffffff",
secondary: "#333333",
// Status
success: "#10b981",
error: "#ef4444",
warning: "#f59e0b",
info: "#3b82f6",
// Inputs
inputBackground: "#1a1a1a",
inputBorder: "#404040",
inputText: "#ffffff",
inputPlaceholder: "#808080",
// Buttons
buttonBackground: "#000000",
buttonText: "#ffffff",
buttonDisabledBackground: "#404040",
buttonDisabledText: "#808080",
// Headers
headerBackground: "#0a0a0a",
headerBorder: "#333333",
headerText: "#ffffff",
};
export const TYPOGRAPHY = {
title: {
fontSize: 28,
fontWeight: "800" as const,
},
headline: {
fontSize: 20,
fontWeight: "700" as const,
},
body: {
fontSize: 16,
fontWeight: "500" as const,
},
label: {
fontSize: 14,
fontWeight: "600" as const,
},
caption: {
fontSize: 12,
fontWeight: "500" as const,
},
};
export const SPACING = {
xs: 4,
sm: 8,
md: 12,
lg: 16,
xl: 24,
xxl: 32,
};
export const BORDER_RADIUS = {
sm: 4,
md: 8,
lg: 12,
xl: 16,
round: 50,
};

View file

@ -0,0 +1,140 @@
import { useRouter } from "expo-router";
import React, { createContext, useContext, useEffect, useState } from "react";
import {
deleteSetting,
getSetting,
initDB,
setSetting,
} from "@/services/db";
type User = {
id: string;
email: string;
name: string;
};
type AuthContextType = {
user: User | null;
token: string | null;
isLoading: boolean;
login: (
email: string,
password: string,
token: string,
user: User,
) => Promise<void>;
signup: (
name: string,
email: string,
password: string,
token: string,
) => Promise<void>;
logout: () => Promise<void>;
isSignedIn: boolean;
};
const AuthContext = createContext<AuthContextType | undefined>(undefined);
export function AuthProvider({ children }: { children: React.ReactNode }) {
const [user, setUser] = useState<User | null>(null);
const [token, setToken] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(true);
const router = useRouter();
useEffect(() => {
bootstrapAsync();
}, []);
const bootstrapAsync = async () => {
try {
await initDB();
const storedToken = await getSetting("authToken");
const storedUser = await getSetting("user");
if (storedToken && storedUser) {
setToken(storedToken);
setUser(JSON.parse(storedUser));
router.replace("/home");
} else {
setToken(null);
setUser(null);
router.replace("/");
}
} catch (e) {
console.error("Failed to restore token", e);
} finally {
setIsLoading(false);
}
};
const authContext: AuthContextType = {
user,
token,
isLoading,
isSignedIn: token !== null,
login: async (
_email: string,
_password: string,
authToken: string,
authUser: User,
) => {
try {
setToken(authToken);
setUser(authUser);
await setSetting("authToken", authToken);
await setSetting("user", JSON.stringify(authUser));
router.replace("/home");
} catch (error) {
console.error("Login failed:", error);
throw error;
}
},
signup: async (
name: string,
email: string,
password: string,
authToken: string,
) => {
try {
const newUser: User = {
id: Date.now().toString(),
email,
name,
};
setToken(authToken);
setUser(newUser);
await setSetting("authToken", authToken);
await setSetting("user", JSON.stringify(newUser));
router.replace("/home");
} catch (error) {
console.error("Signup failed:", error);
throw error;
}
},
logout: async () => {
try {
await deleteSetting("authToken");
await deleteSetting("user");
setToken(null);
setUser(null);
router.replace("/");
} catch (error) {
console.error("Logout failed:", error);
throw error;
}
},
};
return (
<AuthContext.Provider value={authContext}>{children}</AuthContext.Provider>
);
}
export function useAuth() {
const context = useContext(AuthContext);
if (!context) {
throw new Error("useAuth must be used within an AuthProvider");
}
return context;
}

View file

@ -1,9 +1,10 @@
import axios from "axios";
const BASE_URL ="http://175.15.15.93:3000";
const BASE_URL = process.env.EXPO_PUBLIC_API_URL ?? "http://175.15.15.93:3000";
export const api = axios.create({
baseURL: BASE_URL,
timeout: 10000,
});
export function setAuthToken(token: string | null) {

View file

@ -0,0 +1,207 @@
import * as SQLite from "expo-sqlite";
const db = SQLite.openDatabaseSync("toptran.db");
export type UsuarioDB = {
id: string;
email: string;
name: string;
token: string;
created_at: string;
};
export type CorridaDB = {
id: string;
usuario_id: string;
empresa: string;
km: number;
custo_por_km: number;
total: number;
data: string;
sincronizado: 0 | 1;
created_at: string;
};
export const initDB = async () => {
try {
await db.execAsync(`
CREATE TABLE IF NOT EXISTS usuarios (
id TEXT PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT NOT NULL,
token TEXT NOT NULL,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS corridas (
id TEXT PRIMARY KEY,
usuario_id TEXT NOT NULL,
empresa TEXT NOT NULL,
km REAL NOT NULL,
custo_por_km REAL NOT NULL,
total REAL NOT NULL,
data TEXT NOT NULL,
sincronizado INTEGER DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (usuario_id) REFERENCES usuarios(id)
);
CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
);
`);
} catch (error) {
console.error("Failed to initialize database:", error);
throw error;
}
};
// SETTINGS (key-value store)
export const getSetting = async (key: string): Promise<string | null> => {
try {
const row = await db.getFirstAsync<{ value: string }>(
`SELECT value FROM settings WHERE key = ?`,
[key],
);
return row?.value ?? null;
} catch (error) {
console.error("Error reading setting:", error);
throw error;
}
};
export const setSetting = async (key: string, value: string): Promise<void> => {
try {
await db.runAsync(
`INSERT OR REPLACE INTO settings (key, value) VALUES (?, ?)`,
[key, value],
);
} catch (error) {
console.error("Error writing setting:", error);
throw error;
}
};
export const deleteSetting = async (key: string): Promise<void> => {
try {
await db.runAsync(`DELETE FROM settings WHERE key = ?`, [key]);
} catch (error) {
console.error("Error deleting setting:", error);
throw error;
}
};
// USUÁRIOS
export const salvarUsuario = async (usuario: Omit<UsuarioDB, "created_at">) => {
try {
const result = await db.runAsync(
`INSERT OR REPLACE INTO usuarios (id, email, name, token)
VALUES (?, ?, ?, ?)`,
[usuario.id, usuario.email, usuario.name, usuario.token],
);
return result;
} catch (error) {
console.error("Error saving user:", error);
throw error;
}
};
export const obterUsuario = async (
usuarioId: string,
): Promise<UsuarioDB | null> => {
try {
const result = await db.getFirstAsync<UsuarioDB>(
`SELECT * FROM usuarios WHERE id = ?`,
[usuarioId],
);
return result || null;
} catch (error) {
console.error("Error fetching user:", error);
throw error;
}
};
// CORRIDAS
export const salvarCorrida = async (corrida: Omit<CorridaDB, "created_at">) => {
try {
const result = await db.runAsync(
`INSERT INTO corridas (id, usuario_id, empresa, km, custo_por_km, total, data, sincronizado)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
[
corrida.id,
corrida.usuario_id,
corrida.empresa,
corrida.km,
corrida.custo_por_km,
corrida.total,
corrida.data,
corrida.sincronizado,
],
);
return result;
} catch (error) {
console.error("Error saving corrida:", error);
throw error;
}
};
export const obterCorridas = async (
usuarioId: string,
): Promise<CorridaDB[]> => {
try {
const result = await db.getAllAsync<CorridaDB>(
`SELECT * FROM corridas WHERE usuario_id = ? ORDER BY created_at DESC`,
[usuarioId],
);
return result || [];
} catch (error) {
console.error("Error fetching corridas:", error);
throw error;
}
};
export const obterCorridasNaoSincronizadas = async (
usuarioId: string,
): Promise<CorridaDB[]> => {
try {
const result = await db.getAllAsync<CorridaDB>(
`SELECT * FROM corridas WHERE usuario_id = ? AND sincronizado = 0`,
[usuarioId],
);
return result || [];
} catch (error) {
console.error("Error fetching unsync corridas:", error);
throw error;
}
};
export const marcarCorridaComoSincronizada = async (corridaId: string) => {
try {
await db.runAsync(`UPDATE corridas SET sincronizado = 1 WHERE id = ?`, [
corridaId,
]);
} catch (error) {
console.error("Error marking corrida as synced:", error);
throw error;
}
};
export const deletarCorrida = async (corridaId: string) => {
try {
await db.runAsync(`DELETE FROM corridas WHERE id = ?`, [corridaId]);
} catch (error) {
console.error("Error deleting corrida:", error);
throw error;
}
};
export const limparBancoDados = async () => {
try {
await db.execAsync(`DELETE FROM corridas; DELETE FROM usuarios;`);
console.log("Database cleared");
} catch (error) {
console.error("Error clearing database:", error);
throw error;
}
};

View file

@ -1,17 +1,32 @@
{
"extends": "expo/tsconfig.base",
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": [
"ES2020",
"DOM",
"DOM.Iterable"
],
"module": "ESNext",
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"paths": {
"@/*": [
"./src/*"
]
}
},
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "react",
"moduleResolution": "bundler"
},
"include": [
"**/*.ts",
"**/*.tsx",
".expo/types/**/*.ts",
"expo-env.d.ts"
]
],
"extends": "expo/tsconfig.base"
}