Sincronismy on system
This commit is contained in:
parent
2712fd6fa9
commit
0dabfcc484
10 changed files with 1676 additions and 297 deletions
36
toptran-app/database/create_tables.sql
Normal file
36
toptran-app/database/create_tables.sql
Normal file
|
|
@ -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);
|
||||
190
toptran-app/src/app/cadastros.tsx
Normal file
190
toptran-app/src/app/cadastros.tsx
Normal file
|
|
@ -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 (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
style={styles.backButton}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={styles.backIcon}>←</Text>
|
||||
<Text style={styles.backLabel}>Voltar</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={styles.titleSection}>
|
||||
<Text style={styles.title}>Cadastros</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Gerencie os dados base do sistema
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
<Text style={styles.sectionTitle}>Categorias</Text>
|
||||
<View style={styles.grid}>
|
||||
{cards.map((card, index) => (
|
||||
<TouchableOpacity
|
||||
key={index}
|
||||
style={[styles.card, card.highlight && styles.cardHighlight]}
|
||||
onPress={card.onPress}
|
||||
activeOpacity={0.75}
|
||||
>
|
||||
<Text style={styles.cardIcon}>{card.icon}</Text>
|
||||
<Text
|
||||
style={[
|
||||
styles.cardLabel,
|
||||
card.highlight && styles.cardLabelHighlight,
|
||||
]}
|
||||
>
|
||||
{card.label}
|
||||
</Text>
|
||||
<Text style={styles.cardDescription}>{card.description}</Text>
|
||||
</TouchableOpacity>
|
||||
))}
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
@ -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<Corrida[]>([]);
|
||||
|
||||
const loadHistorico = useCallback(async () => {
|
||||
if (!user?.id) return;
|
||||
try {
|
||||
const corridasDB = await obterCorridas(user.id);
|
||||
setHistorico(
|
||||
corridasDB.map((c) => ({
|
||||
id: c.id,
|
||||
data: c.data,
|
||||
empresa: c.empresa,
|
||||
km: c.km,
|
||||
custoPorKm: c.custo_por_km,
|
||||
total: c.total,
|
||||
usuario: user.email,
|
||||
})),
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Erro ao carregar histórico:", error);
|
||||
}
|
||||
}, [user?.id, user?.email]);
|
||||
|
||||
useEffect(() => {
|
||||
loadHistorico();
|
||||
}, [loadHistorico]);
|
||||
|
||||
const handleLancarCorrida = async (_corrida: Corrida) => {
|
||||
await loadHistorico();
|
||||
setActiveTab("historico");
|
||||
};
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.tabsContainer}>
|
||||
<TouchableOpacity
|
||||
style={[styles.tab, activeTab === "lancamento" && styles.tabActive]}
|
||||
onPress={() => setActiveTab("lancamento")}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.tabLabel,
|
||||
activeTab === "lancamento" && styles.tabLabelActive,
|
||||
]}
|
||||
>
|
||||
📍 Lançar
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.tab, activeTab === "historico" && styles.tabActive]}
|
||||
onPress={() => setActiveTab("historico")}
|
||||
>
|
||||
<Text
|
||||
style={[
|
||||
styles.tabLabel,
|
||||
activeTab === "historico" && styles.tabLabelActive,
|
||||
]}
|
||||
>
|
||||
📋 Histórico
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.content}>
|
||||
{activeTab === "lancamento" ? (
|
||||
<Lancamento onLancar={handleLancarCorrida} />
|
||||
) : (
|
||||
<Historico historico={historico} />
|
||||
)}
|
||||
</View>
|
||||
</SafeAreaView>
|
||||
);
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
520
toptran-app/src/app/empresas.tsx
Normal file
520
toptran-app/src/app/empresas.tsx
Normal file
|
|
@ -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<EmpresaDB[]>([]);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [editando, setEditando] = useState<EmpresaDB | null>(null);
|
||||
const [form, setForm] = useState<FormState>(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 }) => (
|
||||
<View style={styles.item}>
|
||||
<View style={styles.itemLeft}>
|
||||
<View style={styles.itemIndex}>
|
||||
<Text style={styles.itemIndexText}>{index + 1}</Text>
|
||||
</View>
|
||||
<View style={styles.itemInfo}>
|
||||
<Text style={styles.itemNome}>{item.nome}</Text>
|
||||
<Text style={styles.itemCusto}>R$ {item.custo_por_km.toFixed(2)} / km</Text>
|
||||
{item.observacoes ? (
|
||||
<Text style={styles.itemObs} numberOfLines={1}>
|
||||
{item.observacoes}
|
||||
</Text>
|
||||
) : null}
|
||||
</View>
|
||||
</View>
|
||||
<View style={styles.itemActions}>
|
||||
<TouchableOpacity
|
||||
style={[styles.actionBtn, styles.editBtn]}
|
||||
onPress={() => abrirEditar(item)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={styles.actionBtnText}>✏️</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={[styles.actionBtn, styles.deleteBtn]}
|
||||
onPress={() => handleDeletar(item)}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={styles.actionBtnText}>🗑️</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
return (
|
||||
<SafeAreaView style={styles.container}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
style={styles.backButton}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={styles.backIcon}>←</Text>
|
||||
<Text style={styles.backLabel}>Voltar</Text>
|
||||
</TouchableOpacity>
|
||||
<TouchableOpacity
|
||||
style={styles.addButton}
|
||||
onPress={abrirAdicionar}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={styles.addButtonText}>+ Adicionar</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
{/* Title */}
|
||||
<View style={styles.titleSection}>
|
||||
<Text style={styles.title}>Empresas</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
{empresas.length} empresa{empresas.length !== 1 ? "s" : ""} cadastrada{empresas.length !== 1 ? "s" : ""}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Lista */}
|
||||
{empresas.length === 0 ? (
|
||||
<ScrollView contentContainerStyle={styles.emptyContainer}>
|
||||
<Text style={styles.emptyEmoji}>🏢</Text>
|
||||
<Text style={styles.emptyTitle}>Nenhuma empresa cadastrada</Text>
|
||||
<Text style={styles.emptyText}>
|
||||
Toque em "+ Adicionar" para cadastrar a primeira empresa.
|
||||
</Text>
|
||||
</ScrollView>
|
||||
) : (
|
||||
<FlatList
|
||||
data={empresas}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={renderItem}
|
||||
contentContainerStyle={styles.listContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Modal Add/Edit */}
|
||||
<Modal
|
||||
visible={modalVisible}
|
||||
animationType="slide"
|
||||
transparent
|
||||
onRequestClose={fecharModal}
|
||||
>
|
||||
<KeyboardAvoidingView
|
||||
style={styles.modalOverlay}
|
||||
behavior={Platform.select({ ios: "padding", android: "height" })}
|
||||
>
|
||||
<View style={styles.modalSheet}>
|
||||
<View style={styles.modalHandle} />
|
||||
|
||||
<View style={styles.modalHeader}>
|
||||
<Text style={styles.modalTitle}>
|
||||
{editando ? "Editar Empresa" : "Nova Empresa"}
|
||||
</Text>
|
||||
<TouchableOpacity onPress={fecharModal} activeOpacity={0.7}>
|
||||
<Text style={styles.modalClose}>✕</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView showsVerticalScrollIndicator={false}>
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={styles.formLabel}>Nome da Empresa</Text>
|
||||
<Input
|
||||
placeholder="Ex: MULTI B"
|
||||
value={form.nome}
|
||||
onChangeText={(v) => setForm((f) => ({ ...f, nome: v }))}
|
||||
autoCapitalize="characters"
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={styles.formLabel}>Custo por km (R$)</Text>
|
||||
<Input
|
||||
placeholder="Ex: 2.70"
|
||||
keyboardType="decimal-pad"
|
||||
value={form.custoPorKm}
|
||||
onChangeText={(v) => setForm((f) => ({ ...f, custoPorKm: v }))}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<View style={styles.formGroup}>
|
||||
<Text style={styles.formLabel}>Observações (opcional)</Text>
|
||||
<Input
|
||||
placeholder="Ex: Pagamento quinzenal"
|
||||
value={form.observacoes}
|
||||
onChangeText={(v) => setForm((f) => ({ ...f, observacoes: v }))}
|
||||
/>
|
||||
</View>
|
||||
|
||||
<TouchableOpacity
|
||||
style={[styles.saveButton, loading && styles.saveButtonDisabled]}
|
||||
onPress={handleSalvar}
|
||||
disabled={loading}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={styles.saveButtonText}>
|
||||
{loading ? "Salvando..." : editando ? "Salvar alterações" : "Cadastrar empresa"}
|
||||
</Text>
|
||||
</TouchableOpacity>
|
||||
</ScrollView>
|
||||
</View>
|
||||
</KeyboardAvoidingView>
|
||||
</Modal>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
flex: 1,
|
||||
backgroundColor: COLORS.background,
|
||||
},
|
||||
|
||||
// Header
|
||||
header: {
|
||||
flexDirection: "row",
|
||||
justifyContent: "space-between",
|
||||
alignItems: "center",
|
||||
paddingHorizontal: SPACING.lg,
|
||||
paddingVertical: SPACING.md,
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: COLORS.border,
|
||||
},
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
@ -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 }) => (
|
||||
<View style={styles.item}>
|
||||
<View style={styles.itemHeader}>
|
||||
|
|
@ -32,41 +38,92 @@ const HistoricoItem = ({ item, index }: { item: Corrida; index: number }) => (
|
|||
<Text style={styles.detailLabel}>Distância</Text>
|
||||
<Text style={styles.detailValue}>{item.km} km</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.detailBox}>
|
||||
<Text style={styles.detailLabel}>Valor/km</Text>
|
||||
<Text style={styles.detailValue}>R$ {item.custoPorKm.toFixed(2)}</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.detailBox}>
|
||||
<View style={[styles.detailBox, styles.detailBoxHighlight]}>
|
||||
<Text style={styles.detailLabel}>Total</Text>
|
||||
<Text style={styles.detailValue}>R$ {item.total.toFixed(2)}</Text>
|
||||
<Text style={[styles.detailValue, styles.detailValueHighlight]}>
|
||||
R$ {item.total.toFixed(2)}
|
||||
</Text>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
|
||||
export default function Historico({ historico }: HistoricoProps) {
|
||||
export default function HistoricoPage() {
|
||||
const { user } = useAuth();
|
||||
const [corridas, setCorridas] = useState<Corrida[]>([]);
|
||||
|
||||
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 (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<Text style={styles.title}>Histórico de Corridas</Text>
|
||||
<TouchableOpacity onPress={() => router.back()} style={styles.backButton} activeOpacity={0.7}>
|
||||
<Text style={styles.backIcon}>←</Text>
|
||||
<Text style={styles.backLabel}>Voltar</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<View style={styles.titleSection}>
|
||||
<Text style={styles.title}>Histórico</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
{historico.length} corrida{historico.length !== 1 ? "s" : ""}
|
||||
{corridas.length} corrida{corridas.length !== 1 ? "s" : ""} registrada{corridas.length !== 1 ? "s" : ""}
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{historico.length === 0 ? (
|
||||
{corridas.length > 0 && (
|
||||
<View style={styles.summaryRow}>
|
||||
<View style={styles.summaryCard}>
|
||||
<Text style={styles.summaryValue}>{totalKm.toFixed(0)} km</Text>
|
||||
<Text style={styles.summaryLabel}>Total Rodado</Text>
|
||||
</View>
|
||||
<View style={[styles.summaryCard, styles.summaryCardAccent]}>
|
||||
<Text style={[styles.summaryValue, styles.summaryValueAccent]}>
|
||||
R$ {totalGanhos.toFixed(2)}
|
||||
</Text>
|
||||
<Text style={styles.summaryLabel}>Total Ganho</Text>
|
||||
</View>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{corridas.length === 0 ? (
|
||||
<ScrollView contentContainerStyle={styles.emptyContainer}>
|
||||
<View style={styles.emptyBox}>
|
||||
<Text style={styles.emptyEmoji}>🚗</Text>
|
||||
<Text style={styles.emptyTitle}>Nenhuma corrida registrada</Text>
|
||||
<Text style={styles.emptyText}>Suas corridas aparecerão aqui</Text>
|
||||
<Text style={styles.emptyTitle}>Nenhuma corrida ainda</Text>
|
||||
<Text style={styles.emptyText}>
|
||||
Registre sua primeira corrida para vê-la aqui.
|
||||
</Text>
|
||||
</View>
|
||||
</ScrollView>
|
||||
) : (
|
||||
<FlatList
|
||||
data={historico}
|
||||
data={corridas}
|
||||
keyExtractor={(item) => item.id}
|
||||
renderItem={({ item, index }) => (
|
||||
<HistoricoItem item={item} index={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,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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.";
|
||||
|
|
|
|||
|
|
@ -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<EmpresaDB[]>([]);
|
||||
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 (
|
||||
<SafeAreaView style={styles.container}>
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity onPress={() => router.back()} style={styles.backButton} activeOpacity={0.7}>
|
||||
<Text style={styles.backIcon}>←</Text>
|
||||
<Text style={styles.backLabel}>Voltar</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<KeyboardAvoidingView
|
||||
style={{ flex: 1 }}
|
||||
behavior={Platform.select({ ios: "padding", android: "height" })}
|
||||
|
|
@ -99,18 +113,14 @@ export default function Lancamento({ onLancar }: LancamentoProps) {
|
|||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={styles.header}>
|
||||
<View style={styles.titleSection}>
|
||||
<Text style={styles.title}>Nova Corrida</Text>
|
||||
<Text style={styles.subtitle}>Preencha os dados da corrida</Text>
|
||||
<Text style={styles.subtitle}>Preencha os dados abaixo</Text>
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
<Text style={styles.cardLabel}>Empresa</Text>
|
||||
<Select
|
||||
value={empresa}
|
||||
onValueChange={setEmpresa}
|
||||
items={EMPRESAS}
|
||||
/>
|
||||
<Select value={empresaId} onValueChange={handleEmpresaChange} items={empresasItems} />
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
|
|
@ -124,24 +134,22 @@ export default function Lancamento({ onLancar }: LancamentoProps) {
|
|||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
<View style={styles.valorRow}>
|
||||
<Text style={styles.valorLabel}>Custo por km</Text>
|
||||
<Text style={styles.valorAmount}>
|
||||
R$ {CUSTO_POR_KM.toFixed(2)}
|
||||
</Text>
|
||||
<View style={styles.infoRow}>
|
||||
<Text style={styles.infoLabel}>Custo por km</Text>
|
||||
<Text style={styles.infoValue}>R$ {custoPorKm.toFixed(2)}</Text>
|
||||
</View>
|
||||
</View>
|
||||
|
||||
<View style={styles.card}>
|
||||
<View style={styles.totalBox}>
|
||||
<View style={[styles.card, styles.totalCard]}>
|
||||
<Text style={styles.totalLabel}>Valor Total</Text>
|
||||
<Text style={styles.totalAmount}>
|
||||
R$ {totalCorrida.toFixed(2)}
|
||||
</Text>
|
||||
</View>
|
||||
<Text style={styles.totalAmount}>R$ {totalCorrida.toFixed(2)}</Text>
|
||||
</View>
|
||||
|
||||
<Button label="Lançar Corrida" onPress={handleLancarCorrida} />
|
||||
<Button
|
||||
label={loading ? "Salvando..." : "Lançar Corrida"}
|
||||
onPress={handleLancarCorrida}
|
||||
disabled={loading}
|
||||
/>
|
||||
</ScrollView>
|
||||
</KeyboardAvoidingView>
|
||||
</SafeAreaView>
|
||||
|
|
@ -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,
|
||||
},
|
||||
});
|
||||
|
|
|
|||
500
toptran-app/src/app/sincronizar.tsx
Normal file
500
toptran-app/src/app/sincronizar.tsx
Normal file
|
|
@ -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<SyncItem[]>(INITIAL_ITEMS);
|
||||
const [syncing, setSyncing] = useState(false);
|
||||
const [done, setDone] = useState(false);
|
||||
|
||||
const update = (key: string, patch: Partial<SyncItem>) =>
|
||||
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 (
|
||||
<SafeAreaView style={styles.container}>
|
||||
{/* Header */}
|
||||
<View style={styles.header}>
|
||||
<TouchableOpacity
|
||||
onPress={() => router.back()}
|
||||
style={styles.backButton}
|
||||
activeOpacity={0.7}
|
||||
>
|
||||
<Text style={styles.backIcon}>←</Text>
|
||||
<Text style={styles.backLabel}>Voltar</Text>
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
|
||||
<ScrollView
|
||||
contentContainerStyle={styles.scrollContent}
|
||||
showsVerticalScrollIndicator={false}
|
||||
>
|
||||
<View style={styles.titleSection}>
|
||||
<Text style={styles.title}>Sincronizar</Text>
|
||||
<Text style={styles.subtitle}>
|
||||
Enviar dados locais para o servidor
|
||||
</Text>
|
||||
</View>
|
||||
|
||||
{/* Status geral após conclusão */}
|
||||
{done && (
|
||||
<View
|
||||
style={[
|
||||
styles.resultBanner,
|
||||
allSuccess ? styles.resultBannerSuccess : styles.resultBannerError,
|
||||
]}
|
||||
>
|
||||
<Text style={styles.resultBannerText}>
|
||||
{allSuccess
|
||||
? "✓ Todos os dados sincronizados com sucesso"
|
||||
: "⚠ Sincronização concluída com erros"}
|
||||
</Text>
|
||||
</View>
|
||||
)}
|
||||
|
||||
{/* Timeline */}
|
||||
<View style={styles.timeline}>
|
||||
{items.map((item, index) => {
|
||||
const isLast = index === items.length - 1;
|
||||
return (
|
||||
<View key={item.key} style={styles.timelineRow}>
|
||||
{/* Track: linha + círculo */}
|
||||
<View style={styles.track}>
|
||||
<NodeCircle status={item.status} />
|
||||
{!isLast && (
|
||||
<View
|
||||
style={[
|
||||
styles.connector,
|
||||
item.status === "success" && styles.connectorSuccess,
|
||||
item.status === "error" && styles.connectorError,
|
||||
]}
|
||||
/>
|
||||
)}
|
||||
</View>
|
||||
|
||||
{/* Conteúdo */}
|
||||
<View style={styles.itemContent}>
|
||||
<View style={styles.itemHeader}>
|
||||
<Text style={styles.itemLabel}>{item.label}</Text>
|
||||
{item.status === "success" && (
|
||||
<Text style={styles.badgeSuccess}>✓ OK</Text>
|
||||
)}
|
||||
{item.status === "error" && (
|
||||
<Text style={styles.badgeError}>✗ Erro</Text>
|
||||
)}
|
||||
</View>
|
||||
<Text style={styles.itemDescription}>{item.description}</Text>
|
||||
{item.detail !== "" && (
|
||||
<Text
|
||||
style={[
|
||||
styles.itemDetail,
|
||||
item.status === "success" && styles.itemDetailSuccess,
|
||||
item.status === "error" && styles.itemDetailError,
|
||||
]}
|
||||
>
|
||||
{item.detail}
|
||||
</Text>
|
||||
)}
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
})}
|
||||
</View>
|
||||
|
||||
{/* Botões */}
|
||||
<View style={styles.actions}>
|
||||
{done && hasError && (
|
||||
<TouchableOpacity
|
||||
style={styles.retryButton}
|
||||
onPress={reset}
|
||||
activeOpacity={0.8}
|
||||
>
|
||||
<Text style={styles.retryButtonText}>Redefinir</Text>
|
||||
</TouchableOpacity>
|
||||
)}
|
||||
|
||||
<TouchableOpacity
|
||||
style={[
|
||||
styles.syncButton,
|
||||
syncing && styles.syncButtonDisabled,
|
||||
]}
|
||||
onPress={handleSync}
|
||||
disabled={syncing}
|
||||
activeOpacity={0.85}
|
||||
>
|
||||
{syncing ? (
|
||||
<ActivityIndicator color={COLORS.background} size="small" />
|
||||
) : (
|
||||
<Text style={styles.syncButtonText}>
|
||||
{done ? "Sincronizar novamente" : "Iniciar sincronização"}
|
||||
</Text>
|
||||
)}
|
||||
</TouchableOpacity>
|
||||
</View>
|
||||
</ScrollView>
|
||||
</SafeAreaView>
|
||||
);
|
||||
}
|
||||
|
||||
function NodeCircle({ status }: { status: SyncStatus }) {
|
||||
if (status === "syncing") {
|
||||
return (
|
||||
<View style={[styles.circle, styles.circleSyncing]}>
|
||||
<ActivityIndicator color={COLORS.warning} size="small" />
|
||||
</View>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<View
|
||||
style={[
|
||||
styles.circle,
|
||||
status === "success" && styles.circleSuccess,
|
||||
status === "error" && styles.circleError,
|
||||
status === "pending" && styles.circlePending,
|
||||
]}
|
||||
>
|
||||
{status === "success" && <Text style={styles.circleIcon}>✓</Text>}
|
||||
{status === "error" && <Text style={styles.circleIcon}>✗</Text>}
|
||||
</View>
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
},
|
||||
});
|
||||
|
|
@ -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<void> => {
|
|||
};
|
||||
|
||||
// USUÁRIOS
|
||||
export const salvarUsuario = async (usuario: Omit<UsuarioDB, "created_at">) => {
|
||||
export const salvarUsuario = async (
|
||||
usuario: Omit<UsuarioDB, "created_at">,
|
||||
): Promise<string> => {
|
||||
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<EmpresaDB[]> => {
|
||||
try {
|
||||
return (
|
||||
(await db.getAllAsync<EmpresaDB>(
|
||||
`SELECT * FROM empresas ORDER BY nome ASC`,
|
||||
)) ?? []
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("Error fetching empresas:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const salvarEmpresa = async (
|
||||
empresa: Omit<EmpresaDB, "created_at">,
|
||||
): Promise<void> => {
|
||||
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<EmpresaDB, "created_at">,
|
||||
): Promise<void> => {
|
||||
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<void> => {
|
||||
try {
|
||||
await db.runAsync(`DELETE FROM empresas WHERE id = ?`, [id]);
|
||||
} catch (error) {
|
||||
console.error("Error deleting empresa:", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue