import React, { useState, useCallback, useMemo } from "react";
import {
View,
Text,
ScrollView,
StyleSheet,
RefreshControl,
TouchableOpacity,
} from "react-native";
import AlertModal from "../../components/ui/AlertModal";
import { Ionicons } from "@expo/vector-icons";
import { useFocusEffect } from "@react-navigation/native";
import { useTheme } from "../../context/ThemeContext";
import { spacing, fontSize, borderRadius } from "../../theme";
import { shadows } from "../../theme/shadows";
import LoadingSpinner from "../../components/ui/LoadingSpinner";
import { getAdminStats, resetAdminStats } from "../../api/api_admin";
import type { AdminStats, WeekdayStat, DayStat, DayRevenueStat, HourStat, ProductStat, ProductQuantityBreakdown, StatSection } from "../../api/api_admin";
// ── Palette graphiques ────────────────────────────────────────────────────────
const CHART_ACCENT = "#6366f1";
const CHART_GREEN = "#10b981";
const CHART_AMBER = "#f59e0b";
const CHART_RED = "#ef4444";
const CHART_BLUE = "#3b82f6";
// ── Utilitaires ───────────────────────────────────────────────────────────────
const maxOf = (arr: number[]) => arr.length ? Math.max(...arr) : 1;
const fmtNum = (n: number) =>
n >= 1000 ? `${(n / 1000).toFixed(1)}k` : String(Math.round(n));
const fmtEuro = (n: number) =>
n >= 1000 ? `${(n / 1000).toFixed(2)}k€` : `${n.toFixed(2)}€`;
// ── Barre horizontale ─────────────────────────────────────────────────────────
function HBar({
label, value, max, color, right,
}: {
label: string; value: number; max: number; color: string; right?: string;
}) {
const pct = max > 0 ? Math.max((value / max) * 100, value > 0 ? 2 : 0) : 0;
const { colors } = useTheme();
return (
{label}
{right ?? fmtNum(value)}
);
}
const hBarStyles = StyleSheet.create({
row: { flexDirection: "row", alignItems: "center", marginBottom: spacing.s, gap: spacing.s },
label: { width: 80, fontSize: fontSize.xs, flexShrink: 0 },
track: { flex: 1, height: 18, borderRadius: 4, overflow: "hidden" },
fill: { height: "100%", borderRadius: 4 },
value: { width: 46, fontSize: fontSize.xs, textAlign: "right" },
});
// ── Barres verticales (sparkline 30 jours — commandes) ───────────────────────
function SparkLine({ data, color }: { data: DayStat[]; color: string }) {
const { colors } = useTheme();
if (!data.length) return null;
const max = maxOf(data.map((d) => d.count));
const BAR_H = 56;
return (
{data.map((d, i) => (
0 ? Math.max((d.count / max) * BAR_H, d.count > 0 ? 3 : 0) : 0,
backgroundColor: color,
borderRadius: 2,
opacity: 0.85,
}}
/>
))}
{data[0]?.label}
{data[data.length - 1]?.label}
);
}
// ── Sparkline revenus 30 jours ────────────────────────────────────────────────
function SparkLineRevenue({ data, color }: { data: DayRevenueStat[]; color: string }) {
const { colors } = useTheme();
if (!data.length) return null;
const max = Math.max(...data.map((d) => d.revenue), 1);
const BAR_H = 56;
return (
{data.map((d, i) => (
0 ? Math.max((d.revenue / max) * BAR_H, d.revenue > 0 ? 3 : 0) : 0,
backgroundColor: color,
borderRadius: 2,
opacity: 0.85,
}}
/>
))}
{data[0]?.label}
{data[data.length - 1]?.label}
);
}
// ── Carte résumé ──────────────────────────────────────────────────────────────
function SummaryCard({
icon, label, value, color,
}: {
icon: keyof typeof Ionicons.glyphMap; label: string; value: string; color: string;
}) {
const { colors } = useTheme();
return (
{value}
{label}
);
}
const sumStyles = StyleSheet.create({
card: { flex: 1, borderRadius: borderRadius.md, padding: spacing.m, borderWidth: 1, alignItems: "center", minWidth: "45%" },
iconWrap:{ width: 36, height: 36, borderRadius: 18, justifyContent: "center", alignItems: "center", marginBottom: spacing.xs },
val: { fontSize: fontSize.lg, fontWeight: "700" },
lbl: { fontSize: fontSize.xs, marginTop: 2, textAlign: "center" },
});
// ── Séparateur de section ─────────────────────────────────────────────────────
function Section({ title, icon, children }: { title: string; icon: keyof typeof Ionicons.glyphMap; children: React.ReactNode }) {
const { colors } = useTheme();
return (
{title}
{children}
);
}
const secStyles = StyleSheet.create({
card: { borderRadius: borderRadius.md, padding: spacing.l, marginBottom: spacing.m, borderWidth: 1 },
header: { flexDirection: "row", alignItems: "center", gap: spacing.s, marginBottom: spacing.m },
title: { fontSize: fontSize.md, fontWeight: "600" },
});
// ── Bouton reset de section ───────────────────────────────────────────────────
function SectionResetBtn({ onPress, loading, resetAt }: { onPress: () => void; loading: boolean; resetAt?: string }) {
const { colors } = useTheme();
return (
{loading ? "..." : "Réinitialiser"}
{resetAt ? (
Depuis le {new Date(resetAt).toLocaleDateString("fr-FR", { day: "2-digit", month: "2-digit", year: "numeric" })}
) : null}
);
}
// ── Sélecteur de période top produits ────────────────────────────────────────
type ProdSort = "quantity" | "orders" | "revenue";
// ═══════════════════════════════════════════════════════════════════════════════
export default function StatsScreen() {
const { colors } = useTheme();
const [stats, setStats] = useState(null);
const [loading, setLoading] = useState(true);
const [refreshing, setRefreshing] = useState(false);
const [resetting, setResetting] = useState(null);
const [prodSort, setProdSort] = useState("quantity");
const [confirmModal, setConfirmModal] = useState<{ section: StatSection; label: string } | null>(null);
const [errorModal, setErrorModal] = useState(null);
const loadStats = useCallback(async () => {
try {
const data = await getAdminStats();
setStats(data);
} catch { /* ignore */ }
setLoading(false);
}, []);
useFocusEffect(useCallback(() => { loadStats(); }, [loadStats]));
const onRefresh = async () => {
setRefreshing(true);
await loadStats();
setRefreshing(false);
};
const handleReset = (section: StatSection, label: string) => {
setConfirmModal({ section, label });
};
const doReset = async () => {
if (!confirmModal) return;
const { section } = confirmModal;
setConfirmModal(null);
setResetting(section);
try {
await resetAdminStats(section);
await loadStats();
} catch (e: any) {
setErrorModal(e?.response?.data?.error || e?.message || "Impossible de réinitialiser");
}
setResetting(null);
};
// Produits triés selon le sélecteur
const sortedProducts = useMemo(() => {
if (!stats?.top_products) return [];
return [...stats.top_products].sort((a, b) => {
if (prodSort === "orders") return b.order_count - a.order_count;
if (prodSort === "revenue") return b.revenue - a.revenue;
return b.quantity - a.quantity;
});
}, [stats, prodSort]);
const styles = useMemo(() => StyleSheet.create({
container: { flex: 1, backgroundColor: colors.bgPrimary },
content: { padding: spacing.l, paddingBottom: spacing.xxxl },
pageTitle: { fontSize: fontSize.xl, fontWeight: "700", color: colors.textPrimary, marginBottom: spacing.xs },
pageSubtitle: { fontSize: fontSize.sm, color: colors.textMuted, marginBottom: spacing.l },
summaryRow: { flexDirection: "row", gap: spacing.m, marginBottom: spacing.m, flexWrap: "wrap" },
sortRow: { flexDirection: "row", gap: spacing.s, marginBottom: spacing.m },
sortBtn: { paddingHorizontal: spacing.m, paddingVertical: spacing.xs, borderRadius: borderRadius.sm, borderWidth: 1 },
sortBtnText:{ fontSize: fontSize.xs, fontWeight: "600" },
emptyText: { color: colors.textMuted, fontSize: fontSize.sm, textAlign: "center", paddingVertical: spacing.l },
headerRow: { flexDirection: "row", alignItems: "center", justifyContent: "space-between", marginBottom: spacing.xs },
resetBtn: { flexDirection: "row", alignItems: "center", gap: 4, paddingHorizontal: spacing.s, paddingVertical: 3, borderRadius: borderRadius.sm, borderWidth: 1, borderColor: CHART_RED + "66", backgroundColor: CHART_RED + "11" },
resetBtnText:{ fontSize: 10, fontWeight: "600", color: CHART_RED },
resetDate: { fontSize: fontSize.xs, color: colors.textMuted, marginBottom: spacing.l },
}), [colors]);
if (loading) return ;
const s = stats?.summary;
const wdMax = maxOf((stats?.by_weekday ?? []).map((w: WeekdayStat) => w.count));
const prodMax = maxOf(sortedProducts.map((p) =>
prodSort === "orders" ? p.order_count : prodSort === "revenue" ? p.revenue : p.quantity,
));
return (
<>
setConfirmModal(null)}
onConfirm={doReset}
/>
setErrorModal(null)}
/>
}
>
Statistiques
Activité globale & produits
{/* ── Cartes résumé ── */}
{/* ── Évolution 30 jours (commandes) ── */}
handleReset("commandes", "Commandes")}
loading={resetting === "commandes"}
resetAt={stats?.reset_at_commandes}
/>
{stats?.by_day_30?.length ? (
) : (
Aucune donnée
)}
{/* ── Revenus par jour (30 jours) ── */}
{(() => {
const revData = stats?.by_day_revenue ?? [];
const revMax = Math.max(...revData.map((d) => d.revenue), 1);
const best = [...revData].sort((a, b) => b.revenue - a.revenue)[0];
return (
handleReset("revenus", "Revenus")}
loading={resetting === "revenus"}
resetAt={stats?.reset_at_revenus}
/>
{revData.length ? (
<>
{[...revData].reverse().map((d) => (
))}
{best && (
Meilleure journée :{" "}
{best.label} · {fmtEuro(best.revenue)}
)}
>
) : (
Aucune donnée de revenu
)}
);
})()}
{/* ── Heures d'affluence ── */}
{(() => {
const hourData: HourStat[] = stats?.by_hour ?? [];
const peakHour = hourData.reduce(
(best, h) => (!best || h.count > best.count ? h : best),
null,
);
const hourMax = maxOf(hourData.map((h) => h.count));
return (
{hourData.some((h) => h.count > 0) ? (
<>
{hourData.map((h) => (
0 ? CHART_AMBER : CHART_BLUE}
right={h.count > 0 ? String(h.count) : "—"}
/>
))}
{peakHour && peakHour.count > 0 && (
Heure de pointe :{" "}
{peakHour.label} · {peakHour.count} cmd · {fmtEuro(peakHour.revenue)}
)}
>
) : (
Aucune donnée horaire
)}
);
})()}
{/* ── Commandes par jour de la semaine ── */}
{(stats?.by_weekday ?? []).map((w: WeekdayStat) => (
0 ? CHART_AMBER : CHART_ACCENT}
right={String(w.count)}
/>
))}
{wdMax > 0 && s?.peak_weekday && (
Pic d'activité : {s.peak_weekday}
)}
{/* ── Doses les plus populaires par produit ── */}
{(() => {
const qtyData: ProductQuantityBreakdown[] = (stats?.by_quantity ?? []).filter(
(p) => p.quantities.length >= 1,
);
if (!qtyData.length) return null;
return (
{qtyData.map((product) => {
const peakCount = product.quantities[0]?.order_count ?? 1;
return (
{/* Nom du produit avec pastille couleur */}
{product.name}
{/* Barre par dose */}
{product.quantities.map((q, i) => {
const pct = peakCount > 0 ? Math.max((q.order_count / peakCount) * 100, q.order_count > 0 ? 2 : 0) : 0;
const isPeak = i === 0;
const label = Number.isInteger(q.quantity)
? `${q.quantity}g`
: `${q.quantity}g`;
return (
{label}
{q.order_count} cmd
{isPeak && (
)}
);
})}
);
})}
);
})()}
{/* ── Top produits ── */}
handleReset("produits", "Produits")}
loading={resetting === "produits"}
resetAt={stats?.reset_at_produits}
/>
{/* Sélecteur tri */}
{(["quantity", "orders", "revenue"] as ProdSort[]).map((key) => {
const labels = { quantity: "Quantité", orders: "Commandes", revenue: "Revenus" };
const active = prodSort === key;
return (
setProdSort(key)}
>
{labels[key]}
);
})}
{sortedProducts.length === 0 ? (
Aucune donnée produit
) : (
sortedProducts.map((p) => {
const val =
prodSort === "orders" ? p.order_count
: prodSort === "revenue" ? p.revenue
: p.quantity;
const rightLabel =
prodSort === "revenue" ? fmtEuro(p.revenue)
: prodSort === "orders" ? `${p.order_count} cmd`
: `×${fmtNum(p.quantity)}`;
return (
);
})
)}
{/* Produit le moins vendu */}
{sortedProducts.length > 1 && prodSort === "quantity" && (
Moins vendu :{" "}
{sortedProducts[sortedProducts.length - 1]?.name}
)}
>
);
}