From 2712fd6fa90ecd045024ebffe026faf46fcf87fc Mon Sep 17 00:00:00 2001 From: Rayan Konecny Date: Sun, 3 May 2026 01:16:25 -0300 Subject: [PATCH] add SQLite and adjusts sistems --- toptran-app/.gitignore | 1 + toptran-app/app.json | 5 +- toptran-app/package-lock.json | 139 +++----- toptran-app/package.json | 9 +- toptran-app/src/app/_layout.tsx | 25 +- toptran-app/src/app/corrida.tsx | 121 +++++++ toptran-app/src/app/historico.tsx | 191 +++++++++++ toptran-app/src/app/home.tsx | 387 +++++++++++++++++++++++ toptran-app/src/app/index.tsx | 73 ++++- toptran-app/src/app/lancamento.tsx | 222 +++++++++++++ toptran-app/src/app/signup.tsx | 57 +++- toptran-app/src/components/Button.tsx | 30 +- toptran-app/src/components/Header.tsx | 100 ++++++ toptran-app/src/components/Input.tsx | 17 +- toptran-app/src/components/Select.tsx | 57 ++++ toptran-app/src/constants/theme.ts | 83 +++++ toptran-app/src/contexts/AuthContext.tsx | 140 ++++++++ toptran-app/src/server/api.ts | 3 +- toptran-app/src/services/db.ts | 207 ++++++++++++ toptran-app/tsconfig.json | 21 +- 20 files changed, 1740 insertions(+), 148 deletions(-) create mode 100644 toptran-app/src/app/corrida.tsx create mode 100644 toptran-app/src/app/historico.tsx create mode 100644 toptran-app/src/app/home.tsx create mode 100644 toptran-app/src/app/lancamento.tsx create mode 100644 toptran-app/src/components/Header.tsx create mode 100644 toptran-app/src/components/Select.tsx create mode 100644 toptran-app/src/constants/theme.ts create mode 100644 toptran-app/src/contexts/AuthContext.tsx create mode 100644 toptran-app/src/services/db.ts diff --git a/toptran-app/.gitignore b/toptran-app/.gitignore index d914c32..da9569c 100644 --- a/toptran-app/.gitignore +++ b/toptran-app/.gitignore @@ -31,6 +31,7 @@ yarn-error.* *.pem # local env files +.env .env*.local # typescript diff --git a/toptran-app/app.json b/toptran-app/app.json index 95b6c03..048fb2b 100644 --- a/toptran-app/app.json +++ b/toptran-app/app.json @@ -20,7 +20,10 @@ "output": "static", "favicon": "./assets/toptranico.png" }, - "plugins": ["expo-router"], + "plugins": [ + "expo-router", + "expo-secure-store" + ], "experiments": { "typedRoutes": true } diff --git a/toptran-app/package-lock.json b/toptran-app/package-lock.json index 526fb88..4b1acb4 100644 --- a/toptran-app/package-lock.json +++ b/toptran-app/package-lock.json @@ -9,24 +9,23 @@ "version": "1.0.0", "dependencies": { "@expo/vector-icons": "^15.0.3", - "@react-navigation/native": "^7.1.8", + "@react-native-picker/picker": "^2.7.5", "axios": "^1.15.2", "expo": "~54.0.33", "expo-constants": "~18.0.13", "expo-font": "~14.0.11", "expo-linking": "~8.0.11", "expo-router": "~6.0.23", + "expo-secure-store": "~15.0.8", "expo-splash-screen": "~31.0.13", + "expo-sqlite": "^55.0.15", "expo-status-bar": "~3.0.9", - "expo-web-browser": "~15.0.10", "react": "19.1.0", "react-dom": "19.1.0", "react-native": "0.81.5", - "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", - "react-native-web": "~0.21.0", - "react-native-worklets": "0.5.1" + "react-native-web": "~0.21.0" }, "devDependencies": { "@types/react": "~19.1.0", @@ -1363,21 +1362,6 @@ "@babel/core": "^7.0.0-0" } }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, "node_modules/@babel/plugin-transform-typescript": { "version": "7.28.6", "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.6.tgz", @@ -2386,6 +2370,19 @@ } } }, + "node_modules/@react-native-picker/picker": { + "version": "2.11.4", + "resolved": "https://registry.npmjs.org/@react-native-picker/picker/-/picker-2.11.4.tgz", + "integrity": "sha512-Kf8h1AMnBo54b1fdiVylP2P/iFcZqzpMYcglC28EEFB1DEnOjsNr6Ucqc+3R9e91vHxEDnhZFbYDmAe79P2gjA==", + "license": "MIT", + "workspaces": [ + "example" + ], + "peerDependencies": { + "react": "*", + "react-native": "*" + } + }, "node_modules/@react-native/assets-registry": { "version": "0.81.5", "resolved": "https://registry.npmjs.org/@react-native/assets-registry/-/assets-registry-0.81.5.tgz", @@ -3103,6 +3100,12 @@ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", "license": "MIT" }, + "node_modules/await-lock": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/await-lock/-/await-lock-2.2.2.tgz", + "integrity": "sha512-aDczADvlvTGajTDjcjpJMqRkOF6Qdz3YbPZm/PyW6tKPkx2hlYBzxMhEywM/tU72HrVZjgl5VCdRuMlA7pZ8Gw==", + "license": "MIT" + }, "node_modules/axios": { "version": "1.15.2", "resolved": "https://registry.npmjs.org/axios/-/axios-1.15.2.tgz", @@ -4607,6 +4610,15 @@ "node": ">=10" } }, + "node_modules/expo-secure-store": { + "version": "15.0.8", + "resolved": "https://registry.npmjs.org/expo-secure-store/-/expo-secure-store-15.0.8.tgz", + "integrity": "sha512-lHnzvRajBu4u+P99+0GEMijQMFCOYpWRO4dWsXSuMt77+THPIGjzNvVKrGSl6mMrLsfVaKL8BpwYZLGlgA+zAw==", + "license": "MIT", + "peerDependencies": { + "expo": "*" + } + }, "node_modules/expo-server": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/expo-server/-/expo-server-1.0.6.tgz", @@ -4628,6 +4640,20 @@ "expo": "*" } }, + "node_modules/expo-sqlite": { + "version": "55.0.15", + "resolved": "https://registry.npmjs.org/expo-sqlite/-/expo-sqlite-55.0.15.tgz", + "integrity": "sha512-vxE5fs6l953QSIyievQ8TuSstj62eC7zUREjNzbUOwRWaHGGnhnlPJM1HLoTIv+oIt3+b1m7k2fmcDGkpK5t3w==", + "license": "MIT", + "dependencies": { + "await-lock": "^2.2.2" + }, + "peerDependencies": { + "expo": "*", + "react": "*", + "react-native": "*" + } + }, "node_modules/expo-status-bar": { "version": "3.0.9", "resolved": "https://registry.npmjs.org/expo-status-bar/-/expo-status-bar-3.0.9.tgz", @@ -4641,16 +4667,6 @@ "react-native": "*" } }, - "node_modules/expo-web-browser": { - "version": "15.0.11", - "resolved": "https://registry.npmjs.org/expo-web-browser/-/expo-web-browser-15.0.11.tgz", - "integrity": "sha512-r2LS4Ro6DgUPZkcaEfgt8mp9eJuoA93x11Jh7S6utFe0FEzvUNn2yFhxg8XVwESaaHGt2k5V8LuK36rsp0BeIw==", - "license": "MIT", - "peerDependencies": { - "expo": "*", - "react-native": "*" - } - }, "node_modules/expo/node_modules/@expo/cli": { "version": "54.0.24", "resolved": "https://registry.npmjs.org/@expo/cli/-/cli-54.0.24.tgz", @@ -7450,33 +7466,6 @@ "react-native": "*" } }, - "node_modules/react-native-reanimated": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/react-native-reanimated/-/react-native-reanimated-4.1.7.tgz", - "integrity": "sha512-Q4H6xA3Tn7QL0/E/KjI86I1KK4tcf+ErRE04LH34Etka2oVQhW6oXQ+Q8ZcDCVxiWp5vgbBH6XcH8BOo4w/Rhg==", - "license": "MIT", - "dependencies": { - "react-native-is-edge-to-edge": "^1.2.1", - "semver": "^7.7.2" - }, - "peerDependencies": { - "react": "*", - "react-native": "0.78 - 0.82", - "react-native-worklets": "0.5 - 0.8" - } - }, - "node_modules/react-native-reanimated/node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/react-native-safe-area-context": { "version": "5.6.2", "resolved": "https://registry.npmjs.org/react-native-safe-area-context/-/react-native-safe-area-context-5.6.2.tgz", @@ -7534,42 +7523,6 @@ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==", "license": "MIT" }, - "node_modules/react-native-worklets": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/react-native-worklets/-/react-native-worklets-0.5.1.tgz", - "integrity": "sha512-lJG6Uk9YuojjEX/tQrCbcbmpdLCSFxDK1rJlkDhgqkVi1KZzG7cdcBFQRqyNOOzR9Y0CXNuldmtWTGOyM0k0+w==", - "license": "MIT", - "dependencies": { - "@babel/plugin-transform-arrow-functions": "^7.0.0-0", - "@babel/plugin-transform-class-properties": "^7.0.0-0", - "@babel/plugin-transform-classes": "^7.0.0-0", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.0.0-0", - "@babel/plugin-transform-optional-chaining": "^7.0.0-0", - "@babel/plugin-transform-shorthand-properties": "^7.0.0-0", - "@babel/plugin-transform-template-literals": "^7.0.0-0", - "@babel/plugin-transform-unicode-regex": "^7.0.0-0", - "@babel/preset-typescript": "^7.16.7", - "convert-source-map": "^2.0.0", - "semver": "7.7.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0", - "react": "*", - "react-native": "*" - } - }, - "node_modules/react-native-worklets/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/react-native/node_modules/@react-native/virtualized-lists": { "version": "0.81.5", "resolved": "https://registry.npmjs.org/@react-native/virtualized-lists/-/virtualized-lists-0.81.5.tgz", diff --git a/toptran-app/package.json b/toptran-app/package.json index a0f1252..e84b408 100644 --- a/toptran-app/package.json +++ b/toptran-app/package.json @@ -10,24 +10,23 @@ }, "dependencies": { "@expo/vector-icons": "^15.0.3", - "@react-navigation/native": "^7.1.8", + "@react-native-picker/picker": "^2.7.5", "axios": "^1.15.2", "expo": "~54.0.33", "expo-constants": "~18.0.13", "expo-font": "~14.0.11", "expo-linking": "~8.0.11", "expo-router": "~6.0.23", + "expo-secure-store": "~15.0.8", "expo-splash-screen": "~31.0.13", + "expo-sqlite": "^55.0.15", "expo-status-bar": "~3.0.9", - "expo-web-browser": "~15.0.10", "react": "19.1.0", "react-dom": "19.1.0", "react-native": "0.81.5", - "react-native-reanimated": "~4.1.1", "react-native-safe-area-context": "~5.6.0", "react-native-screens": "~4.16.0", - "react-native-web": "~0.21.0", - "react-native-worklets": "0.5.1" + "react-native-web": "~0.21.0" }, "devDependencies": { "@types/react": "~19.1.0", diff --git a/toptran-app/src/app/_layout.tsx b/toptran-app/src/app/_layout.tsx index 47c1a9f..cd6efd5 100644 --- a/toptran-app/src/app/_layout.tsx +++ b/toptran-app/src/app/_layout.tsx @@ -1,7 +1,20 @@ +import { AuthProvider } from "@/contexts/AuthContext"; +import { initDB } from "@/services/db"; import { Stack } from "expo-router"; +import React, { useEffect } from "react"; +import { Alert } from "react-native"; + +function RootLayoutNav() { + useEffect(() => { + initDB().catch((error) => { + console.error("Database initialization failed:", error); + Alert.alert( + "Erro", + "Falha ao inicializar o banco de dados. Alguns recursos podem não funcionar corretamente.", + ); + }); + }, []); -// Layout para as páginas de login e cadastro de usuário, sem header -export default function Layout() { return ( ); } + +export default function Layout() { + return ( + + + + ); +} diff --git a/toptran-app/src/app/corrida.tsx b/toptran-app/src/app/corrida.tsx new file mode 100644 index 0000000..f564ead --- /dev/null +++ b/toptran-app/src/app/corrida.tsx @@ -0,0 +1,121 @@ +import { useAuth } from "@/contexts/AuthContext"; +import { obterCorridas } from "@/services/db"; +import React, { useCallback, useEffect, useState } from "react"; +import { StyleSheet, Text, TouchableOpacity, View } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; +import Historico from "./historico"; +import Lancamento, { type Corrida } from "./lancamento"; + +export default function Corrida() { + const { user } = useAuth(); + const [activeTab, setActiveTab] = useState<"lancamento" | "historico">( + "lancamento", + ); + const [historico, setHistorico] = useState([]); + + const loadHistorico = useCallback(async () => { + if (!user?.id) return; + try { + const corridasDB = await obterCorridas(user.id); + setHistorico( + corridasDB.map((c) => ({ + id: c.id, + data: c.data, + empresa: c.empresa, + km: c.km, + custoPorKm: c.custo_por_km, + total: c.total, + usuario: user.email, + })), + ); + } catch (error) { + console.error("Erro ao carregar histórico:", error); + } + }, [user?.id, user?.email]); + + useEffect(() => { + loadHistorico(); + }, [loadHistorico]); + + const handleLancarCorrida = async (_corrida: Corrida) => { + await loadHistorico(); + setActiveTab("historico"); + }; + + return ( + + + setActiveTab("lancamento")} + > + + 📍 Lançar + + + + setActiveTab("historico")} + > + + 📋 Histórico + + + + + + {activeTab === "lancamento" ? ( + + ) : ( + + )} + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#ffffff", + }, + tabsContainer: { + flexDirection: "row", + backgroundColor: "#ffffff", + borderBottomWidth: 2, + borderBottomColor: "#000000", + }, + tab: { + flex: 1, + paddingVertical: 16, + justifyContent: "center", + alignItems: "center", + borderBottomWidth: 3, + borderBottomColor: "transparent", + }, + tabActive: { + borderBottomColor: "#000000", + }, + tabLabel: { + fontSize: 14, + fontWeight: "600", + color: "#666666", + }, + tabLabelActive: { + color: "#000000", + }, + content: { + flex: 1, + }, +}); diff --git a/toptran-app/src/app/historico.tsx b/toptran-app/src/app/historico.tsx new file mode 100644 index 0000000..6c5a871 --- /dev/null +++ b/toptran-app/src/app/historico.tsx @@ -0,0 +1,191 @@ +import React from "react"; +import { FlatList, ScrollView, StyleSheet, Text, View } from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; + +export type Corrida = { + id: string; + data: string; + empresa: string; + km: number; + custoPorKm: number; + total: number; + usuario: string; +}; + +interface HistoricoProps { + historico: Corrida[]; +} + +const HistoricoItem = ({ item, index }: { item: Corrida; index: number }) => ( + + + #{index + 1} + {item.data} + + + + {item.empresa} + + + + + Distância + {item.km} km + + + + Valor/km + R$ {item.custoPorKm.toFixed(2)} + + + + Total + R$ {item.total.toFixed(2)} + + + +); + +export default function Historico({ historico }: HistoricoProps) { + return ( + + + Histórico de Corridas + + {historico.length} corrida{historico.length !== 1 ? "s" : ""} + + + + {historico.length === 0 ? ( + + + 🚗 + Nenhuma corrida registrada + Suas corridas aparecerão aqui + + + ) : ( + item.id} + renderItem={({ item, index }) => ( + + )} + contentContainerStyle={styles.listContent} + showsVerticalScrollIndicator={false} + /> + )} + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: "#ffffff", + }, + header: { + paddingHorizontal: 16, + paddingTop: 16, + paddingBottom: 24, + backgroundColor: "#ffffff", + borderBottomWidth: 2, + borderBottomColor: "#000000", + }, + title: { + fontSize: 28, + fontWeight: "800", + color: "#000000", + marginBottom: 4, + }, + subtitle: { + fontSize: 14, + color: "#333333", + }, + listContent: { + padding: 16, + paddingBottom: 32, + }, + item: { + backgroundColor: "#ffffff", + borderRadius: 12, + marginBottom: 12, + overflow: "hidden", + borderWidth: 1, + borderColor: "#000000", + }, + itemHeader: { + flexDirection: "row", + paddingHorizontal: 16, + paddingTop: 12, + paddingBottom: 8, + justifyContent: "space-between", + alignItems: "center", + borderBottomWidth: 1, + borderBottomColor: "#e0e0e0", + }, + itemNumber: { + fontSize: 12, + fontWeight: "700", + color: "#000000", + }, + itemDate: { + fontSize: 12, + color: "#666666", + }, + itemEmpresa: { + paddingHorizontal: 16, + paddingVertical: 10, + backgroundColor: "#f5f5f5", + }, + empresaLabel: { + fontSize: 14, + fontWeight: "600", + color: "#000000", + }, + itemDetails: { + flexDirection: "row", + paddingHorizontal: 16, + paddingVertical: 12, + gap: 8, + }, + detailBox: { + flex: 1, + paddingVertical: 8, + }, + detailLabel: { + fontSize: 11, + color: "#666666", + fontWeight: "500", + marginBottom: 2, + }, + detailValue: { + fontSize: 14, + fontWeight: "700", + color: "#000000", + }, + emptyContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: 16, + }, + emptyBox: { + alignItems: "center", + }, + emptyEmoji: { + fontSize: 48, + marginBottom: 16, + }, + emptyTitle: { + fontSize: 18, + fontWeight: "600", + color: "#000000", + marginBottom: 8, + }, + emptyText: { + fontSize: 14, + color: "#666666", + textAlign: "center", + }, +}); diff --git a/toptran-app/src/app/home.tsx b/toptran-app/src/app/home.tsx new file mode 100644 index 0000000..79b7d30 --- /dev/null +++ b/toptran-app/src/app/home.tsx @@ -0,0 +1,387 @@ +import { BORDER_RADIUS, COLORS, SPACING } from "@/constants/theme"; +import { useAuth } from "@/contexts/AuthContext"; +import { obterCorridas } from "@/services/db"; +import { router, useFocusEffect } from "expo-router"; +import React, { useCallback, useState } from "react"; +import { + Alert, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View, +} from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; + +type Stats = { + totalCorridas: number; + totalKm: number; + totalGanhos: number; +}; + +type Action = { + icon: string; + label: string; + description: string; + onPress: () => void; + highlight?: boolean; +}; + +function greeting() { + const h = new Date().getHours(); + if (h < 12) return "Bom dia"; + if (h < 18) return "Boa tarde"; + return "Boa noite"; +} + +export default function Home() { + const { user, logout } = useAuth(); + const userName = user?.name ?? "Usuário"; + const firstName = userName.split(" ")[0]; + + const [stats, setStats] = useState({ + totalCorridas: 0, + totalKm: 0, + totalGanhos: 0, + }); + + const loadStats = useCallback(async () => { + if (!user?.id) return; + try { + const corridas = await obterCorridas(user.id); + setStats({ + totalCorridas: corridas.length, + totalKm: corridas.reduce((acc, c) => acc + c.km, 0), + totalGanhos: corridas.reduce((acc, c) => acc + c.total, 0), + }); + } catch (error) { + console.error("Erro ao carregar estatísticas:", error); + } + }, [user?.id]); + + useFocusEffect(useCallback(() => { loadStats(); }, [loadStats])); + + const handleProfilePress = () => { + Alert.alert(userName, user?.email ?? "", [ + { + text: "Editar Perfil", + onPress: () => Alert.alert("Em breve", "Funcionalidade em desenvolvimento."), + }, + { + text: "Sair", + style: "destructive", + onPress: () => + Alert.alert("Sair", "Tem certeza que deseja sair?", [ + { text: "Cancelar", style: "cancel" }, + { + text: "Sair", + style: "destructive", + onPress: async () => { + try { + await logout(); + } catch (e) { + console.error("Logout error:", e); + } + }, + }, + ]), + }, + { text: "Cancelar", style: "cancel" }, + ]); + }; + + const today = new Date().toLocaleDateString("pt-BR", { + weekday: "long", + day: "numeric", + month: "long", + }); + + const actions: Action[] = [ + { + icon: "🚗", + label: "Registrar Corrida", + description: "Lançar nova corrida", + onPress: () => router.push("/corrida"), + highlight: true, + }, + { + icon: "📋", + label: "Histórico", + description: "Ver corridas anteriores", + onPress: () => router.push("/corrida"), + }, + { + icon: "📊", + label: "Relatório", + description: "Análise de ganhos", + onPress: () => Alert.alert("Em breve", "Funcionalidade em desenvolvimento."), + }, + { + icon: "⚙️", + label: "Configurações", + description: "Preferências do app", + onPress: () => Alert.alert("Em breve", "Funcionalidade em desenvolvimento."), + }, + ]; + + return ( + + {/* Header */} + + + TopTran + Sistema de Gestão + + + + + {firstName.charAt(0).toUpperCase()} + + + + + + + + {/* Greeting */} + + + {greeting()}, {firstName}! 👋 + + {today} + + + {/* Stats */} + Resumo Geral + + + {stats.totalCorridas} + Corridas + + + + {stats.totalKm.toFixed(0)} + + Km Rodados + + + + R${stats.totalGanhos.toFixed(0)} + + + Total Ganho + + + + + {/* Actions Grid */} + Acesso Rápido + + {actions.map((action, index) => ( + + {action.icon} + + {action.label} + + {action.description} + + ))} + + + © 2025 TopTran + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: COLORS.background, + }, + + // Header + header: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + paddingHorizontal: SPACING.lg, + paddingVertical: SPACING.md, + borderBottomWidth: 1, + borderBottomColor: COLORS.border, + }, + headerLogo: { + fontSize: 22, + fontWeight: "900", + color: COLORS.text, + letterSpacing: 1, + }, + headerSub: { + fontSize: 11, + color: COLORS.textTertiary, + letterSpacing: 0.5, + }, + avatarButton: { + position: "relative", + }, + avatar: { + width: 42, + height: 42, + borderRadius: BORDER_RADIUS.round, + backgroundColor: COLORS.surfaceLight, + borderWidth: 2, + borderColor: COLORS.borderLight, + justifyContent: "center", + alignItems: "center", + }, + avatarText: { + color: COLORS.text, + fontSize: 18, + fontWeight: "700", + }, + avatarOnline: { + position: "absolute", + bottom: 1, + right: 1, + width: 10, + height: 10, + borderRadius: 5, + backgroundColor: COLORS.success, + borderWidth: 2, + borderColor: COLORS.background, + }, + + // Scroll + scrollContent: { + padding: SPACING.lg, + paddingBottom: SPACING.xxl, + }, + + // Greeting + greetingSection: { + marginTop: SPACING.md, + marginBottom: SPACING.xl, + }, + greetingText: { + fontSize: 26, + fontWeight: "800", + color: COLORS.text, + marginBottom: 4, + }, + dateText: { + fontSize: 13, + color: COLORS.textTertiary, + textTransform: "capitalize", + }, + + // Section title + sectionTitle: { + fontSize: 12, + fontWeight: "700", + color: COLORS.textTertiary, + textTransform: "uppercase", + letterSpacing: 1.2, + marginBottom: SPACING.md, + }, + + // Stats + statsRow: { + flexDirection: "row", + gap: SPACING.sm, + marginBottom: SPACING.xl, + }, + statCard: { + flex: 1, + backgroundColor: COLORS.surface, + borderRadius: BORDER_RADIUS.lg, + padding: SPACING.md, + borderWidth: 1, + borderColor: COLORS.border, + }, + statCardAccent: { + backgroundColor: COLORS.surfaceLight, + borderColor: COLORS.borderLight, + }, + statValue: { + fontSize: 20, + fontWeight: "800", + color: COLORS.text, + marginBottom: 2, + }, + statValueAccent: { + color: COLORS.success, + }, + statLabel: { + fontSize: 10, + color: COLORS.textTertiary, + fontWeight: "600", + textTransform: "uppercase", + letterSpacing: 0.5, + }, + statLabelAccent: { + color: COLORS.textSecondary, + }, + + // Actions grid + actionsGrid: { + flexDirection: "row", + flexWrap: "wrap", + gap: SPACING.sm, + marginBottom: SPACING.xl, + }, + actionCard: { + width: "48%", + backgroundColor: COLORS.surface, + borderRadius: BORDER_RADIUS.lg, + padding: SPACING.lg, + borderWidth: 1, + borderColor: COLORS.border, + minHeight: 110, + justifyContent: "space-between", + }, + actionCardHighlight: { + backgroundColor: COLORS.surfaceLight, + borderColor: COLORS.text, + }, + actionIcon: { + fontSize: 28, + marginBottom: SPACING.sm, + }, + actionLabel: { + fontSize: 14, + fontWeight: "700", + color: COLORS.text, + marginBottom: 2, + }, + actionLabelHighlight: { + color: COLORS.text, + }, + actionDescription: { + fontSize: 11, + color: COLORS.textTertiary, + }, + + // Footer + footer: { + textAlign: "center", + fontSize: 12, + color: COLORS.textTertiary, + marginTop: SPACING.sm, + }, +}); diff --git a/toptran-app/src/app/index.tsx b/toptran-app/src/app/index.tsx index 644f413..841fd30 100644 --- a/toptran-app/src/app/index.tsx +++ b/toptran-app/src/app/index.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import React, { useState } from "react"; import { Alert, @@ -13,28 +13,71 @@ import { import { Button } from "@/components/Button"; import { Input } from "@/components/Input"; +import { COLORS, SPACING } from "@/constants/theme"; +import { useAuth } from "@/contexts/AuthContext"; import { api, setAuthToken } from "@/server/api"; +import { salvarUsuario } from "@/services/db"; import { Link } from "expo-router"; export default function IndexPage() { const [email, setEmail] = useState(""); const [password, setPassword] = useState(""); const [loading, setLoading] = useState(false); + const { login } = useAuth(); + + function isValidEmail(emailToCheck: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(emailToCheck); + } async function handleSignIn() { - if (!email.trim() || !password.trim()) { + const emailTrimmed = email.trim(); + const passwordTrimmed = password.trim(); + + if (!emailTrimmed || !passwordTrimmed) { Alert.alert("Entrar", "Por favor, preencha todos os campos."); return; } + if (!isValidEmail(emailTrimmed)) { + Alert.alert( + "Email inválido", + "Por favor, digite um email válido (ex: usuario@email.com).", + ); + return; + } + + if (passwordTrimmed.length < 6) { + Alert.alert("Senha fraca", "A senha deve ter pelo menos 6 caracteres."); + return; + } + try { setLoading(true); - const { data } = await api.post("/auth/login", { email, password }); + const { data } = await api.post("/auth/login", { + email: emailTrimmed, + password: passwordTrimmed, + }); - console.log(data.accessToken); setAuthToken(data.accessToken); + + // Salvar no SQLite + const userId = Date.now().toString(); + await salvarUsuario({ + id: userId, + email: emailTrimmed, + name: data.name || emailTrimmed.split("@")[0], + token: data.accessToken, + }); + + // Fazer login no contexto + await login(emailTrimmed, passwordTrimmed, data.accessToken, { + id: userId, + email: emailTrimmed, + name: data.name || emailTrimmed.split("@")[0], + }); } catch (err: any) { - const message = err.response?.data?.error ?? "Erro ao fazer login."; + const message = err.response?.data?.error ?? err.message ?? "Erro ao fazer login."; Alert.alert("Erro", message); } finally { setLoading(false); @@ -98,33 +141,33 @@ export default function IndexPage() { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: "#0e0d0d", - padding: 32, + backgroundColor: COLORS.background, + padding: SPACING.xxl, }, illustration: { width: "100%", height: 330, resizeMode: "contain", - marginTop: 62, + marginTop: SPACING.xxl, }, title: { - marginTop: 62, + marginTop: SPACING.xxl, fontSize: 32, fontWeight: "900", - color: "#e7e7e7", + color: COLORS.text, }, subtitle: { fontSize: 16, - color: "#a1a1a1", + color: COLORS.textSecondary, }, form: { - marginTop: 24, - gap: 16, + marginTop: SPACING.lg, + gap: SPACING.lg, }, footerText: { textAlign: "center", - marginTop: 24, - color: "#a1a1a1", + marginTop: SPACING.lg, + color: COLORS.textSecondary, }, footerLink: { color: "#007AFF", diff --git a/toptran-app/src/app/lancamento.tsx b/toptran-app/src/app/lancamento.tsx new file mode 100644 index 0000000..a82a79a --- /dev/null +++ b/toptran-app/src/app/lancamento.tsx @@ -0,0 +1,222 @@ +import { Button } from "@/components/Button"; +import { Input } from "@/components/Input"; +import { Select } from "@/components/Select"; +import { useAuth } from "@/contexts/AuthContext"; +import { salvarCorrida } from "@/services/db"; +import React, { useState } from "react"; +import { + Alert, + KeyboardAvoidingView, + Platform, + ScrollView, + StyleSheet, + Text, + View, +} from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; + +const EMPRESAS = [ + { label: "Selecione uma empresa", value: "" }, + { label: "MULTI B", value: "MULTI_B" }, + { label: "TOP TRANS", value: "TOP_TRANS" }, + { label: "LOGISTICA XYZ", value: "LOGISTICA_XYZ" }, +]; + +const CUSTO_POR_KM = 2.7; + +export type Corrida = { + id: string; + data: string; + empresa: string; + km: number; + custoPorKm: number; + total: number; + usuario: string; +}; + +interface LancamentoProps { + onLancar: (corrida: Corrida) => void; +} + +export default function Lancamento({ onLancar }: LancamentoProps) { + const { user } = useAuth(); + const [empresa, setEmpresa] = useState(""); + const [distancia, setDistancia] = useState(""); + + const distanciaNum = parseFloat(distancia) || 0; + const totalCorrida = distanciaNum * CUSTO_POR_KM; + + const handleLancarCorrida = async () => { + if (!empresa) { + Alert.alert("Validação", "Por favor, selecione uma empresa."); + return; + } + + if (!distancia || distanciaNum <= 0) { + Alert.alert("Validação", "Por favor, insira uma distância válida."); + return; + } + + const empresaSelecionada = EMPRESAS.find((e) => e.value === empresa); + const novaCorrida: Corrida = { + id: Date.now().toString(), + data: new Date().toLocaleString("pt-BR"), + empresa: empresaSelecionada?.label || empresa, + km: distanciaNum, + custoPorKm: CUSTO_POR_KM, + total: totalCorrida, + usuario: user?.email ?? "", + }; + + try { + await salvarCorrida({ + id: novaCorrida.id, + usuario_id: user?.id ?? "", + empresa: novaCorrida.empresa, + km: novaCorrida.km, + custo_por_km: novaCorrida.custoPorKm, + total: novaCorrida.total, + data: novaCorrida.data, + sincronizado: 0, + }); + } catch (error) { + console.error("Erro ao salvar corrida:", error); + } + + onLancar(novaCorrida); + setEmpresa(""); + setDistancia(""); + Alert.alert("Sucesso", "Corrida registrada com sucesso!"); + }; + + return ( + + + + + Nova Corrida + Preencha os dados da corrida + + + + Empresa + + + + + + Custo por km + + R$ {CUSTO_POR_KM.toFixed(2)} + + + + + + + Valor Total + + R$ {totalCorrida.toFixed(2)} + + + + +