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.
308 lines
7.8 KiB
TypeScript
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 vê-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,
|
|
},
|
|
});
|