top-tran/toptran-app/src/app/historico.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

308 lines
7.8 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 {
FlatList,
ScrollView,
StyleSheet,
Text,
TouchableOpacity,
View,
} from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
type Corrida = {
id: string;
data: string;
empresa: string;
km: number;
custoPorKm: number;
total: number;
};
const HistoricoItem = ({ item, index }: { item: Corrida; index: number }) => (
<View style={styles.item}>
<View style={styles.itemHeader}>
<Text style={styles.itemNumber}>#{index + 1}</Text>
<Text style={styles.itemDate}>{item.data}</Text>
</View>
<View style={styles.itemEmpresa}>
<Text style={styles.empresaLabel}>{item.empresa}</Text>
</View>
<View style={styles.itemDetails}>
<View style={styles.detailBox}>
<Text style={styles.detailLabel}>Distância</Text>
<Text style={styles.detailValue}>{item.km} km</Text>
</View>
<View style={styles.detailBox}>
<Text style={styles.detailLabel}>Valor/km</Text>
<Text style={styles.detailValue}>R$ {item.custoPorKm.toFixed(2)}</Text>
</View>
<View style={[styles.detailBox, styles.detailBoxHighlight]}>
<Text style={styles.detailLabel}>Total</Text>
<Text style={[styles.detailValue, styles.detailValueHighlight]}>
R$ {item.total.toFixed(2)}
</Text>
</View>
</View>
</View>
);
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.ride_date,
empresa: c.company,
km: c.km,
custoPorKm: c.cost_per_km,
total: c.total,
})),
);
} catch (error) {
console.error("Erro ao carregar histórico:", error);
}
}, [user?.id]);
useFocusEffect(useCallback(() => { loadCorridas(); }, [loadCorridas]));
const totalGanhos = corridas.reduce((acc, c) => acc + c.total, 0);
const totalKm = corridas.reduce((acc, c) => acc + c.km, 0);
return (
<SafeAreaView style={styles.container}>
<View style={styles.header}>
<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}>
{corridas.length} corrida{corridas.length !== 1 ? "s" : ""} registrada{corridas.length !== 1 ? "s" : ""}
</Text>
</View>
{corridas.length > 0 && (
<View style={styles.summaryRow}>
<View style={styles.summaryCard}>
<Text style={styles.summaryValue}>{totalKm.toFixed(0)} km</Text>
<Text style={styles.summaryLabel}>Total Rodado</Text>
</View>
<View style={[styles.summaryCard, styles.summaryCardAccent]}>
<Text style={[styles.summaryValue, styles.summaryValueAccent]}>
R$ {totalGanhos.toFixed(2)}
</Text>
<Text style={styles.summaryLabel}>Total Ganho</Text>
</View>
</View>
)}
{corridas.length === 0 ? (
<ScrollView contentContainerStyle={styles.emptyContainer}>
<View style={styles.emptyBox}>
<Text style={styles.emptyEmoji}>🚗</Text>
<Text style={styles.emptyTitle}>Nenhuma corrida ainda</Text>
<Text style={styles.emptyText}>
Registre sua primeira corrida para -la aqui.
</Text>
</View>
</ScrollView>
) : (
<FlatList
data={corridas}
keyExtractor={(item) => item.id}
renderItem={({ item, index }) => (
<HistoricoItem item={item} index={index} />
)}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
/>
)}
</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",
},
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,
},
summaryRow: {
flexDirection: "row",
gap: SPACING.sm,
paddingHorizontal: SPACING.lg,
paddingBottom: SPACING.lg,
},
summaryCard: {
flex: 1,
backgroundColor: COLORS.surface,
borderRadius: BORDER_RADIUS.lg,
padding: SPACING.md,
borderWidth: 1,
borderColor: COLORS.border,
},
summaryCardAccent: {
borderColor: COLORS.success,
},
summaryValue: {
fontSize: 18,
fontWeight: "800",
color: COLORS.text,
marginBottom: 2,
},
summaryValueAccent: {
color: COLORS.success,
},
summaryLabel: {
fontSize: 11,
color: COLORS.textTertiary,
fontWeight: "600",
textTransform: "uppercase",
letterSpacing: 0.5,
},
listContent: {
padding: SPACING.lg,
paddingBottom: SPACING.xxl,
},
item: {
backgroundColor: COLORS.surface,
borderRadius: BORDER_RADIUS.lg,
marginBottom: SPACING.md,
overflow: "hidden",
borderWidth: 1,
borderColor: COLORS.border,
},
itemHeader: {
flexDirection: "row",
paddingHorizontal: SPACING.lg,
paddingTop: SPACING.md,
paddingBottom: SPACING.sm,
justifyContent: "space-between",
alignItems: "center",
borderBottomWidth: 1,
borderBottomColor: COLORS.border,
},
itemNumber: {
fontSize: 12,
fontWeight: "700",
color: COLORS.textTertiary,
letterSpacing: 0.5,
},
itemDate: {
fontSize: 12,
color: COLORS.textTertiary,
},
itemEmpresa: {
paddingHorizontal: SPACING.lg,
paddingVertical: SPACING.sm,
backgroundColor: COLORS.surfaceLight,
},
empresaLabel: {
fontSize: 14,
fontWeight: "700",
color: COLORS.text,
},
itemDetails: {
flexDirection: "row",
paddingHorizontal: SPACING.lg,
paddingVertical: SPACING.md,
},
detailBox: {
flex: 1,
},
detailBoxHighlight: {
alignItems: "flex-end",
},
detailLabel: {
fontSize: 10,
color: COLORS.textTertiary,
fontWeight: "600",
textTransform: "uppercase",
letterSpacing: 0.4,
marginBottom: 3,
},
detailValue: {
fontSize: 14,
fontWeight: "700",
color: COLORS.text,
},
detailValueHighlight: {
color: COLORS.success,
fontSize: 16,
},
emptyContainer: {
flex: 1,
justifyContent: "center",
alignItems: "center",
padding: SPACING.lg,
},
emptyBox: {
alignItems: "center",
},
emptyEmoji: {
fontSize: 48,
marginBottom: SPACING.lg,
},
emptyTitle: {
fontSize: 18,
fontWeight: "700",
color: COLORS.text,
marginBottom: SPACING.sm,
},
emptyText: {
fontSize: 14,
color: COLORS.textTertiary,
textAlign: "center",
lineHeight: 20,
},
});