top-tran/toptran-app/src/app/home.tsx
Rayan Konecny fea50d5064 Refactors data models and sync, adds company/rides fetch APIs
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.
2026-05-03 14:15:37 -03:00

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,
},
});