diff --git a/toptran-app/database/create_tables.sql b/toptran-app/database/create_tables.sql new file mode 100644 index 0000000..3cf6d4d --- /dev/null +++ b/toptran-app/database/create_tables.sql @@ -0,0 +1,36 @@ +-- ============================================================ +-- TopTran — Script de criação das tabelas no PostgreSQL +-- ============================================================ + +CREATE TABLE IF NOT EXISTS empresas ( + id TEXT PRIMARY KEY, + nome TEXT NOT NULL, + custo_por_km NUMERIC(10,2) NOT NULL, + observacoes TEXT DEFAULT '', + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS usuarios ( + id TEXT PRIMARY KEY, + email TEXT NOT NULL UNIQUE, + name TEXT NOT NULL, + token TEXT, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +CREATE TABLE IF NOT EXISTS corridas ( + id TEXT PRIMARY KEY, + usuario_id TEXT NOT NULL REFERENCES usuarios(id) ON DELETE CASCADE, + empresa TEXT NOT NULL, + km NUMERIC(10,2) NOT NULL, + custo_por_km NUMERIC(10,2) NOT NULL, + total NUMERIC(10,2) NOT NULL, + data TEXT NOT NULL, + sincronizado SMALLINT DEFAULT 0, + created_at TIMESTAMPTZ DEFAULT NOW() +); + +-- Índices para performance +CREATE INDEX IF NOT EXISTS idx_corridas_usuario_id ON corridas(usuario_id); +CREATE INDEX IF NOT EXISTS idx_corridas_sincronizado ON corridas(sincronizado); +CREATE INDEX IF NOT EXISTS idx_usuarios_email ON usuarios(email); diff --git a/toptran-app/src/app/cadastros.tsx b/toptran-app/src/app/cadastros.tsx new file mode 100644 index 0000000..7f3eca7 --- /dev/null +++ b/toptran-app/src/app/cadastros.tsx @@ -0,0 +1,190 @@ +import { BORDER_RADIUS, COLORS, SPACING } from "@/constants/theme"; +import { router } from "expo-router"; +import React from "react"; +import { + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View, +} from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; + +type CadastroCard = { + icon: string; + label: string; + description: string; + onPress: () => void; + highlight?: boolean; +}; + +export default function CadastrosPage() { + const cards: CadastroCard[] = [ + { + icon: "🏢", + label: "Empresas", + description: "Gerencie empresas e custos por km", + onPress: () => router.push("/empresas"), + highlight: true, + }, + { + icon: "👤", + label: "Motoristas", + description: "Cadastro de motoristas", + onPress: () => {}, + }, + { + icon: "🚛", + label: "Veículos", + description: "Cadastro de veículos", + onPress: () => {}, + }, + { + icon: "📍", + label: "Rotas", + description: "Rotas e destinos frequentes", + onPress: () => {}, + }, + ]; + + return ( + + + router.back()} + style={styles.backButton} + activeOpacity={0.7} + > + + Voltar + + + + + + Cadastros + + Gerencie os dados base do sistema + + + + Categorias + + {cards.map((card, index) => ( + + {card.icon} + + {card.label} + + {card.description} + + ))} + + + + ); +} + +const styles = StyleSheet.create({ + container: { + flex: 1, + backgroundColor: COLORS.background, + }, + header: { + paddingHorizontal: SPACING.lg, + paddingVertical: SPACING.md, + borderBottomWidth: 1, + borderBottomColor: COLORS.border, + }, + backButton: { + flexDirection: "row", + alignItems: "center", + gap: SPACING.xs, + alignSelf: "flex-start", + }, + backIcon: { + fontSize: 18, + color: COLORS.text, + }, + backLabel: { + fontSize: 15, + color: COLORS.textSecondary, + fontWeight: "500", + }, + scrollContent: { + padding: SPACING.lg, + paddingBottom: SPACING.xxl, + }, + titleSection: { + marginBottom: SPACING.xl, + marginTop: SPACING.md, + }, + title: { + fontSize: 28, + fontWeight: "800", + color: COLORS.text, + marginBottom: 4, + }, + subtitle: { + fontSize: 14, + color: COLORS.textTertiary, + }, + sectionTitle: { + fontSize: 11, + fontWeight: "700", + color: COLORS.textTertiary, + textTransform: "uppercase", + letterSpacing: 1.2, + marginBottom: SPACING.md, + }, + grid: { + flexDirection: "row", + flexWrap: "wrap", + gap: SPACING.sm, + }, + card: { + width: "48%", + backgroundColor: COLORS.surface, + borderRadius: BORDER_RADIUS.lg, + padding: SPACING.lg, + borderWidth: 1, + borderColor: COLORS.border, + minHeight: 120, + justifyContent: "space-between", + }, + cardHighlight: { + backgroundColor: COLORS.surfaceLight, + borderColor: COLORS.text, + }, + cardIcon: { + fontSize: 30, + marginBottom: SPACING.sm, + }, + cardLabel: { + fontSize: 15, + fontWeight: "700", + color: COLORS.text, + marginBottom: 3, + }, + cardLabelHighlight: { + color: COLORS.text, + }, + cardDescription: { + fontSize: 11, + color: COLORS.textTertiary, + lineHeight: 15, + }, +}); diff --git a/toptran-app/src/app/corrida.tsx b/toptran-app/src/app/corrida.tsx index f564ead..5657382 100644 --- a/toptran-app/src/app/corrida.tsx +++ b/toptran-app/src/app/corrida.tsx @@ -1,121 +1,9 @@ -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"; +import { router } from "expo-router"; +import { useEffect } from "react"; 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" ? ( - - ) : ( - - )} - - - ); + router.replace("/lancamento"); + }, []); + return null; } - -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/empresas.tsx b/toptran-app/src/app/empresas.tsx new file mode 100644 index 0000000..ee271e3 --- /dev/null +++ b/toptran-app/src/app/empresas.tsx @@ -0,0 +1,520 @@ +import { Input } from "@/components/Input"; +import { BORDER_RADIUS, COLORS, SPACING } from "@/constants/theme"; +import { + EmpresaDB, + atualizarEmpresa, + deletarEmpresa, + obterEmpresas, + salvarEmpresa, +} from "@/services/db"; +import { router, useFocusEffect } from "expo-router"; +import React, { useCallback, useState } from "react"; +import { + Alert, + FlatList, + KeyboardAvoidingView, + Modal, + Platform, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View, +} from "react-native"; +import { SafeAreaView } from "react-native-safe-area-context"; + +type FormState = { + nome: string; + custoPorKm: string; + observacoes: string; +}; + +const FORM_VAZIO: FormState = { nome: "", custoPorKm: "", observacoes: "" }; + +export default function EmpresasPage() { + const [empresas, setEmpresas] = useState([]); + const [modalVisible, setModalVisible] = useState(false); + const [editando, setEditando] = useState(null); + const [form, setForm] = useState(FORM_VAZIO); + const [loading, setLoading] = useState(false); + + const loadEmpresas = useCallback(async () => { + try { + setEmpresas(await obterEmpresas()); + } catch { + Alert.alert("Erro", "Não foi possível carregar as empresas."); + } + }, []); + + useFocusEffect(useCallback(() => { loadEmpresas(); }, [loadEmpresas])); + + const abrirAdicionar = () => { + setEditando(null); + setForm(FORM_VAZIO); + setModalVisible(true); + }; + + const abrirEditar = (empresa: EmpresaDB) => { + setEditando(empresa); + setForm({ + nome: empresa.nome, + custoPorKm: empresa.custo_por_km.toString(), + observacoes: empresa.observacoes ?? "", + }); + setModalVisible(true); + }; + + const fecharModal = () => { + setModalVisible(false); + setEditando(null); + setForm(FORM_VAZIO); + }; + + const handleSalvar = async () => { + const nomeTrimmed = form.nome.trim(); + const custo = parseFloat(form.custoPorKm.replace(",", ".")); + + if (!nomeTrimmed) { + Alert.alert("Validação", "Informe o nome da empresa."); + return; + } + if (isNaN(custo) || custo <= 0) { + Alert.alert("Validação", "Informe um custo por km válido."); + return; + } + + try { + setLoading(true); + if (editando) { + await atualizarEmpresa({ + id: editando.id, + nome: nomeTrimmed, + custo_por_km: custo, + observacoes: form.observacoes.trim(), + }); + } else { + await salvarEmpresa({ + id: Date.now().toString(), + nome: nomeTrimmed, + custo_por_km: custo, + observacoes: form.observacoes.trim(), + }); + } + await loadEmpresas(); + fecharModal(); + } catch { + Alert.alert("Erro", "Não foi possível salvar a empresa."); + } finally { + setLoading(false); + } + }; + + const handleDeletar = (empresa: EmpresaDB) => { + Alert.alert( + "Excluir empresa", + `Deseja excluir "${empresa.nome}"? Esta ação não pode ser desfeita.`, + [ + { text: "Cancelar", style: "cancel" }, + { + text: "Excluir", + style: "destructive", + onPress: async () => { + try { + await deletarEmpresa(empresa.id); + await loadEmpresas(); + } catch { + Alert.alert("Erro", "Não foi possível excluir a empresa."); + } + }, + }, + ], + ); + }; + + const renderItem = ({ item, index }: { item: EmpresaDB; index: number }) => ( + + + + {index + 1} + + + {item.nome} + R$ {item.custo_por_km.toFixed(2)} / km + {item.observacoes ? ( + + {item.observacoes} + + ) : null} + + + + abrirEditar(item)} + activeOpacity={0.7} + > + ✏️ + + handleDeletar(item)} + activeOpacity={0.7} + > + 🗑️ + + + + ); + + return ( + + {/* Header */} + + router.back()} + style={styles.backButton} + activeOpacity={0.7} + > + + Voltar + + + + Adicionar + + + + {/* Title */} + + Empresas + + {empresas.length} empresa{empresas.length !== 1 ? "s" : ""} cadastrada{empresas.length !== 1 ? "s" : ""} + + + + {/* Lista */} + {empresas.length === 0 ? ( + + 🏢 + Nenhuma empresa cadastrada + + Toque em "+ Adicionar" para cadastrar a primeira empresa. + + + ) : ( + item.id} + renderItem={renderItem} + contentContainerStyle={styles.listContent} + showsVerticalScrollIndicator={false} + /> + )} + + {/* Modal Add/Edit */} + + + + + + + + {editando ? "Editar Empresa" : "Nova Empresa"} + + + + + + + + + Nome da Empresa + setForm((f) => ({ ...f, nome: v }))} + autoCapitalize="characters" + /> + + + + Custo por km (R$) + setForm((f) => ({ ...f, custoPorKm: v }))} + /> + + + + Observações (opcional) + setForm((f) => ({ ...f, observacoes: v }))} + /> + + + + + {loading ? "Salvando..." : editando ? "Salvar alterações" : "Cadastrar empresa"} + + + + + + + + ); +} + +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, + }, + backButton: { + flexDirection: "row", + alignItems: "center", + gap: SPACING.xs, + }, + backIcon: { + fontSize: 18, + color: COLORS.text, + }, + backLabel: { + fontSize: 15, + color: COLORS.textSecondary, + fontWeight: "500", + }, + addButton: { + backgroundColor: COLORS.surface, + borderWidth: 1, + borderColor: COLORS.borderLight, + paddingHorizontal: SPACING.md, + paddingVertical: SPACING.sm, + borderRadius: BORDER_RADIUS.md, + }, + addButtonText: { + fontSize: 13, + fontWeight: "700", + color: COLORS.text, + }, + + // Title + titleSection: { + paddingHorizontal: SPACING.lg, + paddingTop: SPACING.lg, + paddingBottom: SPACING.md, + }, + title: { + fontSize: 28, + fontWeight: "800", + color: COLORS.text, + marginBottom: 4, + }, + subtitle: { + fontSize: 13, + color: COLORS.textTertiary, + }, + + // List + listContent: { + padding: SPACING.lg, + paddingBottom: SPACING.xxl, + }, + item: { + flexDirection: "row", + alignItems: "center", + justifyContent: "space-between", + backgroundColor: COLORS.surface, + borderRadius: BORDER_RADIUS.lg, + marginBottom: SPACING.sm, + padding: SPACING.md, + borderWidth: 1, + borderColor: COLORS.border, + }, + itemLeft: { + flexDirection: "row", + alignItems: "center", + flex: 1, + gap: SPACING.md, + }, + itemIndex: { + width: 32, + height: 32, + borderRadius: BORDER_RADIUS.round, + backgroundColor: COLORS.surfaceLight, + justifyContent: "center", + alignItems: "center", + }, + itemIndexText: { + fontSize: 12, + fontWeight: "700", + color: COLORS.textTertiary, + }, + itemInfo: { + flex: 1, + }, + itemNome: { + fontSize: 15, + fontWeight: "700", + color: COLORS.text, + marginBottom: 2, + }, + itemCusto: { + fontSize: 13, + color: COLORS.success, + fontWeight: "600", + }, + itemObs: { + fontSize: 11, + color: COLORS.textTertiary, + marginTop: 2, + }, + itemActions: { + flexDirection: "row", + gap: SPACING.xs, + }, + actionBtn: { + width: 36, + height: 36, + borderRadius: BORDER_RADIUS.md, + justifyContent: "center", + alignItems: "center", + borderWidth: 1, + }, + editBtn: { + backgroundColor: COLORS.surfaceLight, + borderColor: COLORS.border, + }, + deleteBtn: { + backgroundColor: COLORS.surfaceLight, + borderColor: COLORS.border, + }, + actionBtnText: { + fontSize: 15, + }, + + // Empty + emptyContainer: { + flex: 1, + justifyContent: "center", + alignItems: "center", + padding: SPACING.lg, + }, + emptyEmoji: { + fontSize: 48, + marginBottom: SPACING.lg, + }, + emptyTitle: { + fontSize: 18, + fontWeight: "700", + color: COLORS.text, + marginBottom: SPACING.sm, + textAlign: "center", + }, + emptyText: { + fontSize: 14, + color: COLORS.textTertiary, + textAlign: "center", + lineHeight: 20, + }, + + // Modal + modalOverlay: { + flex: 1, + justifyContent: "flex-end", + backgroundColor: "rgba(0,0,0,0.7)", + }, + modalSheet: { + backgroundColor: COLORS.surface, + borderTopLeftRadius: 24, + borderTopRightRadius: 24, + padding: SPACING.lg, + paddingBottom: SPACING.xxl, + borderTopWidth: 1, + borderColor: COLORS.border, + maxHeight: "85%", + }, + modalHandle: { + width: 40, + height: 4, + borderRadius: 2, + backgroundColor: COLORS.border, + alignSelf: "center", + marginBottom: SPACING.lg, + }, + modalHeader: { + flexDirection: "row", + justifyContent: "space-between", + alignItems: "center", + marginBottom: SPACING.xl, + }, + modalTitle: { + fontSize: 20, + fontWeight: "800", + color: COLORS.text, + }, + modalClose: { + fontSize: 18, + color: COLORS.textTertiary, + padding: SPACING.xs, + }, + + // Form + formGroup: { + marginBottom: SPACING.lg, + }, + formLabel: { + fontSize: 11, + fontWeight: "700", + color: COLORS.textTertiary, + textTransform: "uppercase", + letterSpacing: 0.8, + marginBottom: SPACING.sm, + }, + saveButton: { + backgroundColor: COLORS.text, + borderRadius: BORDER_RADIUS.lg, + paddingVertical: SPACING.lg, + alignItems: "center", + marginTop: SPACING.sm, + }, + saveButtonDisabled: { + backgroundColor: COLORS.borderLight, + }, + saveButtonText: { + fontSize: 15, + fontWeight: "700", + color: COLORS.background, + }, +}); diff --git a/toptran-app/src/app/historico.tsx b/toptran-app/src/app/historico.tsx index 6c5a871..fa6995c 100644 --- a/toptran-app/src/app/historico.tsx +++ b/toptran-app/src/app/historico.tsx @@ -1,21 +1,27 @@ -import React from "react"; -import { FlatList, ScrollView, StyleSheet, Text, View } from "react-native"; +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 { + FlatList, + ScrollView, + StyleSheet, + Text, + TouchableOpacity, + View, +} from "react-native"; import { SafeAreaView } from "react-native-safe-area-context"; -export type Corrida = { +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 }) => ( @@ -32,41 +38,92 @@ const HistoricoItem = ({ item, index }: { item: Corrida; index: number }) => ( Distância {item.km} km - Valor/km R$ {item.custoPorKm.toFixed(2)} - - + Total - R$ {item.total.toFixed(2)} + + R$ {item.total.toFixed(2)} + ); -export default function Historico({ historico }: HistoricoProps) { +export default function HistoricoPage() { + const { user } = useAuth(); + const [corridas, setCorridas] = useState([]); + + const loadCorridas = useCallback(async () => { + if (!user?.id) return; + try { + const data = await obterCorridas(user.id); + setCorridas( + data.map((c) => ({ + id: c.id, + data: c.data, + empresa: c.empresa, + km: c.km, + custoPorKm: c.custo_por_km, + total: c.total, + })), + ); + } catch (error) { + console.error("Erro ao carregar histórico:", error); + } + }, [user?.id]); + + useFocusEffect(useCallback(() => { loadCorridas(); }, [loadCorridas])); + + const totalGanhos = corridas.reduce((acc, c) => acc + c.total, 0); + const totalKm = corridas.reduce((acc, c) => acc + c.km, 0); + return ( - Histórico de Corridas + router.back()} style={styles.backButton} activeOpacity={0.7}> + + Voltar + + + + + Histórico - {historico.length} corrida{historico.length !== 1 ? "s" : ""} + {corridas.length} corrida{corridas.length !== 1 ? "s" : ""} registrada{corridas.length !== 1 ? "s" : ""} - {historico.length === 0 ? ( + {corridas.length > 0 && ( + + + {totalKm.toFixed(0)} km + Total Rodado + + + + R$ {totalGanhos.toFixed(2)} + + Total Ganho + + + )} + + {corridas.length === 0 ? ( 🚗 - Nenhuma corrida registrada - Suas corridas aparecerão aqui + Nenhuma corrida ainda + + Registre sua primeira corrida para vê-la aqui. + ) : ( item.id} renderItem={({ item, index }) => ( @@ -82,110 +139,170 @@ export default function Historico({ historico }: HistoricoProps) { const styles = StyleSheet.create({ container: { flex: 1, - backgroundColor: "#ffffff", + backgroundColor: COLORS.background, }, header: { - paddingHorizontal: 16, - paddingTop: 16, - paddingBottom: 24, - backgroundColor: "#ffffff", - borderBottomWidth: 2, - borderBottomColor: "#000000", + paddingHorizontal: SPACING.lg, + paddingVertical: SPACING.md, + borderBottomWidth: 1, + borderBottomColor: COLORS.border, + }, + backButton: { + flexDirection: "row", + alignItems: "center", + gap: SPACING.xs, + alignSelf: "flex-start", + }, + backIcon: { + fontSize: 18, + color: COLORS.text, + }, + backLabel: { + fontSize: 15, + color: COLORS.textSecondary, + fontWeight: "500", + }, + titleSection: { + paddingHorizontal: SPACING.lg, + paddingTop: SPACING.lg, + paddingBottom: SPACING.md, }, title: { fontSize: 28, fontWeight: "800", - color: "#000000", + color: COLORS.text, marginBottom: 4, }, subtitle: { - fontSize: 14, - color: "#333333", + fontSize: 13, + color: COLORS.textTertiary, + }, + summaryRow: { + flexDirection: "row", + gap: SPACING.sm, + paddingHorizontal: SPACING.lg, + paddingBottom: SPACING.lg, + }, + summaryCard: { + flex: 1, + backgroundColor: COLORS.surface, + borderRadius: BORDER_RADIUS.lg, + padding: SPACING.md, + borderWidth: 1, + borderColor: COLORS.border, + }, + summaryCardAccent: { + borderColor: COLORS.success, + }, + summaryValue: { + fontSize: 18, + fontWeight: "800", + color: COLORS.text, + marginBottom: 2, + }, + summaryValueAccent: { + color: COLORS.success, + }, + summaryLabel: { + fontSize: 11, + color: COLORS.textTertiary, + fontWeight: "600", + textTransform: "uppercase", + letterSpacing: 0.5, }, listContent: { - padding: 16, - paddingBottom: 32, + padding: SPACING.lg, + paddingBottom: SPACING.xxl, }, item: { - backgroundColor: "#ffffff", - borderRadius: 12, - marginBottom: 12, + backgroundColor: COLORS.surface, + borderRadius: BORDER_RADIUS.lg, + marginBottom: SPACING.md, overflow: "hidden", borderWidth: 1, - borderColor: "#000000", + borderColor: COLORS.border, }, itemHeader: { flexDirection: "row", - paddingHorizontal: 16, - paddingTop: 12, - paddingBottom: 8, + paddingHorizontal: SPACING.lg, + paddingTop: SPACING.md, + paddingBottom: SPACING.sm, justifyContent: "space-between", alignItems: "center", borderBottomWidth: 1, - borderBottomColor: "#e0e0e0", + borderBottomColor: COLORS.border, }, itemNumber: { fontSize: 12, fontWeight: "700", - color: "#000000", + color: COLORS.textTertiary, + letterSpacing: 0.5, }, itemDate: { fontSize: 12, - color: "#666666", + color: COLORS.textTertiary, }, itemEmpresa: { - paddingHorizontal: 16, - paddingVertical: 10, - backgroundColor: "#f5f5f5", + paddingHorizontal: SPACING.lg, + paddingVertical: SPACING.sm, + backgroundColor: COLORS.surfaceLight, }, empresaLabel: { fontSize: 14, - fontWeight: "600", - color: "#000000", + fontWeight: "700", + color: COLORS.text, }, itemDetails: { flexDirection: "row", - paddingHorizontal: 16, - paddingVertical: 12, - gap: 8, + paddingHorizontal: SPACING.lg, + paddingVertical: SPACING.md, }, detailBox: { flex: 1, - paddingVertical: 8, + }, + detailBoxHighlight: { + alignItems: "flex-end", }, detailLabel: { - fontSize: 11, - color: "#666666", - fontWeight: "500", - marginBottom: 2, + fontSize: 10, + color: COLORS.textTertiary, + fontWeight: "600", + textTransform: "uppercase", + letterSpacing: 0.4, + marginBottom: 3, }, detailValue: { fontSize: 14, fontWeight: "700", - color: "#000000", + color: COLORS.text, + }, + detailValueHighlight: { + color: COLORS.success, + fontSize: 16, }, emptyContainer: { flex: 1, justifyContent: "center", alignItems: "center", - padding: 16, + padding: SPACING.lg, }, emptyBox: { alignItems: "center", }, emptyEmoji: { fontSize: 48, - marginBottom: 16, + marginBottom: SPACING.lg, }, emptyTitle: { fontSize: 18, - fontWeight: "600", - color: "#000000", - marginBottom: 8, + fontWeight: "700", + color: COLORS.text, + marginBottom: SPACING.sm, }, emptyText: { fontSize: 14, - color: "#666666", + color: COLORS.textTertiary, textAlign: "center", + lineHeight: 20, }, }); diff --git a/toptran-app/src/app/home.tsx b/toptran-app/src/app/home.tsx index 79b7d30..4da0efd 100644 --- a/toptran-app/src/app/home.tsx +++ b/toptran-app/src/app/home.tsx @@ -101,14 +101,14 @@ export default function Home() { icon: "🚗", label: "Registrar Corrida", description: "Lançar nova corrida", - onPress: () => router.push("/corrida"), + onPress: () => router.push("/lancamento"), highlight: true, }, { icon: "📋", label: "Histórico", description: "Ver corridas anteriores", - onPress: () => router.push("/corrida"), + onPress: () => router.push("/historico"), }, { icon: "📊", @@ -116,12 +116,24 @@ export default function Home() { description: "Análise de ganhos", onPress: () => Alert.alert("Em breve", "Funcionalidade em desenvolvimento."), }, + { + icon: "👤", + label: "Cadastros", + description: "Cadastros de dados básicos", + onPress: () => router.push("/cadastros"), + }, { icon: "⚙️", label: "Configurações", description: "Preferências do app", onPress: () => Alert.alert("Em breve", "Funcionalidade em desenvolvimento."), }, + { + icon: "🔄", + label: "Sincronizar", + description: "Sincronizar dados para portal", + onPress: () => router.push("/sincronizar"), + }, ]; return ( diff --git a/toptran-app/src/app/index.tsx b/toptran-app/src/app/index.tsx index 841fd30..fc43244 100644 --- a/toptran-app/src/app/index.tsx +++ b/toptran-app/src/app/index.tsx @@ -61,20 +61,20 @@ export default function IndexPage() { setAuthToken(data.accessToken); - // Salvar no SQLite - const userId = Date.now().toString(); - await salvarUsuario({ - id: userId, + const userName = data.name || emailTrimmed.split("@")[0]; + + // Salvar no SQLite — retorna o ID existente se o usuário já tinha conta + const userId = await salvarUsuario({ + id: Date.now().toString(), email: emailTrimmed, - name: data.name || emailTrimmed.split("@")[0], + name: userName, token: data.accessToken, }); - // Fazer login no contexto await login(emailTrimmed, passwordTrimmed, data.accessToken, { id: userId, email: emailTrimmed, - name: data.name || emailTrimmed.split("@")[0], + name: userName, }); } catch (err: any) { const message = err.response?.data?.error ?? err.message ?? "Erro ao fazer login."; diff --git a/toptran-app/src/app/lancamento.tsx b/toptran-app/src/app/lancamento.tsx index a82a79a..1b7f854 100644 --- a/toptran-app/src/app/lancamento.tsx +++ b/toptran-app/src/app/lancamento.tsx @@ -1,9 +1,11 @@ import { Button } from "@/components/Button"; import { Input } from "@/components/Input"; import { Select } from "@/components/Select"; +import { BORDER_RADIUS, COLORS, SPACING } from "@/constants/theme"; import { useAuth } from "@/contexts/AuthContext"; -import { salvarCorrida } from "@/services/db"; -import React, { useState } from "react"; +import { EmpresaDB, obterEmpresas, salvarCorrida } from "@/services/db"; +import { router, useFocusEffect } from "expo-router"; +import React, { useCallback, useState } from "react"; import { Alert, KeyboardAvoidingView, @@ -11,19 +13,11 @@ import { ScrollView, StyleSheet, Text, + TouchableOpacity, 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; @@ -34,63 +28,83 @@ export type Corrida = { usuario: string; }; -interface LancamentoProps { - onLancar: (corrida: Corrida) => void; -} - -export default function Lancamento({ onLancar }: LancamentoProps) { +export default function LancamentoPage() { const { user } = useAuth(); - const [empresa, setEmpresa] = useState(""); + const [empresasDB, setEmpresasDB] = useState([]); + const [empresaId, setEmpresaId] = useState(""); + const [custoPorKm, setCustoPorKm] = useState(0); const [distancia, setDistancia] = useState(""); + const [loading, setLoading] = useState(false); const distanciaNum = parseFloat(distancia) || 0; - const totalCorrida = distanciaNum * CUSTO_POR_KM; + const totalCorrida = distanciaNum * custoPorKm; + + useFocusEffect( + useCallback(() => { + obterEmpresas() + .then(setEmpresasDB) + .catch(() => Alert.alert("Erro", "Não foi possível carregar as empresas.")); + }, []), + ); + + const empresasItems = [ + { label: "Selecione uma empresa", value: "" }, + ...empresasDB.map((e) => ({ label: e.nome, value: e.id })), + ]; + + const handleEmpresaChange = (id: string) => { + setEmpresaId(id); + const found = empresasDB.find((e) => e.id === id); + setCustoPorKm(found?.custo_por_km ?? 0); + }; const handleLancarCorrida = async () => { - if (!empresa) { + if (!empresaId) { 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 ?? "", - }; + const empresaSelecionada = empresasDB.find((e) => e.id === empresaId); try { + setLoading(true); await salvarCorrida({ - id: novaCorrida.id, + id: Date.now().toString(), usuario_id: user?.id ?? "", - empresa: novaCorrida.empresa, - km: novaCorrida.km, - custo_por_km: novaCorrida.custoPorKm, - total: novaCorrida.total, - data: novaCorrida.data, + empresa: empresaSelecionada?.nome ?? empresaId, + km: distanciaNum, + custo_por_km: custoPorKm, + total: totalCorrida, + data: new Date().toLocaleString("pt-BR"), sincronizado: 0, }); + setEmpresaId(""); + setCustoPorKm(0); + setDistancia(""); + Alert.alert("Sucesso", "Corrida registrada com sucesso!", [ + { text: "OK", onPress: () => router.back() }, + ]); } catch (error) { console.error("Erro ao salvar corrida:", error); + Alert.alert("Erro", "Não foi possível registrar a corrida."); + } finally { + setLoading(false); } - - onLancar(novaCorrida); - setEmpresa(""); - setDistancia(""); - Alert.alert("Sucesso", "Corrida registrada com sucesso!"); }; return ( + + router.back()} style={styles.backButton} activeOpacity={0.7}> + + Voltar + + + - + Nova Corrida - Preencha os dados da corrida + Preencha os dados abaixo Empresa - @@ -124,24 +134,22 @@ export default function Lancamento({ onLancar }: LancamentoProps) { - - Custo por km - - R$ {CUSTO_POR_KM.toFixed(2)} - + + Custo por km + R$ {custoPorKm.toFixed(2)} - - - Valor Total - - R$ {totalCorrida.toFixed(2)} - - + + Valor Total + R$ {totalCorrida.toFixed(2)} -