add SQLite and adjusts sistems
This commit is contained in:
parent
960761984d
commit
2712fd6fa9
20 changed files with 1740 additions and 148 deletions
1
toptran-app/.gitignore
vendored
1
toptran-app/.gitignore
vendored
|
|
@ -31,6 +31,7 @@ yarn-error.*
|
|||
*.pem
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env*.local
|
||||
|
||||
# typescript
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@
|
|||
"output": "static",
|
||||
"favicon": "./assets/toptranico.png"
|
||||
},
|
||||
"plugins": ["expo-router"],
|
||||
"plugins": [
|
||||
"expo-router",
|
||||
"expo-secure-store"
|
||||
],
|
||||
"experiments": {
|
||||
"typedRoutes": true
|
||||
}
|
||||
|
|
|
|||
139
toptran-app/package-lock.json
generated
139
toptran-app/package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
121
toptran-app/src/app/corrida.tsx
Normal file
121
toptran-app/src/app/corrida.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
191
toptran-app/src/app/historico.tsx
Normal file
191
toptran-app/src/app/historico.tsx
Normal 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",
|
||||
},
|
||||
});
|
||||
387
toptran-app/src/app/home.tsx
Normal file
387
toptran-app/src/app/home.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
222
toptran-app/src/app/lancamento.tsx
Normal file
222
toptran-app/src/app/lancamento.tsx
Normal 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",
|
||||
},
|
||||
});
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
100
toptran-app/src/components/Header.tsx
Normal file
100
toptran-app/src/components/Header.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
|
|
@ -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,
|
||||
},
|
||||
|
|
|
|||
57
toptran-app/src/components/Select.tsx
Normal file
57
toptran-app/src/components/Select.tsx
Normal 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,
|
||||
},
|
||||
});
|
||||
83
toptran-app/src/constants/theme.ts
Normal file
83
toptran-app/src/constants/theme.ts
Normal 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,
|
||||
};
|
||||
140
toptran-app/src/contexts/AuthContext.tsx
Normal file
140
toptran-app/src/contexts/AuthContext.tsx
Normal 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;
|
||||
}
|
||||
|
|
@ -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) {
|
||||
|
|
|
|||
207
toptran-app/src/services/db.ts
Normal file
207
toptran-app/src/services/db.ts
Normal 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;
|
||||
}
|
||||
};
|
||||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue