diff --git a/toptran-app/.gitignore b/toptran-app/.gitignore
index d914c32..da9569c 100644
--- a/toptran-app/.gitignore
+++ b/toptran-app/.gitignore
@@ -31,6 +31,7 @@ yarn-error.*
*.pem
# local env files
+.env
.env*.local
# typescript
diff --git a/toptran-app/app.json b/toptran-app/app.json
index 95b6c03..048fb2b 100644
--- a/toptran-app/app.json
+++ b/toptran-app/app.json
@@ -20,7 +20,10 @@
"output": "static",
"favicon": "./assets/toptranico.png"
},
- "plugins": ["expo-router"],
+ "plugins": [
+ "expo-router",
+ "expo-secure-store"
+ ],
"experiments": {
"typedRoutes": true
}
diff --git a/toptran-app/package-lock.json b/toptran-app/package-lock.json
index 526fb88..4b1acb4 100644
--- a/toptran-app/package-lock.json
+++ b/toptran-app/package-lock.json
@@ -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",
diff --git a/toptran-app/package.json b/toptran-app/package.json
index a0f1252..e84b408 100644
--- a/toptran-app/package.json
+++ b/toptran-app/package.json
@@ -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",
diff --git a/toptran-app/src/app/_layout.tsx b/toptran-app/src/app/_layout.tsx
index 47c1a9f..cd6efd5 100644
--- a/toptran-app/src/app/_layout.tsx
+++ b/toptran-app/src/app/_layout.tsx
@@ -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 (
);
}
+
+export default function Layout() {
+ return (
+
+
+
+ );
+}
diff --git a/toptran-app/src/app/corrida.tsx b/toptran-app/src/app/corrida.tsx
new file mode 100644
index 0000000..f564ead
--- /dev/null
+++ b/toptran-app/src/app/corrida.tsx
@@ -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([]);
+
+ 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 (
+
+
+ setActiveTab("lancamento")}
+ >
+
+ 📍 Lançar
+
+
+
+ setActiveTab("historico")}
+ >
+
+ 📋 Histórico
+
+
+
+
+
+ {activeTab === "lancamento" ? (
+
+ ) : (
+
+ )}
+
+
+ );
+}
+
+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,
+ },
+});
diff --git a/toptran-app/src/app/historico.tsx b/toptran-app/src/app/historico.tsx
new file mode 100644
index 0000000..6c5a871
--- /dev/null
+++ b/toptran-app/src/app/historico.tsx
@@ -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 }) => (
+
+
+ #{index + 1}
+ {item.data}
+
+
+
+ {item.empresa}
+
+
+
+
+ Distância
+ {item.km} km
+
+
+
+ Valor/km
+ R$ {item.custoPorKm.toFixed(2)}
+
+
+
+ Total
+ R$ {item.total.toFixed(2)}
+
+
+
+);
+
+export default function Historico({ historico }: HistoricoProps) {
+ return (
+
+
+ Histórico de Corridas
+
+ {historico.length} corrida{historico.length !== 1 ? "s" : ""}
+
+
+
+ {historico.length === 0 ? (
+
+
+ 🚗
+ Nenhuma corrida registrada
+ Suas corridas aparecerão aqui
+
+
+ ) : (
+ item.id}
+ renderItem={({ item, index }) => (
+
+ )}
+ contentContainerStyle={styles.listContent}
+ showsVerticalScrollIndicator={false}
+ />
+ )}
+
+ );
+}
+
+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",
+ },
+});
diff --git a/toptran-app/src/app/home.tsx b/toptran-app/src/app/home.tsx
new file mode 100644
index 0000000..79b7d30
--- /dev/null
+++ b/toptran-app/src/app/home.tsx
@@ -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({
+ 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 (
+
+ {/* Header */}
+
+
+ TopTran
+ Sistema de Gestão
+
+
+
+
+ {firstName.charAt(0).toUpperCase()}
+
+
+
+
+
+
+
+ {/* Greeting */}
+
+
+ {greeting()}, {firstName}! 👋
+
+ {today}
+
+
+ {/* Stats */}
+ Resumo Geral
+
+
+ {stats.totalCorridas}
+ Corridas
+
+
+
+ {stats.totalKm.toFixed(0)}
+
+ Km Rodados
+
+
+
+ R${stats.totalGanhos.toFixed(0)}
+
+
+ Total Ganho
+
+
+
+
+ {/* Actions Grid */}
+ Acesso Rápido
+
+ {actions.map((action, index) => (
+
+ {action.icon}
+
+ {action.label}
+
+ {action.description}
+
+ ))}
+
+
+ © 2025 TopTran
+
+
+ );
+}
+
+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,
+ },
+});
diff --git a/toptran-app/src/app/index.tsx b/toptran-app/src/app/index.tsx
index 644f413..841fd30 100644
--- a/toptran-app/src/app/index.tsx
+++ b/toptran-app/src/app/index.tsx
@@ -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",
diff --git a/toptran-app/src/app/lancamento.tsx b/toptran-app/src/app/lancamento.tsx
new file mode 100644
index 0000000..a82a79a
--- /dev/null
+++ b/toptran-app/src/app/lancamento.tsx
@@ -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 (
+
+
+
+
+ Nova Corrida
+ Preencha os dados da corrida
+
+
+
+ Empresa
+
+
+
+
+ Distância (km)
+
+
+
+
+
+ Custo por km
+
+ R$ {CUSTO_POR_KM.toFixed(2)}
+
+
+
+
+
+
+ Valor Total
+
+ R$ {totalCorrida.toFixed(2)}
+
+
+
+
+
+
+
+
+ );
+}
+
+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",
+ },
+});
diff --git a/toptran-app/src/app/signup.tsx b/toptran-app/src/app/signup.tsx
index 0a64cf5..ee3014e 100644
--- a/toptran-app/src/app/signup.tsx
+++ b/toptran-app/src/app/signup.tsx
@@ -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);
diff --git a/toptran-app/src/components/Button.tsx b/toptran-app/src/components/Button.tsx
index 4e03e43..90f35e1 100644
--- a/toptran-app/src/components/Button.tsx
+++ b/toptran-app/src/components/Button.tsx
@@ -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 (
-
- {label}
+
+
+ {label}
+
);
}
@@ -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,
+ },
});
diff --git a/toptran-app/src/components/Header.tsx b/toptran-app/src/components/Header.tsx
new file mode 100644
index 0000000..1834cdc
--- /dev/null
+++ b/toptran-app/src/components/Header.tsx
@@ -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 (
+
+
+ {title}
+
+
+ {showHomeButton && (
+
+ 🏠
+
+ )}
+
+ {showLogoutButton && (
+
+ 🚪
+
+ )}
+
+
+
+ );
+}
+
+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,
+ },
+});
diff --git a/toptran-app/src/components/Input.tsx b/toptran-app/src/components/Input.tsx
index 2ef145f..04d9019 100644
--- a/toptran-app/src/components/Input.tsx
+++ b/toptran-app/src/components/Input.tsx
@@ -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 (
-
+
);
}
-// 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,
},
diff --git a/toptran-app/src/components/Select.tsx b/toptran-app/src/components/Select.tsx
new file mode 100644
index 0000000..f440c3b
--- /dev/null
+++ b/toptran-app/src/components/Select.tsx
@@ -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 (
+
+ {label && {label}}
+
+
+ {items.map((item) => (
+
+ ))}
+
+
+
+ );
+}
+
+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,
+ },
+});
diff --git a/toptran-app/src/constants/theme.ts b/toptran-app/src/constants/theme.ts
new file mode 100644
index 0000000..ec7dd1c
--- /dev/null
+++ b/toptran-app/src/constants/theme.ts
@@ -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,
+};
diff --git a/toptran-app/src/contexts/AuthContext.tsx b/toptran-app/src/contexts/AuthContext.tsx
new file mode 100644
index 0000000..ffb7b83
--- /dev/null
+++ b/toptran-app/src/contexts/AuthContext.tsx
@@ -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;
+ signup: (
+ name: string,
+ email: string,
+ password: string,
+ token: string,
+ ) => Promise;
+ logout: () => Promise;
+ isSignedIn: boolean;
+};
+
+const AuthContext = createContext(undefined);
+
+export function AuthProvider({ children }: { children: React.ReactNode }) {
+ const [user, setUser] = useState(null);
+ const [token, setToken] = useState(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 (
+ {children}
+ );
+}
+
+export function useAuth() {
+ const context = useContext(AuthContext);
+ if (!context) {
+ throw new Error("useAuth must be used within an AuthProvider");
+ }
+ return context;
+}
diff --git a/toptran-app/src/server/api.ts b/toptran-app/src/server/api.ts
index 8e031ff..7bfefa9 100644
--- a/toptran-app/src/server/api.ts
+++ b/toptran-app/src/server/api.ts
@@ -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) {
diff --git a/toptran-app/src/services/db.ts b/toptran-app/src/services/db.ts
new file mode 100644
index 0000000..64cda08
--- /dev/null
+++ b/toptran-app/src/services/db.ts
@@ -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 => {
+ 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 => {
+ 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 => {
+ 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) => {
+ 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 => {
+ try {
+ const result = await db.getFirstAsync(
+ `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) => {
+ 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 => {
+ try {
+ const result = await db.getAllAsync(
+ `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 => {
+ try {
+ const result = await db.getAllAsync(
+ `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;
+ }
+};
diff --git a/toptran-app/tsconfig.json b/toptran-app/tsconfig.json
index 38f3374..70b5554 100644
--- a/toptran-app/tsconfig.json
+++ b/toptran-app/tsconfig.json
@@ -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"
}