273 lines
11 KiB
TypeScript
273 lines
11 KiB
TypeScript
import React, { useState, useEffect, useCallback, useMemo } from "react";
|
|
import {
|
|
View,
|
|
Text,
|
|
StyleSheet,
|
|
ScrollView,
|
|
RefreshControl,
|
|
TouchableOpacity,
|
|
} from "react-native";
|
|
import { Ionicons } from "@expo/vector-icons";
|
|
import { spacing, fontSize } from "../../theme";
|
|
import { useTheme } from "../../context/ThemeContext";
|
|
import { getMyDeliveries, getMyStats } from "../../api/api_delivery";
|
|
import type { DeliveryItem, } from "../../api/types";
|
|
import type { StatPoint } from "../../api/api_delivery";
|
|
import LoadingSpinner from "../../components/ui/LoadingSpinner";
|
|
import Card from "../../components/ui/Card";
|
|
|
|
type Period = "day" | "week" | "month";
|
|
|
|
const BAR_MAX_HEIGHT = 110;
|
|
const BAR_WIDTH = 36;
|
|
const BAR_GAP = 8;
|
|
|
|
function BarChart({ data, colors }: { data: StatPoint[]; colors: any }) {
|
|
const maxVal = Math.max(...data.map((d) => d.count), 1);
|
|
|
|
if (data.length === 0) {
|
|
return (
|
|
<View style={{ alignItems: "center", paddingVertical: spacing.xl }}>
|
|
<Ionicons name="bar-chart-outline" size={36} color={colors.textMuted} />
|
|
<Text style={{ color: colors.textMuted, fontSize: fontSize.sm, marginTop: spacing.s }}>
|
|
Aucune donnée sur cette période
|
|
</Text>
|
|
</View>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<ScrollView horizontal showsHorizontalScrollIndicator={false} style={{ marginTop: spacing.m }}>
|
|
<View style={{ flexDirection: "row", alignItems: "flex-end", paddingBottom: spacing.s, paddingHorizontal: 4 }}>
|
|
{data.map((point, i) => {
|
|
const val = point.count;
|
|
const barH = Math.max(4, (val / maxVal) * BAR_MAX_HEIGHT);
|
|
const isLast = i === data.length - 1;
|
|
return (
|
|
<View
|
|
key={i}
|
|
style={{
|
|
alignItems: "center",
|
|
marginRight: isLast ? 0 : BAR_GAP,
|
|
width: BAR_WIDTH,
|
|
}}
|
|
>
|
|
<Text style={{ color: colors.textMuted, fontSize: 9, marginBottom: 3 }}>
|
|
{val > 0 ? String(val) : ""}
|
|
</Text>
|
|
<View
|
|
style={{
|
|
width: BAR_WIDTH - 6,
|
|
height: barH,
|
|
backgroundColor: val > 0 ? colors.accent : colors.border,
|
|
borderRadius: 5,
|
|
opacity: val > 0 ? 1 : 0.3,
|
|
}}
|
|
/>
|
|
<Text
|
|
style={{
|
|
color: colors.textMuted,
|
|
fontSize: 9,
|
|
marginTop: 4,
|
|
textAlign: "center",
|
|
}}
|
|
numberOfLines={1}
|
|
>
|
|
{point.label}
|
|
</Text>
|
|
</View>
|
|
);
|
|
})}
|
|
</View>
|
|
</ScrollView>
|
|
);
|
|
}
|
|
|
|
export default function StatsScreen() {
|
|
const { colors } = useTheme();
|
|
const [deliveries, setDeliveries] = useState<DeliveryItem[]>([]);
|
|
const [byDay, setByDay] = useState<StatPoint[]>([]);
|
|
const [byWeek, setByWeek] = useState<StatPoint[]>([]);
|
|
const [byMonth, setByMonth] = useState<StatPoint[]>([]);
|
|
const [todayCount, setTodayCount] = useState(0);
|
|
const [loading, setLoading] = useState(true);
|
|
const [refreshing, setRefreshing] = useState(false);
|
|
const [period, setPeriod] = useState<Period>("week");
|
|
|
|
const loadData = useCallback(async () => {
|
|
try {
|
|
const [delivRes, statsRes] = await Promise.all([
|
|
getMyDeliveries(),
|
|
getMyStats(),
|
|
]);
|
|
if (delivRes.success && delivRes.deliveries) setDeliveries(delivRes.deliveries);
|
|
if (statsRes.success) {
|
|
setByDay(statsRes.by_day ?? []);
|
|
setByWeek(statsRes.by_week ?? []);
|
|
setByMonth(statsRes.by_month ?? []);
|
|
setTodayCount(statsRes.today_count ?? 0);
|
|
}
|
|
} catch {
|
|
/* ignore */
|
|
}
|
|
setLoading(false);
|
|
}, []);
|
|
|
|
useEffect(() => { loadData(); }, [loadData]);
|
|
|
|
const onRefresh = async () => {
|
|
setRefreshing(true);
|
|
await loadData();
|
|
setRefreshing(false);
|
|
};
|
|
|
|
const total = deliveries.length;
|
|
const completed = deliveries.filter((d) => d.status === "livre" || d.status === "approved").length;
|
|
const inProgress = deliveries.filter((d) => d.status === "en_route").length;
|
|
const pending = deliveries.filter((d) => d.status === "assigned").length;
|
|
|
|
const chartData = period === "day" ? byDay : period === "week" ? byWeek : byMonth;
|
|
|
|
const periodTotal = useMemo(
|
|
() => chartData.reduce((acc, p) => acc + p.count, 0),
|
|
[chartData],
|
|
);
|
|
|
|
const periodLabels: Record<Period, string> = {
|
|
day: "30 derniers jours",
|
|
week: "12 dernières semaines",
|
|
month: "12 derniers mois",
|
|
};
|
|
|
|
const styles = useMemo(
|
|
() =>
|
|
StyleSheet.create({
|
|
container: { flex: 1, backgroundColor: colors.bgPrimary },
|
|
title: {
|
|
color: colors.textWhite,
|
|
fontSize: fontSize.xl,
|
|
fontWeight: "700",
|
|
marginBottom: spacing.l,
|
|
},
|
|
grid: { flexDirection: "row", flexWrap: "wrap", gap: spacing.m },
|
|
statCard: { width: "47%", alignItems: "center", paddingVertical: spacing.l },
|
|
statValue: { fontSize: fontSize.xxl, fontWeight: "700", marginTop: spacing.s },
|
|
statLabel: {
|
|
color: colors.textMuted,
|
|
fontSize: fontSize.xs,
|
|
marginTop: spacing.xs,
|
|
textAlign: "center",
|
|
},
|
|
sectionTitle: {
|
|
color: colors.textWhite,
|
|
fontSize: fontSize.lg,
|
|
fontWeight: "700",
|
|
marginBottom: spacing.m,
|
|
marginTop: spacing.xl,
|
|
},
|
|
periodRow: {
|
|
flexDirection: "row",
|
|
gap: spacing.s,
|
|
marginBottom: spacing.m,
|
|
},
|
|
periodBtn: {
|
|
flex: 1,
|
|
paddingVertical: spacing.s,
|
|
borderRadius: 8,
|
|
alignItems: "center",
|
|
backgroundColor: colors.bgCard,
|
|
borderWidth: 1,
|
|
borderColor: colors.border,
|
|
},
|
|
periodBtnActive: {
|
|
backgroundColor: colors.accent + "22",
|
|
borderColor: colors.accent,
|
|
},
|
|
periodBtnText: {
|
|
fontSize: fontSize.sm,
|
|
fontWeight: "600",
|
|
color: colors.textMuted,
|
|
},
|
|
periodBtnTextActive: { color: colors.accent },
|
|
chartCard: { paddingBottom: spacing.s },
|
|
summaryRow: {
|
|
flexDirection: "row",
|
|
justifyContent: "space-between",
|
|
marginTop: spacing.s,
|
|
paddingTop: spacing.s,
|
|
borderTopWidth: 1,
|
|
borderTopColor: colors.border,
|
|
},
|
|
summaryItem: { alignItems: "center", flex: 1 },
|
|
summaryValue: { color: colors.textWhite, fontSize: fontSize.lg, fontWeight: "700" },
|
|
summaryLabel: { color: colors.textMuted, fontSize: fontSize.xs, marginTop: 2 },
|
|
periodHint: { color: colors.textMuted, fontSize: fontSize.xs, marginBottom: spacing.s },
|
|
}),
|
|
[colors],
|
|
);
|
|
|
|
const summaryCards = [
|
|
{ label: "Livraisons du jour", value: todayCount.toString(), icon: "today-outline" as const, color: colors.accent },
|
|
{ label: "Total livraisons", value: total.toString(), icon: "cube-outline" as const, color: colors.accent },
|
|
{ label: "Complétées", value: completed.toString(), icon: "checkmark-circle-outline" as const, color: colors.success },
|
|
{ label: "En cours", value: inProgress.toString(), icon: "time-outline" as const, color: colors.warning },
|
|
{ label: "En attente", value: pending.toString(), icon: "hourglass-outline" as const, color: colors.info },
|
|
];
|
|
|
|
if (loading) return <LoadingSpinner message="Chargement stats..." />;
|
|
|
|
return (
|
|
<ScrollView
|
|
style={styles.container}
|
|
contentContainerStyle={{ padding: spacing.l }}
|
|
refreshControl={<RefreshControl refreshing={refreshing} onRefresh={onRefresh} tintColor={colors.success} />}
|
|
>
|
|
<Text style={styles.title}>Mes performances</Text>
|
|
|
|
{/* Cartes résumé */}
|
|
<View style={styles.grid}>
|
|
{summaryCards.map((s, i) => (
|
|
<Card key={i} style={styles.statCard}>
|
|
<Ionicons name={s.icon} size={28} color={s.color} />
|
|
<Text style={[styles.statValue, { color: s.color }]}>{s.value}</Text>
|
|
<Text style={styles.statLabel}>{s.label}</Text>
|
|
</Card>
|
|
))}
|
|
</View>
|
|
|
|
{/* Section graphiques */}
|
|
<Text style={styles.sectionTitle}>Évolution</Text>
|
|
|
|
{/* Sélecteur période */}
|
|
<View style={styles.periodRow}>
|
|
{(["day", "week", "month"] as Period[]).map((p) => (
|
|
<TouchableOpacity
|
|
key={p}
|
|
style={[styles.periodBtn, period === p && styles.periodBtnActive]}
|
|
onPress={() => setPeriod(p)}
|
|
>
|
|
<Text style={[styles.periodBtnText, period === p && styles.periodBtnTextActive]}>
|
|
{p === "day" ? "Jour" : p === "week" ? "Semaine" : "Mois"}
|
|
</Text>
|
|
</TouchableOpacity>
|
|
))}
|
|
</View>
|
|
|
|
<Card style={styles.chartCard}>
|
|
<Text style={styles.periodHint}>{periodLabels[period]}</Text>
|
|
|
|
<BarChart data={chartData} colors={colors} />
|
|
|
|
{chartData.length > 0 && (
|
|
<View style={styles.summaryRow}>
|
|
<View style={styles.summaryItem}>
|
|
<Text style={styles.summaryValue}>{periodTotal}</Text>
|
|
<Text style={styles.summaryLabel}>livraisons</Text>
|
|
</View>
|
|
</View>
|
|
)}
|
|
</Card>
|
|
</ScrollView>
|
|
);
|
|
}
|