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 { router } from "expo-router";
|
||||||
import { obterCorridas } from "@/services/db";
|
import { useEffect } from "react";
|
||||||
import React, { useCallback, useEffect, useState } from "react";
|
|
||||||
import { StyleSheet, Text, TouchableOpacity, View } from "react-native";
|
|
||||||
import { SafeAreaView } from "react-native-safe-area-context";
|
|
||||||
import Historico from "./historico";
|
|
||||||
import Lancamento, { type Corrida } from "./lancamento";
|
|
||||||
|
|
||||||
export default function Corrida() {
|
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(() => {
|
useEffect(() => {
|
||||||
loadHistorico();
|
router.replace("/lancamento");
|
||||||
}, [loadHistorico]);
|
}, []);
|
||||||
|
return null;
|
||||||
const handleLancarCorrida = async (_corrida: Corrida) => {
|
|
||||||
await loadHistorico();
|
|
||||||
setActiveTab("historico");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SafeAreaView style={styles.container}>
|
|
||||||
<View style={styles.tabsContainer}>
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.tab, activeTab === "lancamento" && styles.tabActive]}
|
|
||||||
onPress={() => setActiveTab("lancamento")}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.tabLabel,
|
|
||||||
activeTab === "lancamento" && styles.tabLabelActive,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
📍 Lançar
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
|
|
||||||
<TouchableOpacity
|
|
||||||
style={[styles.tab, activeTab === "historico" && styles.tabActive]}
|
|
||||||
onPress={() => setActiveTab("historico")}
|
|
||||||
>
|
|
||||||
<Text
|
|
||||||
style={[
|
|
||||||
styles.tabLabel,
|
|
||||||
activeTab === "historico" && styles.tabLabelActive,
|
|
||||||
]}
|
|
||||||
>
|
|
||||||
📋 Histórico
|
|
||||||
</Text>
|
|
||||||
</TouchableOpacity>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View style={styles.content}>
|
|
||||||
{activeTab === "lancamento" ? (
|
|
||||||
<Lancamento onLancar={handleLancarCorrida} />
|
|
||||||
) : (
|
|
||||||
<Historico historico={historico} />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</SafeAreaView>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const styles = StyleSheet.create({
|
|
||||||
container: {
|
|
||||||
flex: 1,
|
|
||||||
backgroundColor: "#ffffff",
|
|
||||||
},
|
|
||||||
tabsContainer: {
|
|
||||||
flexDirection: "row",
|
|
||||||
backgroundColor: "#ffffff",
|
|
||||||
borderBottomWidth: 2,
|
|
||||||
borderBottomColor: "#000000",
|
|
||||||
},
|
|
||||||
tab: {
|
|
||||||
flex: 1,
|
|
||||||
paddingVertical: 16,
|
|
||||||
justifyContent: "center",
|
|
||||||
alignItems: "center",
|
|
||||||
borderBottomWidth: 3,
|
|
||||||
borderBottomColor: "transparent",
|
|
||||||
},
|
|
||||||
tabActive: {
|
|
||||||
borderBottomColor: "#000000",
|
|
||||||
},
|
|
||||||
tabLabel: {
|
|
||||||
fontSize: 14,
|
|
||||||
fontWeight: "600",
|
|
||||||
color: "#666666",
|
|
||||||
},
|
|
||||||
tabLabelActive: {
|
|
||||||
color: "#000000",
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
flex: 1,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
|
||||||
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 { BORDER_RADIUS, COLORS, SPACING } from "@/constants/theme";
|
||||||
import { FlatList, ScrollView, StyleSheet, Text, View } from "react-native";
|
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";
|
import { SafeAreaView } from "react-native-safe-area-context";
|
||||||
|
|
||||||
export type Corrida = {
|
type Corrida = {
|
||||||
id: string;
|
id: string;
|
||||||
data: string;
|
data: string;
|
||||||
empresa: string;
|
empresa: string;
|
||||||
km: number;
|
km: number;
|
||||||
custoPorKm: number;
|
custoPorKm: number;
|
||||||
total: number;
|
total: number;
|
||||||
usuario: string;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
interface HistoricoProps {
|
|
||||||
historico: Corrida[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const HistoricoItem = ({ item, index }: { item: Corrida; index: number }) => (
|
const HistoricoItem = ({ item, index }: { item: Corrida; index: number }) => (
|
||||||
<View style={styles.item}>
|
<View style={styles.item}>
|
||||||
<View style={styles.itemHeader}>
|
<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.detailLabel}>Distância</Text>
|
||||||
<Text style={styles.detailValue}>{item.km} km</Text>
|
<Text style={styles.detailValue}>{item.km} km</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.detailBox}>
|
<View style={styles.detailBox}>
|
||||||
<Text style={styles.detailLabel}>Valor/km</Text>
|
<Text style={styles.detailLabel}>Valor/km</Text>
|
||||||
<Text style={styles.detailValue}>R$ {item.custoPorKm.toFixed(2)}</Text>
|
<Text style={styles.detailValue}>R$ {item.custoPorKm.toFixed(2)}</Text>
|
||||||
</View>
|
</View>
|
||||||
|
<View style={[styles.detailBox, styles.detailBoxHighlight]}>
|
||||||
<View style={styles.detailBox}>
|
|
||||||
<Text style={styles.detailLabel}>Total</Text>
|
<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>
|
</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 (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<SafeAreaView style={styles.container}>
|
||||||
<View style={styles.header}>
|
<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}>
|
<Text style={styles.subtitle}>
|
||||||
{historico.length} corrida{historico.length !== 1 ? "s" : ""}
|
{corridas.length} corrida{corridas.length !== 1 ? "s" : ""} registrada{corridas.length !== 1 ? "s" : ""}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</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}>
|
<ScrollView contentContainerStyle={styles.emptyContainer}>
|
||||||
<View style={styles.emptyBox}>
|
<View style={styles.emptyBox}>
|
||||||
<Text style={styles.emptyEmoji}>🚗</Text>
|
<Text style={styles.emptyEmoji}>🚗</Text>
|
||||||
<Text style={styles.emptyTitle}>Nenhuma corrida registrada</Text>
|
<Text style={styles.emptyTitle}>Nenhuma corrida ainda</Text>
|
||||||
<Text style={styles.emptyText}>Suas corridas aparecerão aqui</Text>
|
<Text style={styles.emptyText}>
|
||||||
|
Registre sua primeira corrida para vê-la aqui.
|
||||||
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
) : (
|
) : (
|
||||||
<FlatList
|
<FlatList
|
||||||
data={historico}
|
data={corridas}
|
||||||
keyExtractor={(item) => item.id}
|
keyExtractor={(item) => item.id}
|
||||||
renderItem={({ item, index }) => (
|
renderItem={({ item, index }) => (
|
||||||
<HistoricoItem item={item} index={index} />
|
<HistoricoItem item={item} index={index} />
|
||||||
|
|
@ -82,110 +139,170 @@ export default function Historico({ historico }: HistoricoProps) {
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: "#ffffff",
|
backgroundColor: COLORS.background,
|
||||||
},
|
},
|
||||||
header: {
|
header: {
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: SPACING.lg,
|
||||||
paddingTop: 16,
|
paddingVertical: SPACING.md,
|
||||||
paddingBottom: 24,
|
borderBottomWidth: 1,
|
||||||
backgroundColor: "#ffffff",
|
borderBottomColor: COLORS.border,
|
||||||
borderBottomWidth: 2,
|
},
|
||||||
borderBottomColor: "#000000",
|
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: {
|
title: {
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
fontWeight: "800",
|
fontWeight: "800",
|
||||||
color: "#000000",
|
color: COLORS.text,
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
subtitle: {
|
subtitle: {
|
||||||
fontSize: 14,
|
fontSize: 13,
|
||||||
color: "#333333",
|
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: {
|
listContent: {
|
||||||
padding: 16,
|
padding: SPACING.lg,
|
||||||
paddingBottom: 32,
|
paddingBottom: SPACING.xxl,
|
||||||
},
|
},
|
||||||
item: {
|
item: {
|
||||||
backgroundColor: "#ffffff",
|
backgroundColor: COLORS.surface,
|
||||||
borderRadius: 12,
|
borderRadius: BORDER_RADIUS.lg,
|
||||||
marginBottom: 12,
|
marginBottom: SPACING.md,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: "#000000",
|
borderColor: COLORS.border,
|
||||||
},
|
},
|
||||||
itemHeader: {
|
itemHeader: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: SPACING.lg,
|
||||||
paddingTop: 12,
|
paddingTop: SPACING.md,
|
||||||
paddingBottom: 8,
|
paddingBottom: SPACING.sm,
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: "#e0e0e0",
|
borderBottomColor: COLORS.border,
|
||||||
},
|
},
|
||||||
itemNumber: {
|
itemNumber: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: "700",
|
fontWeight: "700",
|
||||||
color: "#000000",
|
color: COLORS.textTertiary,
|
||||||
|
letterSpacing: 0.5,
|
||||||
},
|
},
|
||||||
itemDate: {
|
itemDate: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: "#666666",
|
color: COLORS.textTertiary,
|
||||||
},
|
},
|
||||||
itemEmpresa: {
|
itemEmpresa: {
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: SPACING.lg,
|
||||||
paddingVertical: 10,
|
paddingVertical: SPACING.sm,
|
||||||
backgroundColor: "#f5f5f5",
|
backgroundColor: COLORS.surfaceLight,
|
||||||
},
|
},
|
||||||
empresaLabel: {
|
empresaLabel: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: "600",
|
fontWeight: "700",
|
||||||
color: "#000000",
|
color: COLORS.text,
|
||||||
},
|
},
|
||||||
itemDetails: {
|
itemDetails: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
paddingHorizontal: 16,
|
paddingHorizontal: SPACING.lg,
|
||||||
paddingVertical: 12,
|
paddingVertical: SPACING.md,
|
||||||
gap: 8,
|
|
||||||
},
|
},
|
||||||
detailBox: {
|
detailBox: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
paddingVertical: 8,
|
},
|
||||||
|
detailBoxHighlight: {
|
||||||
|
alignItems: "flex-end",
|
||||||
},
|
},
|
||||||
detailLabel: {
|
detailLabel: {
|
||||||
fontSize: 11,
|
fontSize: 10,
|
||||||
color: "#666666",
|
color: COLORS.textTertiary,
|
||||||
fontWeight: "500",
|
fontWeight: "600",
|
||||||
marginBottom: 2,
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: 0.4,
|
||||||
|
marginBottom: 3,
|
||||||
},
|
},
|
||||||
detailValue: {
|
detailValue: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
fontWeight: "700",
|
fontWeight: "700",
|
||||||
color: "#000000",
|
color: COLORS.text,
|
||||||
|
},
|
||||||
|
detailValueHighlight: {
|
||||||
|
color: COLORS.success,
|
||||||
|
fontSize: 16,
|
||||||
},
|
},
|
||||||
emptyContainer: {
|
emptyContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
padding: 16,
|
padding: SPACING.lg,
|
||||||
},
|
},
|
||||||
emptyBox: {
|
emptyBox: {
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
emptyEmoji: {
|
emptyEmoji: {
|
||||||
fontSize: 48,
|
fontSize: 48,
|
||||||
marginBottom: 16,
|
marginBottom: SPACING.lg,
|
||||||
},
|
},
|
||||||
emptyTitle: {
|
emptyTitle: {
|
||||||
fontSize: 18,
|
fontSize: 18,
|
||||||
fontWeight: "600",
|
fontWeight: "700",
|
||||||
color: "#000000",
|
color: COLORS.text,
|
||||||
marginBottom: 8,
|
marginBottom: SPACING.sm,
|
||||||
},
|
},
|
||||||
emptyText: {
|
emptyText: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: "#666666",
|
color: COLORS.textTertiary,
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
|
lineHeight: 20,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -101,14 +101,14 @@ export default function Home() {
|
||||||
icon: "🚗",
|
icon: "🚗",
|
||||||
label: "Registrar Corrida",
|
label: "Registrar Corrida",
|
||||||
description: "Lançar nova corrida",
|
description: "Lançar nova corrida",
|
||||||
onPress: () => router.push("/corrida"),
|
onPress: () => router.push("/lancamento"),
|
||||||
highlight: true,
|
highlight: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "📋",
|
icon: "📋",
|
||||||
label: "Histórico",
|
label: "Histórico",
|
||||||
description: "Ver corridas anteriores",
|
description: "Ver corridas anteriores",
|
||||||
onPress: () => router.push("/corrida"),
|
onPress: () => router.push("/historico"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: "📊",
|
icon: "📊",
|
||||||
|
|
@ -116,12 +116,24 @@ export default function Home() {
|
||||||
description: "Análise de ganhos",
|
description: "Análise de ganhos",
|
||||||
onPress: () => Alert.alert("Em breve", "Funcionalidade em desenvolvimento."),
|
onPress: () => Alert.alert("Em breve", "Funcionalidade em desenvolvimento."),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: "👤",
|
||||||
|
label: "Cadastros",
|
||||||
|
description: "Cadastros de dados básicos",
|
||||||
|
onPress: () => router.push("/cadastros"),
|
||||||
|
},
|
||||||
{
|
{
|
||||||
icon: "⚙️",
|
icon: "⚙️",
|
||||||
label: "Configurações",
|
label: "Configurações",
|
||||||
description: "Preferências do app",
|
description: "Preferências do app",
|
||||||
onPress: () => Alert.alert("Em breve", "Funcionalidade em desenvolvimento."),
|
onPress: () => Alert.alert("Em breve", "Funcionalidade em desenvolvimento."),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
icon: "🔄",
|
||||||
|
label: "Sincronizar",
|
||||||
|
description: "Sincronizar dados para portal",
|
||||||
|
onPress: () => router.push("/sincronizar"),
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -61,20 +61,20 @@ export default function IndexPage() {
|
||||||
|
|
||||||
setAuthToken(data.accessToken);
|
setAuthToken(data.accessToken);
|
||||||
|
|
||||||
// Salvar no SQLite
|
const userName = data.name || emailTrimmed.split("@")[0];
|
||||||
const userId = Date.now().toString();
|
|
||||||
await salvarUsuario({
|
// Salvar no SQLite — retorna o ID existente se o usuário já tinha conta
|
||||||
id: userId,
|
const userId = await salvarUsuario({
|
||||||
|
id: Date.now().toString(),
|
||||||
email: emailTrimmed,
|
email: emailTrimmed,
|
||||||
name: data.name || emailTrimmed.split("@")[0],
|
name: userName,
|
||||||
token: data.accessToken,
|
token: data.accessToken,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Fazer login no contexto
|
|
||||||
await login(emailTrimmed, passwordTrimmed, data.accessToken, {
|
await login(emailTrimmed, passwordTrimmed, data.accessToken, {
|
||||||
id: userId,
|
id: userId,
|
||||||
email: emailTrimmed,
|
email: emailTrimmed,
|
||||||
name: data.name || emailTrimmed.split("@")[0],
|
name: userName,
|
||||||
});
|
});
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
const message = err.response?.data?.error ?? err.message ?? "Erro ao fazer login.";
|
const message = err.response?.data?.error ?? err.message ?? "Erro ao fazer login.";
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,11 @@
|
||||||
import { Button } from "@/components/Button";
|
import { Button } from "@/components/Button";
|
||||||
import { Input } from "@/components/Input";
|
import { Input } from "@/components/Input";
|
||||||
import { Select } from "@/components/Select";
|
import { Select } from "@/components/Select";
|
||||||
|
import { BORDER_RADIUS, COLORS, SPACING } from "@/constants/theme";
|
||||||
import { useAuth } from "@/contexts/AuthContext";
|
import { useAuth } from "@/contexts/AuthContext";
|
||||||
import { salvarCorrida } from "@/services/db";
|
import { EmpresaDB, obterEmpresas, salvarCorrida } from "@/services/db";
|
||||||
import React, { useState } from "react";
|
import { router, useFocusEffect } from "expo-router";
|
||||||
|
import React, { useCallback, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Alert,
|
Alert,
|
||||||
KeyboardAvoidingView,
|
KeyboardAvoidingView,
|
||||||
|
|
@ -11,19 +13,11 @@ import {
|
||||||
ScrollView,
|
ScrollView,
|
||||||
StyleSheet,
|
StyleSheet,
|
||||||
Text,
|
Text,
|
||||||
|
TouchableOpacity,
|
||||||
View,
|
View,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { SafeAreaView } from "react-native-safe-area-context";
|
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 = {
|
export type Corrida = {
|
||||||
id: string;
|
id: string;
|
||||||
data: string;
|
data: string;
|
||||||
|
|
@ -34,63 +28,83 @@ export type Corrida = {
|
||||||
usuario: string;
|
usuario: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
interface LancamentoProps {
|
export default function LancamentoPage() {
|
||||||
onLancar: (corrida: Corrida) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Lancamento({ onLancar }: LancamentoProps) {
|
|
||||||
const { user } = useAuth();
|
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 [distancia, setDistancia] = useState("");
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
|
||||||
const distanciaNum = parseFloat(distancia) || 0;
|
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 () => {
|
const handleLancarCorrida = async () => {
|
||||||
if (!empresa) {
|
if (!empresaId) {
|
||||||
Alert.alert("Validação", "Por favor, selecione uma empresa.");
|
Alert.alert("Validação", "Por favor, selecione uma empresa.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!distancia || distanciaNum <= 0) {
|
if (!distancia || distanciaNum <= 0) {
|
||||||
Alert.alert("Validação", "Por favor, insira uma distância válida.");
|
Alert.alert("Validação", "Por favor, insira uma distância válida.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const empresaSelecionada = EMPRESAS.find((e) => e.value === empresa);
|
const empresaSelecionada = empresasDB.find((e) => e.id === empresaId);
|
||||||
const novaCorrida: Corrida = {
|
|
||||||
id: Date.now().toString(),
|
|
||||||
data: new Date().toLocaleString("pt-BR"),
|
|
||||||
empresa: empresaSelecionada?.label || empresa,
|
|
||||||
km: distanciaNum,
|
|
||||||
custoPorKm: CUSTO_POR_KM,
|
|
||||||
total: totalCorrida,
|
|
||||||
usuario: user?.email ?? "",
|
|
||||||
};
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
setLoading(true);
|
||||||
await salvarCorrida({
|
await salvarCorrida({
|
||||||
id: novaCorrida.id,
|
id: Date.now().toString(),
|
||||||
usuario_id: user?.id ?? "",
|
usuario_id: user?.id ?? "",
|
||||||
empresa: novaCorrida.empresa,
|
empresa: empresaSelecionada?.nome ?? empresaId,
|
||||||
km: novaCorrida.km,
|
km: distanciaNum,
|
||||||
custo_por_km: novaCorrida.custoPorKm,
|
custo_por_km: custoPorKm,
|
||||||
total: novaCorrida.total,
|
total: totalCorrida,
|
||||||
data: novaCorrida.data,
|
data: new Date().toLocaleString("pt-BR"),
|
||||||
sincronizado: 0,
|
sincronizado: 0,
|
||||||
});
|
});
|
||||||
|
setEmpresaId("");
|
||||||
|
setCustoPorKm(0);
|
||||||
|
setDistancia("");
|
||||||
|
Alert.alert("Sucesso", "Corrida registrada com sucesso!", [
|
||||||
|
{ text: "OK", onPress: () => router.back() },
|
||||||
|
]);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Erro ao salvar corrida:", 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 (
|
return (
|
||||||
<SafeAreaView style={styles.container}>
|
<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
|
<KeyboardAvoidingView
|
||||||
style={{ flex: 1 }}
|
style={{ flex: 1 }}
|
||||||
behavior={Platform.select({ ios: "padding", android: "height" })}
|
behavior={Platform.select({ ios: "padding", android: "height" })}
|
||||||
|
|
@ -99,18 +113,14 @@ export default function Lancamento({ onLancar }: LancamentoProps) {
|
||||||
contentContainerStyle={styles.scrollContent}
|
contentContainerStyle={styles.scrollContent}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
>
|
>
|
||||||
<View style={styles.header}>
|
<View style={styles.titleSection}>
|
||||||
<Text style={styles.title}>Nova Corrida</Text>
|
<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>
|
||||||
|
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<Text style={styles.cardLabel}>Empresa</Text>
|
<Text style={styles.cardLabel}>Empresa</Text>
|
||||||
<Select
|
<Select value={empresaId} onValueChange={handleEmpresaChange} items={empresasItems} />
|
||||||
value={empresa}
|
|
||||||
onValueChange={setEmpresa}
|
|
||||||
items={EMPRESAS}
|
|
||||||
/>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
|
|
@ -124,24 +134,22 @@ export default function Lancamento({ onLancar }: LancamentoProps) {
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.card}>
|
<View style={styles.card}>
|
||||||
<View style={styles.valorRow}>
|
<View style={styles.infoRow}>
|
||||||
<Text style={styles.valorLabel}>Custo por km</Text>
|
<Text style={styles.infoLabel}>Custo por km</Text>
|
||||||
<Text style={styles.valorAmount}>
|
<Text style={styles.infoValue}>R$ {custoPorKm.toFixed(2)}</Text>
|
||||||
R$ {CUSTO_POR_KM.toFixed(2)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.card}>
|
<View style={[styles.card, styles.totalCard]}>
|
||||||
<View style={styles.totalBox}>
|
|
||||||
<Text style={styles.totalLabel}>Valor Total</Text>
|
<Text style={styles.totalLabel}>Valor Total</Text>
|
||||||
<Text style={styles.totalAmount}>
|
<Text style={styles.totalAmount}>R$ {totalCorrida.toFixed(2)}</Text>
|
||||||
R$ {totalCorrida.toFixed(2)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<Button label="Lançar Corrida" onPress={handleLancarCorrida} />
|
<Button
|
||||||
|
label={loading ? "Salvando..." : "Lançar Corrida"}
|
||||||
|
onPress={handleLancarCorrida}
|
||||||
|
disabled={loading}
|
||||||
|
/>
|
||||||
</ScrollView>
|
</ScrollView>
|
||||||
</KeyboardAvoidingView>
|
</KeyboardAvoidingView>
|
||||||
</SafeAreaView>
|
</SafeAreaView>
|
||||||
|
|
@ -151,72 +159,95 @@ export default function Lancamento({ onLancar }: LancamentoProps) {
|
||||||
const styles = StyleSheet.create({
|
const styles = StyleSheet.create({
|
||||||
container: {
|
container: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: "#ffffff",
|
backgroundColor: COLORS.background,
|
||||||
},
|
|
||||||
scrollContent: {
|
|
||||||
padding: 16,
|
|
||||||
paddingBottom: 32,
|
|
||||||
},
|
},
|
||||||
header: {
|
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: {
|
title: {
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
fontWeight: "800",
|
fontWeight: "800",
|
||||||
color: "#000000",
|
color: COLORS.text,
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
},
|
},
|
||||||
subtitle: {
|
subtitle: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: "#333333",
|
color: COLORS.textTertiary,
|
||||||
},
|
},
|
||||||
card: {
|
card: {
|
||||||
backgroundColor: "#ffffff",
|
backgroundColor: COLORS.surface,
|
||||||
borderRadius: 12,
|
borderRadius: BORDER_RADIUS.lg,
|
||||||
padding: 16,
|
padding: SPACING.lg,
|
||||||
marginBottom: 12,
|
marginBottom: SPACING.md,
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
borderColor: "#000000",
|
borderColor: COLORS.border,
|
||||||
},
|
},
|
||||||
cardLabel: {
|
cardLabel: {
|
||||||
fontSize: 12,
|
fontSize: 11,
|
||||||
fontWeight: "600",
|
fontWeight: "700",
|
||||||
color: "#000000",
|
color: COLORS.textTertiary,
|
||||||
marginBottom: 8,
|
marginBottom: SPACING.sm,
|
||||||
textTransform: "uppercase",
|
textTransform: "uppercase",
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.8,
|
||||||
},
|
},
|
||||||
valorRow: {
|
infoRow: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
justifyContent: "space-between",
|
justifyContent: "space-between",
|
||||||
alignItems: "center",
|
alignItems: "center",
|
||||||
},
|
},
|
||||||
valorLabel: {
|
infoLabel: {
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
color: "#000000",
|
color: COLORS.textSecondary,
|
||||||
fontWeight: "500",
|
fontWeight: "500",
|
||||||
},
|
},
|
||||||
valorAmount: {
|
infoValue: {
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
fontWeight: "700",
|
fontWeight: "700",
|
||||||
color: "#000000",
|
color: COLORS.text,
|
||||||
},
|
},
|
||||||
totalBox: {
|
totalCard: {
|
||||||
backgroundColor: "#ffffff",
|
borderColor: COLORS.borderLight,
|
||||||
borderLeftWidth: 4,
|
borderLeftWidth: 3,
|
||||||
borderLeftColor: "#000000",
|
borderLeftColor: COLORS.success,
|
||||||
padding: 12,
|
marginBottom: SPACING.xl,
|
||||||
borderRadius: 8,
|
|
||||||
},
|
},
|
||||||
totalLabel: {
|
totalLabel: {
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: "#000000",
|
color: COLORS.textTertiary,
|
||||||
marginBottom: 4,
|
fontWeight: "600",
|
||||||
fontWeight: "500",
|
textTransform: "uppercase",
|
||||||
|
letterSpacing: 0.5,
|
||||||
|
marginBottom: SPACING.xs,
|
||||||
},
|
},
|
||||||
totalAmount: {
|
totalAmount: {
|
||||||
fontSize: 24,
|
fontSize: 32,
|
||||||
fontWeight: "800",
|
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 () => {
|
export const initDB = async () => {
|
||||||
try {
|
try {
|
||||||
await db.execAsync(`
|
await db.execAsync(
|
||||||
CREATE TABLE IF NOT EXISTS usuarios (
|
`CREATE TABLE IF NOT EXISTS usuarios (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
email TEXT UNIQUE NOT NULL,
|
email TEXT UNIQUE NOT NULL,
|
||||||
name TEXT NOT NULL,
|
name TEXT NOT NULL,
|
||||||
token TEXT NOT NULL,
|
token TEXT NOT NULL,
|
||||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||||
|
);`,
|
||||||
);
|
);
|
||||||
|
await db.execAsync(
|
||||||
CREATE TABLE IF NOT EXISTS corridas (
|
`CREATE TABLE IF NOT EXISTS corridas (
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
usuario_id TEXT NOT NULL,
|
usuario_id TEXT NOT NULL,
|
||||||
empresa TEXT NOT NULL,
|
empresa TEXT NOT NULL,
|
||||||
|
|
@ -44,13 +45,23 @@ export const initDB = async () => {
|
||||||
sincronizado INTEGER DEFAULT 0,
|
sincronizado INTEGER DEFAULT 0,
|
||||||
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
|
||||||
FOREIGN KEY (usuario_id) REFERENCES usuarios(id)
|
FOREIGN KEY (usuario_id) REFERENCES usuarios(id)
|
||||||
|
);`,
|
||||||
);
|
);
|
||||||
|
await db.execAsync(
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
`CREATE TABLE IF NOT EXISTS settings (
|
||||||
key TEXT PRIMARY KEY,
|
key TEXT PRIMARY KEY,
|
||||||
value TEXT NOT NULL
|
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) {
|
} catch (error) {
|
||||||
console.error("Failed to initialize database:", error);
|
console.error("Failed to initialize database:", error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|
@ -93,14 +104,29 @@ export const deleteSetting = async (key: string): Promise<void> => {
|
||||||
};
|
};
|
||||||
|
|
||||||
// USUÁRIOS
|
// USUÁRIOS
|
||||||
export const salvarUsuario = async (usuario: Omit<UsuarioDB, "created_at">) => {
|
export const salvarUsuario = async (
|
||||||
|
usuario: Omit<UsuarioDB, "created_at">,
|
||||||
|
): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
const result = await db.runAsync(
|
const existing = await db.getFirstAsync<{ id: string }>(
|
||||||
`INSERT OR REPLACE INTO usuarios (id, email, name, token)
|
`SELECT id FROM usuarios WHERE email = ?`,
|
||||||
VALUES (?, ?, ?, ?)`,
|
[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],
|
[usuario.id, usuario.email, usuario.name, usuario.token],
|
||||||
);
|
);
|
||||||
return result;
|
return usuario.id;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error saving user:", error);
|
console.error("Error saving user:", error);
|
||||||
throw error;
|
throw error;
|
||||||
|
|
@ -205,3 +231,62 @@ export const limparBancoDados = async () => {
|
||||||
throw error;
|
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