Unifies naming conventions for users, companies, and rides across backend and mobile app, standardizing field names for consistency and easier data interchange. Introduces endpoints to fetch all companies and rides for sync. Enhances sync logic to support both upload and download of companies, including first-time population from server. Updates local database schema and access logic to match backend structure, improving maintainability and reliability of sync.
399 lines
9.7 KiB
TypeScript
399 lines
9.7 KiB
TypeScript
import { BORDER_RADIUS, COLORS, SPACING } from "@/constants/theme";
|
|
import { useAuth } from "@/contexts/AuthContext";
|
|
import { obterCorridas } from "@/services/db";
|
|
import { router, useFocusEffect } from "expo-router";
|
|
import React, { useCallback, useState } from "react";
|
|
import {
|
|
Alert,
|
|
ScrollView,
|
|
StyleSheet,
|
|
Text,
|
|
TouchableOpacity,
|
|
View,
|
|
} from "react-native";
|
|
import { SafeAreaView } from "react-native-safe-area-context";
|
|
|
|
type Stats = {
|
|
totalCorridas: number;
|
|
totalKm: number;
|
|
totalGanhos: number;
|
|
};
|
|
|
|
type Action = {
|
|
icon: string;
|
|
label: string;
|
|
description: string;
|
|
onPress: () => void;
|
|
highlight?: boolean;
|
|
};
|
|
|
|
function greeting() {
|
|
const h = new Date().getHours();
|
|
if (h < 12) return "Bom dia";
|
|
if (h < 18) return "Boa tarde";
|
|
return "Boa noite";
|
|
}
|
|
|
|
export default function Home() {
|
|
const { user, logout } = useAuth();
|
|
const userName = user?.name ?? "Usuário";
|
|
const firstName = userName.split(" ")[0];
|
|
|
|
const [stats, setStats] = useState<Stats>({
|
|
totalCorridas: 0,
|
|
totalKm: 0,
|
|
totalGanhos: 0,
|
|
});
|
|
|
|
const loadStats = useCallback(async () => {
|
|
if (!user?.id) return;
|
|
try {
|
|
const corridas = await obterCorridas(user.id);
|
|
setStats({
|
|
totalCorridas: corridas.length,
|
|
totalKm: corridas.reduce((acc, c) => acc + c.km, 0),
|
|
totalGanhos: corridas.reduce((acc, c) => acc + c.total, 0),
|
|
});
|
|
} catch (error) {
|
|
console.error("Erro ao carregar estatísticas:", error);
|
|
}
|
|
}, [user?.id]);
|
|
|
|
useFocusEffect(useCallback(() => { loadStats(); }, [loadStats]));
|
|
|
|
const handleProfilePress = () => {
|
|
Alert.alert(userName, user?.email ?? "", [
|
|
{
|
|
text: "Editar Perfil",
|
|
onPress: () => Alert.alert("Em breve", "Funcionalidade em desenvolvimento."),
|
|
},
|
|
{
|
|
text: "Sair",
|
|
style: "destructive",
|
|
onPress: () =>
|
|
Alert.alert("Sair", "Tem certeza que deseja sair?", [
|
|
{ text: "Cancelar", style: "cancel" },
|
|
{
|
|
text: "Sair",
|
|
style: "destructive",
|
|
onPress: async () => {
|
|
try {
|
|
await logout();
|
|
} catch (e) {
|
|
console.error("Logout error:", e);
|
|
}
|
|
},
|
|
},
|
|
]),
|
|
},
|
|
{ text: "Cancelar", style: "cancel" },
|
|
]);
|
|
};
|
|
|
|
const today = new Date().toLocaleDateString("pt-BR", {
|
|
weekday: "long",
|
|
day: "numeric",
|
|
month: "long",
|
|
});
|
|
|
|
const actions: Action[] = [
|
|
{
|
|
icon: "🚗",
|
|
label: "Registrar Corrida",
|
|
description: "Lançar nova corrida",
|
|
onPress: () => router.push("/lancamento"),
|
|
highlight: true,
|
|
},
|
|
{
|
|
icon: "📋",
|
|
label: "Histórico",
|
|
description: "Ver corridas anteriores",
|
|
onPress: () => router.push("/historico"),
|
|
},
|
|
{
|
|
icon: "📊",
|
|
label: "Relatório",
|
|
description: "Análise de ganhos",
|
|
onPress: () => router.push("/relatorio"),
|
|
},
|
|
{
|
|
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 (
|
|
<SafeAreaView style={styles.container}>
|
|
{/* Header */}
|
|
<View style={styles.header}>
|
|
<View>
|
|
<Text style={styles.headerLogo}>TopTran</Text>
|
|
<Text style={styles.headerSub}>Sistema de Gestão</Text>
|
|
</View>
|
|
<TouchableOpacity style={styles.avatarButton} onPress={handleProfilePress} activeOpacity={0.8}>
|
|
<View style={styles.avatar}>
|
|
<Text style={styles.avatarText}>
|
|
{firstName.charAt(0).toUpperCase()}
|
|
</Text>
|
|
</View>
|
|
<View style={styles.avatarOnline} />
|
|
</TouchableOpacity>
|
|
</View>
|
|
|
|
<ScrollView
|
|
contentContainerStyle={styles.scrollContent}
|
|
showsVerticalScrollIndicator={false}
|
|
>
|
|
{/* Greeting */}
|
|
<View style={styles.greetingSection}>
|
|
<Text style={styles.greetingText}>
|
|
{greeting()}, {firstName}! 👋
|
|
</Text>
|
|
<Text style={styles.dateText}>{today}</Text>
|
|
</View>
|
|
|
|
{/* Stats */}
|
|
<Text style={styles.sectionTitle}>Resumo Geral</Text>
|
|
<View style={styles.statsRow}>
|
|
<View style={styles.statCard}>
|
|
<Text style={styles.statValue}>{stats.totalCorridas}</Text>
|
|
<Text style={styles.statLabel}>Corridas</Text>
|
|
</View>
|
|
<View style={styles.statCard}>
|
|
<Text style={styles.statValue}>
|
|
{stats.totalKm.toFixed(0)}
|
|
</Text>
|
|
<Text style={styles.statLabel}>Km Rodados</Text>
|
|
</View>
|
|
<View style={[styles.statCard, styles.statCardAccent]}>
|
|
<Text style={[styles.statValue, styles.statValueAccent]}>
|
|
R${stats.totalGanhos.toFixed(0)}
|
|
</Text>
|
|
<Text style={[styles.statLabel, styles.statLabelAccent]}>
|
|
Total Ganho
|
|
</Text>
|
|
</View>
|
|
</View>
|
|
|
|
{/* Actions Grid */}
|
|
<Text style={styles.sectionTitle}>Acesso Rápido</Text>
|
|
<View style={styles.actionsGrid}>
|
|
{actions.map((action, index) => (
|
|
<TouchableOpacity
|
|
key={index}
|
|
style={[
|
|
styles.actionCard,
|
|
action.highlight && styles.actionCardHighlight,
|
|
]}
|
|
onPress={action.onPress}
|
|
activeOpacity={0.75}
|
|
>
|
|
<Text style={styles.actionIcon}>{action.icon}</Text>
|
|
<Text
|
|
style={[
|
|
styles.actionLabel,
|
|
action.highlight && styles.actionLabelHighlight,
|
|
]}
|
|
>
|
|
{action.label}
|
|
</Text>
|
|
<Text style={styles.actionDescription}>{action.description}</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
|
|
<Text style={styles.footer}>© 2025 TopTran</Text>
|
|
</ScrollView>
|
|
</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,
|
|
},
|
|
headerLogo: {
|
|
fontSize: 22,
|
|
fontWeight: "900",
|
|
color: COLORS.text,
|
|
letterSpacing: 1,
|
|
},
|
|
headerSub: {
|
|
fontSize: 11,
|
|
color: COLORS.textTertiary,
|
|
letterSpacing: 0.5,
|
|
},
|
|
avatarButton: {
|
|
position: "relative",
|
|
},
|
|
avatar: {
|
|
width: 42,
|
|
height: 42,
|
|
borderRadius: BORDER_RADIUS.round,
|
|
backgroundColor: COLORS.surfaceLight,
|
|
borderWidth: 2,
|
|
borderColor: COLORS.borderLight,
|
|
justifyContent: "center",
|
|
alignItems: "center",
|
|
},
|
|
avatarText: {
|
|
color: COLORS.text,
|
|
fontSize: 18,
|
|
fontWeight: "700",
|
|
},
|
|
avatarOnline: {
|
|
position: "absolute",
|
|
bottom: 1,
|
|
right: 1,
|
|
width: 10,
|
|
height: 10,
|
|
borderRadius: 5,
|
|
backgroundColor: COLORS.success,
|
|
borderWidth: 2,
|
|
borderColor: COLORS.background,
|
|
},
|
|
|
|
// Scroll
|
|
scrollContent: {
|
|
padding: SPACING.lg,
|
|
paddingBottom: SPACING.xxl,
|
|
},
|
|
|
|
// Greeting
|
|
greetingSection: {
|
|
marginTop: SPACING.md,
|
|
marginBottom: SPACING.xl,
|
|
},
|
|
greetingText: {
|
|
fontSize: 26,
|
|
fontWeight: "800",
|
|
color: COLORS.text,
|
|
marginBottom: 4,
|
|
},
|
|
dateText: {
|
|
fontSize: 13,
|
|
color: COLORS.textTertiary,
|
|
textTransform: "capitalize",
|
|
},
|
|
|
|
// Section title
|
|
sectionTitle: {
|
|
fontSize: 12,
|
|
fontWeight: "700",
|
|
color: COLORS.textTertiary,
|
|
textTransform: "uppercase",
|
|
letterSpacing: 1.2,
|
|
marginBottom: SPACING.md,
|
|
},
|
|
|
|
// Stats
|
|
statsRow: {
|
|
flexDirection: "row",
|
|
gap: SPACING.sm,
|
|
marginBottom: SPACING.xl,
|
|
},
|
|
statCard: {
|
|
flex: 1,
|
|
backgroundColor: COLORS.surface,
|
|
borderRadius: BORDER_RADIUS.lg,
|
|
padding: SPACING.md,
|
|
borderWidth: 1,
|
|
borderColor: COLORS.border,
|
|
},
|
|
statCardAccent: {
|
|
backgroundColor: COLORS.surfaceLight,
|
|
borderColor: COLORS.borderLight,
|
|
},
|
|
statValue: {
|
|
fontSize: 20,
|
|
fontWeight: "800",
|
|
color: COLORS.text,
|
|
marginBottom: 2,
|
|
},
|
|
statValueAccent: {
|
|
color: COLORS.success,
|
|
},
|
|
statLabel: {
|
|
fontSize: 10,
|
|
color: COLORS.textTertiary,
|
|
fontWeight: "600",
|
|
textTransform: "uppercase",
|
|
letterSpacing: 0.5,
|
|
},
|
|
statLabelAccent: {
|
|
color: COLORS.textSecondary,
|
|
},
|
|
|
|
// Actions grid
|
|
actionsGrid: {
|
|
flexDirection: "row",
|
|
flexWrap: "wrap",
|
|
gap: SPACING.sm,
|
|
marginBottom: SPACING.xl,
|
|
},
|
|
actionCard: {
|
|
width: "48%",
|
|
backgroundColor: COLORS.surface,
|
|
borderRadius: BORDER_RADIUS.lg,
|
|
padding: SPACING.lg,
|
|
borderWidth: 1,
|
|
borderColor: COLORS.border,
|
|
minHeight: 110,
|
|
justifyContent: "space-between",
|
|
},
|
|
actionCardHighlight: {
|
|
backgroundColor: COLORS.surfaceLight,
|
|
borderColor: COLORS.text,
|
|
},
|
|
actionIcon: {
|
|
fontSize: 28,
|
|
marginBottom: SPACING.sm,
|
|
},
|
|
actionLabel: {
|
|
fontSize: 14,
|
|
fontWeight: "700",
|
|
color: COLORS.text,
|
|
marginBottom: 2,
|
|
},
|
|
actionLabelHighlight: {
|
|
color: COLORS.text,
|
|
},
|
|
actionDescription: {
|
|
fontSize: 11,
|
|
color: COLORS.textTertiary,
|
|
},
|
|
|
|
// Footer
|
|
footer: {
|
|
textAlign: "center",
|
|
fontSize: 12,
|
|
color: COLORS.textTertiary,
|
|
marginTop: SPACING.sm,
|
|
},
|
|
});
|