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)}
-
+
@@ -151,72 +159,95 @@ export default function Lancamento({ onLancar }: LancamentoProps) {
const styles = StyleSheet.create({
container: {
flex: 1,
- backgroundColor: "#ffffff",
- },
- scrollContent: {
- padding: 16,
- paddingBottom: 32,
+ backgroundColor: COLORS.background,
},
header: {
- marginBottom: 24,
+ 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: "#000000",
+ color: COLORS.text,
marginBottom: 4,
},
subtitle: {
fontSize: 14,
- color: "#333333",
+ color: COLORS.textTertiary,
},
card: {
- backgroundColor: "#ffffff",
- borderRadius: 12,
- padding: 16,
- marginBottom: 12,
+ backgroundColor: COLORS.surface,
+ borderRadius: BORDER_RADIUS.lg,
+ padding: SPACING.lg,
+ marginBottom: SPACING.md,
borderWidth: 1,
- borderColor: "#000000",
+ borderColor: COLORS.border,
},
cardLabel: {
- fontSize: 12,
- fontWeight: "600",
- color: "#000000",
- marginBottom: 8,
+ fontSize: 11,
+ fontWeight: "700",
+ color: COLORS.textTertiary,
+ marginBottom: SPACING.sm,
textTransform: "uppercase",
- letterSpacing: 0.5,
+ letterSpacing: 0.8,
},
- valorRow: {
+ infoRow: {
flexDirection: "row",
justifyContent: "space-between",
alignItems: "center",
},
- valorLabel: {
+ infoLabel: {
fontSize: 14,
- color: "#000000",
+ color: COLORS.textSecondary,
fontWeight: "500",
},
- valorAmount: {
+ infoValue: {
fontSize: 16,
fontWeight: "700",
- color: "#000000",
+ color: COLORS.text,
},
- totalBox: {
- backgroundColor: "#ffffff",
- borderLeftWidth: 4,
- borderLeftColor: "#000000",
- padding: 12,
- borderRadius: 8,
+ totalCard: {
+ borderColor: COLORS.borderLight,
+ borderLeftWidth: 3,
+ borderLeftColor: COLORS.success,
+ marginBottom: SPACING.xl,
},
totalLabel: {
fontSize: 12,
- color: "#000000",
- marginBottom: 4,
- fontWeight: "500",
+ color: COLORS.textTertiary,
+ fontWeight: "600",
+ textTransform: "uppercase",
+ letterSpacing: 0.5,
+ marginBottom: SPACING.xs,
},
totalAmount: {
- fontSize: 24,
+ fontSize: 32,
fontWeight: "800",
- color: "#000000",
+ color: COLORS.success,
},
});
diff --git a/toptran-app/src/app/sincronizar.tsx b/toptran-app/src/app/sincronizar.tsx
new file mode 100644
index 0000000..044d8fe
--- /dev/null
+++ b/toptran-app/src/app/sincronizar.tsx
@@ -0,0 +1,500 @@
+import { BORDER_RADIUS, COLORS, SPACING } from "@/constants/theme";
+import { useAuth } from "@/contexts/AuthContext";
+import {
+ marcarCorridaComoSincronizada,
+ obterCorridasNaoSincronizadas,
+ obterEmpresas,
+ obterUsuario,
+} from "@/services/db";
+import { api } from "@/server/api";
+import { router } from "expo-router";
+import React, { useState } from "react";
+import {
+ ActivityIndicator,
+ ScrollView,
+ StyleSheet,
+ Text,
+ TouchableOpacity,
+ View,
+} from "react-native";
+import { SafeAreaView } from "react-native-safe-area-context";
+
+type SyncStatus = "pending" | "syncing" | "success" | "error";
+
+type SyncItem = {
+ key: string;
+ label: string;
+ description: string;
+ status: SyncStatus;
+ detail: string;
+};
+
+const INITIAL_ITEMS: SyncItem[] = [
+ {
+ key: "empresas",
+ label: "Empresas",
+ description: "Cadastro de empresas e custos por km",
+ status: "pending",
+ detail: "",
+ },
+ {
+ key: "usuarios",
+ label: "Usuários",
+ description: "Dados do usuário autenticado",
+ status: "pending",
+ detail: "",
+ },
+ {
+ key: "corridas",
+ label: "Corridas",
+ description: "Histórico de corridas não sincronizadas",
+ status: "pending",
+ detail: "",
+ },
+];
+
+export default function SincronizarPage() {
+ const { user } = useAuth();
+ const [items, setItems] = useState(INITIAL_ITEMS);
+ const [syncing, setSyncing] = useState(false);
+ const [done, setDone] = useState(false);
+
+ const update = (key: string, patch: Partial) =>
+ setItems((prev) =>
+ prev.map((i) => (i.key === key ? { ...i, ...patch } : i)),
+ );
+
+ const reset = () => {
+ setItems(INITIAL_ITEMS);
+ setDone(false);
+ };
+
+ const handleSync = async () => {
+ if (!user?.id) return;
+ setSyncing(true);
+ setDone(false);
+ setItems(INITIAL_ITEMS);
+
+ // ── Empresas ────────────────────────────────────────────
+ update("empresas", { status: "syncing" });
+ try {
+ const empresas = await obterEmpresas();
+ await api.post("/sync/empresas", { empresas });
+ update("empresas", {
+ status: "success",
+ detail: `${empresas.length} registro${empresas.length !== 1 ? "s" : ""} enviado${empresas.length !== 1 ? "s" : ""}`,
+ });
+ } catch (e: any) {
+ update("empresas", {
+ status: "error",
+ detail: e?.response?.data?.error ?? e?.message ?? "Falha na conexão",
+ });
+ }
+
+ // ── Usuários ─────────────────────────────────────────────
+ update("usuarios", { status: "syncing" });
+ try {
+ const usuario = await obterUsuario(user.id);
+ if (usuario) {
+ await api.post("/sync/usuarios", { usuarios: [usuario] });
+ update("usuarios", { status: "success", detail: "1 registro enviado" });
+ } else {
+ update("usuarios", { status: "error", detail: "Usuário não encontrado" });
+ }
+ } catch (e: any) {
+ update("usuarios", {
+ status: "error",
+ detail: e?.response?.data?.error ?? e?.message ?? "Falha na conexão",
+ });
+ }
+
+ // ── Corridas ─────────────────────────────────────────────
+ update("corridas", { status: "syncing" });
+ try {
+ const corridas = await obterCorridasNaoSincronizadas(user.id);
+ if (corridas.length === 0) {
+ update("corridas", { status: "success", detail: "Nenhuma corrida pendente" });
+ } else {
+ await api.post("/sync/corridas", { corridas });
+ for (const c of corridas) {
+ await marcarCorridaComoSincronizada(c.id);
+ }
+ update("corridas", {
+ status: "success",
+ detail: `${corridas.length} corrida${corridas.length !== 1 ? "s" : ""} enviada${corridas.length !== 1 ? "s" : ""}`,
+ });
+ }
+ } catch (e: any) {
+ update("corridas", {
+ status: "error",
+ detail: e?.response?.data?.error ?? e?.message ?? "Falha na conexão",
+ });
+ }
+
+ setSyncing(false);
+ setDone(true);
+ };
+
+ const allSuccess = items.every((i) => i.status === "success");
+ const hasError = items.some((i) => i.status === "error");
+
+ return (
+
+ {/* Header */}
+
+ router.back()}
+ style={styles.backButton}
+ activeOpacity={0.7}
+ >
+ ←
+ Voltar
+
+
+
+
+
+ Sincronizar
+
+ Enviar dados locais para o servidor
+
+
+
+ {/* Status geral após conclusão */}
+ {done && (
+
+
+ {allSuccess
+ ? "✓ Todos os dados sincronizados com sucesso"
+ : "⚠ Sincronização concluída com erros"}
+
+
+ )}
+
+ {/* Timeline */}
+
+ {items.map((item, index) => {
+ const isLast = index === items.length - 1;
+ return (
+
+ {/* Track: linha + círculo */}
+
+
+ {!isLast && (
+
+ )}
+
+
+ {/* Conteúdo */}
+
+
+ {item.label}
+ {item.status === "success" && (
+ ✓ OK
+ )}
+ {item.status === "error" && (
+ ✗ Erro
+ )}
+
+ {item.description}
+ {item.detail !== "" && (
+
+ {item.detail}
+
+ )}
+
+
+ );
+ })}
+
+
+ {/* Botões */}
+
+ {done && hasError && (
+
+ Redefinir
+
+ )}
+
+
+ {syncing ? (
+
+ ) : (
+
+ {done ? "Sincronizar novamente" : "Iniciar sincronização"}
+
+ )}
+
+
+
+
+ );
+}
+
+function NodeCircle({ status }: { status: SyncStatus }) {
+ if (status === "syncing") {
+ return (
+
+
+
+ );
+ }
+ return (
+
+ {status === "success" && ✓}
+ {status === "error" && ✗}
+
+ );
+}
+
+const CIRCLE_SIZE = 36;
+const CONNECTOR_WIDTH = 2;
+
+const styles = StyleSheet.create({
+ container: {
+ flex: 1,
+ backgroundColor: COLORS.background,
+ },
+
+ // Header
+ 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: 48,
+ },
+
+ titleSection: {
+ marginBottom: SPACING.xl,
+ marginTop: SPACING.md,
+ },
+ title: {
+ fontSize: 28,
+ fontWeight: "800",
+ color: COLORS.text,
+ marginBottom: 4,
+ },
+ subtitle: {
+ fontSize: 14,
+ color: COLORS.textTertiary,
+ },
+
+ // Banner de resultado
+ resultBanner: {
+ borderRadius: BORDER_RADIUS.lg,
+ padding: SPACING.md,
+ marginBottom: SPACING.xl,
+ borderWidth: 1,
+ },
+ resultBannerSuccess: {
+ backgroundColor: "#0d2b1f",
+ borderColor: COLORS.success,
+ },
+ resultBannerError: {
+ backgroundColor: "#2b0d0d",
+ borderColor: COLORS.error,
+ },
+ resultBannerText: {
+ fontSize: 14,
+ fontWeight: "600",
+ color: COLORS.text,
+ },
+
+ // Timeline
+ timeline: {
+ marginBottom: SPACING.xl,
+ },
+ timelineRow: {
+ flexDirection: "row",
+ alignItems: "flex-start",
+ },
+
+ // Track (círculo + conector)
+ track: {
+ alignItems: "center",
+ width: CIRCLE_SIZE + SPACING.lg,
+ },
+ circle: {
+ width: CIRCLE_SIZE,
+ height: CIRCLE_SIZE,
+ borderRadius: CIRCLE_SIZE / 2,
+ justifyContent: "center",
+ alignItems: "center",
+ borderWidth: 2,
+ zIndex: 1,
+ },
+ circlePending: {
+ backgroundColor: COLORS.surface,
+ borderColor: COLORS.border,
+ },
+ circleSyncing: {
+ backgroundColor: COLORS.surface,
+ borderColor: COLORS.warning,
+ },
+ circleSuccess: {
+ backgroundColor: "#0d2b1f",
+ borderColor: COLORS.success,
+ },
+ circleError: {
+ backgroundColor: "#2b0d0d",
+ borderColor: COLORS.error,
+ },
+ circleIcon: {
+ fontSize: 16,
+ fontWeight: "800",
+ color: COLORS.text,
+ },
+ connector: {
+ width: CONNECTOR_WIDTH,
+ flex: 1,
+ minHeight: 56,
+ backgroundColor: COLORS.border,
+ marginTop: 2,
+ marginBottom: 2,
+ },
+ connectorSuccess: {
+ backgroundColor: COLORS.success,
+ },
+ connectorError: {
+ backgroundColor: COLORS.error,
+ },
+
+ // Item content
+ itemContent: {
+ flex: 1,
+ paddingTop: SPACING.xs,
+ paddingBottom: SPACING.xl,
+ paddingLeft: SPACING.md,
+ },
+ itemHeader: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ marginBottom: 3,
+ },
+ itemLabel: {
+ fontSize: 16,
+ fontWeight: "700",
+ color: COLORS.text,
+ },
+ itemDescription: {
+ fontSize: 12,
+ color: COLORS.textTertiary,
+ marginBottom: 4,
+ },
+ itemDetail: {
+ fontSize: 12,
+ fontWeight: "600",
+ color: COLORS.textSecondary,
+ },
+ itemDetailSuccess: {
+ color: COLORS.success,
+ },
+ itemDetailError: {
+ color: COLORS.error,
+ },
+
+ // Badges
+ badgeSuccess: {
+ fontSize: 12,
+ fontWeight: "700",
+ color: COLORS.success,
+ backgroundColor: "#0d2b1f",
+ paddingHorizontal: SPACING.sm,
+ paddingVertical: 2,
+ borderRadius: BORDER_RADIUS.sm,
+ },
+ badgeError: {
+ fontSize: 12,
+ fontWeight: "700",
+ color: COLORS.error,
+ backgroundColor: "#2b0d0d",
+ paddingHorizontal: SPACING.sm,
+ paddingVertical: 2,
+ borderRadius: BORDER_RADIUS.sm,
+ },
+
+ // Botões
+ actions: {
+ gap: SPACING.sm,
+ },
+ syncButton: {
+ backgroundColor: COLORS.text,
+ borderRadius: BORDER_RADIUS.lg,
+ paddingVertical: SPACING.lg,
+ alignItems: "center",
+ },
+ syncButtonDisabled: {
+ backgroundColor: COLORS.borderLight,
+ },
+ syncButtonText: {
+ fontSize: 15,
+ fontWeight: "700",
+ color: COLORS.background,
+ },
+ retryButton: {
+ backgroundColor: COLORS.surface,
+ borderRadius: BORDER_RADIUS.lg,
+ paddingVertical: SPACING.md,
+ alignItems: "center",
+ borderWidth: 1,
+ borderColor: COLORS.border,
+ },
+ retryButtonText: {
+ fontSize: 14,
+ fontWeight: "600",
+ color: COLORS.textSecondary,
+ },
+});
diff --git a/toptran-app/src/services/db.ts b/toptran-app/src/services/db.ts
index 64cda08..57a9984 100644
--- a/toptran-app/src/services/db.ts
+++ b/toptran-app/src/services/db.ts
@@ -24,16 +24,17 @@ export type CorridaDB = {
export const initDB = async () => {
try {
- await db.execAsync(`
- CREATE TABLE IF NOT EXISTS usuarios (
+ 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 (
+ );`,
+ );
+ await db.execAsync(
+ `CREATE TABLE IF NOT EXISTS corridas (
id TEXT PRIMARY KEY,
usuario_id TEXT NOT NULL,
empresa TEXT NOT NULL,
@@ -44,13 +45,23 @@ export const initDB = async () => {
sincronizado INTEGER DEFAULT 0,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (usuario_id) REFERENCES usuarios(id)
- );
-
- CREATE TABLE IF NOT EXISTS settings (
+ );`,
+ );
+ await db.execAsync(
+ `CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY,
value TEXT NOT NULL
- );
- `);
+ );`,
+ );
+ await db.execAsync(
+ `CREATE TABLE IF NOT EXISTS empresas (
+ id TEXT PRIMARY KEY,
+ nome TEXT NOT NULL,
+ custo_por_km REAL NOT NULL,
+ observacoes TEXT DEFAULT '',
+ created_at TEXT DEFAULT CURRENT_TIMESTAMP
+ );`,
+ );
} catch (error) {
console.error("Failed to initialize database:", error);
throw error;
@@ -93,14 +104,29 @@ export const deleteSetting = async (key: string): Promise => {
};
// USUÁRIOS
-export const salvarUsuario = async (usuario: Omit) => {
+export const salvarUsuario = async (
+ usuario: Omit,
+): Promise => {
try {
- const result = await db.runAsync(
- `INSERT OR REPLACE INTO usuarios (id, email, name, token)
- VALUES (?, ?, ?, ?)`,
+ const existing = await db.getFirstAsync<{ id: string }>(
+ `SELECT id FROM usuarios WHERE email = ?`,
+ [usuario.email],
+ );
+
+ if (existing) {
+ // Preserva o ID original — apenas atualiza nome e token
+ await db.runAsync(
+ `UPDATE usuarios SET name = ?, token = ? WHERE email = ?`,
+ [usuario.name, usuario.token, usuario.email],
+ );
+ return existing.id;
+ }
+
+ await db.runAsync(
+ `INSERT INTO usuarios (id, email, name, token) VALUES (?, ?, ?, ?)`,
[usuario.id, usuario.email, usuario.name, usuario.token],
);
- return result;
+ return usuario.id;
} catch (error) {
console.error("Error saving user:", error);
throw error;
@@ -205,3 +231,62 @@ export const limparBancoDados = async () => {
throw error;
}
};
+
+// EMPRESAS
+export type EmpresaDB = {
+ id: string;
+ nome: string;
+ custo_por_km: number;
+ observacoes: string;
+ created_at: string;
+};
+
+export const obterEmpresas = async (): Promise => {
+ try {
+ return (
+ (await db.getAllAsync(
+ `SELECT * FROM empresas ORDER BY nome ASC`,
+ )) ?? []
+ );
+ } catch (error) {
+ console.error("Error fetching empresas:", error);
+ throw error;
+ }
+};
+
+export const salvarEmpresa = async (
+ empresa: Omit,
+): Promise => {
+ try {
+ await db.runAsync(
+ `INSERT INTO empresas (id, nome, custo_por_km, observacoes) VALUES (?, ?, ?, ?)`,
+ [empresa.id, empresa.nome, empresa.custo_por_km, empresa.observacoes],
+ );
+ } catch (error) {
+ console.error("Error saving empresa:", error);
+ throw error;
+ }
+};
+
+export const atualizarEmpresa = async (
+ empresa: Omit,
+): Promise => {
+ try {
+ await db.runAsync(
+ `UPDATE empresas SET nome = ?, custo_por_km = ?, observacoes = ? WHERE id = ?`,
+ [empresa.nome, empresa.custo_por_km, empresa.observacoes, empresa.id],
+ );
+ } catch (error) {
+ console.error("Error updating empresa:", error);
+ throw error;
+ }
+};
+
+export const deletarEmpresa = async (id: string): Promise => {
+ try {
+ await db.runAsync(`DELETE FROM empresas WHERE id = ?`, [id]);
+ } catch (error) {
+ console.error("Error deleting empresa:", error);
+ throw error;
+ }
+};