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
|
*.pem
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
|
.env
|
||||||
.env*.local
|
.env*.local
|
||||||
|
|
||||||
# typescript
|
# typescript
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,10 @@
|
||||||
"output": "static",
|
"output": "static",
|
||||||
"favicon": "./assets/toptranico.png"
|
"favicon": "./assets/toptranico.png"
|
||||||
},
|
},
|
||||||
"plugins": ["expo-router"],
|
"plugins": [
|
||||||
|
"expo-router",
|
||||||
|
"expo-secure-store"
|
||||||
|
],
|
||||||
"experiments": {
|
"experiments": {
|
||||||
"typedRoutes": true
|
"typedRoutes": true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
139
toptran-app/package-lock.json
generated
139
toptran-app/package-lock.json
generated
|
|
@ -9,24 +9,23 @@
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
"@react-navigation/native": "^7.1.8",
|
"@react-native-picker/picker": "^2.7.5",
|
||||||
"axios": "^1.15.2",
|
"axios": "^1.15.2",
|
||||||
"expo": "~54.0.33",
|
"expo": "~54.0.33",
|
||||||
"expo-constants": "~18.0.13",
|
"expo-constants": "~18.0.13",
|
||||||
"expo-font": "~14.0.11",
|
"expo-font": "~14.0.11",
|
||||||
"expo-linking": "~8.0.11",
|
"expo-linking": "~8.0.11",
|
||||||
"expo-router": "~6.0.23",
|
"expo-router": "~6.0.23",
|
||||||
|
"expo-secure-store": "~15.0.8",
|
||||||
"expo-splash-screen": "~31.0.13",
|
"expo-splash-screen": "~31.0.13",
|
||||||
|
"expo-sqlite": "^55.0.15",
|
||||||
"expo-status-bar": "~3.0.9",
|
"expo-status-bar": "~3.0.9",
|
||||||
"expo-web-browser": "~15.0.10",
|
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
"react-native-reanimated": "~4.1.1",
|
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-web": "~0.21.0",
|
"react-native-web": "~0.21.0"
|
||||||
"react-native-worklets": "0.5.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "~19.1.0",
|
"@types/react": "~19.1.0",
|
||||||
|
|
@ -1363,21 +1362,6 @@
|
||||||
"@babel/core": "^7.0.0-0"
|
"@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": {
|
"node_modules/@babel/plugin-transform-typescript": {
|
||||||
"version": "7.28.6",
|
"version": "7.28.6",
|
||||||
"resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz",
|
"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": {
|
"node_modules/@react-native/assets-registry": {
|
||||||
"version": "0.81.5",
|
"version": "0.81.5",
|
||||||
"resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz",
|
"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==",
|
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/axios": {
|
||||||
"version": "1.15.2",
|
"version": "1.15.2",
|
||||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz",
|
"resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz",
|
||||||
|
|
@ -4607,6 +4610,15 @@
|
||||||
"node": ">=10"
|
"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": {
|
"node_modules/expo-server": {
|
||||||
"version": "1.0.6",
|
"version": "1.0.6",
|
||||||
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.6.tgz",
|
||||||
|
|
@ -4628,6 +4640,20 @@
|
||||||
"expo": "*"
|
"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": {
|
"node_modules/expo-status-bar": {
|
||||||
"version": "3.0.9",
|
"version": "3.0.9",
|
||||||
"resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-3.0.9.tgz",
|
"resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-3.0.9.tgz",
|
||||||
|
|
@ -4641,16 +4667,6 @@
|
||||||
"react-native": "*"
|
"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": {
|
"node_modules/expo/node_modules/@expo/cli": {
|
||||||
"version": "54.0.24",
|
"version": "54.0.24",
|
||||||
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.24.tgz",
|
"resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.24.tgz",
|
||||||
|
|
@ -7450,33 +7466,6 @@
|
||||||
"react-native": "*"
|
"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": {
|
"node_modules/react-native-safe-area-context": {
|
||||||
"version": "5.6.2",
|
"version": "5.6.2",
|
||||||
"resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz",
|
"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==",
|
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
|
||||||
"license": "MIT"
|
"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": {
|
"node_modules/react-native/node_modules/@react-native/virtualized-lists": {
|
||||||
"version": "0.81.5",
|
"version": "0.81.5",
|
||||||
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz",
|
"resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz",
|
||||||
|
|
|
||||||
|
|
@ -10,24 +10,23 @@
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@expo/vector-icons": "^15.0.3",
|
"@expo/vector-icons": "^15.0.3",
|
||||||
"@react-navigation/native": "^7.1.8",
|
"@react-native-picker/picker": "^2.7.5",
|
||||||
"axios": "^1.15.2",
|
"axios": "^1.15.2",
|
||||||
"expo": "~54.0.33",
|
"expo": "~54.0.33",
|
||||||
"expo-constants": "~18.0.13",
|
"expo-constants": "~18.0.13",
|
||||||
"expo-font": "~14.0.11",
|
"expo-font": "~14.0.11",
|
||||||
"expo-linking": "~8.0.11",
|
"expo-linking": "~8.0.11",
|
||||||
"expo-router": "~6.0.23",
|
"expo-router": "~6.0.23",
|
||||||
|
"expo-secure-store": "~15.0.8",
|
||||||
"expo-splash-screen": "~31.0.13",
|
"expo-splash-screen": "~31.0.13",
|
||||||
|
"expo-sqlite": "^55.0.15",
|
||||||
"expo-status-bar": "~3.0.9",
|
"expo-status-bar": "~3.0.9",
|
||||||
"expo-web-browser": "~15.0.10",
|
|
||||||
"react": "19.1.0",
|
"react": "19.1.0",
|
||||||
"react-dom": "19.1.0",
|
"react-dom": "19.1.0",
|
||||||
"react-native": "0.81.5",
|
"react-native": "0.81.5",
|
||||||
"react-native-reanimated": "~4.1.1",
|
|
||||||
"react-native-safe-area-context": "~5.6.0",
|
"react-native-safe-area-context": "~5.6.0",
|
||||||
"react-native-screens": "~4.16.0",
|
"react-native-screens": "~4.16.0",
|
||||||
"react-native-web": "~0.21.0",
|
"react-native-web": "~0.21.0"
|
||||||
"react-native-worklets": "0.5.1"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/react": "~19.1.0",
|
"@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 { 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 (
|
return (
|
||||||
<Stack
|
<Stack
|
||||||
screenOptions={{
|
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 {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
|
@ -13,28 +13,71 @@ import {
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { Input } from "@/components/Input";
|
import { Input } from "@/components/Input";
|
||||||
|
import { COLORS, SPACING } from "@/constants/theme";
|
||||||
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { api, setAuthToken } from "@/server/api";
|
import { api, setAuthToken } from "@/server/api";
|
||||||
|
import { salvarUsuario } from "@/services/db";
|
||||||
import { Link } from "expo-router";
|
import { Link } from "expo-router";
|
||||||
|
|
||||||
export default function IndexPage() {
|
export default function IndexPage() {
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
const [loading, setLoading] = useState(false);
|
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() {
|
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.");
|
Alert.alert("Entrar", "Por favor, preencha todos os campos.");
|
||||||
return;
|
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 {
|
try {
|
||||||
setLoading(true);
|
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);
|
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) {
|
} 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);
|
Alert.alert("Erro", message);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -98,33 +141,33 @@ export default function IndexPage() {
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: "#0e0d0d",
|
backgroundColor: COLORS.background,
|
||||||
padding: 32,
|
padding: SPACING.xxl,
|
||||||
},
|
},
|
||||||
illustration: {
|
illustration: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: 330,
|
height: 330,
|
||||||
resizeMode: "contain",
|
resizeMode: "contain",
|
||||||
marginTop: 62,
|
marginTop: SPACING.xxl,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
marginTop: 62,
|
marginTop: SPACING.xxl,
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: "900",
|
fontWeight: "900",
|
||||||
color: "#e7e7e7",
|
color: COLORS.text,
|
||||||
},
|
},
|
||||||
subtitle: {
|
subtitle: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
color: "#a1a1a1",
|
color: COLORS.textSecondary,
|
||||||
},
|
},
|
||||||
form: {
|
form: {
|
||||||
marginTop: 24,
|
marginTop: SPACING.lg,
|
||||||
gap: 16,
|
gap: SPACING.lg,
|
||||||
},
|
},
|
||||||
footerText: {
|
footerText: {
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
marginTop: 24,
|
marginTop: SPACING.lg,
|
||||||
color: "#a1a1a1",
|
color: COLORS.textSecondary,
|
||||||
},
|
},
|
||||||
footerLink: {
|
footerLink: {
|
||||||
color: "#007AFF",
|
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 {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
|
|
@ -13,11 +13,17 @@ import {
|
||||||
|
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { Input } from "@/components/Input";
|
import { Input } from "@/components/Input";
|
||||||
import { api } from "@/server/api";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { Link, useRouter } from "expo-router";
|
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() {
|
export default function Signup() {
|
||||||
const router = useRouter();
|
const { signup } = useAuth();
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const [email, setEmail] = useState("");
|
const [email, setEmail] = useState("");
|
||||||
const [password, setPassword] = useState("");
|
const [password, setPassword] = useState("");
|
||||||
|
|
@ -25,16 +31,24 @@ export default function Signup() {
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
async function handleSignUp() {
|
async function handleSignUp() {
|
||||||
if (
|
const nameTrimmed = name.trim();
|
||||||
!name.trim() ||
|
const emailTrimmed = email.trim();
|
||||||
!email.trim() ||
|
|
||||||
!password.trim() ||
|
if (!nameTrimmed || !emailTrimmed || !password.trim() || !confirmPassword.trim()) {
|
||||||
!confirmPassword.trim()
|
|
||||||
) {
|
|
||||||
Alert.alert("Cadastrar", "Por favor, preencha todos os campos.");
|
Alert.alert("Cadastrar", "Por favor, preencha todos os campos.");
|
||||||
return;
|
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) {
|
if (password !== confirmPassword) {
|
||||||
Alert.alert("Cadastrar", "As senhas não coincidem.");
|
Alert.alert("Cadastrar", "As senhas não coincidem.");
|
||||||
return;
|
return;
|
||||||
|
|
@ -42,12 +56,25 @@ export default function Signup() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
await api.post("/auth/register", { name, email, password });
|
const { data } = await api.post("/auth/register", {
|
||||||
Alert.alert("Sucesso", "Conta criada com sucesso!", [
|
name: nameTrimmed,
|
||||||
{ text: "OK", onPress: () => router.replace("/") },
|
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) {
|
} 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);
|
Alert.alert("Erro", message);
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,28 @@
|
||||||
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
TouchableOpacity,
|
TouchableOpacity,
|
||||||
TouchableOpacityProps,
|
TouchableOpacityProps,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
|
import { COLORS, SPACING, BORDER_RADIUS } from "@/constants/theme";
|
||||||
|
|
||||||
type ButtonProps = TouchableOpacityProps & {
|
type ButtonProps = TouchableOpacityProps & {
|
||||||
label: string;
|
label: string;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Componente de botão personalizado
|
export function Button({ label, disabled = false, ...rest }: ButtonProps) {
|
||||||
export function Button({ label, ...rest }: ButtonProps) {
|
|
||||||
return (
|
return (
|
||||||
<TouchableOpacity style={styles.container} activeOpacity={0.7} {...rest}>
|
<TouchableOpacity
|
||||||
<Text style={styles.label}>{label}</Text>
|
style={[styles.container, disabled && styles.containerDisabled]}
|
||||||
|
activeOpacity={disabled ? 1 : 0.7}
|
||||||
|
disabled={disabled}
|
||||||
|
{...rest}
|
||||||
|
>
|
||||||
|
<Text style={[styles.label, disabled && styles.labelDisabled]}>
|
||||||
|
{label}
|
||||||
|
</Text>
|
||||||
</TouchableOpacity>
|
</TouchableOpacity>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -22,14 +31,21 @@ const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: 48,
|
height: 48,
|
||||||
backgroundColor: "#a19f9f",
|
backgroundColor: COLORS.buttonBackground,
|
||||||
borderRadius: 8,
|
borderRadius: BORDER_RADIUS.md,
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
|
marginTop: SPACING.lg,
|
||||||
|
},
|
||||||
|
containerDisabled: {
|
||||||
|
backgroundColor: COLORS.buttonDisabledBackground,
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
color: "#050505",
|
color: COLORS.buttonText,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: "600",
|
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 { StyleSheet, TextInput, TextInputProps } from "react-native";
|
||||||
|
import { COLORS, BORDER_RADIUS } from "@/constants/theme";
|
||||||
|
|
||||||
export function Input({ ...rest }: TextInputProps) {
|
export function Input({ ...rest }: TextInputProps) {
|
||||||
return (
|
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({
|
const styles = StyleSheet.create({
|
||||||
input: {
|
input: {
|
||||||
width: "100%",
|
width: "100%",
|
||||||
height: 48,
|
height: 48,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: "#2a2a2a",
|
borderColor: COLORS.inputBorder,
|
||||||
backgroundColor: "#1a1a1a",
|
backgroundColor: COLORS.inputBackground,
|
||||||
borderRadius: 8,
|
borderRadius: BORDER_RADIUS.md,
|
||||||
color: "#e7e7e7",
|
color: COLORS.inputText,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
paddingLeft: 12,
|
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";
|
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({
|
export const api = axios.create({
|
||||||
baseURL: BASE_URL,
|
baseURL: BASE_URL,
|
||||||
|
timeout: 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function setAuthToken(token: string | null) {
|
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": {
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": [
|
||||||
|
"ES2020",
|
||||||
|
"DOM",
|
||||||
|
"DOM.Iterable"
|
||||||
|
],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"allowSyntheticDefaultImports": true,
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": [
|
"@/*": [
|
||||||
"./src/*"
|
"./src/*"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"jsx": "react",
|
||||||
|
"moduleResolution": "bundler"
|
||||||
},
|
},
|
||||||
"include": [
|
"include": [
|
||||||
"**/*.ts",
|
"**/*.ts",
|
||||||
"**/*.tsx",
|
"**/*.tsx",
|
||||||
".expo/types/**/*.ts",
|
".expo/types/**/*.ts",
|
||||||
"expo-env.d.ts"
|
"expo-env.d.ts"
|
||||||
]
|
],
|
||||||
|
"extends": "expo/tsconfig.base"
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue